@logicseek/hello-cc 1.0.2 → 1.0.4
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 +63 -0
- package/docs/dsh.md +3 -3
- package/docs/dsh.zh-CN.md +3 -3
- package/lib/cli/app.mjs +1 -1
- package/lib/cli/commands/hook.mjs +3 -1
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/db/connection.mjs +82 -9
- package/lib/discover.mjs +4 -4
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/process/identity.mjs +36 -15
- package/lib/runtime/client.mjs +9 -1
- package/lib/runtime/native/service.mjs +107 -24
- package/lib/runtime/native/store.mjs +63 -6
- package/lib/runtime/project-path.mjs +90 -7
- package/lib/shared/file-lock.mjs +62 -17
- package/lib/shared/socket-lock-worker.mjs +15 -6
- package/lib/web/codex-app-server.mjs +19 -1
- package/lib/web/codex-sessions.mjs +2 -0
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/http-routes.mjs +11 -7
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +13 -6
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/runtime-main.mjs +24 -7
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +84 -10
- package/lib/web/task-results.mjs +8 -5
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tmux-client-snapshot.mjs +85 -0
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +137 -20
- package/lib/web/ui-interactions.mjs +37 -3
- package/lib/web/ui-native.mjs +21 -9
- package/lib/web/ui-template.mjs +8 -2
- package/package.json +1 -1
- package/scripts/regression.mjs +212 -47
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
+
import { commandPath } from '../cli-runtime.mjs';
|
|
5
|
+
import { shellQuoteArg } from '../format.mjs';
|
|
6
|
+
import { CliError } from '../shared/errors.mjs';
|
|
7
|
+
import { writeJsonSafe } from '../shared/json-file.mjs';
|
|
4
8
|
|
|
5
9
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
6
10
|
const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
|
|
@@ -18,15 +22,15 @@ const CODEX_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'St
|
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Install (or update) hello-cc hooks in ~/.claude/settings.json.
|
|
21
|
-
*
|
|
25
|
+
* Replaces stale hcc entries while retaining unrelated hooks.
|
|
22
26
|
* Returns the settings file path.
|
|
23
27
|
*/
|
|
24
28
|
export function installClaudeHooks(hccBin) {
|
|
25
|
-
const settings =
|
|
29
|
+
const settings = readHookConfig(CLAUDE_SETTINGS_PATH) || {};
|
|
26
30
|
|
|
27
31
|
if (!settings.hooks) settings.hooks = {};
|
|
28
32
|
|
|
29
|
-
const hookCmd = `${hccBin} hook`;
|
|
33
|
+
const hookCmd = `${shellQuoteArg(hccBin)} hook`;
|
|
30
34
|
|
|
31
35
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
32
36
|
settings.hooks[event] = mergeHookEntry(
|
|
@@ -43,29 +47,31 @@ export function installClaudeHooks(hccBin) {
|
|
|
43
47
|
* Remove hello-cc hook entries from ~/.claude/settings.json.
|
|
44
48
|
*/
|
|
45
49
|
export function uninstallClaudeHooks() {
|
|
46
|
-
const settings =
|
|
50
|
+
const settings = readHookConfig(CLAUDE_SETTINGS_PATH);
|
|
47
51
|
if (!settings?.hooks) return false;
|
|
48
52
|
|
|
53
|
+
let removed = false;
|
|
49
54
|
for (const event of Object.keys(settings.hooks)) {
|
|
50
55
|
if (!Array.isArray(settings.hooks[event])) continue;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
56
|
+
const result = removeHccHooks(settings.hooks[event]);
|
|
57
|
+
settings.hooks[event] = result.entries;
|
|
58
|
+
removed ||= result.removed;
|
|
55
59
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
|
|
59
|
-
return
|
|
62
|
+
if (removed) writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
|
|
63
|
+
return removed;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
67
|
* Returns true if hello-cc hooks are present in ~/.claude/settings.json.
|
|
64
68
|
*/
|
|
65
|
-
export function verifyClaudeHooks() {
|
|
66
|
-
const settings =
|
|
69
|
+
export function verifyClaudeHooks(hccBin = commandPath()) {
|
|
70
|
+
const settings = readHookConfig(CLAUDE_SETTINGS_PATH);
|
|
67
71
|
if (!settings?.hooks) return false;
|
|
68
|
-
|
|
72
|
+
const hookCmd = `${shellQuoteArg(hccBin)} hook`;
|
|
73
|
+
return CLAUDE_HOOK_EVENTS.every(event =>
|
|
74
|
+
hasHookEntry(settings.hooks[event], `${hookCmd} ${event.toLowerCase()}`));
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/**
|
|
@@ -75,11 +81,11 @@ export function verifyClaudeHooks() {
|
|
|
75
81
|
* { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [...] }], "Stop": [...] } }
|
|
76
82
|
*/
|
|
77
83
|
export function installCodexHooks(hccBin) {
|
|
78
|
-
const hooks =
|
|
84
|
+
const hooks = readHookConfig(CODEX_HOOKS_PATH) || {};
|
|
79
85
|
|
|
80
86
|
if (!hooks.hooks) hooks.hooks = {};
|
|
81
87
|
|
|
82
|
-
const hookCmd = `${hccBin} hook`;
|
|
88
|
+
const hookCmd = `${shellQuoteArg(hccBin)} hook`;
|
|
83
89
|
|
|
84
90
|
// SessionStart: inject initial coordination context on startup/resume.
|
|
85
91
|
hooks.hooks.SessionStart = mergeCodexHookEntry(
|
|
@@ -111,56 +117,61 @@ export function installCodexHooks(hccBin) {
|
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
export function uninstallCodexHooks() {
|
|
114
|
-
const hooks =
|
|
120
|
+
const hooks = readHookConfig(CODEX_HOOKS_PATH);
|
|
115
121
|
if (!hooks?.hooks) return false;
|
|
116
122
|
|
|
123
|
+
let removed = false;
|
|
117
124
|
for (const event of Object.keys(hooks.hooks)) {
|
|
118
125
|
if (!Array.isArray(hooks.hooks[event])) continue;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
126
|
+
const result = removeHccHooks(hooks.hooks[event]);
|
|
127
|
+
hooks.hooks[event] = result.entries;
|
|
128
|
+
removed ||= result.removed;
|
|
123
129
|
if (hooks.hooks[event].length === 0) delete hooks.hooks[event];
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
writeJsonSafe(CODEX_HOOKS_PATH, hooks);
|
|
127
|
-
return
|
|
132
|
+
if (removed) writeJsonSafe(CODEX_HOOKS_PATH, hooks);
|
|
133
|
+
return removed;
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
export function verifyCodexHooks() {
|
|
131
|
-
const hooks =
|
|
136
|
+
export function verifyCodexHooks(hccBin = commandPath()) {
|
|
137
|
+
const hooks = readHookConfig(CODEX_HOOKS_PATH);
|
|
132
138
|
if (!hooks?.hooks) return false;
|
|
133
|
-
|
|
139
|
+
const hookCmd = `${shellQuoteArg(hccBin)} hook`;
|
|
140
|
+
return CODEX_HOOK_EVENTS.every(event =>
|
|
141
|
+
hasHookEntry(hooks.hooks[event], `${hookCmd} ${event.toLowerCase()}`,
|
|
142
|
+
event === 'PreToolUse' ? 'Bash' : undefined));
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
function mergeHookEntry(existing, command) {
|
|
137
|
-
const entries =
|
|
138
|
-
const alreadyInstalled = entries.some(
|
|
139
|
-
e => Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
|
|
140
|
-
);
|
|
141
|
-
if (alreadyInstalled) return entries;
|
|
146
|
+
const entries = removeHccHooks(hookEntries(existing)).entries;
|
|
142
147
|
entries.push({ hooks: [{ type: 'command', command }] });
|
|
143
148
|
return entries;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
function hasHookEntry(entries) {
|
|
151
|
+
function hasHookEntry(entries, command, matcher) {
|
|
147
152
|
if (!Array.isArray(entries)) return false;
|
|
148
153
|
return entries.some(e =>
|
|
149
|
-
|
|
154
|
+
(matcher === undefined || e?.matcher === matcher) &&
|
|
155
|
+
Array.isArray(e?.hooks) && e.hooks.some(h => h?.type === 'command' && h.command === command)
|
|
150
156
|
);
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
function isHccHookCmd(cmd) {
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
const words = shellWords(cmd);
|
|
161
|
+
if (words?.length) {
|
|
162
|
+
const offset = ['node', 'nodejs', 'node.exe'].includes(path.basename(words[0])) ? 1 : 0;
|
|
163
|
+
if (['hcc', 'hello-cc', 'hcc.mjs'].includes(path.basename(words[offset] || '')) &&
|
|
164
|
+
words[offset + 1] === 'hook') return true;
|
|
165
|
+
}
|
|
166
|
+
// Older installers wrote the absolute script path without shell quoting.
|
|
167
|
+
// Match only that entire generated form, not a command that merely mentions hcc.
|
|
168
|
+
const legacy = typeof cmd === 'string' &&
|
|
169
|
+
/^(\/[^\r\n;&|<>$`()\\]*\/hcc\.mjs) hook (?:sessionstart|userpromptsubmit|pretooluse|posttooluse|stop)$/i.exec(cmd);
|
|
170
|
+
return Boolean(legacy && !/\s\//.test(legacy[1]));
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
function mergeCodexHookEntry(existing, command, matcher) {
|
|
159
|
-
const entries =
|
|
160
|
-
const alreadyInstalled = entries.some(
|
|
161
|
-
e => Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
|
|
162
|
-
);
|
|
163
|
-
if (alreadyInstalled) return entries;
|
|
174
|
+
const entries = removeHccHooks(hookEntries(existing)).entries;
|
|
164
175
|
|
|
165
176
|
const entry = {
|
|
166
177
|
hooks: [{ type: 'command', command }],
|
|
@@ -169,3 +180,84 @@ function mergeCodexHookEntry(existing, command, matcher) {
|
|
|
169
180
|
entries.push(entry);
|
|
170
181
|
return entries;
|
|
171
182
|
}
|
|
183
|
+
|
|
184
|
+
function readHookConfig(filePath) {
|
|
185
|
+
let contents;
|
|
186
|
+
try { contents = fs.readFileSync(filePath, 'utf8'); }
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error?.code === 'ENOENT') return null;
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
let config;
|
|
192
|
+
try { config = JSON.parse(contents); }
|
|
193
|
+
catch { throw new CliError('INVALID_HOOK_CONFIG', `Invalid JSON in ${filePath}; hooks were not changed`); }
|
|
194
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) ||
|
|
195
|
+
(config.hooks !== undefined && (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)))) {
|
|
196
|
+
throw new CliError('INVALID_HOOK_CONFIG', `Invalid hooks structure in ${filePath}; hooks were not changed`);
|
|
197
|
+
}
|
|
198
|
+
if (config.hooks && Object.values(config.hooks).some(entries => !Array.isArray(entries))) {
|
|
199
|
+
throw new CliError('INVALID_HOOK_CONFIG', `Invalid hook event in ${filePath}; hooks were not changed`);
|
|
200
|
+
}
|
|
201
|
+
return config;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hookEntries(existing) {
|
|
205
|
+
if (existing === undefined) return [];
|
|
206
|
+
if (Array.isArray(existing)) return existing;
|
|
207
|
+
throw new CliError('INVALID_HOOK_CONFIG', 'Hook event must be an array; hooks were not changed');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function removeHccHooks(entries) {
|
|
211
|
+
let removed = false;
|
|
212
|
+
const kept = [];
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
215
|
+
kept.push(entry);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const hooks = entry.hooks.filter(hook => {
|
|
219
|
+
if (!isHccHookCmd(hook?.command)) return true;
|
|
220
|
+
removed = true;
|
|
221
|
+
return false;
|
|
222
|
+
});
|
|
223
|
+
if (hooks.length === entry.hooks.length) kept.push(entry);
|
|
224
|
+
else if (hooks.length) kept.push({ ...entry, hooks });
|
|
225
|
+
}
|
|
226
|
+
return { entries: kept, removed };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function shellWords(command) {
|
|
230
|
+
if (typeof command !== 'string') return null;
|
|
231
|
+
const words = [];
|
|
232
|
+
let word = '';
|
|
233
|
+
let started = false;
|
|
234
|
+
let quote = null;
|
|
235
|
+
for (let i = 0; i < command.length; i++) {
|
|
236
|
+
const char = command[i];
|
|
237
|
+
if (quote === "'") {
|
|
238
|
+
if (char === "'") quote = null;
|
|
239
|
+
else word += char;
|
|
240
|
+
} else if (quote === '"') {
|
|
241
|
+
if (char === '"') quote = null;
|
|
242
|
+
else if (char === '\\') word += command[++i] || '';
|
|
243
|
+
else if (char === '$' || char === '`') return null;
|
|
244
|
+
else word += char;
|
|
245
|
+
} else if (/\s/.test(char)) {
|
|
246
|
+
if (started) { words.push(word); word = ''; started = false; }
|
|
247
|
+
} else if (char === "'" || char === '"') {
|
|
248
|
+
quote = char;
|
|
249
|
+
started = true;
|
|
250
|
+
} else if (char === '\\') {
|
|
251
|
+
word += command[++i] || '';
|
|
252
|
+
started = true;
|
|
253
|
+
} else if (';&|<>$`()'.includes(char)) {
|
|
254
|
+
return null;
|
|
255
|
+
} else {
|
|
256
|
+
word += char;
|
|
257
|
+
started = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (quote) return null;
|
|
261
|
+
if (started) words.push(word);
|
|
262
|
+
return words;
|
|
263
|
+
}
|
|
@@ -105,6 +105,29 @@ function detectCliKindFromProcess(args, env) {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function executableCliKind(args) {
|
|
109
|
+
if (argsLookLikeDsh(args)) return 'dsh';
|
|
110
|
+
if (!Array.isArray(args) || !args.length) return null;
|
|
111
|
+
const executable = path.basename(String(args[0] || '')).toLowerCase();
|
|
112
|
+
if (executable === 'codex') return 'codex';
|
|
113
|
+
if (executable === 'claude' || executable === 'claude-code') return 'claude';
|
|
114
|
+
if (!['node', 'nodejs', 'bun'].includes(executable)) return null;
|
|
115
|
+
|
|
116
|
+
let index = 1;
|
|
117
|
+
const valueFlags = new Set(['-r', '--require', '--import', '--loader', '--experimental-loader', '--conditions']);
|
|
118
|
+
while (index < args.length && String(args[index]).startsWith('-')) {
|
|
119
|
+
const flag = String(args[index]);
|
|
120
|
+
if (flag === '--') { index += 1; break; }
|
|
121
|
+
if (/^-[ep]/.test(flag) || ['--eval', '--print'].some(name => flag === name || flag.startsWith(`${name}=`))) return null;
|
|
122
|
+
index += valueFlags.has(flag) ? 2 : 1;
|
|
123
|
+
}
|
|
124
|
+
const entry = String(args[index] || '').replaceAll('\\', '/').toLowerCase();
|
|
125
|
+
const basename = path.posix.basename(entry);
|
|
126
|
+
if (entry.includes('/@openai/codex/') || ['codex', 'codex.js', 'codex.mjs'].includes(basename)) return 'codex';
|
|
127
|
+
if (entry.includes('/@anthropic-ai/claude-code/') || ['claude', 'claude.js', 'claude.mjs'].includes(basename)) return 'claude';
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
108
131
|
export function findLinuxAncestorCliInfo(startPid, options = {}) {
|
|
109
132
|
const read = typeof options.read === 'function'
|
|
110
133
|
? options.read
|
|
@@ -115,17 +138,22 @@ export function findLinuxAncestorCliInfo(startPid, options = {}) {
|
|
|
115
138
|
});
|
|
116
139
|
let pid = Number(startPid);
|
|
117
140
|
const seen = new Set();
|
|
141
|
+
let fallback = null;
|
|
118
142
|
for (let depth = 0; Number.isInteger(pid) && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
|
|
119
143
|
seen.add(pid);
|
|
120
144
|
const row = read(pid);
|
|
121
|
-
if (!row) return
|
|
145
|
+
if (!row) return fallback;
|
|
122
146
|
const args = Array.isArray(row.args) ? row.args : [];
|
|
123
147
|
const env = row.env && typeof row.env === 'object' && !Array.isArray(row.env) ? row.env : {};
|
|
148
|
+
const executableKind = executableCliKind(args);
|
|
149
|
+
if (executableKind && (!options.kind || options.kind === executableKind)) {
|
|
150
|
+
return { pid, kind: executableKind, args, env };
|
|
151
|
+
}
|
|
124
152
|
const kind = detectCliKindFromProcess(args, env);
|
|
125
|
-
if (kind && (!options.kind || options.kind === kind))
|
|
153
|
+
if (!fallback && kind && (!options.kind || options.kind === kind)) fallback = { pid, kind, args, env };
|
|
126
154
|
pid = Number(row.parent);
|
|
127
155
|
}
|
|
128
|
-
return
|
|
156
|
+
return fallback;
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
export function parseMacAncestorLine(text) {
|
|
@@ -153,21 +181,28 @@ export function findMacAncestorCliInfo(startPid, options = {}) {
|
|
|
153
181
|
const inspect = typeof options.inspect === 'function' ? options.inspect : inspectMacAncestor;
|
|
154
182
|
let pid = Number(startPid);
|
|
155
183
|
const seen = new Set();
|
|
184
|
+
let fallback = null;
|
|
156
185
|
for (let depth = 0; Number.isInteger(pid) && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
|
|
157
186
|
seen.add(pid);
|
|
158
187
|
const parsed = parseMacAncestorLine(inspect(pid));
|
|
159
|
-
if (!parsed) return
|
|
188
|
+
if (!parsed) return fallback;
|
|
160
189
|
// ps provides a rendered command, not lossless argv. Only tokenize enough
|
|
161
190
|
// to recognize the dsh executable/Node script; resume parsing remains off.
|
|
162
191
|
const commandArgs = parsed.command.match(/"(?:\\.|[^"\\])*"|'[^']*'|\S+/g)?.map((arg) =>
|
|
163
192
|
/^("|')/.test(arg) ? arg.slice(1, -1) : arg) || [];
|
|
164
|
-
const
|
|
193
|
+
const executableKind = executableCliKind(commandArgs);
|
|
194
|
+
if (executableKind && (!options.kind || options.kind === executableKind)) {
|
|
195
|
+
return { pid, kind: executableKind, args: [], env: {}, command: parsed.command };
|
|
196
|
+
}
|
|
197
|
+
const kind = detectCliKindFromProcess([parsed.command], {});
|
|
165
198
|
// `ps command` is sufficient for provider-kind detection but is not a
|
|
166
199
|
// lossless argv encoding. Do not feed it to resume-ID parsing.
|
|
167
|
-
if (kind && (!options.kind || options.kind === kind))
|
|
200
|
+
if (!fallback && kind && (!options.kind || options.kind === kind)) {
|
|
201
|
+
fallback = { pid, kind, args: [], env: {}, command: parsed.command };
|
|
202
|
+
}
|
|
168
203
|
pid = parsed.ppid;
|
|
169
204
|
}
|
|
170
|
-
return
|
|
205
|
+
return fallback;
|
|
171
206
|
}
|
|
172
207
|
|
|
173
208
|
export function readAncestorCliInfo(kindHint = null) {
|
package/lib/process/identity.mjs
CHANGED
|
@@ -97,35 +97,52 @@ function parseMacBootToken(text) {
|
|
|
97
97
|
return `${seconds}:${microseconds}`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function
|
|
100
|
+
function parseMacStateStart(text) {
|
|
101
|
+
const match = /^(\S+)\s+([^\r\n]+)$/.exec(String(text ?? '').trim());
|
|
102
|
+
// Darwin ps uses X only as a traced/debugged modifier; Z is its exit state.
|
|
103
|
+
if (!match || !/^[IRSTUZ][+<>AELNSsVWX]*$/.test(match[1])) return null;
|
|
104
|
+
return { state: match[1][0], startToken: match[2].trim() };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function macObservation(pid) {
|
|
108
|
+
const unknown = { state: 'unknown', identity: null };
|
|
101
109
|
let boot = run('sysctl', ['-n', 'kern.boottime']);
|
|
102
110
|
// Launchers often omit /usr/sbin from PATH; macOS still provides sysctl here.
|
|
103
111
|
if (boot.error?.code === 'ENOENT') boot = run('/usr/sbin/sysctl', ['-n', 'kern.boottime']);
|
|
104
|
-
if (boot.error || boot.status !== 0) return
|
|
112
|
+
if (boot.error || boot.status !== 0) return unknown;
|
|
105
113
|
const bootToken = parseMacBootToken(boot.stdout);
|
|
106
|
-
if (!bootToken) return
|
|
114
|
+
if (!bootToken) return unknown;
|
|
107
115
|
|
|
108
116
|
const psEnvironment = { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' };
|
|
109
|
-
|
|
117
|
+
// kill(pid, 0) also succeeds for an unreaped zombie on Darwin. Read state
|
|
118
|
+
// alongside each existing start-time probe, without adding subprocesses.
|
|
119
|
+
const started = run('ps', ['-p', String(pid), '-o', 'stat=', '-o', 'lstart='], psEnvironment);
|
|
120
|
+
const first = parseMacStateStart(started.stdout);
|
|
121
|
+
if (started.error || started.status !== 0 || !first) return unknown;
|
|
122
|
+
if (first.state === 'Z') return { state: 'dead', identity: null };
|
|
110
123
|
const command = run('ps', ['-p', String(pid), '-o', 'command='], psEnvironment);
|
|
111
|
-
const confirmedStarted = run('ps', ['-p', String(pid), '-o', 'lstart='], psEnvironment);
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
124
|
+
const confirmedStarted = run('ps', ['-p', String(pid), '-o', 'stat=', '-o', 'lstart='], psEnvironment);
|
|
125
|
+
const last = parseMacStateStart(confirmedStarted.stdout);
|
|
126
|
+
if (confirmedStarted.error || confirmedStarted.status !== 0 || !last) return unknown;
|
|
127
|
+
if (last.state === 'Z') return { state: 'dead', identity: null };
|
|
128
|
+
if (command.error || command.status !== 0) return unknown;
|
|
115
129
|
|
|
116
130
|
const commandOutput = String(command.stdout ?? '').trim();
|
|
117
131
|
const parsed = parsePsStartIdentity(
|
|
118
|
-
`${
|
|
132
|
+
`${first.startToken}\t${commandOutput}\n`
|
|
119
133
|
);
|
|
120
134
|
const confirmed = parsePsStartIdentity(
|
|
121
|
-
`${
|
|
135
|
+
`${last.startToken}\t${commandOutput}\n`
|
|
122
136
|
);
|
|
123
|
-
if (!parsed || !confirmed || confirmed.startToken !== parsed.startToken) return
|
|
137
|
+
if (!parsed || !confirmed || confirmed.startToken !== parsed.startToken) return unknown;
|
|
124
138
|
|
|
125
139
|
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
state: 'live',
|
|
141
|
+
identity: {
|
|
142
|
+
pid,
|
|
143
|
+
startToken: `${bootToken}:${parsed.startToken}`,
|
|
144
|
+
commandHash: hashCommand(parsed.command)
|
|
145
|
+
}
|
|
129
146
|
};
|
|
130
147
|
}
|
|
131
148
|
|
|
@@ -204,7 +221,11 @@ export function inspectProcessIdentity(value) {
|
|
|
204
221
|
if (observed.state === 'dead') return observed;
|
|
205
222
|
identity = observed.identity;
|
|
206
223
|
}
|
|
207
|
-
else if (process.platform === 'darwin')
|
|
224
|
+
else if (process.platform === 'darwin') {
|
|
225
|
+
const observed = macObservation(pid);
|
|
226
|
+
if (observed.state === 'dead') return observed;
|
|
227
|
+
identity = observed.identity;
|
|
228
|
+
}
|
|
208
229
|
} catch {
|
|
209
230
|
identity = null;
|
|
210
231
|
}
|
package/lib/runtime/client.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { CliError } from '../shared/errors.mjs';
|
|
|
2
2
|
import { readRuntime } from './state.mjs';
|
|
3
3
|
import { runtimeApiUrl, runtimeHttpRequest } from '../web/runtime.mjs';
|
|
4
4
|
import { withRuntimeApiVersionHeader } from '../web/api-version.mjs';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
5
6
|
|
|
6
7
|
const DEFAULT_CLI_NAME = 'hcc';
|
|
7
8
|
export const RUNTIME_SESSION_START_TIMEOUT_MS = 60_000;
|
|
@@ -56,7 +57,9 @@ export async function runtimeRequest(ctx, method, route, body = null, runtime =
|
|
|
56
57
|
let res;
|
|
57
58
|
let text;
|
|
58
59
|
const abortScope = requestAbortScope(opts.signal, opts.timeoutMs);
|
|
60
|
+
const startedAt = performance.now();
|
|
59
61
|
try {
|
|
62
|
+
abortScope.signal.throwIfAborted();
|
|
60
63
|
res = await waitForAbort(runtimeHttpRequest(rt, route, {
|
|
61
64
|
method,
|
|
62
65
|
headers,
|
|
@@ -68,7 +71,12 @@ export async function runtimeRequest(ctx, method, route, body = null, runtime =
|
|
|
68
71
|
} catch (err) {
|
|
69
72
|
throw new CliError('RUNTIME_UNREACHABLE', `Runtime is not reachable at ${rt.base_url}. Start ${cliName} web again.`, {
|
|
70
73
|
runtime: rt.source || rt.base_url,
|
|
71
|
-
message: err.message
|
|
74
|
+
message: err.message,
|
|
75
|
+
method,
|
|
76
|
+
path: url.pathname,
|
|
77
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
78
|
+
timeoutMs: opts.timeoutMs === undefined ? 8000 : opts.timeoutMs,
|
|
79
|
+
errorName: err.name || 'Error'
|
|
72
80
|
});
|
|
73
81
|
} finally {
|
|
74
82
|
abortScope.cleanup();
|