@wafflebase/cli 0.4.0 → 0.4.1

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/dist/bin.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { createProgram } from './commands/root.js';
3
3
  import { registerDocsCommand } from './commands/docs.js';
4
4
  import { registerSheetsCommand } from './commands/sheets.js';
5
+ import { registerSlidesCommand } from './commands/slides.js';
5
6
  import { registerApiKeysCommand } from './commands/api-keys.js';
6
7
  import { registerSchemaCommand } from './commands/schema.js';
7
8
  import { registerLoginCommand } from './commands/login.js';
@@ -15,6 +16,7 @@ registerStatusCommand(program);
15
16
  registerCtxCommand(program);
16
17
  registerDocsCommand(program);
17
18
  registerSheetsCommand(program);
19
+ registerSlidesCommand(program);
18
20
  registerApiKeysCommand(program);
19
21
  registerSchemaCommand(program);
20
22
  program.parse();
@@ -1,4 +1,5 @@
1
1
  import type { Document } from '@wafflebase/docs';
2
+ import type { SlidesDocument } from '@wafflebase/slides/node';
2
3
  import type { CliConfig } from '../config/config.js';
3
4
  export interface ApiResponse<T = unknown> {
4
5
  ok: boolean;
@@ -23,12 +24,13 @@ export declare class HttpClient {
23
24
  private refreshSession;
24
25
  request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>>;
25
26
  listDocuments(): Promise<ApiResponse<unknown[]>>;
26
- createDocument(title: string, type?: 'doc' | 'sheet'): Promise<ApiResponse<unknown>>;
27
+ createDocument(title: string, type?: 'doc' | 'sheet' | 'slides'): Promise<ApiResponse<unknown>>;
27
28
  getDocument(id: string): Promise<ApiResponse<unknown>>;
28
29
  updateDocument(id: string, title: string): Promise<ApiResponse<unknown>>;
29
30
  deleteDocument(id: string): Promise<ApiResponse<unknown>>;
30
31
  getDocContent(docId: string): Promise<ApiResponse<Document>>;
31
32
  putDocContent(docId: string, doc: Document): Promise<ApiResponse<Document>>;
33
+ putSlidesContent(docId: string, deck: SlidesDocument): Promise<ApiResponse<SlidesDocument>>;
32
34
  listTabs(docId: string): Promise<ApiResponse<unknown[]>>;
33
35
  getCells(docId: string, tabId: string, range?: string): Promise<ApiResponse<unknown>>;
34
36
  getCell(docId: string, tabId: string, sref: string): Promise<ApiResponse<unknown>>;
@@ -116,6 +116,12 @@ export class HttpClient {
116
116
  putDocContent(docId, doc) {
117
117
  return this.request('PUT', `/documents/${docId}/content`, doc);
118
118
  }
119
+ // Slides content — same endpoint as docs; the backend dispatches on
120
+ // the persisted document type, picking the docs writer for `'doc'`
121
+ // and the slides writer for `'slides'`.
122
+ putSlidesContent(docId, deck) {
123
+ return this.request('PUT', `/documents/${docId}/content`, deck);
124
+ }
119
125
  // Tabs
120
126
  listTabs(docId) {
121
127
  return this.request('GET', `/documents/${docId}/tabs`);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSlidesCommand(program: Command): void;
3
+ export declare function registerSlidesImportCommand(slides: Command): void;
@@ -0,0 +1,38 @@
1
+ import { getGlobalOpts, getClient } from './root.js';
2
+ import { outputError } from '../output/formatter.js';
3
+ import { runSlidesImport } from '../slides/import.js';
4
+ export function registerSlidesCommand(program) {
5
+ const slides = program
6
+ .command('slides')
7
+ .alias('slide')
8
+ .alias('deck')
9
+ .description('Manage slide decks');
10
+ registerSlidesImportCommand(slides);
11
+ }
12
+ export function registerSlidesImportCommand(slides) {
13
+ slides
14
+ .command('import <file>')
15
+ .description('Import a .pptx file as a new (or replacement) slides deck')
16
+ .option('--title <title>', 'Deck title (default: file basename)')
17
+ .option('--replace <doc-id>', 'Replace content of an existing deck')
18
+ .option('--yes', 'Skip --replace confirmation prompt', false)
19
+ .action(async function (file) {
20
+ const opts = getGlobalOpts(this);
21
+ const local = this.opts();
22
+ try {
23
+ const result = await runSlidesImport({
24
+ file,
25
+ title: local.title,
26
+ replace: local.replace,
27
+ yes: local.yes,
28
+ quiet: opts.quiet,
29
+ dryRun: opts.dryRun,
30
+ }, getClient(opts));
31
+ if (result.exitCode !== 0)
32
+ process.exitCode = result.exitCode;
33
+ }
34
+ catch (e) {
35
+ outputError(e, opts.quiet);
36
+ }
37
+ });
38
+ }
@@ -0,0 +1,82 @@
1
+ import type { SlidesDocument, UploadImage } from '@wafflebase/slides/node';
2
+ import { type CliPptxImportOptions } from './pptx-import.js';
3
+ import type { ImportReport } from '@wafflebase/slides/node';
4
+ /**
5
+ * Parsing surface — split out so tests can inject a stub that returns
6
+ * a synthetic `SlidesDocument` without building a real `.pptx` byte
7
+ * buffer. Production callers use the default (`importPptx` from
8
+ * `./pptx-import.js`).
9
+ */
10
+ export type SlidesImportParser = (buf: Uint8Array, opts?: CliPptxImportOptions) => Promise<{
11
+ document: SlidesDocument;
12
+ report: ImportReport;
13
+ }>;
14
+ /**
15
+ * Minimal HTTP surface `runSlidesImport` needs from the CLI's
16
+ * `HttpClient`. Spelled out so tests can pass a stub without depending
17
+ * on the full client. Mirrors `ImportClient` in `../docs/import.ts`.
18
+ */
19
+ export interface SlidesImportClient {
20
+ createDocument: (title: string, type?: 'doc' | 'sheet' | 'slides') => Promise<{
21
+ ok: boolean;
22
+ status: number;
23
+ data: unknown;
24
+ }>;
25
+ putSlidesContent: (docId: string, deck: SlidesDocument) => Promise<{
26
+ ok: boolean;
27
+ status: number;
28
+ data: unknown;
29
+ }>;
30
+ }
31
+ export interface SlidesImportIO {
32
+ /** Final structured payload — written to stdout as JSON. */
33
+ stdout: (line: string) => void;
34
+ stderr: (line: string) => void;
35
+ /**
36
+ * Read the pptx bytes for `path`. `'-'` reads from stdin.
37
+ *
38
+ * Split out so tests can inject in-memory buffers without juggling
39
+ * tmp files; the default reads `process.stdin` (or the file
40
+ * synchronously) at the action site.
41
+ */
42
+ readBytes: (path: string) => Promise<Uint8Array>;
43
+ /**
44
+ * Prompt the user for a yes/no answer when running interactively.
45
+ * Returns `true` for affirmative responses (case-insensitive `y` /
46
+ * `yes`); anything else (including end-of-stream) → `false`.
47
+ */
48
+ confirm: (prompt: string) => Promise<boolean>;
49
+ /** Whether the current process is interactive (a TTY on stdin). */
50
+ isTTY: boolean;
51
+ }
52
+ export declare const defaultSlidesImportIO: SlidesImportIO;
53
+ export interface RunSlidesImportArgs {
54
+ /** Source path. `'-'` reads from stdin. */
55
+ file: string;
56
+ /** Override the deck title. Falls back to the file basename. */
57
+ title?: string;
58
+ /** Existing document ID to overwrite. Pairs with `--yes`. */
59
+ replace?: string;
60
+ /** Skip the interactive `--replace` confirmation. */
61
+ yes?: boolean;
62
+ /** Override the default base64 image uploader. Tests pass a no-op. */
63
+ uploadImage?: UploadImage;
64
+ /** Suppress informational stderr output (errors still reported). */
65
+ quiet?: boolean;
66
+ /** Print the request that *would* fire instead of issuing it. */
67
+ dryRun?: boolean;
68
+ /** Override the .pptx parser — primarily for tests. */
69
+ parser?: SlidesImportParser;
70
+ }
71
+ export interface RunSlidesImportResult {
72
+ /** Process exit code. The action sets `process.exitCode` accordingly. */
73
+ exitCode: number;
74
+ }
75
+ /**
76
+ * Pure orchestration for `slides import`. Reads the source `.pptx`,
77
+ * parses it into a `SlidesDocument`, then either creates a new deck
78
+ * (default) or overwrites an existing one (`--replace`). Mirrors
79
+ * `runDocsImport` — see that file for the rationale behind the split
80
+ * between this function and the Commander action.
81
+ */
82
+ export declare function runSlidesImport(args: RunSlidesImportArgs, client: SlidesImportClient, io?: SlidesImportIO): Promise<RunSlidesImportResult>;
@@ -0,0 +1,169 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename, extname } from 'node:path';
3
+ import { createInterface } from 'node:readline';
4
+ import { importPptx as defaultImportPptx, InvalidPptxError, } from './pptx-import.js';
5
+ export const defaultSlidesImportIO = {
6
+ stdout: (line) => {
7
+ process.stdout.write(line);
8
+ if (!line.endsWith('\n'))
9
+ process.stdout.write('\n');
10
+ },
11
+ stderr: (line) => {
12
+ console.error(line);
13
+ },
14
+ readBytes: async (path) => {
15
+ if (path === '-') {
16
+ return new Promise((resolve, reject) => {
17
+ const chunks = [];
18
+ process.stdin.on('data', (c) => chunks.push(c));
19
+ process.stdin.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));
20
+ process.stdin.on('error', reject);
21
+ });
22
+ }
23
+ return new Uint8Array(readFileSync(path));
24
+ },
25
+ confirm: (prompt) => {
26
+ return new Promise((resolve) => {
27
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
28
+ rl.question(prompt, (answer) => {
29
+ rl.close();
30
+ const a = answer.trim().toLowerCase();
31
+ resolve(a === 'y' || a === 'yes');
32
+ });
33
+ });
34
+ },
35
+ isTTY: process.stdin.isTTY ?? false,
36
+ };
37
+ /**
38
+ * Pure orchestration for `slides import`. Reads the source `.pptx`,
39
+ * parses it into a `SlidesDocument`, then either creates a new deck
40
+ * (default) or overwrites an existing one (`--replace`). Mirrors
41
+ * `runDocsImport` — see that file for the rationale behind the split
42
+ * between this function and the Commander action.
43
+ */
44
+ export async function runSlidesImport(args, client, io = defaultSlidesImportIO) {
45
+ const { file, replace, yes = false, uploadImage, quiet = false, dryRun = false, parser = defaultImportPptx, } = args;
46
+ const inferredTitle = args.title ?? defaultTitleFor(file);
47
+ if (replace) {
48
+ // `--replace` is destructive: confirm interactively unless `--yes`.
49
+ if (!yes) {
50
+ if (!io.isTTY) {
51
+ io.stderr(JSON.stringify({
52
+ error: {
53
+ code: 'CONFIRMATION_REQ',
54
+ message: `Pass --yes to confirm replacing deck "${replace}".`,
55
+ },
56
+ }, null, 2));
57
+ return { exitCode: 1 };
58
+ }
59
+ const ok = await io.confirm(`This will replace content of ${replace}. Continue? [y/N] `);
60
+ if (!ok) {
61
+ if (!quiet)
62
+ io.stderr('Aborted.');
63
+ return { exitCode: 0 };
64
+ }
65
+ }
66
+ const buf = await io.readBytes(file);
67
+ const parsed = await safeImportPptx(buf, uploadImage, io, parser);
68
+ if (parsed === null)
69
+ return { exitCode: 1 };
70
+ const { document: deck, report } = parsed;
71
+ if (dryRun) {
72
+ io.stdout(JSON.stringify({
73
+ method: 'PUT',
74
+ path: `/documents/${replace}/content`,
75
+ body: deck,
76
+ report: summariseReport(report),
77
+ }, null, 2));
78
+ return { exitCode: 0 };
79
+ }
80
+ const res = await client.putSlidesContent(replace, deck);
81
+ if (!res.ok) {
82
+ io.stderr(JSON.stringify(res.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
83
+ return { exitCode: 1 };
84
+ }
85
+ io.stdout(JSON.stringify({ id: replace, replaced: true, report: summariseReport(report) }, null, 2));
86
+ return { exitCode: 0 };
87
+ }
88
+ // Default flow: POST + PUT.
89
+ const buf = await io.readBytes(file);
90
+ const parsedNew = await safeImportPptx(buf, uploadImage, io, parser);
91
+ if (parsedNew === null)
92
+ return { exitCode: 1 };
93
+ const { document: deck, report } = parsedNew;
94
+ // The parser fills `meta.title` with whatever string the .pptx carries
95
+ // (often "PowerPoint Presentation" or "Imported deck"). The user's
96
+ // `--title` flag is the source of truth for the deck name — keep the
97
+ // Yorkie root's title in lock-step with the Document row so the deck
98
+ // list, editor header, and GET /content all agree.
99
+ deck.meta.title = inferredTitle;
100
+ if (dryRun) {
101
+ io.stdout(JSON.stringify({
102
+ method: 'POST',
103
+ path: '/documents',
104
+ body: { title: inferredTitle, type: 'slides' },
105
+ followUp: { method: 'PUT', path: '/documents/<new-id>/content' },
106
+ report: summariseReport(report),
107
+ }, null, 2));
108
+ return { exitCode: 0 };
109
+ }
110
+ const created = await client.createDocument(inferredTitle, 'slides');
111
+ if (!created.ok) {
112
+ io.stderr(JSON.stringify(created.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
113
+ return { exitCode: 1 };
114
+ }
115
+ const newId = created.data?.id;
116
+ if (!newId) {
117
+ io.stderr(JSON.stringify({ error: { code: 'INVALID_RESPONSE', message: 'Server did not return an id' } }, null, 2));
118
+ return { exitCode: 1 };
119
+ }
120
+ const put = await client.putSlidesContent(newId, deck);
121
+ if (!put.ok) {
122
+ io.stderr(JSON.stringify(put.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
123
+ return { exitCode: 1 };
124
+ }
125
+ io.stdout(JSON.stringify({ id: newId, title: inferredTitle, report: summariseReport(report) }, null, 2));
126
+ return { exitCode: 0 };
127
+ }
128
+ function defaultTitleFor(file) {
129
+ if (file === '-')
130
+ return 'Untitled';
131
+ return basename(file, extname(file)) || 'Untitled';
132
+ }
133
+ /**
134
+ * Collect the parser's `ImportReport` counters into a plain object so
135
+ * the CLI can print them as JSON. We surface every counter rather than
136
+ * the `.summary()` string so agentic callers can parse without
137
+ * regex-matching prose.
138
+ */
139
+ function summariseReport(report) {
140
+ return {
141
+ groupsFlattened: report.groupsFlattened,
142
+ tablesFlattened: report.tablesFlattened,
143
+ shadowsDropped: report.shadowsDropped,
144
+ textBoxesPreScaled: report.textBoxesPreScaled,
145
+ unknownShapes: report.unknownShapes,
146
+ unknownLayoutTypes: report.unknownLayoutTypes,
147
+ tableMergesIgnored: report.tableMergesIgnored,
148
+ tableBordersApproximated: report.tableBordersApproximated,
149
+ skippedImages: report.skippedImages,
150
+ };
151
+ }
152
+ /**
153
+ * Run `importPptx` and turn `InvalidPptxError` into a structured stderr
154
+ * body + `null` return so callers can short-circuit with `exitCode: 1`
155
+ * while keeping the rest of the error envelope (`HTTP_ERROR`,
156
+ * `INVALID_RESPONSE`, etc.) consistent.
157
+ */
158
+ async function safeImportPptx(buf, uploadImage, io, parser) {
159
+ try {
160
+ return await parser(buf, { uploadImage });
161
+ }
162
+ catch (e) {
163
+ if (e instanceof InvalidPptxError) {
164
+ io.stderr(JSON.stringify({ error: { code: e.code, message: e.message } }, null, 2));
165
+ return null;
166
+ }
167
+ throw e;
168
+ }
169
+ }
@@ -0,0 +1,35 @@
1
+ import '../docs/dom-polyfill.js';
2
+ import { type ImportPptxResult, type UploadImage } from '@wafflebase/slides/node';
3
+ /**
4
+ * Default `UploadImage` for the CLI: encodes each embedded image as a
5
+ * `data:` URL. Matches the docs CLI's `inlineBase64Uploader` — the
6
+ * frontend treats slide image `src` values as opaque strings, so a
7
+ * self-contained data URL round-trips cleanly through the API and
8
+ * renders without needing the workspace's `/images` endpoint.
9
+ *
10
+ * The trade-off is payload size: a benchmark deck with 25 unique
11
+ * images and 63 references gains roughly the image-byte total to the
12
+ * Yorkie document. Acceptable for a v1 CLI import — power users who
13
+ * want hosted URLs can pass a custom uploader.
14
+ */
15
+ export declare const inlineBase64SlidesUploader: UploadImage;
16
+ export interface CliPptxImportOptions {
17
+ /** Override the default base64 uploader — primarily for tests. */
18
+ uploadImage?: UploadImage;
19
+ }
20
+ /**
21
+ * Parse a .pptx byte buffer into a `SlidesDocument` via the slides
22
+ * parser. The DOM polyfill side-effect import installs xmldom's
23
+ * `DOMParser` globally so the parser's browser-targeted XML reads work
24
+ * in Node.
25
+ *
26
+ * Throws a wrapped `INVALID_PPTX` error when the underlying parser
27
+ * cannot make sense of the buffer. The CLI command translates that
28
+ * into the documented `{ error.code: 'INVALID_PPTX' }` exit body
29
+ * without leaking implementation details.
30
+ */
31
+ export declare function importPptx(buf: Uint8Array, opts?: CliPptxImportOptions): Promise<ImportPptxResult>;
32
+ export declare class InvalidPptxError extends Error {
33
+ readonly code = "INVALID_PPTX";
34
+ constructor(message: string);
35
+ }
@@ -0,0 +1,49 @@
1
+ import '../docs/dom-polyfill.js';
2
+ import { importPptx as importPptxFromSlides, } from '@wafflebase/slides/node';
3
+ /**
4
+ * Default `UploadImage` for the CLI: encodes each embedded image as a
5
+ * `data:` URL. Matches the docs CLI's `inlineBase64Uploader` — the
6
+ * frontend treats slide image `src` values as opaque strings, so a
7
+ * self-contained data URL round-trips cleanly through the API and
8
+ * renders without needing the workspace's `/images` endpoint.
9
+ *
10
+ * The trade-off is payload size: a benchmark deck with 25 unique
11
+ * images and 63 references gains roughly the image-byte total to the
12
+ * Yorkie document. Acceptable for a v1 CLI import — power users who
13
+ * want hosted URLs can pass a custom uploader.
14
+ */
15
+ export const inlineBase64SlidesUploader = async (bytes, mime) => {
16
+ const base64 = Buffer.from(bytes).toString('base64');
17
+ const mediaType = mime || 'application/octet-stream';
18
+ return `data:${mediaType};base64,${base64}`;
19
+ };
20
+ /**
21
+ * Parse a .pptx byte buffer into a `SlidesDocument` via the slides
22
+ * parser. The DOM polyfill side-effect import installs xmldom's
23
+ * `DOMParser` globally so the parser's browser-targeted XML reads work
24
+ * in Node.
25
+ *
26
+ * Throws a wrapped `INVALID_PPTX` error when the underlying parser
27
+ * cannot make sense of the buffer. The CLI command translates that
28
+ * into the documented `{ error.code: 'INVALID_PPTX' }` exit body
29
+ * without leaking implementation details.
30
+ */
31
+ export async function importPptx(buf, opts = {}) {
32
+ const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
33
+ try {
34
+ return await importPptxFromSlides(arrayBuf, {
35
+ uploadImage: opts.uploadImage ?? inlineBase64SlidesUploader,
36
+ });
37
+ }
38
+ catch (e) {
39
+ const message = e instanceof Error ? e.message : String(e);
40
+ throw new InvalidPptxError(message);
41
+ }
42
+ }
43
+ export class InvalidPptxError extends Error {
44
+ code = 'INVALID_PPTX';
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = 'InvalidPptxError';
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafflebase/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI for Wafflebase spreadsheet API",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "open": "^11.0.0",
21
21
  "pdf-lib": "^1.17.1",
22
22
  "yaml": "^2.0.0",
23
- "@wafflebase/docs": "0.4.0"
23
+ "@wafflebase/docs": "0.4.1",
24
+ "@wafflebase/slides": "0.4.1"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/fontkit": "^2.0.0",