@deckflow/deckuse 1.1.0 → 1.2.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
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { access, readFile } from 'node:fs/promises';
3
3
  import { dirname, extname, join, resolve } from 'node:path';
4
- import { PROTOCOL_VERSION, revisionAsNumber, } from '@deckflow/deckuse-core';
4
+ import { PROTOCOL_VERSION, commandJsonSchema, measureText, parseLength, revisionAsNumber, } from '@deckflow/deckuse-core';
5
+ import { resolveCliText } from './cli-text.js';
5
6
  import { helpTopicFromArgs, resolveHelp } from './help.js';
6
7
  import { runCommand } from './index.js';
7
- import { startMonitor } from './monitor.js';
8
+ import { monitorStart, monitorStatus, monitorStop, runMonitorForeground, } from './monitor-daemon.js';
8
9
  import { renderPage } from './render.js';
9
10
  import { EDITION } from './edition.js';
10
11
  import { version } from './version.js';
@@ -84,6 +85,11 @@ const toEnvelope = (commandLabel, result, extras = {}) => {
84
85
  message: result.error.message,
85
86
  ...(result.error.target ? { target: result.error.target } : {}),
86
87
  ...(result.error.hint ? { hint: result.error.hint } : {}),
88
+ ...(result.error.diagnostics?.length
89
+ ? { diagnostics: result.error.diagnostics }
90
+ : result.diagnostics.length
91
+ ? { diagnostics: result.diagnostics }
92
+ : {}),
87
93
  },
88
94
  ...extras,
89
95
  };
@@ -124,6 +130,13 @@ const outputEnvelope = (envelope) => {
124
130
  process.stderr.write(`deckuse: ${envelope.error?.code ?? 'ERROR'}: ${envelope.error?.message ?? 'failed'}\n`);
125
131
  if (envelope.error?.hint)
126
132
  process.stderr.write(`hint: ${envelope.error.hint}\n`);
133
+ const diagnostics = envelope.error?.diagnostics;
134
+ if (Array.isArray(diagnostics)) {
135
+ for (const d of diagnostics) {
136
+ const path = Array.isArray(d.path) && d.path.length > 0 ? `${d.path.map(String).join('.')}: ` : '';
137
+ process.stderr.write(` - ${path}${d.message}\n`);
138
+ }
139
+ }
127
140
  return;
128
141
  }
129
142
  process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
@@ -199,6 +212,63 @@ const optionFrom = (list, name) => {
199
212
  const i = list.indexOf(name);
200
213
  return i >= 0 ? list[i + 1] : undefined;
201
214
  };
