@steerable/agent-shell 0.6.20 → 0.6.22
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 +34 -0
- package/dist/auth/index.d.ts +38 -0
- package/dist/auth/index.js +20 -0
- package/dist/host/pack-assembly.d.ts +7 -7
- package/dist/host/runtime.d.ts +6 -2
- package/dist/host/runtime.js +54 -15
- package/dist/host/visible-terminal-exec.d.ts +0 -2
- package/dist/host/visible-terminal-exec.js +4 -8
- package/dist/insights/flush.d.ts +5 -4
- package/dist/insights/flush.js +14 -15
- package/dist/insights/record.d.ts +6 -5
- package/dist/insights/record.js +9 -10
- package/dist/llm/index.d.ts +6 -2
- package/dist/llm/index.js +19 -9
- package/dist/local-backend/ai-suggestions.d.ts +32 -0
- package/dist/local-backend/ai-suggestions.js +160 -0
- package/dist/local-backend/coreloop-stream.js +1 -1
- package/dist/local-backend/mentioned-paths.d.ts +28 -0
- package/dist/local-backend/mentioned-paths.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +10 -4
- package/dist/local-backend/prompt-builder.js +13 -12
- package/dist/local-backend/router.d.ts +26 -4
- package/dist/local-backend/router.js +243 -96
- package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
- package/dist/local-backend/task-service.d.ts +14 -13
- package/dist/local-backend/task-service.js +32 -29
- package/dist/local-backend/turn-files.d.ts +57 -0
- package/dist/local-backend/turn-files.js +285 -0
- package/dist/local-backend/worktree-service.d.ts +1 -1
- package/dist/local-backend/worktree-service.js +6 -6
- package/dist/main.js +45 -26
- package/dist/preload.cjs +30 -10
- package/dist/preload.js +33 -10
- package/dist/server/http-server.d.ts +13 -1
- package/dist/server/http-server.js +45 -8
- package/dist/server/index.js +4 -90
- package/dist/server/sse-bus.d.ts +1 -1
- package/dist/server/start.d.ts +26 -0
- package/dist/server/start.js +111 -0
- package/dist/sidecar/boot.d.ts +3 -1
- package/dist/sidecar/boot.js +6 -7
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -1
- package/dist/sidecar/reverse-ask-user.d.ts +2 -0
- package/dist/sidecar/reverse-ask-user.js +2 -1
- package/dist/sidecar/reverse-spawn.js +12 -4
- package/dist/sidecar/reverse-tools.d.ts +1 -1
- package/dist/sidecar/reverse-tools.js +1 -1
- package/dist/sidecar/storage-path.js +2 -3
- package/dist/storage/driver.d.ts +57 -0
- package/dist/storage/driver.js +104 -0
- package/dist/storage/index.d.ts +80 -67
- package/dist/storage/index.js +528 -296
- package/dist/storage/pack-migrations.d.ts +2 -4
- package/dist/storage/pack-migrations.js +2 -4
- package/dist/storage/scoped-store.d.ts +125 -0
- package/dist/storage/scoped-store.js +1 -0
- package/dist/storage/sqlite-driver.d.ts +16 -0
- package/dist/storage/sqlite-driver.js +135 -0
- package/dist/storage/write-lease-error.d.ts +11 -0
- package/dist/storage/write-lease-error.js +18 -0
- package/package.json +14 -5
- package/dist/storage/local-store-singleton.d.ts +0 -28
- package/dist/storage/local-store-singleton.js +0 -38
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable BS host assembly for executable and embedded deployments.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import dotenv from 'dotenv';
|
|
8
|
+
import log from 'electron-log';
|
|
9
|
+
import { getBrand } from '../brand.js';
|
|
10
|
+
import { registerPackHttpRoutes } from '../host/http-routes.js';
|
|
11
|
+
import { createHostRuntime } from '../host/runtime.js';
|
|
12
|
+
import { getUserDataDir } from '../runtime.js';
|
|
13
|
+
import { createBsServer, } from './http-server.js';
|
|
14
|
+
import { SseBus } from './sse-bus.js';
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
// Match the Electron entry's dotenv precedence: local file first, existing
|
|
17
|
+
// process variables always win.
|
|
18
|
+
for (const file of ['.env.local', '.env']) {
|
|
19
|
+
const fullPath = path.join(path.resolve(__dirname, '..', '..'), file);
|
|
20
|
+
if (existsSync(fullPath))
|
|
21
|
+
dotenv.config({ path: fullPath, override: false });
|
|
22
|
+
}
|
|
23
|
+
log.transports.file.resolvePathFn = () => path.join(getUserDataDir(), 'logs', 'main.log');
|
|
24
|
+
/**
|
|
25
|
+
* Assembles and starts the BS HTTP host.
|
|
26
|
+
*
|
|
27
|
+
* Product composition roots may register providers, package contributions,
|
|
28
|
+
* and middleware before calling this function.
|
|
29
|
+
*/
|
|
30
|
+
export async function startBsHost(options = {}) {
|
|
31
|
+
const brand = getBrand();
|
|
32
|
+
const bus = new SseBus();
|
|
33
|
+
const broadcast = (channel, payload) => bus.broadcast(channel, payload);
|
|
34
|
+
const runtime = await createHostRuntime({
|
|
35
|
+
scope: options.scope,
|
|
36
|
+
broadcast,
|
|
37
|
+
hasWindow: () => bus.size > 0,
|
|
38
|
+
onLog: (line) => log.info('[sidecar]', line),
|
|
39
|
+
taskSweepReason: '服务重启,任务流已中断',
|
|
40
|
+
});
|
|
41
|
+
const { localExecutor, localScriptRegistry, terminalManager, packHandles, localBackendRouter, approvalBridge, askUserBridge, maybeExecInTerminal, } = runtime;
|
|
42
|
+
const webDistDir = process.env.DEEPPATH_WEB_DIST ??
|
|
43
|
+
path.join(__dirname, '..', '..', 'web', 'dist');
|
|
44
|
+
if (!existsSync(path.join(webDistDir, 'index.html'))) {
|
|
45
|
+
await runtime.shutdown();
|
|
46
|
+
throw new Error(`[bs] web build not found at ${webDistDir} — run \`pnpm build\` first.`);
|
|
47
|
+
}
|
|
48
|
+
for (const [packId, handle] of packHandles) {
|
|
49
|
+
const routes = handle.httpRoutes?.();
|
|
50
|
+
if (routes)
|
|
51
|
+
registerPackHttpRoutes(packId, routes);
|
|
52
|
+
}
|
|
53
|
+
const server = createBsServer({
|
|
54
|
+
store: runtime.store,
|
|
55
|
+
localBackendRouter,
|
|
56
|
+
localExecutor,
|
|
57
|
+
localScriptRegistry,
|
|
58
|
+
terminalManager,
|
|
59
|
+
approvalBridge,
|
|
60
|
+
askUserBridge,
|
|
61
|
+
maybeExecInTerminal,
|
|
62
|
+
bus,
|
|
63
|
+
webDistDir,
|
|
64
|
+
authToken: options.authToken,
|
|
65
|
+
middleware: options.middleware,
|
|
66
|
+
});
|
|
67
|
+
const host = options.host ?? (process.env.DEEPPATH_BS_HOST || '127.0.0.1');
|
|
68
|
+
const requestedPort = options.port ?? Number(process.env.DEEPPATH_BS_PORT || 4787);
|
|
69
|
+
try {
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
const onError = (error) => reject(error);
|
|
72
|
+
server.once('error', onError);
|
|
73
|
+
server.listen(requestedPort, host, () => {
|
|
74
|
+
server.off('error', onError);
|
|
75
|
+
resolve();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await runtime.shutdown();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const address = server.address();
|
|
84
|
+
const port = address.port;
|
|
85
|
+
console.log(`[bs] ${brand.displayName} server listening at http://${host}:${port} (flavor=${brand.flavor})`);
|
|
86
|
+
await runtime.start();
|
|
87
|
+
let shutdownPromise = null;
|
|
88
|
+
return {
|
|
89
|
+
server,
|
|
90
|
+
host,
|
|
91
|
+
port,
|
|
92
|
+
shutdown() {
|
|
93
|
+
shutdownPromise ??= (async () => {
|
|
94
|
+
try {
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
server.close((error) => {
|
|
97
|
+
if (error)
|
|
98
|
+
reject(error);
|
|
99
|
+
else
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await runtime.shutdown();
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
return shutdownPromise;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/sidecar/boot.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
1
2
|
import { type ToolRouter } from '../tool-router.js';
|
|
2
3
|
import type { createApprovalBridge } from './reverse-approval.js';
|
|
3
4
|
import type { SidecarReverseHandler } from './types.js';
|
|
4
5
|
export interface HostSidecarDeps {
|
|
6
|
+
store: ScopedStore;
|
|
5
7
|
toolRouter: ToolRouter;
|
|
6
8
|
/** W4-2 项目模式围栏:chatId → 项目根(无项目对话返回 null)。 */
|
|
7
|
-
resolveProjectRoot: (chatId: string) => string | null
|
|
9
|
+
resolveProjectRoot: (chatId: string) => Promise<string | null>;
|
|
8
10
|
/**
|
|
9
11
|
* 项目模式之外额外放行的只读根(会话附件目录等)。文件写入仍只受
|
|
10
12
|
* projectRoot 围栏约束;这些根只放宽 local_read_file 的读取范围。
|
package/dist/sidecar/boot.js
CHANGED
|
@@ -10,7 +10,6 @@ import path from 'node:path';
|
|
|
10
10
|
import log from 'electron-log';
|
|
11
11
|
import { SidecarSupervisor, resolveSidecarPython } from './supervisor.js';
|
|
12
12
|
import { setSidecarSupervisor, setSidecarSupervisorPending, llmService } from '../llm/index.js';
|
|
13
|
-
import { localStore } from '../storage/index.js';
|
|
14
13
|
import { resolveSidecarStoragePath } from './storage-path.js';
|
|
15
14
|
import { deriveEgressAllowListFromBaseUrl, probeExecSandboxCapability, } from './exec-sandbox.js';
|
|
16
15
|
import { createToolInvokeHandler } from './reverse-tools.js';
|
|
@@ -58,7 +57,7 @@ async function deriveSidecarEgressAllowList() {
|
|
|
58
57
|
* ambient 端点并进允许列表,见 W4-8)。
|
|
59
58
|
* - 启动失败 / 派生不出任何代理条目。
|
|
60
59
|
*/
|
|
61
|
-
async function startEgressProxyIfEnabled() {
|
|
60
|
+
async function startEgressProxyIfEnabled(store) {
|
|
62
61
|
try {
|
|
63
62
|
const ambient = process.env.STEERABLE_EGRESS_PROXY === '0' ? [] : await collectAmbientProxyEndpoints();
|
|
64
63
|
const decision = decideEgressProxy({ env: process.env, ambientProxies: ambient });
|
|
@@ -72,7 +71,7 @@ async function startEgressProxyIfEnabled() {
|
|
|
72
71
|
return null;
|
|
73
72
|
}
|
|
74
73
|
const settings = llmService.getSettings();
|
|
75
|
-
const searchSettings =
|
|
74
|
+
const searchSettings = await store.getWebSearchSettings();
|
|
76
75
|
const searchEnv = sidecarWebSearchEnv({
|
|
77
76
|
processEnv: process.env,
|
|
78
77
|
storedApiKey: searchSettings?.apiKey,
|
|
@@ -214,7 +213,7 @@ export async function startHostSidecar(deps) {
|
|
|
214
213
|
consentGranted: true,
|
|
215
214
|
timeoutMs: WEB_TOOL_RPC_TIMEOUT_MS,
|
|
216
215
|
}), webNames);
|
|
217
|
-
const searchSettings =
|
|
216
|
+
const searchSettings = await deps.store.getWebSearchSettings();
|
|
218
217
|
const searchEnv = sidecarWebSearchEnv({
|
|
219
218
|
processEnv: process.env,
|
|
220
219
|
storedApiKey: searchSettings?.apiKey,
|
|
@@ -254,7 +253,7 @@ export async function startHostSidecar(deps) {
|
|
|
254
253
|
};
|
|
255
254
|
const boot = (async () => {
|
|
256
255
|
try {
|
|
257
|
-
const egressProxy = await startEgressProxyIfEnabled();
|
|
256
|
+
const egressProxy = await startEgressProxyIfEnabled(deps.store);
|
|
258
257
|
// W2.6.1: durable sessions/traces/history in a zero-dependency
|
|
259
258
|
// sqlite database under ~/.steerable — the one root the Seatbelt
|
|
260
259
|
// profile already allows writes to; userData would be denied.
|
|
@@ -286,8 +285,8 @@ export async function startHostSidecar(deps) {
|
|
|
286
285
|
...egressProxy?.env,
|
|
287
286
|
...sidecarWebSearchEnv({
|
|
288
287
|
processEnv: process.env,
|
|
289
|
-
storedApiKey:
|
|
290
|
-
storedProvider:
|
|
288
|
+
storedApiKey: (await deps.store.getWebSearchSettings())?.apiKey,
|
|
289
|
+
storedProvider: (await deps.store.getWebSearchSettings())?.provider,
|
|
291
290
|
llmBaseUrl: llmService.getSettings().baseUrl,
|
|
292
291
|
}),
|
|
293
292
|
// P1: offer the sidecar's run_code (programmatic tool calls) to the
|
|
@@ -43,6 +43,8 @@ export interface ApprovalBridgeDeps {
|
|
|
43
43
|
export interface ApprovalBridge {
|
|
44
44
|
/** Reverse-channel handler for the sidecar's `approval.request`. */
|
|
45
45
|
handler: SidecarReverseHandler;
|
|
46
|
+
/** Requests still awaiting a renderer decision, used after page refresh. */
|
|
47
|
+
pending: () => ApprovalPromptRequest[];
|
|
46
48
|
/**
|
|
47
49
|
* IPC entry point for the renderer's answer (`ipcMain.handle(
|
|
48
50
|
* 'approval:decide')`). Unknown requestIds are dropped (late answer to an
|
|
@@ -54,7 +54,7 @@ export function createApprovalBridge(deps) {
|
|
|
54
54
|
};
|
|
55
55
|
deps.onLog?.(`approval: prompting for ${toolName} (${prompt.mode}/${prompt.category})`);
|
|
56
56
|
return await new Promise((resolve) => {
|
|
57
|
-
pending.set(requestId, { resolve });
|
|
57
|
+
pending.set(requestId, { prompt, resolve });
|
|
58
58
|
try {
|
|
59
59
|
deps.broadcast('approval:request', prompt);
|
|
60
60
|
}
|
|
@@ -64,6 +64,7 @@ export function createApprovalBridge(deps) {
|
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
},
|
|
67
|
+
pending: () => Array.from(pending.values(), ({ prompt }) => prompt),
|
|
67
68
|
decide: (payload) => {
|
|
68
69
|
const p = (payload ?? {});
|
|
69
70
|
const requestId = typeof p.requestId === 'string' ? p.requestId : '';
|
|
@@ -26,6 +26,8 @@ export interface AskUserBridgeDeps {
|
|
|
26
26
|
export interface AskUserBridge {
|
|
27
27
|
/** 注册为 sidecar 的 `ask_user.request` 反向方法处理器。 */
|
|
28
28
|
handler: SidecarReverseHandler;
|
|
29
|
+
/** 返回仍在等待 renderer 应答的请求,供刷新后的页面恢复提问。 */
|
|
30
|
+
pending: () => AskUserPromptRequest[];
|
|
29
31
|
/** renderer `ask-user:answer` 的入口;返回是否匹配到待应答请求。 */
|
|
30
32
|
answer: (payload: unknown) => {
|
|
31
33
|
ok: boolean;
|
|
@@ -32,7 +32,7 @@ export function createAskUserBridge(deps) {
|
|
|
32
32
|
questions: questions,
|
|
33
33
|
};
|
|
34
34
|
return new Promise((resolve) => {
|
|
35
|
-
pending.set(requestId, { resolve });
|
|
35
|
+
pending.set(requestId, { prompt, resolve });
|
|
36
36
|
try {
|
|
37
37
|
deps.broadcast('ask-user:request', prompt);
|
|
38
38
|
}
|
|
@@ -43,6 +43,7 @@ export function createAskUserBridge(deps) {
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
},
|
|
46
|
+
pending: () => Array.from(pending.values(), ({ prompt }) => prompt),
|
|
46
47
|
answer: (payload) => {
|
|
47
48
|
const p = (payload ?? {});
|
|
48
49
|
const entry = typeof p.requestId === 'string' ? pending.get(p.requestId) : undefined;
|
|
@@ -29,17 +29,25 @@ export function resolveWinSpawnHelperPath() {
|
|
|
29
29
|
// 显式覆盖(测试/调试逃生门):设置后只认这一条路径。指向不存在的
|
|
30
30
|
// 路径即可强制走"helper 缺失"的失败分支,与 dev checkout 是否已构建
|
|
31
31
|
// Rust 二进制无关,保证失败分支的测试确定性。
|
|
32
|
-
const override = process.env.DEEPPATH_WIN_SPAWN_HELPER;
|
|
32
|
+
const override = process.env.DEEPPATH_WIN_SPAWN_HELPER ?? process.env.STEERABLE_WIN_SPAWN_HELPER;
|
|
33
33
|
if (override !== undefined) {
|
|
34
34
|
return override && existsSync(override) ? override : null;
|
|
35
35
|
}
|
|
36
|
-
const
|
|
36
|
+
const electron = process;
|
|
37
|
+
const resourcesPath = electron.resourcesPath;
|
|
37
38
|
const candidates = [];
|
|
38
39
|
if (resourcesPath) {
|
|
39
40
|
candidates.push(join(resourcesPath, 'win-spawn-helper', binary));
|
|
40
41
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
42
|
+
// Packaged extraResources live under resourcesPath. cwd is the user's
|
|
43
|
+
// launch directory and must not supply the confinement binary.
|
|
44
|
+
// `electron .` sets defaultApp and does not copy extraResources there;
|
|
45
|
+
// npm-published shell also cannot see the host checkout via __dirname.
|
|
46
|
+
const packagedElectron = Boolean(resourcesPath) && electron.defaultApp !== true;
|
|
47
|
+
if (!packagedElectron) {
|
|
48
|
+
candidates.push(join(process.cwd(), 'resources', 'windows-spawn-helper', binary));
|
|
49
|
+
candidates.push(join(__dirname, '..', '..', 'resources', 'windows-spawn-helper', binary));
|
|
50
|
+
}
|
|
43
51
|
for (const candidate of candidates) {
|
|
44
52
|
if (existsSync(candidate))
|
|
45
53
|
return candidate;
|
|
@@ -19,7 +19,7 @@ export interface ReverseToolDeps {
|
|
|
19
19
|
* main.ts 注入 LocalBackendRouter.resolveChatProject 的适配;未注入时
|
|
20
20
|
* 项目围栏不生效(CLI/test 场景)。
|
|
21
21
|
*/
|
|
22
|
-
resolveProjectRoot?: (chatId: string) => string | null
|
|
22
|
+
resolveProjectRoot?: (chatId: string) => Promise<string | null>;
|
|
23
23
|
/**
|
|
24
24
|
* 额外放行的只读根(会话附件目录等)。写入仍只受 projectRoot 围栏约束,
|
|
25
25
|
* 这里只放宽 local_read_file 的读取范围。
|
|
@@ -53,7 +53,7 @@ export function createToolInvokeHandler(deps) {
|
|
|
53
53
|
const projectRoot = typeof p.context?.workspaceRoot === 'string' && p.context.workspaceRoot
|
|
54
54
|
? p.context.workspaceRoot
|
|
55
55
|
: p.context?.chatId
|
|
56
|
-
? (deps.resolveProjectRoot?.(p.context.chatId) ?? null)
|
|
56
|
+
? (await deps.resolveProjectRoot?.(p.context.chatId) ?? null)
|
|
57
57
|
: null;
|
|
58
58
|
const additionalReadRoots = p.context?.chatId
|
|
59
59
|
? (deps.resolveAdditionalReadRoots?.(p.context.chatId) ?? [])
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sidecar 持久化库(sessions/traces/history)路径解析。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 路径付这个代价。
|
|
4
|
+
* Kept outside boot.ts so history readers can resolve this separate database
|
|
5
|
+
* without initializing host storage.
|
|
7
6
|
*/
|
|
8
7
|
import path from 'node:path';
|
|
9
8
|
import { homedir } from 'node:os';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ScopedStore } from './scoped-store.js';
|
|
2
|
+
/** Ownership context applied to every host and scenario-pack database operation. */
|
|
3
|
+
export interface TenantScope {
|
|
4
|
+
tenantId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
}
|
|
7
|
+
/** Ownership used by the personal, single-user product. */
|
|
8
|
+
export declare const LOCAL_SCOPE: TenantScope;
|
|
9
|
+
export type PackDbParams = readonly unknown[] | Readonly<Record<string, unknown>>;
|
|
10
|
+
export interface PackDbRunResult {
|
|
11
|
+
changes: number;
|
|
12
|
+
lastInsertRowid: number | bigint;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Driver-neutral SQL access for scenario-pack repositories.
|
|
16
|
+
*
|
|
17
|
+
* The access object carries scope but cannot safely rewrite arbitrary SQL.
|
|
18
|
+
* Every pack statement must include tenant_id and user_id predicates or values;
|
|
19
|
+
* drivers and source gates reject missing or mismatched ownership. The driver
|
|
20
|
+
* connection is never exposed.
|
|
21
|
+
*/
|
|
22
|
+
export interface PackDbAccess {
|
|
23
|
+
readonly scope: TenantScope;
|
|
24
|
+
get<T extends Record<string, unknown>>(sql: string, params?: PackDbParams): Promise<T | undefined>;
|
|
25
|
+
all<T extends Record<string, unknown>>(sql: string, params?: PackDbParams): Promise<T[]>;
|
|
26
|
+
run(sql: string, params?: PackDbParams): Promise<PackDbRunResult>;
|
|
27
|
+
exec(sql: string): Promise<void>;
|
|
28
|
+
transaction<T>(operation: (db: PackDbAccess) => Promise<T>): Promise<T>;
|
|
29
|
+
}
|
|
30
|
+
export type { ScopedStore } from './scoped-store.js';
|
|
31
|
+
/** Product-selectable host storage implementation. */
|
|
32
|
+
export interface StorageDriver {
|
|
33
|
+
initialize(): Promise<void>;
|
|
34
|
+
scoped(scope: TenantScope): ScopedStore;
|
|
35
|
+
packAccess(scope: TenantScope): PackDbAccess;
|
|
36
|
+
applyPackMigrations(): Promise<void>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export type StorageDriverFactory = () => StorageDriver | Promise<StorageDriver>;
|
|
40
|
+
/** Registers a storage driver. Product packs call this during composition. */
|
|
41
|
+
export declare function registerStorageDriver(id: string, factory: StorageDriverFactory, options?: {
|
|
42
|
+
default?: boolean;
|
|
43
|
+
}): () => void;
|
|
44
|
+
/** Selects a registered driver before storage initialization. */
|
|
45
|
+
export declare function selectStorageDriver(id: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Initializes storage after all product-pack registrations have run.
|
|
48
|
+
*
|
|
49
|
+
* Calls are idempotent and share one initialization promise.
|
|
50
|
+
*/
|
|
51
|
+
export declare function initializeStorage(): Promise<StorageDriver>;
|
|
52
|
+
/** Returns a scoped view after explicit storage initialization. */
|
|
53
|
+
export declare function getScopedStore(scope: TenantScope): ScopedStore;
|
|
54
|
+
/** Returns scope-bound scenario-pack access after initialization. */
|
|
55
|
+
export declare function getPackDbAccess(scope: TenantScope): PackDbAccess;
|
|
56
|
+
/** Closes the active driver. Mainly used by host shutdown and tests. */
|
|
57
|
+
export declare function closeStorage(): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Ownership used by the personal, single-user product. */
|
|
2
|
+
export const LOCAL_SCOPE = Object.freeze({
|
|
3
|
+
tenantId: 'local',
|
|
4
|
+
userId: 'local',
|
|
5
|
+
});
|
|
6
|
+
const DEFAULT_DRIVER_ID = 'sqlite';
|
|
7
|
+
const factories = new Map();
|
|
8
|
+
let selectedDriverId = DEFAULT_DRIVER_ID;
|
|
9
|
+
let activeDriver = null;
|
|
10
|
+
let initialization = null;
|
|
11
|
+
/** Registers a storage driver. Product packs call this during composition. */
|
|
12
|
+
export function registerStorageDriver(id, factory, options = {}) {
|
|
13
|
+
if (factories.has(id)) {
|
|
14
|
+
throw new Error(`[storage] duplicate driver registration: ${id}`);
|
|
15
|
+
}
|
|
16
|
+
if (activeDriver || initialization) {
|
|
17
|
+
throw new Error('[storage] drivers must be registered before initialization');
|
|
18
|
+
}
|
|
19
|
+
factories.set(id, factory);
|
|
20
|
+
if (options.default)
|
|
21
|
+
selectedDriverId = id;
|
|
22
|
+
return () => {
|
|
23
|
+
if (!activeDriver && !initialization) {
|
|
24
|
+
factories.delete(id);
|
|
25
|
+
if (selectedDriverId === id)
|
|
26
|
+
selectedDriverId = DEFAULT_DRIVER_ID;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Selects a registered driver before storage initialization. */
|
|
31
|
+
export function selectStorageDriver(id) {
|
|
32
|
+
if (activeDriver || initialization) {
|
|
33
|
+
throw new Error('[storage] driver selection is closed after initialization');
|
|
34
|
+
}
|
|
35
|
+
selectedDriverId = id;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Initializes storage after all product-pack registrations have run.
|
|
39
|
+
*
|
|
40
|
+
* Calls are idempotent and share one initialization promise.
|
|
41
|
+
*/
|
|
42
|
+
export function initializeStorage() {
|
|
43
|
+
if (activeDriver)
|
|
44
|
+
return Promise.resolve(activeDriver);
|
|
45
|
+
if (initialization)
|
|
46
|
+
return initialization;
|
|
47
|
+
const attempt = (async () => {
|
|
48
|
+
const factory = factories.get(selectedDriverId);
|
|
49
|
+
if (!factory) {
|
|
50
|
+
throw new Error(`[storage] unknown storage driver: ${selectedDriverId}`);
|
|
51
|
+
}
|
|
52
|
+
let driver = null;
|
|
53
|
+
try {
|
|
54
|
+
driver = await factory();
|
|
55
|
+
await driver.initialize();
|
|
56
|
+
await driver.applyPackMigrations();
|
|
57
|
+
activeDriver = driver;
|
|
58
|
+
return driver;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (driver) {
|
|
62
|
+
try {
|
|
63
|
+
await driver.close();
|
|
64
|
+
}
|
|
65
|
+
catch (_closeError) {
|
|
66
|
+
// Initialization error remains the actionable startup failure.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
initialization = attempt;
|
|
73
|
+
void attempt.catch(() => {
|
|
74
|
+
if (initialization === attempt)
|
|
75
|
+
initialization = null;
|
|
76
|
+
});
|
|
77
|
+
return attempt;
|
|
78
|
+
}
|
|
79
|
+
/** Returns a scoped view after explicit storage initialization. */
|
|
80
|
+
export function getScopedStore(scope) {
|
|
81
|
+
if (!activeDriver) {
|
|
82
|
+
throw new Error('[storage] initializeStorage() must complete before use');
|
|
83
|
+
}
|
|
84
|
+
return activeDriver.scoped(scope);
|
|
85
|
+
}
|
|
86
|
+
/** Returns scope-bound scenario-pack access after initialization. */
|
|
87
|
+
export function getPackDbAccess(scope) {
|
|
88
|
+
if (!activeDriver) {
|
|
89
|
+
throw new Error('[storage] initializeStorage() must complete before use');
|
|
90
|
+
}
|
|
91
|
+
return activeDriver.packAccess(scope);
|
|
92
|
+
}
|
|
93
|
+
/** Closes the active driver. Mainly used by host shutdown and tests. */
|
|
94
|
+
export async function closeStorage() {
|
|
95
|
+
const driver = activeDriver;
|
|
96
|
+
activeDriver = null;
|
|
97
|
+
initialization = null;
|
|
98
|
+
if (driver)
|
|
99
|
+
await driver.close();
|
|
100
|
+
}
|
|
101
|
+
registerStorageDriver(DEFAULT_DRIVER_ID, async () => {
|
|
102
|
+
const { SqliteStorageDriver } = await import('./sqlite-driver.js');
|
|
103
|
+
return new SqliteStorageDriver();
|
|
104
|
+
});
|