dev-harness-cli 1.0.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +299 -0
  3. package/adapters/amazon-q/README.md +23 -0
  4. package/adapters/antigravity/README.md +22 -0
  5. package/adapters/claude-code/README.md +30 -0
  6. package/adapters/cline/README.md +23 -0
  7. package/adapters/codex/README.md +31 -0
  8. package/adapters/copilot/README.md +23 -0
  9. package/adapters/cursor/README.md +29 -0
  10. package/adapters/gemini/README.md +23 -0
  11. package/adapters/generic/README.md +40 -0
  12. package/adapters/hermes/README.md +31 -0
  13. package/adapters/hermes/SKILL.md +89 -0
  14. package/adapters/hermes/scripts/init.mjs +27 -0
  15. package/adapters/hermes/scripts/phase.mjs +27 -0
  16. package/adapters/hermes/scripts/validate.mjs +27 -0
  17. package/adapters/kilo-code/README.md +23 -0
  18. package/adapters/openclaw/README.md +22 -0
  19. package/adapters/pi/README.md +22 -0
  20. package/adapters/roo/README.md +23 -0
  21. package/adapters/windsurf/README.md +23 -0
  22. package/cli/commands/checkpoint.mjs +94 -0
  23. package/cli/commands/config.mjs +268 -0
  24. package/cli/commands/contract.mjs +155 -0
  25. package/cli/commands/detect-tool.mjs +112 -0
  26. package/cli/commands/init.mjs +351 -0
  27. package/cli/commands/learn.mjs +47 -0
  28. package/cli/commands/pause.mjs +34 -0
  29. package/cli/commands/phase.mjs +182 -0
  30. package/cli/commands/resume.mjs +33 -0
  31. package/cli/commands/rollback.mjs +261 -0
  32. package/cli/commands/set-mode.mjs +75 -0
  33. package/cli/commands/status.mjs +168 -0
  34. package/cli/commands/validate.mjs +118 -0
  35. package/cli/commands/worktree.mjs +298 -0
  36. package/cli/harness-dev.mjs +88 -0
  37. package/cli/lib/args.mjs +111 -0
  38. package/cli/lib/command-helpers.mjs +50 -0
  39. package/cli/lib/config-registry.mjs +329 -0
  40. package/cli/lib/constants.mjs +30 -0
  41. package/cli/lib/contract.mjs +306 -0
  42. package/cli/lib/detect-stack.mjs +235 -0
  43. package/cli/lib/errors.mjs +71 -0
  44. package/cli/lib/file-io.mjs +90 -0
  45. package/cli/lib/gates.mjs +492 -0
  46. package/cli/lib/git.mjs +144 -0
  47. package/cli/lib/help.mjs +246 -0
  48. package/cli/lib/modes.mjs +92 -0
  49. package/cli/lib/output.mjs +49 -0
  50. package/cli/lib/paths.mjs +75 -0
  51. package/cli/lib/phases.mjs +58 -0
  52. package/cli/lib/platform.mjs +78 -0
  53. package/cli/lib/progress.mjs +357 -0
  54. package/cli/lib/ralph-inner.mjs +314 -0
  55. package/cli/lib/ralph-outer.mjs +249 -0
  56. package/cli/lib/ralph-output.mjs +178 -0
  57. package/cli/lib/scaffold.mjs +431 -0
  58. package/cli/lib/schemas/stacks.json +477 -0
  59. package/cli/lib/state.mjs +333 -0
  60. package/cli/lib/templates.mjs +264 -0
  61. package/cli/lib/tool-registry.mjs +218 -0
  62. package/cli/lib/validate-schema.mjs +131 -0
  63. package/cli/lib/vars.mjs +114 -0
  64. package/package.json +50 -0
  65. package/schema/harness-config.schema.json +127 -0
  66. package/templates/AGENTS.md +63 -0
  67. package/templates/ci/github-actions.yml +78 -0
  68. package/templates/ci/gitlab-ci.yml +59 -0
  69. package/templates/docs/agents/evaluator.md +14 -0
  70. package/templates/docs/agents/generator.md +13 -0
  71. package/templates/docs/agents/planner.md +13 -0
  72. package/templates/docs/agents/simplifier.md +13 -0
  73. package/templates/docs/phases/build.md +41 -0
  74. package/templates/docs/phases/define.md +51 -0
  75. package/templates/docs/phases/plan.md +36 -0
  76. package/templates/docs/phases/review.md +42 -0
  77. package/templates/docs/phases/ship.md +43 -0
  78. package/templates/docs/phases/simplify.md +40 -0
  79. package/templates/docs/phases/verify.md +38 -0
  80. package/templates/evaluator-rubric.md +28 -0
  81. package/templates/init.ps1 +97 -0
  82. package/templates/init.sh +102 -0
  83. package/templates/sprint-contract.md +31 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Help text builder — centralized to keep all formatting in one place.
