@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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/README.md +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -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/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- 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 +64 -7
- 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/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -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 +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- 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-store.ts +2 -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/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- 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 +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- 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 +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- 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/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- 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 +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -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 +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -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 +19 -7
- package/src/server/auth-cors.ts +114 -24
- 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/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- 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 +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- 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/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -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 +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- 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/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/oauth/kimi.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
6
7
|
import { getConfigDir } from "../config";
|
|
7
8
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
8
9
|
|
|
@@ -106,6 +107,7 @@ function getDeviceId(): string {
|
|
|
106
107
|
if ((e as { code?: string })?.code !== "ENOENT") throw e;
|
|
107
108
|
}
|
|
108
109
|
const id = randomUUID().replace(/-/g, "");
|
|
110
|
+
recordOwnedConfigPath(getConfigDir(), p);
|
|
109
111
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
110
112
|
writeFileSync(p, id + "\n", { mode: 0o600 });
|
|
111
113
|
deviceIdCache = id;
|
|
@@ -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
|
-
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { isAbsolute, join, posix, win32 } 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 = [
|
|
@@ -32,7 +39,14 @@ export type KiroDiagnosticStatus =
|
|
|
32
39
|
| "registration_found";
|
|
33
40
|
|
|
34
41
|
export interface KiroImportDiagnostic {
|
|
35
|
-
location:
|
|
42
|
+
location:
|
|
43
|
+
| "kiro-creds-file"
|
|
44
|
+
| "kiro-cli-db-env"
|
|
45
|
+
| "kiro-cli-data"
|
|
46
|
+
| "kiro-cli-linux-data"
|
|
47
|
+
| "kiro-cli-windows-data"
|
|
48
|
+
| "amazon-q-data"
|
|
49
|
+
| "kiro-sso-cache";
|
|
36
50
|
status: KiroDiagnosticStatus;
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -49,6 +63,13 @@ export interface ImportedKiroCredential {
|
|
|
49
63
|
clientSecret?: string;
|
|
50
64
|
}
|
|
51
65
|
|
|
66
|
+
/** Opaque backup retained on disk while a forced Kiro CLI login is pending persistence. */
|
|
67
|
+
export interface KiroCliSessionSnapshot {
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly database: Buffer;
|
|
70
|
+
readonly recoveryPath: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
type JsonObject = Record<string, unknown>;
|
|
53
74
|
|
|
54
75
|
function userHome(): string {
|
|
@@ -108,18 +129,72 @@ function jsonCredentialPaths(): string[] {
|
|
|
108
129
|
.map(expandPath);
|
|
109
130
|
}
|
|
110
131
|
|
|
132
|
+
export type KiroCliNativeLocation = "kiro-cli-data" | "kiro-cli-linux-data" | "kiro-cli-windows-data";
|
|
133
|
+
|
|
134
|
+
export interface KiroCliNativeInputs {
|
|
135
|
+
env: Record<string, string | undefined>;
|
|
136
|
+
platform: NodeJS.Platform;
|
|
137
|
+
home: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Native kiro-cli session stores, as a pure function of (env, platform, home).
|
|
142
|
+
*
|
|
143
|
+
* Pure + parameterized following `src/claude/desktop-3p-paths.ts`: `process.platform` is stubbable
|
|
144
|
+
* in this repo, but `os.platform()` does NOT follow it under Bun, so a pure resolver is the reliable
|
|
145
|
+
* way to exercise the win32 branch (and its env fallbacks) on a macOS/Linux host.
|
|
146
|
+
*
|
|
147
|
+
* Windows (issue #710): the official installer stores the auth DB at
|
|
148
|
+
* `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`. When LOCALAPPDATA is unset or blank, fall back to
|
|
149
|
+
* `%USERPROFILE%\AppData\Local`, then to the injected platform-native home. Deliberately NOT
|
|
150
|
+
* `userHome()`: that is `HOME || homedir()` and Windows shells (Git Bash / MSYS / CI) routinely
|
|
151
|
+
* export a POSIX-style `HOME`, which would point this at a non-native path.
|
|
152
|
+
*
|
|
153
|
+
* One entry per platform on purpose: this list also drives forced-login snapshot/rollback, so it
|
|
154
|
+
* must name the database the LOCAL kiro-cli actually mutates, never a foreign platform's path.
|
|
155
|
+
*/
|
|
156
|
+
export function resolveKiroCliNativeSessionEntries(
|
|
157
|
+
inputs: KiroCliNativeInputs,
|
|
158
|
+
): Array<{ location: KiroCliNativeLocation; path: string }> {
|
|
159
|
+
const { env, platform, home } = inputs;
|
|
160
|
+
if (platform === "win32") {
|
|
161
|
+
const base = env.LOCALAPPDATA?.trim()
|
|
162
|
+
|| (env.USERPROFILE?.trim() ? win32.join(env.USERPROFILE.trim(), "AppData", "Local") : "")
|
|
163
|
+
|| win32.join(home, "AppData", "Local");
|
|
164
|
+
return [{ location: "kiro-cli-windows-data", path: win32.join(base, "Kiro-Cli", "data.sqlite3") }];
|
|
165
|
+
}
|
|
166
|
+
if (platform === "darwin") {
|
|
167
|
+
return [{ location: "kiro-cli-data", path: posix.join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") }];
|
|
168
|
+
}
|
|
169
|
+
return [{ location: "kiro-cli-linux-data", path: posix.join(home, ".local", "share", "kiro-cli", "data.sqlite3") }];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function nativeKiroCliSessionEntries(): Array<{ location: KiroCliNativeLocation; path: string }> {
|
|
173
|
+
// Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks
|
|
174
|
+
// (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback.
|
|
175
|
+
return resolveKiroCliNativeSessionEntries({
|
|
176
|
+
env: process.env,
|
|
177
|
+
platform: process.platform,
|
|
178
|
+
// POSIX keeps HOME-first userHome() so existing HOME-based fixtures still resolve; win32 prefers
|
|
179
|
+
// LOCALAPPDATA/USERPROFILE and only falls back to this platform-native home.
|
|
180
|
+
home: process.platform === "win32" ? homedir() : userHome(),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
111
184
|
function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {
|
|
112
|
-
const home = userHome();
|
|
113
185
|
const configured = process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim();
|
|
114
186
|
if (configured) return [{ location: "kiro-cli-db-env", path: expandPath(configured) }];
|
|
115
187
|
return [
|
|
116
|
-
|
|
117
|
-
{ location: "
|
|
118
|
-
{ location: "
|
|
119
|
-
{ location: "kiro-sso-cache", path: join(home, ".kiro", "sso", "cache.db") },
|
|
188
|
+
...nativeKiroCliSessionEntries(),
|
|
189
|
+
{ location: "amazon-q-data", path: join(userHome(), ".local", "share", "amazon-q", "data.sqlite3") },
|
|
190
|
+
{ location: "kiro-sso-cache", path: join(userHome(), ".kiro", "sso", "cache.db") },
|
|
120
191
|
];
|
|
121
192
|
}
|
|
122
193
|
|
|
194
|
+
function kiroCliImportSelectorConfigured(): boolean {
|
|
195
|
+
return Boolean(process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim());
|
|
196
|
+
}
|
|
197
|
+
|
|
123
198
|
function selectTokenRow(db: Database): { value: string } | null | "ambiguous" | "selected_missing" {
|
|
124
199
|
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
200
|
const selectedKey = process.env.KIROCLI_TOKEN_KEY?.trim();
|
|
@@ -210,7 +285,16 @@ function readStateProfile(db: Database): { profileArn?: string; apiRegion?: stri
|
|
|
210
285
|
}
|
|
211
286
|
}
|
|
212
287
|
|
|
213
|
-
|
|
288
|
+
interface LocatedKiroCliCredential {
|
|
289
|
+
credential: ImportedKiroCredential;
|
|
290
|
+
path: string;
|
|
291
|
+
database?: Buffer;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function readSqliteCredentials(
|
|
295
|
+
diagnostics: KiroImportDiagnostic[],
|
|
296
|
+
includeSnapshot = false,
|
|
297
|
+
): LocatedKiroCliCredential | undefined {
|
|
214
298
|
for (const { location, path } of sqliteEntries()) {
|
|
215
299
|
if (!existsSync(path)) {
|
|
216
300
|
diagnostics.push({ location, status: "missing" });
|
|
@@ -261,7 +345,13 @@ function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKir
|
|
|
261
345
|
const merged = { ...registrationData, ...tokenData, ...profile };
|
|
262
346
|
const credential = credentialFromJson(merged, "sqlite");
|
|
263
347
|
diagnostics.push({ location, status: credential ? "token_found" : "token_missing" });
|
|
264
|
-
if (credential)
|
|
348
|
+
if (credential) {
|
|
349
|
+
return {
|
|
350
|
+
credential,
|
|
351
|
+
path,
|
|
352
|
+
...(includeSnapshot ? { database: db.serialize() } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
265
355
|
} catch (error) {
|
|
266
356
|
if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) throw error;
|
|
267
357
|
diagnostics.push({ location, status: "schema_mismatch" });
|
|
@@ -277,12 +367,12 @@ export function inspectKiroCredentialSources(): { credential: ImportedKiroCreden
|
|
|
277
367
|
const json = readJsonCredentials(diagnostics);
|
|
278
368
|
if (json) return { credential: json, diagnostics };
|
|
279
369
|
const sqlite = readSqliteCredentials(diagnostics);
|
|
280
|
-
return { credential: sqlite ?? null, diagnostics };
|
|
370
|
+
return { credential: sqlite?.credential ?? null, diagnostics };
|
|
281
371
|
}
|
|
282
372
|
|
|
283
373
|
export function inspectKiroCliSqliteSources(): { credential: ImportedKiroCredential | null; diagnostics: KiroImportDiagnostic[] } {
|
|
284
374
|
const diagnostics: KiroImportDiagnostic[] = [];
|
|
285
|
-
return { credential: readSqliteCredentials(diagnostics) ?? null, diagnostics };
|
|
375
|
+
return { credential: readSqliteCredentials(diagnostics)?.credential ?? null, diagnostics };
|
|
286
376
|
}
|
|
287
377
|
|
|
288
378
|
export function readImportedKiroCredential(): ImportedKiroCredential | null {
|
|
@@ -292,3 +382,274 @@ export function readImportedKiroCredential(): ImportedKiroCredential | null {
|
|
|
292
382
|
export function readKiroCliSqliteCredential(): ImportedKiroCredential | null {
|
|
293
383
|
return inspectKiroCliSqliteSources().credential;
|
|
294
384
|
}
|
|
385
|
+
|
|
386
|
+
/** Capture the complete active CLI database so a failed account switch can restore it exactly. */
|
|
387
|
+
export function snapshotKiroCliSession(): KiroCliSessionSnapshot | null {
|
|
388
|
+
return inspectKiroCliSessionSnapshot().snapshot;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A store that exists but cannot be captured must never be logged out of: the recovery contract
|
|
393
|
+
* promises an exact restore, and without a snapshot a later failure would destroy the session for
|
|
394
|
+
* good. `missing`/`token_missing` are the only statuses that mean "there is nothing to lose".
|
|
395
|
+
*/
|
|
396
|
+
const KIRO_UNSNAPSHOTTABLE_SESSION_STATUSES: ReadonlySet<KiroDiagnosticStatus> = new Set([
|
|
397
|
+
"unreadable",
|
|
398
|
+
"schema_mismatch",
|
|
399
|
+
"invalid_json",
|
|
400
|
+
"token_ambiguous",
|
|
401
|
+
"token_key_missing",
|
|
402
|
+
"token_found",
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Capture the active CLI session and report why capture failed. `blocked` is true when a session
|
|
407
|
+
* store is present but could not be snapshotted (unreadable / schema-mismatched / ambiguous), so
|
|
408
|
+
* callers can abort before mutating it.
|
|
409
|
+
*
|
|
410
|
+
* Forced-login rollback must target the exact database `kiro-cli` mutates. Custom import selectors
|
|
411
|
+
* and lower-priority Amazon Q / SSO caches are never used here: snapshotting the wrong file would
|
|
412
|
+
* leave the real CLI session switched or lost after failure.
|
|
413
|
+
*/
|
|
414
|
+
export function inspectKiroCliSessionSnapshot(): {
|
|
415
|
+
snapshot: KiroCliSessionSnapshot | null;
|
|
416
|
+
diagnostics: KiroImportDiagnostic[];
|
|
417
|
+
blocked: boolean;
|
|
418
|
+
} {
|
|
419
|
+
const diagnostics: KiroImportDiagnostic[] = [];
|
|
420
|
+
if (kiroCliImportSelectorConfigured()) {
|
|
421
|
+
diagnostics.push({ location: "kiro-cli-db-env", status: "token_ambiguous" });
|
|
422
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const { location, path } of nativeKiroCliSessionEntries()) {
|
|
426
|
+
if (!existsSync(path)) {
|
|
427
|
+
diagnostics.push({ location, status: "missing" });
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let db: Database | undefined;
|
|
432
|
+
try {
|
|
433
|
+
db = new Database(path, { readonly: true });
|
|
434
|
+
try { db.exec("PRAGMA busy_timeout = 5000"); } catch { /* read-only best effort */ }
|
|
435
|
+
} catch {
|
|
436
|
+
diagnostics.push({ location, status: "unreadable" });
|
|
437
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const row = selectTokenRow(db);
|
|
442
|
+
if (row === "ambiguous") {
|
|
443
|
+
diagnostics.push({ location, status: "token_ambiguous" });
|
|
444
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
445
|
+
}
|
|
446
|
+
if (row === "selected_missing") {
|
|
447
|
+
diagnostics.push({ location, status: "token_key_missing" });
|
|
448
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
449
|
+
}
|
|
450
|
+
if (!row) {
|
|
451
|
+
// An existing CLI database with no recognized token is still what logout mutates. Falling
|
|
452
|
+
// through to Amazon Q / another platform path would snapshot the wrong store.
|
|
453
|
+
diagnostics.push({ location, status: "token_missing" });
|
|
454
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
JSON.parse(row.value);
|
|
458
|
+
} catch {
|
|
459
|
+
diagnostics.push({ location, status: "invalid_json" });
|
|
460
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
461
|
+
}
|
|
462
|
+
const database = db.serialize();
|
|
463
|
+
diagnostics.push({ location, status: "token_found" });
|
|
464
|
+
return {
|
|
465
|
+
snapshot: {
|
|
466
|
+
path,
|
|
467
|
+
database,
|
|
468
|
+
recoveryPath: `${path}${KIRO_CLI_RECOVERY_SUFFIX}`,
|
|
469
|
+
},
|
|
470
|
+
diagnostics,
|
|
471
|
+
blocked: false,
|
|
472
|
+
};
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) {
|
|
475
|
+
diagnostics.push({ location, status: "token_key_missing" });
|
|
476
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
477
|
+
}
|
|
478
|
+
diagnostics.push({ location, status: "schema_mismatch" });
|
|
479
|
+
return { snapshot: null, diagnostics, blocked: true };
|
|
480
|
+
} finally {
|
|
481
|
+
db.close();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
snapshot: null,
|
|
487
|
+
diagnostics,
|
|
488
|
+
blocked: diagnostics.some(entry => KIRO_UNSNAPSHOTTABLE_SESSION_STATUSES.has(entry.status)),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Persist a complete, private recovery image before kiro-cli is allowed to mutate its store. */
|
|
493
|
+
export function persistKiroCliSessionRecovery(snapshot: KiroCliSessionSnapshot): void {
|
|
494
|
+
if (existsSync(snapshot.recoveryPath)) {
|
|
495
|
+
throw new Error("Kiro CLI session recovery is already pending.");
|
|
496
|
+
}
|
|
497
|
+
const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
498
|
+
const staged = `${snapshot.recoveryPath}.${nonce}.tmp`;
|
|
499
|
+
try {
|
|
500
|
+
writeFileSync(staged, Buffer.concat([
|
|
501
|
+
KIRO_CLI_RECOVERY_HEADER,
|
|
502
|
+
Buffer.from(`${process.pid}\n`, "utf8"),
|
|
503
|
+
Buffer.from(`${KIRO_CLI_RECOVERY_PROCESS_INSTANCE}\n`, "utf8"),
|
|
504
|
+
snapshot.database,
|
|
505
|
+
]), { flag: "wx", mode: 0o600 });
|
|
506
|
+
try { chmodSync(staged, 0o600); } catch { /* platform may ignore chmod */ }
|
|
507
|
+
const fd = openSync(staged, "r+");
|
|
508
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
509
|
+
// A hard link publishes the fsynced inode atomically and, unlike rename, can never replace
|
|
510
|
+
// another process's live recovery transaction if both raced past the initial existence check.
|
|
511
|
+
linkSync(staged, snapshot.recoveryPath);
|
|
512
|
+
} finally {
|
|
513
|
+
rmSync(staged, { force: true });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Remove recovery data only after the corresponding login transaction has settled. */
|
|
518
|
+
export function discardKiroCliSessionRecovery(snapshot: KiroCliSessionSnapshot): void {
|
|
519
|
+
rmSync(snapshot.recoveryPath, { force: true });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
interface ParsedKiroCliSessionRecovery {
|
|
523
|
+
ownerPid: number;
|
|
524
|
+
ownerProcessInstance?: string;
|
|
525
|
+
database: Buffer;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseKiroCliSessionRecovery(payload: Buffer): ParsedKiroCliSessionRecovery | null {
|
|
529
|
+
const isV2 = payload.subarray(0, KIRO_CLI_RECOVERY_HEADER.length).equals(KIRO_CLI_RECOVERY_HEADER);
|
|
530
|
+
const isV1 = payload.subarray(0, KIRO_CLI_RECOVERY_HEADER_V1.length).equals(KIRO_CLI_RECOVERY_HEADER_V1);
|
|
531
|
+
if (!isV2 && !isV1) return null;
|
|
532
|
+
const headerLength = isV2 ? KIRO_CLI_RECOVERY_HEADER.length : KIRO_CLI_RECOVERY_HEADER_V1.length;
|
|
533
|
+
const ownerEnd = payload.indexOf(0x0a, headerLength);
|
|
534
|
+
if (ownerEnd <= headerLength) return null;
|
|
535
|
+
const ownerPid = Number(payload.subarray(headerLength, ownerEnd).toString("utf8"));
|
|
536
|
+
let databaseStart = ownerEnd + 1;
|
|
537
|
+
let ownerProcessInstance: string | undefined;
|
|
538
|
+
if (isV2) {
|
|
539
|
+
const instanceEnd = payload.indexOf(0x0a, databaseStart);
|
|
540
|
+
if (instanceEnd <= databaseStart) return null;
|
|
541
|
+
ownerProcessInstance = payload.subarray(databaseStart, instanceEnd).toString("utf8");
|
|
542
|
+
if (!KIRO_CLI_RECOVERY_PROCESS_INSTANCE_PATTERN.test(ownerProcessInstance)) return null;
|
|
543
|
+
databaseStart = instanceEnd + 1;
|
|
544
|
+
}
|
|
545
|
+
const database = payload.subarray(databaseStart);
|
|
546
|
+
if (
|
|
547
|
+
!Number.isSafeInteger(ownerPid) || ownerPid <= 0 ||
|
|
548
|
+
!database.subarray(0, SQLITE_DATABASE_HEADER.length).equals(SQLITE_DATABASE_HEADER)
|
|
549
|
+
) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
return { ownerPid, ...(ownerProcessInstance ? { ownerProcessInstance } : {}), database };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isKiroRecoveryOwnerAlive(ownerPid: number, ownerProcessInstance?: string): boolean {
|
|
556
|
+
// A restarted supervisor may reuse the exact same PID (commonly PID 1). Only this process
|
|
557
|
+
// instance's nonce proves that a same-PID recovery transaction is still live.
|
|
558
|
+
if (ownerPid === process.pid) return ownerProcessInstance === KIRO_CLI_RECOVERY_PROCESS_INSTANCE;
|
|
559
|
+
try {
|
|
560
|
+
process.kill(ownerPid, 0);
|
|
561
|
+
return true;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error && typeof error === "object" && "code" in error) return error.code !== "ESRCH";
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Restore a previously captured CLI database after every kiro-cli child process has exited. */
|
|
569
|
+
export function restoreKiroCliSession(snapshot: KiroCliSessionSnapshot): void {
|
|
570
|
+
const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
571
|
+
const staged = `${snapshot.path}.ocx-restore.${nonce}.tmp`;
|
|
572
|
+
const displacedBase = `${snapshot.path}.ocx-restore.${nonce}.new`;
|
|
573
|
+
const displaced: Array<{ current: string; backup: string }> = [];
|
|
574
|
+
let published = false;
|
|
575
|
+
try {
|
|
576
|
+
writeFileSync(staged, snapshot.database, { mode: 0o600 });
|
|
577
|
+
try { chmodSync(staged, 0o600); } catch { /* platform may ignore chmod */ }
|
|
578
|
+
for (const suffix of ["", "-wal", "-shm", "-journal"]) {
|
|
579
|
+
const current = `${snapshot.path}${suffix}`;
|
|
580
|
+
if (!existsSync(current)) continue;
|
|
581
|
+
const backup = `${displacedBase}${suffix || ".db"}`;
|
|
582
|
+
renameSync(current, backup);
|
|
583
|
+
displaced.push({ current, backup });
|
|
584
|
+
}
|
|
585
|
+
renameSync(staged, snapshot.path);
|
|
586
|
+
published = true;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
const recoveryErrors: unknown[] = [];
|
|
589
|
+
for (const { current, backup } of [...displaced].reverse()) {
|
|
590
|
+
if (!existsSync(backup) || existsSync(current)) continue;
|
|
591
|
+
try { renameSync(backup, current); } catch (recoveryError) { recoveryErrors.push(recoveryError); }
|
|
592
|
+
}
|
|
593
|
+
if (recoveryErrors.length > 0) throw new AggregateError([error, ...recoveryErrors], "Kiro CLI session restore failed during rollback.");
|
|
594
|
+
throw error;
|
|
595
|
+
} finally {
|
|
596
|
+
rmSync(staged, { force: true });
|
|
597
|
+
if (published) {
|
|
598
|
+
for (const { backup } of displaced) {
|
|
599
|
+
try { rmSync(backup, { force: true }); } catch { /* restored database is already published */ }
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Restore a transaction abandoned by a crashed process before starting another forced login. */
|
|
606
|
+
export function restoreStaleKiroCliSessionRecovery(): boolean {
|
|
607
|
+
for (const { path } of nativeKiroCliSessionEntries()) {
|
|
608
|
+
const recoveryPath = `${path}${KIRO_CLI_RECOVERY_SUFFIX}`;
|
|
609
|
+
if (!existsSync(recoveryPath)) continue;
|
|
610
|
+
const payload = readFileSync(recoveryPath);
|
|
611
|
+
const recovery = parseKiroCliSessionRecovery(payload);
|
|
612
|
+
if (!recovery) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`Kiro CLI session recovery data is invalid: ${recoveryPath}. Remove this file to continue.`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (isKiroRecoveryOwnerAlive(recovery.ownerPid, recovery.ownerProcessInstance)) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Another Kiro CLI login transaction is still in progress (pid ${recovery.ownerPid}, ${recoveryPath}).`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
// Atomically claim the marker before restoring so a concurrent process cannot restore the
|
|
623
|
+
// same stale image (and later delete a newer recovery file published by the winner).
|
|
624
|
+
const claimedPath = `${recoveryPath}.claimed.${process.pid}.${Date.now()}`;
|
|
625
|
+
try {
|
|
626
|
+
renameSync(recoveryPath, claimedPath);
|
|
627
|
+
} catch {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const claimed = readFileSync(claimedPath);
|
|
632
|
+
if (!claimed.equals(payload)) {
|
|
633
|
+
// Put the unexpected claim back so an operator / later process can inspect it.
|
|
634
|
+
try { renameSync(claimedPath, recoveryPath); } catch { /* keep claimed path for manual recovery */ }
|
|
635
|
+
throw new Error(
|
|
636
|
+
`Kiro CLI session recovery data changed while claiming ${recoveryPath}. Remove leftover claim files under the kiro-cli data directory and retry.`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
restoreKiroCliSession({
|
|
640
|
+
path,
|
|
641
|
+
database: recovery.database,
|
|
642
|
+
recoveryPath: claimedPath,
|
|
643
|
+
});
|
|
644
|
+
rmSync(claimedPath, { force: true });
|
|
645
|
+
return true;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
// Preserve the only copy of the prior database when restore fails (disk full, permissions).
|
|
648
|
+
if (existsSync(claimedPath) && !existsSync(recoveryPath)) {
|
|
649
|
+
try { renameSync(claimedPath, recoveryPath); } catch { /* claimed file remains for manual recovery */ }
|
|
650
|
+
}
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return false;
|
|
655
|
+
}
|