@xenonbyte/da-vinci-workflow 0.2.8 → 0.2.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.9 - 2026-04-05
4
+
5
+ ### Added
6
+ - modular CLI command registration across `lib/cli/command-handlers-*.js`, with dedicated registry regression coverage for core, workflow, design, and pen command families
7
+ - shared CLI test helpers in `scripts/cli-test-helpers.js` to keep `runCli` / JSON parsing behavior consistent across command-facing test suites
8
+
9
+ ### Changed
10
+ - `runCli` now assembles family-scoped handler context instead of wiring a single flat command map payload, reducing coupling between unrelated CLI surfaces
11
+ - release notes in `README.md` and `README.zh-CN.md` now point to the `0.2.9` modular CLI release
12
+
13
+ ### Fixed
14
+ - `da-vinci --help` now keeps long option descriptions readable by wrapping oversized flags instead of collapsing flag and description text together
15
+ - CLI regression suites now reuse shared process-spawn helpers instead of repeating slightly different local implementations
16
+
3
17
  ## v0.2.8 - 2026-04-05
4
18
 
5
19
  ### Added
package/README.md CHANGED
@@ -34,15 +34,15 @@ Use `da-vinci maintainer-readiness` as the canonical maintainer diagnosis surfac
34
34
 
35
35
  Latest published npm package:
36
36
 
37
- - `@xenonbyte/da-vinci-workflow@0.2.8`
37
+ - `@xenonbyte/da-vinci-workflow@0.2.9`
38
38
 
39
- Release highlights for `0.2.8`:
39
+ Release highlights for `0.2.9`:
40
40
 
41
- - optional workflow decision tracing is now available for `workflow-status` and `next-step` through `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1`, with records written to `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`
42
- - the initial trace-family allowlist now covers persisted-state trust, task-group seed fallback, task-group focus override, stale planning-signal fallback, verification freshness downgrade, and worktree-isolation downgrade
43
- - workflow decision traces remain bounded and diagnostic-only: they do not change routing truth, do not run on non-eligible commands, and do not block normal command execution when trace persistence fails
44
- - trace records now reject invalid schema combinations with visible diagnostics instead of silently disappearing
45
- - trace payloads no longer claim missing evidence exists, and persisted fallback traces no longer imply a fingerprint comparison happened when it did not
41
+ - CLI command dispatch is now split into core, workflow, design, and pen handler registries instead of one oversized `runCli` branch chain
42
+ - registry-level regression coverage now directly exercises representative commands from every CLI family, reducing the risk of missed command wiring during future refactors
43
+ - command-facing test suites now share a common `runCli` / JSON parsing harness, which removes duplicated spawn helpers and keeps test behavior consistent
44
+ - `da-vinci --help` now wraps long flags such as `--confirm-test-evidence-executed` onto a readable second line instead of collapsing flag and description text together
45
+ - the release keeps runtime behavior unchanged while lowering maintenance cost for future CLI surface expansion
46
46
 
47
47
  ## Discipline And Orchestration Upgrade
48
48
 
package/README.zh-CN.md CHANGED
@@ -37,15 +37,15 @@ Da Vinci 是一个把产品需求一路推进到结构化规格、Pencil 设计
37
37
 
38
38
  最新已发布 npm 包:
39
39
 
40
- - `@xenonbyte/da-vinci-workflow@0.2.8`
40
+ - `@xenonbyte/da-vinci-workflow@0.2.9`
41
41
 
42
- `0.2.8` 版本重点:
42
+ `0.2.9` 版本重点:
43
43
 