3
+ */
4
+
5
+ const USAGE = `Usage: harness-dev <command> [options]
6
+
7
+ Pipeline commands:
8
+ init Scaffold full harness in current directory
9
+ phase <name> Invoke a phase (define|plan|build|verify|simplify|review|ship)
10
+ validate Run gate checks for current phase
11
+ validate --feature X --task Y Validate a single task (feature-iterate phases)
12
+
13
+ State commands:
14
+ status Show current phase + gate state + detected stack
15
+ config list List all config parameters with descriptions
16
+ config get [key] Get config value (omit key for all)
17
+ config set <key> <val> Set config value (e.g. config set gates.enabled true)
18
+ pause Pause autopilot execution
19
+ resume Resume autopilot execution
20
+ learn <message> Append a lesson to progress.md
21
+
22
+ Agent workflow commands:
23
+ contract propose Write/update sprint-contract.md
24
+ contract review Evaluator reviews contract, sets status
25
+ contract status Show current contract state
26
+ contract escalate Human adjudication when agents can't agree
27
+
28
+ Git workflow commands:
29
+ worktree create <name> Create isolated worktree for a feature
30
+ worktree list List active worktrees
31
+ worktree prune Remove orphaned worktrees
32
+ worktree remove <name> Clean up worktree (optionally merge branch)
33
+ rollback list Show available checkpoints
34
+ rollback to <tag> Restore state to a checkpoint
35
+ rollback branch <tag> Branch off a good iteration
36
+ checkpoint create <label> Force a manual checkpoint tag
37
+ detect-tool Detect which agent tools are configured
38
+
39
+ Mode:
40
+ set-mode <mode> Switch mode (copilot|autopilot)
41
+
42
+ Other:
43
+ help Alias for --help
44
+
45
+ Global flags:
46
+ --json Machine-parseable JSON output
47
+ --help, -h Show this help message
48
+ --version Show version
49
+
50
+ Exit codes:
51
+ 0 Success
52
+ 1 Validation failure (gate check failed)
53
+ 2 Usage error (bad arguments)
54
+ 3 Internal error`;
55
+
56
+ const VERSION = '0.2.0';
57
+
58
+ // Help text for JSON output
59
+ function buildJsonHelp() {
60
+ return {
61
+ help: true,
62
+ version: VERSION,
63
+ usage: 'harness-dev <command> [options]',
64
+ commands: {
65
+ init: 'Scaffold full harness in current directory',
66
+ status: 'Show current phase + gate state + detected stack',
67
+ phase: 'Invoke a phase (define|plan|build|verify|simplify|review|ship)',
68
+ validate: 'Run gate checks for current phase (--feature --task for per-task check)',
69
+ 'set-mode': 'Switch mode (copilot|autopilot)',
70
+ config: 'Get/set config values',
71
+ pause: 'Pause autopilot execution',
72
+ resume: 'Resume autopilot execution',
73
+ learn: 'Append a lesson to progress.md',
74
+ contract: 'Sprint Contract workflow (propose/review/status/escalate)',
75
+ worktree: 'Git worktree management (create/list/prune/remove)',
76
+ rollback: 'Checkpoint recovery (list/to/branch)',
77
+ checkpoint: 'Manual checkpoint tagging (create)',
78
+ 'detect-tool': 'Detect which agent coding tools are configured in the project',
79
+ help: 'Alias for --help',
80
+ },
81
+ flags: {
82
+ json: 'Machine-parseable JSON output',
83
+ help: 'Show this help message',
84
+ version: 'Show version',
85
+ },
86
+ exitCodes: {
87
+ 0: 'Success',
88
+ 1: 'Validation failure (gate check failed)',
89
+ 2: 'Usage error (bad arguments)',
90
+ 3: 'Internal error',
91
+ },
92
+ };
93
+ }
94
+
95
+ /**
96
+ * @param {boolean} json
97
+ * @returns {string}
98
+ */
99
+ export function helpText(json = false) {
100
+ if (json) {
101
+ return JSON.stringify(buildJsonHelp());
102
+ }
103
+ return USAGE;
104
+ }
105
+
106
+ /**
107
+ * @param {boolean} json
108
+ * @returns {string}
109
+ */
110
+ export function versionText(json = false) {
111
+ if (json) {
112
+ return JSON.stringify({ version: VERSION });
113
+ }
114
+ return `harness-dev v${VERSION}`;
115
+ }
116
+
117
+ // Per-command help text (for `harness-dev <command> --help`).
118
+ const COMMAND_HELP = {
119
+ init: `Usage: harness-dev init [--stack <name>] [--target <dir>] [--force] [--no-git] [--json]
120
+
121
+ Scaffold a full harness in the target directory:
122
+ - Detects stack (or use --stack)
123
+ - Generates AGENTS.md, harness-config.json, init.sh, progress.md,
124
+ sprint-contract.md, feature_list.json, docs/, ci/, evaluator-rubric.md
125
+ - Initializes git repo + initial commit (unless --no-git)
126
+
127
+ Flags:
128
+ --stack <name> Override stack detection (node|python|go|rust|java|...)
129
+ --target <dir> Target directory (default: cwd)
130
+ --force Overwrite existing harness files
131
+ --no-git Skip git init
132
+ --json JSON output`,
133
+
134
+ status: `Usage: harness-dev status [--target <dir>] [--json]
135
+
136
+ Show current phase, gate state, detected stack, recent lessons, and next action.
137
+
138
+ Flags:
139
+ --target <dir> Project directory (default: cwd)
140
+ --json JSON output`,
141
+
142
+ phase: `Usage: harness-dev phase <name> [--target <dir>] [--git-ops] [--json]
143
+
144
+ Invoke a phase. Valid phases: define, plan, build, verify, simplify, review, ship.
145
+
146
+ Flags:
147
+ --target <dir> Project directory (default: cwd)
148
+ --git-ops Execute git reset --hard + clean on retry (fresh context)
149
+ --json JSON output`,
150
+
151
+ validate: `Usage: harness-dev validate [--phase <name>] [--feature <id> --task <id>] [--target <dir>] [--json]
152
+
153
+ Run gate checks for the current (or specified) phase.
154
+
155
+ Flags:
156
+ --phase <name> Override current phase
157
+ --feature <id> Validate a single feature (feature-iterate phases)
158
+ --task <id> Validate a single task within a feature
159
+ --target <dir> Project directory (default: cwd)
160
+ --json JSON output`,
161
+
162
+ 'set-mode': `Usage: harness-dev set-mode <copilot|autopilot> [--target <dir>] [--json]
163
+
164
+ Switch execution mode. Autopilot requires DEFINE phase or later.`,
165
+
166
+ config: `Usage: harness-dev config list [--target <dir>] [--json]
167
+ harness-dev config get [key] [--target <dir>] [--json]
168
+ harness-dev config set <key> <value> [--target <dir>] [--json]
169
+
170
+ List all parameters with descriptions, or get/set values via dot-notation.
171
+ Use 'config list' to see all configurable parameters, their current values,
172
+ types, allowed options, and descriptions.
173
+
174
+ Examples:
175
+ harness-dev config list
176
+ harness-dev config list --json
177
+ harness-dev config get gates.enabled
178
+ harness-dev config set gates.enabled true
179
+ harness-dev config set mode autopilot
180
+ harness-dev config set maxRetries 5`,
181
+
182
+ pause: `Usage: harness-dev pause [--target <dir>] [--json]
183
+
184
+ Pause autopilot execution. Autopilot stops after the current phase gate.`,
185
+
186
+ resume: `Usage: harness-dev resume [--target <dir>] [--json]
187
+
188
+ Resume autopilot execution.`,
189
+
190
+ learn: `Usage: harness-dev learn "<message>" [--target <dir>] [--json]
191
+
192
+ Append a lesson to the Lessons section of progress.md.`,
193
+
194
+ contract: `Usage: harness-dev contract <subcommand> [options] [--target <dir>] [--json]
195
+
196
+ Subcommands:
197
+ propose --scope "..." [--exclusions "..."] [--criteria "..."] Generator proposes
198
+ review --decision <agreed|needs-revision> [--notes "..."] Evaluator reviews
199
+ status Show contract state
200
+ escalate [--reason "..."] Human adjudication`,
201
+
202
+ worktree: `Usage: harness-dev worktree <subcommand> [options] [--target <dir>] [--json]
203
+
204
+ Subcommands:
205
+ create <name> Create isolated worktree for a feature
206
+ list List active worktrees
207
+ prune Remove orphaned worktrees
208
+ remove <name> Clean up worktree (optionally merge branch)`,
209
+
210
+ rollback: `Usage: harness-dev rollback <subcommand> [checkpoint] [--target <dir>] [--json]
211
+
212
+ Subcommands:
213
+ list Show available checkpoints
214
+ to <tag> Restore state to a checkpoint
215
+ branch <tag> Branch off a good iteration`,
216
+
217
+ checkpoint: `Usage: harness-dev checkpoint create <label> [--force] [--target <dir>] [--json]
218
+
219
+ Create a manual checkpoint tag (manual/<label>). Requires clean working tree
220
+ unless --force is given.`,
221
+
222
+ 'detect-tool': `Usage: harness-dev detect-tool [--target <dir>] [--json]
223
+
224
+ Scan the project for agent-tool files (CLAUDE.md, .cursorrules, AGENTS.md, etc.)
225
+ and report which coding agents are available. Recommends a tool based on config
226
+ and detected files.`,
227
+
228
+ help: `Usage: harness-dev help
229
+
230
+ Show the global help message. Alias for --help.`,
231
+ };
232
+
233
+ /**
234
+ * Get per-command help text.
235
+ * @param {string} command
236
+ * @param {boolean} json
237
+ * @returns {string|null} — null if no per-command help exists
238
+ */
239
+ export function commandHelpText(command, json = false) {
240
+ const text = COMMAND_HELP[command];
241
+ if (!text) { return null; }
242
+ if (json) {
243
+ return JSON.stringify({ command, help: text });
244
+ }
245
+ return text;
246
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * modes — Copilot/Autopilot mode configuration and behavior.
3
+ *
4
+ * Shared utilities for determining mode, reading copilot config,
5
+ * and handling interactive prompts.
6
+ *
7
+ * Usage:
8
+ * import { getMode, shouldAutoPrompt, shouldConfirmGates, promptYesNo } from './modes.mjs';
9
+ */
10
+ import { loadConfig, set } from './state.mjs';
11
+ import * as readline from 'node:readline';
12
+
13
+ /**
14
+ * Get the current mode for a project.
15
+ * @param {string} targetDir
16
+ * @returns {'copilot'|'autopilot'}
17
+ */
18
+ export function getMode(targetDir) {
19
+ const { config } = loadConfig(targetDir);
20
+ return (config.mode === 'autopilot') ? 'autopilot' : 'copilot';
21
+ }
22
+
23
+ /**
24
+ * Check if copilot should auto-prompt after gate passes.
25
+ * @param {string} targetDir
26
+ * @returns {boolean}
27
+ */
28
+ export function shouldAutoPrompt(targetDir) {
29
+ const { config } = loadConfig(targetDir);
30
+ if (config.mode !== 'copilot') {return false;}
31
+ return config.copilot?.autoPrompt !== false; // default true
32
+ }
33
+
34
+ /**
35
+ * Check if copilot should confirm before advancing gates.
36
+ * @param {string} targetDir
37
+ * @returns {boolean}
38
+ */
39
+ export function shouldConfirmGates(targetDir) {
40
+ const { config } = loadConfig(targetDir);
41
+ if (config.mode !== 'copilot') {return false;}
42
+ return config.copilot?.confirmGates !== false; // default true
43
+ }
44
+
45
+ /**
46
+ * Prompt the user with a yes/no question. Returns true for y/yes.
47
+ * In non-interactive contexts (stdin not a TTY), returns null (no answer).
48
+ * @param {string} question — question text to display
49
+ * @returns {Promise<boolean|null>} true=y, false=n, null=no answer
50
+ */
51
+ export function promptYesNo(question) {
52
+ // Use readline for interactive prompt
53
+ return new Promise((resolve) => {
54
+ try {
55
+ const rl = readline.createInterface({
56
+ input: process.stdin,
57
+ output: process.stdout,
58
+ });
59
+ rl.question(`${question} (y/n) `, (answer) => {
60
+ rl.close();
61
+ const trimmed = answer.trim().toLowerCase();
62
+ if (trimmed === 'y' || trimmed === 'yes') {
63
+ resolve(true);
64
+ } else if (trimmed === 'n' || trimmed === 'no') {
65
+ resolve(false);
66
+ } else {
67
+ // Invalid input — treat as no
68
+ resolve(false);
69
+ }
70
+ });
71
+ } catch {
72
+ // Not interactive or readline unavailable
73
+ resolve(null);
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Ensure copilot config block exists in project config.
80
+ * @param {string} targetDir
81
+ * @param {object} [overrides]
82
+ */
83
+ export function ensureCopilotConfig(targetDir, overrides = {}) {
84
+ const { config } = loadConfig(targetDir);
85
+ if (!config.copilot) {
86
+ config.copilot = { autoPrompt: true, confirmGates: true };
87
+ }
88
+ // Apply overrides
89
+ if (overrides.autoPrompt !== undefined) {config.copilot.autoPrompt = overrides.autoPrompt;}
90
+ if (overrides.confirmGates !== undefined) {config.copilot.confirmGates = overrides.confirmGates;}
91
+ set(targetDir, 'copilot', config.copilot);
92
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * output — Centralized CLI output helpers.
3
+ *
4
+ * Standardizes the JSON output contract { command, status, message, ... }
5
+ * and human-mode text emission so every command emits output the same way.
6
+ *
7
+ * Usage:
8
+ * import { emitJson, emitHuman, emitError } from '../lib/output.mjs';
9
+ * emitJson({ command: 'status', status: 'ok', message: '...', ...extras });
10
+ * emitHuman('✓ Done\n');
11
+ */
12
+
13
+ /**
14
+ * Write a JSON object to stdout followed by a newline.
15
+ * @param {object} obj
16
+ */
17
+ export function emitJson(obj) {
18
+ process.stdout.write(JSON.stringify(obj) + '\n');
19
+ }
20
+
21
+ /**
22
+ * Write human-readable text to stdout.
23
+ * @param {string} text
24
+ */
25
+ export function emitHuman(text) {
26
+ process.stdout.write(text);
27
+ }
28
+
29
+ /**
30
+ * Write human-readable error text to stderr.
31
+ * @param {string} text
32
+ */
33
+ export function emitHumanError(text) {
34
+ process.stderr.write(text);
35
+ }
36
+
37
+ /**
38
+ * Emit a standard command result in JSON or human form.
39
+ *
40
+ * @param {object} opts
41
+ * @param {string} opts.command — command name
42
+ * @param {boolean} opts.json — JSON mode
43
+ * @param {boolean} opts.ok — success flag
44
+ * @param {string} opts.message — status message
45
+ * @param {string} [opts.okText] — human success text (defaults to message)
46
+ * @param {string} [opts.errText] — human error text (defaults to message)
47
+ * @param {object} [opts.extras] — additional JSON fields
48
+ */
49
+ // emitResult and emitFatalError removed — commands use emitJson/emitHuman directly.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * paths — Centralized path resolution for the CLI.
3
+ *
4
+ * Single source of truth for the lib directory location and all
5
+ * project-relative file paths (config, feature list, contract, progress,
6
+ * schemas, templates). Eliminates 4× duplicated __dirname boilerplate and
7
+ * scattered resolve(targetDir, '...') calls.
8
+ *
9
+ * Usage:
10
+ * import { LIB_DIR, TEMPLATES_DIR, SCHEMA_DIR, CONFIG_PATH, FEATURE_LIST_PATH } from './paths.mjs';
11
+ */
12
+ import { resolve, dirname } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ /** Directory containing cli/lib/ (this module's directory). */
16
+ export const LIB_DIR = dirname(fileURLToPath(import.meta.url));
17
+
18
+ /** Project root (parent of cli/). */
19
+ export const PROJECT_ROOT = resolve(LIB_DIR, '..', '..');
20
+
21
+ /** Templates directory (project_root/templates). */
22
+ export const TEMPLATES_DIR = resolve(PROJECT_ROOT, 'templates');
23
+
24
+ /** Adapters directory (project_root/adapters) — tool-specific integration files. */
25
+ export const ADAPTERS_DIR = resolve(PROJECT_ROOT, 'adapters');
26
+
27
+ /** JSON schemas directory (project_root/schema). */
28
+ export const SCHEMA_DIR = resolve(PROJECT_ROOT, 'schema');
29
+
30
+ /** harness-config.json schema path. */
31
+ export const CONFIG_SCHEMA_PATH = resolve(SCHEMA_DIR, 'harness-config.schema.json');
32
+
33
+ /** feature_list.json schema path. */
34
+ export const FEATURE_LIST_SCHEMA_PATH = resolve(SCHEMA_DIR, 'feature-list.schema.json');
35
+
36
+ /** stacks.json metadata path (cli/lib/schemas/stacks.json). */
37
+ export const STACKS_SCHEMA_PATH = resolve(LIB_DIR, 'schemas', 'stacks.json');
38
+
39
+ // ── Project-relative paths (target project, not this CLI) ────────────────────
40
+
41
+ /**
42
+ * Path to a project's harness-config.json.
43
+ * @param {string} targetDir
44
+ * @returns {string}
45
+ */
46
+ export function CONFIG_PATH(targetDir) {
47
+ return resolve(targetDir, 'harness-config.json');
48
+ }
49
+
50
+ /**
51
+ * Path to a project's feature_list.json.
52
+ * @param {string} targetDir
53
+ * @returns {string}
54
+ */
55
+ export function FEATURE_LIST_PATH(targetDir) {
56
+ return resolve(targetDir, 'feature_list.json');
57
+ }
58
+
59
+ /**
60
+ * Path to a project's sprint-contract.md.
61
+ * @param {string} targetDir
62
+ * @returns {string}
63
+ */
64
+ export function CONTRACT_PATH(targetDir) {
65
+ return resolve(targetDir, 'sprint-contract.md');
66
+ }
67
+
68
+ /**
69
+ * Path to a project's progress.md.
70
+ * @param {string} targetDir
71
+ * @returns {string}
72
+ */
73
+ export function PROGRESS_PATH(targetDir) {
74
+ return resolve(targetDir, 'progress.md');
75
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * phases — Phase pipeline definitions and pure transition logic.
3
+ *
4
+ * Extracted from state.mjs to separate the phase state machine (pure logic,
5
+ * no I/O) from config file I/O. state.mjs re-exports these for backward
6
+ * compatibility.
7
+ *
8
+ * Usage:
9
+ * import { PHASE_ORDER, getPhaseOrder, isValidTransition } from './phases.mjs';
10
+ */
11
+
12
+ /** Canonical phase pipeline order. */
13
+ export const PHASE_ORDER = [
14
+ 'init',
15
+ 'define',
16
+ 'plan',
17
+ 'build',
18
+ 'verify',
19
+ 'simplify',
20
+ 'review',
21
+ 'ship',
22
+ ];
23
+
24
+ /**
25
+ * Get the ordered list of enabled phases.
26
+ * Filters out SIMPLIFY unless explicitly enabled.
27
+ * @param {string[]} [enabled]
28
+ * @returns {string[]}
29
+ */
30
+ export function getPhaseOrder(enabled) {
31
+ if (enabled === undefined || enabled === null) {
32
+ // Default: all phases except simplify
33
+ return PHASE_ORDER.filter(p => p !== 'simplify');
34
+ }
35
+ if (Array.isArray(enabled)) {
36
+ return PHASE_ORDER.filter(p => enabled.includes(p));
37
+ }
38
+ // Fallback: default
39
+ return PHASE_ORDER.filter(p => p !== 'simplify');
40
+ }
41
+
42
+ /**
43
+ * Check if a phase transition is valid.
44
+ * @param {string|null} fromPhase — current phase (null = start)
45
+ * @param {string} toPhase — target phase
46
+ * @param {string[]} [enabled]
47
+ * @returns {boolean}
48
+ */
49
+ export function isValidTransition(fromPhase, toPhase, enabled) {
50
+ const order = getPhaseOrder(enabled);
51
+ if (!order.includes(toPhase)) {return false;}
52
+ if (fromPhase === null) {return order[0] === toPhase;}
53
+ // Re-running the same phase is always valid
54
+ if (fromPhase === toPhase) {return true;}
55
+ const fromIdx = order.indexOf(fromPhase);
56
+ const toIdx = order.indexOf(toPhase);
57
+ return toIdx === fromIdx + 1;
58
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * platform — Cross-platform detection and execution helpers.
3
+ *
4
+ * Usage:
5
+ * import { getPlatform, isWindows, crossExec } from './platform.mjs';
6
+ */
7
+ import { execSync } from 'node:child_process';
8
+
9
+ /**
10
+ * Detect the current platform.
11
+ * @returns {'linux'|'darwin'|'win32'}
12
+ */
13
+ export function getPlatform() {
14
+ return process.platform;
15
+ }
16
+
17
+ /**
18
+ * Check if running on Windows.
19
+ * @returns {boolean}
20
+ */
21
+ export function isWindows() {
22
+ return process.platform === 'win32';
23
+ }
24
+
25
+ /**
26
+ * Check if running on macOS.
27
+ * @returns {boolean}
28
+ */
29
+ export function isMacOS() {
30
+ return process.platform === 'darwin';
31
+ }
32
+
33
+ /**
34
+ * Shell-quote a string for the current platform.
35
+ * On Windows, uses double quotes. On Unix, wraps in single quotes (or double if shell chars present).
36
+ * @param {string} str
37
+ * @returns {string}
38
+ */
39
+ export function shellQuote(str) {
40
+ if (isWindows()) {
41
+ // Windows CMD: double quotes, doubled for literal
42
+ return `"${str.replace(/"/g, '""')}"`;
43
+ }
44
+ // Unix: single quotes, escaped for any single quotes in the string
45
+ if (str.includes("'")) {
46
+ return `"${str.replace(/"/g, '\\"')}"`;
47
+ }
48
+ return `'${str}'`;
49
+ }
50
+
51
+ /**
52
+ * Execute a command using the platform-appropriate shell.
53
+ * On Windows, uses 'cmd /c'. On Unix, uses 'sh -c'.
54
+ * @param {string} cmd
55
+ * @param {object} [options]
56
+ * @returns {{ stdout: string, stderr: string, exitCode: number }}
57
+ */
58
+ export function crossExec(cmd, options = {}) {
59
+ const shell = isWindows() ? 'cmd' : 'sh';
60
+ const shellFlag = isWindows() ? '/c' : '-c';
61
+ try {
62
+ // execSync with encoding:'utf-8' returns the stdout string directly
63
+ // (not an object with .stdout/.stderr).
64
+ const stdout = execSync(`${shell} ${shellFlag} ${shellQuote(cmd)}`, {
65
+ stdio: 'pipe',
66
+ encoding: 'utf-8',
67
+ timeout: options.timeout || 60000,
68
+ ...options,
69
+ });
70
+ return { stdout: stdout || '', stderr: '', exitCode: 0 };
71
+ } catch (err) {
72
+ return {
73
+ stdout: typeof err.stdout === 'string' ? err.stdout : (err.stdout?.toString() || ''),
74
+ stderr: typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() || err.message),
75
+ exitCode: err.status || 1,
76
+ };
77
+ }
78
+ }