@logicseek/hello-cc 0.1.8 → 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.
- package/CHANGELOG.md +117 -0
- package/README.md +26 -4
- package/README.zh-CN.md +22 -3
- package/bin/hcc.mjs +3 -5622
- package/docs/README.md +8 -0
- package/docs/README.zh-CN.md +6 -0
- package/docs/architecture.md +29 -18
- package/docs/architecture.zh-CN.md +20 -10
- package/docs/commands.md +16 -5
- package/docs/commands.zh-CN.md +16 -5
- package/docs/current-state-review-2026-08.md +341 -0
- package/docs/defect-review-2026-07.md +382 -0
- package/docs/guide.md +53 -5
- package/docs/guide.zh-CN.md +46 -3
- package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
- package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
- package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
- package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
- package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
- package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
- package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
- package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
- package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
- package/lib/cli/app.mjs +642 -0
- package/lib/cli/commands/coordination.mjs +247 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +852 -0
- package/lib/cli/commands/hook.mjs +205 -0
- package/lib/cli/commands/install.mjs +294 -0
- package/lib/cli/commands/lifecycle.mjs +101 -0
- package/lib/cli/commands/lock.mjs +197 -0
- package/lib/cli/commands/misc.mjs +61 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +233 -0
- package/lib/cli/commands/query.mjs +96 -0
- package/lib/cli/commands/run.mjs +463 -0
- package/lib/cli/commands/task.mjs +405 -0
- package/lib/cli/commands/team.mjs +131 -0
- package/lib/cli/commands/tmux.mjs +838 -0
- package/lib/cli/commands/up.mjs +135 -0
- package/lib/cli-args.mjs +16 -2
- package/lib/coordination-state.mjs +112 -51
- package/lib/core/coordination/automation.mjs +5 -8
- package/lib/core/coordination/clock-safety.mjs +323 -0
- package/lib/core/coordination/event-retention.mjs +17 -0
- package/lib/core/coordination/gc-plan.mjs +614 -0
- package/lib/core/coordination/lease-renewal.mjs +102 -0
- package/lib/core/coordination/lock-evidence.mjs +77 -0
- package/lib/core/coordination/optimistic-evidence.mjs +26 -0
- package/lib/core/coordination/tasks.mjs +156 -36
- package/lib/core/peers/evidence-runtime.mjs +312 -0
- package/lib/core/peers/evidence.mjs +103 -0
- package/lib/core/peers/hook-owner.mjs +32 -0
- package/lib/core/peers/liveness.mjs +44 -4
- package/lib/core/peers/peer-helpers.mjs +121 -0
- package/lib/core/peers/session.mjs +8 -2
- package/lib/core/peers/tmux-safety.mjs +290 -0
- package/lib/core/sessions/launch.mjs +15 -1
- package/lib/db/connection.mjs +104 -0
- package/lib/db/events.mjs +34 -0
- package/lib/db/migration-backup.mjs +225 -0
- package/lib/db/schema.mjs +102 -5
- package/lib/db/stores/peers.mjs +34 -0
- package/lib/integrations/peers/identity.mjs +62 -16
- package/lib/integrations/shims/script.mjs +139 -15
- package/lib/process/identity.mjs +192 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +47 -0
- package/lib/runtime/buffer-evidence.mjs +336 -0
- package/lib/runtime/buffer-gc-protocol.mjs +167 -0
- package/lib/runtime/buffer-gc.mjs +270 -0
- package/lib/runtime/client.mjs +61 -8
- package/lib/runtime/fatal-shutdown.mjs +54 -0
- package/lib/runtime/project-path.mjs +139 -0
- package/lib/runtime/projects.mjs +320 -40
- package/lib/runtime/state.mjs +232 -29
- package/lib/shared/cleanup-error.mjs +54 -0
- package/lib/shared/clock-grace.mjs +106 -0
- package/lib/shared/errors.mjs +9 -0
- package/lib/shared/file-lock.mjs +370 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +201 -0
- package/lib/terminal/tmux.mjs +78 -4
- package/lib/tmux.mjs +4 -0
- package/lib/ui/help.mjs +53 -15
- package/lib/web/api-version.mjs +42 -0
- package/lib/web/auto-attach.mjs +185 -0
- package/lib/web/buffer-gc-runtime.mjs +175 -0
- package/lib/web/cookie-auth.mjs +119 -0
- package/lib/web/external-sessions.mjs +254 -0
- package/lib/web/http-routes.mjs +542 -0
- package/lib/web/http.mjs +143 -7
- package/lib/web/liveness-reaper.mjs +145 -0
- package/lib/web/peer-actions.mjs +132 -23
- package/lib/web/project-contexts.mjs +178 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +714 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +333 -0
- package/lib/web/tls.mjs +365 -0
- package/lib/web/tmux-sessions.mjs +1361 -0
- package/lib/web/tmux-stream.mjs +166 -0
- package/lib/web/ui-template.mjs +152 -20
- package/package.json +5 -2
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5646 -262
package/lib/db/schema.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { aggregateCleanupFailure } from '../shared/cleanup-error.mjs';
|
|
1
2
|
import { CliError } from '../shared/errors.mjs';
|
|
2
3
|
|
|
3
|
-
export const DB_SCHEMA_VERSION =
|
|
4
|
+
export const DB_SCHEMA_VERSION = 7;
|
|
4
5
|
|
|
5
6
|
const sleepBuffer = new SharedArrayBuffer(4);
|
|
6
7
|
const sleepView = new Int32Array(sleepBuffer);
|
|
@@ -21,6 +22,18 @@ function isSqliteBusy(err) {
|
|
|
21
22
|
/SQLITE_BUSY|SQLITE_LOCKED|database is locked|database is busy/i.test(text);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function failedRollbackMetadata(db, context) {
|
|
26
|
+
let transactionActive = null;
|
|
27
|
+
try {
|
|
28
|
+
transactionActive = Boolean(db.isTransaction);
|
|
29
|
+
} catch {}
|
|
30
|
+
return {
|
|
31
|
+
context,
|
|
32
|
+
transactionActive,
|
|
33
|
+
connectionUsable: false
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
export function execWithBusyRetry(db, sql, { attempts = 30, delayMs = 100, ignoreBusy = false } = {}) {
|
|
25
38
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
26
39
|
try {
|
|
@@ -47,14 +60,49 @@ export function tx(db, fn) {
|
|
|
47
60
|
} catch (err) {
|
|
48
61
|
try {
|
|
49
62
|
db.exec('ROLLBACK;');
|
|
50
|
-
} catch {
|
|
51
|
-
|
|
63
|
+
} catch (cleanupError) {
|
|
64
|
+
throw aggregateCleanupFailure(
|
|
65
|
+
err,
|
|
66
|
+
cleanupError,
|
|
67
|
+
'Write transaction failed and rollback also failed',
|
|
68
|
+
failedRollbackMetadata(db, 'Write transaction rollback')
|
|
69
|
+
);
|
|
52
70
|
}
|
|
53
71
|
throw err;
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
// Deferred read transaction: gives a consistent snapshot for multi-table reads
|
|
76
|
+
// without taking a write lock (unlike tx's BEGIN IMMEDIATE). Use for read-only
|
|
77
|
+
// snapshot assembly so peers/tasks/locks counts cannot drift mid-read (conc-06).
|
|
78
|
+
export function readTx(db, fn, options = {}) {
|
|
79
|
+
db.exec('BEGIN;');
|
|
80
|
+
try {
|
|
81
|
+
const result = fn();
|
|
82
|
+
db.exec('COMMIT;');
|
|
83
|
+
return result;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
try {
|
|
86
|
+
db.exec('ROLLBACK;');
|
|
87
|
+
} catch (cleanupError) {
|
|
88
|
+
throw aggregateCleanupFailure(
|
|
89
|
+
err,
|
|
90
|
+
cleanupError,
|
|
91
|
+
`${options.cleanupContext || 'Read transaction'} failed and rollback also failed`,
|
|
92
|
+
failedRollbackMetadata(
|
|
93
|
+
db,
|
|
94
|
+
`${options.cleanupContext || 'Read transaction'} rollback`
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function initSchema(db, {
|
|
103
|
+
beforeMigration = null,
|
|
104
|
+
beforePostMigrationIndexes = null
|
|
105
|
+
} = {}) {
|
|
58
106
|
const existingVersion = readSchemaVersion(db);
|
|
59
107
|
if (existingVersion > DB_SCHEMA_VERSION) {
|
|
60
108
|
throw new CliError('DB_SCHEMA_TOO_NEW', `Database schema version ${existingVersion} is newer than this hcc (${DB_SCHEMA_VERSION})`, {
|
|
@@ -62,6 +110,17 @@ export function initSchema(db, { beforePostMigrationIndexes = null } = {}) {
|
|
|
62
110
|
supported_schema_version: DB_SCHEMA_VERSION
|
|
63
111
|
});
|
|
64
112
|
}
|
|
113
|
+
const hasExistingSchema = Boolean(db.prepare(`
|
|
114
|
+
SELECT 1
|
|
115
|
+
FROM sqlite_master
|
|
116
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
117
|
+
LIMIT 1
|
|
118
|
+
`).get());
|
|
119
|
+
if (existingVersion < DB_SCHEMA_VERSION &&
|
|
120
|
+
(existingVersion > 0 || hasExistingSchema) &&
|
|
121
|
+
typeof beforeMigration === 'function') {
|
|
122
|
+
beforeMigration({ fromVersion: existingVersion, toVersion: DB_SCHEMA_VERSION });
|
|
123
|
+
}
|
|
65
124
|
createBaseSchema(db);
|
|
66
125
|
runSchemaMigrations(db);
|
|
67
126
|
if (typeof beforePostMigrationIndexes === 'function') beforePostMigrationIndexes(db);
|
|
@@ -88,6 +147,8 @@ function createBaseSchema(db) {
|
|
|
88
147
|
worktree TEXT,
|
|
89
148
|
branch TEXT,
|
|
90
149
|
pid INTEGER,
|
|
150
|
+
pid_start_token TEXT,
|
|
151
|
+
pid_command_hash TEXT,
|
|
91
152
|
status TEXT NOT NULL DEFAULT 'idle',
|
|
92
153
|
capabilities TEXT,
|
|
93
154
|
created_at INTEGER NOT NULL,
|
|
@@ -155,7 +216,8 @@ function createBaseSchema(db) {
|
|
|
155
216
|
task_id INTEGER,
|
|
156
217
|
reason TEXT,
|
|
157
218
|
expires_at INTEGER NOT NULL,
|
|
158
|
-
created_at INTEGER NOT NULL
|
|
219
|
+
created_at INTEGER NOT NULL,
|
|
220
|
+
ttl_sec INTEGER NOT NULL DEFAULT 900
|
|
159
221
|
);
|
|
160
222
|
|
|
161
223
|
CREATE TABLE IF NOT EXISTS handoffs (
|
|
@@ -294,6 +356,35 @@ function runSchemaMigrations(db) {
|
|
|
294
356
|
CREATE INDEX IF NOT EXISTS idx_locks_base_scope ON locks(base_resource, scope);
|
|
295
357
|
`);
|
|
296
358
|
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
version: 6,
|
|
362
|
+
name: 'persist lock ttl',
|
|
363
|
+
up(database) {
|
|
364
|
+
const hadTtlColumn = tableColumns(database, 'locks').has('ttl_sec');
|
|
365
|
+
addColumnIfMissing(database, 'locks', 'ttl_sec', 'ttl_sec INTEGER NOT NULL DEFAULT 900');
|
|
366
|
+
database.exec(hadTtlColumn
|
|
367
|
+
? `
|
|
368
|
+
UPDATE locks
|
|
369
|
+
SET ttl_sec = 900
|
|
370
|
+
WHERE ttl_sec IS NULL OR ttl_sec <= 0;
|
|
371
|
+
`
|
|
372
|
+
: `
|
|
373
|
+
UPDATE locks
|
|
374
|
+
SET ttl_sec = CASE
|
|
375
|
+
WHEN expires_at > created_at THEN MIN(expires_at - created_at, 3600)
|
|
376
|
+
ELSE 900
|
|
377
|
+
END;
|
|
378
|
+
`);
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
version: 7,
|
|
383
|
+
name: 'peer process identity fingerprints',
|
|
384
|
+
up(database) {
|
|
385
|
+
addColumnIfMissing(database, 'peers', 'pid_start_token', 'pid_start_token TEXT');
|
|
386
|
+
addColumnIfMissing(database, 'peers', 'pid_command_hash', 'pid_command_hash TEXT');
|
|
387
|
+
}
|
|
297
388
|
}
|
|
298
389
|
];
|
|
299
390
|
let version = readSchemaVersion(db);
|
|
@@ -304,6 +395,12 @@ function runSchemaMigrations(db) {
|
|
|
304
395
|
});
|
|
305
396
|
}
|
|
306
397
|
const applied = new Set(db.prepare('SELECT version FROM schema_migrations').all().map((row) => Number(row.version)));
|
|
398
|
+
// Already fully migrated: skip the write transaction entirely. Read-only
|
|
399
|
+
// commands (hcc status/peers/task list) then open the DB without taking a
|
|
400
|
+
// RESERVED write lock or appending WAL frames on every connect (conc-01).
|
|
401
|
+
if (version === DB_SCHEMA_VERSION && migrations.every((m) => applied.has(m.version))) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
307
404
|
tx(db, () => {
|
|
308
405
|
for (const migration of migrations) {
|
|
309
406
|
if (version >= migration.version && applied.has(migration.version)) continue;
|
package/lib/db/stores/peers.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
mergePeerBinding,
|
|
8
8
|
mergeRuntimeBinding
|
|
9
9
|
} from '../../core/peers/bindings.mjs';
|
|
10
|
+
import { providerSessionPeerId } from '../../core/peers/session.mjs';
|
|
10
11
|
|
|
11
12
|
function defaultNow() {
|
|
12
13
|
return Math.floor(Date.now() / 1000);
|
|
@@ -14,6 +15,23 @@ function defaultNow() {
|
|
|
14
15
|
|
|
15
16
|
function noopEvent() {}
|
|
16
17
|
|
|
18
|
+
function crossesV1ProviderIdentityBoundary(binding, conflict) {
|
|
19
|
+
const providerSession = bindingProviderSessionValue(binding);
|
|
20
|
+
if (!providerSession || !binding?.provider) return false;
|
|
21
|
+
const v1Peer = providerSessionPeerId(binding.provider, providerSession);
|
|
22
|
+
return binding.peer === v1Peer && conflict?.peer !== v1Peer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function withoutProviderSession(binding) {
|
|
26
|
+
return {
|
|
27
|
+
...binding,
|
|
28
|
+
provider_session_id: null,
|
|
29
|
+
provider_session_name: null,
|
|
30
|
+
resume_mode: 'unknown',
|
|
31
|
+
resume_arg: null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
export function createPeerBindingStore(deps = {}) {
|
|
18
36
|
const now = typeof deps.now === 'function' ? deps.now : defaultNow;
|
|
19
37
|
const addEvent = typeof deps.addEvent === 'function' ? deps.addEvent : noopEvent;
|
|
@@ -125,6 +143,22 @@ export function createPeerBindingStore(deps = {}) {
|
|
|
125
143
|
const conflictRuntime = bindingHasRuntime(conflict);
|
|
126
144
|
const providerSession = bindingProviderSessionValue(next);
|
|
127
145
|
const override = Boolean(options.override);
|
|
146
|
+
const preV1Conflict = crossesV1ProviderIdentityBoundary(next, conflict);
|
|
147
|
+
const passiveObservation = !incomingRuntime || next.resume_mode === 'detected';
|
|
148
|
+
|
|
149
|
+
if (preV1Conflict && passiveObservation) {
|
|
150
|
+
next = withoutProviderSession(next);
|
|
151
|
+
return { peer: next.peer, binding: next, merged_from: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (preV1Conflict && incomingRuntime && !override) {
|
|
155
|
+
throw new CliError('PROVIDER_SESSION_IN_USE', `${next.provider} session ${providerSession} is already bound to ${conflict.peer}`, {
|
|
156
|
+
peer: conflict.peer,
|
|
157
|
+
provider: conflict.provider,
|
|
158
|
+
provider_session: providerSession,
|
|
159
|
+
runtime_target: conflict.runtime_target || null
|
|
160
|
+
});
|
|
161
|
+
}
|
|
128
162
|
|
|
129
163
|
if (override && incomingRuntime) {
|
|
130
164
|
db.prepare('DELETE FROM peer_bindings WHERE peer = ?').run(conflict.peer);
|
|
@@ -84,28 +84,74 @@ function detectCliKindFromProcess(args, env) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
export function findLinuxAncestorCliInfo(startPid, options = {}) {
|
|
88
|
+
const read = typeof options.read === 'function'
|
|
89
|
+
? options.read
|
|
90
|
+
: (pid) => ({
|
|
91
|
+
args: readProcCmdline(pid),
|
|
92
|
+
env: readProcEnv(pid),
|
|
93
|
+
parent: readProcParentPid(pid)
|
|
94
|
+
});
|
|
95
|
+
let pid = Number(startPid);
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (let depth = 0; Number.isInteger(pid) && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
|
|
98
|
+
seen.add(pid);
|
|
99
|
+
const row = read(pid);
|
|
100
|
+
if (!row) return null;
|
|
101
|
+
const args = Array.isArray(row.args) ? row.args : [];
|
|
102
|
+
const env = row.env && typeof row.env === 'object' && !Array.isArray(row.env) ? row.env : {};
|
|
103
|
+
const kind = detectCliKindFromProcess(args, env);
|
|
104
|
+
if (kind) return { pid, kind, args, env };
|
|
105
|
+
pid = Number(row.parent);
|
|
92
106
|
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
93
109
|
|
|
94
|
-
|
|
110
|
+
export function parseMacAncestorLine(text) {
|
|
111
|
+
const line = String(text ?? '').trimEnd();
|
|
112
|
+
if (!line || line.includes('\n') || line.includes('\r')) return null;
|
|
113
|
+
const match = line.match(/^\s*([0-9]+)\s+(.+?)\s*$/);
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
const ppid = Number(match[1]);
|
|
116
|
+
const command = match[2];
|
|
117
|
+
if (!Number.isSafeInteger(ppid) || ppid < 0 || !command) return null;
|
|
118
|
+
return { ppid, command };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function inspectMacAncestor(pid) {
|
|
122
|
+
const result = spawnSync('ps', ['-ww', '-o', 'ppid=', '-o', 'command=', '-p', String(pid)], {
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
125
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C' }
|
|
126
|
+
});
|
|
127
|
+
if (result.error || result.status !== 0) return null;
|
|
128
|
+
return result.stdout || '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function findMacAncestorCliInfo(startPid, options = {}) {
|
|
132
|
+
const inspect = typeof options.inspect === 'function' ? options.inspect : inspectMacAncestor;
|
|
133
|
+
let pid = Number(startPid);
|
|
95
134
|
const seen = new Set();
|
|
96
|
-
for (let depth = 0; pid && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
|
|
135
|
+
for (let depth = 0; Number.isInteger(pid) && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
|
|
97
136
|
seen.add(pid);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const kind = detectCliKindFromProcess(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
pid = readProcParentPid(pid);
|
|
137
|
+
const parsed = parseMacAncestorLine(inspect(pid));
|
|
138
|
+
if (!parsed) return null;
|
|
139
|
+
const kind = detectCliKindFromProcess([parsed.command], {});
|
|
140
|
+
// `ps command` is sufficient for provider-kind detection but is not a
|
|
141
|
+
// lossless argv encoding. Do not feed it to resume-ID parsing.
|
|
142
|
+
if (kind) return { pid, kind, args: [], env: {}, command: parsed.command };
|
|
143
|
+
pid = parsed.ppid;
|
|
106
144
|
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
107
147
|
|
|
108
|
-
|
|
148
|
+
export function readAncestorCliInfo() {
|
|
149
|
+
if (ancestorCliInfoCache !== undefined) return ancestorCliInfoCache;
|
|
150
|
+
ancestorCliInfoCache = process.platform === 'linux'
|
|
151
|
+
? findLinuxAncestorCliInfo(process.ppid)
|
|
152
|
+
: process.platform === 'darwin'
|
|
153
|
+
? findMacAncestorCliInfo(process.ppid)
|
|
154
|
+
: null;
|
|
109
155
|
return ancestorCliInfoCache;
|
|
110
156
|
}
|
|
111
157
|
|
|
@@ -57,8 +57,59 @@ if [ -z "$HCC_ROOT" ]; then
|
|
|
57
57
|
exec "$REAL_BIN" "$@"
|
|
58
58
|
fi
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
# sess-04: sha1 hash of the FULL provider id/name, first 8 hex chars — must
|
|
61
|
+
# match the JS shortHash() exactly (lib/core/peers/format.mjs). Name-based ids
|
|
62
|
+
# that share a prefix used to collide into the same peer id.
|
|
63
|
+
is_sha1_hex() {
|
|
64
|
+
[ "\${#1}" -eq 40 ] || return 1
|
|
65
|
+
case "$1" in
|
|
66
|
+
*[!0-9a-f]*) return 1 ;;
|
|
67
|
+
esac
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
peer_hash() {
|
|
72
|
+
local HASH=""
|
|
73
|
+
local OUTPUT=""
|
|
74
|
+
|
|
75
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
76
|
+
OUTPUT=$(printf '%s' "$1" | openssl dgst -sha1 2>/dev/null || true)
|
|
77
|
+
HASH=$(printf '%s\n' "$OUTPUT" | awk 'NR == 1 { print $NF }' | tr '[:upper:]' '[:lower:]')
|
|
78
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [ -z "$HASH" ] && command -v sha1sum >/dev/null 2>&1; then
|
|
82
|
+
OUTPUT=$(printf '%s' "$1" | sha1sum 2>/dev/null || true)
|
|
83
|
+
HASH=$(printf '%s\n' "$OUTPUT" | awk 'NR == 1 { print $1 }' | tr '[:upper:]' '[:lower:]')
|
|
84
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if [ -z "$HASH" ] && command -v shasum >/dev/null 2>&1; then
|
|
88
|
+
OUTPUT=$(printf '%s' "$1" | shasum 2>/dev/null || true)
|
|
89
|
+
HASH=$(printf '%s\n' "$OUTPUT" | awk 'NR == 1 { print $1 }' | tr '[:upper:]' '[:lower:]')
|
|
90
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# A hello-cc environment always has node on PATH (hcc itself is a node CLI),
|
|
94
|
+
# so this fallback guarantees registration even on minimal systems without
|
|
95
|
+
# openssl/sha1sum/shasum. Digest must match the JS shortHash exactly.
|
|
96
|
+
if [ -z "$HASH" ] && command -v node >/dev/null 2>&1; then
|
|
97
|
+
HASH=$(printf '%s' "$1" | node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>process.stdout.write(require("node:crypto").createHash("sha1").update(d).digest("hex")))' 2>/dev/null || true)
|
|
98
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if ! is_sha1_hex "$HASH"; then
|
|
102
|
+
printf '%s\n' 'hello-cc: unable to derive peer id: no SHA-1 command produced a valid digest' >&2
|
|
103
|
+
return 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
printf '%.8s' "$HASH"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
provider_peer_id() {
|
|
110
|
+
local HASH_PART=""
|
|
111
|
+
HASH_PART=$(peer_hash "$1") || return 1
|
|
112
|
+
printf '%s-%s' '${tool.kind}' "$HASH_PART"
|
|
62
113
|
}
|
|
63
114
|
|
|
64
115
|
tty_peer_part() {
|
|
@@ -96,13 +147,13 @@ done
|
|
|
96
147
|
if [ -n "$HCC_PEER" ]; then
|
|
97
148
|
PEER_ID="$HCC_PEER"
|
|
98
149
|
elif [ -n "$SESSION_ID" ]; then
|
|
99
|
-
PEER_ID
|
|
150
|
+
PEER_ID=$(provider_peer_id "$SESSION_ID") || exec "$REAL_BIN" "$@"
|
|
100
151
|
elif [ -n "$RESUME_ID" ] && [ "$IS_FORK" != "1" ]; then
|
|
101
|
-
PEER_ID
|
|
152
|
+
PEER_ID=$(provider_peer_id "$RESUME_ID") || exec "$REAL_BIN" "$@"
|
|
102
153
|
elif [ -n "$CLAUDE_CODE_SESSION_ID" ]; then
|
|
103
|
-
PEER_ID
|
|
154
|
+
PEER_ID=$(provider_peer_id "$CLAUDE_CODE_SESSION_ID") || exec "$REAL_BIN" "$@"
|
|
104
155
|
elif [ -n "$SESSION_NAME" ] && [ "$IS_FORK" != "1" ] && [ "$IS_CONTINUE" != "1" ]; then
|
|
105
|
-
PEER_ID
|
|
156
|
+
PEER_ID=$(provider_peer_id "$SESSION_NAME") || exec "$REAL_BIN" "$@"
|
|
106
157
|
else
|
|
107
158
|
PEER_ID="${tool.kind}-\$(tty_peer_part)"
|
|
108
159
|
fi
|
|
@@ -139,14 +190,22 @@ detect_current_hcc_pane() {
|
|
|
139
190
|
return 1
|
|
140
191
|
}
|
|
141
192
|
|
|
193
|
+
hcc_runtime_unavailable_start_failure() {
|
|
194
|
+
case "$1" in
|
|
195
|
+
*"No running web runtime found"*|*"No running hello-cc web runtime found"*|*"Runtime is not reachable"*|*"RUNTIME_NOT_RUNNING"*|*"RUNTIME_UNREACHABLE"*|*"RUNTIME_START_FAILED"*|*"RUNTIME_START_TIMEOUT"*)
|
|
196
|
+
return 0
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
return 1
|
|
200
|
+
}
|
|
201
|
+
|
|
142
202
|
if detect_current_hcc_pane; then
|
|
143
203
|
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
144
204
|
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
145
205
|
PEER_ID="$SESSION_PEER_ID"
|
|
146
206
|
fi
|
|
147
207
|
export HCC_PEER="$PEER_ID"
|
|
148
|
-
|
|
149
|
-
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
208
|
+
HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
150
209
|
exec "$REAL_BIN" "$@"
|
|
151
210
|
fi
|
|
152
211
|
|
|
@@ -155,12 +214,15 @@ HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:
|
|
|
155
214
|
set +e
|
|
156
215
|
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
157
216
|
START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
|
|
158
|
-
(
|
|
217
|
+
(HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
|
|
159
218
|
START_STATUS=$?
|
|
160
219
|
START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
|
|
161
220
|
rm -f "$START_OUT_FILE"
|
|
162
221
|
set -e
|
|
163
222
|
if [ "$START_STATUS" -ne 0 ]; then
|
|
223
|
+
if hcc_runtime_unavailable_start_failure "$START_OUT"; then
|
|
224
|
+
exec "$REAL_BIN" "$@"
|
|
225
|
+
fi
|
|
164
226
|
printf '%s\\n' "$START_OUT" >&2
|
|
165
227
|
exit "$START_STATUS"
|
|
166
228
|
fi
|
|
@@ -278,8 +340,59 @@ if [ -z "$HCC_ROOT" ]; then
|
|
|
278
340
|
exec "$REAL_BIN" "$@"
|
|
279
341
|
fi
|
|
280
342
|
|
|
281
|
-
|
|
282
|
-
|
|
343
|
+
# sess-04: sha1 hash of the FULL provider id/name, first 8 hex chars — must
|
|
344
|
+
# match the JS shortHash() exactly (lib/core/peers/format.mjs). Name-based ids
|
|
345
|
+
# that share a prefix used to collide into the same peer id.
|
|
346
|
+
is_sha1_hex() {
|
|
347
|
+
[ "\${#1}" -eq 40 ] || return 1
|
|
348
|
+
case "$1" in
|
|
349
|
+
*[!0-9a-f]*) return 1 ;;
|
|
350
|
+
esac
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
peer_hash() {
|
|
355
|
+
local HASH=""
|
|
356
|
+
local OUTPUT=""
|
|
357
|
+
|
|
358
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
359
|
+
OUTPUT=$(printf '%s' "$1" | openssl dgst -sha1 2>/dev/null || true)
|
|
360
|
+
HASH=$(printf '%s\n' "$OUTPUT" | awk 'NR == 1 { print $NF }' | tr '[:upper:]' '[:lower:]')
|
|
361
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
if [ -z "$HASH" ] && command -v sha1sum >/dev/null 2>&1; then
|
|
365
|
+
OUTPUT=$(printf '%s' "$1" | sha1sum 2>/dev/null || true)
|
|
366
|
+
HASH=$(printf '%s\n' "$OUTPUT" | awk 'NR == 1 { print $1 }' | tr '[:upper:]' '[:lower:]')
|
|
367
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
if [ -z "$HASH" ] && command -v shasum >/dev/null 2>&1; then
|
|
371
|
+
OUTPUT=$(printf '%s' "$1" | shasum 2>/dev/null || true)
|
|
372
|
+
HASH=$(printf '%s\n' "$OUTPUT" | awk 'NR == 1 { print $1 }' | tr '[:upper:]' '[:lower:]')
|
|
373
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
# A hello-cc environment always has node on PATH (hcc itself is a node CLI),
|
|
377
|
+
# so this fallback guarantees registration even on minimal systems without
|
|
378
|
+
# openssl/sha1sum/shasum. Digest must match the JS shortHash exactly.
|
|
379
|
+
if [ -z "$HASH" ] && command -v node >/dev/null 2>&1; then
|
|
380
|
+
HASH=$(printf '%s' "$1" | node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>process.stdout.write(require("node:crypto").createHash("sha1").update(d).digest("hex")))' 2>/dev/null || true)
|
|
381
|
+
is_sha1_hex "$HASH" || HASH=""
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
if ! is_sha1_hex "$HASH"; then
|
|
385
|
+
printf '%s\n' 'hello-cc: unable to derive peer id: no SHA-1 command produced a valid digest' >&2
|
|
386
|
+
return 1
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
printf '%.8s' "$HASH"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
provider_peer_id() {
|
|
393
|
+
local HASH_PART=""
|
|
394
|
+
HASH_PART=$(peer_hash "$1") || return 1
|
|
395
|
+
printf '%s-%s' '${tool.kind}' "$HASH_PART"
|
|
283
396
|
}
|
|
284
397
|
|
|
285
398
|
tty_peer_part() {
|
|
@@ -315,7 +428,7 @@ done
|
|
|
315
428
|
if [ -n "$HCC_PEER" ]; then
|
|
316
429
|
PEER_ID="$HCC_PEER"
|
|
317
430
|
elif [ "$SUBCMD" = "resume" ] && [ -n "$RESUME_ID" ]; then
|
|
318
|
-
PEER_ID
|
|
431
|
+
PEER_ID=$(provider_peer_id "$RESUME_ID") || exec "$REAL_BIN" "$@"
|
|
319
432
|
else
|
|
320
433
|
PEER_ID="${tool.kind}-\$(tty_peer_part)"
|
|
321
434
|
fi
|
|
@@ -352,14 +465,22 @@ detect_current_hcc_pane() {
|
|
|
352
465
|
return 1
|
|
353
466
|
}
|
|
354
467
|
|
|
468
|
+
hcc_runtime_unavailable_start_failure() {
|
|
469
|
+
case "$1" in
|
|
470
|
+
*"No running web runtime found"*|*"No running hello-cc web runtime found"*|*"Runtime is not reachable"*|*"RUNTIME_NOT_RUNNING"*|*"RUNTIME_UNREACHABLE"*|*"RUNTIME_START_FAILED"*|*"RUNTIME_START_TIMEOUT"*)
|
|
471
|
+
return 0
|
|
472
|
+
;;
|
|
473
|
+
esac
|
|
474
|
+
return 1
|
|
475
|
+
}
|
|
476
|
+
|
|
355
477
|
if detect_current_hcc_pane; then
|
|
356
478
|
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
357
479
|
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
358
480
|
PEER_ID="$SESSION_PEER_ID"
|
|
359
481
|
fi
|
|
360
482
|
export HCC_PEER="$PEER_ID"
|
|
361
|
-
|
|
362
|
-
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
483
|
+
HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
363
484
|
exec "$REAL_BIN" "$@"
|
|
364
485
|
fi
|
|
365
486
|
|
|
@@ -368,12 +489,15 @@ HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:
|
|
|
368
489
|
set +e
|
|
369
490
|
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
370
491
|
START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
|
|
371
|
-
(
|
|
492
|
+
(HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
|
|
372
493
|
START_STATUS=$?
|
|
373
494
|
START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
|
|
374
495
|
rm -f "$START_OUT_FILE"
|
|
375
496
|
set -e
|
|
376
497
|
if [ "$START_STATUS" -ne 0 ]; then
|
|
498
|
+
if hcc_runtime_unavailable_start_failure "$START_OUT"; then
|
|
499
|
+
exec "$REAL_BIN" "$@"
|
|
500
|
+
fi
|
|
377
501
|
printf '%s\\n' "$START_OUT" >&2
|
|
378
502
|
exit "$START_STATUS"
|
|
379
503
|
fi
|