@deckflow/deckuse 1.0.1 → 1.1.0

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
@@ -1,122 +1,58 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from 'node:fs/promises';
3
- import { extname, resolve } from 'node:path';
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import { dirname, extname, join, resolve } from 'node:path';
4
+ import { PROTOCOL_VERSION, revisionAsNumber, } from '@deckflow/deckuse-core';
5
+ import { helpTopicFromArgs, resolveHelp } from './help.js';
4
6
  import { runCommand } from './index.js';
7
+ import { startMonitor } from './monitor.js';
8
+ import { renderPage } from './render.js';
9
+ import { EDITION } from './edition.js';
10
+ import { version } from './version.js';
5
11
  const args = process.argv.slice(2);
6
- const json = args.includes('--json');
7
- const clean = args.filter((arg) => arg !== '--json');
8
- const version = '0.0.0';
9
- const HELP = {
10
- main: `usage: deckuse <command> [<args>] [--json]
11
-
12
- Inspect, modify, validate, and export Office document workspaces.
13
-
14
- Common commands:
15
- init <input> <workspace> Create a workspace from an Office document
16
- inspect <workspace> Inspect the document structure
17
- query <workspace> [selector] Query document elements
18
- apply <workspace> Apply JSON command objects
19
- validate <workspace> Validate workspace changes
20
- commit <workspace> Export the workspace to a document
21
-
22
- Run 'deckuse <command> --help' for details about a command.
23
- Run 'deckuse help <command>' for the same command reference.
24
-
25
- Options:
26
- -h, --help Show this help message
27
- -v, -V, --version
28
- Show the deckuse version
29
- --json Format command results as compact JSON
30
- `,
31
- init: `usage: deckuse init <input> <workspace> [--json]
32
-
33
- Create a deckuse workspace from an Office document.
34
-
35
- Arguments:
36
- <input> Source document (.pptx, .docx, .xlsx, .key, or .numbers)
37
- <workspace> Directory in which to create the workspace
38
-
39
- Example:
40
- deckuse init presentation.pptx ./presentation
41
- `,
42
- inspect: `usage: deckuse inspect <workspace> [--depth <n>] [--json]
43
-
44
- Inspect the indexed structure of a workspace.
45
-
46
- Arguments:
47
- <workspace> Existing deckuse workspace
48
-
49
- Options:
50
- --depth <n> Traversal depth (default: 2)
51
-
52
- Example:
53
- deckuse inspect ./presentation --depth 3
54
- `,
55
- query: `usage: deckuse query <workspace> [selector] [--limit <n>] [--json]
56
-
57
- Query indexed elements in a workspace.
58
-
59
- Arguments:
60
- <workspace> Existing deckuse workspace
61
- [selector] Element selector (default: *)
62
-
63
- Options:
64
- --limit <n> Maximum results to return (default: 100)
65
-
66
- Example:
67
- deckuse query ./presentation 'slide[*] text' --limit 20
68
- `,
69
- apply: `usage: deckuse apply <workspace> [--input <file|->] [--json]
70
-
71
- Apply one or more JSON command objects to a workspace. Input may be a JSON
72
- object, a JSON array, or newline-delimited JSON. Commands read from standard
73
- input by default.
74
-
75
- Arguments:
76
- <workspace> Existing deckuse workspace
77
-
78
- Options:
79
- --input <file|-> Command input file, or - for standard input (default: -)
80
-
81
- Examples:
82
- deckuse apply ./presentation --input changes.json
83
- printf '%s\n' '{"type":"add", ...}' | deckuse apply ./presentation
84
- `,
85
- validate: `usage: deckuse validate <workspace> [--level <level>] [--json]
86
-
87
- Validate workspace changes before export.
88
-
89
- Arguments:
90
- <workspace> Existing deckuse workspace
91
-
92
- Options:
93
- --level <level> Validation level (default: full)
94
-
95
- Example:
96
- deckuse validate ./presentation --level full
97
- `,
98
- commit: `usage: deckuse commit <workspace> [-o <output>] [--force] [--json]
99
-
100
- Export workspace changes to an Office document.
101
-
102
- Arguments:
103
- <workspace> Existing deckuse workspace
104
-
105
- Options:
106
- -o, --output <output>
107
- Destination document path
108
- -f, --force Overwrite an existing destination
109
-
110
- Example:
111
- deckuse commit ./presentation -o updated.pptx
112
- `,
12
+ const takeFlag = (list, name) => {
13
+ const i = list.indexOf(name);
14
+ if (i < 0)
15
+ return false;
16
+ list.splice(i, 1);
17
+ return true;
113
18
  };
114
- const writeHelp = (command = 'main') => {
115
- process.stdout.write(HELP[command]);
19
+ const takeOption = (list, name) => {
20
+ const i = list.indexOf(name);
21
+ if (i < 0)
22
+ return undefined;
23
+ const value = list[i + 1];
24
+ list.splice(i, 2);
25
+ return value;
26
+ };
27
+ const json = takeFlag(args, '--json');
28
+ const quiet = takeFlag(args, '--quiet');
29
+ const dryRun = takeFlag(args, '--dry-run');
30
+ const workspaceOpt = takeOption(args, '--workspace');
31
+ const revisionOpt = takeOption(args, '--revision');
32
+ const expectRevisionOpt = takeOption(args, '--expect-revision');
33
+ const reasonOpt = takeOption(args, '--reason');
34
+ const clean = args;
35
+ const findWorkspace = async (start) => {
36
+ if (start)
37
+ return resolve(start);
38
+ let dir = resolve(process.cwd());
39
+ for (;;) {
40
+ try {
41
+ await access(join(dir, '.deckuse', 'manifest.json'));
42
+ return dir;
43
+ }
44
+ catch {
45
+ const parent = dirname(dir);
46
+ if (parent === dir)
47
+ throw new Error('No deckuse workspace found; pass --workspace <path>');
48
+ dir = parent;
49
+ }
50
+ }
51
+ };
52
+ const revisionFor = async (workspace) => {
53
+ const manifest = JSON.parse(await readFile(resolve(workspace, '.deckuse', 'manifest.json'), 'utf8'));
54
+ return manifest.revision;
116
55
  };
117
- const isHelpFlag = (value) => value === '--help' || value === '-h';
118
- const isVersionFlag = (value) => value === '--version' || value === '-v' || value === '-V';
119
- const isHelpCommand = (value) => value in HELP && value !== 'main';
120
56
  const readStdin = async () => {
121
57
  let value = '';
122
58
  process.stdin.setEncoding('utf8');
@@ -124,137 +60,631 @@ const readStdin = async () => {
124
60
  value += String(chunk);
125
61
  return value;
126
62
  };
127
- const option = (name) => {
128
- const i = clean.indexOf(name);
129
- return i >= 0 ? clean[i + 1] : undefined;
130
- };
131
- const output = (result) => {
132
- process.stdout.write(`${JSON.stringify(result, null, json ? 0 : 2)}\n`);
133
- };
134
- const execute = async (command) => {
135
- const result = await runCommand(command);
136
- output(result);
137
- return result.ok;
63
+ const mutationExtras = async (workspace) => {
64
+ const transactionId = expectRevisionOpt ?? (await revisionFor(workspace));
65
+ return {
66
+ version: PROTOCOL_VERSION,
67
+ workspaceId: workspace,
68
+ transactionId,
69
+ ...(dryRun ? { dryRun: true } : {}),
70
+ ...(expectRevisionOpt ? { expectRevision: expectRevisionOpt } : {}),
71
+ ...(reasonOpt ? { reason: reasonOpt } : {}),
72
+ };
138
73
  };
139
- const revisionFor = async (workspace) => {
140
- const manifest = JSON.parse(await readFile(resolve(workspace, '.deckuse', 'manifest.json'), 'utf8'));
141
- return manifest.revision;
74
+ const toEnvelope = (commandLabel, result, extras = {}) => {
75
+ if (!result.ok) {
76
+ return {
77
+ ok: false,
78
+ command: commandLabel,
79
+ warnings: result.diagnostics
80
+ .filter((d) => d.severity === 'warning')
81
+ .map((d) => d.message),
82
+ error: {
83
+ code: result.error.code,
84
+ message: result.error.message,
85
+ ...(result.error.target ? { target: result.error.target } : {}),
86
+ ...(result.error.hint ? { hint: result.error.hint } : {}),
87
+ },
88
+ ...extras,
89
+ };
90
+ }
91
+ const value = result.value;
92
+ const revision = revisionAsNumber(value['revision']) ??
93
+ (typeof value['revision'] === 'number' ? value['revision'] : undefined);
94
+ return {
95
+ ok: true,
96
+ command: commandLabel,
97
+ ...(revision !== undefined ? { revision } : {}),
98
+ branch: typeof value['branch'] === 'string' ? value['branch'] : 'main',
99
+ ...(Array.isArray(value['affectedSlides'])
100
+ ? { affectedSlides: value['affectedSlides'] }
101
+ : {}),
102
+ ...(Array.isArray(value['changedTargets'])
103
+ ? { changedTargets: value['changedTargets'] }
104
+ : {}),
105
+ ...(Array.isArray(value['changedParts'])
106
+ ? { changedParts: value['changedParts'] }
107
+ : {}),
108
+ warnings: [
109
+ ...result.diagnostics.filter((d) => d.severity === 'warning').map((d) => d.message),
110
+ ...(Array.isArray(value['warnings']) ? value['warnings'] : []),
111
+ ],
112
+ data: value,
113
+ ...extras,
114
+ };
142
115
  };
143
- try {
144
- if (clean.length === 0) {
145
- const payload = await readStdin();
146
- const ok = await execute(JSON.parse(payload));
147
- if (!ok)
148
- process.exitCode = 1;
116
+ const outputEnvelope = (envelope) => {
117
+ if (json) {
118
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
119
+ return;
149
120
  }
150
- else if (isHelpFlag(clean[0])) {
151
- writeHelp();
121
+ if (quiet)
122
+ return;
123
+ if (!envelope.ok) {
124
+ process.stderr.write(`deckuse: ${envelope.error?.code ?? 'ERROR'}: ${envelope.error?.message ?? 'failed'}\n`);
125
+ if (envelope.error?.hint)
126
+ process.stderr.write(`hint: ${envelope.error.hint}\n`);
127
+ return;
152
128
  }
153
- else if (isVersionFlag(clean[0])) {
154
- process.stdout.write(`deckuse ${version}\n`);
129
+ process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
130
+ };
131
+ const execute = async (commandLabel, command) => {
132
+ if (revisionOpt && command && typeof command === 'object' && command !== null) {
133
+ const type = command.type;
134
+ if (type &&
135
+ ![
136
+ 'status',
137
+ 'list',
138
+ 'get',
139
+ 'inspect',
140
+ 'search',
141
+ 'query',
142
+ 'getText',
143
+ 'validate',
144
+ 'history',
145
+ 'export',
146
+ ].includes(type)) {
147
+ outputEnvelope({
148
+ ok: false,
149
+ command: commandLabel,
150
+ error: {
151
+ code: 'INVALID_COMMAND',
152
+ message: 'Write commands reject --revision; omit it or use a read command',
153
+ },
154
+ });
155
+ return false;
156
+ }
155
157
  }
156
- else if (clean[0] === 'help') {
157
- const command = clean[1];
158
- if (!command)
159
- writeHelp();
160
- else if (isHelpCommand(command))
161
- writeHelp(command);
162
- else
163
- throw new Error(`Unknown command: ${command}`);
158
+ const result = await runCommand(command);
159
+ outputEnvelope(toEnvelope(commandLabel, result));
160
+ return result.ok;
161
+ };
162
+ const parseProps = (list) => {
163
+ const props = {};
164
+ for (let i = 0; i < list.length;) {
165
+ const token = list[i];
166
+ if (!token.startsWith('--')) {
167
+ i += 1;
168
+ continue;
169
+ }
170
+ const key = token.slice(2);
171
+ const next = list[i + 1];
172
+ if (next === undefined || next.startsWith('--')) {
173
+ props[key] = true;
174
+ i += 1;
175
+ continue;
176
+ }
177
+ let value = next;
178
+ if (next === 'true')
179
+ value = true;
180
+ else if (next === 'false')
181
+ value = false;
182
+ else if (/^-?\d+(\.\d+)?$/.test(next))
183
+ value = Number(next);
184
+ else if ((next.startsWith('{') && next.endsWith('}')) ||
185
+ (next.startsWith('[') && next.endsWith(']'))) {
186
+ try {
187
+ value = JSON.parse(next);
188
+ }
189
+ catch {
190
+ value = next;
191
+ }
192
+ }
193
+ props[key] = value;
194
+ i += 2;
164
195
  }
165
- else {
166
- const action = clean[0];
167
- let ok = true;
168
- if (!action || !isHelpCommand(action))
169
- throw new Error(`Unknown command: ${action ?? '(missing)'}`);
170
- if (isHelpFlag(clean[1])) {
171
- writeHelp(action);
196
+ return props;
197
+ };
198
+ const optionFrom = (list, name) => {
199
+ const i = list.indexOf(name);
200
+ return i >= 0 ? list[i + 1] : undefined;
201
+ };
202
+ const main = async () => {
203
+ try {
204
+ if (clean.length === 0) {
205
+ const payload = await readStdin();
206
+ const parsed = JSON.parse(payload);
207
+ const ok = await execute('stdin', parsed);
208
+ if (!ok)
209
+ process.exitCode = 1;
172
210
  }
173
- else if (action === 'init') {
174
- const source = clean[1], workspace = clean[2];
175
- if (!source || !workspace)
176
- throw new Error('Usage: deckuse init input.pptx workspace/');
177
- ok = await execute({
178
- version: '1.0',
179
- type: 'init',
180
- workspaceId: workspace,
181
- format: extname(source).slice(1).toLowerCase(),
182
- source,
183
- });
211
+ else if (clean[0] === '--version' || clean[0] === '-v' || clean[0] === '-V') {
212
+ process.stdout.write(`deckuse ${version} (edition=${EDITION})\n`);
184
213
  }
185
- else if (action === 'inspect' || action === 'query' || action === 'validate') {
186
- const workspace = clean[1];
187
- if (!workspace)
188
- throw new Error(`Usage: deckuse ${action} workspace/`);
189
- ok = await execute(action === 'query'
190
- ? {
191
- version: '1.0',
192
- type: 'query',
214
+ else {
215
+ const helpTopic = helpTopicFromArgs(clean);
216
+ if (helpTopic !== null) {
217
+ process.stdout.write(resolveHelp(helpTopic));
218
+ return;
219
+ }
220
+ const action = clean[0];
221
+ let ok = true;
222
+ if (action === 'init') {
223
+ const source = clean[1];
224
+ const workspace = workspaceOpt ?? clean[2];
225
+ if (!source || !workspace)
226
+ throw new Error('Usage: deckuse init <input.pptx> <workspace/>');
227
+ ok = await execute('deckuse init', {
228
+ version: PROTOCOL_VERSION,
229
+ type: 'init',
230
+ workspaceId: resolve(workspace),
231
+ format: extname(source).slice(1).toLowerCase(),
232
+ source: resolve(source),
233
+ });
234
+ }
235
+ else if (action === 'status') {
236
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
237
+ ok = await execute('deckuse status', {
238
+ version: PROTOCOL_VERSION,
239
+ type: 'status',
193
240
  workspaceId: workspace,
194
- selector: clean[2] ?? '*',
195
- limit: Number(option('--limit') ?? 100),
241
+ });
242
+ }
243
+ else if (action === 'list') {
244
+ const resource = clean[1];
245
+ if (!resource)
246
+ throw new Error('Usage: deckuse list <slides|shapes|layouts|masters|theme>');
247
+ const workspace = await findWorkspace(workspaceOpt);
248
+ const slide = optionFrom(clean, '--slide');
249
+ ok = await execute(`deckuse list ${resource}`, {
250
+ version: PROTOCOL_VERSION,
251
+ type: 'list',
252
+ workspaceId: workspace,
253
+ resource,
254
+ ...(slide ? { slide: Number(slide) } : {}),
255
+ });
256
+ }
257
+ else if (action === 'get') {
258
+ const target = clean[1];
259
+ if (!target)
260
+ throw new Error('Usage: deckuse get <target> [--resolve both] [--json]');
261
+ const workspace = await findWorkspace(workspaceOpt);
262
+ const resolveMode = optionFrom(clean, '--resolve') ?? 'both';
263
+ const propsRaw = optionFrom(clean, '--props');
264
+ ok = await execute('deckuse get', {
265
+ version: PROTOCOL_VERSION,
266
+ type: 'get',
267
+ workspaceId: workspace,
268
+ target,
269
+ resolve: resolveMode,
270
+ provenance: !clean.includes('--no-provenance'),
271
+ ...(propsRaw ? { props: propsRaw.split(',').map((p) => p.trim()) } : {}),
272
+ });
273
+ }
274
+ else if (action === 'inspect') {
275
+ const target = clean[1];
276
+ const workspace = await findWorkspace(workspaceOpt);
277
+ ok = await execute('deckuse inspect', {
278
+ version: PROTOCOL_VERSION,
279
+ type: 'inspect',
280
+ workspaceId: workspace,
281
+ ...(target && !target.startsWith('--') ? { target } : {}),
282
+ visualTree: clean.includes('--visual-tree'),
283
+ depth: Number(optionFrom(clean, '--depth') ?? 2),
284
+ });
285
+ }
286
+ else if (action === 'search') {
287
+ const kind = clean[1];
288
+ if (kind !== 'text' && kind !== 'shape')
289
+ throw new Error('Usage: deckuse search <text|shape> ...');
290
+ const workspace = await findWorkspace(workspaceOpt);
291
+ const query = kind === 'text' ? clean[2] : optionFrom(clean, '--query');
292
+ ok = await execute('deckuse search', {
293
+ version: PROTOCOL_VERSION,
294
+ type: 'search',
295
+ workspaceId: workspace,
296
+ kind,
297
+ ...(query ? { query } : {}),
298
+ ...(optionFrom(clean, '--name') ? { name: optionFrom(clean, '--name') } : {}),
299
+ limit: Number(optionFrom(clean, '--limit') ?? 100),
300
+ });
301
+ }
302
+ else if (action === 'add') {
303
+ const what = clean[1];
304
+ const workspace = await findWorkspace(workspaceOpt);
305
+ const base = await mutationExtras(workspace);
306
+ if (what === 'slide') {
307
+ ok = await execute('deckuse add slide', {
308
+ ...base,
309
+ type: 'addSlide',
310
+ ...(optionFrom(clean, '--after') ? { after: Number(optionFrom(clean, '--after')) } : {}),
311
+ ...(optionFrom(clean, '--layout') ? { layout: optionFrom(clean, '--layout') } : {}),
312
+ ...(optionFrom(clean, '--name') ? { name: optionFrom(clean, '--name') } : {}),
313
+ });
314
+ }
315
+ else if (what === 'shape') {
316
+ const slide = optionFrom(clean, '--slide');
317
+ const shapeType = optionFrom(clean, '--type');
318
+ if (!slide || !shapeType)
319
+ throw new Error('Usage: deckuse add shape --slide <n> --type <text|rect|rounded-rect|ellipse|line|image|group|table|chart|video|audio>');
320
+ const rowsRaw = optionFrom(clean, '--rows');
321
+ let rows;
322
+ if (rowsRaw !== undefined) {
323
+ try {
324
+ const parsed = JSON.parse(rowsRaw);
325
+ if (!Array.isArray(parsed) ||
326
+ !parsed.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string'))) {
327
+ throw new Error('expected string[][]');
328
+ }
329
+ rows = parsed;
330
+ }
331
+ catch {
332
+ throw new Error(`--rows must be JSON string[][] (example: --rows '[["A","B"],["1","2"]]')`);
333
+ }
334
+ }
335
+ const dataRaw = optionFrom(clean, '--data');
336
+ let data;
337
+ if (dataRaw !== undefined) {
338
+ try {
339
+ data = JSON.parse(dataRaw);
340
+ }
341
+ catch {
342
+ throw new Error(`--data must be JSON (example: --data '{"categories":["Q1"],"series":[{"name":"S1","values":[1]}]}')`);
343
+ }
344
+ }
345
+ const chartType = optionFrom(clean, '--chart-type');
346
+ ok = await execute('deckuse add shape', {
347
+ ...base,
348
+ type: 'addShape',
349
+ slide: Number(slide),
350
+ shapeType,
351
+ ...(optionFrom(clean, '--name') ? { name: optionFrom(clean, '--name') } : {}),
352
+ ...(optionFrom(clean, '--role') ? { role: optionFrom(clean, '--role') } : {}),
353
+ ...(optionFrom(clean, '--x') ? { x: Number(optionFrom(clean, '--x')) } : {}),
354
+ ...(optionFrom(clean, '--y') ? { y: Number(optionFrom(clean, '--y')) } : {}),
355
+ ...(optionFrom(clean, '--width') ? { width: Number(optionFrom(clean, '--width')) } : {}),
356
+ ...(optionFrom(clean, '--height')
357
+ ? { height: Number(optionFrom(clean, '--height')) }
358
+ : {}),
359
+ ...(optionFrom(clean, '--file') ? { file: optionFrom(clean, '--file') } : {}),
360
+ ...(optionFrom(clean, '--text') !== undefined
361
+ ? { text: optionFrom(clean, '--text') }
362
+ : {}),
363
+ ...(rows !== undefined ? { rows } : {}),
364
+ ...(chartType ? { chartType } : {}),
365
+ ...(data !== undefined ? { data } : {}),
366
+ });
367
+ }
368
+ else
369
+ throw new Error('Usage: deckuse add <slide|shape> ...');
370
+ }
371
+ else if (action === 'remove') {
372
+ const target = clean[1];
373
+ if (!target)
374
+ throw new Error('Usage: deckuse remove <target>');
375
+ const workspace = await findWorkspace(workspaceOpt);
376
+ ok = await execute('deckuse remove', {
377
+ ...(await mutationExtras(workspace)),
378
+ type: 'remove',
379
+ target,
380
+ });
381
+ }
382
+ else if (action === 'set') {
383
+ const workspace = await findWorkspace(workspaceOpt);
384
+ const base = await mutationExtras(workspace);
385
+ if (clean[1] === 'text') {
386
+ const target = clean[2];
387
+ const value = optionFrom(clean, '--value');
388
+ if (!target || value === undefined)
389
+ throw new Error('Usage: deckuse set text <target> --value <text>');
390
+ ok = await execute('deckuse set text', {
391
+ ...base,
392
+ type: 'setText',
393
+ target,
394
+ value,
395
+ });
396
+ }
397
+ else {
398
+ const target = clean[1];
399
+ if (!target)
400
+ throw new Error('Usage: deckuse set <target> --font.size 42 ...');
401
+ const props = parseProps(clean.slice(2));
402
+ delete props['scope'];
403
+ const scope = optionFrom(clean, '--scope') ?? 'local';
404
+ // remove flag keys that are not properties
405
+ for (const key of Object.keys(props)) {
406
+ if (['expect-revision', 'reason', 'dry-run', 'workspace', 'json', 'quiet'].includes(key))
407
+ delete props[key];
408
+ }
409
+ ok = await execute('deckuse set', {
410
+ ...base,
411
+ type: 'set',
412
+ target,
413
+ properties: props,
414
+ scope,
415
+ });
416
+ }
417
+ }
418
+ else if (action === 'replace-text') {
419
+ const source = optionFrom(clean, '--source');
420
+ const target = optionFrom(clean, '--target');
421
+ if (source === undefined || target === undefined)
422
+ throw new Error('Usage: deckuse replace-text --source <text> --target <text> [--regex] [--limit <n>] [--selector <sel>]');
423
+ if (!source)
424
+ throw new Error('replace-text --source must be a non-empty string');
425
+ const workspace = await findWorkspace(workspaceOpt);
426
+ ok = await execute('deckuse replace-text', {
427
+ ...(await mutationExtras(workspace)),
428
+ type: 'replaceText',
429
+ find: source,
430
+ replace: target,
431
+ ...(clean.includes('--regex') ? { regex: true } : {}),
432
+ ...(optionFrom(clean, '--limit')
433
+ ? { limit: Number(optionFrom(clean, '--limit')) }
434
+ : {}),
435
+ ...(optionFrom(clean, '--selector')
436
+ ? { selector: optionFrom(clean, '--selector') }
437
+ : {}),
438
+ });
439
+ }
440
+ else if (action === 'xfrm') {
441
+ if (clean[1] !== 'set')
442
+ throw new Error('Usage: deckuse xfrm set --slide <n> --shape <id> ...');
443
+ const workspace = await findWorkspace(workspaceOpt);
444
+ const slide = optionFrom(clean, '--slide');
445
+ const shape = optionFrom(clean, '--shape');
446
+ const target = optionFrom(clean, '--target') ??
447
+ (slide && shape ? `slide:${slide}/shape:${shape}` : undefined);
448
+ if (!target)
449
+ throw new Error('xfrm set requires --target or --slide and --shape');
450
+ ok = await execute('deckuse xfrm set', {
451
+ ...(await mutationExtras(workspace)),
452
+ type: 'xfrmSet',
453
+ target,
454
+ ...(optionFrom(clean, '--x') ? { x: Number(optionFrom(clean, '--x')) } : {}),
455
+ ...(optionFrom(clean, '--y') ? { y: Number(optionFrom(clean, '--y')) } : {}),
456
+ ...(optionFrom(clean, '--width') || optionFrom(clean, '--cx')
457
+ ? { width: Number(optionFrom(clean, '--width') ?? optionFrom(clean, '--cx')) }
458
+ : {}),
459
+ ...(optionFrom(clean, '--height') || optionFrom(clean, '--cy')
460
+ ? { height: Number(optionFrom(clean, '--height') ?? optionFrom(clean, '--cy')) }
461
+ : {}),
462
+ ...(optionFrom(clean, '--rotation')
463
+ ? { rotation: Number(optionFrom(clean, '--rotation')) }
464
+ : {}),
465
+ });
466
+ }
467
+ else if (action === 'z') {
468
+ if (clean[1] !== 'move')
469
+ throw new Error('Usage: deckuse z move <target> --above <target>');
470
+ const target = clean[2];
471
+ if (!target)
472
+ throw new Error('Usage: deckuse z move <target> ...');
473
+ const workspace = await findWorkspace(workspaceOpt);
474
+ ok = await execute('deckuse z move', {
475
+ ...(await mutationExtras(workspace)),
476
+ type: 'zMove',
477
+ target,
478
+ ...(optionFrom(clean, '--above') ? { above: optionFrom(clean, '--above') } : {}),
479
+ ...(optionFrom(clean, '--below') ? { below: optionFrom(clean, '--below') } : {}),
480
+ ...(clean.includes('--to-front') ? { toFront: true } : {}),
481
+ ...(clean.includes('--to-back') ? { toBack: true } : {}),
482
+ });
483
+ }
484
+ else if (action === 'apply') {
485
+ const input = optionFrom(clean, '--input') ?? '-';
486
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
487
+ const raw = input === '-' ? await readStdin() : await readFile(resolve(input), 'utf8');
488
+ let values;
489
+ let transactionOps;
490
+ try {
491
+ const parsed = JSON.parse(raw);
492
+ if (typeof parsed === 'object' &&
493
+ parsed !== null &&
494
+ Array.isArray(parsed.operations)) {
495
+ transactionOps = parsed.operations;
496
+ }
497
+ else {
498
+ values = Array.isArray(parsed) ? parsed : [parsed];
499
+ }
500
+ }
501
+ catch {
502
+ values = raw
503
+ .split(/\r?\n/)
504
+ .filter(Boolean)
505
+ .map((line) => JSON.parse(line));
506
+ }
507
+ if (transactionOps) {
508
+ ok = await execute('deckuse apply', {
509
+ ...(await mutationExtras(workspace)),
510
+ type: 'applyTransaction',
511
+ operations: transactionOps,
512
+ });
196
513
  }
197
- : {
198
- version: '1.0',
199
- type: action,
514
+ else {
515
+ const extras = await mutationExtras(workspace);
516
+ const WRITE = new Set([
517
+ 'setText',
518
+ 'replaceText',
519
+ 'setTransform',
520
+ 'setProperties',
521
+ 'set',
522
+ 'xfrmSet',
523
+ 'zMove',
524
+ 'add',
525
+ 'addSlide',
526
+ 'addShape',
527
+ 'remove',
528
+ 'replacePicture',
529
+ 'duplicate',
530
+ 'batch',
531
+ ]);
532
+ const list = values ?? [];
533
+ if (list.length > 0 &&
534
+ list.every((value) => typeof value === 'object' &&
535
+ value !== null &&
536
+ 'op' in value)) {
537
+ ok = await execute('deckuse apply', {
538
+ ...extras,
539
+ type: 'applyTransaction',
540
+ operations: list,
541
+ });
542
+ }
543
+ else {
544
+ const commands = list.map((value) => {
545
+ if (typeof value !== 'object' || value === null)
546
+ throw new Error('apply input must contain command objects');
547
+ const record = value;
548
+ if (record['type'] === 'batch')
549
+ return record;
550
+ if (!WRITE.has(String(record['type'])))
551
+ throw new Error(`apply input contains non-write command: ${String(record['type'])}`);
552
+ return { ...extras, ...record };
553
+ });
554
+ const payload = commands.length === 1 && commands[0]?.['type'] !== 'batch'
555
+ ? commands[0]
556
+ : {
557
+ ...extras,
558
+ type: 'batch',
559
+ atomic: true,
560
+ commands: commands.flatMap((command) => command['type'] === 'batch'
561
+ ? command.commands
562
+ : [command]),
563
+ };
564
+ ok = await execute('deckuse apply', payload);
565
+ }
566
+ }
567
+ }
568
+ else if (action === 'validate') {
569
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
570
+ ok = await execute('deckuse validate', {
571
+ version: PROTOCOL_VERSION,
572
+ type: 'validate',
200
573
  workspaceId: workspace,
201
- ...(action === 'inspect'
202
- ? { depth: Number(option('--depth') ?? 2) }
203
- : { level: option('--level') ?? 'full' }),
574
+ level: optionFrom(clean, '--level') ?? 'full',
575
+ package: clean.includes('--package'),
576
+ relationships: clean.includes('--relationships'),
577
+ ...(optionFrom(clean, '--slide') ? { slide: Number(optionFrom(clean, '--slide')) } : {}),
204
578
  });
205
- }
206
- else if (action === 'commit') {
207
- const workspace = clean[1];
208
- if (!workspace)
209
- throw new Error('Usage: deckuse commit workspace/ -o output.pptx [--force]');
210
- const force = clean.includes('--force') || clean.includes('-f');
211
- ok = await execute({
212
- version: '1.0',
213
- type: 'commit',
214
- workspaceId: workspace,
215
- transactionId: await revisionFor(workspace),
216
- ...(option('-o') || option('--output')
217
- ? { destination: option('-o') ?? option('--output') }
218
- : {}),
219
- ...(force ? { overwrite: true } : {}),
220
- });
221
- }
222
- else {
223
- const workspace = clean[1], input = option('--input') ?? '-';
224
- if (!workspace)
225
- throw new Error('Usage: deckuse apply workspace/ --input file|-');
226
- const raw = input === '-' ? await readStdin() : await readFile(resolve(input), 'utf8');
227
- let values;
228
- try {
229
- const parsed = JSON.parse(raw);
230
- values = Array.isArray(parsed) ? parsed : [parsed];
231
579
  }
232
- catch {
233
- values = raw
234
- .split(/\r?\n/)
235
- .filter(Boolean)
236
- .map((line) => JSON.parse(line));
580
+ else if (action === 'history') {
581
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
582
+ ok = await execute('deckuse history', {
583
+ version: PROTOCOL_VERSION,
584
+ type: 'history',
585
+ workspaceId: workspace,
586
+ limit: Number(optionFrom(clean, '--limit') ?? 100),
587
+ offset: Number(optionFrom(clean, '--offset') ?? 0),
588
+ ...(optionFrom(clean, '--slide') ? { slide: Number(optionFrom(clean, '--slide')) } : {}),
589
+ });
590
+ }
591
+ else if (action === 'undo') {
592
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
593
+ ok = await execute('deckuse undo', {
594
+ version: PROTOCOL_VERSION,
595
+ type: 'undo',
596
+ workspaceId: workspace,
597
+ steps: Number(optionFrom(clean, '--steps') ?? 1),
598
+ });
237
599
  }
238
- for (const value of values) {
239
- if (typeof value !== 'object' || value === null)
240
- throw new Error('apply input must contain command objects');
241
- const command = {
242
- version: '1.0',
600
+ else if (action === 'export') {
601
+ const output = clean[1];
602
+ if (!output)
603
+ throw new Error('Usage: deckuse export <output.pptx>');
604
+ const workspace = await findWorkspace(workspaceOpt);
605
+ ok = await execute('deckuse export', {
606
+ version: PROTOCOL_VERSION,
607
+ type: 'export',
243
608
  workspaceId: workspace,
244
- transactionId: await revisionFor(workspace),
245
- ...value,
609
+ output: resolve(output),
610
+ ...(revisionOpt ? { revision: revisionOpt } : {}),
611
+ });
612
+ }
613
+ else if (action === 'monitor') {
614
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
615
+ const port = Number(optionFrom(clean, '--port') ?? 4173);
616
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
617
+ throw new Error('--port must be an integer between 0 and 65535');
618
+ const monitor = await startMonitor(workspace, {
619
+ host: optionFrom(clean, '--host') ?? '0.0.0.0',
620
+ port,
621
+ });
622
+ process.stdout.write(`Deckuse monitor: ${monitor.url}\n`);
623
+ const shutdown = async (signal) => {
624
+ process.stderr.write(`deckuse monitor: received ${signal}, shutting down\n`);
625
+ await monitor.close();
626
+ process.exit(0);
246
627
  };
247
- ok = (await execute(command)) && ok;
248
- if (!ok)
249
- break;
628
+ process.once('SIGINT', () => void shutdown('SIGINT'));
629
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
630
+ return;
631
+ }
632
+ else if (action === 'render') {
633
+ const pageRaw = optionFrom(clean, '--page');
634
+ if (!pageRaw)
635
+ throw new Error('Usage: deckuse render --page <n> [--output <file.png>]');
636
+ if (/\s|,|-/.test(pageRaw))
637
+ throw new Error('--page accepts exactly one positive integer (not a range or list)');
638
+ const page = Number(pageRaw);
639
+ if (!Number.isInteger(page) || page < 1)
640
+ throw new Error('--page must be a positive integer');
641
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
642
+ const outputOpt = optionFrom(clean, '--output');
643
+ try {
644
+ const rendered = await renderPage(workspace, {
645
+ page,
646
+ ...(outputOpt ? { output: resolve(outputOpt) } : {}),
647
+ });
648
+ outputEnvelope({
649
+ ok: true,
650
+ command: 'deckuse render',
651
+ data: { page: rendered.page, output: rendered.output },
652
+ });
653
+ }
654
+ catch (error) {
655
+ outputEnvelope({
656
+ ok: false,
657
+ command: 'deckuse render',
658
+ error: {
659
+ code: 'RENDER_FAILED',
660
+ message: error instanceof Error ? error.message : 'Render failed',
661
+ },
662
+ });
663
+ ok = false;
664
+ }
665
+ }
666
+ else if (action === 'query') {
667
+ // Back-compat shim
668
+ const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
669
+ ok = await execute('deckuse query', {
670
+ version: PROTOCOL_VERSION,
671
+ type: 'query',
672
+ workspaceId: workspace,
673
+ selector: clean[2] ?? '*',
674
+ limit: Number(optionFrom(clean, '--limit') ?? 100),
675
+ });
250
676
  }
677
+ else {
678
+ throw new Error(`Unknown command: ${action}`);
679
+ }
680
+ if (!ok)
681
+ process.exitCode = 1;
251
682
  }
252
- if (!ok)
253
- process.exitCode = 1;
254
683
  }
255
- }
256
- catch (cause) {
257
- process.stderr.write(`deckuse: ${cause instanceof Error ? cause.message : 'Invalid input'}\n`);
258
- process.exitCode = 2;
259
- }
684
+ catch (cause) {
685
+ process.stderr.write(`deckuse: ${cause instanceof Error ? cause.message : 'Invalid input'}\n`);
686
+ process.exitCode = 2;
687
+ }
688
+ };
689
+ void main();
260
690
  //# sourceMappingURL=bin.js.map