@bvdm/delano 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/HANDBOOK.md +1511 -0
  2. package/README.md +122 -0
  3. package/assets/install-manifest.json +102 -0
  4. package/assets/payload/.agents/README.md +12 -0
  5. package/assets/payload/.agents/adapters/claude/README.md +5 -0
  6. package/assets/payload/.agents/adapters/codex/README.md +5 -0
  7. package/assets/payload/.agents/adapters/opencode/README.md +5 -0
  8. package/assets/payload/.agents/adapters/pi/README.md +5 -0
  9. package/assets/payload/.agents/common/README.md +3 -0
  10. package/assets/payload/.agents/hooks/README.md +11 -0
  11. package/assets/payload/.agents/hooks/bash-worktree-fix.sh +7 -0
  12. package/assets/payload/.agents/hooks/post-tool-logger.js +18 -0
  13. package/assets/payload/.agents/hooks/session-tracker.js +17 -0
  14. package/assets/payload/.agents/hooks/user-prompt-logger.js +18 -0
  15. package/assets/payload/.agents/logs/.gitkeep +0 -0
  16. package/assets/payload/.agents/logs/schema.md +42 -0
  17. package/assets/payload/.agents/rules/README.md +12 -0
  18. package/assets/payload/.agents/rules/agent-coordination.md +5 -0
  19. package/assets/payload/.agents/rules/datetime.md +5 -0
  20. package/assets/payload/.agents/rules/frontmatter-operations.md +6 -0
  21. package/assets/payload/.agents/rules/github-operations.md +5 -0
  22. package/assets/payload/.agents/rules/path-standards.md +5 -0
  23. package/assets/payload/.agents/rules/test-execution.md +5 -0
  24. package/assets/payload/.agents/rules/worktree-operations.md +5 -0
  25. package/assets/payload/.agents/scripts/README.md +31 -0
  26. package/assets/payload/.agents/scripts/check-path-standards.sh +26 -0
  27. package/assets/payload/.agents/scripts/fix-path-standards.sh +14 -0
  28. package/assets/payload/.agents/scripts/git-sparse-download.sh +162 -0
  29. package/assets/payload/.agents/scripts/log-event.js +33 -0
  30. package/assets/payload/.agents/scripts/log-event.sh +5 -0
  31. package/assets/payload/.agents/scripts/pm/blocked.sh +36 -0
  32. package/assets/payload/.agents/scripts/pm/epic-list.sh +34 -0
  33. package/assets/payload/.agents/scripts/pm/in-progress.sh +36 -0
  34. package/assets/payload/.agents/scripts/pm/init.sh +139 -0
  35. package/assets/payload/.agents/scripts/pm/next.sh +110 -0
  36. package/assets/payload/.agents/scripts/pm/prd-list.sh +34 -0
  37. package/assets/payload/.agents/scripts/pm/search.sh +13 -0
  38. package/assets/payload/.agents/scripts/pm/standup.sh +19 -0
  39. package/assets/payload/.agents/scripts/pm/status.sh +61 -0
  40. package/assets/payload/.agents/scripts/pm/validate.sh +309 -0
  41. package/assets/payload/.agents/scripts/query-log.sh +57 -0
  42. package/assets/payload/.agents/scripts/test-and-log.sh +28 -0
  43. package/assets/payload/.agents/skills/.gitkeep +0 -0
  44. package/assets/payload/.agents/skills/README.md +23 -0
  45. package/assets/payload/.agents/skills/breakdown-skill/SKILL.md +40 -0
  46. package/assets/payload/.agents/skills/breakdown-skill/references/runbook.md +16 -0
  47. package/assets/payload/.agents/skills/breakdown-skill/templates/ambiguity-report.md +11 -0
  48. package/assets/payload/.agents/skills/breakdown-skill/templates/task-batch-summary.md +11 -0
  49. package/assets/payload/.agents/skills/closeout-skill/SKILL.md +42 -0
  50. package/assets/payload/.agents/skills/closeout-skill/references/runbook.md +16 -0
  51. package/assets/payload/.agents/skills/closeout-skill/templates/closure-checklist.md +7 -0
  52. package/assets/payload/.agents/skills/closeout-skill/templates/outcome-review.md +11 -0
  53. package/assets/payload/.agents/skills/discovery-skill/SKILL.md +44 -0
  54. package/assets/payload/.agents/skills/discovery-skill/references/runbook.md +14 -0
  55. package/assets/payload/.agents/skills/discovery-skill/templates/clarification-questions.md +18 -0
  56. package/assets/payload/.agents/skills/discovery-skill/templates/discovery-summary.md +14 -0
  57. package/assets/payload/.agents/skills/execution-skill/SKILL.md +42 -0
  58. package/assets/payload/.agents/skills/execution-skill/references/runbook.md +16 -0
  59. package/assets/payload/.agents/skills/execution-skill/templates/blocker-update.md +13 -0
  60. package/assets/payload/.agents/skills/execution-skill/templates/stream-update.md +9 -0
  61. package/assets/payload/.agents/skills/learning-skill/SKILL.md +41 -0
  62. package/assets/payload/.agents/skills/learning-skill/references/runbook.md +13 -0
  63. package/assets/payload/.agents/skills/learning-skill/templates/improvement-backlog.md +10 -0
  64. package/assets/payload/.agents/skills/learning-skill/templates/retrospective.md +11 -0
  65. package/assets/payload/.agents/skills/planning-skill/SKILL.md +40 -0
  66. package/assets/payload/.agents/skills/planning-skill/references/runbook.md +15 -0
  67. package/assets/payload/.agents/skills/planning-skill/templates/architecture-decision.md +15 -0
  68. package/assets/payload/.agents/skills/planning-skill/templates/workstream-definition.md +13 -0
  69. package/assets/payload/.agents/skills/quality-skill/SKILL.md +40 -0
  70. package/assets/payload/.agents/skills/quality-skill/references/runbook.md +14 -0
  71. package/assets/payload/.agents/skills/quality-skill/templates/gate-decision.md +10 -0
  72. package/assets/payload/.agents/skills/quality-skill/templates/quality-evidence.md +16 -0
  73. package/assets/payload/.agents/skills/sync-skill/SKILL.md +41 -0
  74. package/assets/payload/.agents/skills/sync-skill/references/runbook.md +17 -0
  75. package/assets/payload/.agents/skills/sync-skill/templates/conflict-resolution-actions.md +10 -0
  76. package/assets/payload/.agents/skills/sync-skill/templates/drift-report.md +14 -0
  77. package/assets/payload/.delano/README.md +7 -0
  78. package/assets/payload/.gitattributes +14 -0
  79. package/assets/payload/.project/context/README.md +15 -0
  80. package/assets/payload/.project/context/gui-testing.md +20 -0
  81. package/assets/payload/.project/context/product-context.md +17 -0
  82. package/assets/payload/.project/context/progress.md +23 -0
  83. package/assets/payload/.project/context/project-brief.md +13 -0
  84. package/assets/payload/.project/context/project-overview.md +14 -0
  85. package/assets/payload/.project/context/project-structure.md +24 -0
  86. package/assets/payload/.project/context/project-style-guide.md +17 -0
  87. package/assets/payload/.project/context/system-patterns.md +22 -0
  88. package/assets/payload/.project/context/tech-context.md +19 -0
  89. package/assets/payload/.project/projects/.gitkeep +0 -0
  90. package/assets/payload/.project/registry/linear-map.json +6 -0
  91. package/assets/payload/.project/registry/migration-map.json +5 -0
  92. package/assets/payload/.project/templates/completion-summary.md +16 -0
  93. package/assets/payload/.project/templates/plan.md +30 -0
  94. package/assets/payload/.project/templates/progress-update.md +20 -0
  95. package/assets/payload/.project/templates/spec.md +42 -0
  96. package/assets/payload/.project/templates/task.md +33 -0
  97. package/assets/payload/.project/templates/workstream.md +19 -0
  98. package/assets/payload/HANDBOOK.md +1511 -0
  99. package/assets/payload/install-delano.sh +311 -0
  100. package/bin/delano.js +13 -0
  101. package/install-delano.sh +311 -0
  102. package/package.json +26 -0
  103. package/src/cli/commands/install.js +57 -0
  104. package/src/cli/commands/wrapper.js +26 -0
  105. package/src/cli/index.js +97 -0
  106. package/src/cli/lib/errors.js +11 -0
  107. package/src/cli/lib/install.js +261 -0
  108. package/src/cli/lib/pm.js +29 -0
  109. package/src/cli/lib/runtime.js +122 -0
