@indigoai-us/hq-cli 5.90.0 → 5.92.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.
- package/CHANGELOG.md +26 -0
- package/dist/commands/core-checkpoint.d.ts +1 -1
- package/dist/commands/core-checkpoint.js +27 -8
- package/dist/commands/core.js +3 -7
- package/dist/lib/core-utils/codex-skill-bridge-entry.d.ts +13 -0
- package/dist/lib/core-utils/codex-skill-bridge-entry.js +48 -0
- package/dist/lib/core-utils/codex-skill-bridge.d.ts +31 -0
- package/dist/lib/core-utils/codex-skill-bridge.js +505 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.92.0]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `hq core codex-skill-bridge` now runs as native, tested TypeScript instead of
|
|
10
|
+
a bundled shell asset, completing the `hq core` utility migration. Install,
|
|
11
|
+
status, doctor, dry-run, stale-HQ-root repair, blocked-target refusal, legacy
|
|
12
|
+
`.Codex` handling, and the local-config path rewrite are all preserved and
|
|
13
|
+
proven byte-identical to the shell implementation by a differential suite
|
|
14
|
+
covering sixteen scenarios. The scaffold script still ships for loose HQ
|
|
15
|
+
trees. (#325)
|
|
16
|
+
|
|
17
|
+
## [5.91.0]
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The `hq core checkpoint` maintenance sibling now curates the policy and
|
|
22
|
+
knowledge stores instead of only appending to them. It searches the existing
|
|
23
|
+
policies first and then amends, supersedes, merges or creates, and corrects a
|
|
24
|
+
stale durable fact in place rather than shadowing it with a contradictory
|
|
25
|
+
copy. Deletion is bounded: only as part of a merge or supersede, only inside
|
|
26
|
+
the policy directories the sibling could already write, never a HARD-marked
|
|
27
|
+
policy (those are flagged in the report for a human), and never a file it has
|
|
28
|
+
not read. Every amendment, supersession, merge and deletion must be named in
|
|
29
|
+
the run report with its reason. (#320)
|
|
30
|
+
|
|
5
31
|
## [5.90.0]
|
|
6
32
|
|
|
7
33
|
### Changed
|
|
@@ -11,7 +11,7 @@ import { Command } from "commander";
|
|
|
11
11
|
* Kept in TypeScript rather than in a bundled asset: it is an instruction to
|
|
12
12
|
* a locally-installed agent, not a scaffold script that should be packaged.
|
|
13
13
|
*/
|
|
14
|
-
export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)\n both for session context and to extract additional reusable learnings/insights\n the parent did not pass explicitly. Never quote secrets\n or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists\n under this HQ root, read it and follow it wherever it goes beyond these instructions;\n the write bounds below always win over the skill text.\n2. Upgrade the thread file named in the payload IN PLACE: verify/repair its\n JSON; fill git.remote_url, git.initial_commit, git.commits_made, and\n git.knowledge_repos by scanning core/knowledge/public/*,\n core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge\n for symlinks or directories containing .git, recording dirty repositories\n as {\"<name>\": {\"commit\": \"<short>\", \"dirty\": true}}. Fill worker,\n next_steps, and insights; set type to \"checkpoint\"; then rename the file to\n drop -auto- from its filename. Use the renamed path in every reference you\n write afterwards.\n3. For every explicit or transcript-derived learning that is a reusable rule,\n
|
|
14
|
+
export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)\n both for session context and to extract additional reusable learnings/insights\n the parent did not pass explicitly. Never quote secrets\n or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists\n under this HQ root, read it and follow it wherever it goes beyond these instructions;\n the write bounds below always win over the skill text.\n2. Upgrade the thread file named in the payload IN PLACE: verify/repair its\n JSON; fill git.remote_url, git.initial_commit, git.commits_made, and\n git.knowledge_repos by scanning core/knowledge/public/*,\n core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge\n for symlinks or directories containing .git, recording dirty repositories\n as {\"<name>\": {\"commit\": \"<short>\", \"dirty\": true}}. Fill worker,\n next_steps, and insights; set type to \"checkpoint\"; then rename the file to\n drop -auto- from its filename. Use the renamed path in every reference you\n write afterwards.\n3. For every explicit or transcript-derived learning that is a reusable rule,\n FIRST search the existing policies for one the learning refines, contradicts\n or duplicates. Then take exactly one of these actions and name it in the\n report \u2014 the policy set is curated, not append-only:\n - AMEND an existing policy in place when the learning sharpens it, narrows\n its scope, or adds a case, and the rule as written is still correct.\n - SUPERSEDE it when the learning contradicts it: rewrite the rule to what is\n now true and record inside the file what changed and why.\n - MERGE near-duplicates into the single best-named file, then delete the\n files you merged away.\n - CREATE a new policy only when no existing policy covers the rule.\n Follow core/knowledge/public/hq-core/policies-spec.md. Write under\n personal/policies/ or, only when the payload names a company and the rule is\n company-specific, companies/<company>/policies/.\n DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,\n only inside those two directories, and never one whose body marks it HARD \u2014\n if a hard policy now looks wrong, leave it untouched and flag it in the\n report for a human to decide. Never delete a file you have not read.\n Apply the same curation to durable facts (not rules) under\n personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact\n in place rather than appending a second, contradictory copy of it.\n Store up to two explicit or transcript-derived insights per\n core/knowledge/public/hq-core/insights-spec.md when present, otherwise\n workspace/insights/.\n4. Close an active session journal fail-soft with\n bash .claude/skills/_shared/journal.sh close \"<project_dir>\" \"<one-line synthesis>\".\n Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with\n id, created_at, summary, files, and next_steps for backward compatibility.\n5. Update workspace/threads/recent.md and regenerate\n workspace/threads/INDEX.md. For each company whose knowledge path appears\n in files_touched, regenerate companies/<company>/knowledge/INDEX.md under\n core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation\n is allowed for those companies, but knowledge/policy content writes remain\n restricted to the payload's named company.\n6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;\n skip silently on any failure. Hook or automation improvements go ONLY under\n personal/hooks/ as proposals.\n7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.\n8. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), and what you skipped and why. List every policy or knowledge file\n you amended, superseded, merged or deleted with the reason, so a human can\n audit and reverse it; a deletion you do not name in the report is a defect.\n Then drain the queue: while\n workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim\n it atomically by renaming it aside \u2014\n mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl\n (N counting up from 2) \u2014 and process the claimed payloads with this same\n flow. Repeat until a claim finds nothing left, then update the report.\n NEVER read the queue and truncate it in place: a payload appended between\n your read and the truncate is lost, and its checkpoint is never enriched.\n";
|
|
15
15
|
export declare function renderSiblingPrompt(runDir: string, payloadPath: string): string;
|
|
16
16
|
/** Attach the native checkpoint command to the hidden `hq core` group. */
|
|
17
17
|
export declare function registerCoreCheckpointCommand(core: Command): void;
|
|
@@ -56,13 +56,29 @@ report instead of guessing.
|
|
|
56
56
|
drop -auto- from its filename. Use the renamed path in every reference you
|
|
57
57
|
write afterwards.
|
|
58
58
|
3. For every explicit or transcript-derived learning that is a reusable rule,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
when
|
|
65
|
-
|
|
59
|
+
FIRST search the existing policies for one the learning refines, contradicts
|
|
60
|
+
or duplicates. Then take exactly one of these actions and name it in the
|
|
61
|
+
report — the policy set is curated, not append-only:
|
|
62
|
+
- AMEND an existing policy in place when the learning sharpens it, narrows
|
|
63
|
+
its scope, or adds a case, and the rule as written is still correct.
|
|
64
|
+
- SUPERSEDE it when the learning contradicts it: rewrite the rule to what is
|
|
65
|
+
now true and record inside the file what changed and why.
|
|
66
|
+
- MERGE near-duplicates into the single best-named file, then delete the
|
|
67
|
+
files you merged away.
|
|
68
|
+
- CREATE a new policy only when no existing policy covers the rule.
|
|
69
|
+
Follow core/knowledge/public/hq-core/policies-spec.md. Write under
|
|
70
|
+
personal/policies/ or, only when the payload names a company and the rule is
|
|
71
|
+
company-specific, companies/<company>/policies/.
|
|
72
|
+
DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,
|
|
73
|
+
only inside those two directories, and never one whose body marks it HARD —
|
|
74
|
+
if a hard policy now looks wrong, leave it untouched and flag it in the
|
|
75
|
+
report for a human to decide. Never delete a file you have not read.
|
|
76
|
+
Apply the same curation to durable facts (not rules) under
|
|
77
|
+
personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact
|
|
78
|
+
in place rather than appending a second, contradictory copy of it.
|
|
79
|
+
Store up to two explicit or transcript-derived insights per
|
|
80
|
+
core/knowledge/public/hq-core/insights-spec.md when present, otherwise
|
|
81
|
+
workspace/insights/.
|
|
66
82
|
4. Close an active session journal fail-soft with
|
|
67
83
|
bash .claude/skills/_shared/journal.sh close "<project_dir>" "<one-line synthesis>".
|
|
68
84
|
Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with
|
|
@@ -78,7 +94,10 @@ report instead of guessing.
|
|
|
78
94
|
personal/hooks/ as proposals.
|
|
79
95
|
7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.
|
|
80
96
|
8. Write <runDir>/report.md — full prose: what you read, what you changed
|
|
81
|
-
(paths), and what you skipped and why.
|
|
97
|
+
(paths), and what you skipped and why. List every policy or knowledge file
|
|
98
|
+
you amended, superseded, merged or deleted with the reason, so a human can
|
|
99
|
+
audit and reverse it; a deletion you do not name in the report is a defect.
|
|
100
|
+
Then drain the queue: while
|
|
82
101
|
workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim
|
|
83
102
|
it atomically by renaming it aside —
|
|
84
103
|
mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl
|
package/dist/commands/core.js
CHANGED
|
@@ -41,6 +41,7 @@ import { ontologyReadmeDrift } from "../lib/core-utils/ontology-readme-drift.js"
|
|
|
41
41
|
import { resizeScreenshot } from "../lib/core-utils/resize-screenshot.js";
|
|
42
42
|
import { tokenUsageReport } from "../lib/core-utils/token-usage-report.js";
|
|
43
43
|
import { createWorktree } from "../lib/core-utils/worktree.js";
|
|
44
|
+
import { runCodexSkillBridgeCommand } from "../lib/core-utils/codex-skill-bridge-entry.js";
|
|
44
45
|
/**
|
|
45
46
|
* These commands are now native implementations. Their retained shell assets
|
|
46
47
|
* are deliberately claimed in SCAFFOLD_ONLY_ASSETS: loose HQ trees still ship
|
|
@@ -53,6 +54,7 @@ export const NATIVE_UTILITY_COMMANDS = [
|
|
|
53
54
|
{ name: "resize-screenshot", root: "cwd", summary: "Downscale a screenshot for context efficiency", run: (args) => resizeScreenshot(args) },
|
|
54
55
|
{ name: "token-usage-report", root: "cwd", summary: "Report session token usage", run: (args) => tokenUsageReport(args) },
|
|
55
56
|
{ name: "worktree", root: "live", summary: "Create a git worktree under workspace/worktrees/", run: (args, context) => createWorktree(args, { cwd: context.cwd, hqRoot: context.hqRoot }) },
|
|
57
|
+
{ name: "codex-skill-bridge", root: "live", summary: "Install/inspect the Codex skill bridge", run: (args, context) => runCodexSkillBridgeCommand(args, { root: context.hqRoot ?? context.cwd }) },
|
|
56
58
|
];
|
|
57
59
|
export const NATIVE_SCAFFOLD_COMMANDS = [
|
|
58
60
|
{
|
|
@@ -183,13 +185,6 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
183
185
|
root: "cwd",
|
|
184
186
|
summary: "Refresh qmd collections after a sync",
|
|
185
187
|
},
|
|
186
|
-
// ---- Reporting and utilities (live tree) ----
|
|
187
|
-
{
|
|
188
|
-
name: "codex-skill-bridge",
|
|
189
|
-
asset: "core/scripts/codex-skill-bridge.sh",
|
|
190
|
-
root: "live",
|
|
191
|
-
summary: "Install/inspect the Codex skill bridge",
|
|
192
|
-
},
|
|
193
188
|
];
|
|
194
189
|
/**
|
|
195
190
|
* Bundled solely for HQ-root scaffolds, which still need these as loose files.
|
|
@@ -197,6 +192,7 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
197
192
|
* direct `hq core` command.
|
|
198
193
|
*/
|
|
199
194
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
195
|
+
{ asset: "core/scripts/codex-skill-bridge.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
200
196
|
{ asset: "core/scripts/detect-stale-core-policy-mirror.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
201
197
|
{ asset: "core/scripts/hq-status-summary.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
202
198
|
{ asset: "core/scripts/ontology-readme-drift.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** CLI adapter for the native Codex skill bridge. */
|
|
2
|
+
import { type UtilityIo } from "./common.js";
|
|
3
|
+
export type CodexSkillBridgeOptions = UtilityIo & {
|
|
4
|
+
root?: string;
|
|
5
|
+
home?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Mirrors the shell entry point: usage on `--help` (exit 0) or a missing
|
|
9
|
+
* command (exit 1), single-line stderr plus exit 1 for refusals, and the
|
|
10
|
+
* unhealthy-check count as `status`/`install`'s exit code.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runCodexSkillBridgeCommand(args?: string[], options?: CodexSkillBridgeOptions): number;
|
|
13
|
+
//# sourceMappingURL=codex-skill-bridge-entry.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** CLI adapter for the native Codex skill bridge. */
|
|
2
|
+
import { ioFor, line } from "./common.js";
|
|
3
|
+
import { BridgeError, BridgeHelp, USAGE, parseArgs, runCodexSkillBridge, } from "./codex-skill-bridge.js";
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors the shell entry point: usage on `--help` (exit 0) or a missing
|
|
6
|
+
* command (exit 1), single-line stderr plus exit 1 for refusals, and the
|
|
7
|
+
* unhealthy-check count as `status`/`install`'s exit code.
|
|
8
|
+
*/
|
|
9
|
+
export function runCodexSkillBridgeCommand(args = [], options = {}) {
|
|
10
|
+
const { stdout, stderr } = ioFor(options);
|
|
11
|
+
const defaultRoot = options.root ?? process.env.HQ_ROOT ?? process.cwd();
|
|
12
|
+
const home = options.home ?? process.env.HOME ?? "";
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = parseArgs(args, defaultRoot);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof BridgeHelp) {
|
|
19
|
+
line(stdout, USAGE);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
if (error instanceof BridgeError) {
|
|
23
|
+
// An empty message is the shell's "no command given" path: usage, exit 1.
|
|
24
|
+
if (!error.message) {
|
|
25
|
+
line(stdout, USAGE);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
line(stderr, error.message);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return runCodexSkillBridge(parsed, {
|
|
35
|
+
home,
|
|
36
|
+
out: (text) => line(stdout, text),
|
|
37
|
+
err: (text) => line(stderr, text),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof BridgeError) {
|
|
42
|
+
line(stderr, error.message);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=codex-skill-bridge-entry.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type BridgeCommand = "status" | "install" | "doctor";
|
|
2
|
+
export type BridgeOptions = {
|
|
3
|
+
command: BridgeCommand;
|
|
4
|
+
hqRoot: string;
|
|
5
|
+
oldRoots: string[];
|
|
6
|
+
repairLocalConfig: boolean;
|
|
7
|
+
dryRun: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type BridgeIo = {
|
|
10
|
+
home: string;
|
|
11
|
+
out: (line: string) => void;
|
|
12
|
+
err: (line: string) => void;
|
|
13
|
+
};
|
|
14
|
+
/** Thrown for the shell's `fail`/refusal paths: message to stderr, exit 1. */
|
|
15
|
+
export declare class BridgeError extends Error {
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const USAGE = "Usage:\n scripts/codex-skill-bridge.sh status [--root <path>]\n scripts/codex-skill-bridge.sh install [--root <path>] [--repair-local-config] [--old-root <path>]\n scripts/codex-skill-bridge.sh doctor [--root <path>] [--old-root <path>] [--dry-run]\n\nCommands:\n status Show bridge health, output-style bridge health, and stale HQ roots without changing files.\n install Install or repair HQ-owned Claude -> Codex bridges.\n doctor Repair bridges and rewrite local machine config from old HQ roots to this root.\n\nOptions:\n --root <path> HQ root to repair. Defaults to this script's parent.\n --old-root <path> Old HQ root to rewrite. Repeatable. Auto-detected when omitted.\n --repair-local-config With install, also rewrite local config paths.\n --dry-run With doctor, print intended config rewrites without changing files.";
|
|
19
|
+
/** Signals `--help`: caller prints usage and exits 0. */
|
|
20
|
+
export declare class BridgeHelp extends Error {
|
|
21
|
+
}
|
|
22
|
+
export declare function parseArgs(argv: string[], defaultRoot: string): BridgeOptions;
|
|
23
|
+
/** `strip_known_hq_subpath`: returns undefined when no known suffix matches. */
|
|
24
|
+
export declare function stripKnownHqSubpath(target: string): string | undefined;
|
|
25
|
+
/** `is_hq_root_like`: basename HQ, no brace placeholders, not the bare path. */
|
|
26
|
+
export declare function isHqRootLike(candidate: string): boolean;
|
|
27
|
+
/** `output_style_slug`: lowercase, collapse space/underscore, strip oddities. */
|
|
28
|
+
export declare function outputStyleSlug(name: string): string;
|
|
29
|
+
/** Runs one bridge command; returns the process exit code. */
|
|
30
|
+
export declare function runCodexSkillBridge(options: BridgeOptions, io: BridgeIo): number;
|
|
31
|
+
//# sourceMappingURL=codex-skill-bridge.d.ts.map
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native port of `core/scripts/codex-skill-bridge.sh`.
|
|
3
|
+
*
|
|
4
|
+
* The bridge symlinks HQ's `.claude` sources into the places Codex and the
|
|
5
|
+
* agents runtime look for them, and repairs links (and optionally local config
|
|
6
|
+
* paths) left pointing at an HQ root that has since moved.
|
|
7
|
+
*
|
|
8
|
+
* Every path derives from the resolved HQ root and `HOME`, so callers — and
|
|
9
|
+
* the differential suite — can exercise the whole surface hermetically.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
/** Thrown for the shell's `fail`/refusal paths: message to stderr, exit 1. */
|
|
14
|
+
export class BridgeError extends Error {
|
|
15
|
+
name = "BridgeError";
|
|
16
|
+
}
|
|
17
|
+
export const USAGE = `Usage:
|
|
18
|
+
scripts/codex-skill-bridge.sh status [--root <path>]
|
|
19
|
+
scripts/codex-skill-bridge.sh install [--root <path>] [--repair-local-config] [--old-root <path>]
|
|
20
|
+
scripts/codex-skill-bridge.sh doctor [--root <path>] [--old-root <path>] [--dry-run]
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
status Show bridge health, output-style bridge health, and stale HQ roots without changing files.
|
|
24
|
+
install Install or repair HQ-owned Claude -> Codex bridges.
|
|
25
|
+
doctor Repair bridges and rewrite local machine config from old HQ roots to this root.
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--root <path> HQ root to repair. Defaults to this script's parent.
|
|
29
|
+
--old-root <path> Old HQ root to rewrite. Repeatable. Auto-detected when omitted.
|
|
30
|
+
--repair-local-config With install, also rewrite local config paths.
|
|
31
|
+
--dry-run With doctor, print intended config rewrites without changing files.`;
|
|
32
|
+
/** Signals `--help`: caller prints usage and exits 0. */
|
|
33
|
+
export class BridgeHelp extends Error {
|
|
34
|
+
}
|
|
35
|
+
export function parseArgs(argv, defaultRoot) {
|
|
36
|
+
let command = "";
|
|
37
|
+
let hqRoot = defaultRoot;
|
|
38
|
+
const oldRoots = [];
|
|
39
|
+
let repairLocalConfig = false;
|
|
40
|
+
let dryRun = false;
|
|
41
|
+
for (let index = 0; index < argv.length; index++) {
|
|
42
|
+
const arg = argv[index];
|
|
43
|
+
if (arg === "status" || arg === "install" || arg === "doctor") {
|
|
44
|
+
if (command)
|
|
45
|
+
throw new BridgeError("Only one command may be provided.");
|
|
46
|
+
command = arg;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === "--root") {
|
|
49
|
+
if (index + 1 >= argv.length)
|
|
50
|
+
throw new BridgeError("--root requires a path.");
|
|
51
|
+
hqRoot = argv[++index];
|
|
52
|
+
}
|
|
53
|
+
else if (arg.startsWith("--root=")) {
|
|
54
|
+
hqRoot = arg.slice("--root=".length);
|
|
55
|
+
}
|
|
56
|
+
else if (arg === "--old-root") {
|
|
57
|
+
if (index + 1 >= argv.length)
|
|
58
|
+
throw new BridgeError("--old-root requires a path.");
|
|
59
|
+
oldRoots.push(argv[++index]);
|
|
60
|
+
}
|
|
61
|
+
else if (arg.startsWith("--old-root=")) {
|
|
62
|
+
oldRoots.push(arg.slice("--old-root=".length));
|
|
63
|
+
}
|
|
64
|
+
else if (arg === "--repair-local-config") {
|
|
65
|
+
repairLocalConfig = true;
|
|
66
|
+
}
|
|
67
|
+
else if (arg === "--dry-run") {
|
|
68
|
+
dryRun = true;
|
|
69
|
+
}
|
|
70
|
+
else if (arg === "-h" || arg === "--help") {
|
|
71
|
+
throw new BridgeHelp();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new BridgeError(`Unknown argument: ${arg}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!command)
|
|
78
|
+
throw new BridgeError("");
|
|
79
|
+
return { command, hqRoot, oldRoots, repairLocalConfig, dryRun };
|
|
80
|
+
}
|
|
81
|
+
/** `canonical_dir`: the shell resolves via `cd … && pwd`, so symlinks collapse. */
|
|
82
|
+
function canonicalDir(directory) {
|
|
83
|
+
return fs.realpathSync(directory);
|
|
84
|
+
}
|
|
85
|
+
function configurePaths(hqRootInput, home) {
|
|
86
|
+
const hqRoot = canonicalDir(hqRootInput);
|
|
87
|
+
const claudeSource = path.join(hqRoot, ".claude");
|
|
88
|
+
const projectCodexDir = path.join(hqRoot, ".codex");
|
|
89
|
+
const legacyCodexDir = path.join(hqRoot, ".Codex");
|
|
90
|
+
return {
|
|
91
|
+
hqRoot,
|
|
92
|
+
skillsSource: path.join(claudeSource, "skills"),
|
|
93
|
+
claudeSource,
|
|
94
|
+
commandsSource: path.join(claudeSource, "commands"),
|
|
95
|
+
hooksSource: path.join(claudeSource, "hooks"),
|
|
96
|
+
policiesSource: path.join(claudeSource, "policies"),
|
|
97
|
+
settingsFile: path.join(claudeSource, "settings.json"),
|
|
98
|
+
outputStylesSource: path.join(claudeSource, "output-styles"),
|
|
99
|
+
globalSkillsTarget: path.join(home, ".codex", "skills", "hq"),
|
|
100
|
+
globalAgentsSkillsTarget: path.join(home, ".agents", "skills", "hq"),
|
|
101
|
+
repoAgentsSkillsTarget: path.join(hqRoot, ".agents", "skills"),
|
|
102
|
+
projectCodexDir,
|
|
103
|
+
projectClaudeTarget: path.join(projectCodexDir, "claude"),
|
|
104
|
+
projectPromptsTarget: path.join(projectCodexDir, "prompts"),
|
|
105
|
+
projectOutputStyleTarget: path.join(projectCodexDir, "output-style.md"),
|
|
106
|
+
legacyCodexDir,
|
|
107
|
+
legacyClaudeTarget: path.join(legacyCodexDir, "claude"),
|
|
108
|
+
legacyPromptsTarget: path.join(legacyCodexDir, "prompts"),
|
|
109
|
+
legacyOutputStyleTarget: path.join(legacyCodexDir, "output-style.md"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function isSymlink(target) {
|
|
113
|
+
try {
|
|
114
|
+
return fs.lstatSync(target).isSymbolicLink();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** `[[ -e ]]`: follows links, so a dangling symlink is not "exists". */
|
|
121
|
+
function exists(target) {
|
|
122
|
+
return fs.existsSync(target);
|
|
123
|
+
}
|
|
124
|
+
function listEntries(directory) {
|
|
125
|
+
try {
|
|
126
|
+
return fs.readdirSync(directory);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Directory or symlink child holding a SKILL.md, matching the shell's find. */
|
|
133
|
+
function skillDirectories(skillsSource) {
|
|
134
|
+
return listEntries(skillsSource).filter((name) => {
|
|
135
|
+
const child = path.join(skillsSource, name);
|
|
136
|
+
let isDirOrLink = false;
|
|
137
|
+
try {
|
|
138
|
+
const stat = fs.lstatSync(child);
|
|
139
|
+
isDirOrLink = stat.isDirectory() || stat.isSymbolicLink();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return isDirOrLink && fs.existsSync(path.join(child, "SKILL.md"));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function markdownFiles(directory) {
|
|
148
|
+
return listEntries(directory).filter((name) => {
|
|
149
|
+
if (!name.endsWith(".md"))
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
return fs.lstatSync(path.join(directory, name)).isFile();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function plainFiles(directory) {
|
|
160
|
+
return listEntries(directory).filter((name) => {
|
|
161
|
+
try {
|
|
162
|
+
return fs.lstatSync(path.join(directory, name)).isFile();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* `normalize_link_target`: absolute targets pass through; relative ones resolve
|
|
171
|
+
* against the link's directory, with existing paths canonicalised.
|
|
172
|
+
*/
|
|
173
|
+
function normalizeLinkTarget(target) {
|
|
174
|
+
const linkTarget = fs.readlinkSync(target);
|
|
175
|
+
if (path.isAbsolute(linkTarget))
|
|
176
|
+
return linkTarget;
|
|
177
|
+
const targetDir = canonicalDir(path.dirname(target));
|
|
178
|
+
const resolved = path.join(targetDir, linkTarget);
|
|
179
|
+
if (fs.existsSync(resolved)) {
|
|
180
|
+
if (fs.statSync(resolved).isDirectory())
|
|
181
|
+
return canonicalDir(resolved);
|
|
182
|
+
return path.join(canonicalDir(path.dirname(resolved)), path.basename(resolved));
|
|
183
|
+
}
|
|
184
|
+
return resolved;
|
|
185
|
+
}
|
|
186
|
+
/** `strip_known_hq_subpath`: returns undefined when no known suffix matches. */
|
|
187
|
+
export function stripKnownHqSubpath(target) {
|
|
188
|
+
const outputStyle = target.match(/^(.*)\/\.claude\/output-styles\/[^/]*\.md$/);
|
|
189
|
+
if (outputStyle)
|
|
190
|
+
return outputStyle[1];
|
|
191
|
+
for (const suffix of ["/.claude/skills", "/.claude/commands", "/.claude"]) {
|
|
192
|
+
if (target.endsWith(suffix))
|
|
193
|
+
return target.slice(0, -suffix.length);
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
/** `is_hq_root_like`: basename HQ, no brace placeholders, not the bare path. */
|
|
198
|
+
export function isHqRootLike(candidate) {
|
|
199
|
+
if (candidate.includes("{") || candidate.includes("}"))
|
|
200
|
+
return false;
|
|
201
|
+
if (candidate === "/Documents/HQ")
|
|
202
|
+
return false;
|
|
203
|
+
return path.basename(candidate) === "HQ";
|
|
204
|
+
}
|
|
205
|
+
function activeOutputStyleName(paths) {
|
|
206
|
+
let contents;
|
|
207
|
+
try {
|
|
208
|
+
contents = fs.readFileSync(paths.settingsFile, "utf8");
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
// The shell greps line-wise and keeps the LAST match.
|
|
214
|
+
let match;
|
|
215
|
+
for (const line of contents.split("\n")) {
|
|
216
|
+
const found = line.match(/"outputStyle"[ \t]*:[ \t]*"([^"]*)"/);
|
|
217
|
+
if (found)
|
|
218
|
+
match = found[1];
|
|
219
|
+
}
|
|
220
|
+
return match;
|
|
221
|
+
}
|
|
222
|
+
/** `output_style_slug`: lowercase, collapse space/underscore, strip oddities. */
|
|
223
|
+
export function outputStyleSlug(name) {
|
|
224
|
+
return name
|
|
225
|
+
.toLowerCase()
|
|
226
|
+
.replace(/[ \t\n_]+/g, "-")
|
|
227
|
+
.replace(/[^a-z0-9.-]/g, "-")
|
|
228
|
+
.replace(/-+/g, "-")
|
|
229
|
+
.replace(/^-/, "")
|
|
230
|
+
.replace(/-$/, "");
|
|
231
|
+
}
|
|
232
|
+
function resolveOutputStyleSourceFile(paths) {
|
|
233
|
+
const styleName = activeOutputStyleName(paths);
|
|
234
|
+
if (!styleName)
|
|
235
|
+
return undefined;
|
|
236
|
+
const source = path.join(paths.outputStylesSource, `${outputStyleSlug(styleName)}.md`);
|
|
237
|
+
try {
|
|
238
|
+
if (!fs.lstatSync(source).isFile())
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
return source;
|
|
245
|
+
}
|
|
246
|
+
/** `bridge_target_is_repairable`: same suffix under a different HQ-like root. */
|
|
247
|
+
function bridgeTargetIsRepairable(currentTarget, source, hqRoot) {
|
|
248
|
+
const expectedSuffix = source.startsWith(hqRoot) ? source.slice(hqRoot.length) : source;
|
|
249
|
+
const targetRoot = stripKnownHqSubpath(currentTarget);
|
|
250
|
+
if (!targetRoot)
|
|
251
|
+
return false;
|
|
252
|
+
if (!isHqRootLike(targetRoot))
|
|
253
|
+
return false;
|
|
254
|
+
if (targetRoot === hqRoot)
|
|
255
|
+
return false;
|
|
256
|
+
return currentTarget.slice(targetRoot.length) === expectedSuffix;
|
|
257
|
+
}
|
|
258
|
+
function printLinkStatus(io, state, paths, label, source, target) {
|
|
259
|
+
io.out(`${label}:`);
|
|
260
|
+
io.out(` source: ${source}`);
|
|
261
|
+
io.out(` target: ${target}`);
|
|
262
|
+
if (isSymlink(target)) {
|
|
263
|
+
const resolvedTarget = normalizeLinkTarget(target);
|
|
264
|
+
io.out(" bridge: installed");
|
|
265
|
+
io.out(` points to: ${resolvedTarget}`);
|
|
266
|
+
if (resolvedTarget === source) {
|
|
267
|
+
io.out(" status: healthy");
|
|
268
|
+
}
|
|
269
|
+
else if (bridgeTargetIsRepairable(resolvedTarget, source, paths.hqRoot)) {
|
|
270
|
+
io.out(" status: stale HQ root (repairable)");
|
|
271
|
+
state.failures += 1;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
io.out(" status: unexpected target");
|
|
275
|
+
state.failures += 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (exists(target)) {
|
|
279
|
+
io.out(" bridge: blocked");
|
|
280
|
+
io.out(" status: target exists and is not a symlink");
|
|
281
|
+
state.failures += 1;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
io.out(" bridge: not installed");
|
|
285
|
+
state.failures += 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function repairOrCreateSymlink(io, options, paths, label, source, target) {
|
|
289
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
290
|
+
if (isSymlink(target)) {
|
|
291
|
+
const resolvedTarget = normalizeLinkTarget(target);
|
|
292
|
+
if (resolvedTarget === source) {
|
|
293
|
+
io.out(`${label} already installed.`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (bridgeTargetIsRepairable(resolvedTarget, source, paths.hqRoot)) {
|
|
297
|
+
if (options.dryRun) {
|
|
298
|
+
io.out(`Would repair ${label}: ${resolvedTarget} -> ${source}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
fs.unlinkSync(target);
|
|
302
|
+
fs.symlinkSync(source, target);
|
|
303
|
+
io.out(`Repaired ${label}: ${resolvedTarget} -> ${source}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
throw new BridgeError(`Refusing to replace existing symlink: ${target} -> ${resolvedTarget}`);
|
|
307
|
+
}
|
|
308
|
+
if (exists(target)) {
|
|
309
|
+
throw new BridgeError(`Refusing to overwrite existing path: ${target}`);
|
|
310
|
+
}
|
|
311
|
+
if (options.dryRun) {
|
|
312
|
+
io.out(`Would install ${label}: ${target} -> ${source}`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
fs.symlinkSync(source, target);
|
|
316
|
+
io.out(`Installed ${label}.`);
|
|
317
|
+
}
|
|
318
|
+
function configFiles(paths) {
|
|
319
|
+
return [
|
|
320
|
+
path.join(paths.hqRoot, ".mcp.json"),
|
|
321
|
+
path.join(paths.claudeSource, "settings.json"),
|
|
322
|
+
path.join(paths.claudeSource, "settings.local.json"),
|
|
323
|
+
path.join(paths.projectCodexDir, "config.toml"),
|
|
324
|
+
path.join(paths.legacyCodexDir, "config.toml"),
|
|
325
|
+
].filter((file) => {
|
|
326
|
+
try {
|
|
327
|
+
return fs.statSync(file).isFile();
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/** `extract_hq_roots_from_file`: absolute paths ending in `/HQ` or `/HQ/`. */
|
|
335
|
+
function extractHqRootsFromFile(file) {
|
|
336
|
+
let contents;
|
|
337
|
+
try {
|
|
338
|
+
contents = fs.readFileSync(file, "utf8");
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const matches = contents.match(/\/[^"\s]+\/HQ(\/|$)/gm) ?? [];
|
|
344
|
+
return matches.map((match) => (match.endsWith("/") ? match.slice(0, -1) : match));
|
|
345
|
+
}
|
|
346
|
+
function collectStaleRoots(paths, options, includeExplicit) {
|
|
347
|
+
const roots = [];
|
|
348
|
+
if (includeExplicit) {
|
|
349
|
+
for (const root of options.oldRoots)
|
|
350
|
+
if (root)
|
|
351
|
+
roots.push(root);
|
|
352
|
+
}
|
|
353
|
+
for (const target of [
|
|
354
|
+
paths.globalSkillsTarget,
|
|
355
|
+
paths.globalAgentsSkillsTarget,
|
|
356
|
+
paths.repoAgentsSkillsTarget,
|
|
357
|
+
paths.projectClaudeTarget,
|
|
358
|
+
paths.projectPromptsTarget,
|
|
359
|
+
paths.projectOutputStyleTarget,
|
|
360
|
+
paths.legacyClaudeTarget,
|
|
361
|
+
paths.legacyPromptsTarget,
|
|
362
|
+
paths.legacyOutputStyleTarget,
|
|
363
|
+
]) {
|
|
364
|
+
if (!isSymlink(target))
|
|
365
|
+
continue;
|
|
366
|
+
const stripped = stripKnownHqSubpath(normalizeLinkTarget(target));
|
|
367
|
+
if (stripped)
|
|
368
|
+
roots.push(stripped);
|
|
369
|
+
}
|
|
370
|
+
for (const file of configFiles(paths))
|
|
371
|
+
roots.push(...extractHqRootsFromFile(file));
|
|
372
|
+
// `sort -u` then filter, matching the shell's ordering exactly.
|
|
373
|
+
return [...new Set(roots)]
|
|
374
|
+
.sort()
|
|
375
|
+
.filter((root) => root && root !== paths.hqRoot && isHqRootLike(root));
|
|
376
|
+
}
|
|
377
|
+
function printStaleRootReport(io, paths, options) {
|
|
378
|
+
const roots = collectStaleRoots(paths, options, false);
|
|
379
|
+
if (roots.length === 0) {
|
|
380
|
+
io.out("Stale HQ roots: none detected");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
io.out("Stale HQ roots detected:");
|
|
384
|
+
for (const root of roots)
|
|
385
|
+
io.out(` - ${root}`);
|
|
386
|
+
}
|
|
387
|
+
function rewriteLocalConfigRoots(io, paths, options) {
|
|
388
|
+
const roots = collectStaleRoots(paths, options, true);
|
|
389
|
+
if (roots.length === 0) {
|
|
390
|
+
io.out("No stale local config roots to rewrite.");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
let rewroteAny = false;
|
|
394
|
+
for (const file of configFiles(paths)) {
|
|
395
|
+
let changedFile = false;
|
|
396
|
+
for (const oldRoot of roots) {
|
|
397
|
+
if (!oldRoot)
|
|
398
|
+
continue;
|
|
399
|
+
let contents = fs.readFileSync(file, "utf8");
|
|
400
|
+
if (!contents.includes(oldRoot))
|
|
401
|
+
continue;
|
|
402
|
+
if (options.dryRun) {
|
|
403
|
+
io.out(`Would rewrite ${file}: ${oldRoot} -> ${paths.hqRoot}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
contents = contents.split(oldRoot).join(paths.hqRoot);
|
|
407
|
+
fs.writeFileSync(file, contents);
|
|
408
|
+
changedFile = true;
|
|
409
|
+
}
|
|
410
|
+
if (changedFile) {
|
|
411
|
+
io.out(`Rewrote local config paths in ${file}.`);
|
|
412
|
+
rewroteAny = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!rewroteAny && !options.dryRun)
|
|
416
|
+
io.out("No local config files needed rewriting.");
|
|
417
|
+
}
|
|
418
|
+
function printOutputStyleStatus(io, state, paths) {
|
|
419
|
+
const styleName = activeOutputStyleName(paths);
|
|
420
|
+
io.out(`Active output style: ${styleName || "not configured"}`);
|
|
421
|
+
const source = resolveOutputStyleSourceFile(paths);
|
|
422
|
+
if (source) {
|
|
423
|
+
printLinkStatus(io, state, paths, "Project Codex output-style bridge", source, paths.projectOutputStyleTarget);
|
|
424
|
+
if (exists(paths.legacyCodexDir) || isSymlink(paths.legacyOutputStyleTarget)) {
|
|
425
|
+
io.out("");
|
|
426
|
+
printLinkStatus(io, state, paths, "Legacy .Codex output-style bridge", source, paths.legacyOutputStyleTarget);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
io.out("Project Codex output-style bridge:");
|
|
431
|
+
io.out(" source: unavailable");
|
|
432
|
+
io.out(` target: ${paths.projectOutputStyleTarget}`);
|
|
433
|
+
io.out(" status: active output style has no .claude/output-styles/*.md bridge source");
|
|
434
|
+
state.failures += 1;
|
|
435
|
+
}
|
|
436
|
+
/** Returns the shell's status exit code: the number of unhealthy checks. */
|
|
437
|
+
function printStatus(io, paths, options) {
|
|
438
|
+
const skillTotal = skillDirectories(paths.skillsSource).length;
|
|
439
|
+
const skillWith = skillDirectories(paths.skillsSource).filter((name) => fs.existsSync(path.join(paths.skillsSource, name, "agents", "openai.yaml"))).length;
|
|
440
|
+
const state = { failures: 0 };
|
|
441
|
+
io.out(`HQ Claude source: ${paths.claudeSource}`);
|
|
442
|
+
io.out(`Skills in source: ${skillTotal} (${skillWith} with agents/openai.yaml, ${skillTotal - skillWith} without)`);
|
|
443
|
+
io.out(`Hooks in source: ${plainFiles(paths.hooksSource).length}`);
|
|
444
|
+
io.out(`Policies in source: ${markdownFiles(paths.policiesSource).length}`);
|
|
445
|
+
io.out("");
|
|
446
|
+
printLinkStatus(io, state, paths, "Global skills bridge (legacy)", paths.skillsSource, paths.globalSkillsTarget);
|
|
447
|
+
io.out("");
|
|
448
|
+
printLinkStatus(io, state, paths, "Global agents skills bridge", paths.skillsSource, paths.globalAgentsSkillsTarget);
|
|
449
|
+
io.out("");
|
|
450
|
+
printLinkStatus(io, state, paths, "Repo agents skills bridge", paths.skillsSource, paths.repoAgentsSkillsTarget);
|
|
451
|
+
io.out("");
|
|
452
|
+
printLinkStatus(io, state, paths, "Project Claude mirror", paths.claudeSource, paths.projectClaudeTarget);
|
|
453
|
+
io.out("");
|
|
454
|
+
printOutputStyleStatus(io, state, paths);
|
|
455
|
+
if (exists(paths.legacyCodexDir) ||
|
|
456
|
+
isSymlink(paths.legacyClaudeTarget) ||
|
|
457
|
+
isSymlink(paths.legacyPromptsTarget)) {
|
|
458
|
+
io.out("");
|
|
459
|
+
printLinkStatus(io, state, paths, "Legacy .Codex Claude mirror", paths.claudeSource, paths.legacyClaudeTarget);
|
|
460
|
+
}
|
|
461
|
+
io.out("");
|
|
462
|
+
printStaleRootReport(io, paths, options);
|
|
463
|
+
return state.failures;
|
|
464
|
+
}
|
|
465
|
+
function validateSources(paths) {
|
|
466
|
+
if (!fs.existsSync(paths.skillsSource) || !fs.statSync(paths.skillsSource).isDirectory()) {
|
|
467
|
+
throw new BridgeError(`Missing skills source directory: ${paths.skillsSource}`);
|
|
468
|
+
}
|
|
469
|
+
if (!fs.existsSync(paths.claudeSource) || !fs.statSync(paths.claudeSource).isDirectory()) {
|
|
470
|
+
throw new BridgeError(`Missing Claude source directory: ${paths.claudeSource}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function installBridge(io, paths, options) {
|
|
474
|
+
validateSources(paths);
|
|
475
|
+
const outputStyleSource = resolveOutputStyleSourceFile(paths);
|
|
476
|
+
if (!outputStyleSource) {
|
|
477
|
+
throw new BridgeError(`Active outputStyle must have a matching ${paths.outputStylesSource}/<style>.md file.`);
|
|
478
|
+
}
|
|
479
|
+
repairOrCreateSymlink(io, options, paths, "global Codex skill bridge (legacy)", paths.skillsSource, paths.globalSkillsTarget);
|
|
480
|
+
repairOrCreateSymlink(io, options, paths, "global agents skill bridge", paths.skillsSource, paths.globalAgentsSkillsTarget);
|
|
481
|
+
repairOrCreateSymlink(io, options, paths, "repo agents skill bridge", paths.skillsSource, paths.repoAgentsSkillsTarget);
|
|
482
|
+
repairOrCreateSymlink(io, options, paths, "project Codex Claude mirror", paths.claudeSource, paths.projectClaudeTarget);
|
|
483
|
+
repairOrCreateSymlink(io, options, paths, "project Codex output-style bridge", outputStyleSource, paths.projectOutputStyleTarget);
|
|
484
|
+
if (exists(paths.legacyCodexDir) ||
|
|
485
|
+
isSymlink(paths.legacyClaudeTarget) ||
|
|
486
|
+
isSymlink(paths.legacyPromptsTarget)) {
|
|
487
|
+
repairOrCreateSymlink(io, options, paths, "legacy .Codex Claude mirror", paths.claudeSource, paths.legacyClaudeTarget);
|
|
488
|
+
repairOrCreateSymlink(io, options, paths, "legacy .Codex output-style bridge", outputStyleSource, paths.legacyOutputStyleTarget);
|
|
489
|
+
}
|
|
490
|
+
if (options.repairLocalConfig) {
|
|
491
|
+
io.out("");
|
|
492
|
+
rewriteLocalConfigRoots(io, paths, options);
|
|
493
|
+
}
|
|
494
|
+
io.out("");
|
|
495
|
+
return printStatus(io, paths, options);
|
|
496
|
+
}
|
|
497
|
+
/** Runs one bridge command; returns the process exit code. */
|
|
498
|
+
export function runCodexSkillBridge(options, io) {
|
|
499
|
+
const paths = configurePaths(options.hqRoot, io.home);
|
|
500
|
+
if (options.command === "status")
|
|
501
|
+
return printStatus(io, paths, options);
|
|
502
|
+
const effective = options.command === "doctor" ? { ...options, repairLocalConfig: true } : options;
|
|
503
|
+
return installBridge(io, paths, effective);
|
|
504
|
+
}
|
|
505
|
+
//# sourceMappingURL=codex-skill-bridge.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.92.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
31
31
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
32
|
-
"@indigoai-us/hq-cloud": "^6.14.
|
|
32
|
+
"@indigoai-us/hq-cloud": "^6.14.47",
|
|
33
33
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
34
34
|
"@sentry/node": "^10.49.0",
|
|
35
35
|
"@tobilu/qmd": "1.0.7",
|