@bvdm/delano 0.2.3 → 0.2.4
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 +28 -3
- package/assets/install-manifest.json +2 -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 +18 -0
- package/assets/payload/.agents/scripts/README.md +1 -1
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +90 -2
- package/assets/payload/.agents/scripts/pm/status.sh +135 -28
- package/assets/payload/.agents/scripts/pm/validate.sh +2 -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/package.json +1 -1
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/viewer.js +2 -1
- package/src/cli/commands/wrapper.js +13 -2
- package/src/cli/index.js +1 -0
- package/src/cli/lib/install.js +179 -2
|
@@ -12,7 +12,16 @@ const { spawn, spawnSync } = require('node:child_process');
|
|
|
12
12
|
const repoRoot = path.resolve(process.env.DELANO_VIEWER_ROOT || path.resolve(__dirname, '..', '..'));
|
|
13
13
|
const projectRoot = path.join(repoRoot, '.project');
|
|
14
14
|
const publicRoot = path.join(__dirname, 'public');
|
|
15
|
-
const
|
|
15
|
+
const DEFAULT_PORT = 3977;
|
|
16
|
+
const MAX_PORT = 65535;
|
|
17
|
+
const MAX_PORT_ATTEMPTS = 100;
|
|
18
|
+
const startPort = normalizePort(process.env.DELANO_VIEWER_PORT || process.env.PORT, DEFAULT_PORT);
|
|
19
|
+
|
|
20
|
+
function normalizePort(value, fallback) {
|
|
21
|
+
const parsed = Number(value || fallback);
|
|
22
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_PORT) return fallback;
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
function isInside(parent, child) {
|
|
18
27
|
const rel = path.relative(parent, child);
|
|
@@ -332,6 +341,7 @@ function sendStatic(res, pathname) {
|
|
|
332
341
|
const ext = path.extname(resolved).toLowerCase();
|
|
333
342
|
const mimeMap = {
|
|
334
343
|
'.js': 'text/javascript',
|
|
344
|
+
'.jsx': 'text/javascript',
|
|
335
345
|
'.css': 'text/css',
|
|
336
346
|
'.svg': 'image/svg+xml',
|
|
337
347
|
'.png': 'image/png',
|
|
@@ -340,7 +350,7 @@ function sendStatic(res, pathname) {
|
|
|
340
350
|
'.webp': 'image/webp',
|
|
341
351
|
'.ico': 'image/x-icon',
|
|
342
352
|
};
|
|
343
|
-
const isText = ext === '.js' || ext === '.css' || ext === '.svg' || ext === '' || ext === '.html';
|
|
353
|
+
const isText = ext === '.js' || ext === '.jsx' || ext === '.css' || ext === '.svg' || ext === '' || ext === '.html';
|
|
344
354
|
const type = mimeMap[ext] || 'text/html';
|
|
345
355
|
const headers = isText ? { 'content-type': `${type}; charset=utf-8` } : { 'content-type': type };
|
|
346
356
|
res.writeHead(200, headers);
|
|
@@ -384,6 +394,37 @@ const server = http.createServer((req, res) => {
|
|
|
384
394
|
}
|
|
385
395
|
});
|
|
386
396
|
|
|
387
|
-
server
|
|
388
|
-
|
|
389
|
-
|
|
397
|
+
function listenWithPortFallback(server, firstPort, host = '127.0.0.1') {
|
|
398
|
+
let port = firstPort;
|
|
399
|
+
let attempts = 0;
|
|
400
|
+
|
|
401
|
+
const listen = () => {
|
|
402
|
+
server.once('error', onError);
|
|
403
|
+
server.listen(port, host);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const onError = (error) => {
|
|
407
|
+
if (error.code === 'EADDRINUSE' && port < MAX_PORT && attempts < MAX_PORT_ATTEMPTS) {
|
|
408
|
+
attempts += 1;
|
|
409
|
+
port += 1;
|
|
410
|
+
listen();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.error(`Failed to start Delano viewer on ${host}:${port}: ${error.message}`);
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const onListening = () => {
|
|
419
|
+
server.removeListener('error', onError);
|
|
420
|
+
const address = server.address();
|
|
421
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
422
|
+
const skipped = actualPort !== firstPort ? ` (${firstPort} was unavailable)` : '';
|
|
423
|
+
console.log(`Delano read-only viewer: http://${host}:${actualPort}${skipped}`);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
server.on('listening', onListening);
|
|
427
|
+
listen();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
listenWithPortFallback(server, startPort);
|
package/package.json
CHANGED
|
@@ -30,7 +30,7 @@ function getInstallHelp() {
|
|
|
30
30
|
" -h, --help Show command help.",
|
|
31
31
|
"",
|
|
32
32
|
"Categories:",
|
|
33
|
-
" agent-runtime, skills, viewer, project-context, project-templates,",
|
|
33
|
+
" agent-runtime, codex-hooks, skills, viewer, project-context, project-templates,",
|
|
34
34
|
" project-registry, project-projects, handbook, legacy-installer",
|
|
35
35
|
"",
|
|
36
36
|
"Behavior:",
|
|
@@ -38,6 +38,7 @@ function getInstallHelp() {
|
|
|
38
38
|
" - Aborts on conflicts by default.",
|
|
39
39
|
" - Filters the plan before conflict detection when --only or --exclude is used.",
|
|
40
40
|
" - Treats .project/context, .project/projects, and .project/registry as repo-owned state after install.",
|
|
41
|
+
" - Merges .codex/hooks.json when it exists; invalid or non-file hook configs are skipped without blocking install.",
|
|
41
42
|
" - Only installs the approved base payload; top-level adapter entry docs remain opt-in and are not installed in v1.",
|
|
42
43
|
"",
|
|
43
44
|
"Examples:",
|
|
@@ -43,7 +43,8 @@ function getViewerHelp() {
|
|
|
43
43
|
" -h, --help Show help",
|
|
44
44
|
"",
|
|
45
45
|
"Environment:",
|
|
46
|
-
" DELANO_VIEWER_PORT or PORT
|
|
46
|
+
" DELANO_VIEWER_PORT or PORT sets the starting port, defaulting to 3977.",
|
|
47
|
+
" If that port is busy, the viewer starts on the next available port."
|
|
47
48
|
].join("\n");
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -8,7 +8,7 @@ function createWrapperCommand(scriptName) {
|
|
|
8
8
|
return runPmScript(scriptName, passthrough);
|
|
9
9
|
},
|
|
10
10
|
help() {
|
|
11
|
-
|
|
11
|
+
const lines = [
|
|
12
12
|
"Usage:",
|
|
13
13
|
` delano ${scriptName} [-- <script-args>]`,
|
|
14
14
|
"",
|
|
@@ -16,7 +16,18 @@ function createWrapperCommand(scriptName) {
|
|
|
16
16
|
` - Resolves the current Delano repository by searching upward for .project/ and .agents/scripts/pm/.`,
|
|
17
17
|
` - Runs .agents/scripts/pm/${scriptName}.sh through bash.`,
|
|
18
18
|
" - Pass '--' to make argument passthrough explicit when needed."
|
|
19
|
-
]
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (scriptName === "status") {
|
|
22
|
+
lines.push(
|
|
23
|
+
"",
|
|
24
|
+
"Status examples:",
|
|
25
|
+
" delano status --open --brief",
|
|
26
|
+
" delano status -- --open --brief"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return lines.join("\n");
|
|
20
31
|
}
|
|
21
32
|
};
|
|
22
33
|
}
|
package/src/cli/index.js
CHANGED
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
|
};
|