@@ -0,0 +1,57 @@
1
+ const {
2
+ applyInstallPlan,
3
+ buildInstallPlan,
4
+ collectConflicts,
5
+ confirmInstall,
6
+ parseInstallArgs,
7
+ printConflicts,
8
+ printPlanSummary
9
+ } = require("../lib/install");
10
+
11
+ function getInstallHelp() {
12
+ return [
13
+ "Usage:",
14
+ " delano install [options]",
15
+ "",
16
+ "Options:",
17
+ " --target <dir> Install into the given directory. Defaults to the current working directory.",
18
+ " --agents <list> Comma-separated agent list for future opt-in adapter docs: claude,codex,opencode,pi.",
19
+ " --force Overwrite existing allowlisted target paths. Does not override parent-path blockers.",
20
+ " --yes Skip the final confirmation prompt.",
21
+ " -h, --help Show command help.",
22
+ "",
23
+ "Behavior:",
24
+ " - Computes the full install plan before writing files.",
25
+ " - Aborts on conflicts by default.",
26
+ " - Only installs the approved base payload; top-level adapter entry docs remain opt-in and are not installed in v1."
27
+ ].join("\n");
28
+ }
29
+
30
+ async function runInstall(args) {
31
+ const options = parseInstallArgs(args);
32
+ const plan = buildInstallPlan(options);
33
+ const conflicts = collectConflicts(plan);
34
+ const unforceableConflicts = conflicts.filter((conflict) => !conflict.forceable);
35
+ const blockingConflicts = options.force ? unforceableConflicts : conflicts;
36
+
37
+ printPlanSummary(plan, options);
38
+
39
+ if (blockingConflicts.length > 0) {
40
+ printConflicts(blockingConflicts, options);
41
+ return 1;
42
+ }
43
+
44
+ const confirmed = await confirmInstall(plan, options);
45
+ if (!confirmed) {
46
+ console.error("Install canceled.");
47
+ return 1;
48
+ }
49
+
50
+ applyInstallPlan(plan, options);
51
+ return 0;
52
+ }
53
+
54
+ module.exports = {
55
+ getInstallHelp,
56
+ runInstall
57
+ };
@@ -0,0 +1,26 @@
1
+ const { runPmScript } = require("../lib/pm");
2
+
3
+ function createWrapperCommand(scriptName) {
4
+ return {
5
+ description: `Run .agents/scripts/pm/${scriptName}.sh in the current Delano repository.`,
6
+ run(args) {
7
+ const passthrough = args[0] === "--" ? args.slice(1) : args;
8
+ return runPmScript(scriptName, passthrough);
9
+ },
10
+ help() {
11
+ return [
12
+ "Usage:",
13
+ ` delano ${scriptName} [-- <script-args>]`,
14
+ "",
15
+ "Behavior:",
16
+ ` - Resolves the current Delano repository by searching upward for .project/ and .agents/scripts/pm/.`,
17
+ ` - Runs .agents/scripts/pm/${scriptName}.sh through bash.`,
18
+ " - Pass '--' to make argument passthrough explicit when needed."
19
+ ].join("\n");
20
+ }
21
+ };
22
+ }
23
+
24
+ module.exports = {
25
+ createWrapperCommand
26
+ };
@@ -0,0 +1,97 @@
1
+ const { readFileSync } = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const { CliError } = require("./lib/errors");
5
+ const { getPackageRoot } = require("./lib/runtime");
6
+ const { runInstall, getInstallHelp } = require("./commands/install");
7
+ const { createWrapperCommand } = require("./commands/wrapper");
8
+
9
+ const wrapperCommands = {
10
+ init: createWrapperCommand("init"),
11
+ validate: createWrapperCommand("validate"),
12
+ status: createWrapperCommand("status"),
13
+ next: createWrapperCommand("next")
14
+ };
15
+
16
+ const commands = {
17
+ install: {
18
+ description: "Install the approved Delano runtime payload into a target repository.",
19
+ run: runInstall,
20
+ help: getInstallHelp
21
+ },
22
+ init: wrapperCommands.init,
23
+ validate: wrapperCommands.validate,
24
+ status: wrapperCommands.status,
25
+ next: wrapperCommands.next
26
+ };
27
+
28
+ function getPackageVersion() {
29
+ const packageJsonPath = path.join(getPackageRoot(), "package.json");
30
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
31
+ return packageJson.version;
32
+ }
33
+
34
+ function getGeneralHelp() {
35
+ return [
36
+ "Delano CLI",
37
+ "",
38
+ "Usage:",
39
+ " delano <command> [options]",
40
+ "",
41
+ "Commands:",
42
+ " install Install the approved Delano runtime payload",
43
+ " init Run .agents/scripts/pm/init.sh in the current Delano repo",
44
+ " validate Run .agents/scripts/pm/validate.sh in the current Delano repo",
45
+ " status Run .agents/scripts/pm/status.sh in the current Delano repo",
46
+ " next Run .agents/scripts/pm/next.sh in the current Delano repo",
47
+ "",
48
+ "Global options:",
49
+ " -h, --help Show help",
50
+ " -v, --version Show version",
51
+ "",
52
+ "Examples:",
53
+ " delano install --target ../my-repo --yes",
54
+ " delano validate",
55
+ " delano next -- --all",
56
+ "",
57
+ "Use 'delano <command> --help' for command-specific help."
58
+ ].join("\n");
59
+ }
60
+
61
+ async function run(argv) {
62
+ if (argv.length === 0) {
63
+ console.log(getGeneralHelp());
64
+ return 0;
65
+ }
66
+
67
+ const [commandName, ...commandArgs] = argv;
68
+
69
+ if (commandName === "-h" || commandName === "--help" || commandName === "help") {
70
+ console.log(getGeneralHelp());
71
+ return 0;
72
+ }
73
+
74
+ if (commandName === "-v" || commandName === "--version" || commandName === "version") {
75
+ console.log(getPackageVersion());
76
+ return 0;
77
+ }
78
+
79
+ const command = commands[commandName];
80
+ if (!command) {
81
+ throw new CliError(`Unknown command: ${commandName}\n\n${getGeneralHelp()}`, 1);
82
+ }
83
+
84
+ if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
85
+ const helpText = typeof command.help === "function" ? command.help() : getGeneralHelp();
86
+ console.log(helpText);
87
+ return 0;
88
+ }
89
+
90
+ return command.run(commandArgs);
91
+ }
92
+
93
+ module.exports = {
94
+ commands,
95
+ getGeneralHelp,
96
+ run
97
+ };
@@ -0,0 +1,11 @@
1
+ class CliError extends Error {
2
+ constructor(message, exitCode = 1) {
3
+ super(message);
4
+ this.name = "CliError";
5
+ this.exitCode = exitCode;
6
+ }
7
+ }
8
+
9
+ module.exports = {
10
+ CliError
11
+ };
@@ -0,0 +1,261 @@
1
+ const {
2
+ chmodSync,
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ statSync,
9
+ } = require("node:fs");
10
+ const path = require("node:path");
11
+ const readline = require("node:readline/promises");
12
+ const { stdin, stdout } = require("node:process");
13
+
14
+ const { CliError } = require("./errors");
15
+ const { getPackageRoot, getPathType } = require("./runtime");
16
+
17
+ const SUPPORTED_AGENTS = ["claude", "codex", "opencode", "pi"];
18
+
19
+ function readInstallManifest() {
20
+ const manifestPath = path.join(getPackageRoot(), "assets", "install-manifest.json");
21
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
22
+ return {
23
+ manifestPath,
24
+ manifest,
25
+ payloadRoot: path.join(getPackageRoot(), "assets", "payload")
26
+ };
27
+ }
28
+
29
+ function parseAgentList(rawValue) {
30
+ if (!rawValue) {
31
+ return [...SUPPORTED_AGENTS];
32
+ }
33
+
34
+ const selected = [];
35
+ for (const chunk of rawValue.split(",")) {
36
+ const value = chunk.trim().toLowerCase();
37
+ if (!value) {
38
+ continue;
39
+ }
40
+ if (!SUPPORTED_AGENTS.includes(value)) {
41
+ throw new CliError(
42
+ `Unknown agent '${value}'. Supported values: ${SUPPORTED_AGENTS.join(", ")}.`,
43
+ 1
44
+ );
45
+ }
46
+ if (!selected.includes(value)) {
47
+ selected.push(value);
48
+ }
49
+ }
50
+
51
+ if (selected.length === 0) {
52
+ throw new CliError("No agents selected. Pass --agents claude,codex,opencode,pi or omit the flag.", 1);
53
+ }
54
+
55
+ return selected;
56
+ }
57
+
58
+ function parseInstallArgs(args) {
59
+ const options = {
60
+ target: process.cwd(),
61
+ force: false,
62
+ yes: false,
63
+ agents: [...SUPPORTED_AGENTS]
64
+ };
65
+
66
+ for (let index = 0; index < args.length; index += 1) {
67
+ const arg = args[index];
68
+
69
+ if (arg === "--target") {
70
+ index += 1;
71
+ if (!args[index]) {
72
+ throw new CliError("Missing value for --target.", 1);
73
+ }
74
+ options.target = args[index];
75
+ continue;
76
+ }
77
+
78
+ if (arg.startsWith("--target=")) {
79
+ options.target = arg.slice("--target=".length);
80
+ continue;
81
+ }
82
+
83
+ if (arg === "--agents") {
84
+ index += 1;
85
+ if (!args[index]) {
86
+ throw new CliError("Missing value for --agents.", 1);
87
+ }
88
+ options.agents = parseAgentList(args[index]);
89
+ continue;
90
+ }
91
+
92
+ if (arg.startsWith("--agents=")) {
93
+ options.agents = parseAgentList(arg.slice("--agents=".length));
94
+ continue;
95
+ }
96
+
97
+ if (arg === "--force") {
98
+ options.force = true;
99
+ continue;
100
+ }
101
+
102
+ if (arg === "--yes") {
103
+ options.yes = true;
104
+ continue;
105
+ }
106
+
107
+ throw new CliError(`Unknown install option: ${arg}`, 1);
108
+ }
109
+
110
+ options.target = path.resolve(options.target);
111
+ return options;
112
+ }
113
+
114
+ function buildInstallPlan(options) {
115
+ const { manifest, payloadRoot } = readInstallManifest();
116
+ const items = manifest.paths.map((relativePath) => {
117
+ const sourcePath = path.join(payloadRoot, relativePath);
118
+ if (!existsSync(sourcePath)) {
119
+ throw new CliError(
120
+ `Packaged asset missing for '${relativePath}'. Run 'npm run build:assets' before using 'delano install' from a source checkout.`,
121
+ 1
122
+ );
123
+ }
124
+
125
+ return {
126
+ relativePath,
127
+ sourcePath,
128
+ targetPath: path.join(options.target, relativePath)
129
+ };
130
+ });
131
+
132
+ return {
133
+ manifest,
134
+ items,
135
+ targetRoot: options.target
136
+ };
137
+ }
138
+
139
+ function collectConflicts(plan) {
140
+ const conflicts = [];
141
+
142
+ for (const item of plan.items) {
143
+ let current = path.dirname(item.targetPath);
144
+ const targetRoot = path.parse(item.targetPath).root;
145
+
146
+ while (current && current !== targetRoot) {
147
+ const parentType = getPathType(current);
148
+ if (parentType && parentType !== "directory") {
149
+ conflicts.push({
150
+ relativePath: item.relativePath,
151
+ conflictPath: path.relative(plan.targetRoot, current) || current,
152
+ targetPath: current,
153
+ pathType: parentType,
154
+ reason: "a parent path exists as a non-directory and blocks install",
155
+ forceable: false
156
+ });
157
+ break;
158
+ }
159
+ current = path.dirname(current);
160
+ }
161
+
162
+ const exactType = getPathType(item.targetPath);
163
+ if (exactType) {
164
+ conflicts.push({
165
+ relativePath: item.relativePath,
166
+ conflictPath: item.relativePath,
167
+ targetPath: item.targetPath,
168
+ pathType: exactType,
169
+ reason: "target already exists and install would overwrite it",
170
+ forceable: true
171
+ });
172
+ }
173
+ }
174
+
175
+ return conflicts.sort((left, right) => {
176
+ if (left.relativePath === right.relativePath) {
177
+ return left.targetPath.localeCompare(right.targetPath);
178
+ }
179
+ return left.relativePath.localeCompare(right.relativePath);
180
+ });
181
+ }
182
+
183
+ function printPlanSummary(plan, options) {
184
+ console.log("Install plan");
185
+ console.log("------------");
186
+ console.log(`Target: ${options.target}`);
187
+ console.log(`Files: ${plan.items.length}`);
188
+ console.log(`Agents: ${options.agents.join(", ")}`);
189
+ console.log(`Force: ${options.force ? "yes" : "no"}`);
190
+ console.log("");
191
+ console.log("Note: --agents is accepted now for forward compatibility, but v1 base install still excludes top-level adapter entry docs by default.");
192
+ }
193
+
194
+ function printConflicts(conflicts, options) {
195
+ console.error("");
196
+ console.error("Conflicts");
197
+ console.error("---------");
198
+ for (const conflict of conflicts) {
199
+ const forceLabel = conflict.forceable ? "forceable" : "not forceable";
200
+ console.error(
201
+ `- ${conflict.conflictPath} [${conflict.pathType}; ${forceLabel}]: ${conflict.reason} (target: ${conflict.relativePath})`
202
+ );
203
+ }
204
+ console.error("");
205
+ if (options.force) {
206
+ console.error("Install cannot continue because at least one conflict is not forceable.");
207
+ } else {
208
+ console.error("Install aborted before writing files. Re-run with --force to overwrite only allowlisted target paths.");
209
+ }
210
+ }
211
+
212
+ async function confirmInstall(plan, options) {
213
+ if (options.yes) {
214
+ return true;
215
+ }
216
+
217
+ const rl = readline.createInterface({ input: stdin, output: stdout });
218
+ try {
219
+ const prompt = options.force
220
+ ? `Proceed with force-installing ${plan.items.length} files into ${options.target}? [y/N] `
221
+ : `Proceed with installing ${plan.items.length} files into ${options.target}? [y/N] `;
222
+ const answer = await rl.question(prompt);
223
+ return /^[Yy](es)?$/.test(answer.trim());
224
+ } finally {
225
+ rl.close();
226
+ }
227
+ }
228
+
229
+ function applyInstallPlan(plan, options) {
230
+ for (const item of plan.items) {
231
+ const existingType = getPathType(item.targetPath);
232
+ if (existingType) {
233
+ rmSync(item.targetPath, { recursive: true, force: true });
234
+ }
235
+
236
+ mkdirSync(path.dirname(item.targetPath), { recursive: true });
237
+ copyFileSync(item.sourcePath, item.targetPath);
238
+ const sourceMode = statSync(item.sourcePath).mode & 0o777;
239
+ try {
240
+ chmodSync(item.targetPath, sourceMode);
241
+ } catch {
242
+ // Ignore mode-setting failures on platforms that do not preserve POSIX modes.
243
+ }
244
+ }
245
+
246
+ console.log("");
247
+ console.log(`Installed ${plan.items.length} files into ${options.target}.`);
248
+ }
249
+
250
+ module.exports = {
251
+ SUPPORTED_AGENTS,
252
+ applyInstallPlan,
253
+ buildInstallPlan,
254
+ collectConflicts,
255
+ confirmInstall,
256
+ parseAgentList,
257
+ parseInstallArgs,
258
+ printConflicts,
259
+ printPlanSummary,
260
+ readInstallManifest
261
+ };
@@ -0,0 +1,29 @@
1
+ const path = require("node:path");
2
+
3
+ const { CliError } = require("./errors");
4
+ const { findDelanoRoot, runBashScript } = require("./runtime");
5
+
6
+ function resolvePmScript(scriptName, startDir = process.cwd()) {
7
+ const repoRoot = findDelanoRoot(startDir);
8
+ if (!repoRoot) {
9
+ throw new CliError(
10
+ "Could not find a Delano repository from the current working directory. Run this inside a repo containing .project/ and .agents/scripts/pm/, or install Delano first.",
11
+ 1
12
+ );
13
+ }
14
+
15
+ return {
16
+ repoRoot,
17
+ scriptPath: path.join(repoRoot, ".agents", "scripts", "pm", `${scriptName}.sh`)
18
+ };
19
+ }
20
+
21
+ function runPmScript(scriptName, args) {
22
+ const { repoRoot, scriptPath } = resolvePmScript(scriptName);
23
+ return runBashScript(scriptPath, args, { cwd: repoRoot });
24
+ }
25
+
26
+ module.exports = {
27
+ resolvePmScript,
28
+ runPmScript
29
+ };
@@ -0,0 +1,122 @@
1
+ const { existsSync, lstatSync } = require("node:fs");
2
+ const path = require("node:path");
3
+ const { spawnSync } = require("node:child_process");
4
+
5
+ const { CliError } = require("./errors");
6
+
7
+ function getPackageRoot() {
8
+ return path.resolve(__dirname, "../../..");
9
+ }
10
+
11
+ function getPathType(targetPath) {
12
+ try {
13
+ const stat = lstatSync(targetPath);
14
+ if (stat.isSymbolicLink()) {
15
+ return "symlink";
16
+ }
17
+ if (stat.isDirectory()) {
18
+ return "directory";
19
+ }
20
+ return "file";
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function findDelanoRoot(startDir = process.cwd()) {
27
+ let current = path.resolve(startDir);
28
+ const { root } = path.parse(current);
29
+
30
+ while (true) {
31
+ const hasRuntime = existsSync(path.join(current, ".agents", "scripts", "pm"));
32
+ const hasProject = existsSync(path.join(current, ".project"));
33
+ if (hasRuntime && hasProject) {
34
+ return current;
35
+ }
36
+
37
+ if (current === root) {
38
+ return null;
39
+ }
40
+
41
+ current = path.dirname(current);
42
+ }
43
+ }
44
+
45
+ function resolveBash() {
46
+ const candidates = [];
47
+
48
+ if (process.env.DELANO_BASH) {
49
+ candidates.push(process.env.DELANO_BASH);
50
+ }
51
+
52
+ if (process.platform === "win32") {
53
+ const whereResult = spawnSync("where.exe", ["bash"], {
54
+ encoding: "utf8",
55
+ stdio: ["ignore", "pipe", "ignore"]
56
+ });
57
+
58
+ if (whereResult.status === 0 && whereResult.stdout) {
59
+ for (const line of whereResult.stdout.split(/\r?\n/)) {
60
+ if (line.trim()) {
61
+ candidates.push(line.trim());
62
+ }
63
+ }
64
+ }
65
+
66
+ candidates.push(
67
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
68
+ "C:\\Program Files\\Git\\bin\\bash.exe"
69
+ );
70
+ } else {
71
+ const whichResult = spawnSync("which", ["bash"], {
72
+ encoding: "utf8",
73
+ stdio: ["ignore", "pipe", "ignore"]
74
+ });
75
+ if (whichResult.status === 0 && whichResult.stdout.trim()) {
76
+ candidates.push(whichResult.stdout.trim());
77
+ }
78
+ candidates.push("/usr/bin/bash", "/bin/bash");
79
+ }
80
+
81
+ for (const candidate of candidates) {
82
+ if (candidate && existsSync(candidate)) {
83
+ return candidate;
84
+ }
85
+ }
86
+
87
+ throw new CliError(
88
+ "Could not find a usable bash runtime. Install bash or set DELANO_BASH to its full path.",
89
+ 1
90
+ );
91
+ }
92
+
93
+ function runBashScript(scriptPath, args, options = {}) {
94
+ const bashPath = resolveBash();
95
+ const result = spawnSync(bashPath, [scriptPath, ...args], {
96
+ cwd: options.cwd || process.cwd(),
97
+ stdio: "inherit",
98
+ env: options.env || process.env
99
+ });
100
+
101
+ if (result.error) {
102
+ throw new CliError(`Failed to launch bash for ${path.basename(scriptPath)}: ${result.error.message}`, 1);
103
+ }
104
+
105
+ return typeof result.status === "number" ? result.status : 1;
106
+ }
107
+
108
+ function ensureDirectoryPath(targetPath) {
109
+ const kind = getPathType(targetPath);
110
+ if (kind && kind !== "directory") {
111
+ throw new CliError(`${targetPath} exists as a ${kind}, but a directory is required.`, 1);
112
+ }
113
+ }
114
+
115
+ module.exports = {
116
+ ensureDirectoryPath,
117
+ findDelanoRoot,
118
+ getPackageRoot,
119
+ getPathType,
120
+ resolveBash,
121
+ runBashScript
122
+ };