@bitkyc08/opencodex 2.7.42 → 2.7.43
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/README.md +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { isAbsolute, join } from "node:path";
|
|
4
5
|
import { Database } from "bun:sqlite";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_EXPIRES_MS = 3600_000;
|
|
8
|
+
const KIRO_CLI_RECOVERY_SUFFIX = ".opencodex-recovery";
|
|
9
|
+
const KIRO_CLI_RECOVERY_HEADER_V1 = Buffer.from("opencodex-kiro-session-v1\n", "utf8");
|
|
10
|
+
const KIRO_CLI_RECOVERY_HEADER = Buffer.from("opencodex-kiro-session-v2\n", "utf8");
|
|
11
|
+
const KIRO_CLI_RECOVERY_PROCESS_INSTANCE = randomUUID();
|
|
12
|
+
const KIRO_CLI_RECOVERY_PROCESS_INSTANCE_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
13
|
+
const SQLITE_DATABASE_HEADER = Buffer.from("SQLite format 3\0", "binary");
|
|
7
14
|
const KIRO_REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d$/;
|
|
8
15
|
const CLIENT_ID_HASH_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
9
16
|
const TOKEN_KEYS = [
|
|
@@ -49,6 +56,13 @@ export interface ImportedKiroCredential {
|
|
|
49
56
|
clientSecret?: string;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/** Opaque backup retained on disk while a forced Kiro CLI login is pending persistence. */
|
|
60
|
+
export interface KiroCliSessionSnapshot {
|
|
61
|
+
readonly path: string;
|
|
62
|
+
readonly database: Buffer;
|
|
63
|
+
readonly recoveryPath: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
type JsonObject = Record<string, unknown>;
|
|
53
67
|
|
|
54
68
|
function userHome(): string {
|
|
@@ -108,18 +122,30 @@ function jsonCredentialPaths(): string[] {
|
|
|
108
122
|
.map(expandPath);
|
|
109
123
|
}
|
|
110
124
|
|
|
111
|
-
function
|
|
125
|
+
function nativeKiroCliSessionEntries(): Array<{ location: "kiro-cli-data" | "kiro-cli-linux-data"; path: string }> {
|
|
112
126
|
const home = userHome();
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
// Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks
|
|
128
|
+
// (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback.
|
|
115
129
|
return [
|
|
116
130
|
{ location: "kiro-cli-data", path: join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") },
|
|
117
131
|
{ location: "kiro-cli-linux-data", path: join(home, ".local", "share", "kiro-cli", "data.sqlite3") },
|
|
118
|
-
{ location: "amazon-q-data", path: join(home, ".local", "share", "amazon-q", "data.sqlite3") },
|
|
119
|
-
{ location: "kiro-sso-cache", path: join(home, ".kiro", "sso", "cache.db") },
|
|
120
132
|
];
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {
|
|
136
|
+
const configured = process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim();
|
|
137
|
+
if (configured) return [{ location: "kiro-cli-db-env", path: expandPath(configured) }];
|
|
138
|
+
return [
|
|
139
|
+
...nativeKiroCliSessionEntries(),
|
|
140
|
+
{ location: "amazon-q-data", path: join(userHome(), ".local", "share", "amazon-q", "data.sqlite3") },
|
|
141
|
+
{ location: "kiro-sso-cache", path: join(userHome(), ".kiro", "sso", "cache.db") },
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function kiroCliImportSelectorConfigured(): boolean {
|
|
146
|
+
return Boolean(process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim());
|
|
147
|
+
}
|
|
148
|
+
|
|
123
149
|
function selectTokenRow(db: Database): { value: string } | null | "ambiguous" | "selected_missing" {
|
|
124
150
|
const rows = db.query("SELECT key, value FROM auth_kv WHERE key LIKE ? ORDER BY key ASC").all("%:token") as Array<{ key: string; value: string }>;
|
|
125
151
|
const selectedKey = process.env.KIROCLI_TOKEN_KEY?.trim();
|
|
@@ -210,7 +236,16 @@ function readStateProfile(db: Database): { profileArn?: string; apiRegion?: stri
|
|
|
210
236
|
}
|
|
211
237
|
}
|
|
212
238
|
|
|
213
|
-
|
|
239
|
+
interface LocatedKiroCliCredential {
|
|
240
|
+
credential: ImportedKiroCredential;
|
|
241
|
+
path: string;
|
|
242
|
+
database?: Buffer;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readSqliteCredentials(
|
|
246
|
+
diagnostics: KiroImportDiagnostic[],
|
|
247
|
+
includeSnapshot = false,
|
|
248
|
+
): LocatedKiroCliCredential | undefined {
|
|
214
249
|
for (const { location, path } of sqliteEntries()) {
|
|
215
250
|
if (!existsSync(path)) {
|
|
216
251
|
diagnostics.push({ location, status: "missing" });
|
|
@@ -261,7 +296,13 @@ function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKir
|
|
|
261
296
|
const merged = { ...registrationData, ...tokenData, ...profile };
|
|
262
297
|
const credential = credentialFromJson(merged, "sqlite");
|
|
263
298
|
diagnostics.push({ location, status: credential ? "token_found" : "token_missing" });
|
|
264
|
-
if (credential)
|
|
299
|
+
if (credential) {
|
|
300
|
+
return {
|
|
301
|
+
credential,
|
|
302
|
+
path,
|
|
303
|
+
...(includeSnapshot ? { database: db.serialize() } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
265
306
|
} catch (error) {
|
|
266
307
|
if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) throw error;
|
|
267
308
|
diagnostics.push({ location, status: "schema_mismatch" });
|
|
@@ -277,12 +318,12 @@ export function inspectKiroCredentialSources(): { credential: ImportedKiroCreden
|
|
|
277
318
|
const json = readJsonCredentials(diagnostics);
|
|
278
319
|
if (json) return { credential: json, diagnostics };
|
|
279
320
|
const sqlite = readSqliteCredentials(diagnostics);
|
|
280
|
-
return { credential: sqlite ?? null, diagnostics };
|
|
321
|
+
return { credential: sqlite?.credential ?? null, diagnostics };
|
|
281
322
|
}
|
|
282
323
|
|
|
283
324
|
export function inspectKiroCliSqliteSources(): { credential: ImportedKiroCredential | null; diagnostics: KiroImportDiagnostic[] } {
|
|
284
325
|
const diagnostics: KiroImportDiagnostic[] = [];
|
|
285
|
-
return { credential: readSqliteCredentials(diagnostics) ?? null, diagnostics };
|
|
326
|
+
return { credential: readSqliteCredentials(diagnostics)?.credential ?? null, diagnostics };
|
|
286
327
|
}
|
|
287
328
|
|
|
288
329
|
export function readImportedKiroCredential(): ImportedKiroCredential | null {
|
|
@@ -292,3 +333,274 @@ export function readImportedKiroCredential(): ImportedKiroCredential | null {
|
|
|
292
333
|
export function readKiroCliSqliteCredential(): ImportedKiroCredential | null {
|
|
293
334
|
return inspectKiroCliSqliteSources().credential;
|
|
294
335
|
}
|
|
336
|
+
|
|
337
|
+
/** Capture the complete active CLI database so a failed account switch can restore it exactly. */
|
|
338
|
+
export function snapshotKiroCliSession(): KiroCliSessionSnapshot | null {
|
|
339
|
+
return inspectKiroCliSessionSnapshot().snapshot;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* A store that exists but cannot be captured must never be logged out of: the recovery contract
|
|
344
|
+
* promises an exact restore, and without a snapshot a later failure would destroy the session for
|
|
345
|
+
* good. `missing`/`token_missing` are the only statuses that mean "there is nothing to lose".
|
|
346
|
+
*/
|
|
347
|
+
const KIRO_UNSNAPSHOTTABLE_SESSION_STATUSES: ReadonlySet<KiroDiagnosticStatus> = new Set([
|
|
348
|
+
"unreadable",
|
|
349
|
+
"schema_mismatch",
|
|
350
|
+
"invalid_json",
|
|
351
|
+
"token_ambiguous",
|
|
352
|
+
"token_key_missing",
|
|
353
|
+
"token_found",
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Capture the active CLI session and report why capture failed. `blocked` is true when a session
|
|
358
|
+
* store is present but could not be snapshotted (unreadable / schema-mismatched / ambiguous), so
|
|
359
|
+
* callers can abort before mutating it.
|
|
360
|
+
*
|
|
361
|
+
* Forced-login rollback must target the exact database `kiro-cli` mutates. Custom import selectors
|
|
362
|
+
* and lower-priority Amazon Q / SSO caches are never used here: snapshotting the wrong file would
|
|
363
|
+
* leave the real CLI session switched or lost after failure.
|
|
364
|
+
*/
|
|
365
|
+
export function inspectKiroCliSessionSnapshot(): {
|
|
366
|
+
snapshot: KiroCliSessionSnapshot | null;
|
|
367
|
+
diagnostics: KiroImportDiagnostic[];
|
|
368
|
+
blocked: boolean;
|
|
369
|
+
} {
|
|
370
|
+
const diagnostics: KiroImportDiagnostic[] = [];
|
|
371
|
+
if (kiroCliImportSelectorConfigured()) {
|
|
372
|
+
diagnostics.push({ location: "kiro-cli-db-env", status: "token_ambiguous" });
|
|
373
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const { location, path } of nativeKiroCliSessionEntries()) {
|
|
377
|
+
if (!existsSync(path)) {
|
|
378
|
+
diagnostics.push({ location, status: "missing" });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let db: Database | undefined;
|
|
383
|
+
try {
|
|
384
|
+
db = new Database(path, { readonly: true });
|
|
385
|
+
try { db.exec("PRAGMA busy_timeout = 5000"); } catch { /* read-only best effort */ }
|
|
386
|
+
} catch {
|
|
387
|
+
diagnostics.push({ location, status: "unreadable" });
|
|
388
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const row = selectTokenRow(db);
|
|
393
|
+
if (row === "ambiguous") {
|
|
394
|
+
diagnostics.push({ location, status: "token_ambiguous" });
|
|
395
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
396
|
+
}
|
|
397
|
+
if (row === "selected_missing") {
|
|
398
|
+
diagnostics.push({ location, status: "token_key_missing" });
|
|
399
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
400
|
+
}
|
|
401
|
+
if (!row) {
|
|
402
|
+
// An existing CLI database with no recognized token is still what logout mutates. Falling
|
|
403
|
+
// through to Amazon Q / another platform path would snapshot the wrong store.
|
|
404
|
+
diagnostics.push({ location, status: "token_missing" });
|
|
405
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
JSON.parse(row.value);
|
|
409
|
+
} catch {
|
|
410
|
+
diagnostics.push({ location, status: "invalid_json" });
|
|
411
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
412
|
+
}
|
|
413
|
+
const database = db.serialize();
|
|
414
|
+
diagnostics.push({ location, status: "token_found" });
|
|
415
|
+
return {
|
|
416
|
+
snapshot: {
|
|
417
|
+
path,
|
|
418
|
+
database,
|
|
419
|
+
recoveryPath: `${path}${KIRO_CLI_RECOVERY_SUFFIX}`,
|
|
420
|
+
},
|
|
421
|
+
diagnostics,
|
|
422
|
+
blocked: false,
|
|
423
|
+
};
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) {
|
|
426
|
+
diagnostics.push({ location, status: "token_key_missing" });
|
|
427
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
428
|
+
}
|
|
429
|
+
diagnostics.push({ location, status: "schema_mismatch" });
|
|
430
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
431
|
+
} finally {
|
|
432
|
+
db.close();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
snapshot: null,
|
|
438
|
+
diagnostics,
|
|
439
|
+
blocked: diagnostics.some(entry => KIRO_UNSNAPSHOTTABLE_SESSION_STATUSES.has(entry.status)),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Persist a complete, private recovery image before kiro-cli is allowed to mutate its store. */
|
|
444
|
+
export function persistKiroCliSessionRecovery(snapshot: KiroCliSessionSnapshot): void {
|
|
445
|
+
if (existsSync(snapshot.recoveryPath)) {
|
|
446
|
+
throw new Error("Kiro CLI session recovery is already pending.");
|
|
447
|
+
}
|
|
448
|
+
const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
449
|
+
const staged = `${snapshot.recoveryPath}.${nonce}.tmp`;
|
|
450
|
+
try {
|
|
451
|
+
writeFileSync(staged, Buffer.concat([
|
|
452
|
+
KIRO_CLI_RECOVERY_HEADER,
|
|
453
|
+
Buffer.from(`${process.pid}\n`, "utf8"),
|
|
454
|
+
Buffer.from(`${KIRO_CLI_RECOVERY_PROCESS_INSTANCE}\n`, "utf8"),
|
|
455
|
+
snapshot.database,
|
|
456
|
+
]), { flag: "wx", mode: 0o600 });
|
|
457
|
+
try { chmodSync(staged, 0o600); } catch { /* platform may ignore chmod */ }
|
|
458
|
+
const fd = openSync(staged, "r+");
|
|
459
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
460
|
+
// A hard link publishes the fsynced inode atomically and, unlike rename, can never replace
|
|
461
|
+
// another process's live recovery transaction if both raced past the initial existence check.
|
|
462
|
+
linkSync(staged, snapshot.recoveryPath);
|
|
463
|
+
} finally {
|
|
464
|
+
rmSync(staged, { force: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Remove recovery data only after the corresponding login transaction has settled. */
|
|
469
|
+
export function discardKiroCliSessionRecovery(snapshot: KiroCliSessionSnapshot): void {
|
|
470
|
+
rmSync(snapshot.recoveryPath, { force: true });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
interface ParsedKiroCliSessionRecovery {
|
|
474
|
+
ownerPid: number;
|
|
475
|
+
ownerProcessInstance?: string;
|
|
476
|
+
database: Buffer;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function parseKiroCliSessionRecovery(payload: Buffer): ParsedKiroCliSessionRecovery | null {
|
|
480
|
+
const isV2 = payload.subarray(0, KIRO_CLI_RECOVERY_HEADER.length).equals(KIRO_CLI_RECOVERY_HEADER);
|
|
481
|
+
const isV1 = payload.subarray(0, KIRO_CLI_RECOVERY_HEADER_V1.length).equals(KIRO_CLI_RECOVERY_HEADER_V1);
|
|
482
|
+
if (!isV2 && !isV1) return null;
|
|
483
|
+
const headerLength = isV2 ? KIRO_CLI_RECOVERY_HEADER.length : KIRO_CLI_RECOVERY_HEADER_V1.length;
|
|
484
|
+
const ownerEnd = payload.indexOf(0x0a, headerLength);
|
|
485
|
+
if (ownerEnd <= headerLength) return null;
|
|
486
|
+
const ownerPid = Number(payload.subarray(headerLength, ownerEnd).toString("utf8"));
|
|
487
|
+
let databaseStart = ownerEnd + 1;
|
|
488
|
+
let ownerProcessInstance: string | undefined;
|
|
489
|
+
if (isV2) {
|
|
490
|
+
const instanceEnd = payload.indexOf(0x0a, databaseStart);
|
|
491
|
+
if (instanceEnd <= databaseStart) return null;
|
|
492
|
+
ownerProcessInstance = payload.subarray(databaseStart, instanceEnd).toString("utf8");
|
|
493
|
+
if (!KIRO_CLI_RECOVERY_PROCESS_INSTANCE_PATTERN.test(ownerProcessInstance)) return null;
|
|
494
|
+
databaseStart = instanceEnd + 1;
|
|
495
|
+
}
|
|
496
|
+
const database = payload.subarray(databaseStart);
|
|
497
|
+
if (
|
|
498
|
+
!Number.isSafeInteger(ownerPid) || ownerPid <= 0 ||
|
|
499
|
+
!database.subarray(0, SQLITE_DATABASE_HEADER.length).equals(SQLITE_DATABASE_HEADER)
|
|
500
|
+
) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return { ownerPid, ...(ownerProcessInstance ? { ownerProcessInstance } : {}), database };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isKiroRecoveryOwnerAlive(ownerPid: number, ownerProcessInstance?: string): boolean {
|
|
507
|
+
// A restarted supervisor may reuse the exact same PID (commonly PID 1). Only this process
|
|
508
|
+
// instance's nonce proves that a same-PID recovery transaction is still live.
|
|
509
|
+
if (ownerPid === process.pid) return ownerProcessInstance === KIRO_CLI_RECOVERY_PROCESS_INSTANCE;
|
|
510
|
+
try {
|
|
511
|
+
process.kill(ownerPid, 0);
|
|
512
|
+
return true;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error && typeof error === "object" && "code" in error) return error.code !== "ESRCH";
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Restore a previously captured CLI database after every kiro-cli child process has exited. */
|
|
520
|
+
export function restoreKiroCliSession(snapshot: KiroCliSessionSnapshot): void {
|
|
521
|
+
const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
522
|
+
const staged = `${snapshot.path}.ocx-restore.${nonce}.tmp`;
|
|
523
|
+
const displacedBase = `${snapshot.path}.ocx-restore.${nonce}.new`;
|
|
524
|
+
const displaced: Array<{ current: string; backup: string }> = [];
|
|
525
|
+
let published = false;
|
|
526
|
+
try {
|
|
527
|
+
writeFileSync(staged, snapshot.database, { mode: 0o600 });
|
|
528
|
+
try { chmodSync(staged, 0o600); } catch { /* platform may ignore chmod */ }
|
|
529
|
+
for (const suffix of ["", "-wal", "-shm", "-journal"]) {
|
|
530
|
+
const current = `${snapshot.path}${suffix}`;
|
|
531
|
+
if (!existsSync(current)) continue;
|
|
532
|
+
const backup = `${displacedBase}${suffix || ".db"}`;
|
|
533
|
+
renameSync(current, backup);
|
|
534
|
+
displaced.push({ current, backup });
|
|
535
|
+
}
|
|
536
|
+
renameSync(staged, snapshot.path);
|
|
537
|
+
published = true;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
const recoveryErrors: unknown[] = [];
|
|
540
|
+
for (const { current, backup } of [...displaced].reverse()) {
|
|
541
|
+
if (!existsSync(backup) || existsSync(current)) continue;
|
|
542
|
+
try { renameSync(backup, current); } catch (recoveryError) { recoveryErrors.push(recoveryError); }
|
|
543
|
+
}
|
|
544
|
+
if (recoveryErrors.length > 0) throw new AggregateError([error, ...recoveryErrors], "Kiro CLI session restore failed during rollback.");
|
|
545
|
+
throw error;
|
|
546
|
+
} finally {
|
|
547
|
+
rmSync(staged, { force: true });
|
|
548
|
+
if (published) {
|
|
549
|
+
for (const { backup } of displaced) {
|
|
550
|
+
try { rmSync(backup, { force: true }); } catch { /* restored database is already published */ }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Restore a transaction abandoned by a crashed process before starting another forced login. */
|
|
557
|
+
export function restoreStaleKiroCliSessionRecovery(): boolean {
|
|
558
|
+
for (const { path } of nativeKiroCliSessionEntries()) {
|
|
559
|
+
const recoveryPath = `${path}${KIRO_CLI_RECOVERY_SUFFIX}`;
|
|
560
|
+
if (!existsSync(recoveryPath)) continue;
|
|
561
|
+
const payload = readFileSync(recoveryPath);
|
|
562
|
+
const recovery = parseKiroCliSessionRecovery(payload);
|
|
563
|
+
if (!recovery) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Kiro CLI session recovery data is invalid: ${recoveryPath}. Remove this file to continue.`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (isKiroRecoveryOwnerAlive(recovery.ownerPid, recovery.ownerProcessInstance)) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Another Kiro CLI login transaction is still in progress (pid ${recovery.ownerPid}, ${recoveryPath}).`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
// Atomically claim the marker before restoring so a concurrent process cannot restore the
|
|
574
|
+
// same stale image (and later delete a newer recovery file published by the winner).
|
|
575
|
+
const claimedPath = `${recoveryPath}.claimed.${process.pid}.${Date.now()}`;
|
|
576
|
+
try {
|
|
577
|
+
renameSync(recoveryPath, claimedPath);
|
|
578
|
+
} catch {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const claimed = readFileSync(claimedPath);
|
|
583
|
+
if (!claimed.equals(payload)) {
|
|
584
|
+
// Put the unexpected claim back so an operator / later process can inspect it.
|
|
585
|
+
try { renameSync(claimedPath, recoveryPath); } catch { /* keep claimed path for manual recovery */ }
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Kiro CLI session recovery data changed while claiming ${recoveryPath}. Remove leftover claim files under the kiro-cli data directory and retry.`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
restoreKiroCliSession({
|
|
591
|
+
path,
|
|
592
|
+
database: recovery.database,
|
|
593
|
+
recoveryPath: claimedPath,
|
|
594
|
+
});
|
|
595
|
+
rmSync(claimedPath, { force: true });
|
|
596
|
+
return true;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
// Preserve the only copy of the prior database when restore fails (disk full, permissions).
|
|
599
|
+
if (existsSync(claimedPath) && !existsSync(recoveryPath)) {
|
|
600
|
+
try { renameSync(claimedPath, recoveryPath); } catch { /* claimed file remains for manual recovery */ }
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return false;
|
|
606
|
+
}
|