@logicseek/hello-cc 1.1.0-rc.1 → 1.1.0-rc.3
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 +190 -108
- package/README.md +1 -10
- package/README.zh-CN.md +1 -8
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +13 -28
- package/docs/commands.zh-CN.md +11 -20
- package/docs/dsh.md +5 -3
- package/docs/dsh.zh-CN.md +6 -4
- package/docs/macos-process-identity.md +15 -0
- package/docs/native.md +95 -4
- package/docs/native.zh-CN.md +70 -1
- package/docs/web-handoff.zh-CN.md +107 -5
- package/lib/cli/app.mjs +9 -6
- package/lib/cli/commands/coordination.mjs +7 -2
- package/lib/cli/commands/gc.mjs +14 -12
- package/lib/cli/commands/hook.mjs +16 -3
- package/lib/cli/commands/install.mjs +37 -9
- package/lib/cli/commands/lock.mjs +3 -1
- package/lib/cli/commands/misc.mjs +105 -13
- package/lib/cli/commands/native.mjs +3 -44
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/gc-plan.mjs +5 -0
- package/lib/core/coordination/locks.mjs +3 -1
- package/lib/core/peers/evidence.mjs +21 -1
- package/lib/core/peers/tmux-safety.mjs +4 -0
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +67 -21
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +20 -36
- package/lib/integrations/codex-account.mjs +143 -0
- package/lib/integrations/dsh-collaboration.mjs +3 -4
- package/lib/integrations/dsh.mjs +4 -2
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +67 -28
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +7 -1
- package/lib/runtime/client.mjs +18 -1
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +160 -27
- package/lib/runtime/native/store.mjs +51 -21
- package/lib/runtime/project-path.mjs +24 -5
- package/lib/runtime/projects.mjs +3 -0
- package/lib/runtime/state.mjs +61 -26
- package/lib/shared/file-lock.mjs +62 -17
- package/lib/shared/redact.mjs +2 -1
- package/lib/shared/socket-lock-worker.mjs +15 -6
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +104 -33
- package/lib/web/codex-sessions.mjs +60 -11
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +22 -4
- package/lib/web/http-routes.mjs +236 -23
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +322 -13
- package/lib/web/project-contexts.mjs +235 -42
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +134 -82
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +76 -27
- package/lib/web/tmux-client-snapshot.mjs +85 -0
- package/lib/web/tmux-sessions.mjs +71 -7
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +81 -73
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +6 -4
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -49
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +27 -1967
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +204 -6
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +637 -186
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +22 -7
- package/scripts/web-mcp-form-live-acceptance.mjs +1 -1
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +152 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
assertSelectedCwdSnapshot,
|
|
4
|
+
captureSelectedCwdSnapshot,
|
|
5
|
+
sameSelectedCwdIdentity
|
|
6
|
+
} from '../../process/selected-cwd-identity.mjs';
|
|
7
|
+
import { CliError } from '../../shared/errors.mjs';
|
|
8
|
+
|
|
9
|
+
export const HOOK_ROOT_IDENTITY_ENV = 'HCC_HOOK_ROOT_IDENTITY';
|
|
10
|
+
|
|
11
|
+
function mismatch() {
|
|
12
|
+
return new CliError('HOOK_ROOT_IDENTITY_MISMATCH',
|
|
13
|
+
'Hook project root no longer matches the provider launch identity');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Carry the launch-time directory identity into short-lived hook children.
|
|
17
|
+
// A pathname alone cannot distinguish a moved project from another directory
|
|
18
|
+
// subsequently installed at the same pathname.
|
|
19
|
+
export function hookRootIdentityValue(root, original = null) {
|
|
20
|
+
const selected = captureSelectedCwdSnapshot(root);
|
|
21
|
+
if (original) {
|
|
22
|
+
if (typeof original.assertUnchanged === 'function') original.assertUnchanged();
|
|
23
|
+
else assertSelectedCwdSnapshot(original);
|
|
24
|
+
if (!sameSelectedCwdIdentity(selected, original)) throw mismatch();
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify({ version: 1, ...selected });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertHookRootIdentity(root, value) {
|
|
30
|
+
if (value === undefined) return false; // Unmanaged and pre-upgrade hooks have no launch marker.
|
|
31
|
+
try {
|
|
32
|
+
if (typeof value !== 'string' || value.length > 4096) throw mismatch();
|
|
33
|
+
const snapshot = JSON.parse(value);
|
|
34
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot) ||
|
|
35
|
+
snapshot.version !== 1 || snapshot.requested !== path.resolve(root) ||
|
|
36
|
+
typeof snapshot.canonical !== 'string' || !path.isAbsolute(snapshot.canonical) ||
|
|
37
|
+
!snapshot.identity || typeof snapshot.identity !== 'object' ||
|
|
38
|
+
!/^\d+$/.test(snapshot.identity.dev) || !/^\d+$/.test(snapshot.identity.ino) ||
|
|
39
|
+
(snapshot.identity.birthtimeNs !== null && !/^\d+$/.test(snapshot.identity.birthtimeNs))) {
|
|
40
|
+
throw mismatch();
|
|
41
|
+
}
|
|
42
|
+
assertSelectedCwdSnapshot(snapshot);
|
|
43
|
+
return snapshot;
|
|
44
|
+
} catch {
|
|
45
|
+
throw mismatch();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { shortHash } from '../peers/format.mjs';
|
|
5
|
+
import { HOOK_ROOT_IDENTITY_ENV, hookRootIdentityValue } from './hook-root-identity.mjs';
|
|
5
6
|
|
|
6
7
|
export const WEB_CHILD_ENV = 'HCC_WEB_CHILD';
|
|
7
8
|
export const LAUNCH_FINGERPRINT_ENV = 'HCC_LAUNCH_FINGERPRINT';
|
|
@@ -11,6 +12,7 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
|
|
|
11
12
|
'_',
|
|
12
13
|
'COLUMNS',
|
|
13
14
|
'HCC_DB',
|
|
15
|
+
HOOK_ROOT_IDENTITY_ENV,
|
|
14
16
|
'HCC_NO_AUTO_INSTALL_TMUX',
|
|
15
17
|
'HCC_PEER',
|
|
16
18
|
'HCC_ROOT',
|
|
@@ -41,8 +43,12 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
|
|
|
41
43
|
'SSH_AUTH_SOCK'
|
|
42
44
|
]);
|
|
43
45
|
|
|
44
|
-
export function childSessionEnv(extra = {}, baseEnv = process.env) {
|
|
46
|
+
export function childSessionEnv(extra = {}, baseEnv = process.env, { rootIdentity = null } = {}) {
|
|
45
47
|
const env = { ...(baseEnv || {}), ...extra };
|
|
48
|
+
if (Object.hasOwn(extra, 'HCC_ROOT')) {
|
|
49
|
+
if (extra.HCC_ROOT) env[HOOK_ROOT_IDENTITY_ENV] = hookRootIdentityValue(extra.HCC_ROOT, rootIdentity);
|
|
50
|
+
else delete env[HOOK_ROOT_IDENTITY_ENV];
|
|
51
|
+
}
|
|
46
52
|
delete env[WEB_CHILD_ENV];
|
|
47
53
|
delete env.HCC_RUNTIME_LOCAL_ONLY;
|
|
48
54
|
return env;
|
package/lib/db/connection.mjs
CHANGED
|
@@ -10,7 +10,8 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
10
10
|
import { CliError } from '../shared/errors.mjs';
|
|
11
11
|
import {
|
|
12
12
|
DB_SCHEMA_VERSION,
|
|
13
|
-
execWithBusyRetry
|
|
13
|
+
execWithBusyRetry,
|
|
14
|
+
readSchemaVersion
|
|
14
15
|
} from './schema.mjs';
|
|
15
16
|
import { initSchemaWithBackup } from './migration-backup.mjs';
|
|
16
17
|
import { readProjectRegistry } from '../runtime/projects.mjs';
|
|
@@ -18,6 +19,7 @@ import { resolveProjectDatabase, secureExistingProjectStateDirectory } from '../
|
|
|
18
19
|
import { projectStateDir, legacyProjectStateDir } from '../runtime/paths.mjs';
|
|
19
20
|
import { privateProjectStateDir, ensurePrivateProjectStateDir } from '../runtime/private-state.mjs';
|
|
20
21
|
import { unsafeDirectoryAcl } from '../runtime/project-trust.mjs';
|
|
22
|
+
import { assertSelectedCwdSnapshot } from '../process/selected-cwd-identity.mjs';
|
|
21
23
|
|
|
22
24
|
const MIGRATION_FAILURE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
23
25
|
|
|
@@ -28,16 +30,40 @@ function managedDatabase(ctx) {
|
|
|
28
30
|
try { canonicalRoot = fs.realpathSync.native(requestedRoot); } catch {}
|
|
29
31
|
const requestedDb = path.resolve(ctx.dbPath);
|
|
30
32
|
let canonicalCandidate = requestedDb;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
let parent = path.dirname(requestedDb);
|
|
34
|
+
const missingParents = [];
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
canonicalCandidate = path.join(fs.realpathSync.native(parent),
|
|
38
|
+
...missingParents, path.basename(requestedDb));
|
|
39
|
+
break;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code !== 'ENOENT' || path.dirname(parent) === parent) break;
|
|
42
|
+
missingParents.unshift(path.basename(parent));
|
|
43
|
+
parent = path.dirname(parent);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const candidates = new Set([requestedDb, canonicalCandidate]);
|
|
47
|
+
try { candidates.add(fs.realpathSync.native(requestedDb)); } catch {}
|
|
48
|
+
const within = (parent, candidate) => {
|
|
49
|
+
const relative = path.relative(parent, candidate);
|
|
50
|
+
return relative !== '' && relative !== '..' &&
|
|
51
|
+
!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
52
|
+
};
|
|
53
|
+
const managed = [requestedRoot, canonicalRoot].some(root =>
|
|
35
54
|
[legacyProjectStateDir(root), privateProjectStateDir(root)].some(stateDir =>
|
|
36
|
-
[
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
[...candidates].some(db => within(stateDir, db))));
|
|
56
|
+
if (managed) return true;
|
|
57
|
+
// A context may have captured A's local or private .hello-cc DB before its
|
|
58
|
+
// root alias was repointed to B. A reserved state path is never an explicit
|
|
59
|
+
// external DB just because it no longer belongs to the current root.
|
|
60
|
+
const reservedStatePath = db => path.dirname(db).split(path.sep).some(segment =>
|
|
61
|
+
segment.toLowerCase() === '.hello-cc');
|
|
62
|
+
if ([...candidates].some(reservedStatePath)) {
|
|
63
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN',
|
|
64
|
+
`Private database path is not bound to the current project root: ${requestedDb}`);
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
function secureOwnedSqliteFile(file, { create = false, readOnly = false, optional = false } = {}) {
|
|
@@ -115,6 +141,11 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
115
141
|
const migratedRegisteredProjectDbs = new Set();
|
|
116
142
|
|
|
117
143
|
function connect(ctx, options = {}) {
|
|
144
|
+
const existingSchema = options.existingSchema === true;
|
|
145
|
+
if (existingSchema && options.create !== false) {
|
|
146
|
+
throw new TypeError('existingSchema requires create: false');
|
|
147
|
+
}
|
|
148
|
+
if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
|
|
118
149
|
let dbPath = ctx.dbPath;
|
|
119
150
|
if (managedDatabase(ctx)) {
|
|
120
151
|
const rootWasMissing = options.create !== false && !fs.existsSync(ctx.root);
|
|
@@ -144,20 +175,35 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
144
175
|
}
|
|
145
176
|
if (options.create !== false) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
146
177
|
}
|
|
147
|
-
const db = new DatabaseSync(dbPath, { timeout: 5000 });
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
178
|
+
const db = new DatabaseSync(dbPath, { timeout: existingSchema ? 0 : 5000 });
|
|
179
|
+
try {
|
|
180
|
+
if (existingSchema) {
|
|
181
|
+
// Shutdown may abandon advisory detachment when another writer owns
|
|
182
|
+
// the DB. Never migrate, change journal mode, or retry while exiting.
|
|
183
|
+
db.exec('PRAGMA busy_timeout = 0; PRAGMA foreign_keys = ON;');
|
|
184
|
+
if (readSchemaVersion(db) !== DB_SCHEMA_VERSION) {
|
|
185
|
+
throw new CliError('DB_SCHEMA_NOT_CURRENT', 'Shutdown requires an existing current database schema');
|
|
186
|
+
}
|
|
187
|
+
return db;
|
|
188
|
+
}
|
|
189
|
+
db.exec('PRAGMA busy_timeout = 5000;');
|
|
190
|
+
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL;', { ignoreBusy: true });
|
|
191
|
+
db.exec('PRAGMA synchronous = NORMAL;');
|
|
192
|
+
db.exec('PRAGMA wal_autocheckpoint = 1000;');
|
|
193
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
194
|
+
initSchemaWithBackup(db, dbPath, {
|
|
195
|
+
beforePostMigrationIndexes: dedupePeerBindings
|
|
196
|
+
});
|
|
197
|
+
if (options.migrateRegistered !== false) migrateRegisteredProjectDbs(ctx);
|
|
198
|
+
return db;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
try { db.close(); } catch {}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
158
203
|
}
|
|
159
204
|
|
|
160
205
|
function connectReadOnly(ctx) {
|
|
206
|
+
if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
|
|
161
207
|
const managed = managedDatabase(ctx);
|
|
162
208
|
const dbPath = managed
|
|
163
209
|
? resolveProjectDatabase({ root: ctx.root, db: ctx.dbPath, createStateDir: false }).db
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CODEX_INTERACTION_METHODS } from '../integrations/codex-interactions.mjs';
|
|
2
|
+
|
|
3
|
+
// A bounded upgrade gate, not a JSON Schema validator or a runtime permission
|
|
4
|
+
// check. Generated types remain the authoritative version-specific artifacts.
|
|
5
|
+
export const CODEX_CLIENT_METHODS = Object.freeze([
|
|
6
|
+
'initialize', 'thread/start', 'thread/resume', 'thread/read', 'thread/list',
|
|
7
|
+
'thread/fork', 'turn/start', 'turn/steer', 'turn/interrupt',
|
|
8
|
+
'mcpServerStatus/list', 'mcpServer/tool/call', 'account/read', 'account/rateLimits/read'
|
|
9
|
+
]);
|
|
10
|
+
export const CODEX_NOTIFICATION_METHODS = Object.freeze([
|
|
11
|
+
'thread/started', 'thread/status/changed', 'turn/started', 'turn/completed',
|
|
12
|
+
'turn/plan/updated', 'turn/diff/updated', 'item/started', 'item/completed',
|
|
13
|
+
'item/agentMessage/delta', 'serverRequest/resolved', 'error',
|
|
14
|
+
'account/updated', 'account/login/completed', 'account/rateLimits/updated'
|
|
15
|
+
]);
|
|
16
|
+
const ACCOUNT_SCHEMA_FILES = ['GetAccountParams', 'GetAccountResponse', 'GetAccountRateLimitsResponse',
|
|
17
|
+
'AccountUpdatedNotification', 'AccountLoginCompletedNotification', 'AccountRateLimitsUpdatedNotification'];
|
|
18
|
+
export const CODEX_PROTOCOL_SCHEMA_FILES = Object.freeze([
|
|
19
|
+
'ClientRequest', 'ClientNotification', 'ServerRequest', 'ServerNotification',
|
|
20
|
+
'CommandExecutionRequestApprovalResponse', 'FileChangeRequestApprovalResponse',
|
|
21
|
+
'PermissionsRequestApprovalResponse', 'ToolRequestUserInputResponse',
|
|
22
|
+
'McpServerElicitationRequestParams', 'McpServerElicitationRequestResponse',
|
|
23
|
+
...ACCOUNT_SCHEMA_FILES
|
|
24
|
+
]);
|
|
25
|
+
// Official account schemas are generated only in the versioned v2 directory.
|
|
26
|
+
export const CODEX_PROTOCOL_SCHEMA_PATHS = Object.freeze(Object.fromEntries(CODEX_PROTOCOL_SCHEMA_FILES
|
|
27
|
+
.map(name => [name, (ACCOUNT_SCHEMA_FILES.includes(name) ? 'v2/' : '') + name + '.json'])));
|
|
28
|
+
|
|
29
|
+
function methods(schema) {
|
|
30
|
+
return (schema?.oneOf || []).flatMap(row => row.properties?.method?.enum || []);
|
|
31
|
+
}
|
|
32
|
+
function enumValues(schema, document, seen = new Set()) {
|
|
33
|
+
if (!schema || typeof schema !== 'object') return [];
|
|
34
|
+
if (schema.$ref) {
|
|
35
|
+
const ref = schema.$ref;
|
|
36
|
+
if (seen.has(ref) || !ref.startsWith('#/definitions/')) return [];
|
|
37
|
+
const next = new Set(seen); next.add(ref);
|
|
38
|
+
return enumValues(document.definitions?.[ref.slice('#/definitions/'.length)], document, next);
|
|
39
|
+
}
|
|
40
|
+
return [...(schema.enum || []), ...(Object.hasOwn(schema, 'const') ? [schema.const] : []),
|
|
41
|
+
...['oneOf', 'anyOf', 'allOf'].flatMap(key => (schema[key] || []).flatMap(row => enumValues(row, document, seen)))];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function inspectCodexProtocol(schemas) {
|
|
45
|
+
const checks = [];
|
|
46
|
+
const check = (contract, ok) => checks.push({ contract, status: ok ? 'pass' : 'fail' });
|
|
47
|
+
for (const [file, expected] of [
|
|
48
|
+
['ClientRequest', CODEX_CLIENT_METHODS], ['ClientNotification', ['initialized']],
|
|
49
|
+
['ServerRequest', [...CODEX_INTERACTION_METHODS]], ['ServerNotification', CODEX_NOTIFICATION_METHODS]
|
|
50
|
+
]) {
|
|
51
|
+
const available = methods(schemas[file]);
|
|
52
|
+
for (const method of expected) check(`${file}: ${method}`, available.includes(method));
|
|
53
|
+
}
|
|
54
|
+
for (const [file, fields] of [
|
|
55
|
+
['CommandExecutionRequestApprovalResponse', ['decision']],
|
|
56
|
+
['FileChangeRequestApprovalResponse', ['decision']],
|
|
57
|
+
['PermissionsRequestApprovalResponse', ['permissions', 'scope']],
|
|
58
|
+
['ToolRequestUserInputResponse', ['answers']],
|
|
59
|
+
['McpServerElicitationRequestResponse', ['action', 'content']]
|
|
60
|
+
]) {
|
|
61
|
+
const schema = schemas[file];
|
|
62
|
+
check(`${file}: response fields`, fields.every(field => Object.hasOwn(schema?.properties || {}, field)));
|
|
63
|
+
// HCC never supplies unknown required response fields. Optional extensions
|
|
64
|
+
// are compatible with this gate; new required fields demand adapter review.
|
|
65
|
+
check(`${file}: no new required response fields`, Boolean(schema) && (schema.required || []).every(field => fields.includes(field)));
|
|
66
|
+
}
|
|
67
|
+
for (const [file, field, expected] of [
|
|
68
|
+
['CommandExecutionRequestApprovalResponse', 'decision', ['accept', 'decline', 'cancel']],
|
|
69
|
+
['FileChangeRequestApprovalResponse', 'decision', ['accept', 'decline', 'cancel']],
|
|
70
|
+
['PermissionsRequestApprovalResponse', 'scope', ['turn', 'session']],
|
|
71
|
+
['McpServerElicitationRequestResponse', 'action', ['accept', 'decline', 'cancel']]
|
|
72
|
+
]) {
|
|
73
|
+
const values = enumValues(schemas[file]?.properties?.[field], schemas[file] || {});
|
|
74
|
+
check(`${file}: ${field} choices`, expected.every(value => values.includes(value)));
|
|
75
|
+
}
|
|
76
|
+
const elicitation = schemas.McpServerElicitationRequestParams;
|
|
77
|
+
check('MCP elicitation: thread and server identity',
|
|
78
|
+
['threadId', 'serverName'].every(field => elicitation?.required?.includes(field)) &&
|
|
79
|
+
Object.hasOwn(elicitation?.properties || {}, 'turnId'));
|
|
80
|
+
for (const [mode, fields] of [['form', ['message', 'mode', 'requestedSchema']], ['url', ['message', 'mode', 'elicitationId', 'url']]]) {
|
|
81
|
+
check(`MCP elicitation: ${mode} identity and payload`, Boolean(elicitation?.oneOf?.some(row =>
|
|
82
|
+
row.properties?.mode?.enum?.includes(mode) && fields.every(field => row.required?.includes(field) && Object.hasOwn(row.properties || {}, field)))));
|
|
83
|
+
}
|
|
84
|
+
check('Account read: explicit nonrefreshing option', schemas.GetAccountParams?.properties?.refreshToken?.type === 'boolean' &&
|
|
85
|
+
(schemas.GetAccountParams?.required || []).every(field => field === 'refreshToken'));
|
|
86
|
+
check('Account read: auth requirement is explicit', schemas.GetAccountResponse?.required?.includes('requiresOpenaiAuth') &&
|
|
87
|
+
schemas.GetAccountResponse?.properties?.requiresOpenaiAuth?.type === 'boolean');
|
|
88
|
+
check('Account read: recognized account types', ['apiKey', 'chatgpt', 'amazonBedrock'].every(type =>
|
|
89
|
+
schemas.GetAccountResponse?.definitions?.Account?.oneOf?.some(row => row.properties?.type?.enum?.includes(type))));
|
|
90
|
+
check('Account limits: historical and multi-bucket fields', ['rateLimits', 'rateLimitsByLimitId'].every(field =>
|
|
91
|
+
Object.hasOwn(schemas.GetAccountRateLimitsResponse?.properties || {}, field)));
|
|
92
|
+
check('Account limits: window percentage', schemas.GetAccountRateLimitsResponse?.definitions?.RateLimitWindow?.required?.includes('usedPercent') &&
|
|
93
|
+
['integer', 'number'].includes(schemas.GetAccountRateLimitsResponse?.definitions?.RateLimitWindow?.properties?.usedPercent?.type));
|
|
94
|
+
check('Account notifications: status and quota fields', Object.hasOwn(schemas.AccountUpdatedNotification?.properties || {}, 'authMode') &&
|
|
95
|
+
Object.hasOwn(schemas.AccountUpdatedNotification?.properties || {}, 'planType') && schemas.AccountRateLimitsUpdatedNotification?.required?.includes('rateLimits'));
|
|
96
|
+
check('Account notification: correlated login completion', schemas.AccountLoginCompletedNotification?.required?.includes('success') &&
|
|
97
|
+
Object.hasOwn(schemas.AccountLoginCompletedNotification?.properties || {}, 'loginId'));
|
|
98
|
+
const unsupportedServerRequests = methods(schemas.ServerRequest).filter(method => !CODEX_INTERACTION_METHODS.has(method)).sort();
|
|
99
|
+
return {
|
|
100
|
+
status: checks.every(entry => entry.status === 'pass') ? 'compatible' : 'incompatible',
|
|
101
|
+
checks, unsupportedServerRequests,
|
|
102
|
+
scope: 'Method presence, response fields and choices, MCP form/URL identity, and read-only account/quota fields; not full schema or runtime validation.',
|
|
103
|
+
inferenceCalled: false, accountAccessed: false
|
|
104
|
+
};
|
|
105
|
+
}
|
package/lib/guidance.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { resolveProjectDatabase } from './runtime/project-path.mjs';
|
|
2
|
+
import { writeProjectGuidanceFiles, removeProjectGuidanceFiles } from './process/pinned-guidance.mjs';
|
|
3
|
+
import { writePinnedProjectGuidance } from './process/pinned-project-state.mjs';
|
|
4
|
+
import { captureSelectedCwdIdentity } from './process/selected-cwd-identity.mjs';
|
|
4
5
|
|
|
5
6
|
export function guidanceMarkdown() {
|
|
6
7
|
return `# hello-cc Coordination Rules
|
|
@@ -84,41 +85,24 @@ Before stopping:
|
|
|
84
85
|
`;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
export function writeGuidance(root) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: `${existing.trimEnd()}\n${clause}`;
|
|
103
|
-
fs.writeFileSync(target, updated);
|
|
88
|
+
export function writeGuidance(root, options = {}) {
|
|
89
|
+
const selectedRoot = captureSelectedCwdIdentity(root, options.expectedSnapshot);
|
|
90
|
+
let selectedState;
|
|
91
|
+
try {
|
|
92
|
+
const stateDir = resolveProjectDatabase({ root, createStateDir: true }).stateDir;
|
|
93
|
+
selectedState = captureSelectedCwdIdentity(stateDir);
|
|
94
|
+
selectedRoot.assertUnchanged();
|
|
95
|
+
const content = guidanceMarkdown();
|
|
96
|
+
writeProjectGuidanceFiles(root, content, { ...options, expectedBinding: selectedRoot });
|
|
97
|
+
selectedRoot.assertUnchanged();
|
|
98
|
+
return writePinnedProjectGuidance(stateDir, content, selectedState,
|
|
99
|
+
{ spawnProcess: options.stateSpawnProcess });
|
|
100
|
+
} finally {
|
|
101
|
+
selectedState?.release();
|
|
102
|
+
selectedRoot.release();
|
|
104
103
|
}
|
|
105
|
-
return guidePath;
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
export function removeGuidanceBlocks(root) {
|
|
109
|
-
|
|
110
|
-
for (const file of ['CLAUDE.md', 'AGENTS.md']) {
|
|
111
|
-
const target = path.join(root, file);
|
|
112
|
-
if (!fs.existsSync(target)) continue;
|
|
113
|
-
const existing = fs.readFileSync(target, 'utf8');
|
|
114
|
-
const updated = existing
|
|
115
|
-
.replace(/\n?<!-- hello-cc:start -->[\s\S]*?<!-- hello-cc:end -->\n?/g, '\n')
|
|
116
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
117
|
-
.trimEnd();
|
|
118
|
-
if (updated !== existing) {
|
|
119
|
-
fs.writeFileSync(target, updated ? `${updated}\n` : '');
|
|
120
|
-
changed.push(target);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return changed;
|
|
106
|
+
export function removeGuidanceBlocks(root, options = {}) {
|
|
107
|
+
return removeProjectGuidanceFiles(root, options);
|
|
124
108
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Read-only projection of official App Server account and quota data. Never
|
|
2
|
+
// forward emails, credential identifiers, tokens, raw errors or unknown fields.
|
|
3
|
+
const PLANS = new Set(['free', 'go', 'plus', 'pro', 'prolite', 'team',
|
|
4
|
+
'self_serve_business_usage_based', 'business', 'enterprise_cbp_usage_based', 'enterprise', 'edu', 'unknown']);
|
|
5
|
+
const TYPES = new Set(['apiKey', 'chatgpt', 'amazonBedrock']);
|
|
6
|
+
const REASONS = new Set(['rate_limit_reached', 'workspace_owner_credits_depleted', 'workspace_member_credits_depleted',
|
|
7
|
+
'workspace_owner_usage_limit_reached', 'workspace_member_usage_limit_reached']);
|
|
8
|
+
const record = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
10
|
+
const integer = value => Number.isSafeInteger(value) && value >= 0;
|
|
11
|
+
const bucketId = value => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,80}$/.test(value) ? value : null;
|
|
12
|
+
|
|
13
|
+
function windowValue(value) {
|
|
14
|
+
if (!record(value) || !finite(value.usedPercent)) return null;
|
|
15
|
+
return { usedPercent: value.usedPercent,
|
|
16
|
+
resetsAt: integer(value.resetsAt) ? value.resetsAt : null,
|
|
17
|
+
windowDurationMins: integer(value.windowDurationMins) && value.windowDurationMins > 0 ? value.windowDurationMins : null };
|
|
18
|
+
}
|
|
19
|
+
function bucketValue(value, id = null) {
|
|
20
|
+
if (!record(value)) return null;
|
|
21
|
+
const primary = windowValue(value.primary), secondary = windowValue(value.secondary);
|
|
22
|
+
const credits = record(value.credits) && typeof value.credits.hasCredits === 'boolean' && typeof value.credits.unlimited === 'boolean'
|
|
23
|
+
? { hasCredits: value.credits.hasCredits, unlimited: value.credits.unlimited } : null;
|
|
24
|
+
if (!primary && !secondary && !credits && !REASONS.has(value.rateLimitReachedType)) return null;
|
|
25
|
+
return { id: bucketId(id) || bucketId(value.limitId), primary, secondary, credits,
|
|
26
|
+
planType: PLANS.has(value.planType) ? value.planType : null,
|
|
27
|
+
reached: REASONS.has(value.rateLimitReachedType) ? value.rateLimitReachedType : null };
|
|
28
|
+
}
|
|
29
|
+
function unavailableReason(error) {
|
|
30
|
+
return error?.code === -32601 || (error?.extra?.rpcCode ?? error?.extra?.rpc_code) === -32601 ? 'unsupported' : 'unavailable';
|
|
31
|
+
}
|
|
32
|
+
function accountValue(value) {
|
|
33
|
+
if (!record(value) || typeof value.requiresOpenaiAuth !== 'boolean' ||
|
|
34
|
+
(value.account != null && (!record(value.account) || !TYPES.has(value.account.type)))) return null;
|
|
35
|
+
const type = value.account?.type || null;
|
|
36
|
+
return { requiresOpenaiAuth: value.requiresOpenaiAuth, type,
|
|
37
|
+
planType: type === 'chatgpt' && PLANS.has(value.account.planType) ? value.account.planType : null,
|
|
38
|
+
authentication: value.requiresOpenaiAuth === false ? 'providerManaged' : type ? 'authenticated' : 'required' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createCodexAccountState({ request, onChange = () => {}, now = Date.now } = {}) {
|
|
42
|
+
let state = { status: 'unknown', stale: false, reason: null, type: null, planType: null,
|
|
43
|
+
authentication: 'unknown', requiresOpenaiAuth: null, checkedAt: null,
|
|
44
|
+
rateLimits: { status: 'unknown', stale: false, reason: null, checkedAt: null, buckets: [] } };
|
|
45
|
+
let accountRevision = 0, limitsRevision = 0, inFlight = null, closed = false, recheck = false;
|
|
46
|
+
const snapshot = () => structuredClone(state);
|
|
47
|
+
const publish = () => { try { onChange(snapshot()); } catch {} };
|
|
48
|
+
function relevantLimits() { return state.status === 'ready' && state.type === 'chatgpt' && state.requiresOpenaiAuth === true; }
|
|
49
|
+
function notification(method, params = {}) {
|
|
50
|
+
if (!['account/updated', 'account/rateLimits/updated', 'account/login/completed'].includes(method)) return false;
|
|
51
|
+
if (closed) return true;
|
|
52
|
+
if (method !== 'account/rateLimits/updated') {
|
|
53
|
+
// Account changes can precede a read reply. That older response cannot
|
|
54
|
+
// reattach the previous account's quotas to the current executor.
|
|
55
|
+
accountRevision++; limitsRevision++; recheck = true;
|
|
56
|
+
state = { ...state, status: 'unknown', stale: true, reason: null, type: null, planType: null,
|
|
57
|
+
authentication: 'unknown', requiresOpenaiAuth: null,
|
|
58
|
+
rateLimits: { status: 'unknown', stale: false, reason: null, checkedAt: null, buckets: [] } };
|
|
59
|
+
publish();
|
|
60
|
+
queueMicrotask(() => { if (!closed && !inFlight) void refresh(); });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (!relevantLimits()) return true;
|
|
64
|
+
const next = bucketValue(params.rateLimits);
|
|
65
|
+
if (!next) return true;
|
|
66
|
+
limitsRevision++;
|
|
67
|
+
const buckets = state.rateLimits.buckets.slice();
|
|
68
|
+
if (next.id === null && buckets.length === 1) next.id = buckets[0].id;
|
|
69
|
+
if (next.id === null && buckets.length > 1 && !buckets.some(entry => entry.id === null)) return true;
|
|
70
|
+
const index = buckets.findIndex(entry => entry.id === next.id);
|
|
71
|
+
if (index < 0) { if (buckets.length < 16) buckets.push(next); }
|
|
72
|
+
else {
|
|
73
|
+
const previous = buckets[index];
|
|
74
|
+
// Rolling updates are sparse. Null metadata/windows mean unavailable in
|
|
75
|
+
// this update, not a quota of zero or a request to erase the prior value.
|
|
76
|
+
buckets[index] = Object.fromEntries(Object.keys(next).map(key => [key,
|
|
77
|
+
['primary', 'secondary'].includes(key) && next[key] && previous[key]
|
|
78
|
+
? Object.fromEntries(Object.keys(next[key]).map(field => [field, next[key][field] ?? previous[key][field]]))
|
|
79
|
+
: next[key] ?? previous[key]]));
|
|
80
|
+
}
|
|
81
|
+
state.rateLimits = { status: 'ready', stale: false, reason: null, checkedAt: now(), buckets };
|
|
82
|
+
publish(); return true;
|
|
83
|
+
}
|
|
84
|
+
async function read() {
|
|
85
|
+
const revision = accountRevision;
|
|
86
|
+
recheck = false;
|
|
87
|
+
let value;
|
|
88
|
+
try { value = accountValue(await request('account/read', { refreshToken: false }));
|
|
89
|
+
if (!value) throw Object.assign(new Error('Invalid account response'), { code: 'CODEX_ACCOUNT_PROTOCOL_ERROR' });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!closed && revision === accountRevision) {
|
|
92
|
+
state.status = 'unavailable'; state.stale = state.checkedAt !== null; state.reason = unavailableReason(error);
|
|
93
|
+
state.authentication = 'unknown';
|
|
94
|
+
state.rateLimits = { ...state.rateLimits, status: 'unavailable', stale: state.rateLimits.checkedAt !== null, reason: 'unavailable' };
|
|
95
|
+
publish();
|
|
96
|
+
}
|
|
97
|
+
return snapshot();
|
|
98
|
+
}
|
|
99
|
+
if (closed || revision !== accountRevision) return snapshot();
|
|
100
|
+
state = { ...state, ...value, status: 'ready', stale: false, reason: null, checkedAt: now() };
|
|
101
|
+
if (!relevantLimits()) {
|
|
102
|
+
state.rateLimits = { status: 'notApplicable', stale: false, reason: null, checkedAt: null, buckets: [] };
|
|
103
|
+
publish(); return snapshot();
|
|
104
|
+
}
|
|
105
|
+
publish();
|
|
106
|
+
const quotaRevision = limitsRevision;
|
|
107
|
+
try {
|
|
108
|
+
const response = await request('account/rateLimits/read', {});
|
|
109
|
+
if (!record(response) || !record(response.rateLimits)) throw new Error('Invalid quota response');
|
|
110
|
+
const entries = record(response.rateLimitsByLimitId) ? Object.entries(response.rateLimitsByLimitId)
|
|
111
|
+
.filter(([id]) => bucketId(id)).slice(0, 16).map(([id, bucket]) => bucketValue(bucket, id)).filter(Boolean) : [];
|
|
112
|
+
const legacy = bucketValue(response.rateLimits);
|
|
113
|
+
const buckets = entries.length ? entries : legacy ? [legacy] : [];
|
|
114
|
+
if (!closed && revision === accountRevision && quotaRevision === limitsRevision) {
|
|
115
|
+
state.rateLimits = { status: buckets.length ? 'ready' : 'unavailable', stale: false,
|
|
116
|
+
reason: buckets.length ? null : 'unavailable', checkedAt: buckets.length ? now() : null, buckets };
|
|
117
|
+
publish();
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!closed && revision === accountRevision && quotaRevision === limitsRevision) {
|
|
121
|
+
state.rateLimits = { ...state.rateLimits, status: 'unavailable', stale: state.rateLimits.checkedAt !== null, reason: unavailableReason(error) };
|
|
122
|
+
publish();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return snapshot();
|
|
126
|
+
}
|
|
127
|
+
function refresh() {
|
|
128
|
+
if (closed) return Promise.resolve(snapshot());
|
|
129
|
+
if (inFlight) return inFlight;
|
|
130
|
+
inFlight = read().finally(() => {
|
|
131
|
+
inFlight = null;
|
|
132
|
+
if (recheck && !closed) queueMicrotask(() => { if (!inFlight && !closed) void refresh(); });
|
|
133
|
+
});
|
|
134
|
+
return inFlight;
|
|
135
|
+
}
|
|
136
|
+
function close() {
|
|
137
|
+
closed = true; accountRevision++; limitsRevision++;
|
|
138
|
+
state.status = 'unavailable'; state.stale = state.checkedAt !== null; state.reason = 'disconnected';
|
|
139
|
+
state.authentication = 'unknown';
|
|
140
|
+
state.rateLimits = { ...state.rateLimits, status: 'unavailable', stale: state.rateLimits.checkedAt !== null, reason: 'disconnected' };
|
|
141
|
+
}
|
|
142
|
+
return { snapshot, refresh, notification, close };
|
|
143
|
+
}
|
|
@@ -16,7 +16,7 @@ import { createCoordinationState } from '../coordination-state.mjs';
|
|
|
16
16
|
import { createScopedMcpTools } from '../mcp/tools.mjs';
|
|
17
17
|
import { resolveProjectDatabase } from '../runtime/project-path.mjs';
|
|
18
18
|
import { registerProjectActivity } from '../runtime/projects.mjs';
|
|
19
|
-
import { inspectProcessIdentity } from '../process/identity.mjs';
|
|
19
|
+
import { compareProcessIdentity, inspectProcessIdentity } from '../process/identity.mjs';
|
|
20
20
|
|
|
21
21
|
const fail = (message) => { throw new CliError('DSH_COLLABORATION_CONFLICT', message); };
|
|
22
22
|
|
|
@@ -29,9 +29,8 @@ function verifiedDeadOwner(peer) {
|
|
|
29
29
|
// A recycled PID has a different start token. A changed command alone does
|
|
30
30
|
// not prove the original process exited (a live process may change its title).
|
|
31
31
|
return observed.state === 'live' && observed.identity?.pid === peer.pid &&
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
observed.identity.startToken !== peer.pid_start_token;
|
|
32
|
+
compareProcessIdentity({ pid: peer.pid, startToken: peer.pid_start_token,
|
|
33
|
+
commandHash: peer.pid_command_hash }, observed.identity) === 'dead';
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
// One in-process authority per Agent. All mutations still go through the same
|
package/lib/integrations/dsh.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
-
import {
|
|
5
|
+
import { spawnPinnedCwdProcess } from '../process/pinned-cwd.mjs';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { CliError } from '../shared/errors.mjs';
|
|
8
8
|
import { withFileLock } from '../shared/file-lock.mjs';
|
|
@@ -271,7 +271,9 @@ export async function launchDshWeb(ctx, options = {}) {
|
|
|
271
271
|
const env = createDshEnvironment(options.env || process.env, { dshHome: options.dshHome, cwd: ctx.cwd });
|
|
272
272
|
const args = ['web', '--patch', setup.patchPath, ...(options.args || [])];
|
|
273
273
|
return new Promise((resolve, reject) => {
|
|
274
|
-
const child =
|
|
274
|
+
const child = spawnPinnedCwdProcess(binary, args, {
|
|
275
|
+
cwd: setup.root, env, stdio: options.stdio || 'inherit'
|
|
276
|
+
});
|
|
275
277
|
const handlers = new Map();
|
|
276
278
|
const cleanup = () => {
|
|
277
279
|
for (const signal of handlers.keys()) process.removeListener(signal, handlers.get(signal));
|