@wafflebase/cli 0.3.5 → 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.
Files changed (51) hide show
  1. package/README.md +129 -0
  2. package/dist/bin.js +6 -12
  3. package/dist/client/http-client.d.ts +4 -1
  4. package/dist/client/http-client.js +12 -2
  5. package/dist/commands/api-keys.d.ts +2 -0
  6. package/dist/commands/{api-key.js → api-keys.js} +5 -2
  7. package/dist/commands/cells.d.ts +2 -0
  8. package/dist/commands/{cell.js → cells.js} +5 -2
  9. package/dist/commands/docs.d.ts +3 -0
  10. package/dist/commands/docs.js +306 -0
  11. package/dist/commands/sheets-export.d.ts +2 -0
  12. package/dist/commands/{export.js → sheets-export.js} +2 -2
  13. package/dist/commands/sheets-import.d.ts +2 -0
  14. package/dist/commands/{import.js → sheets-import.js} +2 -2
  15. package/dist/commands/sheets.d.ts +2 -0
  16. package/dist/commands/sheets.js +17 -0
  17. package/dist/commands/tabs.d.ts +2 -0
  18. package/dist/commands/{tab.js → tabs.js} +2 -2
  19. package/dist/docs/content.d.ts +37 -0
  20. package/dist/docs/content.js +90 -0
  21. package/dist/docs/docx-export.d.ts +23 -0
  22. package/dist/docs/docx-export.js +14 -0
  23. package/dist/docs/docx-import.d.ts +36 -0
  24. package/dist/docs/docx-import.js +70 -0
  25. package/dist/docs/dom-polyfill.d.ts +1 -0
  26. package/dist/docs/dom-polyfill.js +29 -0
  27. package/dist/docs/fontkit-measurer.d.ts +41 -0
  28. package/dist/docs/fontkit-measurer.js +64 -0
  29. package/dist/docs/import.d.ts +72 -0
  30. package/dist/docs/import.js +146 -0
  31. package/dist/docs/page-range.d.ts +29 -0
  32. package/dist/docs/page-range.js +74 -0
  33. package/dist/docs/page-slice.d.ts +45 -0
  34. package/dist/docs/page-slice.js +61 -0
  35. package/dist/docs/paginate.d.ts +13 -0
  36. package/dist/docs/paginate.js +19 -0
  37. package/dist/docs/pdf-export.d.ts +38 -0
  38. package/dist/docs/pdf-export.js +91 -0
  39. package/dist/output/binary.d.ts +37 -0
  40. package/dist/output/binary.js +47 -0
  41. package/dist/output/formatter.js +15 -1
  42. package/dist/schema/registry.d.ts +27 -0
  43. package/dist/schema/registry.js +103 -23
  44. package/package.json +7 -2
  45. package/dist/commands/api-key.d.ts +0 -2
  46. package/dist/commands/cell.d.ts +0 -2
  47. package/dist/commands/document.d.ts +0 -2
  48. package/dist/commands/document.js +0 -93
  49. package/dist/commands/export.d.ts +0 -2
  50. package/dist/commands/import.d.ts +0 -2
  51. 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 { registerDocumentCommand } from './commands/document.js';
4
- import { registerTabCommand } from './commands/tab.js';
5
- import { registerCellCommand } from './commands/cell.js';
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
- registerDocumentCommand(program);
20
- registerTabCommand(program);
21
- registerCellCommand(program);
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
- return this.request('POST', '/documents', { title });
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`);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerApiKeysCommand(program: Command): void;
@@ -1,7 +1,10 @@
1
1
  import { getGlobalOpts, getClient } from './root.js';
2
2
  import { output, outputError } from '../output/formatter.js';
3
- export function registerApiKeyCommand(program) {
4
- const apiKey = program.command('api-key').description('Manage API keys');
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')
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerCellsCommand(parent: Command): void;
@@ -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 registerCellCommand(program) {
5
- const cell = program.command('cell').description('Read and write cells');
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,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDocsCommand(program: Command): void;
3
+ export declare function registerDocsImportCommand(doc: Command): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSheetsExportCommand(parent: Command): void;
@@ -17,8 +17,8 @@ function detectFormat(file, formatFlag) {
17
17
  return 'json';
18
18
  return 'csv';
19
19
  }
20
- export function registerExportCommand(program) {
21
- program
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')
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSheetsImportCommand(parent: Command): void;
@@ -23,8 +23,8 @@ function readInput(file) {
23
23
  }
24
24
  return readFileSync(file, 'utf-8');
25
25
  }
26
- export function registerImportCommand(program) {
27
- program
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSheetsCommand(program: Command): Command;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerTabsCommand(parent: Command): void;
@@ -1,7 +1,7 @@
1
1
  import { getGlobalOpts, getClient } from './root.js';
2
2
  import { output, outputError } from '../output/formatter.js';
3
- export function registerTabCommand(program) {
4
- const tab = program.command('tab').description('Manage tabs');
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;