@indigoai-us/hq-cli 5.91.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 +12 -0
- 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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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
|
+
|
|
5
17
|
## [5.91.0]
|
|
6
18
|
|
|
7
19
|
### Changed
|
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
|