@indigoai-us/hq-cli 5.323.0 → 5.325.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/bin/hq-auth-refresh.d.ts +5 -1
  3. package/dist/bin/hq-auth-refresh.js +33 -1
  4. package/dist/command-catalog.generated.d.ts +125 -6
  5. package/dist/command-catalog.generated.js +162 -6
  6. package/dist/command-registration-plan.d.ts +24 -0
  7. package/dist/command-registration-plan.js +4 -0
  8. package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
  9. package/dist/commands/anywhere-runtime-flag.js +22 -0
  10. package/dist/commands/core-derive-trigger-facts-native.js +37 -3
  11. package/dist/commands/daemon.js +39 -2
  12. package/dist/commands/db-provision.js +6 -1
  13. package/dist/commands/doctor.js +8 -4
  14. package/dist/commands/feedback.d.ts +2 -0
  15. package/dist/commands/feedback.js +10 -0
  16. package/dist/commands/install-global.d.ts +46 -0
  17. package/dist/commands/install-global.js +674 -0
  18. package/dist/commands/link.d.ts +30 -0
  19. package/dist/commands/link.js +184 -0
  20. package/dist/commands/mcp-status.d.ts +6 -0
  21. package/dist/commands/mcp-status.js +21 -4
  22. package/dist/commands/pkg-install.js +20 -1
  23. package/dist/commands/update.d.ts +19 -0
  24. package/dist/commands/update.js +52 -6
  25. package/dist/fast-core.d.ts +2 -1
  26. package/dist/fast-core.js +12 -8
  27. package/dist/index.js +9 -7
  28. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  29. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  30. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  31. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  32. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  33. package/dist/lib/core-utils/timeout-guard.js +2 -1
  34. package/dist/lib/daemon/convergence.d.ts +42 -0
  35. package/dist/lib/daemon/convergence.js +66 -0
  36. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  37. package/dist/lib/daemon/heartbeat.js +70 -0
  38. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  39. package/dist/lib/daemon/ops/journal.js +141 -0
  40. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  41. package/dist/lib/daemon/ops/memory.js +65 -0
  42. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  43. package/dist/lib/daemon/ops/mesh.js +70 -0
  44. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  45. package/dist/lib/daemon/ops/policy.js +283 -0
  46. package/dist/lib/daemon/protocol.d.ts +68 -0
  47. package/dist/lib/daemon/protocol.js +154 -0
  48. package/dist/lib/daemon/run.d.ts +10 -0
  49. package/dist/lib/daemon/run.js +85 -3
  50. package/dist/lib/daemon/server.d.ts +36 -0
  51. package/dist/lib/daemon/server.js +306 -0
  52. package/dist/lib/daemon/session-registry.d.ts +30 -0
  53. package/dist/lib/daemon/session-registry.js +42 -0
  54. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  55. package/dist/lib/doctor/checks/anywhere.js +182 -0
  56. package/dist/lib/doctor/registry.js +6 -0
  57. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  58. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  59. package/dist/lib/install/claude-targets.d.ts +47 -0
  60. package/dist/lib/install/claude-targets.js +203 -0
  61. package/dist/lib/install/codex-targets.d.ts +57 -0
  62. package/dist/lib/install/codex-targets.js +187 -0
  63. package/dist/lib/mesh/live/session-seq.js +62 -9
  64. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  65. package/dist/lib/registry/normalize-remote.js +68 -0
  66. package/dist/lib/registry/registry.d.ts +75 -0
  67. package/dist/lib/registry/registry.js +261 -0
  68. package/dist/utils/cognito-session.d.ts +5 -1
  69. package/dist/utils/cognito-session.js +8 -12
  70. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  71. package/dist/utils/version-check.d.ts +2 -0
  72. package/dist/utils/version-check.js +7 -0
  73. package/package.json +2 -2
