@logicseek/hello-cc 0.1.9 → 1.0.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 (108) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +17 -3
  4. package/bin/hcc.mjs +3 -5656
  5. package/docs/README.md +8 -0
  6. package/docs/README.zh-CN.md +6 -0
  7. package/docs/architecture.md +29 -18
  8. package/docs/architecture.zh-CN.md +20 -10
  9. package/docs/commands.md +13 -4
  10. package/docs/commands.zh-CN.md +13 -4
  11. package/docs/current-state-review-2026-08.md +341 -0
  12. package/docs/defect-review-2026-07.md +382 -0
  13. package/docs/guide.md +39 -3
  14. package/docs/guide.zh-CN.md +34 -2
  15. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
  16. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
  17. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
  18. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
  19. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
  20. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
  21. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
  22. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
  23. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
  24. package/lib/cli/app.mjs +642 -0
  25. package/lib/cli/commands/coordination.mjs +247 -0
  26. package/lib/cli/commands/doctor.mjs +81 -0
  27. package/lib/cli/commands/gc.mjs +852 -0
  28. package/lib/cli/commands/hook.mjs +205 -0
  29. package/lib/cli/commands/install.mjs +294 -0
  30. package/lib/cli/commands/lifecycle.mjs +101 -0
  31. package/lib/cli/commands/lock.mjs +197 -0
  32. package/lib/cli/commands/misc.mjs +61 -0
  33. package/lib/cli/commands/msg.mjs +152 -0
  34. package/lib/cli/commands/peer.mjs +233 -0
  35. package/lib/cli/commands/query.mjs +96 -0
  36. package/lib/cli/commands/run.mjs +463 -0
  37. package/lib/cli/commands/task.mjs +405 -0
  38. package/lib/cli/commands/team.mjs +131 -0
  39. package/lib/cli/commands/tmux.mjs +838 -0
  40. package/lib/cli/commands/up.mjs +135 -0
  41. package/lib/cli-args.mjs +16 -2
  42. package/lib/coordination-state.mjs +112 -51
  43. package/lib/core/coordination/automation.mjs +5 -8
  44. package/lib/core/coordination/clock-safety.mjs +323 -0
  45. package/lib/core/coordination/event-retention.mjs +17 -0
  46. package/lib/core/coordination/gc-plan.mjs +614 -0
  47. package/lib/core/coordination/lease-renewal.mjs +102 -0
  48. package/lib/core/coordination/lock-evidence.mjs +77 -0
  49. package/lib/core/coordination/optimistic-evidence.mjs +26 -0
  50. package/lib/core/coordination/tasks.mjs +156 -36
  51. package/lib/core/peers/evidence-runtime.mjs +312 -0
  52. package/lib/core/peers/evidence.mjs +103 -0
  53. package/lib/core/peers/hook-owner.mjs +32 -0
  54. package/lib/core/peers/liveness.mjs +44 -4
  55. package/lib/core/peers/peer-helpers.mjs +121 -0
  56. package/lib/core/peers/session.mjs +8 -2
  57. package/lib/core/peers/tmux-safety.mjs +290 -0
  58. package/lib/core/sessions/launch.mjs +13 -1
  59. package/lib/db/connection.mjs +104 -0
  60. package/lib/db/events.mjs +34 -0
  61. package/lib/db/migration-backup.mjs +225 -0
  62. package/lib/db/schema.mjs +102 -5
  63. package/lib/db/stores/peers.mjs +34 -0
  64. package/lib/integrations/peers/identity.mjs +62 -16
  65. package/lib/integrations/shims/script.mjs +111 -9
  66. package/lib/process/identity.mjs +192 -0
  67. package/lib/process/pty-lifecycle.mjs +149 -0
  68. package/lib/runtime/buffer-directory-lease.mjs +47 -0
  69. package/lib/runtime/buffer-evidence.mjs +336 -0
  70. package/lib/runtime/buffer-gc-protocol.mjs +167 -0
  71. package/lib/runtime/buffer-gc.mjs +270 -0
  72. package/lib/runtime/client.mjs +60 -7
  73. package/lib/runtime/fatal-shutdown.mjs +54 -0
  74. package/lib/runtime/project-path.mjs +139 -0
  75. package/lib/runtime/projects.mjs +320 -40
  76. package/lib/runtime/state.mjs +227 -27
  77. package/lib/shared/cleanup-error.mjs +54 -0
  78. package/lib/shared/clock-grace.mjs +106 -0
  79. package/lib/shared/errors.mjs +9 -0
  80. package/lib/shared/file-lock.mjs +370 -0
  81. package/lib/shared/json-file.mjs +6 -2
  82. package/lib/shared/redact.mjs +60 -0
  83. package/lib/shared/socket-lock-worker.mjs +201 -0
  84. package/lib/terminal/tmux.mjs +78 -4
  85. package/lib/tmux.mjs +4 -0
  86. package/lib/ui/help.mjs +53 -15
  87. package/lib/web/api-version.mjs +42 -0
  88. package/lib/web/auto-attach.mjs +185 -0
  89. package/lib/web/buffer-gc-runtime.mjs +175 -0
  90. package/lib/web/cookie-auth.mjs +119 -0
  91. package/lib/web/external-sessions.mjs +254 -0
  92. package/lib/web/http-routes.mjs +542 -0
  93. package/lib/web/http.mjs +135 -6
  94. package/lib/web/liveness-reaper.mjs +145 -0
  95. package/lib/web/peer-actions.mjs +132 -23
  96. package/lib/web/project-contexts.mjs +178 -0
  97. package/lib/web/pty-sessions.mjs +203 -0
  98. package/lib/web/runtime-main.mjs +714 -0
  99. package/lib/web/runtime.mjs +91 -36
  100. package/lib/web/session-serialize.mjs +0 -0
  101. package/lib/web/startup.mjs +333 -0
  102. package/lib/web/tls.mjs +365 -0
  103. package/lib/web/tmux-sessions.mjs +1361 -0
  104. package/lib/web/tmux-stream.mjs +166 -0
  105. package/lib/web/ui-template.mjs +152 -20
  106. package/package.json +5 -2
  107. package/scripts/check-free-identifiers.mjs +290 -0
  108. package/scripts/regression.mjs +5397 -263
