@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 +14 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/lib/async-offload.js +39 -2
- package/lib/cli/command-handlers-core.js +132 -0
- package/lib/cli/command-handlers-design.js +129 -0
- package/lib/cli/command-handlers-pen.js +231 -0
- package/lib/cli/command-handlers-workflow.js +221 -0
- package/lib/cli/command-handlers.js +49 -0
- package/lib/cli/helpers.js +62 -0
- package/lib/cli.js +98 -542
- package/lib/execution-signals.js +33 -0
- package/lib/fs-safety.js +1 -12
- package/lib/path-inside.js +17 -0
- package/lib/utils.js +2 -7
- package/lib/workflow-base-view.js +134 -0
- package/lib/workflow-overlay.js +1033 -0
- package/lib/workflow-persisted-state.js +4 -0
- package/lib/workflow-stage.js +244 -0
- package/lib/workflow-state.js +359 -1998
- package/lib/workflow-task-groups.js +881 -0
- package/lib/worktree-preflight.js +31 -11
- package/package.json +1 -1
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.
|
|
37
|
+
- `@xenonbyte/da-vinci-workflow@0.2.9`
|
|
38
38
|
|
|
39
|
-
Release highlights for `0.2.
|
|
39
|
+
Release highlights for `0.2.9`:
|
|
40
40
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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.
|
|
40
|
+
- `@xenonbyte/da-vinci-workflow@0.2.9`
|
|
41
41
|
|
|
42
|
-
`0.2.
|
|
42
|
+
`0.2.9` 版本重点:
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
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
|
|
package/lib/async-offload.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
+
};
|