215
+ /** Pass through bare numbers as number; keep unit strings for protocol parseLength. */
216
+ const lengthArg = (raw) => {
217
+ if (raw === undefined)
218
+ return undefined;
219
+ if (raw === 'auto')
220
+ return 'auto';
221
+ if (/^-?\d+(\.\d+)?$/.test(raw))
222
+ return Number(raw);
223
+ return raw;
224
+ };
225
+ const APPLY_WRITE_TYPES = new Set([
226
+ 'setText',
227
+ 'replaceText',
228
+ 'setTransform',
229
+ 'setProperties',
230
+ 'set',
231
+ 'xfrmSet',
232
+ 'zMove',
233
+ 'add',
234
+ 'addSlide',
235
+ 'addShape',
236
+ 'remove',
237
+ 'replacePicture',
238
+ 'duplicate',
239
+ 'alignElements',
240
+ 'batch',
241
+ ]);
242
+ const isRecord = (value) => typeof value === 'object' && value !== null;
243
+ const isTransactionOp = (value) => isRecord(value) && 'op' in value;
244
+ const isHighLevelWrite = (value) => isRecord(value) && APPLY_WRITE_TYPES.has(String(value['type']));
245
+ /** Classify apply payload items: high-level batch vs low-level transaction. */
246
+ const classifyApplyOps = (ops) => {
247
+ if (ops.length === 0)
248
+ return { kind: 'invalid', message: 'apply input operations array is empty' };
249
+ if (ops.every(isTransactionOp))
250
+ return { kind: 'transaction' };
251
+ if (ops.every(isHighLevelWrite))
252
+ return { kind: 'batch' };
253
+ if (ops.some(isHighLevelWrite) && ops.some(isTransactionOp)) {
254
+ return {
255
+ kind: 'invalid',
256
+ message: 'apply input mixes high-level write commands (type) with transaction ops (op); use one format',
257
+ };
258
+ }
259
+ if (ops.some(isHighLevelWrite))
260
+ return { kind: 'batch' };
261
+ if (ops.some(isTransactionOp)) {
262
+ return {
263
+ kind: 'invalid',
264
+ message: 'apply transaction operations must all include "op"; high-level writes must use "type"',
265
+ };
266
+ }
267
+ return {
268
+ kind: 'invalid',
269
+ message: 'apply input must be high-level write commands ({ "type": "addShape"|... }) or transaction ops ({ "op": ... })',
270
+ };
271
+ };
202
272
  const main = async () => {
203
273
  try {
204
274
  if (clean.length === 0) {
@@ -219,6 +289,89 @@ const main = async () => {
219
289
  }
220
290
  const action = clean[0];
221
291
  let ok = true;
292
+ if (action === 'schema') {
293
+ const typeOpt = optionFrom(clean, '--type') ??
294
+ (clean[1] && !clean[1].startsWith('--') ? clean[1] : undefined);
295
+ const full = commandJsonSchema;
296
+ const defs = full.$defs ?? {};
297
+ const resolveRef = (entry) => {
298
+ if (!entry || typeof entry !== 'object')
299
+ return undefined;
300
+ const record = entry;
301
+ if (typeof record['$ref'] === 'string') {
302
+ const ref = record['$ref'];
303
+ const name = ref.replace(/^#\/\$defs\//, '');
304
+ const resolved = defs[name];
305
+ return resolved && typeof resolved === 'object'
306
+ ? resolved
307
+ : undefined;
308
+ }
309
+ return record;
310
+ };
311
+ const typeOf = (entry) => {
312
+ const resolved = resolveRef(entry);
313
+ const typeProp = resolved?.['properties']
314
+ ? resolved['properties']['type']
315
+ : undefined;
316
+ if (!typeProp || typeof typeProp !== 'object')
317
+ return undefined;
318
+ const typeRecord = typeProp;
319
+ if (typeof typeRecord['const'] === 'string')
320
+ return typeRecord['const'];
321
+ const nested = resolveRef(typeProp);
322
+ return typeof nested?.['const'] === 'string' ? nested['const'] : undefined;
323
+ };
324
+ let schema = full;
325
+ if (typeOpt) {
326
+ const variants = full.oneOf ?? full.anyOf ?? [];
327
+ const match = variants.find((entry) => typeOf(entry) === typeOpt);
328
+ if (!match) {
329
+ const known = variants.map(typeOf).filter(Boolean).join(', ');
330
+ throw new Error(`Unknown command type for schema: ${typeOpt}. Known: ${known || '(none)'}`);
331
+ }
332
+ schema = { ...(resolveRef(match) ?? match), $defs: defs };
333
+ }
334
+ const payload = {
335
+ ok: true,
336
+ command: 'deckuse schema',
337
+ data: {
338
+ cliVersion: version,
339
+ protocolVersion: PROTOCOL_VERSION,
340
+ edition: EDITION,
341
+ ...(typeOpt ? { type: typeOpt } : {}),
342
+ schema,
343
+ },
344
+ };
345
+ process.stdout.write(`${JSON.stringify(payload, null, json ? undefined : 2)}\n`);
346
+ return;
347
+ }
348
+ if (action === 'measure') {
349
+ const text = optionFrom(clean, '--text');
350
+ if (text === undefined)
351
+ throw new Error('Usage: deckuse measure --text <string> --font-size <pt>');
352
+ const fontSize = Number(optionFrom(clean, '--font-size') ?? optionFrom(clean, '--fontSize') ?? 12);
353
+ if (!(fontSize > 0))
354
+ throw new Error('--font-size must be a positive number (pt)');
355
+ const bold = clean.includes('--bold');
356
+ const fontFamily = optionFrom(clean, '--font-family') ?? optionFrom(clean, '--fontFamily');
357
+ const maxWidthRaw = optionFrom(clean, '--max-width') ?? optionFrom(clean, '--maxWidth');
358
+ const maxWidthEmu = maxWidthRaw !== undefined
359
+ ? parseLength(lengthArg(maxWidthRaw) ?? maxWidthRaw, { axis: 'x' })
360
+ : undefined;
361
+ const measured = measureText({
362
+ text,
363
+ fontSize,
364
+ ...(bold ? { bold: true } : {}),
365
+ ...(fontFamily ? { fontFamily } : {}),
366
+ ...(maxWidthEmu !== undefined ? { maxWidthEmu } : {}),
367
+ });
368
+ outputEnvelope({
369
+ ok: true,
370
+ command: 'deckuse measure',
371
+ data: measured,
372
+ });
373
+ return;
374
+ }
222
375
  if (action === 'init') {
223
376
  const source = clean[1];
224
377
  const workspace = workspaceOpt ?? clean[2];
@@ -316,7 +469,7 @@ const main = async () => {
316
469
  const slide = optionFrom(clean, '--slide');
317
470
  const shapeType = optionFrom(clean, '--type');
318
471
  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>');
472
+ throw new Error('Usage: deckuse add shape --slide <n> --type <text|rect|rounded-rect|ellipse|line|connector|elbow|arrow|chevron|circular-arrow|image|group|table|chart|…>');
320
473
  const rowsRaw = optionFrom(clean, '--rows');
321
474
  let rows;
322
475
  if (rowsRaw !== undefined) {
@@ -343,6 +496,25 @@ const main = async () => {
343
496
  }
344
497
  }
345
498
  const chartType = optionFrom(clean, '--chart-type');
499
+ const textRaw = takeFlag(clean, '--text-raw');
500
+ const textFile = optionFrom(clean, '--text-file');
501
+ const textOpt = optionFrom(clean, '--text');
502
+ const initialText = await resolveCliText({
503
+ ...(textOpt !== undefined ? { value: textOpt } : {}),
504
+ ...(textFile !== undefined ? { textFile } : {}),
505
+ raw: textRaw,
506
+ });
507
+ const theme = optionFrom(clean, '--theme');
508
+ const alignColumnsRaw = optionFrom(clean, '--align-columns');
509
+ let alignColumns;
510
+ if (alignColumnsRaw) {
511
+ try {
512
+ alignColumns = JSON.parse(alignColumnsRaw);
513
+ }
514
+ catch {
515
+ alignColumns = alignColumnsRaw.split(',').map((s) => s.trim());
516
+ }
517
+ }
346
518
  ok = await execute('deckuse add shape', {
347
519
  ...base,
348
520
  type: 'addShape',
@@ -350,19 +522,26 @@ const main = async () => {
350
522
  shapeType,
351
523
  ...(optionFrom(clean, '--name') ? { name: optionFrom(clean, '--name') } : {}),
352
524
  ...(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')) }
525
+ ...(lengthArg(optionFrom(clean, '--x')) !== undefined
526
+ ? { x: lengthArg(optionFrom(clean, '--x')) }
358
527
  : {}),
359
- ...(optionFrom(clean, '--file') ? { file: optionFrom(clean, '--file') } : {}),
360
- ...(optionFrom(clean, '--text') !== undefined
361
- ? { text: optionFrom(clean, '--text') }
528
+ ...(lengthArg(optionFrom(clean, '--y')) !== undefined
529
+ ? { y: lengthArg(optionFrom(clean, '--y')) }
530
+ : {}),
531
+ ...(lengthArg(optionFrom(clean, '--width')) !== undefined
532
+ ? { width: lengthArg(optionFrom(clean, '--width')) }
362
533
  : {}),
534
+ ...(lengthArg(optionFrom(clean, '--height')) !== undefined
535
+ ? { height: lengthArg(optionFrom(clean, '--height')) }
536
+ : {}),
537
+ ...(optionFrom(clean, '--file') ? { file: optionFrom(clean, '--file') } : {}),
538
+ ...(initialText !== undefined ? { text: initialText } : {}),
363
539
  ...(rows !== undefined ? { rows } : {}),
540
+ ...(theme ? { theme } : {}),
541
+ ...(alignColumns ? { alignColumns } : {}),
364
542
  ...(chartType ? { chartType } : {}),
365
543
  ...(data !== undefined ? { data } : {}),
544
+ ...(takeFlag(clean, '--show-data-labels') ? { showDataLabels: true } : {}),
366
545
  });
367
546
  }
368
547
  else
@@ -384,15 +563,42 @@ const main = async () => {
384
563
  const base = await mutationExtras(workspace);
385
564
  if (clean[1] === 'text') {
386
565
  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
- });
566
+ const textRaw = takeFlag(clean, '--text-raw');
567
+ const textFile = optionFrom(clean, '--text-file');
568
+ const valueOpt = optionFrom(clean, '--value');
569
+ const blocksRaw = optionFrom(clean, '--blocks');
570
+ if (!target)
571
+ throw new Error('Usage: deckuse set text <target> --value <text> | --text-file <path> | --blocks <json>');
572
+ if (blocksRaw !== undefined) {
573
+ let blocks;
574
+ try {
575
+ blocks = JSON.parse(blocksRaw);
576
+ }
577
+ catch {
578
+ throw new Error('--blocks must be JSON array of {text, fontSize?, textColor?, ...}');
579
+ }
580
+ ok = await execute('deckuse set text', {
581
+ ...base,
582
+ type: 'setText',
583
+ target,
584
+ blocks,
585
+ });
586
+ }
587
+ else {
588
+ const value = await resolveCliText({
589
+ ...(valueOpt !== undefined ? { value: valueOpt } : {}),
590
+ ...(textFile !== undefined ? { textFile } : {}),
591
+ raw: textRaw,
592
+ });
593
+ if (value === undefined)
594
+ throw new Error('Usage: deckuse set text <target> --value <text> | --text-file <path> | --blocks <json>');
595
+ ok = await execute('deckuse set text', {
596
+ ...base,
597
+ type: 'setText',
598
+ target,
599
+ value,
600
+ });
601
+ }
396
602
  }
397
603
  else {
398
604
  const target = clean[1];
@@ -451,19 +657,42 @@ const main = async () => {
451
657
  ...(await mutationExtras(workspace)),
452
658
  type: 'xfrmSet',
453
659
  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')) }
660
+ ...(lengthArg(optionFrom(clean, '--x')) !== undefined
661
+ ? { x: lengthArg(optionFrom(clean, '--x')) }
662
+ : {}),
663
+ ...(lengthArg(optionFrom(clean, '--y')) !== undefined
664
+ ? { y: lengthArg(optionFrom(clean, '--y')) }
665
+ : {}),
666
+ ...(lengthArg(optionFrom(clean, '--width') ?? optionFrom(clean, '--cx')) !== undefined
667
+ ? { width: lengthArg(optionFrom(clean, '--width') ?? optionFrom(clean, '--cx')) }
458
668
  : {}),
459
- ...(optionFrom(clean, '--height') || optionFrom(clean, '--cy')
460
- ? { height: Number(optionFrom(clean, '--height') ?? optionFrom(clean, '--cy')) }
669
+ ...(lengthArg(optionFrom(clean, '--height') ?? optionFrom(clean, '--cy')) !== undefined
670
+ ? { height: lengthArg(optionFrom(clean, '--height') ?? optionFrom(clean, '--cy')) }
461
671
  : {}),
462
672
  ...(optionFrom(clean, '--rotation')
463
673
  ? { rotation: Number(optionFrom(clean, '--rotation')) }
464
674
  : {}),
465
675
  });
466
676
  }
677
+ else if (action === 'align') {
678
+ const workspace = await findWorkspace(workspaceOpt);
679
+ const slide = optionFrom(clean, '--slide');
680
+ const targetsRaw = optionFrom(clean, '--targets');
681
+ const mode = optionFrom(clean, '--mode');
682
+ if (!slide || !targetsRaw || !mode)
683
+ throw new Error('Usage: deckuse align --slide <n> --targets <t1,t2,...> --mode <left|right|top|bottom|center-h|center-v|distribute-h|distribute-v> [--gap <length>]');
684
+ const targets = targetsRaw.split(',').map((t) => t.trim()).filter(Boolean);
685
+ ok = await execute('deckuse align', {
686
+ ...(await mutationExtras(workspace)),
687
+ type: 'alignElements',
688
+ slide: Number(slide),
689
+ targets,
690
+ mode,
691
+ ...(lengthArg(optionFrom(clean, '--gap')) !== undefined
692
+ ? { gap: lengthArg(optionFrom(clean, '--gap')) }
693
+ : {}),
694
+ });
695
+ }
467
696
  else if (action === 'z') {
468
697
  if (clean[1] !== 'move')
469
698
  throw new Error('Usage: deckuse z move <target> --above <target>');
@@ -486,13 +715,13 @@ const main = async () => {
486
715
  const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
487
716
  const raw = input === '-' ? await readStdin() : await readFile(resolve(input), 'utf8');
488
717
  let values;
489
- let transactionOps;
718
+ let wrappedOperations;
490
719
  try {
491
720
  const parsed = JSON.parse(raw);
492
721
  if (typeof parsed === 'object' &&
493
722
  parsed !== null &&
494
723
  Array.isArray(parsed.operations)) {
495
- transactionOps = parsed.operations;
724
+ wrappedOperations = parsed.operations;
496
725
  }
497
726
  else {
498
727
  values = Array.isArray(parsed) ? parsed : [parsed];
@@ -504,36 +733,50 @@ const main = async () => {
504
733
  .filter(Boolean)
505
734
  .map((line) => JSON.parse(line));
506
735
  }
507
- if (transactionOps) {
508
- ok = await execute('deckuse apply', {
509
- ...(await mutationExtras(workspace)),
510
- type: 'applyTransaction',
511
- operations: transactionOps,
736
+ const extras = await mutationExtras(workspace);
737
+ const runHighLevelBatch = async (list) => {
738
+ const commands = list.map((value) => {
739
+ if (!isRecord(value))
740
+ throw new Error('apply input must contain command objects');
741
+ if (value['type'] === 'batch')
742
+ return value;
743
+ if (!APPLY_WRITE_TYPES.has(String(value['type'])))
744
+ throw new Error(`apply input contains non-write command: ${String(value['type'])}`);
745
+ return { ...extras, ...value };
512
746
  });
747
+ const payload = commands.length === 1 && commands[0]?.['type'] !== 'batch'
748
+ ? commands[0]
749
+ : {
750
+ ...extras,
751
+ type: 'batch',
752
+ atomic: true,
753
+ commands: commands.flatMap((command) => command['type'] === 'batch'
754
+ ? (command.commands ?? [])
755
+ : [command]),
756
+ };
757
+ return execute('deckuse apply', payload);
758
+ };
759
+ if (wrappedOperations) {
760
+ const classified = classifyApplyOps(wrappedOperations);
761
+ if (classified.kind === 'invalid')
762
+ throw new Error(classified.message);
763
+ if (classified.kind === 'transaction') {
764
+ ok = await execute('deckuse apply', {
765
+ ...extras,
766
+ type: 'applyTransaction',
767
+ operations: wrappedOperations,
768
+ });
769
+ }
770
+ else {
771
+ ok = await runHighLevelBatch(wrappedOperations);
772
+ }
513
773
  }
514
774
  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
775
  const list = values ?? [];
533
- if (list.length > 0 &&
534
- list.every((value) => typeof value === 'object' &&
535
- value !== null &&
536
- 'op' in value)) {
776
+ const classified = classifyApplyOps(list);
777
+ if (classified.kind === 'invalid')
778
+ throw new Error(classified.message);
779
+ if (classified.kind === 'transaction') {
537
780
  ok = await execute('deckuse apply', {
538
781
  ...extras,
539
782
  type: 'applyTransaction',
@@ -541,27 +784,7 @@ const main = async () => {
541
784
  });
542
785
  }
543
786
  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);
787
+ ok = await runHighLevelBatch(list);
565
788
  }
566
789
  }
567
790
  }
@@ -602,24 +825,78 @@ const main = async () => {
602
825
  if (!output)
603
826
  throw new Error('Usage: deckuse export <output.pptx>');
604
827
  const workspace = await findWorkspace(workspaceOpt);
828
+ const fromPackage = takeFlag(clean, '--from-package');
605
829
  ok = await execute('deckuse export', {
606
830
  version: PROTOCOL_VERSION,
607
831
  type: 'export',
608
832
  workspaceId: workspace,
609
833
  output: resolve(output),
610
834
  ...(revisionOpt ? { revision: revisionOpt } : {}),
835
+ ...(fromPackage ? { fromPackage: true } : {}),
611
836
  });
612
837
  }
613
838
  else if (action === 'monitor') {
614
- const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
839
+ const sub = clean[1] === 'start' || clean[1] === 'stop' || clean[1] === 'status' ? clean[1] : undefined;
840
+ const daemonWorker = takeFlag(clean, '--daemon-worker');
841
+ const positionalWorkspace = sub
842
+ ? clean[2] && !clean[2].startsWith('--')
843
+ ? clean[2]
844
+ : undefined
845
+ : clean[1] && !clean[1].startsWith('--')
846
+ ? clean[1]
847
+ : undefined;
848
+ const workspace = await findWorkspace(workspaceOpt ?? positionalWorkspace);
615
849
  const port = Number(optionFrom(clean, '--port') ?? 4173);
616
850
  if (!Number.isInteger(port) || port < 0 || port > 65535)
617
851
  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',
852
+ const host = optionFrom(clean, '--host') ?? '0.0.0.0';
853
+ if (sub === 'start') {
854
+ try {
855
+ const meta = await monitorStart(workspace, { host, port });
856
+ outputEnvelope({
857
+ ok: true,
858
+ command: 'deckuse monitor start',
859
+ data: meta,
860
+ });
861
+ }
862
+ catch (error) {
863
+ outputEnvelope({
864
+ ok: false,
865
+ command: 'deckuse monitor start',
866
+ error: {
867
+ code: 'IO_ERROR',
868
+ message: error instanceof Error ? error.message : 'Monitor start failed',
869
+ },
870
+ });
871
+ process.exitCode = 1;
872
+ }
873
+ return;
874
+ }
875
+ if (sub === 'stop') {
876
+ const result = await monitorStop(workspace);
877
+ outputEnvelope({
878
+ ok: true,
879
+ command: 'deckuse monitor stop',
880
+ data: result,
881
+ });
882
+ return;
883
+ }
884
+ if (sub === 'status') {
885
+ const result = await monitorStatus(workspace);
886
+ outputEnvelope({
887
+ ok: true,
888
+ command: 'deckuse monitor status',
889
+ data: result,
890
+ });
891
+ return;
892
+ }
893
+ const monitor = await runMonitorForeground(workspace, {
894
+ host,
620
895
  port,
896
+ asDaemonWorker: daemonWorker,
621
897
  });
622
- process.stdout.write(`Deckuse monitor: ${monitor.url}\n`);
898
+ if (!daemonWorker)
899
+ process.stdout.write(`Deckuse monitor: ${monitor.url}\n`);
623
900
  const shutdown = async (signal) => {
624
901
  process.stderr.write(`deckuse monitor: received ${signal}, shutting down\n`);
625
902
  await monitor.close();
@@ -632,23 +909,29 @@ const main = async () => {
632
909
  else if (action === 'render') {
633
910
  const pageRaw = optionFrom(clean, '--page');
634
911
  if (!pageRaw)
635
- throw new Error('Usage: deckuse render --page <n> [--output <file.png>]');
912
+ throw new Error('Usage: deckuse render --page <n> [--output <file.png>] [--scale <n>]');
636
913
  if (/\s|,|-/.test(pageRaw))
637
914
  throw new Error('--page accepts exactly one positive integer (not a range or list)');
638
915
  const page = Number(pageRaw);
639
916
  if (!Number.isInteger(page) || page < 1)
640
917
  throw new Error('--page must be a positive integer');
918
+ const scaleRaw = optionFrom(clean, '--scale');
919
+ const scale = scaleRaw !== undefined ? Number(scaleRaw) : undefined;
920
+ if (scale !== undefined && !(scale > 0))
921
+ throw new Error('--scale must be a positive number');
641
922
  const workspace = await findWorkspace(workspaceOpt ?? clean[1]);
642
923
  const outputOpt = optionFrom(clean, '--output');
643
924
  try {
644
925
  const rendered = await renderPage(workspace, {
645
926
  page,
646
927
  ...(outputOpt ? { output: resolve(outputOpt) } : {}),
928
+ ...(scale !== undefined ? { scale } : {}),
647
929
  });
648
930
  outputEnvelope({
649
931
  ok: true,
650
932
  command: 'deckuse render',
651
- data: { page: rendered.page, output: rendered.output },
933
+ warnings: [...rendered.warnings],
934
+ data: { page: rendered.page, output: rendered.output, warnings: rendered.warnings },
652
935
  });
653
936
  }
654
937
  catch (error) {