44
- - 现在可以通过 `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1` 为 `workflow-status` `next-step` 开启可选的 workflow decision tracing,记录会写入 `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`
45
- - 首批 trace-family allowlist 已覆盖 persisted-state trust、task-group seed fallback、task-group focus override、stale planning-signal fallback、verification freshness downgrade 与 worktree-isolation downgrade
46
- - workflow decision trace 仍然是 bounded diagnostic-only:不会改变 routing truth,不会在非 eligible command 上发射,也不会因为 trace 持久化失败而阻断正常命令
47
- - trace record 现在会对非法 schema 组合给出可见诊断,不再静默丢失
48
- - trace payload 不再为缺失证据伪造 `evidenceRefs`,persisted fallback trace 也不再错误暗示某些未发生的 fingerprint comparison
44
+ - CLI 命令分发现在拆成 core、workflow、design、pen 四组 handler registry,不再把所有命令逻辑都堆在一个超长 `runCli` 分支链里
45
+ - registry 层回归测试现在直接覆盖每个 CLI family 的代表性命令,后续做命令面重构时更容易及时发现 wiring 漏洞
46
+ - 多个命令测试脚本现在共用统一的 `runCli` / JSON 解析 helper,减少重复的 spawn 逻辑并保持测试行为一致
47
+ - `da-vinci --help` 现在会把 `--confirm-test-evidence-executed` 这类超长 flag 正确换行显示,不再把 flag 和说明挤成一行
48
+ - 这次发布不改变既有运行时契约,但显著降低了 CLI 表面的维护成本和后续扩展风险
49
49
 
50
50
  ## Discipline And Orchestration 升级
51
51
 
@@ -2,6 +2,7 @@ const { Worker } = require("worker_threads");
2
2
  const path = require("path");
3
3
 
4
4
  const WORKER_SCRIPT_PATH = path.join(__dirname, "async-offload-worker.js");
5
+ const DEFAULT_WORKER_TIMEOUT_MS = 5 * 60 * 1000;
5
6
 