@@ -0,0 +1,674 @@
1
+ /**
2
+ * `hq install --global --runtime claude` / `hq uninstall --global --runtime claude`
3
+ * (hq-anywhere-runtime US-005).
4
+ *
5
+ * Makes HQ skills, the charter, master-hook.sh and the HQ MCP server available in
6
+ * every Claude Code session. Every user config write goes through the
7
+ * mcp-registration safe-write substrate (lock, backup, atomic replace, verify).
8
+ * Install state, including the pre-install bytes of each touched file, lives in
9
+ * ~/.hq/anywhere/claude-install.json so uninstall can restore files byte-for-byte.
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { claudeConfigFormat, codexConfigFormat, isCodexInstalled, jsonFormat, unregisterCodexServer, readConfigDoc, realpathOrSelf, resolveEnv, unregisterClaudeServer, writeConfigAtomic, } from './mcp-registration.js';
14
+ import { seedRegistryFromHqRoot } from './link.js';
15
+ import { resolveHqRoot } from './doctor.js';
16
+ import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
17
+ import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
18
+ import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
19
+ import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
20
+ export { AnywhereRuntimeDisabledError as AnywhereDisabledError };
21
+ export const isAnywhereEnabled = (_home, env = process.env) => anywhereRuntimeEnabled(env);
22
+ function readOrNull(file) {
23
+ try {
24
+ return fs.readFileSync(file, 'utf-8');
25
+ }
26
+ catch (e) {
27
+ if (e.code === 'ENOENT')
28
+ return null;
29
+ throw e;
30
+ }
31
+ }
32
+ function writeFileAtomic(file, data) {
33
+ const target = realpathOrSelf(file);
34
+ fs.mkdirSync(path.dirname(target), { recursive: true });
35
+ const tmp = `${target}.hq-tmp-${process.pid}`;
36
+ fs.writeFileSync(tmp, data);
37
+ fs.renameSync(tmp, target);
38
+ }
39
+ function canonical(v) {
40
+ return JSON.stringify(v, (_k, val) => val && typeof val === 'object' && !Array.isArray(val)
41
+ ? Object.fromEntries(Object.keys(val).sort().map((k) => [k, val[k]]))
42
+ : val);
43
+ }
44
+ /** JSON settings format whose verify step checks every HQ hook is registered. */
45
+ function settingsInstallFormat(hqRoot) {
46
+ return {
47
+ ...jsonFormat,
48
+ emptyDoc: () => ({}),
49
+ assertEntry(doc) {
50
+ if (!hasAllHqHooks(doc, hqRoot))
51
+ throw new Error('HQ hooks not present after write');
52
+ },
53
+ };
54
+ }
55
+ /**
56
+ * Removal format: verifies no HQ hook remains, and serializes a doc that equals
57
+ * the pre-install document back to the ORIGINAL bytes (byte-identical uninstall).
58
+ */
59
+ function settingsRemovalFormat(hqRoot, original) {
60
+ return restoringRemovalFormat(original, (doc) => removeHqHooks(doc, hqRoot) !== doc);
61
+ }
62
+ /** Shared removal format for a JSON hooks file (see settingsRemovalFormat). */
63
+ function restoringRemovalFormat(original, stillPresent) {
64
+ let originalCanon = null;
65
+ if (original !== null && original.trim() !== '') {
66
+ try {
67
+ originalCanon = canonical(JSON.parse(original));
68
+ }
69
+ catch {
70
+ originalCanon = null; // original was not JSON; nothing to restore byte-for-byte.
71
+ }
72
+ }
73
+ return {
74
+ ...jsonFormat,
75
+ emptyDoc: () => ({}),
76
+ serialize(doc) {
77
+ if (originalCanon !== null && canonical(doc) === originalCanon)
78
+ return original;
79
+ return jsonFormat.serialize(doc);
80
+ },
81
+ assertEntry(doc) {
82
+ if (stillPresent(doc))
83
+ throw new Error('HQ hooks still present after removal');
84
+ },
85
+ };
86
+ }
87
+ function planSkills(hqRoot, t, previouslyManaged = []) {
88
+ const source = hqSkillsSource(hqRoot);
89
+ const plan = { link: [], replace: [], already: [], skipped: [] };
90
+ let names;
91
+ try {
92
+ names = fs
93
+ .readdirSync(source, { withFileTypes: true })
94
+ .filter((d) => (d.isDirectory() || d.isSymbolicLink()) && !d.name.startsWith('_') && !d.name.startsWith('.'))
95
+ .map((d) => d.name)
96
+ .sort();
97
+ }
98
+ catch (e) {
99
+ if (e.code === 'ENOENT')
100
+ return plan;
101
+ throw e;
102
+ }
103
+ for (const name of names) {
104
+ const link = path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`);
105
+ const target = path.join(source, name);
106
+ let st = null;
107
+ try {
108
+ st = fs.lstatSync(link);
109
+ }
110
+ catch (e) {
111
+ if (e.code !== 'ENOENT')
112
+ throw e;
113
+ }
114
+ if (!st)
115
+ plan.link.push(name);
116
+ else if (st.isSymbolicLink() && path.resolve(t.skillsDir, fs.readlinkSync(link)) === target)
117
+ plan.already.push(name);
118
+ else if (st.isSymbolicLink() && previouslyManaged.includes(name)) {
119
+ plan.link.push(name);
120
+ plan.replace.push(name);
121
+ }
122
+ else
123
+ plan.skipped.push(name);
124
+ }
125
+ return plan;
126
+ }
127
+ function removeSeeded(seeded, regFile) {
128
+ if (seeded.length === 0 || !fs.existsSync(regFile))
129
+ return;
130
+ const registry = readRegistry(regFile);
131
+ let removed = 0;
132
+ for (const key of seeded) {
133
+ if (registry.entries[key]?.source === 'manifest') {
134
+ delete registry.entries[key];
135
+ removed++;
136
+ }
137
+ }
138
+ if (removed > 0)
139
+ writeRegistry(registry, regFile);
140
+ }
141
+ function readState(t) {
142
+ const text = readOrNull(t.stateFile);
143
+ if (text === null)
144
+ return null;
145
+ return JSON.parse(text);
146
+ }
147
+ function assertHqRoot(hqRoot) {
148
+ if (!path.isAbsolute(hqRoot))
149
+ throw new Error(`HQ root must be absolute: ${hqRoot}`);
150
+ for (const p of [charterPath(hqRoot), masterHookPath(hqRoot)]) {
151
+ if (!fs.existsSync(p))
152
+ throw new Error(`${hqRoot} is not an HQ checkout (missing ${p})`);
153
+ }
154
+ }
155
+ /** Install HQ into Claude Code user scope. Returns the files touched (or that would be). */
156
+ export function installGlobalClaude(opts) {
157
+ const home = resolveEnv({ home: opts.home }).home;
158
+ const env = opts.env ?? process.env;
159
+ const log = opts.log ?? ((l) => console.log(l));
160
+ const hqRoot = opts.hqRoot;
161
+ assertHqRoot(hqRoot);
162
+ if (!isAnywhereEnabled(home, env))
163
+ throw new AnywhereRuntimeDisabledError();
164
+ const t = claudeTargets(home);
165
+ const prior = readState(t);
166
+ const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
167
+ const touched = [];
168
+ const pointerOld = readOrNull(t.hqPointer);
169
+ const pointerNew = `${hqRoot}\n`;
170
+ const mdOld = readOrNull(t.claudeMd);
171
+ const mdNew = addClaudeMdBlock(mdOld, hqRoot);
172
+ const skills = planSkills(hqRoot, t, prior?.skills.linked ?? []);
173
+ const settingsOld = readOrNull(t.settingsJson);
174
+ const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
175
+ const settingsNext = addHqHooks(settingsDoc, hqRoot);
176
+ if (pointerOld !== pointerNew)
177
+ touched.push(t.hqPointer);
178
+ if (mdOld !== mdNew)
179
+ touched.push(t.claudeMd);
180
+ for (const s of skills.link)
181
+ touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
182
+ if (settingsNext !== settingsDoc)
183
+ touched.push(t.settingsJson);
184
+ touched.push(regFile, t.stateFile);
185
+ if (opts.dryRun) {
186
+ log(`hq install --global --runtime claude --dry-run (HQ root ${hqRoot})`);
187
+ log('Would write:');
188
+ for (const f of touched)
189
+ log(` ${f}`);
190
+ if (skills.skipped.length > 0) {
191
+ log(`Would skip ${skills.skipped.length} skill(s) whose hq-<name> already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
192
+ }
193
+ log(`\n--- ${t.settingsJson}`);
194
+ log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
195
+ return touched;
196
+ }
197
+ writeFileAtomic(t.hqPointer, pointerNew);
198
+ writeFileAtomic(t.claudeMd, mdNew);
199
+ fs.mkdirSync(t.skillsDir, { recursive: true });
200
+ for (const name of skills.replace)
201
+ fs.unlinkSync(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
202
+ for (const name of skills.link) {
203
+ fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
204
+ }
205
+ writeConfigAtomic({
206
+ target: t.settingsJson,
207
+ pack: HQ_ANYWHERE_PACK,
208
+ format: settingsInstallFormat(hqRoot),
209
+ merge: (doc) => addHqHooks(doc, hqRoot),
210
+ assertName: 'hooks',
211
+ env: { home },
212
+ });
213
+ // Seeding is best-effort: the runtime wiring above is already in place, so a
214
+ // missing or unreadable manifest is reported rather than aborting the install.
215
+ let seeded = [];
216
+ try {
217
+ seeded = seedRegistryFromHqRoot(hqRoot, { file: regFile }).owned;
218
+ }
219
+ catch (e) {
220
+ log(`Registry not seeded from companies/manifest.yaml: ${e.message}`);
221
+ }
222
+ const state = {
223
+ version: 1,
224
+ hqRoot,
225
+ // A re-install keeps the FIRST install's originals so uninstall restores pre-HQ bytes.
226
+ hqPointer: { original: prior ? prior.hqPointer.original : pointerOld },
227
+ claudeMd: { original: prior ? prior.claudeMd.original : mdOld, installed: readOrNull(t.claudeMd) ?? '' },
228
+ settings: { original: prior ? prior.settings.original : settingsOld, installed: readOrNull(t.settingsJson) ?? '' },
229
+ skills: {
230
+ linked: [...new Set([...(prior?.skills.linked ?? []), ...skills.link, ...skills.already])].sort(),
231
+ skipped: skills.skipped,
232
+ },
233
+ mcp: { name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK },
234
+ seeded: [...new Set([...(prior?.seeded ?? []), ...seeded])].sort(),
235
+ };
236
+ writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
237
+ log(`HQ installed for Claude Code from ${hqRoot}.`);
238
+ log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
239
+ log(` hooks: master-hook.sh registered in ${t.settingsJson}`);
240
+ log(' MCP server registration is added by the stacked MCP shim change.');
241
+ log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
242
+ return touched;
243
+ }
244
+ /** Reverse every install step; non-HQ entries are left untouched. */
245
+ export function uninstallGlobalClaude(opts = {}) {
246
+ const home = resolveEnv({ home: opts.home }).home;
247
+ const env = opts.env ?? process.env;
248
+ const log = opts.log ?? ((l) => console.log(l));
249
+ const t = claudeTargets(home);
250
+ const state = readState(t);
251
+ const hqRoot = state?.hqRoot ?? opts.hqRoot ?? readOrNull(t.hqPointer)?.trim();
252
+ if (!hqRoot)
253
+ throw new Error('No HQ global install found (no install state and no ~/.hq/root).');
254
+ const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
255
+ const touched = [];
256
+ // MCP server (provenance-scoped: a foreign "hq" server is never removed).
257
+ const claudeDoc = readConfigDoc(t.claudeJson, claudeConfigFormat).doc;
258
+ const servers = claudeDoc.mcpServers;
259
+ if (servers?.[HQ_MCP_SERVER_NAME]?._hqPack === HQ_ANYWHERE_PACK)
260
+ touched.push(t.claudeJson);
261
+ // settings.json hooks.
262
+ const settingsOriginal = state ? state.settings.original : null;
263
+ const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
264
+ const settingsNext = removeHqHooks(settingsDoc, hqRoot);
265
+ if (settingsNext !== settingsDoc)
266
+ touched.push(t.settingsJson);
267
+ // Skills: only symlinks that point into this HQ checkout's skills dir.
268
+ const source = hqSkillsSource(hqRoot);
269
+ const links = [];
270
+ try {
271
+ for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
272
+ if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
273
+ continue;
274
+ const link = path.join(t.skillsDir, d.name);
275
+ const target = path.resolve(t.skillsDir, fs.readlinkSync(link));
276
+ if (path.dirname(target) === source)
277
+ links.push(link);
278
+ }
279
+ }
280
+ catch (e) {
281
+ if (e.code !== 'ENOENT')
282
+ throw e;
283
+ }
284
+ touched.push(...links);
285
+ const md = readOrNull(t.claudeMd);
286
+ let mdNext = md;
287
+ if (md !== null) {
288
+ if (state && md === state.claudeMd.installed)
289
+ mdNext = state.claudeMd.original;
290
+ else
291
+ mdNext = removeClaudeMdBlock(md);
292
+ if (mdNext !== md)
293
+ touched.push(t.claudeMd);
294
+ }
295
+ const pointer = readOrNull(t.hqPointer);
296
+ const codexStillInstalled = fs.existsSync(codexTargets(home).stateFile);
297
+ const pointerOwned = !codexStillInstalled && pointer !== null && pointer.trim() === hqRoot;
298
+ if (pointerOwned)
299
+ touched.push(t.hqPointer);
300
+ if (state)
301
+ touched.push(t.stateFile);
302
+ if (opts.dryRun) {
303
+ log('hq uninstall --global --runtime claude --dry-run');
304
+ log('Would change:');
305
+ for (const f of touched)
306
+ log(` ${f}`);
307
+ log(`\n--- ${t.settingsJson}`);
308
+ log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
309
+ return touched;
310
+ }
311
+ unregisterClaudeServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
312
+ if (fs.existsSync(t.settingsJson)) {
313
+ writeConfigAtomic({
314
+ target: t.settingsJson,
315
+ pack: HQ_ANYWHERE_PACK,
316
+ format: settingsRemovalFormat(hqRoot, settingsOriginal),
317
+ merge: (doc) => removeHqHooks(doc, hqRoot),
318
+ assertName: 'hooks',
319
+ env: { home },
320
+ });
321
+ // The file did not exist before install and nothing else was added: remove it.
322
+ if (state && settingsOriginal === null && canonical(readConfigDoc(t.settingsJson, jsonFormat).doc) === '{}') {
323
+ fs.unlinkSync(t.settingsJson);
324
+ }
325
+ }
326
+ for (const link of links)
327
+ fs.unlinkSync(link);
328
+ if (md !== null && mdNext !== md) {
329
+ if (mdNext === null || (mdNext === '' && state?.claudeMd.original == null))
330
+ fs.unlinkSync(t.claudeMd);
331
+ else
332
+ writeFileAtomic(t.claudeMd, mdNext);
333
+ }
334
+ if (pointerOwned) {
335
+ const original = state?.hqPointer.original ?? null;
336
+ if (original === null)
337
+ fs.unlinkSync(t.hqPointer);
338
+ else
339
+ writeFileAtomic(t.hqPointer, original);
340
+ }
341
+ if (state && !codexStillInstalled)
342
+ removeSeeded(state.seeded, regFile);
343
+ if (state) {
344
+ fs.unlinkSync(t.stateFile);
345
+ try {
346
+ fs.rmdirSync(path.dirname(t.stateFile));
347
+ }
348
+ catch (e) {
349
+ if (e.code !== 'ENOTEMPTY' && e.code !== 'ENOENT')
350
+ throw e;
351
+ }
352
+ }
353
+ log(`HQ removed from Claude Code user scope (${links.length} skill link(s), hooks, MCP server, CLAUDE.md block).`);
354
+ return touched;
355
+ }
356
+ function readCodexState(t) {
357
+ const text = readOrNull(t.stateFile);
358
+ if (text === null)
359
+ return null;
360
+ return JSON.parse(text);
361
+ }
362
+ function assertCodexHqRoot(hqRoot) {
363
+ if (!path.isAbsolute(hqRoot))
364
+ throw new Error(`HQ root must be absolute: ${hqRoot}`);
365
+ for (const p of [codexCharterSource(hqRoot), codexAdapterPath(hqRoot)]) {
366
+ if (!fs.existsSync(p))
367
+ throw new Error(`${hqRoot} is not an HQ checkout (missing ${p})`);
368
+ }
369
+ }
370
+ function hooksInstallFormat(hqRoot) {
371
+ return {
372
+ ...jsonFormat,
373
+ emptyDoc: () => ({}),
374
+ assertEntry(doc) {
375
+ if (!hasAllCodexHooks(doc, hqRoot))
376
+ throw new Error('HQ Codex hooks not present after write');
377
+ },
378
+ };
379
+ }
380
+ function legacyWarning(skillsDir) {
381
+ return (`Left ${path.join(skillsDir, LEGACY_SKILLS_NAME)} in place: it is not an HQ-owned symlink and has no ` +
382
+ `${HQ_OWNED_MARKER} marker. Remove it yourself if it is an old HQ skills copy.`);
383
+ }
384
+ /** Install HQ into Codex user scope. Returns the files touched (or that would be). */
385
+ export function installGlobalCodex(opts) {
386
+ const home = resolveEnv({ home: opts.home }).home;
387
+ const env = opts.env ?? process.env;
388
+ const log = opts.log ?? ((l) => console.log(l));
389
+ const hqRoot = opts.hqRoot;
390
+ assertCodexHqRoot(hqRoot);
391
+ if (!isCodexInstalled({ home })) {
392
+ throw Object.assign(new Error(`Codex is not installed (${path.join(home, '.codex')} is missing).`), {
393
+ expected: true,
394
+ });
395
+ }
396
+ if (!isAnywhereEnabled(home, env))
397
+ throw new AnywhereRuntimeDisabledError();
398
+ const t = codexTargets(home);
399
+ const hqPointer = claudeTargets(home).hqPointer;
400
+ const prior = readCodexState(t);
401
+ const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
402
+ const touched = [];
403
+ const pointerOld = readOrNull(hqPointer);
404
+ const pointerNew = `${hqRoot}\n`;
405
+ const charter = fs.readFileSync(codexCharterSource(hqRoot), 'utf-8');
406
+ const mdOld = readOrNull(t.agentsMd);
407
+ const mdNew = addAgentsMdBlock(mdOld, charter);
408
+ const legacy = classifyLegacySkills(t.skillsDir, hqRoot);
409
+ const skills = planSkills(hqRoot, t, prior?.skills.linked ?? []);
410
+ const hooksOld = readOrNull(t.hooksJson);
411
+ const tomlOld = readOrNull(t.configToml);
412
+ const hooksDoc = readConfigDoc(t.hooksJson, jsonFormat).doc;
413
+ const hooksNext = addCodexHooks(hooksDoc, hqRoot);
414
+ if (pointerOld !== pointerNew)
415
+ touched.push(hqPointer);
416
+ if (mdOld !== mdNew)
417
+ touched.push(t.agentsMd);
418
+ if (legacy.kind === 'hq-symlink' || legacy.kind === 'hq-dir')
419
+ touched.push(path.join(t.skillsDir, LEGACY_SKILLS_NAME));
420
+ for (const s of skills.link)
421
+ touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
422
+ if (hooksNext !== hooksDoc)
423
+ touched.push(t.hooksJson);
424
+ touched.push(regFile, t.stateFile);
425
+ if (opts.dryRun) {
426
+ log(`hq install --global --runtime codex --dry-run (HQ root ${hqRoot})`);
427
+ log('Would write:');
428
+ for (const f of touched)
429
+ log(` ${f}`);
430
+ if (legacy.kind === 'hq-symlink' || legacy.kind === 'hq-dir') {
431
+ log(`Would replace the HQ-owned legacy mirror ${path.join(t.skillsDir, LEGACY_SKILLS_NAME)} with hq-<skill> links`);
432
+ }
433
+ else if (legacy.kind === 'foreign')
434
+ log(legacyWarning(t.skillsDir));
435
+ if (skills.skipped.length > 0) {
436
+ log(`Would skip ${skills.skipped.length} skill(s) whose hq-<name> already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
437
+ }
438
+ log(`\n--- ${t.hooksJson}`);
439
+ log(lineDiff(jsonFormat.serialize(hooksDoc), jsonFormat.serialize(hooksNext)));
440
+ return touched;
441
+ }
442
+ if (legacy.kind === 'hq-dir' && fs.existsSync(path.join(t.skillsDir, LEGACY_BACKUP_NAME))) {
443
+ throw new Error(`${path.join(t.skillsDir, LEGACY_BACKUP_NAME)} already exists; move it away before installing.`);
444
+ }
445
+ writeFileAtomic(hqPointer, pointerNew);
446
+ writeFileAtomic(t.agentsMd, mdNew);
447
+ // Legacy mirror: replace only when HQ-owned; record enough to put it back.
448
+ let legacyRecord = prior?.legacy ?? { kind: 'none' };
449
+ const legacyPath = path.join(t.skillsDir, LEGACY_SKILLS_NAME);
450
+ if (legacy.kind === 'hq-symlink') {
451
+ fs.unlinkSync(legacyPath);
452
+ legacyRecord = { kind: 'symlink', target: legacy.target };
453
+ }
454
+ else if (legacy.kind === 'hq-dir') {
455
+ const backup = path.join(t.skillsDir, LEGACY_BACKUP_NAME);
456
+ fs.renameSync(legacyPath, backup);
457
+ legacyRecord = { kind: 'dir', backup };
458
+ }
459
+ else if (legacy.kind === 'foreign')
460
+ log(legacyWarning(t.skillsDir));
461
+ fs.mkdirSync(t.skillsDir, { recursive: true });
462
+ for (const name of skills.replace)
463
+ fs.unlinkSync(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
464
+ for (const name of skills.link) {
465
+ fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
466
+ }
467
+ writeConfigAtomic({
468
+ target: t.hooksJson,
469
+ pack: HQ_ANYWHERE_PACK,
470
+ format: hooksInstallFormat(hqRoot),
471
+ merge: (doc) => addCodexHooks(doc, hqRoot),
472
+ assertName: 'hooks',
473
+ env: { home },
474
+ });
475
+ let seeded = [];
476
+ try {
477
+ seeded = seedRegistryFromHqRoot(hqRoot, { file: regFile }).owned;
478
+ }
479
+ catch (e) {
480
+ log(`Registry not seeded from companies/manifest.yaml: ${e.message}`);
481
+ }
482
+ const state = {
483
+ version: 1,
484
+ hqRoot,
485
+ hqPointer: { original: prior ? prior.hqPointer.original : pointerOld },
486
+ agentsMd: { original: prior ? prior.agentsMd.original : mdOld, installed: readOrNull(t.agentsMd) ?? '' },
487
+ hooks: { original: prior ? prior.hooks.original : hooksOld, installed: readOrNull(t.hooksJson) ?? '' },
488
+ configToml: { original: prior ? prior.configToml.original : tomlOld },
489
+ skills: {
490
+ linked: [...new Set([...(prior?.skills.linked ?? []), ...skills.link, ...skills.already])].sort(),
491
+ skipped: skills.skipped,
492
+ },
493
+ legacy: legacyRecord,
494
+ mcp: { name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK },
495
+ seeded: [...new Set([...(prior?.seeded ?? []), ...seeded])].sort(),
496
+ };
497
+ writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
498
+ log(`HQ installed for Codex from ${hqRoot}.`);
499
+ log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
500
+ log(` hooks: ${codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
501
+ log(' MCP server registration is added by the stacked MCP shim change.');
502
+ return touched;
503
+ }
504
+ /** Reverse every Codex install step; non-HQ entries are left untouched. */
505
+ export function uninstallGlobalCodex(opts = {}) {
506
+ const home = resolveEnv({ home: opts.home }).home;
507
+ const env = opts.env ?? process.env;
508
+ const log = opts.log ?? ((l) => console.log(l));
509
+ const t = codexTargets(home);
510
+ const hqPointer = claudeTargets(home).hqPointer;
511
+ const state = readCodexState(t);
512
+ const hqRoot = state?.hqRoot ?? opts.hqRoot ?? readOrNull(hqPointer)?.trim();
513
+ if (!hqRoot)
514
+ throw new Error('No HQ global install found (no install state and no ~/.hq/root).');
515
+ const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
516
+ const claudeStillInstalled = fs.existsSync(claudeTargets(home).stateFile);
517
+ const touched = [];
518
+ const tomlDoc = readConfigDoc(t.configToml, codexConfigFormat).doc;
519
+ const servers = tomlDoc.value.mcp_servers;
520
+ if (servers?.[HQ_MCP_SERVER_NAME]?._hqPack === HQ_ANYWHERE_PACK)
521
+ touched.push(t.configToml);
522
+ const hooksOriginal = state ? state.hooks.original : null;
523
+ const hooksDoc = readConfigDoc(t.hooksJson, jsonFormat).doc;
524
+ const hooksNext = removeCodexHooks(hooksDoc, hqRoot);
525
+ if (hooksNext !== hooksDoc)
526
+ touched.push(t.hooksJson);
527
+ const source = hqSkillsSource(hqRoot);
528
+ const links = [];
529
+ try {
530
+ for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
531
+ if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
532
+ continue;
533
+ const link = path.join(t.skillsDir, d.name);
534
+ const target = path.resolve(t.skillsDir, fs.readlinkSync(link));
535
+ if (path.dirname(target) === source)
536
+ links.push(link);
537
+ }
538
+ }
539
+ catch (e) {
540
+ if (e.code !== 'ENOENT')
541
+ throw e;
542
+ }
543
+ touched.push(...links);
544
+ const legacyPath = path.join(t.skillsDir, LEGACY_SKILLS_NAME);
545
+ const restoreLegacy = state !== null && state.legacy.kind !== 'none' && !fs.existsSync(legacyPath);
546
+ if (restoreLegacy)
547
+ touched.push(legacyPath);
548
+ const md = readOrNull(t.agentsMd);
549
+ let mdNext = md;
550
+ if (md !== null) {
551
+ if (state && md === state.agentsMd.installed)
552
+ mdNext = state.agentsMd.original;
553
+ else
554
+ mdNext = removeAgentsMdBlock(md);
555
+ if (mdNext !== md)
556
+ touched.push(t.agentsMd);
557
+ }
558
+ const pointer = readOrNull(hqPointer);
559
+ const pointerOwned = !claudeStillInstalled && pointer !== null && pointer.trim() === hqRoot;
560
+ if (pointerOwned)
561
+ touched.push(hqPointer);
562
+ if (state)
563
+ touched.push(t.stateFile);
564
+ if (opts.dryRun) {
565
+ log('hq uninstall --global --runtime codex --dry-run');
566
+ log('Would change:');
567
+ for (const f of touched)
568
+ log(` ${f}`);
569
+ log(`\n--- ${t.hooksJson}`);
570
+ log(lineDiff(jsonFormat.serialize(hooksDoc), jsonFormat.serialize(hooksNext)));
571
+ return touched;
572
+ }
573
+ unregisterCodexServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
574
+ // Removal re-serializes the TOML (dropping comments). When what is left equals
575
+ // the pre-install document, put the original bytes back.
576
+ const tomlOriginal = state?.configToml.original ?? null;
577
+ if (tomlOriginal !== null && fs.existsSync(t.configToml)) {
578
+ const now = readConfigDoc(t.configToml, codexConfigFormat).doc.value;
579
+ if (canonical(now) === canonical(codexConfigFormat.parse(tomlOriginal).value)) {
580
+ writeFileAtomic(realpathOrSelf(t.configToml), tomlOriginal);
581
+ }
582
+ }
583
+ if (fs.existsSync(t.hooksJson)) {
584
+ writeConfigAtomic({
585
+ target: t.hooksJson,
586
+ pack: HQ_ANYWHERE_PACK,
587
+ format: restoringRemovalFormat(hooksOriginal, (doc) => removeCodexHooks(doc, hqRoot) !== doc),
588
+ merge: (doc) => removeCodexHooks(doc, hqRoot),
589
+ assertName: 'hooks',
590
+ env: { home },
591
+ });
592
+ if (state && hooksOriginal === null && canonical(readConfigDoc(t.hooksJson, jsonFormat).doc) === '{}') {
593
+ fs.unlinkSync(t.hooksJson);
594
+ }
595
+ }
596
+ for (const link of links)
597
+ fs.unlinkSync(link);
598
+ if (restoreLegacy && state) {
599
+ if (state.legacy.kind === 'symlink')
600
+ fs.symlinkSync(state.legacy.target, legacyPath);
601
+ else if (state.legacy.kind === 'dir' && fs.existsSync(state.legacy.backup))
602
+ fs.renameSync(state.legacy.backup, legacyPath);
603
+ }
604
+ if (md !== null && mdNext !== md) {
605
+ if (mdNext === null || (mdNext === '' && state?.agentsMd.original == null))
606
+ fs.unlinkSync(t.agentsMd);
607
+ else
608
+ writeFileAtomic(t.agentsMd, mdNext);
609
+ }
610
+ if (pointerOwned) {
611
+ const original = state?.hqPointer.original ?? null;
612
+ if (original === null)
613
+ fs.unlinkSync(hqPointer);
614
+ else
615
+ writeFileAtomic(hqPointer, original);
616
+ }
617
+ if (state && !claudeStillInstalled)
618
+ removeSeeded(state.seeded, regFile);
619
+ if (state) {
620
+ fs.unlinkSync(t.stateFile);
621
+ try {
622
+ fs.rmdirSync(path.dirname(t.stateFile));
623
+ }
624
+ catch (e) {
625
+ if (e.code !== 'ENOTEMPTY' && e.code !== 'ENOENT')
626
+ throw e;
627
+ }
628
+ }
629
+ log(`HQ removed from Codex user scope (${links.length} skill link(s), hooks, MCP server, AGENTS.md block).`);
630
+ return touched;
631
+ }
632
+ function assertRuntime(runtime) {
633
+ const r = runtime ?? 'claude';
634
+ if (r !== 'claude' && r !== 'codex') {
635
+ throw Object.assign(new Error(`--runtime ${r} is not supported. Use --runtime claude or --runtime codex.`), {
636
+ expected: true,
637
+ });
638
+ }
639
+ return r;
640
+ }
641
+ function resolveInstallRoot(explicit) {
642
+ const fromFlag = explicit?.trim() || process.env.HQ_ROOT?.trim();
643
+ const root = fromFlag ? path.resolve(fromFlag) : resolveHqRoot();
644
+ if (!root) {
645
+ throw Object.assign(new Error('Run from inside your HQ checkout, or pass --hq-root <dir>.'), { expected: true });
646
+ }
647
+ return root;
648
+ }
649
+ /** Entry used by `hq install --global` (wired from pkg-install.ts). */
650
+ export function runInstallGlobal(opts) {
651
+ const runtime = assertRuntime(opts.runtime);
652
+ const install = runtime === 'codex' ? installGlobalCodex : installGlobalClaude;
653
+ install({ hqRoot: resolveInstallRoot(opts.hqRoot), dryRun: opts.dryRun });
654
+ }
655
+ export function registerUninstallCommand(program) {
656
+ program
657
+ .command('uninstall')
658
+ .description('Remove the HQ global install from an agent runtime (use with --global)')
659
+ .option('--global', 'Reverse `hq install --global`')
660
+ .option('--runtime <runtime>', 'Runtime to uninstall from (claude | codex)', 'claude')
661
+ .option('--dry-run', 'Print what would change without writing')
662
+ .option('--hq-root <dir>', 'HQ checkout (only needed when install state is missing)')
663
+ .action((opts) => {
664
+ if (!opts.global) {
665
+ console.error('hq uninstall currently supports only --global. For packs use `hq packs uninstall <name>`.');
666
+ process.exitCode = 1;
667
+ return;
668
+ }
669
+ const runtime = assertRuntime(opts.runtime);
670
+ const uninstall = runtime === 'codex' ? uninstallGlobalCodex : uninstallGlobalClaude;
671
+ uninstall({ hqRoot: opts.hqRoot, dryRun: opts.dryRun });
672
+ });
673
+ }
674
+ //# sourceMappingURL=install-global.js.map