@wafflebase/cli 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/dist/bin.js +6 -12
- package/dist/client/http-client.d.ts +4 -1
- package/dist/client/http-client.js +12 -2
- package/dist/commands/api-keys.d.ts +2 -0
- package/dist/commands/{api-key.js → api-keys.js} +5 -2
- package/dist/commands/cells.d.ts +2 -0
- package/dist/commands/{cell.js → cells.js} +5 -2
- package/dist/commands/docs.d.ts +3 -0
- package/dist/commands/docs.js +306 -0
- package/dist/commands/sheets-export.d.ts +2 -0
- package/dist/commands/{export.js → sheets-export.js} +2 -2
- package/dist/commands/sheets-import.d.ts +2 -0
- package/dist/commands/{import.js → sheets-import.js} +2 -2
- package/dist/commands/sheets.d.ts +2 -0
- package/dist/commands/sheets.js +17 -0
- package/dist/commands/tabs.d.ts +2 -0
- package/dist/commands/{tab.js → tabs.js} +2 -2
- package/dist/docs/content.d.ts +37 -0
- package/dist/docs/content.js +90 -0
- package/dist/docs/docx-export.d.ts +23 -0
- package/dist/docs/docx-export.js +14 -0
- package/dist/docs/docx-import.d.ts +36 -0
- package/dist/docs/docx-import.js +70 -0
- package/dist/docs/dom-polyfill.d.ts +1 -0
- package/dist/docs/dom-polyfill.js +29 -0
- package/dist/docs/fontkit-measurer.d.ts +41 -0
- package/dist/docs/fontkit-measurer.js +64 -0
- package/dist/docs/import.d.ts +72 -0
- package/dist/docs/import.js +146 -0
- package/dist/docs/page-range.d.ts +29 -0
- package/dist/docs/page-range.js +74 -0
- package/dist/docs/page-slice.d.ts +45 -0
- package/dist/docs/page-slice.js +61 -0
- package/dist/docs/paginate.d.ts +13 -0
- package/dist/docs/paginate.js +19 -0
- package/dist/docs/pdf-export.d.ts +38 -0
- package/dist/docs/pdf-export.js +91 -0
- package/dist/output/binary.d.ts +37 -0
- package/dist/output/binary.js +47 -0
- package/dist/output/formatter.js +15 -1
- package/dist/schema/registry.d.ts +27 -0
- package/dist/schema/registry.js +103 -23
- package/package.json +7 -2
- package/dist/commands/api-key.d.ts +0 -2
- package/dist/commands/cell.d.ts +0 -2
- package/dist/commands/document.d.ts +0 -2
- package/dist/commands/document.js +0 -93
- package/dist/commands/export.d.ts +0 -2
- package/dist/commands/import.d.ts +0 -2
- package/dist/commands/tab.d.ts +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @wafflebase/cli
|
|
2
|
+
|
|
3
|
+
`wafflebase` — terminal access to the Wafflebase REST API for data
|
|
4
|
+
pipelines, scripting, CSV/JSON import/export, document management, and
|
|
5
|
+
word-processor (`docs content / export / import`) operations.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Local install (recommended for scripts)
|
|
11
|
+
npm install -D @wafflebase/cli
|
|
12
|
+
npx wafflebase --help
|
|
13
|
+
|
|
14
|
+
# Global install (recommended for interactive use)
|
|
15
|
+
npm install -g @wafflebase/cli
|
|
16
|
+
wafflebase --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Auth
|
|
20
|
+
|
|
21
|
+
Two paths, in priority order:
|
|
22
|
+
|
|
23
|
+
1. **API key** (recommended for scripts) — pass `--api-key` /
|
|
24
|
+
`WAFFLEBASE_API_KEY`, or set `api-key:` in `~/.wafflebase/config.yaml`.
|
|
25
|
+
2. **OAuth session** — `wafflebase login` opens a browser, completes
|
|
26
|
+
GitHub OAuth, and writes a JWT session to `~/.wafflebase/session.json`.
|
|
27
|
+
|
|
28
|
+
Always pair API keys with a workspace ID:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export WAFFLEBASE_API_KEY=wfb_…
|
|
32
|
+
export WAFFLEBASE_WORKSPACE=ws-…
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Command Tree (v0.3.7)
|
|
36
|
+
|
|
37
|
+
Plural namespaces are canonical; singular forms are accepted as
|
|
38
|
+
aliases for back-compat with earlier scripts.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
wafflebase
|
|
42
|
+
├── login / logout / status Browser OAuth + session
|
|
43
|
+
├── ctx list / switch <name|id> Workspace context
|
|
44
|
+
│
|
|
45
|
+
├── docs (aliases: doc, document, documents)
|
|
46
|
+
│ ├── list [--type doc|sheet]
|
|
47
|
+
│ ├── create <title> [--type doc|sheet]
|
|
48
|
+
│ ├── get <doc-id>
|
|
49
|
+
│ ├── rename <doc-id> <title>
|
|
50
|
+
│ ├── delete <doc-id>
|
|
51
|
+
│ ├── content <doc-id> --format json|md|text + --pages
|
|
52
|
+
│ ├── export <doc-id> <file> --format pdf|docx + --pages (PDF)
|
|
53
|
+
│ └── import <file> --replace <id> --yes for in-place
|
|
54
|
+
│
|
|
55
|
+
├── sheets (aliases: sheet, spreadsheet, spreadsheets)
|
|
56
|
+
│ ├── tabs (alias: tab) list <doc-id>
|
|
57
|
+
│ ├── cells (alias: cell)
|
|
58
|
+
│ │ ├── get <doc-id> [<range>]
|
|
59
|
+
│ │ ├── set <doc-id> <ref> <value> --formula
|
|
60
|
+
│ │ ├── batch <doc-id> --data <json> | stdin
|
|
61
|
+
│ │ └── delete <doc-id> <ref>
|
|
62
|
+
│ ├── import <doc-id> <file> CSV/JSON
|
|
63
|
+
│ └── export <doc-id> <file> CSV/JSON
|
|
64
|
+
│
|
|
65
|
+
├── api-keys (alias: api-key)
|
|
66
|
+
│ ├── create <name>
|
|
67
|
+
│ ├── list
|
|
68
|
+
│ └── revoke <key-id>
|
|
69
|
+
│
|
|
70
|
+
└── schema [<command>] Discover parameters/safety
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Global flags**: `--server`, `--api-key`, `--workspace`, `--profile`,
|
|
74
|
+
`--format json|table|csv` (default `json`), `--quiet`, `--verbose`,
|
|
75
|
+
`--dry-run`. The `--format` flag also doubles as the per-content shape
|
|
76
|
+
on `docs content` (`json|md|text`) and the export type override on
|
|
77
|
+
`docs export` (`pdf|docx`).
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Documents
|
|
83
|
+
wafflebase docs list
|
|
84
|
+
wafflebase docs create "Q1 Notes" --type doc
|
|
85
|
+
|
|
86
|
+
# Spreadsheets
|
|
87
|
+
wafflebase sheets cells get abc-123 A1:D100
|
|
88
|
+
echo '{"A1":"Name","B1":"Score"}' | wafflebase sheets cells batch abc-123
|
|
89
|
+
wafflebase sheets export abc-123 out.csv
|
|
90
|
+
|
|
91
|
+
# Word-processor docs
|
|
92
|
+
wafflebase docs content abc-123 --format md
|
|
93
|
+
wafflebase docs export abc-123 out.pdf --pages 1-3
|
|
94
|
+
wafflebase docs import draft.docx --title "Final Draft"
|
|
95
|
+
wafflebase docs import revision.docx --replace abc-123 --yes
|
|
96
|
+
|
|
97
|
+
# Schema introspection (singular aliases resolve too)
|
|
98
|
+
wafflebase schema docs.content
|
|
99
|
+
wafflebase schema cell.get # → sheets.cells.get
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Output Conventions
|
|
103
|
+
|
|
104
|
+
- **Text results** (json/md/text): stdout by default; `--out <file>` to
|
|
105
|
+
redirect; `-` writes to stdout explicitly. `--force` is required to
|
|
106
|
+
overwrite an existing `--out` target.
|
|
107
|
+
- **Binary results** (pdf/docx): positional `<file>`; `-` writes to stdout.
|
|
108
|
+
`--force` is required to overwrite an existing target.
|
|
109
|
+
- **Errors**: a single JSON line on stderr —
|
|
110
|
+
`{"error":{"code":"…","message":"…"}}`. Typed errors (e.g.,
|
|
111
|
+
`INVALID_DOCX`, `TYPE_MISMATCH`, `CONFIRMATION_REQ`) carry a
|
|
112
|
+
command-specific `code` agents can branch on; everything else
|
|
113
|
+
reports `"ERROR"`.
|
|
114
|
+
- **Exit codes**: `0` success, `1` user error (bad input, 404, type
|
|
115
|
+
mismatch), `2` system error (network, auth).
|
|
116
|
+
|
|
117
|
+
## Skills (for AI agents)
|
|
118
|
+
|
|
119
|
+
Skill files live in `skills/` and ship with the package — namespace
|
|
120
|
+
prefixed (`docs-…`, `sheets-…`, `recipe-…`). Agents load them, read
|
|
121
|
+
the YAML frontmatter for safety + tool list, and `wafflebase schema
|
|
122
|
+
<command>` for parameter shapes. See [`skills/SKILL.md`](skills/SKILL.md)
|
|
123
|
+
for the index.
|
|
124
|
+
|
|
125
|
+
## Design
|
|
126
|
+
|
|
127
|
+
Full design in [`/docs/design/rest-api-and-cli.md`](../../docs/design/rest-api-and-cli.md)
|
|
128
|
+
and [`/docs/design/docs-cli.md`](../../docs/design/docs-cli.md) (the
|
|
129
|
+
docs-side `content / export / import` pipeline).
|
package/dist/bin.js
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProgram } from './commands/root.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { registerApiKeyCommand } from './commands/api-key.js';
|
|
3
|
+
import { registerDocsCommand } from './commands/docs.js';
|
|
4
|
+
import { registerSheetsCommand } from './commands/sheets.js';
|
|
5
|
+
import { registerApiKeysCommand } from './commands/api-keys.js';
|
|
7
6
|
import { registerSchemaCommand } from './commands/schema.js';
|
|
8
7
|
import { registerLoginCommand } from './commands/login.js';
|
|
9
8
|
import { registerLogoutCommand } from './commands/logout.js';
|
|
10
9
|
import { registerStatusCommand } from './commands/status.js';
|
|
11
10
|
import { registerCtxCommand } from './commands/ctx.js';
|
|
12
|
-
import { registerImportCommand } from './commands/import.js';
|
|
13
|
-
import { registerExportCommand } from './commands/export.js';
|
|
14
11
|
const program = createProgram();
|
|
15
12
|
registerLoginCommand(program);
|
|
16
13
|
registerLogoutCommand(program);
|
|
17
14
|
registerStatusCommand(program);
|
|
18
15
|
registerCtxCommand(program);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
registerApiKeyCommand(program);
|
|
23
|
-
registerImportCommand(program);
|
|
24
|
-
registerExportCommand(program);
|
|
16
|
+
registerDocsCommand(program);
|
|
17
|
+
registerSheetsCommand(program);
|
|
18
|
+
registerApiKeysCommand(program);
|
|
25
19
|
registerSchemaCommand(program);
|
|
26
20
|
program.parse();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Document } from '@wafflebase/docs';
|
|
1
2
|
import type { CliConfig } from '../config/config.js';
|
|
2
3
|
export interface ApiResponse<T = unknown> {
|
|
3
4
|
ok: boolean;
|
|
@@ -22,10 +23,12 @@ export declare class HttpClient {
|
|
|
22
23
|
private refreshSession;
|
|
23
24
|
request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
24
25
|
listDocuments(): Promise<ApiResponse<unknown[]>>;
|
|
25
|
-
createDocument(title: string): Promise<ApiResponse<unknown>>;
|
|
26
|
+
createDocument(title: string, type?: 'doc' | 'sheet'): Promise<ApiResponse<unknown>>;
|
|
26
27
|
getDocument(id: string): Promise<ApiResponse<unknown>>;
|
|
27
28
|
updateDocument(id: string, title: string): Promise<ApiResponse<unknown>>;
|
|
28
29
|
deleteDocument(id: string): Promise<ApiResponse<unknown>>;
|
|
30
|
+
getDocContent(docId: string): Promise<ApiResponse<Document>>;
|
|
31
|
+
putDocContent(docId: string, doc: Document): Promise<ApiResponse<Document>>;
|
|
29
32
|
listTabs(docId: string): Promise<ApiResponse<unknown[]>>;
|
|
30
33
|
getCells(docId: string, tabId: string, range?: string): Promise<ApiResponse<unknown>>;
|
|
31
34
|
getCell(docId: string, tabId: string, sref: string): Promise<ApiResponse<unknown>>;
|
|
@@ -94,8 +94,11 @@ export class HttpClient {
|
|
|
94
94
|
listDocuments() {
|
|
95
95
|
return this.request('GET', '/documents');
|
|
96
96
|
}
|
|
97
|
-
createDocument(title) {
|
|
98
|
-
|
|
97
|
+
createDocument(title, type) {
|
|
98
|
+
const body = { title };
|
|
99
|
+
if (type)
|
|
100
|
+
body.type = type;
|
|
101
|
+
return this.request('POST', '/documents', body);
|
|
99
102
|
}
|
|
100
103
|
getDocument(id) {
|
|
101
104
|
return this.request('GET', `/documents/${id}`);
|
|
@@ -106,6 +109,13 @@ export class HttpClient {
|
|
|
106
109
|
deleteDocument(id) {
|
|
107
110
|
return this.request('DELETE', `/documents/${id}`);
|
|
108
111
|
}
|
|
112
|
+
// Docs (word-processor) content
|
|
113
|
+
getDocContent(docId) {
|
|
114
|
+
return this.request('GET', `/documents/${docId}/content`);
|
|
115
|
+
}
|
|
116
|
+
putDocContent(docId, doc) {
|
|
117
|
+
return this.request('PUT', `/documents/${docId}/content`, doc);
|
|
118
|
+
}
|
|
109
119
|
// Tabs
|
|
110
120
|
listTabs(docId) {
|
|
111
121
|
return this.request('GET', `/documents/${docId}/tabs`);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { getGlobalOpts, getClient } from './root.js';
|
|
2
2
|
import { output, outputError } from '../output/formatter.js';
|
|
3
|
-
export function
|
|
4
|
-
const apiKey = program
|
|
3
|
+
export function registerApiKeysCommand(program) {
|
|
4
|
+
const apiKey = program
|
|
5
|
+
.command('api-keys')
|
|
6
|
+
.alias('api-key')
|
|
7
|
+
.description('Manage API keys');
|
|
5
8
|
apiKey
|
|
6
9
|
.command('create <name>')
|
|
7
10
|
.description('Create a new API key')
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { getGlobalOpts, getClient, getConfig } from './root.js';
|
|
2
2
|
import { output, outputError } from '../output/formatter.js';
|
|
3
3
|
import { printDryRun } from '../client/dry-run.js';
|
|
4
|
-
export function
|
|
5
|
-
const cell =
|
|
4
|
+
export function registerCellsCommand(parent) {
|
|
5
|
+
const cell = parent
|
|
6
|
+
.command('cells')
|
|
7
|
+
.alias('cell')
|
|
8
|
+
.description('Read and write cells');
|
|
6
9
|
cell
|
|
7
10
|
.command('get <doc-id> [range]')
|
|
8
11
|
.description('Get cells (default: all, or A1, or A1:C10)')
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { getGlobalOpts, getClient, getConfig } from './root.js';
|
|
3
|
+
import { output, outputError } from '../output/formatter.js';
|
|
4
|
+
import { printDryRun } from '../client/dry-run.js';
|
|
5
|
+
import { parseContentFormat, runDocsContent } from '../docs/content.js';
|
|
6
|
+
import { exportPdf } from '../docs/pdf-export.js';
|
|
7
|
+
import { exportDocx } from '../docs/docx-export.js';
|
|
8
|
+
import { parsePageRange } from '../docs/page-range.js';
|
|
9
|
+
import { writeBinary } from '../output/binary.js';
|
|
10
|
+
import { runDocsImport } from '../docs/import.js';
|
|
11
|
+
function parseType(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (value !== 'doc' && value !== 'sheet') {
|
|
15
|
+
throw new Error(`Invalid --type "${value}". Use "doc" or "sheet".`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
const VALID_EXPORT_FORMATS = ['pdf', 'docx'];
|
|
20
|
+
function detectExportFormat(file, formatFlag) {
|
|
21
|
+
if (formatFlag) {
|
|
22
|
+
if (!VALID_EXPORT_FORMATS.includes(formatFlag)) {
|
|
23
|
+
throw new Error(`Invalid --format "${formatFlag}". Use one of: ${VALID_EXPORT_FORMATS.join(', ')}.`);
|
|
24
|
+
}
|
|
25
|
+
return formatFlag;
|
|
26
|
+
}
|
|
27
|
+
const ext = extname(file).toLowerCase();
|
|
28
|
+
if (ext === '.pdf')
|
|
29
|
+
return 'pdf';
|
|
30
|
+
if (ext === '.docx')
|
|
31
|
+
return 'docx';
|
|
32
|
+
throw new Error(`Cannot infer format from "${file}". Pass --format pdf|docx, or use a .pdf/.docx extension.`);
|
|
33
|
+
}
|
|
34
|
+
export function registerDocsCommand(program) {
|
|
35
|
+
const doc = program
|
|
36
|
+
.command('docs')
|
|
37
|
+
.alias('doc')
|
|
38
|
+
.alias('document')
|
|
39
|
+
.alias('documents')
|
|
40
|
+
.description('Manage documents');
|
|
41
|
+
doc
|
|
42
|
+
.command('list')
|
|
43
|
+
.description('List documents in workspace')
|
|
44
|
+
.option('--type <type>', 'Filter by document type (doc|sheet)')
|
|
45
|
+
.action(async function () {
|
|
46
|
+
const opts = getGlobalOpts(this);
|
|
47
|
+
const { type: typeStr } = this.opts();
|
|
48
|
+
try {
|
|
49
|
+
const filterType = parseType(typeStr);
|
|
50
|
+
const res = await getClient(opts).listDocuments();
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
throw new Error(`HTTP ${res.status}`);
|
|
53
|
+
let data = res.data;
|
|
54
|
+
if (filterType && Array.isArray(data)) {
|
|
55
|
+
data = data.filter((d) => (d.type ?? 'sheet') === filterType);
|
|
56
|
+
}
|
|
57
|
+
output(data, opts.format, opts.quiet);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
outputError(e, opts.quiet);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
doc
|
|
64
|
+
.command('create <title>')
|
|
65
|
+
.description('Create a new document')
|
|
66
|
+
.option('--type <type>', 'Document type (doc|sheet)', 'sheet')
|
|
67
|
+
.action(async function (title) {
|
|
68
|
+
const opts = getGlobalOpts(this);
|
|
69
|
+
const { type: typeStr } = this.opts();
|
|
70
|
+
try {
|
|
71
|
+
const type = parseType(typeStr) ?? 'sheet';
|
|
72
|
+
if (opts.dryRun) {
|
|
73
|
+
printDryRun(getConfig(opts), 'POST', '/documents', { title, type });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const res = await getClient(opts).createDocument(title, type);
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`HTTP ${res.status}`);
|
|
79
|
+
output(res.data, opts.format, opts.quiet);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
outputError(e, opts.quiet);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
doc
|
|
86
|
+
.command('get <doc-id>')
|
|
87
|
+
.description('Show document metadata')
|
|
88
|
+
.action(async function (docId) {
|
|
89
|
+
const opts = getGlobalOpts(this);
|
|
90
|
+
try {
|
|
91
|
+
const res = await getClient(opts).getDocument(docId);
|
|
92
|
+
if (!res.ok)
|
|
93
|
+
throw new Error(`HTTP ${res.status}`);
|
|
94
|
+
output(res.data, opts.format, opts.quiet);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
outputError(e, opts.quiet);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
doc
|
|
101
|
+
.command('rename <doc-id> <title>')
|
|
102
|
+
.description('Rename a document')
|
|
103
|
+
.action(async function (docId, title) {
|
|
104
|
+
const opts = getGlobalOpts(this);
|
|
105
|
+
if (opts.dryRun) {
|
|
106
|
+
printDryRun(getConfig(opts), 'PATCH', `/documents/${docId}`, { title });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const res = await getClient(opts).updateDocument(docId, title);
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
throw new Error(`HTTP ${res.status}`);
|
|
113
|
+
output(res.data, opts.format, opts.quiet);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
outputError(e, opts.quiet);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
doc
|
|
120
|
+
.command('delete <doc-id>')
|
|
121
|
+
.description('Delete a document')
|
|
122
|
+
.action(async function (docId) {
|
|
123
|
+
const opts = getGlobalOpts(this);
|
|
124
|
+
if (opts.dryRun) {
|
|
125
|
+
printDryRun(getConfig(opts), 'DELETE', `/documents/${docId}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const res = await getClient(opts).deleteDocument(docId);
|
|
130
|
+
if (!res.ok)
|
|
131
|
+
throw new Error(`HTTP ${res.status}`);
|
|
132
|
+
output(res.data, opts.format, opts.quiet);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
outputError(e, opts.quiet);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
doc
|
|
139
|
+
.command('content <doc-id>')
|
|
140
|
+
.description('Read document content as JSON, Markdown, or plain text')
|
|
141
|
+
// NOTE: `--format` is intentionally not redeclared here. The global
|
|
142
|
+
// `--format` option (declared in `createProgram`) catches the user's
|
|
143
|
+
// value because commander resolves duplicate-named flags through the
|
|
144
|
+
// earliest parent that owns them. Reading via `opts.format` (the
|
|
145
|
+
// merged form) keeps a single source of truth and lets
|
|
146
|
+
// `parseContentFormat` validate that the value is one of json|md|text.
|
|
147
|
+
.option('--pages <range>', 'Page range to include (e.g. 1-3,5)')
|
|
148
|
+
.option('--include-header-footer', 'Include header/footer (md/text)', false)
|
|
149
|
+
.option('--inline-images', 'Inline data: image URLs (md only)', false)
|
|
150
|
+
.option('--out <file>', 'Output file (- for stdout)')
|
|
151
|
+
.option('--force', 'Overwrite existing output file', false)
|
|
152
|
+
.action(async function (docId) {
|
|
153
|
+
const opts = getGlobalOpts(this);
|
|
154
|
+
const local = this.opts();
|
|
155
|
+
try {
|
|
156
|
+
const format = parseContentFormat(opts.format);
|
|
157
|
+
if (opts.dryRun) {
|
|
158
|
+
printDryRun(getConfig(opts), 'GET', `/documents/${docId}/content`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const res = await getClient(opts).getDocContent(docId);
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const body = res.data;
|
|
164
|
+
if (body?.error) {
|
|
165
|
+
// Surface backend-shaped errors (e.g., TYPE_MISMATCH) verbatim
|
|
166
|
+
// so agents reading stderr can act on the `code` field.
|
|
167
|
+
console.error(JSON.stringify(body, null, 2));
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`HTTP ${res.status}`);
|
|
172
|
+
}
|
|
173
|
+
runDocsContent({
|
|
174
|
+
doc: res.data,
|
|
175
|
+
format,
|
|
176
|
+
pages: local.pages,
|
|
177
|
+
includeHeaderFooter: local.includeHeaderFooter,
|
|
178
|
+
inlineImages: local.inlineImages,
|
|
179
|
+
out: local.out,
|
|
180
|
+
force: local.force,
|
|
181
|
+
quiet: opts.quiet,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
outputError(e, opts.quiet);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
doc
|
|
189
|
+
.command('export <doc-id> <file>')
|
|
190
|
+
.description('Export a document to PDF or DOCX')
|
|
191
|
+
// NOTE: `--format` reuses the global option for the same reason
|
|
192
|
+
// documented on `docs content` above; we route through `opts.format`
|
|
193
|
+
// and use commander's `getOptionValueSourceWithGlobals` to tell a
|
|
194
|
+
// user-supplied value from the implicit default. That keeps an
|
|
195
|
+
// explicit `--format json` from silently falling through to the
|
|
196
|
+
// extension — it now surfaces as the documented "invalid format"
|
|
197
|
+
// error.
|
|
198
|
+
.option('--pages <range>', 'Page range to export (PDF only)')
|
|
199
|
+
.option('--include-header-footer', 'Include header/footer regions', true)
|
|
200
|
+
.option('--force', 'Overwrite existing output file', false)
|
|
201
|
+
.action(async function (docId, file) {
|
|
202
|
+
const opts = getGlobalOpts(this);
|
|
203
|
+
const local = this.opts();
|
|
204
|
+
try {
|
|
205
|
+
// `'cli'` source means the user typed the flag (or its alias)
|
|
206
|
+
// on the command line; anything else (`'default'`, `'env'`,
|
|
207
|
+
// `'config'`) is treated as "no override intended" so the
|
|
208
|
+
// file extension wins.
|
|
209
|
+
const formatSource = this.getOptionValueSourceWithGlobals('format');
|
|
210
|
+
const formatOverride = formatSource === 'cli' ? opts.format : undefined;
|
|
211
|
+
const format = detectExportFormat(file, formatOverride);
|
|
212
|
+
if (opts.dryRun) {
|
|
213
|
+
printDryRun(getConfig(opts), 'GET', `/documents/${docId}/content`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const res = await getClient(opts).getDocContent(docId);
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
const body = res.data;
|
|
219
|
+
if (body?.error) {
|
|
220
|
+
console.error(JSON.stringify(body, null, 2));
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`HTTP ${res.status}`);
|
|
225
|
+
}
|
|
226
|
+
const fetchedDoc = res.data;
|
|
227
|
+
let bytes;
|
|
228
|
+
if (format === 'pdf') {
|
|
229
|
+
// Pagination uses the FontkitMeasurer's coarse Latin estimate
|
|
230
|
+
// when no Korean fonts are needed; we still pre-page to know
|
|
231
|
+
// the document's page count for `parsePageRange`. PdfExporter
|
|
232
|
+
// does the authoritative paint-time pagination internally.
|
|
233
|
+
let pageRange = undefined;
|
|
234
|
+
if (local.pages) {
|
|
235
|
+
// Resolve pages against the rendered PDF's page count
|
|
236
|
+
// (rather than running our own paginator twice). We render
|
|
237
|
+
// the full PDF first, then strip non-selected pages —
|
|
238
|
+
// see `extractPages` in pdf-export.ts.
|
|
239
|
+
const fullPdf = await exportPdf(fetchedDoc, {
|
|
240
|
+
includeHeaderFooter: local.includeHeaderFooter,
|
|
241
|
+
});
|
|
242
|
+
const total = await pdfPageCount(fullPdf);
|
|
243
|
+
pageRange = parsePageRange(local.pages, total);
|
|
244
|
+
if (!opts.quiet) {
|
|
245
|
+
for (const w of pageRange.warnings)
|
|
246
|
+
console.error(w);
|
|
247
|
+
}
|
|
248
|
+
bytes = await exportPdf(fetchedDoc, {
|
|
249
|
+
includeHeaderFooter: local.includeHeaderFooter,
|
|
250
|
+
pages: pageRange,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
bytes = await exportPdf(fetchedDoc, {
|
|
255
|
+
includeHeaderFooter: local.includeHeaderFooter,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
if (local.pages && !opts.quiet) {
|
|
261
|
+
console.error('DOCX has no page concept — exporting full document, --pages ignored.');
|
|
262
|
+
}
|
|
263
|
+
bytes = await exportDocx(fetchedDoc, {
|
|
264
|
+
includeHeaderFooter: local.includeHeaderFooter,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
writeBinary(bytes, file, { force: local.force, quiet: opts.quiet });
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
outputError(e, opts.quiet);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
registerDocsImportCommand(doc);
|
|
274
|
+
}
|
|
275
|
+
async function pdfPageCount(bytes) {
|
|
276
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
277
|
+
const pdf = await PDFDocument.load(bytes);
|
|
278
|
+
return pdf.getPageCount();
|
|
279
|
+
}
|
|
280
|
+
export function registerDocsImportCommand(doc) {
|
|
281
|
+
doc
|
|
282
|
+
.command('import <file>')
|
|
283
|
+
.description('Import a .docx file as a new (or replacement) document')
|
|
284
|
+
.option('--title <title>', 'Document title (default: file basename)')
|
|
285
|
+
.option('--replace <doc-id>', 'Replace content of an existing document')
|
|
286
|
+
.option('--yes', 'Skip --replace confirmation prompt', false)
|
|
287
|
+
.action(async function (file) {
|
|
288
|
+
const opts = getGlobalOpts(this);
|
|
289
|
+
const local = this.opts();
|
|
290
|
+
try {
|
|
291
|
+
const result = await runDocsImport({
|
|
292
|
+
file,
|
|
293
|
+
title: local.title,
|
|
294
|
+
replace: local.replace,
|
|
295
|
+
yes: local.yes,
|
|
296
|
+
quiet: opts.quiet,
|
|
297
|
+
dryRun: opts.dryRun,
|
|
298
|
+
}, getClient(opts));
|
|
299
|
+
if (result.exitCode !== 0)
|
|
300
|
+
process.exitCode = result.exitCode;
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
outputError(e, opts.quiet);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
@@ -17,8 +17,8 @@ function detectFormat(file, formatFlag) {
|
|
|
17
17
|
return 'json';
|
|
18
18
|
return 'csv';
|
|
19
19
|
}
|
|
20
|
-
export function
|
|
21
|
-
|
|
20
|
+
export function registerSheetsExportCommand(parent) {
|
|
21
|
+
parent
|
|
22
22
|
.command('export <doc-id> <file>')
|
|
23
23
|
.description('Export tab data to CSV/JSON')
|
|
24
24
|
.option('--tab <tab-id>', 'Source tab', 'tab-1')
|
|
@@ -23,8 +23,8 @@ function readInput(file) {
|
|
|
23
23
|
}
|
|
24
24
|
return readFileSync(file, 'utf-8');
|
|
25
25
|
}
|
|
26
|
-
export function
|
|
27
|
-
|
|
26
|
+
export function registerSheetsImportCommand(parent) {
|
|
27
|
+
parent
|
|
28
28
|
.command('import <doc-id> <file>')
|
|
29
29
|
.description('Import CSV/JSON into a tab')
|
|
30
30
|
.option('--tab <tab-id>', 'Target tab', 'tab-1')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerCellsCommand } from './cells.js';
|
|
2
|
+
import { registerTabsCommand } from './tabs.js';
|
|
3
|
+
import { registerSheetsImportCommand } from './sheets-import.js';
|
|
4
|
+
import { registerSheetsExportCommand } from './sheets-export.js';
|
|
5
|
+
export function registerSheetsCommand(program) {
|
|
6
|
+
const sheets = program
|
|
7
|
+
.command('sheets')
|
|
8
|
+
.alias('sheet')
|
|
9
|
+
.alias('spreadsheet')
|
|
10
|
+
.alias('spreadsheets')
|
|
11
|
+
.description('Spreadsheet commands');
|
|
12
|
+
registerTabsCommand(sheets);
|
|
13
|
+
registerCellsCommand(sheets);
|
|
14
|
+
registerSheetsImportCommand(sheets);
|
|
15
|
+
registerSheetsExportCommand(sheets);
|
|
16
|
+
return sheets;
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getGlobalOpts, getClient } from './root.js';
|
|
2
2
|
import { output, outputError } from '../output/formatter.js';
|
|
3
|
-
export function
|
|
4
|
-
const tab =
|
|
3
|
+
export function registerTabsCommand(parent) {
|
|
4
|
+
const tab = parent.command('tabs').alias('tab').description('Manage tabs');
|
|
5
5
|
tab
|
|
6
6
|
.command('list <doc-id>')
|
|
7
7
|
.description('List tabs in a document')
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Document } from '@wafflebase/docs';
|
|
2
|
+
export declare const VALID_CONTENT_FORMATS: readonly ["json", "md", "text"];
|
|
3
|
+
export type ContentFormat = (typeof VALID_CONTENT_FORMATS)[number];
|
|
4
|
+
export declare const LOSSY_NOTICE = "Lossy conversion: see docs-cli design for the exact mapping";
|
|
5
|
+
export declare const PAGES_FONT_WARNING: string;
|
|
6
|
+
export declare function parseContentFormat(value: string): ContentFormat;
|
|
7
|
+
/**
|
|
8
|
+
* Surface for `runDocsContent` to talk back to the CLI without taking a
|
|
9
|
+
* direct dependency on `process.stdout` / `process.stderr` / `fs`. The
|
|
10
|
+
* command wires the real implementations; tests inject in-memory
|
|
11
|
+
* collectors.
|
|
12
|
+
*/
|
|
13
|
+
export interface ContentIO {
|
|
14
|
+
stdout: (text: string) => void;
|
|
15
|
+
stderr: (line: string) => void;
|
|
16
|
+
/** Write `text` to `path`. Throws if the file exists and `force` is false. */
|
|
17
|
+
writeFile: (path: string, text: string, force: boolean) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare const defaultIO: ContentIO;
|
|
20
|
+
export interface RunContentArgs {
|
|
21
|
+
doc: Document;
|
|
22
|
+
format: ContentFormat;
|
|
23
|
+
pages?: string;
|
|
24
|
+
includeHeaderFooter?: boolean;
|
|
25
|
+
inlineImages?: boolean;
|
|
26
|
+
out?: string;
|
|
27
|
+
force?: boolean;
|
|
28
|
+
quiet?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pure orchestration for `docs content`: takes an already-fetched
|
|
32
|
+
* Document plus user flags, produces the rendered output, and routes it
|
|
33
|
+
* through the supplied IO surface. Everything between the HTTP fetch
|
|
34
|
+
* and the side-effecting IO lives here so it can be tested without
|
|
35
|
+
* spawning the CLI binary.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runDocsContent(args: RunContentArgs, io?: ContentIO): void;
|