@@ -0,0 +1,205 @@
1
+ // Hook command extracted from bin/hcc.mjs.
2
+ // This is the provider hook entry point called by Claude/Codex hooks.
3
+
4
+ export function createHookCommand(deps) {
5
+ const {
6
+ connect, now, addEvent, auditPayload,
7
+ registerProjectActivity, touchCurrentPeer,
8
+ liveProcessIdentity, detectBranch,
9
+ resolveCurrentPeer, providerSessionPeerId, providerSessionParts,
10
+ readAncestorCliInfo, latestHookProviderSession, formatHookEventName,
11
+ upsertPeer, upsertCanonicalPeerBinding,
12
+ autoPeerKind, autoPeerBasis, autoPeerProviderSession,
13
+ observeLockClockSafety, observePeerEvidence,
14
+ buildHookCoordinationContext, ackMessages,
15
+ reconcileRunningPeerBindings, inspectProviderProcess,
16
+ resumeIdFromArgs, shortHash, renewOwnedLocks, refreshHookOwnerIdentity,
17
+ path, fs, process, CliError
18
+ } = deps;
19
+
20
+ async function cmdHook(ctx, args) {
21
+ let hookType = args[0] || 'unknown';
22
+
23
+ // Read stdin with a short timeout (hooks must complete quickly)
24
+ const raw = await new Promise((resolve) => {
25
+ let buf = '';
26
+ const timer = setTimeout(() => resolve(buf), 2000);
27
+ process.stdin.setEncoding('utf8');
28
+ process.stdin.on('data', (c) => { buf += c; });
29
+ process.stdin.on('end', () => { clearTimeout(timer); resolve(buf); });
30
+ process.stdin.on('error', () => { clearTimeout(timer); resolve(buf); });
31
+ });
32
+
33
+ let payload = {};
34
+ try { payload = JSON.parse(raw); } catch {}
35
+ hookType = payload.hook_event_name || payload.hookEventName || hookType;
36
+ const hookEventName = formatHookEventName(hookType);
37
+ const hookKey = hookEventName.replace(/[^a-z]/gi, '').toLowerCase();
38
+
39
+ // Extract session identity (Claude Code sets these in its hook JSON)
40
+ const kind = autoPeerKind('other');
41
+ const providerSession = autoPeerProviderSession(kind);
42
+ const sessionId = payload.session_id || payload.sessionId || payload.conversation_id || payload.conversationId ||
43
+ providerSession.sessionId || '';
44
+ const hookCwd = payload.cwd || payload.workingDirectory || process.cwd();
45
+
46
+ // Hooks auto-join the exact current project path, creating its mesh.db on
47
+ // first use. Cross-path sharing is explicit via HCC_ROOT/HCC_DB.
48
+ const hccRoot = process.env.HCC_ROOT
49
+ ? path.resolve(process.env.HCC_ROOT)
50
+ : path.resolve(hookCwd);
51
+
52
+ const hookCtx = {
53
+ ...ctx,
54
+ root: hccRoot,
55
+ dbPath: path.join(hccRoot, '.hello-cc', 'mesh.db')
56
+ };
57
+ registerProjectActivity(hookCtx);
58
+
59
+ // Derive peer ID: HCC_PEER > resume ID from parent cmdline > session ID > terminal
60
+ let peerId = process.env.HCC_PEER;
61
+ let resumeId = null;
62
+ if (!peerId) {
63
+ resumeId = providerSession.resumeId || readParentResumeId(kind);
64
+ if (resumeId) {
65
+ peerId = providerSessionPeerId(kind, resumeId);
66
+ }
67
+ }
68
+ if (!peerId) {
69
+ peerId = sessionId
70
+ ? `${kind}-${shortHash(sessionId)}`
71
+ : `${kind}-${shortHash(`${hookCtx.root}:${autoPeerBasis(kind)}`)}`;
72
+ }
73
+
74
+ const db = connect(hookCtx);
75
+ try {
76
+ const status = hookKey === 'stop' ? 'idle' : 'working';
77
+ const providerAncestor = readAncestorCliInfo();
78
+ const providerPid = providerAncestor?.kind === kind ? Number(providerAncestor.pid) : null;
79
+ const providerIdentity = providerPid ? liveProcessIdentity(providerPid) : null;
80
+ const existing = db.prepare('SELECT id FROM peers WHERE id = ?').get(peerId);
81
+ if (!existing) {
82
+ upsertPeer(db, {
83
+ id: peerId, kind, role: 'peer',
84
+ worktree: hookCwd,
85
+ branch: detectBranch(hookCwd),
86
+ pid: providerIdentity?.pid || null,
87
+ processIdentity: providerIdentity,
88
+ status,
89
+ capabilities: `hook-${hookKey}`
90
+ });
91
+ } else {
92
+ refreshHookOwnerIdentity(db, {
93
+ peerId,
94
+ status,
95
+ observedAt: now(),
96
+ processIdentity: providerIdentity
97
+ });
98
+ }
99
+ const hookBinding = {
100
+ peer: peerId,
101
+ provider: kind,
102
+ ...providerSessionParts(resumeId || sessionId),
103
+ resume_mode: resumeId ? 'resume' : (sessionId ? 'detected' : 'unknown'),
104
+ resume_arg: resumeId || null,
105
+ command: null,
106
+ transport: 'hook',
107
+ runtime_session_id: peerId
108
+ };
109
+ const canonical = upsertCanonicalPeerBinding(db, hookBinding, true);
110
+ if (canonical.peer !== peerId) {
111
+ const previousPeer = peerId;
112
+ peerId = canonical.peer;
113
+ refreshHookOwnerIdentity(db, {
114
+ peerId,
115
+ status,
116
+ observedAt: now(),
117
+ processIdentity: providerIdentity
118
+ });
119
+ addEvent(db, 'provider.session.merged', peerId, null, auditPayload({
120
+ actor: peerId,
121
+ target: peerId,
122
+ source: 'hook',
123
+ from_peer: previousPeer,
124
+ provider: kind,
125
+ session_id: sessionId || resumeId || null
126
+ }));
127
+ }
128
+ addEvent(db, `hook.${hookKey}`, peerId, null, auditPayload({
129
+ actor: peerId,
130
+ target: peerId,
131
+ source: 'hook',
132
+ session_id: sessionId,
133
+ cwd: hookCwd
134
+ }));
135
+ // hb-06: an active hook is proof the peer is working. During clock grace,
136
+ // renew retained locks even when the wall-clock jump made them look expired.
137
+ const hookNow = now();
138
+ const hookClockObservation = observeLockClockSafety(db, hookCtx, {
139
+ owner: peerId,
140
+ observedAt: hookNow
141
+ });
142
+ const hookLockRenewals = hookClockObservation.renewed > 0
143
+ ? hookClockObservation.renewed
144
+ : renewOwnedLocks(db, {
145
+ owner: peerId,
146
+ nowSec: hookNow,
147
+ ttlCap: 3600,
148
+ // Receiving the hook is direct lease-heartbeat evidence. It does not
149
+ // grant provider-restart or tmux-destructive authority.
150
+ includeExpired: true
151
+ });
152
+ if (hookLockRenewals > 0) {
153
+ addEvent(db, 'lock.renewed_by_hook', peerId, null, { renewed: hookLockRenewals });
154
+ }
155
+ try {
156
+ reconcileRunningPeerBindings(db, hookCtx, {
157
+ inspectProcess: inspectProviderProcess,
158
+ latestProviderSessionForPeer: (peer) => latestHookProviderSession(db, peer),
159
+ addEvent,
160
+ now
161
+ });
162
+ } catch {}
163
+
164
+ if (['sessionstart', 'userpromptsubmit'].includes(hookKey)) {
165
+ const snapshot = buildHookCoordinationContext(db, hookCtx, peerId);
166
+ ackMessages(db, peerId, snapshot.messages);
167
+ process.stdout.write(JSON.stringify({
168
+ hookSpecificOutput: {
169
+ hookEventName,
170
+ additionalContext: snapshot.text
171
+ }
172
+ }) + '\n');
173
+ } else if (['posttooluse', 'stop'].includes(hookKey)) {
174
+ const snapshot = buildHookCoordinationContext(db, hookCtx, peerId);
175
+ if (snapshot.messages.length > 0) {
176
+ ackMessages(db, peerId, snapshot.messages);
177
+ process.stdout.write(JSON.stringify({
178
+ hookSpecificOutput: {
179
+ hookEventName,
180
+ additionalContext: snapshot.text
181
+ }
182
+ }) + '\n');
183
+ }
184
+ }
185
+ } finally {
186
+ try { db.close(); } catch {}
187
+ }
188
+ process.exit(0);
189
+ }
190
+
191
+ // ─── hcc install-hooks ───────────────────────────────────────────────────────
192
+
193
+
194
+ return { cmdHook };
195
+ }
196
+
197
+ function readParentResumeId(kind) {
198
+ if (process.platform !== 'linux') return null;
199
+ try {
200
+ const raw = fs.readFileSync(`/proc/${process.ppid}/cmdline`, 'utf8');
201
+ const args = raw.split('\0').filter(Boolean);
202
+ return resumeIdFromArgs(kind, args);
203
+ } catch {}
204
+ return null;
205
+ }
@@ -0,0 +1,294 @@
1
+ // Install/setup/update/uninstall/shim commands extracted from bin/hcc.mjs.
2
+
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
+
5
+ export function createInstallCommands(deps) {
6
+ const {
7
+ path, fs, process, CliError, parseOpts, validateOpts, wantsHelp,
8
+ printResult, commandPath, connect, readRuntime, runtimeRequest,
9
+ runtimePath, globalRuntimePath, PRODUCT_NAME, VERSION, NPM_PACKAGE_NAME,
10
+ helpInstallHooks, helpShim, helpUninstall, helpUpdate,
11
+ loadSetup, shellCommand,
12
+
13
+
14
+ sameResolvedPath, writeGuidance, removeGuidanceBlocks,
15
+
16
+ } = deps;
17
+
18
+ async function cmdInstallHooks(ctx, args) {
19
+ if (wantsHelp(args)) return helpInstallHooks();
20
+ const opts = parseOpts(args, { booleans: ['uninstall', 'status'] });
21
+ const { installClaudeHooks, uninstallClaudeHooks, verifyClaudeHooks,
22
+ installCodexHooks, uninstallCodexHooks, verifyCodexHooks } = await loadSetup();
23
+ const hccBin = commandPath();
24
+
25
+ if (opts.uninstall) {
26
+ const claude = uninstallClaudeHooks();
27
+ const codex = uninstallCodexHooks();
28
+ const parts = [];
29
+ if (claude) parts.push('~/.claude/settings.json');
30
+ if (codex) parts.push('~/.codex/hooks.json');
31
+ printResult(ctx, { claude, codex }, () =>
32
+ parts.length ? 'hooks removed from ' + parts.join(', ') : 'no hooks found to remove');
33
+ return;
34
+ }
35
+ if (opts.status) {
36
+ const claudeOk = verifyClaudeHooks();
37
+ const codexOk = verifyCodexHooks();
38
+ printResult(ctx, { claude: claudeOk, codex: codexOk }, () =>
39
+ `hooks: claude=${claudeOk ? 'yes' : 'no'} codex=${codexOk ? 'yes' : 'no'}`);
40
+ return;
41
+ }
42
+ const cp = installClaudeHooks(hccBin);
43
+ let cxp = null;
44
+ try { cxp = installCodexHooks(hccBin); } catch {}
45
+ printResult(ctx, { claude: cp, codex: cxp }, () =>
46
+ `hooks installed: claude → ${cp}${cxp ? `, codex → ${cxp}` : ''}`);
47
+ }
48
+
49
+ function pathEntryRemovalMessage(pathEntry) {
50
+ if (pathEntry.error) return `PATH entry not removed: ${pathEntry.error}`;
51
+ if (pathEntry.missing) return `PATH entry not present (${pathEntry.rcFile} not found)`;
52
+ if (pathEntry.removed === false) return `PATH entry not present in ${pathEntry.rcFile}`;
53
+ return `PATH entry removed from ${pathEntry.rcFile}`;
54
+ }
55
+
56
+ // ─── hcc shim ────────────────────────────────────────────────────────────────
57
+
58
+ async function cmdShim(ctx, args) {
59
+ const sub = args[0];
60
+ const { installShims, uninstallShims, shimStatus, installPathEntry, uninstallPathEntry, SHIM_DIR } = await loadSetup();
61
+
62
+ if (wantsHelp(args)) return helpShim();
63
+
64
+ if (sub === 'ensure') {
65
+ const name = args[1];
66
+ const target = args[2] ? path.resolve(args[2]) : (name ? path.join(SHIM_DIR, name) : null);
67
+ if (!['claude', 'codex'].includes(name) || !target) {
68
+ throw new CliError('BAD_ARGS', 'Usage: hcc shim ensure claude|codex PATH');
69
+ }
70
+ const realBin = args[3] || null;
71
+ const result = installShims(commandPath(), realBin ? { realBins: { [name]: realBin } } : {});
72
+ const changed = (result.changed || []).map((p) => path.resolve(p));
73
+ if (changed.some((p) => sameResolvedPath(p, target))) {
74
+ process.exitCode = 75;
75
+ return;
76
+ }
77
+ return;
78
+ }
79
+
80
+ if (!sub || sub === 'install') {
81
+ const hccBin = commandPath();
82
+ const result = installShims(hccBin);
83
+ const lines = [
84
+ result.installed.length
85
+ ? `shims installed:\n${result.installed.map((p) => ` ${p}`).join('\n')}`
86
+ : 'no shims installed (claude/codex not found on PATH)',
87
+ ];
88
+ if (result.skipped.length) lines.push(`skipped: ${result.skipped.join(', ')}`);
89
+ if (result.installed.length) {
90
+ const { alreadyPresent, rcFile } = installPathEntry();
91
+ if (!alreadyPresent) {
92
+ lines.push(`PATH updated in ${rcFile}`);
93
+ lines.push(`run: source ${rcFile} (or open a new terminal)`);
94
+ } else {
95
+ lines.push(`PATH entry already present in ${rcFile}`);
96
+ }
97
+ }
98
+ printResult(ctx, result, () => lines.join('\n'));
99
+ return;
100
+ }
101
+ if (sub === 'uninstall') {
102
+ const removed = uninstallShims();
103
+ const pathEntry = uninstallPathEntry();
104
+ printResult(ctx, { removed, path_entry: pathEntry }, () => [
105
+ removed.length ? `removed: ${removed.join(', ')}` : 'no shims to remove',
106
+ pathEntryRemovalMessage(pathEntry)
107
+ ].join('\n'));
108
+ return;
109
+ }
110
+ if (sub === 'status') {
111
+ const status = shimStatus();
112
+ printResult(ctx, status, (r) => [
113
+ `shim dir: ${r.shimDir}`,
114
+ `claude: ${r.tools.claude.installed ? 'installed' : 'missing'} (${r.tools.claude.path})`,
115
+ `codex: ${r.tools.codex.installed ? 'installed' : 'missing'} (${r.tools.codex.path})`,
116
+ r.complete
117
+ ? 'status: complete'
118
+ : r.installed
119
+ ? 'status: partial (run: hcc shim install)'
120
+ : 'status: not installed (run: hcc shim install)'
121
+ ].join('\n'));
122
+ return;
123
+ }
124
+ throw new CliError('BAD_ARGS', `Unknown shim subcommand: ${sub}`);
125
+ }
126
+
127
+ // ─── hcc setup (maintenance bootstrap) ───────────────────────────────────────
128
+
129
+ async function cmdSetup(ctx, args) {
130
+ const opts = parseOpts(args, { booleans: ['quiet'] });
131
+ const log = opts.quiet ? () => {} : console.log;
132
+
133
+ log('hello-cc setup\n');
134
+
135
+ // 1. Init project if needed
136
+ if (!fs.existsSync(ctx.dbPath)) {
137
+ const db = connect(ctx);
138
+ db.close();
139
+ writeGuidance(ctx);
140
+ log(`✓ project initialized: ${ctx.root}`);
141
+ } else {
142
+ log(`✓ project already initialized`);
143
+ }
144
+
145
+ // 2. Install Claude Code + Codex hooks
146
+ const { installClaudeHooks, verifyClaudeHooks, installCodexHooks, verifyCodexHooks,
147
+ installShims, installPathEntry } = await loadSetup();
148
+ const hccBin = commandPath();
149
+
150
+ if (!verifyClaudeHooks()) {
151
+ installClaudeHooks(hccBin);
152
+ log('✓ Claude Code hooks installed → ~/.claude/settings.json');
153
+ } else {
154
+ log('✓ Claude Code hooks already installed');
155
+ }
156
+
157
+ if (!verifyCodexHooks()) {
158
+ try { installCodexHooks(hccBin); log('✓ Codex hooks installed → ~/.codex/hooks.json'); }
159
+ catch { log('⚠ Codex hooks installation failed (ignored)'); }
160
+ } else {
161
+ log('✓ Codex hooks already installed');
162
+ }
163
+
164
+ // 3. Install shims
165
+ const result = installShims(hccBin);
166
+ if (result.installed.length) {
167
+ if (result.changed.length) {
168
+ log(`✓ shims installed → ${result.changed.join(', ')}`);
169
+ } else {
170
+ log(`✓ shims already installed → ${result.installed.join(', ')}`);
171
+ }
172
+ if (result.skipped.length) log(`⚠ shims skipped: ${result.skipped.join(', ')}`);
173
+ const { alreadyPresent, rcFile } = installPathEntry();
174
+ if (!alreadyPresent) {
175
+ log(`✓ PATH updated in ${rcFile}`);
176
+ log(` run: source ${rcFile} (or open a new terminal)`);
177
+ } else {
178
+ log(`✓ PATH entry already present in ${rcFile}`);
179
+ }
180
+ } else {
181
+ log('⚠ shims: claude/codex not found on PATH — skipped');
182
+ }
183
+
184
+ log('\nDone. Run `hcc web` for the default coordinated terminal experience.');
185
+ }
186
+
187
+ // ─── hcc update ──────────────────────────────────────────────────────────────
188
+
189
+ async function cmdUpdate(ctx, args) {
190
+ if (args[0] === '--help' || args[0] === '-h') return helpUpdate();
191
+
192
+ const opts = parseOpts(args, { booleans: ['dry-run'] });
193
+ validateOpts('update', opts, ['tag', 'registry', 'dry-run']);
194
+ const tag = opts.tag || 'latest';
195
+ if (!/^[A-Za-z0-9._+-]+$/.test(tag)) {
196
+ throw new CliError('BAD_ARGS', '--tag must be an npm dist-tag or version');
197
+ }
198
+
199
+ const packageSpec = `${NPM_PACKAGE_NAME}@${tag}`;
200
+ const npmArgs = ['install', '-g', packageSpec];
201
+ if (opts.registry) npmArgs.push('--registry', opts.registry);
202
+ const command = shellCommand(['npm', ...npmArgs]);
203
+ const data = { package: NPM_PACKAGE_NAME, tag, registry: opts.registry || null, command, dry_run: Boolean(opts['dry-run']) };
204
+
205
+ if (opts['dry-run']) {
206
+ printResult(ctx, data, () => `would run: ${command}`);
207
+ return;
208
+ }
209
+
210
+ const result = spawnSync('npm', npmArgs, {
211
+ encoding: ctx.json ? 'utf8' : undefined,
212
+ stdio: ctx.json ? ['ignore', 'pipe', 'pipe'] : 'inherit'
213
+ });
214
+ if (result.error) {
215
+ throw new CliError('UPDATE_FAILED', `npm update command failed: ${result.error.message}`);
216
+ }
217
+ if (result.status !== 0) {
218
+ const output = ctx.json
219
+ ? [result.stdout, result.stderr].filter(Boolean).join('\n').trim()
220
+ : '';
221
+ throw new CliError('UPDATE_FAILED', output || `npm exited with status ${result.status}`);
222
+ }
223
+
224
+ printResult(ctx, data, () => `updated ${packageSpec}`);
225
+ }
226
+
227
+ // ─── hcc uninstall ───────────────────────────────────────────────────────────
228
+
229
+ async function cmdUninstall(ctx, args) {
230
+ if (args[0] === '--help' || args[0] === '-h') {
231
+ helpUninstall();
232
+ return;
233
+ }
234
+
235
+ const opts = parseOpts(args, { booleans: ['purge', 'yes'] });
236
+ if (opts.purge && !opts.yes) {
237
+ throw new CliError('CONFIRM_REQUIRED', 'Refusing to purge project data without --yes');
238
+ }
239
+
240
+ const lines = [];
241
+
242
+ let runtime = null;
243
+ try {
244
+ runtime = readRuntime(ctx);
245
+ } catch (err) {
246
+ if (!(err instanceof CliError && err.code === 'RUNTIME_NOT_RUNNING')) throw err;
247
+ }
248
+ if (runtime) {
249
+ try {
250
+ await runtimeRequest(ctx, 'POST', '/api/runtime/stop', {}, runtime);
251
+ lines.push('runtime stopped');
252
+ } catch (err) {
253
+ if (!(err instanceof CliError && err.code === 'RUNTIME_UNREACHABLE')) throw err;
254
+ try { fs.rmSync(runtimePath(ctx), { force: true }); } catch {}
255
+ lines.push('stale runtime file removed');
256
+ }
257
+ } else {
258
+ lines.push('runtime not running');
259
+ }
260
+
261
+ const { uninstallClaudeHooks, uninstallCodexHooks, uninstallShims, uninstallPathEntry } = await loadSetup();
262
+ const claude = uninstallClaudeHooks();
263
+ const codex = uninstallCodexHooks();
264
+ const shims = uninstallShims();
265
+ const pathEntry = uninstallPathEntry();
266
+ lines.push(claude ? 'Claude Code hooks removed' : 'Claude Code hooks not found');
267
+ lines.push(codex ? 'Codex hooks removed' : 'Codex hooks not found');
268
+ lines.push(shims.length ? `shims removed: ${shims.join(', ')}` : 'shims not found');
269
+ lines.push(pathEntryRemovalMessage(pathEntry));
270
+
271
+ let guidance = [];
272
+ let purged = false;
273
+ if (opts.purge) {
274
+ guidance = removeGuidanceBlocks(ctx);
275
+ try {
276
+ fs.rmSync(path.join(ctx.root, '.hello-cc'), { recursive: true, force: true });
277
+ purged = true;
278
+ lines.push(`project data removed: ${path.join(ctx.root, '.hello-cc')}`);
279
+ } catch (err) {
280
+ throw new CliError('PURGE_FAILED', `Could not remove .hello-cc: ${err.message}`);
281
+ }
282
+ if (guidance.length) lines.push(`guidance blocks removed: ${guidance.join(', ')}`);
283
+ } else {
284
+ lines.push('project data kept; run hcc uninstall --purge --yes to remove .hello-cc and guidance blocks');
285
+ }
286
+
287
+ printResult(ctx, { runtime: Boolean(runtime), claude, codex, shims, path_entry: pathEntry, purge: purged, guidance }, () => lines.join('\n'));
288
+ }
289
+
290
+ // ─── hcc scan ────────────────────────────────────────────────────────────────
291
+
292
+
293
+ return { cmdInstallHooks, cmdShim, cmdSetup, cmdUpdate, cmdUninstall };
294
+ }
@@ -0,0 +1,101 @@
1
+ // Lifecycle CLI commands extracted from bin/hcc.mjs: init, register, env, join.
2
+
3
+ export function createLifecycleCommands(deps) {
4
+ const {
5
+ connect, now, addEvent, printResult, CliError, parseOpts, intOpt,
6
+ registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
7
+ upsertPeer, upsertCanonicalPeerBinding, detectBranch,
8
+ writeGuidance, helpEnv, helpJoin, shellExports, path, process
9
+ } = deps;
10
+
11
+ async function cmdInit(ctx, args) {
12
+ registerProjectActivity(ctx);
13
+ const opts = parseOpts(args, { booleans: ['no-guidance'] });
14
+ const db = connect(ctx);
15
+ const guidance = opts['no-guidance'] ? null : writeGuidance(ctx);
16
+ addEvent(db, 'mesh.init', 'human', null, { root: ctx.root, db: ctx.dbPath, guidance });
17
+ printResult(ctx, { root: ctx.root, db: ctx.dbPath, guidance }, (data) => [
18
+ 'hello-cc initialized',
19
+ `root: ${data.root}`,
20
+ `db: ${data.db}`,
21
+ data.guidance ? `guidance: ${data.guidance}` : 'guidance: skipped'
22
+ ].join('\n'));
23
+ }
24
+
25
+ async function cmdRegister(ctx, args) {
26
+ registerProjectActivity(ctx);
27
+ const opts = parseOpts(args, { arrays: ['cap'] });
28
+ const identity = resolveCurrentPeer(ctx, opts, 'peer', opts.kind || 'shell');
29
+ const id = identity.id;
30
+ const db = connect(ctx);
31
+ const peer = {
32
+ id,
33
+ kind: opts.kind || 'other',
34
+ role: opts.role || '',
35
+ worktree: path.resolve(opts.worktree || ctx.cwd),
36
+ branch: opts.branch || detectBranch(ctx.cwd),
37
+ pid: intOpt(opts, 'pid', process.ppid),
38
+ status: opts.status || 'idle',
39
+ capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || '')
40
+ };
41
+ upsertPeer(db, peer);
42
+ addEvent(db, 'peer.registered', id, null, peer);
43
+ printResult(ctx, peer, (data) => `registered ${data.id} (${data.kind}${data.role ? `, ${data.role}` : ''})`);
44
+ }
45
+
46
+ async function cmdEnv(ctx, args) {
47
+ if (args[0] === '--help' || args[0] === '-h') return helpEnv();
48
+ const opts = parseOpts(args);
49
+ const peer = resolveTargetPeer(ctx, opts, 'peer', 'shell').id;
50
+ const values = {
51
+ HCC_PEER: peer,
52
+ HCC_ROOT: ctx.root,
53
+ HCC_DB: ctx.dbPath
54
+ };
55
+ printResult(ctx, values, (data) => shellExports(data));
56
+ }
57
+
58
+ async function cmdJoin(ctx, args) {
59
+ if (args[0] === '--help' || args[0] === '-h') return helpJoin();
60
+ registerProjectActivity(ctx);
61
+ const opts = parseOpts(args, { arrays: ['cap'] });
62
+ const identity = resolveCurrentPeer(ctx, opts, 'peer', opts.kind || 'shell');
63
+ const id = identity.id;
64
+ const peer = {
65
+ id,
66
+ kind: opts.kind || 'other',
67
+ role: opts.role || 'peer',
68
+ worktree: path.resolve(opts.worktree || ctx.cwd),
69
+ branch: opts.branch || detectBranch(ctx.cwd),
70
+ pid: intOpt(opts, 'pid', process.ppid),
71
+ status: opts.status || 'working',
72
+ capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || 'manual-shell')
73
+ };
74
+ const db = connect(ctx);
75
+ try {
76
+ upsertPeer(db, peer);
77
+ upsertCanonicalPeerBinding(db, {
78
+ peer: id,
79
+ provider: peer.kind,
80
+ provider_session_id: null,
81
+ provider_session_name: null,
82
+ resume_mode: 'manual',
83
+ resume_arg: null,
84
+ command: null,
85
+ transport: 'manual-shell',
86
+ runtime_session_id: id
87
+ }, true);
88
+ addEvent(db, 'peer.joined', id, null, { kind: peer.kind, role: peer.role, root: ctx.root });
89
+ } finally {
90
+ db.close();
91
+ }
92
+ const exports = {
93
+ HCC_PEER: id,
94
+ HCC_ROOT: ctx.root,
95
+ HCC_DB: ctx.dbPath
96
+ };
97
+ printResult(ctx, exports, (data) => shellExports(data));
98
+ }
99
+
100
+ return { cmdInit, cmdRegister, cmdEnv, cmdJoin };
101
+ }