@cocorograph/hub-agent 0.8.112 → 0.8.114
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/package.json +1 -1
- package/src/claude-stream-bridge.mjs +7 -0
- package/src/codex-profiles.mjs +38 -4
- package/src/enroll.mjs +5 -0
- package/src/main.mjs +55 -27
- package/src/profiles.mjs +43 -5
- package/src/registry-cache.mjs +47 -0
package/package.json
CHANGED
|
@@ -454,6 +454,7 @@ export class ClaudeStreamSession {
|
|
|
454
454
|
cwd,
|
|
455
455
|
model,
|
|
456
456
|
permissionMode,
|
|
457
|
+
chromeEnabled,
|
|
457
458
|
maxTurns,
|
|
458
459
|
maxThinkingTokens,
|
|
459
460
|
effort,
|
|
@@ -495,6 +496,7 @@ export class ClaudeStreamSession {
|
|
|
495
496
|
this._seq = nextSessionSeq()
|
|
496
497
|
this.cwd = cwd
|
|
497
498
|
this.model = model || null
|
|
499
|
+
this.chromeEnabled = chromeEnabled === true
|
|
498
500
|
this.permissionMode = permissionMode || null
|
|
499
501
|
this.maxTurns = typeof maxTurns === "number" ? maxTurns : null
|
|
500
502
|
this.maxThinkingTokens =
|
|
@@ -1534,6 +1536,7 @@ export class ClaudeStreamSession {
|
|
|
1534
1536
|
abortController: this._abortController,
|
|
1535
1537
|
stderr: (data) => this._stderrHandler(data),
|
|
1536
1538
|
}
|
|
1539
|
+
if (this.chromeEnabled) options.extraArgs = { ...options.extraArgs, chrome: null }
|
|
1537
1540
|
if (this.model) options.model = this.model
|
|
1538
1541
|
if (this.permissionMode) options.permissionMode = this.permissionMode
|
|
1539
1542
|
// Phase B: チャット SDK に効くオプション (ツール往復上限)。
|
|
@@ -2272,6 +2275,7 @@ export class ClaudeStreamSession {
|
|
|
2272
2275
|
abortController: this._residentAbortController,
|
|
2273
2276
|
stderr: (data) => this._stderrHandler(data),
|
|
2274
2277
|
}
|
|
2278
|
+
if (this.chromeEnabled) options.extraArgs = { ...options.extraArgs, chrome: null }
|
|
2275
2279
|
if (this.model) options.model = this.model
|
|
2276
2280
|
if (this.permissionMode) options.permissionMode = this.permissionMode
|
|
2277
2281
|
if (this.maxTurns != null) options.maxTurns = this.maxTurns
|
|
@@ -2985,6 +2989,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2985
2989
|
* cwd?: string,
|
|
2986
2990
|
* model?: string|null,
|
|
2987
2991
|
* permissionMode?: string|null,
|
|
2992
|
+
* chromeEnabled?: boolean,
|
|
2988
2993
|
* maxTurns?: number|null,
|
|
2989
2994
|
* maxThinkingTokens?: number|null,
|
|
2990
2995
|
* effort?: string|null,
|
|
@@ -2997,6 +3002,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2997
3002
|
cwd,
|
|
2998
3003
|
model,
|
|
2999
3004
|
permissionMode,
|
|
3005
|
+
chromeEnabled,
|
|
3000
3006
|
maxTurns,
|
|
3001
3007
|
maxThinkingTokens,
|
|
3002
3008
|
effort,
|
|
@@ -3071,6 +3077,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
3071
3077
|
cwd: scwd,
|
|
3072
3078
|
model: model || null,
|
|
3073
3079
|
permissionMode: permissionMode || null,
|
|
3080
|
+
chromeEnabled,
|
|
3074
3081
|
maxTurns: typeof maxTurns === "number" ? maxTurns : null,
|
|
3075
3082
|
maxThinkingTokens:
|
|
3076
3083
|
typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
|
package/src/codex-profiles.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import path from "node:path"
|
|
|
29
29
|
import os from "node:os"
|
|
30
30
|
|
|
31
31
|
import { paths as hubPaths, ensureConfigDir } from "./config.mjs"
|
|
32
|
+
import { fileSignature, keepRuntimeActive } from "./registry-cache.mjs"
|
|
32
33
|
|
|
33
34
|
/** 既定 (プライマリ) プロファイルの ID。 */
|
|
34
35
|
export const DEFAULT_CODEX_PROFILE_ID = "default"
|
|
@@ -94,6 +95,9 @@ export function registryPath() {
|
|
|
94
95
|
|
|
95
96
|
let _cache = null
|
|
96
97
|
|
|
98
|
+
/** 稼働中の active(env へ反映済みのもの)。keepRuntimeActive の docstring 参照。 */
|
|
99
|
+
let _runtimeActive = null
|
|
100
|
+
|
|
97
101
|
/** チルダ展開。 */
|
|
98
102
|
function resolveDir(dir) {
|
|
99
103
|
if (!dir) return defaultCodexDir()
|
|
@@ -106,13 +110,19 @@ function resolveDir(dir) {
|
|
|
106
110
|
* レジストリを読み込む。存在しなければ既定プロファイルのみの仮想レジストリを返す。
|
|
107
111
|
* @returns {Promise<{activeId: string, profiles: Array<{id:string,label:string,configDir:string}>}>}
|
|
108
112
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
async function readRegistry() {
|
|
114
|
+
// ⚠️ profiles.mjs と同じく、キャッシュはファイルの状態と組で持つ(手動編集を即時反映する)。
|
|
115
|
+
const signature = await fileSignature(registryPath())
|
|
116
|
+
if (_cache && _cache.signature === signature) return _cache.registry
|
|
111
117
|
let parsed = null
|
|
112
118
|
try {
|
|
113
119
|
const raw = await fs.readFile(registryPath(), "utf-8")
|
|
114
120
|
parsed = JSON.parse(raw)
|
|
115
121
|
} catch (err) {
|
|
122
|
+
// 手編集の途中(保存前の truncate・書きかけ)で壊れた JSON を読んだときは、直前に
|
|
123
|
+
// 正しく読めた内容を使い続ける(署名は更新しないので、次の呼び出しで読み直す)。
|
|
124
|
+
// 壊れたまま再起動した場合は、従来どおり起動時に失敗する。
|
|
125
|
+
if (err instanceof SyntaxError && _cache) return _cache.registry
|
|
116
126
|
if (err.code !== "ENOENT") throw err
|
|
117
127
|
}
|
|
118
128
|
|
|
@@ -143,8 +153,27 @@ export async function loadRegistry() {
|
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
const registry = { activeId, profiles }
|
|
157
|
+
_runtimeActive = keepRuntimeActive(registry, _runtimeActive)
|
|
158
|
+
_cache = { signature, registry }
|
|
159
|
+
return registry
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* レジストリを読み込む(readRegistry の docstring 参照)。
|
|
164
|
+
*
|
|
165
|
+
* ⚠️ 読み込みが失敗した時点で稼働中の active がまだ決まっていなければ、既定に固定する。
|
|
166
|
+
* 起動時に失敗すると main.mjs は既定のディレクトリで env を組むので、あとでファイルが
|
|
167
|
+
* 直ったときにファイル上の activeId を「稼働中の active」と誤認しないため。stat・パース・
|
|
168
|
+
* 正規化のどこで失敗しても同じに扱う。固定済みなら(稼働後の失敗では)変えない。
|
|
169
|
+
*/
|
|
170
|
+
export async function loadRegistry() {
|
|
171
|
+
try {
|
|
172
|
+
return await readRegistry()
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (!_runtimeActive) _runtimeActive = makeDefaultCodexProfile()
|
|
175
|
+
throw err
|
|
176
|
+
}
|
|
148
177
|
}
|
|
149
178
|
|
|
150
179
|
/** レジストリを永続化する。default は profiles[] に含めず defaultLabel / activeId だけ保存。 */
|
|
@@ -237,6 +266,10 @@ export async function setActiveProfile(id) {
|
|
|
237
266
|
if (!target) throw new Error(`unknown codex profile: ${id}`)
|
|
238
267
|
reg.activeId = id
|
|
239
268
|
await saveRegistry(reg)
|
|
269
|
+
// 保存と並行して読み直した結果が旧 active のままキャッシュされないよう、ここで捨てる
|
|
270
|
+
// (同期で続けて書くので、以後に完了する読み直しは新しい active を使う)。
|
|
271
|
+
_runtimeActive = { ...target }
|
|
272
|
+
_cache = null
|
|
240
273
|
return target
|
|
241
274
|
}
|
|
242
275
|
|
|
@@ -406,4 +439,5 @@ export async function provisionProfile(configDir) {
|
|
|
406
439
|
/** テスト用: モジュールキャッシュを破棄する。 */
|
|
407
440
|
export function _clearCache() {
|
|
408
441
|
_cache = null
|
|
442
|
+
_runtimeActive = null
|
|
409
443
|
}
|
package/src/enroll.mjs
CHANGED
|
@@ -75,6 +75,11 @@ export async function enroll(enrollmentToken, opts = {}) {
|
|
|
75
75
|
hub_url: hubUrl,
|
|
76
76
|
// Hub から取得した Claude CLI 設定 (cockpit 管理画面でユーザーが指定可能)。
|
|
77
77
|
// backend 未対応時は空文字で保存され、起動時の cmd 組み立てで無視される。
|
|
78
|
+
// Claude in Chrome: boolean で届いたときだけ保存する。古い Hub は値を返さないのでキーを
|
|
79
|
+
// 省略し (= 無効)、既存 agent.json は読み直さない (破損時に --force 再登録を妨げないため)。
|
|
80
|
+
...(typeof data.claude_chrome_enabled === "boolean"
|
|
81
|
+
? { claude_chrome_enabled: data.claude_chrome_enabled }
|
|
82
|
+
: {}),
|
|
78
83
|
claude_model: typeof data.claude_model === "string" ? data.claude_model : "",
|
|
79
84
|
claude_permission_mode:
|
|
80
85
|
typeof data.claude_permission_mode === "string" ? data.claude_permission_mode : "",
|
package/src/main.mjs
CHANGED
|
@@ -166,6 +166,7 @@ const BUNDLE_MANIFEST_PATH =
|
|
|
166
166
|
export function resolveClaudeAttachOptions(config, msg = {}) {
|
|
167
167
|
return {
|
|
168
168
|
model: msg.model || config?.claude_model || null,
|
|
169
|
+
chromeEnabled: config?.claude_chrome_enabled === true,
|
|
169
170
|
permissionMode:
|
|
170
171
|
msg.permission_mode ||
|
|
171
172
|
config?.claude_permission_mode ||
|
|
@@ -912,27 +913,37 @@ export function wireBridgeEvents(client, { claudeBridge, codexBridge }) {
|
|
|
912
913
|
}
|
|
913
914
|
}
|
|
914
915
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
916
|
+
/**
|
|
917
|
+
* 起動時の env を組む(シェル補完 → Claude / Codex プロファイルの env 反映)。
|
|
918
|
+
*
|
|
919
|
+
* launchd / systemd 起動では .zshrc 等の対話シェル設定が読まれず、~/.claude.json の
|
|
920
|
+
* MCP 定義が参照する環境変数 (例: Authorization: Bearer ${WRI_MCP_TOKEN}) が解決
|
|
921
|
+
* できない。ユーザーのログインシェル env + dotenv 層を 1 回解決して process.env を
|
|
922
|
+
* 補完する。spawn される全 Claude query が継承し、MCP トークンを出どころ
|
|
923
|
+
* (Keychain / シェル変数 / .env) を問わず引き継げる。WHY: ナレッジ/インフラ/cockpit-mcp-reauth
|
|
924
|
+
*
|
|
925
|
+
* ⚠️ プロファイルの env はシェル補完の**後**に反映する。補完は「欠けているキー」を足すので、
|
|
926
|
+
* 既定プロファイルで CLAUDE_CONFIG_DIR / CODEX_HOME を消したあとに補完すると、シェルに
|
|
927
|
+
* 書かれた値が戻り、一覧(既定)と実行(シェルの値)が食い違う。
|
|
928
|
+
* ⚠️ プロファイルを読めなかったときも既定の env を反映する(継承した値を残すと、一覧は
|
|
929
|
+
* 既定・実行は継承先になる)。profiles.mjs / codex-profiles.mjs の loadRegistry は、この
|
|
930
|
+
* 失敗時に稼働中の active を既定として固定する。
|
|
931
|
+
*
|
|
932
|
+
* @param {object} p
|
|
933
|
+
* @param {import('pino').Logger} p.logger
|
|
934
|
+
* @param {object} [p.deps] テスト用の差し替え(hydrate / getClaudeProfile / getCodexProfile)
|
|
935
|
+
*/
|
|
936
|
+
export async function applyStartupEnv({ logger, deps = {} }) {
|
|
937
|
+
const hydrate = deps.hydrate || hydrateProcessEnvFromShell
|
|
938
|
+
const getClaudeProfile = deps.getClaudeProfile || getActiveProfile
|
|
939
|
+
const getCodexProfile = deps.getCodexProfile || codexProfiles.getActiveProfile
|
|
924
940
|
|
|
925
|
-
|
|
926
|
-
// ctx と dispatchCtx の shallow copy を跨いで共有する lifecycle 正本。
|
|
927
|
-
const lifecycle = { restartPending: false, shutdownStarted: false }
|
|
928
|
-
const ctx = { logger, config, plugins, lifecycle }
|
|
941
|
+
await hydrate({ logger })
|
|
929
942
|
|
|
930
|
-
// 起動時: 保存済み active プロファイルの CLAUDE_CONFIG_DIR を反映してから
|
|
931
|
-
// Claude SDK を初期化する。profiles.json が無ければ既定 (~/.claude) のまま。
|
|
932
943
|
try {
|
|
933
|
-
|
|
934
|
-
applyActiveProfileEnv(activeProfile, logger)
|
|
944
|
+
applyActiveProfileEnv(await getClaudeProfile(), logger)
|
|
935
945
|
} catch (err) {
|
|
946
|
+
applyActiveProfileEnv(null, logger)
|
|
936
947
|
logger.warn(
|
|
937
948
|
{ err: err.message },
|
|
938
949
|
"failed to apply active claude profile; falling back to default ~/.claude",
|
|
@@ -941,22 +952,32 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
941
952
|
|
|
942
953
|
// 同様に active Codex プロファイルの CODEX_HOME を反映する (独立切替)。
|
|
943
954
|
try {
|
|
944
|
-
|
|
945
|
-
applyActiveCodexEnv(activeCodex, logger)
|
|
955
|
+
applyActiveCodexEnv(await getCodexProfile(), logger)
|
|
946
956
|
} catch (err) {
|
|
957
|
+
applyActiveCodexEnv(null, logger)
|
|
947
958
|
logger.warn(
|
|
948
959
|
{ err: err.message },
|
|
949
960
|
"failed to apply active codex profile; falling back to default ~/.codex",
|
|
950
961
|
)
|
|
951
962
|
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export async function startDaemon({ version, claudeSdk } = {}) {
|
|
966
|
+
const logRotationTimer = startLogRotation(logger, { isDeferred: hasInheritedOutputChild })
|
|
967
|
+
startEventLoopMetrics(logger)
|
|
968
|
+
const config = await readConfig()
|
|
969
|
+
if (!config) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
"~/.hub/agent.json が見つかりません。`hub-agent enroll <token>` を先に実行してください。",
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const plugins = await loadPlugins(logger)
|
|
976
|
+
// ctx と dispatchCtx の shallow copy を跨いで共有する lifecycle 正本。
|
|
977
|
+
const lifecycle = { restartPending: false, shutdownStarted: false }
|
|
978
|
+
const ctx = { logger, config, plugins, lifecycle }
|
|
952
979
|
|
|
953
|
-
|
|
954
|
-
// MCP 定義が参照する環境変数 (例: Authorization: Bearer ${WRI_MCP_TOKEN}) が解決
|
|
955
|
-
// できない。ユーザーのログインシェル env + dotenv 層を 1 回解決して process.env を
|
|
956
|
-
// 補完する。spawn される全 Claude query が継承し、MCP トークンを出どころ
|
|
957
|
-
// (Keychain / シェル変数 / .env) を問わず引き継げる。
|
|
958
|
-
// WHY: ナレッジ/インフラ/cockpit-mcp-reauth
|
|
959
|
-
await hydrateProcessEnvFromShell({ logger })
|
|
980
|
+
await applyStartupEnv({ logger })
|
|
960
981
|
|
|
961
982
|
await runHookBroadcast(plugins, "onAgentStart", ctx)
|
|
962
983
|
|
|
@@ -2304,13 +2325,17 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
2304
2325
|
*/
|
|
2305
2326
|
export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
2306
2327
|
const hasClaudeFields =
|
|
2307
|
-
typeof msg?.claude_model === "string" || typeof msg?.claude_permission_mode === "string"
|
|
2328
|
+
typeof msg?.claude_model === "string" || typeof msg?.claude_permission_mode === "string" ||
|
|
2329
|
+
typeof msg?.claude_chrome_enabled === "boolean"
|
|
2308
2330
|
const hasCodexFields =
|
|
2309
2331
|
typeof msg?.codex_model === "string" || typeof msg?.codex_effort === "string"
|
|
2310
2332
|
if (!hasClaudeFields && !hasCodexFields) return
|
|
2311
2333
|
|
|
2312
2334
|
const nextModel = typeof msg.claude_model === "string" ? msg.claude_model : ctx.config.claude_model || ""
|
|
2313
2335
|
const nextMode = typeof msg.claude_permission_mode === "string" ? msg.claude_permission_mode : ctx.config.claude_permission_mode || ""
|
|
2336
|
+
const nextChromeEnabled = typeof msg.claude_chrome_enabled === "boolean"
|
|
2337
|
+
? msg.claude_chrome_enabled
|
|
2338
|
+
: ctx.config.claude_chrome_enabled
|
|
2314
2339
|
const nextCodexModel = typeof msg.codex_model === "string" ? msg.codex_model : ctx.config.codex_model || ""
|
|
2315
2340
|
const nextCodexEffort = typeof msg.codex_effort === "string" ? msg.codex_effort : ctx.config.codex_effort || ""
|
|
2316
2341
|
const prevModel = ctx.config.claude_model || ""
|
|
@@ -2320,6 +2345,7 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
2320
2345
|
if (
|
|
2321
2346
|
nextModel === prevModel &&
|
|
2322
2347
|
nextMode === prevMode &&
|
|
2348
|
+
nextChromeEnabled === ctx.config.claude_chrome_enabled &&
|
|
2323
2349
|
nextCodexModel === prevCodexModel &&
|
|
2324
2350
|
nextCodexEffort === prevCodexEffort
|
|
2325
2351
|
) {
|
|
@@ -2327,6 +2353,7 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
2327
2353
|
}
|
|
2328
2354
|
ctx.config.claude_model = nextModel
|
|
2329
2355
|
ctx.config.claude_permission_mode = nextMode
|
|
2356
|
+
if (typeof nextChromeEnabled === "boolean") ctx.config.claude_chrome_enabled = nextChromeEnabled
|
|
2330
2357
|
ctx.config.codex_model = nextCodexModel
|
|
2331
2358
|
ctx.config.codex_effort = nextCodexEffort
|
|
2332
2359
|
try {
|
|
@@ -2336,6 +2363,7 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
2336
2363
|
hub_url: ctx.config.hub_url,
|
|
2337
2364
|
claude_model: nextModel,
|
|
2338
2365
|
claude_permission_mode: nextMode,
|
|
2366
|
+
claude_chrome_enabled: nextChromeEnabled,
|
|
2339
2367
|
codex_model: nextCodexModel,
|
|
2340
2368
|
codex_effort: nextCodexEffort,
|
|
2341
2369
|
})
|
package/src/profiles.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import path from "node:path"
|
|
|
32
32
|
import os from "node:os"
|
|
33
33
|
|
|
34
34
|
import { paths as hubPaths, ensureConfigDir } from "./config.mjs"
|
|
35
|
+
import { fileSignature, keepRuntimeActive } from "./registry-cache.mjs"
|
|
35
36
|
|
|
36
37
|
/** 既定 (プライマリ) プロファイルの ID。 */
|
|
37
38
|
export const DEFAULT_PROFILE_ID = "default"
|
|
@@ -56,9 +57,13 @@ export function registryPath() {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** モジュール内キャッシュ (頻繁に呼ばれる getActiveConfigDir のため)。
|
|
59
|
-
*
|
|
60
|
+
* `{ signature, registry }`。signature がファイルの現状と一致するときだけ使う
|
|
61
|
+
* (手動編集の即時反映)。setActiveProfile / 書き込み系でも念のため無効化する。 */
|
|
60
62
|
let _cache = null
|
|
61
63
|
|
|
64
|
+
/** 稼働中の active(env へ反映済みのもの)。keepRuntimeActive の docstring 参照。 */
|
|
65
|
+
let _runtimeActive = null
|
|
66
|
+
|
|
62
67
|
/** チルダ展開 (`~` / `~/...` を home 起点に解決)。それ以外は path.resolve。 */
|
|
63
68
|
function resolveDir(dir) {
|
|
64
69
|
if (!dir) return defaultConfigDir()
|
|
@@ -73,13 +78,22 @@ function resolveDir(dir) {
|
|
|
73
78
|
*
|
|
74
79
|
* @returns {Promise<{activeId: string, profiles: Array<{id:string,label:string,configDir:string}>}>}
|
|
75
80
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
async function readRegistry() {
|
|
82
|
+
// ⚠️ キャッシュは「読んだ時点のファイルの状態」と組で持ち、毎回 stat して変わっていたら
|
|
83
|
+
// 読み直す。起動時に読んだ内容を使い続けると、稼働中に ~/.hub/profiles.json を手で
|
|
84
|
+
// 作成・編集しても agent を再起動するまで反映されなかった(初期登録で再起動が 2 回要った)。
|
|
85
|
+
// stat は極小ファイル 1 回分なので、頻繁に呼ばれる getActiveConfigDir でも負荷は無視できる。
|
|
86
|
+
const signature = await fileSignature(registryPath())
|
|
87
|
+
if (_cache && _cache.signature === signature) return _cache.registry
|
|
78
88
|
let parsed = null
|
|
79
89
|
try {
|
|
80
90
|
const raw = await fs.readFile(registryPath(), "utf-8")
|
|
81
91
|
parsed = JSON.parse(raw)
|
|
82
92
|
} catch (err) {
|
|
93
|
+
// 手編集の途中(保存前の truncate・書きかけ)で壊れた JSON を読んだときは、直前に
|
|
94
|
+
// 正しく読めた内容を使い続ける(署名は更新しないので、次の呼び出しで読み直す)。
|
|
95
|
+
// 壊れたまま再起動した場合は、従来どおり起動時に失敗する。
|
|
96
|
+
if (err instanceof SyntaxError && _cache) return _cache.registry
|
|
83
97
|
if (err.code !== "ENOENT") throw err
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -112,8 +126,27 @@ export async function loadRegistry() {
|
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
const registry = { activeId, profiles }
|
|
130
|
+
_runtimeActive = keepRuntimeActive(registry, _runtimeActive)
|
|
131
|
+
_cache = { signature, registry }
|
|
132
|
+
return registry
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* レジストリを読み込む(readRegistry の docstring 参照)。
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ 読み込みが失敗した時点で稼働中の active がまだ決まっていなければ、既定に固定する。
|
|
139
|
+
* 起動時に失敗すると main.mjs は既定のディレクトリで env を組むので、あとでファイルが
|
|
140
|
+
* 直ったときにファイル上の activeId を「稼働中の active」と誤認しないため。stat・パース・
|
|
141
|
+
* 正規化のどこで失敗しても同じに扱う。固定済みなら(稼働後の失敗では)変えない。
|
|
142
|
+
*/
|
|
143
|
+
export async function loadRegistry() {
|
|
144
|
+
try {
|
|
145
|
+
return await readRegistry()
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (!_runtimeActive) _runtimeActive = makeDefaultProfile()
|
|
148
|
+
throw err
|
|
149
|
+
}
|
|
117
150
|
}
|
|
118
151
|
|
|
119
152
|
/** レジストリを永続化する。default は profiles[] に含めず、追加分と activeId / defaultLabel だけ保存。 */
|
|
@@ -215,6 +248,10 @@ export async function setActiveProfile(id) {
|
|
|
215
248
|
if (!target) throw new Error(`unknown profile: ${id}`)
|
|
216
249
|
reg.activeId = id
|
|
217
250
|
await saveRegistry(reg)
|
|
251
|
+
// 保存と並行して読み直した結果が旧 active のままキャッシュされないよう、ここで捨てる
|
|
252
|
+
// (同期で続けて書くので、以後に完了する読み直しは新しい active を使う)。
|
|
253
|
+
_runtimeActive = { ...target }
|
|
254
|
+
_cache = null
|
|
218
255
|
return target
|
|
219
256
|
}
|
|
220
257
|
|
|
@@ -537,4 +574,5 @@ async function isBrokenSymlink(p) {
|
|
|
537
574
|
/** テスト用: モジュールキャッシュを破棄する。 */
|
|
538
575
|
export function _clearCache() {
|
|
539
576
|
_cache = null
|
|
577
|
+
_runtimeActive = null
|
|
540
578
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ファイルの現在の状態を表す文字列を返す(読み込み結果をキャッシュするときの照合キー)。
|
|
5
|
+
*
|
|
6
|
+
* パス・inode・サイズ・mtime・ctime の組で、どれかが変われば別物として扱う
|
|
7
|
+
* (同じミリ秒内の同サイズ書き換えも ctime / inode で拾う。エディタの「別名保存 → rename」も
|
|
8
|
+
* inode で拾う)。ファイルが無ければ `<path>:absent`。ENOENT 以外の失敗はそのまま投げる。
|
|
9
|
+
*
|
|
10
|
+
* @param {string} p 絶対パス
|
|
11
|
+
* @returns {Promise<string>}
|
|
12
|
+
*/
|
|
13
|
+
export async function fileSignature(p) {
|
|
14
|
+
try {
|
|
15
|
+
const st = await fs.stat(p)
|
|
16
|
+
return `${p}:${st.ino}:${st.size}:${st.mtimeMs}:${st.ctimeMs}`
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code === "ENOENT") return `${p}:absent`
|
|
19
|
+
throw err
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 稼働中に使っている active プロファイルを、読み直したレジストリへ当て直す。
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ active は「env(CLAUDE_CONFIG_DIR / CODEX_HOME)に反映済みのもの」をプロセスが持ち続ける。
|
|
27
|
+
* ファイルを読み直すたびに activeId や active の configDir を手編集どおりに変えると、一覧は
|
|
28
|
+
* 新しい active を表示するのに、子プロセスは起動時の env を継承して古いアカウントで動く
|
|
29
|
+
* (表示と実行が食い違う)。env の反映と既存セッションの停止は UI の切替経路(main.mjs の
|
|
30
|
+
* profile.switch / codex.profile.switch)だけが行うので、稼働中の active の変更もその経路に限る。
|
|
31
|
+
* active への手編集(activeId の変更・active の configDir 変更・active の削除)は再起動で反映される。
|
|
32
|
+
*
|
|
33
|
+
* @param {{activeId:string, profiles:Array<{id:string,label:string,configDir:string}>}} registry 読み直した結果(破壊的に更新する)
|
|
34
|
+
* @param {{id:string,label:string,configDir:string}|null} pinned 稼働中の active(初回は null)
|
|
35
|
+
* @returns {{id:string,label:string,configDir:string}} 以後保持する active
|
|
36
|
+
*/
|
|
37
|
+
export function keepRuntimeActive(registry, pinned) {
|
|
38
|
+
if (!pinned) {
|
|
39
|
+
const active = registry.profiles.find((p) => p.id === registry.activeId) || registry.profiles[0]
|
|
40
|
+
return { ...active }
|
|
41
|
+
}
|
|
42
|
+
const hit = registry.profiles.find((p) => p.id === pinned.id)
|
|
43
|
+
if (!hit) registry.profiles.push({ ...pinned })
|
|
44
|
+
else if (hit.configDir !== pinned.configDir) hit.configDir = pinned.configDir
|
|
45
|
+
registry.activeId = pinned.id
|
|
46
|
+
return pinned
|
|
47
|
+
}
|