@bvdm/delano 0.2.3 → 0.2.5
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/.delano/viewer/README.md +3 -2
- package/.delano/viewer/public/app.js +13 -1
- package/.delano/viewer/public/app.jsx +2312 -0
- package/.delano/viewer/public/delano-mark.svg +4 -0
- package/.delano/viewer/public/index.html +12 -14
- package/.delano/viewer/public/styles.css +1005 -833
- package/.delano/viewer/server.js +46 -5
- package/README.md +63 -3
- package/assets/install-manifest.json +7 -0
- package/assets/payload/.agents/adapters/manifest.schema.json +103 -0
- package/assets/payload/.agents/adapters/spec-kit/adapter.json +71 -0
- package/assets/payload/.agents/hooks/README.md +6 -1
- package/assets/payload/.agents/hooks/codex-session-status.js +123 -0
- package/assets/payload/.agents/schemas/status-transitions.json +35 -0
- package/assets/payload/.agents/scripts/README.md +1 -1
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +171 -2
- package/assets/payload/.agents/scripts/pm/import-spec-kit.sh +605 -0
- package/assets/payload/.agents/scripts/pm/init.sh +31 -2
- package/assets/payload/.agents/scripts/pm/research.sh +296 -0
- package/assets/payload/.agents/scripts/pm/status.sh +135 -28
- package/assets/payload/.agents/scripts/pm/validate.sh +16 -0
- package/assets/payload/.codex/hooks.json +17 -0
- package/assets/payload/.delano/viewer/README.md +3 -2
- package/assets/payload/.delano/viewer/public/app.js +13 -1
- package/assets/payload/.delano/viewer/public/index.html +12 -14
- package/assets/payload/.delano/viewer/public/styles.css +1005 -833
- package/assets/payload/.delano/viewer/server.js +46 -5
- package/assets/payload/.project/templates/decisions.md +18 -0
- package/assets/payload/.project/templates/plan.md +17 -0
- package/assets/payload/.project/templates/spec.md +12 -0
- package/assets/payload/.project/templates/task.md +6 -0
- package/assets/payload/.project/templates/workstream.md +1 -0
- package/package.json +4 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/state.js +689 -0
- package/src/cli/commands/viewer.js +2 -1
- package/src/cli/commands/wrapper.js +29 -5
- package/src/cli/index.js +120 -7
- package/src/cli/lib/install.js +179 -2
- package/src/cli/lib/project-state.js +918 -0
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
1
3
|
const { runPmScript } = require("../lib/pm");
|
|
2
4
|
|
|
3
|
-
function createWrapperCommand(scriptName) {
|
|
5
|
+
function createWrapperCommand(scriptName, options = {}) {
|
|
4
6
|
return {
|
|
5
|
-
description: `Run .agents/scripts/pm/${scriptName}.sh in the current Delano repository.`,
|
|
7
|
+
description: options.description || `Run .agents/scripts/pm/${scriptName}.sh in the current Delano repository.`,
|
|
6
8
|
run(args) {
|
|
7
9
|
const passthrough = args[0] === "--" ? args.slice(1) : args;
|
|
8
|
-
return runPmScript(scriptName, passthrough);
|
|
10
|
+
return runPmScript(scriptName, normalizePassthrough(scriptName, passthrough));
|
|
9
11
|
},
|
|
10
12
|
help() {
|
|
11
|
-
|
|
13
|
+
if (typeof options.help === "function") {
|
|
14
|
+
return options.help();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const lines = [
|
|
12
18
|
"Usage:",
|
|
13
19
|
` delano ${scriptName} [-- <script-args>]`,
|
|
14
20
|
"",
|
|
@@ -16,11 +22,29 @@ function createWrapperCommand(scriptName) {
|
|
|
16
22
|
` - Resolves the current Delano repository by searching upward for .project/ and .agents/scripts/pm/.`,
|
|
17
23
|
` - Runs .agents/scripts/pm/${scriptName}.sh through bash.`,
|
|
18
24
|
" - Pass '--' to make argument passthrough explicit when needed."
|
|
19
|
-
]
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (scriptName === "status") {
|
|
28
|
+
lines.push(
|
|
29
|
+
"",
|
|
30
|
+
"Status examples:",
|
|
31
|
+
" delano status --open --brief",
|
|
32
|
+
" delano status -- --open --brief"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return lines.join("\n");
|
|
20
37
|
}
|
|
21
38
|
};
|
|
22
39
|
}
|
|
23
40
|
|
|
41
|
+
function normalizePassthrough(scriptName, args) {
|
|
42
|
+
if (scriptName !== "import-spec-kit" || args.length < 2 || path.isAbsolute(args[1])) {
|
|
43
|
+
return args;
|
|
44
|
+
}
|
|
45
|
+
return [args[0], path.resolve(process.cwd(), args[1]), ...args.slice(2)];
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
module.exports = {
|
|
25
49
|
createWrapperCommand
|
|
26
50
|
};
|
package/src/cli/index.js
CHANGED
|
@@ -7,9 +7,28 @@ const { getOnboardingHelp, runOnboarding } = require("./commands/onboarding");
|
|
|
7
7
|
const { runInstall, getInstallHelp } = require("./commands/install");
|
|
8
8
|
const { runViewer, getViewerHelp } = require("./commands/viewer");
|
|
9
9
|
const { createWrapperCommand } = require("./commands/wrapper");
|
|
10
|
+
const {
|
|
11
|
+
getProjectHelp,
|
|
12
|
+
getTaskHelp,
|
|
13
|
+
getUpdateHelp,
|
|
14
|
+
getWorkstreamHelp,
|
|
15
|
+
parseTaskArgs,
|
|
16
|
+
runProjectCommand,
|
|
17
|
+
runTaskCommand,
|
|
18
|
+
runUpdateCommand,
|
|
19
|
+
runWorkstreamCommand
|
|
20
|
+
} = require("./commands/state");
|
|
10
21
|
|
|
11
22
|
const wrapperCommands = {
|
|
12
23
|
init: createWrapperCommand("init"),
|
|
24
|
+
"import-spec-kit": createWrapperCommand("import-spec-kit", {
|
|
25
|
+
description: "Create a planned Delano project from a Spec Kit-style markdown artifact.",
|
|
26
|
+
help: getImportSpecKitHelp
|
|
27
|
+
}),
|
|
28
|
+
research: createWrapperCommand("research", {
|
|
29
|
+
description: "Create repo-native research intake files for a Delano project.",
|
|
30
|
+
help: getResearchHelp
|
|
31
|
+
}),
|
|
13
32
|
validate: createWrapperCommand("validate"),
|
|
14
33
|
status: createWrapperCommand("status"),
|
|
15
34
|
next: createWrapperCommand("next")
|
|
@@ -31,18 +50,36 @@ const commands = {
|
|
|
31
50
|
run: runViewer,
|
|
32
51
|
help: getViewerHelp
|
|
33
52
|
},
|
|
53
|
+
project: {
|
|
54
|
+
description: "Create, show, and patch Delano project contracts.",
|
|
55
|
+
run: runProjectCommand,
|
|
56
|
+
help: getProjectHelp
|
|
57
|
+
},
|
|
58
|
+
workstream: {
|
|
59
|
+
description: "Add and patch Delano workstream contracts.",
|
|
60
|
+
run: runWorkstreamCommand,
|
|
61
|
+
help: getWorkstreamHelp
|
|
62
|
+
},
|
|
63
|
+
task: {
|
|
64
|
+
description: "Add and patch Delano task contracts with scoped lifecycle rollups.",
|
|
65
|
+
run: runTaskCommand,
|
|
66
|
+
help: getTaskHelp
|
|
67
|
+
},
|
|
68
|
+
update: {
|
|
69
|
+
description: "Add project progress updates from the project update template.",
|
|
70
|
+
run: runUpdateCommand,
|
|
71
|
+
help: getUpdateHelp
|
|
72
|
+
},
|
|
34
73
|
init: wrapperCommands.init,
|
|
74
|
+
"import-spec-kit": wrapperCommands["import-spec-kit"],
|
|
75
|
+
research: wrapperCommands.research,
|
|
35
76
|
validate: wrapperCommands.validate,
|
|
36
77
|
status: wrapperCommands.status,
|
|
37
78
|
next: wrapperCommands.next
|
|
38
79
|
};
|
|
39
80
|
|
|
40
81
|
function isInstallShorthand(argv) {
|
|
41
|
-
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return argv[0].startsWith("-");
|
|
82
|
+
return argv.length > 0 && argv[0].startsWith("-");
|
|
46
83
|
}
|
|
47
84
|
|
|
48
85
|
function resolveInvocation(argv) {
|
|
@@ -99,8 +136,14 @@ function getGeneralHelp() {
|
|
|
99
136
|
" onboarding Analyze AGENTS.md with the approval-first onboarding skill",
|
|
100
137
|
" install Install the approved Delano runtime payload",
|
|
101
138
|
" viewer Launch the read-only local UI for .project contracts",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
139
|
+
" project Create, show, and patch project contracts",
|
|
140
|
+
" workstream Add and patch workstream contracts",
|
|
141
|
+
" task Add and patch task contracts with scoped lifecycle rollups",
|
|
142
|
+
" update Add a progress update from .project/templates",
|
|
143
|
+
" init Run .agents/scripts/pm/init.sh in the current Delano repo",
|
|
144
|
+
" import-spec-kit Create a planned project from a Spec Kit-style markdown artifact",
|
|
145
|
+
" research Create repo-native research intake files for a project",
|
|
146
|
+
" validate Run .agents/scripts/pm/validate.sh in the current Delano repo",
|
|
104
147
|
" status Run .agents/scripts/pm/status.sh in the current Delano repo",
|
|
105
148
|
" next Run .agents/scripts/pm/next.sh in the current Delano repo",
|
|
106
149
|
"",
|
|
@@ -115,7 +158,16 @@ function getGeneralHelp() {
|
|
|
115
158
|
" delano --target ../my-repo --yes",
|
|
116
159
|
" npx -y @bvdm/delano@latest --yes",
|
|
117
160
|
" delano viewer",
|
|
161
|
+
" delano project create my-project --name \"My Project\" --owner team",
|
|
162
|
+
" delano workstream add my-project WS-A --name \"API Foundation\" --owner backend-team",
|
|
163
|
+
" delano task add my-project T-001 --name \"Build endpoint\" --workstream WS-A",
|
|
164
|
+
" delano task start my-project T-001",
|
|
165
|
+
" delano task close my-project T-001 --evidence \"Implemented and tested\"",
|
|
166
|
+
" delano update add my-project --message \"Implemented endpoint smoke test\"",
|
|
167
|
+
" delano import-spec-kit reminder-email-preferences docs/spec-kit/fixtures/minimal-spec-kit-project.md --json",
|
|
168
|
+
" delano research my-project api-options --title \"API options\" --question \"Which API shape should we use?\" --json",
|
|
118
169
|
" delano validate",
|
|
170
|
+
" delano status --open --brief",
|
|
119
171
|
" delano next -- --all",
|
|
120
172
|
"",
|
|
121
173
|
"Shorthand:",
|
|
@@ -128,6 +180,63 @@ function getGeneralHelp() {
|
|
|
128
180
|
].join("\n");
|
|
129
181
|
}
|
|
130
182
|
|
|
183
|
+
function getResearchHelp() {
|
|
184
|
+
return [
|
|
185
|
+
"Usage:",
|
|
186
|
+
" delano research <project-slug> <research-slug> [options]",
|
|
187
|
+
"",
|
|
188
|
+
"Creates repo-native research intake files under .project/projects/<project-slug>/research/<research-slug>/, then runs Delano validation by default.",
|
|
189
|
+
"",
|
|
190
|
+
"Arguments:",
|
|
191
|
+
" project-slug Existing Delano project slug",
|
|
192
|
+
" research-slug Research folder slug in kebab-case",
|
|
193
|
+
"",
|
|
194
|
+
"Options:",
|
|
195
|
+
" --title <title> Human-readable research title",
|
|
196
|
+
" --question <question> Primary research question",
|
|
197
|
+
" --owner <owner> Research owner, defaults to team",
|
|
198
|
+
" --no-validate Create artifacts without running Delano validation",
|
|
199
|
+
" --json Print a single machine-readable JSON result",
|
|
200
|
+
" -h, --help Show help",
|
|
201
|
+
"",
|
|
202
|
+
"Agent examples:",
|
|
203
|
+
" delano research delano-spec-kit-interop import-edge-cases --title \"Import edge cases\" --question \"Which inputs should block import?\" --json",
|
|
204
|
+
"",
|
|
205
|
+
"Output:",
|
|
206
|
+
" Human mode prints a concise summary plus validation output.",
|
|
207
|
+
" JSON mode prints: { ok, command, project, research, files, validation }."
|
|
208
|
+
].join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getImportSpecKitHelp() {
|
|
212
|
+
return [
|
|
213
|
+
"Usage:",
|
|
214
|
+
" delano import-spec-kit <slug> <source-md> [options]",
|
|
215
|
+
"",
|
|
216
|
+
"Creates a planned Delano project from the first supported Spec Kit-style markdown fixture, then runs Delano validation by default.",
|
|
217
|
+
"",
|
|
218
|
+
"Arguments:",
|
|
219
|
+
" slug Target Delano project slug in kebab-case",
|
|
220
|
+
" source-md Path to the markdown source artifact",
|
|
221
|
+
"",
|
|
222
|
+
"Options:",
|
|
223
|
+
" --name <project-name> Project name override",
|
|
224
|
+
" --owner <owner> Project owner, defaults to team",
|
|
225
|
+
" --lead <lead> Project lead, defaults to owner",
|
|
226
|
+
" --no-validate Create artifacts without running Delano validation",
|
|
227
|
+
" --json Print a single machine-readable JSON result",
|
|
228
|
+
" -h, --help Show help",
|
|
229
|
+
"",
|
|
230
|
+
"Agent examples:",
|
|
231
|
+
" delano import-spec-kit reminder-email-preferences docs/spec-kit/fixtures/minimal-spec-kit-project.md --json",
|
|
232
|
+
" delano import-spec-kit reminder-email-preferences input.md --name \"Reminder Email Preferences\" --owner platform --lead clark --json",
|
|
233
|
+
"",
|
|
234
|
+
"Output:",
|
|
235
|
+
" Human mode prints a concise summary plus validation output.",
|
|
236
|
+
" JSON mode prints: { ok, command, project, source, validation }."
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
131
240
|
async function run(argv) {
|
|
132
241
|
const invocation = resolveInvocation(argv);
|
|
133
242
|
|
|
@@ -155,6 +264,10 @@ async function run(argv) {
|
|
|
155
264
|
module.exports = {
|
|
156
265
|
commands,
|
|
157
266
|
getGeneralHelp,
|
|
267
|
+
getImportSpecKitHelp,
|
|
268
|
+
getResearchHelp,
|
|
269
|
+
getTaskHelp,
|
|
270
|
+
parseTaskArgs,
|
|
158
271
|
resolveInvocation,
|
|
159
272
|
run
|
|
160
273
|
};
|
package/src/cli/lib/install.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
readFileSync,
|
|
7
7
|
rmSync,
|
|
8
8
|
statSync,
|
|
9
|
+
writeFileSync,
|
|
9
10
|
} = require("node:fs");
|
|
10
11
|
const path = require("node:path");
|
|
11
12
|
const readline = require("node:readline");
|
|
@@ -14,8 +15,16 @@ const { stdin, stdout } = require("node:process");
|
|
|
14
15
|
const { CliError } = require("./errors");
|
|
15
16
|
const { getPackageRoot, getPathType } = require("./runtime");
|
|
16
17
|
|
|
18
|
+
const CODEX_HOOKS_TARGET = ".codex/hooks.json";
|
|
19
|
+
const CODEX_SESSION_STATUS_SCRIPT = ".agents/hooks/codex-session-status.js";
|
|
20
|
+
|
|
17
21
|
const SUPPORTED_AGENTS = ["claude", "codex", "opencode", "pi"];
|
|
18
22
|
const INSTALL_CATEGORIES = [
|
|
23
|
+
{
|
|
24
|
+
name: "codex-hooks",
|
|
25
|
+
description: ".codex hook configuration and SessionStart shim",
|
|
26
|
+
matches: (target) => target.startsWith(".codex/") || target === CODEX_SESSION_STATUS_SCRIPT
|
|
27
|
+
},
|
|
19
28
|
{
|
|
20
29
|
name: "agent-runtime",
|
|
21
30
|
description: ".agents runtime except skills",
|
|
@@ -67,6 +76,8 @@ const INSTALL_CATEGORY_ALIASES = new Map([
|
|
|
67
76
|
["agent-skills", "skills"],
|
|
68
77
|
["agents", "agent-runtime"],
|
|
69
78
|
["runtime", "agent-runtime"],
|
|
79
|
+
["codex", "codex-hooks"],
|
|
80
|
+
["codex-config", "codex-hooks"],
|
|
70
81
|
["context", "project-context"],
|
|
71
82
|
["templates", "project-templates"],
|
|
72
83
|
["project-state", "project-projects"],
|
|
@@ -531,6 +542,9 @@ function collectConflicts(plan) {
|
|
|
531
542
|
|
|
532
543
|
const exactType = getPathType(item.targetPath);
|
|
533
544
|
if (exactType) {
|
|
545
|
+
if (isNonBlockingExistingTarget(item.relativePath)) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
534
548
|
conflicts.push({
|
|
535
549
|
relativePath: item.relativePath,
|
|
536
550
|
conflictPath: item.relativePath,
|
|
@@ -566,6 +580,7 @@ function printPlanSummary(plan, options) {
|
|
|
566
580
|
console.log(`Force: ${options.force ? "yes" : "no"}`);
|
|
567
581
|
console.log("");
|
|
568
582
|
console.log("Note: --agents is accepted now for forward compatibility, but v1 base install still excludes top-level adapter entry docs by default.");
|
|
583
|
+
console.log("Note: .codex/hooks.json is merged when it already exists, and Codex runs the hook only after hooks are enabled and trusted.");
|
|
569
584
|
console.log("Note: .project/context, .project/projects, and .project/registry are repo-owned after install; use --no-project-state or --only for update-safe refreshes.");
|
|
570
585
|
}
|
|
571
586
|
|
|
@@ -605,7 +620,20 @@ async function confirmInstall(plan, options) {
|
|
|
605
620
|
}
|
|
606
621
|
|
|
607
622
|
function applyInstallPlan(plan, options) {
|
|
623
|
+
let appliedCount = 0;
|
|
624
|
+
let skippedCount = 0;
|
|
625
|
+
|
|
608
626
|
for (const item of plan.items) {
|
|
627
|
+
if (item.relativePath === CODEX_HOOKS_TARGET) {
|
|
628
|
+
const result = applyCodexHooksConfig(item);
|
|
629
|
+
if (result === "skipped") {
|
|
630
|
+
skippedCount += 1;
|
|
631
|
+
} else {
|
|
632
|
+
appliedCount += 1;
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
609
637
|
const existingType = getPathType(item.targetPath);
|
|
610
638
|
if (existingType) {
|
|
611
639
|
rmSync(item.targetPath, { recursive: true, force: true });
|
|
@@ -619,13 +647,161 @@ function applyInstallPlan(plan, options) {
|
|
|
619
647
|
} catch {
|
|
620
648
|
// Ignore mode-setting failures on platforms that do not preserve POSIX modes.
|
|
621
649
|
}
|
|
650
|
+
appliedCount += 1;
|
|
622
651
|
}
|
|
623
652
|
|
|
624
653
|
console.log("");
|
|
625
|
-
console.log(`Installed ${
|
|
654
|
+
console.log(`Installed or updated ${appliedCount} files into ${options.target}.`);
|
|
655
|
+
if (skippedCount > 0) {
|
|
656
|
+
console.log(`Skipped ${skippedCount} non-blocking file(s).`);
|
|
657
|
+
}
|
|
658
|
+
if (plan.items.some((item) => item.relativePath === CODEX_HOOKS_TARGET)) {
|
|
659
|
+
console.log("");
|
|
660
|
+
console.log("Codex hook config installed or merged at .codex/hooks.json.");
|
|
661
|
+
console.log("To activate it, enable Codex hooks, then approve the project and hook trust prompts.");
|
|
662
|
+
console.log("For one session, run: codex --enable hooks");
|
|
663
|
+
console.log("For persistent config, set [features].hooks = true in ~/.codex/config.toml.");
|
|
664
|
+
}
|
|
626
665
|
console.log("Recommended next step: run 'delano onboarding' to review AGENTS.md. The command asks for explicit approval before analysis.");
|
|
627
666
|
}
|
|
628
667
|
|
|
668
|
+
function isNonBlockingExistingTarget(relativePath) {
|
|
669
|
+
return relativePath === CODEX_HOOKS_TARGET;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function applyCodexHooksConfig(item) {
|
|
673
|
+
const existingType = getPathType(item.targetPath);
|
|
674
|
+
if (!existingType) {
|
|
675
|
+
mkdirSync(path.dirname(item.targetPath), { recursive: true });
|
|
676
|
+
copyFileSync(item.sourcePath, item.targetPath);
|
|
677
|
+
applySourceMode(item.sourcePath, item.targetPath);
|
|
678
|
+
return "installed";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (existingType !== "file") {
|
|
682
|
+
console.warn(`Skipped ${CODEX_HOOKS_TARGET}: existing ${existingType} cannot be merged safely.`);
|
|
683
|
+
return "skipped";
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let existingConfig;
|
|
687
|
+
try {
|
|
688
|
+
existingConfig = readJsonFile(item.targetPath);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.warn(`Skipped ${CODEX_HOOKS_TARGET}: existing file is not valid JSON (${error.message}).`);
|
|
691
|
+
return "skipped";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const packagedConfig = readJsonFile(item.sourcePath);
|
|
695
|
+
const mergeResult = mergeCodexHooksConfig(existingConfig, packagedConfig);
|
|
696
|
+
if (!mergeResult.ok) {
|
|
697
|
+
console.warn(`Skipped ${CODEX_HOOKS_TARGET}: ${mergeResult.reason}`);
|
|
698
|
+
return "skipped";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (mergeResult.changed) {
|
|
702
|
+
writeFileSync(item.targetPath, `${JSON.stringify(mergeResult.config, null, 2)}\n`, "utf8");
|
|
703
|
+
return "merged";
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return "unchanged";
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function mergeCodexHooksConfig(existingConfig, packagedConfig) {
|
|
710
|
+
if (!isPlainObject(existingConfig)) {
|
|
711
|
+
return { ok: false, reason: "existing config must be a JSON object." };
|
|
712
|
+
}
|
|
713
|
+
if (!isPlainObject(packagedConfig) || !isPlainObject(packagedConfig.hooks)) {
|
|
714
|
+
throw new CliError("Packaged .codex/hooks.json is missing a hooks object.", 1);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const packagedSessionStart = packagedConfig.hooks.SessionStart;
|
|
718
|
+
if (!Array.isArray(packagedSessionStart)) {
|
|
719
|
+
throw new CliError("Packaged .codex/hooks.json is missing hooks.SessionStart.", 1);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const nextConfig = deepClone(existingConfig);
|
|
723
|
+
if (nextConfig.hooks === undefined) {
|
|
724
|
+
nextConfig.hooks = {};
|
|
725
|
+
}
|
|
726
|
+
if (!isPlainObject(nextConfig.hooks)) {
|
|
727
|
+
return { ok: false, reason: "existing hooks field must be a JSON object." };
|
|
728
|
+
}
|
|
729
|
+
if (nextConfig.hooks.SessionStart === undefined) {
|
|
730
|
+
nextConfig.hooks.SessionStart = [];
|
|
731
|
+
}
|
|
732
|
+
if (!Array.isArray(nextConfig.hooks.SessionStart)) {
|
|
733
|
+
return { ok: false, reason: "existing hooks.SessionStart field must be an array." };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
let changed = false;
|
|
737
|
+
for (const desiredGroup of packagedSessionStart) {
|
|
738
|
+
if (!hasDelanoSessionStatusHook(nextConfig.hooks.SessionStart, desiredGroup)) {
|
|
739
|
+
nextConfig.hooks.SessionStart.push(deepClone(desiredGroup));
|
|
740
|
+
changed = true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
ok: true,
|
|
746
|
+
changed,
|
|
747
|
+
config: nextConfig
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function hasDelanoSessionStatusHook(sessionStartGroups, desiredGroup) {
|
|
752
|
+
const desiredCommands = collectHookCommands([desiredGroup]);
|
|
753
|
+
for (const group of sessionStartGroups) {
|
|
754
|
+
const commands = collectHookCommands([group]);
|
|
755
|
+
if (commands.some((command) => command.includes("codex-session-status.js"))) {
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
if (commands.some((command) => desiredCommands.includes(command))) {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function collectHookCommands(groups) {
|
|
766
|
+
const commands = [];
|
|
767
|
+
for (const group of groups) {
|
|
768
|
+
if (!group || !Array.isArray(group.hooks)) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
for (const hook of group.hooks) {
|
|
772
|
+
if (hook && typeof hook.command === "string") {
|
|
773
|
+
commands.push(hook.command);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return commands;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function applySourceMode(sourcePath, targetPath) {
|
|
781
|
+
const sourceMode = statSync(sourcePath).mode & 0o777;
|
|
782
|
+
try {
|
|
783
|
+
chmodSync(targetPath, sourceMode);
|
|
784
|
+
} catch {
|
|
785
|
+
// Ignore mode-setting failures on platforms that do not preserve POSIX modes.
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function deepClone(value) {
|
|
790
|
+
return JSON.parse(JSON.stringify(value));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function isPlainObject(value) {
|
|
794
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function readJsonFile(filePath) {
|
|
798
|
+
return JSON.parse(stripByteOrderMark(readFileSync(filePath, "utf8")));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function stripByteOrderMark(text) {
|
|
802
|
+
return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
|
|
803
|
+
}
|
|
804
|
+
|
|
629
805
|
function normalizeManifestEntries(rawManifest) {
|
|
630
806
|
const entries = Array.isArray(rawManifest.files) ? rawManifest.files : rawManifest.paths;
|
|
631
807
|
if (!Array.isArray(entries)) {
|
|
@@ -686,5 +862,6 @@ module.exports = {
|
|
|
686
862
|
printConflicts,
|
|
687
863
|
printPlanSummary,
|
|
688
864
|
readInstallManifest,
|
|
689
|
-
getMissingPackagedAssetMessage
|
|
865
|
+
getMissingPackagedAssetMessage,
|
|
866
|
+
mergeCodexHooksConfig
|
|
690
867
|
};
|