6
7
  function normalizeError(error) {
7
8
  if (!error || typeof error !== "object") {
@@ -24,22 +25,51 @@ function normalizeError(error) {
24
25
  return wrapped;
25
26
  }
26
27
 
27
- function runModuleExportInWorker(modulePath, exportName, args = []) {
28
+ function resolveWorkerTimeoutMs(options = {}) {
29
+ const optionValue = Number(options.timeoutMs);
30
+ if (Number.isFinite(optionValue) && optionValue > 0) {
31
+ return optionValue;
32
+ }
33
+
34
+ const envValue = Number(process.env.DA_VINCI_WORKER_TIMEOUT_MS);
35
+ if (Number.isFinite(envValue) && envValue > 0) {
36
+ return envValue;
37
+ }
38
+
39
+ return DEFAULT_WORKER_TIMEOUT_MS;
40
+ }
41
+
42
+ function buildWorkerTimeoutError(modulePath, exportName, timeoutMs) {
43
+ const error = new Error(
44
+ `Worker timed out after ${timeoutMs}ms while running ${path.basename(modulePath)}#${exportName}.`
45
+ );
46
+ error.code = "ETIMEDOUT";
47
+ return error;
48
+ }
49
+
50
+ function runModuleExportInWorker(modulePath, exportName, args = [], options = {}) {
28
51
  return new Promise((resolve, reject) => {
52
+ const resolvedModulePath = path.resolve(modulePath);
53
+ const timeoutMs = resolveWorkerTimeoutMs(options);
29
54
  const worker = new Worker(WORKER_SCRIPT_PATH, {
30
55
  workerData: {
31
- modulePath: path.resolve(modulePath),
56
+ modulePath: resolvedModulePath,
32
57
  exportName,
33
58
  args
34
59
  }
35
60
  });
36
61
 
37
62
  let settled = false;
63
+ let timeoutHandle = null;
38
64
  function settle(callback, value) {
39
65
  if (settled) {
40
66
  return;
41
67
  }
42
68
  settled = true;
69
+ if (timeoutHandle) {
70
+ clearTimeout(timeoutHandle);
71
+ timeoutHandle = null;
72
+ }
43
73
 
44
74
  const termination = worker.terminate();
45
75
  if (termination && typeof termination.then === "function") {
@@ -54,6 +84,13 @@ function runModuleExportInWorker(modulePath, exportName, args = []) {
54
84
  callback(value);
55
85
  }
56
86
 
87
+ timeoutHandle = setTimeout(() => {
88
+ settle(reject, buildWorkerTimeoutError(resolvedModulePath, exportName, timeoutMs));
89
+ }, timeoutMs);
90
+ if (typeof timeoutHandle.unref === "function") {
91
+ timeoutHandle.unref();
92
+ }
93
+
57
94
  worker.once("message", (message) => {
58
95
  if (message && message.ok) {
59
96
  settle(resolve, message.result);
@@ -0,0 +1,132 @@
1
+ const { registerCommand } = require("./helpers");
2
+
3
+ /**
4
+ * @typedef {Object} CliCoreCommandContext
5
+ * @property {string[]} argv
6
+ * @property {string} homeDir
7
+ * @property {string[]} positionalArgs
8
+ * @property {boolean} continueOnError
9
+ * @property {(args: string[], name: string) => string | undefined} getOption
10
+ * @property {string} VERSION
11
+ * @property {Function} installPlatforms
12
+ * @property {Function} uninstallPlatforms
13
+ * @property {Function} getStatus
14
+ * @property {Function} verifyInstall
15
+ * @property {Function} validateAssets
16
+ * @property {Function} runMaintainerReadinessCheck
17
+ * @property {Function} formatMaintainerReadinessReport
18
+ * @property {Function} formatStatus
19
+ * @property {Function} formatVerifyInstallReport
20
+ * @property {Function} emitCommandOutput
21
+ * @property {Function} formatTuiHelp
22
+ * @property {Function} launchTui
23
+ */
24
+
25
+ /**
26
+ * Register platform/bootstrap-adjacent CLI commands plus the TUI entrypoint.
27
+ *
28
+ * @param {import("./helpers").CliCommandHandlerMap} handlers
29
+ * @param {CliCoreCommandContext} context
30
+ * @returns {void}
31
+ */
32
+ function registerCoreCommands(handlers, context) {
33
+ const {
34
+ argv,
35
+ homeDir,
36
+ positionalArgs,
37
+ continueOnError,
38
+ getOption,
39
+ VERSION,
40
+ installPlatforms,
41
+ uninstallPlatforms,
42
+ getStatus,
43
+ verifyInstall,
44
+ validateAssets,
45
+ runMaintainerReadinessCheck,
46
+ formatMaintainerReadinessReport,
47
+ formatStatus,
48
+ formatVerifyInstallReport,
49
+ emitCommandOutput,
50
+ formatTuiHelp,
51
+ launchTui
52
+ } = context;
53
+
54
+ registerCommand(handlers, ["--version", "-v", "version"], async () => {
55
+ console.log(`Da Vinci v${VERSION}`);
56
+ });
57
+
58
+ registerCommand(handlers, "install", async () => {
59
+ const platformValue = getOption(argv, "--platform") || "all";
60
+ const result = installPlatforms(platformValue, { homeDir });
61
+ console.log(
62
+ `Installed Da Vinci v${result.version} for ${result.platforms.join(", ")} at ${result.homeDir}`
63
+ );
64
+ });
65
+
66
+ registerCommand(handlers, "uninstall", async () => {
67
+ const platformValue = getOption(argv, "--platform") || "all";
68
+ const result = uninstallPlatforms(platformValue, { homeDir });
69
+ console.log(
70
+ `Uninstalled Da Vinci v${result.version} for ${result.platforms.join(", ")} from ${result.homeDir}`
71
+ );
72
+ });
73
+
74
+ registerCommand(handlers, "status", async () => {
75
+ console.log(formatStatus(getStatus({ homeDir })));
76
+ });
77
+
78
+ registerCommand(handlers, "verify-install", async () => {
79
+ const platformValue = getOption(argv, "--platform") || "";
80
+ const result = verifyInstall({
81
+ homeDir,
82
+ platforms: platformValue
83
+ });
84
+ emitCommandOutput(result, argv, formatVerifyInstallReport, ["BLOCK"], continueOnError);
85
+ });
86
+
87
+ registerCommand(handlers, "maintainer-readiness", async () => {
88
+ const platformValue = getOption(argv, "--platform") || "";
89
+ const repoRoot = getOption(argv, "--project") || process.cwd();
90
+ const result = runMaintainerReadinessCheck({
91
+ repoRoot,
92
+ homeDir,
93
+ platforms: platformValue
94
+ });
95
+ emitCommandOutput(result, argv, formatMaintainerReadinessReport, ["BLOCK"], continueOnError);
96
+ });
97
+
98
+ registerCommand(handlers, "tui", async () => {
99
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
100
+ const changeId = getOption(argv, "--change");
101
+ const lang = getOption(argv, "--lang");
102
+ const tuiWidth = getOption(argv, "--tui-width");
103
+ const altScreen = argv.includes("--alt-screen")
104
+ ? true
105
+ : argv.includes("--no-alt-screen")
106
+ ? false
107
+ : undefined;
108
+ if (argv.includes("--help") || argv.includes("-h")) {
109
+ console.log(formatTuiHelp(lang));
110
+ return;
111
+ }
112
+ await launchTui({
113
+ projectPath,
114
+ changeId,
115
+ lang,
116
+ tuiWidth,
117
+ altScreen,
118
+ strict: argv.includes("--strict"),
119
+ jsonOutput: argv.includes("--json"),
120
+ continueOnError
121
+ });
122
+ });
123
+
124
+ registerCommand(handlers, "validate-assets", async () => {
125
+ const result = validateAssets();
126
+ console.log(`Da Vinci v${result.version} assets are complete (${result.requiredAssets} required files).`);
127
+ });
128
+ }
129
+
130
+ module.exports = {
131
+ registerCoreCommands
132
+ };
@@ -0,0 +1,129 @@
1
+ const { registerCommand } = require("./helpers");
2
+
3
+ /**
4
+ * @typedef {Object} CliDesignCommandContext
5
+ * @property {string[]} argv
6
+ * @property {string} homeDir
7
+ * @property {string[]} positionalArgs
8
+ * @property {boolean} continueOnError
9
+ * @property {(args: string[], name: string) => string | undefined} getOption
10
+ * @property {(args: string[], name: string, options?: object) => number | undefined} getIntegerOption
11
+ * @property {Function} syncIconCatalog
12
+ * @property {Function} formatIconSyncReport
13
+ * @property {Function} handleIconSearchCommand
14
+ * @property {Function} readOperations
15
+ * @property {Function} readLimitedStdin
16
+ * @property {Function} preflightPencilBatch
17
+ * @property {Function} formatPencilPreflightReport
18
+ * @property {Function} emitOrThrowOnStatus
19
+ * @property {Function} handleSupervisorReviewCommand
20
+ * @property {Function} saveCurrentDesign
21
+ * @property {Function} formatSaveCurrentDesignReport
22
+ * @property {{ BLOCKED: string, UNAVAILABLE: string }} SAVE_STATUS
23
+ */
24
+
25
+ /**
26
+ * Register design-tooling and design-review command handlers.
27
+ *
28
+ * @param {import("./helpers").CliCommandHandlerMap} handlers
29
+ * @param {CliDesignCommandContext} context
30
+ * @returns {void}
31
+ */
32
+ function registerDesignCommands(handlers, context) {
33
+ const {
34
+ argv,
35
+ homeDir,
36
+ positionalArgs,
37
+ continueOnError,
38
+ getOption,
39
+ getIntegerOption,
40
+ syncIconCatalog,
41
+ formatIconSyncReport,
42
+ handleIconSearchCommand,
43
+ readOperations,
44
+ readLimitedStdin,
45
+ preflightPencilBatch,
46
+ formatPencilPreflightReport,
47
+ emitOrThrowOnStatus,
48
+ handleSupervisorReviewCommand,
49
+ saveCurrentDesign,
50
+ formatSaveCurrentDesignReport,
51
+ SAVE_STATUS
52
+ } = context;
53
+
54
+ registerCommand(handlers, "icon-sync", async () => {
55
+ const outputPath = getOption(argv, "--output") || getOption(argv, "--catalog");
56
+ const timeoutMs = getIntegerOption(argv, "--timeout-ms", { min: 1 });
57
+ const strict = argv.includes("--strict");
58
+
59
+ const result = await syncIconCatalog({
60
+ outputPath,
61
+ timeoutMs,
62
+ strict,
63
+ homeDir
64
+ });
65
+
66
+ console.log(formatIconSyncReport(result));
67
+ });
68
+
69
+ registerCommand(handlers, "icon-search", async () => {
70
+ await handleIconSearchCommand(argv, homeDir);
71
+ });
72
+
73
+ registerCommand(handlers, "preflight-pencil", async () => {
74
+ const opsFile = getOption(argv, "--ops-file");
75
+ let operations = "";
76
+
77
+ if (opsFile) {
78
+ operations = readOperations(opsFile);
79
+ } else if (!process.stdin.isTTY) {
80
+ operations = readLimitedStdin();
81
+ } else {
82
+ throw new Error("`preflight-pencil` requires `--ops-file <path>` or piped stdin input.");
83
+ }
84
+
85
+ const result = preflightPencilBatch(operations);
86
+ const report = formatPencilPreflightReport(result);
87
+
88
+ if (emitOrThrowOnStatus(result.status, ["FAIL"], report, continueOnError)) {
89
+ return;
90
+ }
91
+
92
+ console.log(report);
93
+ });
94
+
95
+ registerCommand(handlers, "supervisor-review", async () => {
96
+ await handleSupervisorReviewCommand(argv);
97
+ });
98
+
99
+ registerCommand(handlers, "save-current-design", async () => {
100
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
101
+ const pencilBin = getOption(argv, "--pencil-bin");
102
+ const result = await saveCurrentDesign({
103
+ projectPath,
104
+ homeDir,
105
+ pencilBin,
106
+ allowLocalBridge: true
107
+ });
108
+ const output = argv.includes("--json")
109
+ ? JSON.stringify(result, null, 2)
110
+ : formatSaveCurrentDesignReport(result);
111
+
112
+ if (
113
+ emitOrThrowOnStatus(
114
+ result.status,
115
+ [SAVE_STATUS.BLOCKED, SAVE_STATUS.UNAVAILABLE],
116
+ output,
117
+ continueOnError
118
+ )
119
+ ) {
120
+ return;
121
+ }
122
+
123
+ console.log(output);
124
+ });
125
+ }
126
+
127
+ module.exports = {
128
+ registerDesignCommands
129
+ };
@@ -0,0 +1,231 @@
1
+ const { registerCommand } = require("./helpers");
2
+
3
+ /**
4
+ * @typedef {Object} CliPenCommandContext
5
+ * @property {string[]} argv
6
+ * @property {string} homeDir
7
+ * @property {string[]} positionalArgs
8
+ * @property {boolean} continueOnError
9
+ * @property {(args: string[], name: string) => string | undefined} getOption
10
+ * @property {(args: string[], name: string) => string[]} getCommaSeparatedOptionValues
11
+ * @property {Function} ensurePenFile
12
+ * @property {Function} writePenFromPayloadFiles
13
+ * @property {Function} comparePenSync
14
+ * @property {Function} comparePenBaselineAlignment
15
+ * @property {Function} formatPenBaselineAlignmentReport
16
+ * @property {Function} syncPenSource
17
+ * @property {Function} snapshotPenFile
18
+ * @property {Function} handlePencilLockCommand
19
+ * @property {Function} handlePencilSessionCommand
20
+ * @property {Function} emitOrThrowOnStatus
21
+ */
22
+
23
+ /**
24
+ * Register `.pen`, Pencil lock, and Pencil session command handlers.
25
+ *
26
+ * @param {import("./helpers").CliCommandHandlerMap} handlers
27
+ * @param {CliPenCommandContext} context
28
+ * @returns {void}
29
+ */
30
+ function registerPenCommands(handlers, context) {
31
+ const {
32
+ argv,
33
+ homeDir,
34
+ positionalArgs,
35
+ continueOnError,
36
+ getOption,
37
+ getCommaSeparatedOptionValues,
38
+ ensurePenFile,
39
+ writePenFromPayloadFiles,
40
+ comparePenSync,
41
+ comparePenBaselineAlignment,
42
+ formatPenBaselineAlignmentReport,
43
+ syncPenSource,
44
+ snapshotPenFile,
45
+ handlePencilLockCommand,
46
+ handlePencilSessionCommand,
47
+ emitOrThrowOnStatus
48
+ } = context;
49
+
50
+ registerCommand(handlers, "ensure-pen", async () => {
51
+ const outputPath = getOption(argv, "--output");
52
+ const version = getOption(argv, "--version");
53
+ const verifyWithPencil = argv.includes("--verify-open");
54
+
55
+ if (!outputPath) {
56
+ throw new Error("`ensure-pen` requires `--output <path>`.");
57
+ }
58
+
59
+ const result = ensurePenFile({
60
+ outputPath,
61
+ version,
62
+ verifyWithPencil
63
+ });
64
+
65
+ console.log(`${result.created ? "Created" : "Verified existing"} .pen source at ${result.outputPath}`);
66
+ console.log(`State file: ${result.statePath}`);
67
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
68
+ if (result.verification) {
69
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
70
+ }
71
+ });
72
+
73
+ registerCommand(handlers, "write-pen", async () => {
74
+ const outputPath = getOption(argv, "--output");
75
+ const nodesFile = getOption(argv, "--nodes-file");
76
+ const variablesFile = getOption(argv, "--variables-file");
77
+ const version = getOption(argv, "--version");
78
+ const verifyWithPencil = argv.includes("--verify-open");
79
+
80
+ if (!outputPath || !nodesFile) {
81
+ throw new Error("`write-pen` requires `--output <path>` and `--nodes-file <path>`.");
82
+ }
83
+
84
+ const result = writePenFromPayloadFiles({
85
+ outputPath,
86
+ nodesFile,
87
+ variablesFile,
88
+ version,
89
+ verifyWithPencil
90
+ });
91
+
92
+ console.log(`Wrote .pen file to ${result.outputPath}`);
93
+ console.log(`State file: ${result.statePath}`);
94
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
95
+ console.log(`Top-level nodes: ${result.document.children.length}`);
96
+ if (result.verification) {
97
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
98
+ }
99
+ });
100
+
101
+ registerCommand(handlers, "check-pen-sync", async () => {
102
+ const penPath = getOption(argv, "--pen");
103
+ const nodesFile = getOption(argv, "--nodes-file");
104
+ const variablesFile = getOption(argv, "--variables-file");
105
+ const version = getOption(argv, "--version");
106
+
107
+ if (!penPath || !nodesFile) {
108
+ throw new Error("`check-pen-sync` requires `--pen <path>` and `--nodes-file <path>`.");
109
+ }
110
+
111
+ const result = comparePenSync({
112
+ penPath,
113
+ nodesFile,
114
+ variablesFile,
115
+ version
116
+ });
117
+
118
+ if (!result.inSync) {
119
+ throw new Error(
120
+ [
121
+ "Registered `.pen` is out of sync with the provided live payload.",
122
+ `Pen: ${result.penPath}`,
123
+ `State file: ${result.statePath}`,
124
+ `Persisted hash: ${result.persistedHash}`,
125
+ `Live hash: ${result.liveHash}`
126
+ ].join("\n")
127
+ );
128
+ }
129
+
130
+ console.log(`Registered .pen is in sync: ${result.penPath}`);
131
+ console.log(`State file: ${result.statePath}`);
132
+ console.log(`Snapshot hash: ${result.liveHash}`);
133
+ if (!result.usedStateFile) {
134
+ console.log(
135
+ result.stateHash
136
+ ? "State file hash was stale; sync comparison fell back to hashing the disk .pen file directly."
137
+ : "State file was missing; sync comparison fell back to hashing the disk .pen file directly."
138
+ );
139
+ }
140
+ });
141
+
142
+ registerCommand(handlers, "check-pen-baseline", async () => {
143
+ const penPath = getOption(argv, "--pen");
144
+ const baselinePaths = getCommaSeparatedOptionValues(argv, "--baseline");
145
+ const preferredSource = getOption(argv, "--prefer-source");
146
+
147
+ if (!penPath || baselinePaths.length === 0) {
148
+ throw new Error(
149
+ "`check-pen-baseline` requires `--pen <path>` and at least one `--baseline <path>`."
150
+ );
151
+ }
152
+
153
+ const result = comparePenBaselineAlignment({
154
+ penPath,
155
+ baselinePaths,
156
+ preferredSource
157
+ });
158
+
159
+ if (
160
+ emitOrThrowOnStatus(
161
+ result.status,
162
+ ["BLOCK"],
163
+ [
164
+ "Baseline alignment check failed.",
165
+ formatPenBaselineAlignmentReport(result)
166
+ ].join("\n"),
167
+ continueOnError
168
+ )
169
+ ) {
170
+ return;
171
+ }
172
+
173
+ console.log(formatPenBaselineAlignmentReport(result));
174
+ });
175
+
176
+ registerCommand(handlers, "sync-pen-source", async () => {
177
+ const sourcePath = getOption(argv, "--from");
178
+ const targetPath = getOption(argv, "--to");
179
+
180
+ if (!sourcePath || !targetPath) {
181
+ throw new Error("`sync-pen-source` requires `--from <path>` and `--to <path>`.");
182
+ }
183
+
184
+ const result = syncPenSource({
185
+ sourcePath,
186
+ targetPath
187
+ });
188
+
189
+ console.log(`Synced .pen source from ${result.sourcePath} to ${result.targetPath}`);
190
+ console.log(`State file: ${result.statePath}`);
191
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
192
+ });
193
+
194
+ registerCommand(handlers, "snapshot-pen", async () => {
195
+ const inputPath = getOption(argv, "--input");
196
+ const outputPath = getOption(argv, "--output");
197
+ const version = getOption(argv, "--version");
198
+ const verifyWithPencil = argv.includes("--verify-open");
199
+
200
+ if (!inputPath || !outputPath) {
201
+ throw new Error("`snapshot-pen` requires `--input <path>` and `--output <path>`.");
202
+ }
203
+
204
+ const result = snapshotPenFile({
205
+ inputPath,
206
+ outputPath,
207
+ version,
208
+ verifyWithPencil
209
+ });
210
+
211
+ console.log(`Snapshotted ${result.inputPath} to ${result.outputPath}`);
212
+ console.log(`State file: ${result.statePath}`);
213
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
214
+ console.log(`Top-level nodes: ${result.document.children.length}`);
215
+ if (result.verification) {
216
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
217
+ }
218
+ });
219
+
220
+ registerCommand(handlers, "pencil-lock", async () => {
221
+ handlePencilLockCommand(argv, homeDir);
222
+ });
223
+
224
+ registerCommand(handlers, "pencil-session", async () => {
225
+ handlePencilSessionCommand(argv, homeDir);
226
+ });
227
+ }
228
+
229
+ module.exports = {
230
+ registerPenCommands
231
+ };