indusagi-coding-agent 0.2.8 → 0.2.9
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 +2 -1
- package/src/_decls/entry.ts +18 -0
- package/src/_decls/guardrails.ts +35 -0
- package/src/_decls/index.ts +26 -0
- package/src/addons/contract.ts +236 -0
- package/src/addons/dispatch/event-dispatcher.ts +164 -0
- package/src/addons/dispatch/index.ts +25 -0
- package/src/addons/dispatch/tool-interceptor.ts +208 -0
- package/src/addons/host.ts +225 -0
- package/src/addons/index.ts +112 -0
- package/src/addons/manifest.ts +158 -0
- package/src/addons/sandbox.ts +170 -0
- package/src/addons/surface.ts +78 -0
- package/src/boot/auth-vault.ts +195 -0
- package/src/boot/boot.ts +138 -0
- package/src/boot/contract.ts +238 -0
- package/src/boot/heap.ts +59 -0
- package/src/boot/index.ts +28 -0
- package/src/boot/invocation.ts +93 -0
- package/src/boot/runners/addon-wiring.ts +153 -0
- package/src/boot/runners/checkpoint.ts +169 -0
- package/src/boot/runners/delegate-runner.ts +294 -0
- package/src/boot/runners/index.ts +13 -0
- package/src/boot/runners/link-runner.ts +45 -0
- package/src/boot/runners/memdir.ts +168 -0
- package/src/boot/runners/oneshot-runner.ts +58 -0
- package/src/boot/runners/read-state.ts +90 -0
- package/src/boot/runners/registry.ts +42 -0
- package/src/boot/runners/repl-runner.ts +143 -0
- package/src/boot/runners/server-mode.ts +121 -0
- package/src/boot/runners/session.ts +641 -0
- package/src/boot/server-token.ts +148 -0
- package/src/boot/stages.ts +167 -0
- package/src/boot/upgrade/apply.ts +94 -0
- package/src/boot/upgrade/index.ts +13 -0
- package/src/boot/upgrade/upgrades.ts +289 -0
- package/src/briefing/compose.ts +150 -0
- package/src/briefing/context-docs.ts +19 -0
- package/src/briefing/contract.ts +717 -0
- package/src/briefing/index.ts +31 -0
- package/src/briefing/macros.ts +97 -0
- package/src/briefing/skills.ts +47 -0
- package/src/capability-deck/bridge-ledger/index.ts +27 -0
- package/src/capability-deck/bridge-ledger/key.ts +67 -0
- package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
- package/src/capability-deck/bridge-ledger/network.ts +117 -0
- package/src/capability-deck/builtin-bridge.ts +312 -0
- package/src/capability-deck/cards/bg-process-card.ts +335 -0
- package/src/capability-deck/cards/index.ts +115 -0
- package/src/capability-deck/cards/memory-card.ts +146 -0
- package/src/capability-deck/cards/plan-file.ts +97 -0
- package/src/capability-deck/cards/plan-tools.ts +185 -0
- package/src/capability-deck/cards/saas-card.ts +183 -0
- package/src/capability-deck/cards/task-card.ts +207 -0
- package/src/capability-deck/cards/todo-card.ts +168 -0
- package/src/capability-deck/cards/workflow-card.ts +247 -0
- package/src/capability-deck/contract.ts +388 -0
- package/src/capability-deck/index.ts +48 -0
- package/src/capability-deck/manifest.ts +109 -0
- package/src/capability-deck/provision.ts +169 -0
- package/src/channels/contract.ts +191 -0
- package/src/channels/framer.ts +50 -0
- package/src/channels/index.ts +101 -0
- package/src/channels/link/dialog.ts +129 -0
- package/src/channels/link/driver.ts +190 -0
- package/src/channels/link/index.ts +34 -0
- package/src/channels/link/server.ts +134 -0
- package/src/channels/oneshot.ts +90 -0
- package/src/channels/ops.ts +65 -0
- package/src/channels/session-ops.ts +81 -0
- package/src/conductor/bash-guard.ts +599 -0
- package/src/conductor/catalog/catalog.ts +116 -0
- package/src/conductor/catalog/index.ts +8 -0
- package/src/conductor/catalog/matcher.ts +134 -0
- package/src/conductor/conductor.ts +234 -0
- package/src/conductor/contract.ts +842 -0
- package/src/conductor/diagnostics.ts +227 -0
- package/src/conductor/index.ts +33 -0
- package/src/conductor/permissions.ts +588 -0
- package/src/conductor/quota-error.ts +49 -0
- package/src/conductor/signal-hub/hub.ts +46 -0
- package/src/conductor/signal-hub/index.ts +2 -0
- package/src/conductor/signal-hub/translate.test.ts +81 -0
- package/src/conductor/signal-hub/translate.ts +74 -0
- package/src/conductor/skill-parse/index.ts +2 -0
- package/src/conductor/skill-parse/parse.ts +108 -0
- package/src/conductor/transcript-store/index.ts +22 -0
- package/src/conductor/transcript-store/serialize.ts +116 -0
- package/src/conductor/transcript-store/store.ts +205 -0
- package/src/console/auth-status.ts +56 -0
- package/src/console/components/AgentsView.ts +165 -0
- package/src/console/components/BackgroundAgents.ts +155 -0
- package/src/console/components/Banner.ts +334 -0
- package/src/console/components/Composer.ts +94 -0
- package/src/console/components/StatusBar.ts +49 -0
- package/src/console/components/TerminalConsole.ts +1090 -0
- package/src/console/components/WorkingIndicator.ts +98 -0
- package/src/console/components/banner-sweep.ts +24 -0
- package/src/console/components/welcome.ts +74 -0
- package/src/console/contract.ts +630 -0
- package/src/console/index.ts +34 -0
- package/src/console/input/complete.ts +127 -0
- package/src/console/input/dir-reader.ts +34 -0
- package/src/console/input/index.ts +23 -0
- package/src/console/input/keymap.ts +159 -0
- package/src/console/input/paste.ts +104 -0
- package/src/console/mount.ts +56 -0
- package/src/console/overlays/approval-queue.ts +57 -0
- package/src/console/overlays/approval.ts +130 -0
- package/src/console/overlays/auth.ts +342 -0
- package/src/console/overlays/boards.ts +308 -0
- package/src/console/overlays/host.ts +36 -0
- package/src/console/overlays/index.ts +26 -0
- package/src/console/overlays/pickers.ts +258 -0
- package/src/console/overlays/sessions.ts +190 -0
- package/src/console/reducer.ts +182 -0
- package/src/console/slash/builtins.ts +81 -0
- package/src/console/slash/commands/dynamic.ts +83 -0
- package/src/console/slash/commands/integrations.ts +695 -0
- package/src/console/slash/commands/shared.ts +75 -0
- package/src/console/slash/commands/transcript.ts +263 -0
- package/src/console/slash/commands/workbench.ts +246 -0
- package/src/console/slash/index.ts +15 -0
- package/src/console/slash/registry.ts +70 -0
- package/src/console/slash/resolve.ts +63 -0
- package/src/console/startup.ts +209 -0
- package/src/console/theme/adapter.ts +45 -0
- package/src/console/theme/index.ts +7 -0
- package/src/console/theme/palette.ts +68 -0
- package/src/console/theme/resolve.ts +39 -0
- package/src/console/theme/tokens.ts +71 -0
- package/src/entry.ts +55 -0
- package/src/guardrails.ts +37 -0
- package/src/index.ts +18 -0
- package/src/insight/channel.ts +88 -0
- package/src/insight/contract.ts +185 -0
- package/src/insight/index.ts +110 -0
- package/src/insight/recorder.ts +213 -0
- package/src/insight/redaction.ts +157 -0
- package/src/insight/replay.ts +158 -0
- package/src/insight/sampling.ts +70 -0
- package/src/insight/serialize.ts +50 -0
- package/src/insight/sinks/console.ts +64 -0
- package/src/insight/sinks/file.ts +40 -0
- package/src/insight/sinks/index.ts +24 -0
- package/src/insight/sinks/stream.ts +54 -0
- package/src/integrations/sarvam/attach.ts +239 -0
- package/src/integrations/sarvam/config.ts +156 -0
- package/src/integrations/sarvam/index.ts +25 -0
- package/src/integrations/sarvam/sarvam.test.ts +60 -0
- package/src/integrations/sarvam/types.ts +27 -0
- package/src/integrations/zoho/attach.ts +342 -0
- package/src/integrations/zoho/config.ts +125 -0
- package/src/integrations/zoho/index.ts +27 -0
- package/src/integrations/zoho/types.ts +21 -0
- package/src/integrations/zoho/zoho.test.ts +50 -0
- package/src/kit/clipboard-image.ts +107 -0
- package/src/kit/external-editor.ts +48 -0
- package/src/kit/image.ts +59 -0
- package/src/kit/index.ts +51 -0
- package/src/kit/shell.ts +19 -0
- package/src/kit/tool-fetch.ts +85 -0
- package/src/launch/catalog.ts +148 -0
- package/src/launch/contract.ts +187 -0
- package/src/launch/credentials.ts +625 -0
- package/src/launch/index.ts +98 -0
- package/src/launch/invocation/attachments.ts +179 -0
- package/src/launch/invocation/flags.ts +196 -0
- package/src/launch/invocation/index.ts +25 -0
- package/src/launch/invocation/read.ts +260 -0
- package/src/launch/invocation/usage.ts +67 -0
- package/src/launch/login.ts +324 -0
- package/src/launch/oauth.test.ts +18 -0
- package/src/launch/oauth.ts +203 -0
- package/src/launch/packages.ts +194 -0
- package/src/launch/pickers.ts +189 -0
- package/src/runtime-bridge/bridges/_drive.ts +96 -0
- package/src/runtime-bridge/bridges/builtins.ts +68 -0
- package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
- package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
- package/src/runtime-bridge/bridges/index.ts +33 -0
- package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
- package/src/runtime-bridge/broker.ts +227 -0
- package/src/runtime-bridge/contract.ts +122 -0
- package/src/runtime-bridge/index.ts +79 -0
- package/src/runtime-bridge/sink.ts +180 -0
- package/src/sessions/contract.ts +81 -0
- package/src/sessions/index.ts +13 -0
- package/src/sessions/library.ts +229 -0
- package/src/settings/contract.ts +114 -0
- package/src/settings/index.ts +32 -0
- package/src/settings/manager.ts +117 -0
- package/src/transcript-export/index.ts +45 -0
- package/src/transcript-export/publish.ts +260 -0
- package/src/transcript-export/sgr.ts +315 -0
- package/src/transcript-export/template.ts +272 -0
- package/src/transcript-export/theme-bridge.ts +150 -0
- package/src/window-budget/budget/estimate.ts +135 -0
- package/src/window-budget/budget/gate.ts +33 -0
- package/src/window-budget/budget/index.ts +16 -0
- package/src/window-budget/budget/slice.ts +56 -0
- package/src/window-budget/condenser.ts +58 -0
- package/src/window-budget/contract.ts +184 -0
- package/src/window-budget/index.ts +19 -0
- package/src/window-budget/microcompact.ts +95 -0
- package/src/window-budget/rehydrate.ts +136 -0
- package/src/window-budget/summarize/condense.ts +103 -0
- package/src/window-budget/summarize/index.ts +14 -0
- package/src/window-budget/summarize/prompt.ts +149 -0
- package/src/workflow-engine/agent-runner.ts +181 -0
- package/src/workflow-engine/display.ts +224 -0
- package/src/workflow-engine/engine.ts +294 -0
- package/src/workflow-engine/index.ts +23 -0
- package/src/workflow-engine/parse.ts +172 -0
- package/src/workflow-engine/structured-output.ts +35 -0
- package/src/workspace/brand.ts +29 -0
- package/src/workspace/index.ts +18 -0
- package/src/workspace/locator.ts +103 -0
- package/src/workspace/runtime-detect.ts +64 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-token store — the boot-layer persistence for the indus device-login
|
|
3
|
+
* session token. This is the app side of the login + model-gateway contract: a
|
|
4
|
+
* successful `indus login` device flow writes the better-auth session token
|
|
5
|
+
* here, and the key resolver later reads it back to drive "server mode" (Case B
|
|
6
|
+
* — no local provider key, but a valid server session, so requests are routed
|
|
7
|
+
* through the indus-server gateway with the session token as the api key).
|
|
8
|
+
*
|
|
9
|
+
* The token lives in a single JSON file under the app's profile directory
|
|
10
|
+
* (`~/.indusagi/agent/server-token.json`) — deliberately NOT the framework's
|
|
11
|
+
* `~/.better-auth` location, so the CLI session is isolated from any other
|
|
12
|
+
* tooling. As with the credential vault, the file is tiny and access is
|
|
13
|
+
* interactive, so we read / write the whole file each time and tolerate a
|
|
14
|
+
* missing or malformed file as "no token".
|
|
15
|
+
*
|
|
16
|
+
* Ported from supercli `server/src/lib/token.ts`; the load-bearing detail is the
|
|
17
|
+
* `expires_in` (seconds, from the device-flow response) → `expires_at` (absolute
|
|
18
|
+
* ISO timestamp, on disk) conversion, which lets {@link isServerTokenExpired}
|
|
19
|
+
* decide freshness without re-deriving the clock offset.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
/** Root of the indus-server. Used to build the gateway base URL and the device-login endpoints. Any trailing slash(es) are stripped so callers can append paths with a single separator. */
|
|
26
|
+
export const INDUS_SERVER_URL: string = process.env.INDUS_SERVER_URL?.replace(/\/+$/, "") ?? "https://indus-server.vercel.app";
|
|
27
|
+
/** The app profile directory that holds the server session token. */
|
|
28
|
+
export const CONFIG_DIR: string = path.join(os.homedir(), ".indusagi", "agent");
|
|
29
|
+
/** Absolute path of the JSON file the session token is persisted to. */
|
|
30
|
+
export const TOKEN_FILE: string = path.join(CONFIG_DIR, "server-token.json");
|
|
31
|
+
|
|
32
|
+
/** On-disk token shape (what {@link storeServerToken} writes / {@link getServerToken} returns). */
|
|
33
|
+
export interface TokenData {
|
|
34
|
+
/** The better-auth session token, used as the gateway api key in server mode. */
|
|
35
|
+
access_token: string;
|
|
36
|
+
/** Optional refresh token, when the device flow returns one. */
|
|
37
|
+
refresh_token?: string;
|
|
38
|
+
/** Token scheme; defaults to `"Bearer"`. */
|
|
39
|
+
token_type: string;
|
|
40
|
+
/** Optional granted scope string. */
|
|
41
|
+
scope?: string;
|
|
42
|
+
/** Absolute expiry as an ISO string, or null when the token has no expiry. */
|
|
43
|
+
expires_at: string | null;
|
|
44
|
+
/** When this record was written, as an ISO string. */
|
|
45
|
+
created_at: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Device-flow response shape accepted by {@link storeServerToken} (`expires_in` → `expires_at`). */
|
|
49
|
+
export interface ServerTokenInput {
|
|
50
|
+
/** The better-auth session token. */
|
|
51
|
+
access_token: string;
|
|
52
|
+
/** Optional refresh token. */
|
|
53
|
+
refresh_token?: string;
|
|
54
|
+
/** Token scheme; defaults to `"Bearer"` when omitted. */
|
|
55
|
+
token_type?: string;
|
|
56
|
+
/** Optional granted scope string. */
|
|
57
|
+
scope?: string;
|
|
58
|
+
/** Lifetime in seconds, relative to now; converted to an absolute `expires_at`. */
|
|
59
|
+
expires_in?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read the stored session token, tolerating a missing or malformed file as
|
|
64
|
+
* "no token".
|
|
65
|
+
*
|
|
66
|
+
* @returns the parsed {@link TokenData}, or `null` when nothing is stored.
|
|
67
|
+
*/
|
|
68
|
+
export async function getServerToken(): Promise<TokenData | null> {
|
|
69
|
+
try {
|
|
70
|
+
const data = await readFile(TOKEN_FILE, "utf8");
|
|
71
|
+
return JSON.parse(data);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Persist a session token from a device-flow response, converting the relative
|
|
79
|
+
* `expires_in` (seconds) into an absolute `expires_at` ISO timestamp and
|
|
80
|
+
* defaulting `token_type` to `"Bearer"`. Creates the profile directory if
|
|
81
|
+
* needed and writes the file with owner-only permissions.
|
|
82
|
+
*
|
|
83
|
+
* @param token the device-flow token payload.
|
|
84
|
+
* @returns `true` on success, `false` if the write failed.
|
|
85
|
+
*/
|
|
86
|
+
export async function storeServerToken(token: ServerTokenInput): Promise<boolean> {
|
|
87
|
+
try {
|
|
88
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
89
|
+
const tokenData = {
|
|
90
|
+
access_token: token.access_token,
|
|
91
|
+
refresh_token: token.refresh_token,
|
|
92
|
+
token_type: token.token_type ?? "Bearer",
|
|
93
|
+
scope: token.scope,
|
|
94
|
+
expires_at: token.expires_in !== undefined ? new Date(Date.now() + token.expires_in * 1000).toISOString() : null,
|
|
95
|
+
created_at: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
await writeFile(TOKEN_FILE, `${JSON.stringify(tokenData, null, 2)}\n`, {
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
mode: 384,
|
|
100
|
+
});
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error("Failed to store server token:", (error as Error).message);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove the stored session token (used by `indus logout`). A missing file is
|
|
110
|
+
* treated as nothing-to-do.
|
|
111
|
+
*
|
|
112
|
+
* @returns `true` when a file was deleted, `false` when none existed / removal failed.
|
|
113
|
+
*/
|
|
114
|
+
export async function clearServerToken(): Promise<boolean> {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(TOKEN_FILE);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Decide whether a token is expired. A token counts as expired when it is
|
|
125
|
+
* absent, has no `expires_at`, or has under 5 minutes of life remaining (the
|
|
126
|
+
* margin keeps a long turn from racing the expiry boundary).
|
|
127
|
+
*
|
|
128
|
+
* @param token the token to inspect (or `null`).
|
|
129
|
+
* @returns `true` when the token is missing or stale.
|
|
130
|
+
*/
|
|
131
|
+
export function isServerTokenExpired(token: TokenData | null): boolean {
|
|
132
|
+
if (!token || !token.expires_at) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
const expiresAt = new Date(token.expires_at);
|
|
136
|
+
const now = new Date();
|
|
137
|
+
return expiresAt.getTime() - now.getTime() < 5 * 60 * 1000;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convenience predicate: there is a stored token and it is not expired.
|
|
142
|
+
*
|
|
143
|
+
* @returns `true` when a usable (fresh) session token is on disk.
|
|
144
|
+
*/
|
|
145
|
+
export async function hasValidServerToken(): Promise<boolean> {
|
|
146
|
+
const token = await getServerToken();
|
|
147
|
+
return token !== null && !isServerTokenExpired(token);
|
|
148
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot stage pipeline — the ordered list of {@link Stage} transforms that turn a
|
|
3
|
+
* bare {@link BootContext} (argv + workspace + brand) into a fully-resolved one
|
|
4
|
+
* (parsed invocation, materialised directories, applied upgrades, resolved
|
|
5
|
+
* startup resources, selected runner).
|
|
6
|
+
*
|
|
7
|
+
* The pipeline is data, not control flow: {@link STAGES} lists each step in order,
|
|
8
|
+
* and {@link runStages} folds an *immutable* context through them — every stage
|
|
9
|
+
* receives a context and returns its successor (built by spreading, never by
|
|
10
|
+
* mutating). A stage may be async; the fold awaits each in turn so ordering and
|
|
11
|
+
* side-effect sequencing are deterministic.
|
|
12
|
+
*
|
|
13
|
+
* Stage order and intent:
|
|
14
|
+
* 1. `locate-workspace` — materialise the resolved {@link Workspace} on disk
|
|
15
|
+
* (`createWorkspace` + `ensureDirs`). Pure path computation already happened
|
|
16
|
+
* when the initial context was built; this stage only `mkdir`s.
|
|
17
|
+
* 2. `apply-upgrades` — fold the idempotent {@link applyUpgrades} registry
|
|
18
|
+
* over the workspace (a no-op on an already-current profile).
|
|
19
|
+
* 3. `build-invocation` — parse `argv` into the typed {@link Invocation}.
|
|
20
|
+
* 4. `resolve-resources` — best-effort construct the framework
|
|
21
|
+
* settings/auth/model graph; degrade to a minimal object if the framework
|
|
22
|
+
* pieces are unavailable.
|
|
23
|
+
* 5. `select-runner` — no-op transform that exists for symmetry and tracing
|
|
24
|
+
* (the actual dispatch happens in {@link "./boot"} after the pipeline, so the
|
|
25
|
+
* selected runner can own the exit code). Kept in the list so the ordered
|
|
26
|
+
* pipeline reads as the full launch sequence.
|
|
27
|
+
*/
|
|
28
|
+
import { tokenizeInvocation } from "./invocation.js";
|
|
29
|
+
import { applyUpgrades } from "./upgrade/apply.js";
|
|
30
|
+
import { ensureDirs } from "../workspace/locator.js";
|
|
31
|
+
import type { BootContext, Stage } from "./contract.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Materialise the resolved workspace directories on disk.
|
|
35
|
+
*
|
|
36
|
+
* The {@link BootContext.workspace} was already computed (pure) when the initial
|
|
37
|
+
* context was assembled; this stage only ensures the directory subset exists so
|
|
38
|
+
* later stages and runners can write into it. Returns the same context (the
|
|
39
|
+
* workspace record is unchanged — only the filesystem side-effects happen here).
|
|
40
|
+
*/
|
|
41
|
+
const locateWorkspace: Stage = {
|
|
42
|
+
name: "locate-workspace",
|
|
43
|
+
apply(ctx) {
|
|
44
|
+
ensureDirs(ctx.workspace);
|
|
45
|
+
return ctx;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run any pending one-time profile upgrades, idempotently.
|
|
51
|
+
*
|
|
52
|
+
* Folds the upgrade registry over the workspace. The driver is non-fatal: a step
|
|
53
|
+
* that fails is reported and retried on a later launch, never aborting boot. The
|
|
54
|
+
* context is returned unchanged (upgrades touch the filesystem, not the context).
|
|
55
|
+
*/
|
|
56
|
+
const upgrade: Stage = {
|
|
57
|
+
name: "apply-upgrades",
|
|
58
|
+
async apply(ctx) {
|
|
59
|
+
await applyUpgrades(ctx.workspace);
|
|
60
|
+
return ctx;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse `argv` into the typed {@link Invocation} and thread it onto the context.
|
|
66
|
+
*
|
|
67
|
+
* The initial context carries a placeholder invocation (the bootstrapper cannot
|
|
68
|
+
* know the mode before parsing); this stage replaces it with the real parse.
|
|
69
|
+
*/
|
|
70
|
+
const buildInvocation: Stage = {
|
|
71
|
+
name: "build-invocation",
|
|
72
|
+
apply(ctx) {
|
|
73
|
+
const invocation = tokenizeInvocation(ctx.argv);
|
|
74
|
+
return { ...ctx, invocation };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Best-effort assembly of the startup resource graph.
|
|
80
|
+
*
|
|
81
|
+
* Where the rebuilt framework owns a concept, the field is constructed from it:
|
|
82
|
+
* {@link StartupResources.settings} from the framework's `DEFAULT_SETTINGS`, and
|
|
83
|
+
* {@link StartupResources.models} from the framework's `ModelRegistry`. The
|
|
84
|
+
* credential graph has no framework type yet, so it is an empty placeholder until
|
|
85
|
+
* Phase 2. If a framework piece cannot be loaded, the stage degrades to a minimal
|
|
86
|
+
* resources object built from empty literals rather than failing the boot — the
|
|
87
|
+
* shape is what later phases depend on, and they fill it in.
|
|
88
|
+
*/
|
|
89
|
+
const resolveResources: Stage = {
|
|
90
|
+
name: "resolve-resources",
|
|
91
|
+
async apply(ctx) {
|
|
92
|
+
const resources = await buildStartupResources();
|
|
93
|
+
return { ...ctx, resources };
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
async function buildStartupResources(): Promise<{ settings: unknown; auth: Record<string, unknown>; models: unknown }> {
|
|
98
|
+
const settings = await loadDefaultSettings();
|
|
99
|
+
const models = await loadModelRegistry();
|
|
100
|
+
return { settings, auth: {}, models };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadDefaultSettings(): Promise<unknown> {
|
|
104
|
+
try {
|
|
105
|
+
const mod = await import("indusagi/shell-app");
|
|
106
|
+
return (mod as { DEFAULT_SETTINGS?: unknown }).DEFAULT_SETTINGS ?? {};
|
|
107
|
+
} catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadModelRegistry(): Promise<unknown> {
|
|
113
|
+
const mod = (await import("indusagi/ai")) as { ModelRegistry?: unknown; modelRegistry?: unknown };
|
|
114
|
+
if (typeof mod.ModelRegistry === "function") {
|
|
115
|
+
return new (mod.ModelRegistry as new () => unknown)();
|
|
116
|
+
}
|
|
117
|
+
return mod.modelRegistry;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Marker / tracing stage for runner selection.
|
|
122
|
+
*
|
|
123
|
+
* The real dispatch is performed by {@link "./boot"} after the pipeline so the
|
|
124
|
+
* chosen runner can own the process exit code; this stage exists to keep the
|
|
125
|
+
* ordered pipeline a faithful description of the launch sequence and to give the
|
|
126
|
+
* selection step a name for tracing. It returns the context unchanged.
|
|
127
|
+
*/
|
|
128
|
+
const selectRunnerStage: Stage = {
|
|
129
|
+
name: "select-runner",
|
|
130
|
+
apply(ctx) {
|
|
131
|
+
return ctx;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The launch pipeline, in execution order. Folded by {@link runStages}.
|
|
137
|
+
*/
|
|
138
|
+
export const STAGES: readonly Stage[] = [
|
|
139
|
+
locateWorkspace,
|
|
140
|
+
upgrade,
|
|
141
|
+
buildInvocation,
|
|
142
|
+
resolveResources,
|
|
143
|
+
selectRunnerStage,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fold an ordered list of {@link Stage} transforms over an immutable
|
|
148
|
+
* {@link BootContext}.
|
|
149
|
+
*
|
|
150
|
+
* Each stage receives the current context and returns its successor; the result
|
|
151
|
+
* of one stage is the input to the next. Awaits every stage so async ordering is
|
|
152
|
+
* deterministic. The input `initial` is never mutated — stages produce new
|
|
153
|
+
* contexts by spreading.
|
|
154
|
+
*
|
|
155
|
+
* @param initial The seed context (argv + workspace + brand + placeholder fields).
|
|
156
|
+
* @param stages The ordered transforms to apply; defaults to {@link STAGES}.
|
|
157
|
+
* @returns The fully-resolved context after every stage has run.
|
|
158
|
+
*/
|
|
159
|
+
export async function runStages(initial: BootContext, stages: readonly Stage[] = STAGES): Promise<BootContext> {
|
|
160
|
+
let ctx: BootContext = initial;
|
|
161
|
+
for (const stage of stages) {
|
|
162
|
+
ctx = await stage.apply(ctx);
|
|
163
|
+
}
|
|
164
|
+
return ctx;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { locateWorkspace, upgrade, buildInvocation, resolveResources, selectRunnerStage };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade driver — folds the ordered {@link UPGRADES} registry over a
|
|
3
|
+
* {@link Workspace}, exactly once per step.
|
|
4
|
+
*
|
|
5
|
+
* The driver is the only place that knows about the *marker file*: a small JSON
|
|
6
|
+
* record under the profile directory listing the ids of upgrades that have
|
|
7
|
+
* already run. Each registry step is skipped if its id is present in the marker;
|
|
8
|
+
* otherwise it is applied and, on success, its id is appended and the marker is
|
|
9
|
+
* persisted. A step that throws is *not* recorded (so it is retried next launch)
|
|
10
|
+
* and is reported as a warning rather than aborting the remaining steps — one
|
|
11
|
+
* bad migration must never wedge startup.
|
|
12
|
+
*
|
|
13
|
+
* The result is purely informational: {@link UpgradeReport.applied} lists ids run
|
|
14
|
+
* *this* invocation (empty on an already-current profile), and
|
|
15
|
+
* {@link UpgradeReport.warnings} carries human-readable notes for any step that
|
|
16
|
+
* failed. Callers typically log both and continue.
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import type { Workspace } from "../contract.js";
|
|
21
|
+
import { UPGRADES } from "./upgrades.js";
|
|
22
|
+
|
|
23
|
+
const MARKER_FILENAME = ".upgrade-state.json";
|
|
24
|
+
|
|
25
|
+
function markerPath(ws: Workspace): string {
|
|
26
|
+
return join(ws.profileDir, MARKER_FILENAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readAppliedIds(ws: Workspace): Promise<Set<string>> {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await fs.readFile(markerPath(ws), "utf8");
|
|
32
|
+
const parsed = JSON.parse(raw) as { appliedIds?: unknown };
|
|
33
|
+
const ids = Array.isArray(parsed.appliedIds) ? parsed.appliedIds : [];
|
|
34
|
+
return new Set(ids.filter((id): id is string => typeof id === "string"));
|
|
35
|
+
} catch {
|
|
36
|
+
return new Set();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function writeAppliedIds(ws: Workspace, appliedIds: Set<string>): Promise<void> {
|
|
41
|
+
const state = { appliedIds: [...appliedIds] };
|
|
42
|
+
await fs.mkdir(ws.profileDir, { recursive: true });
|
|
43
|
+
await fs.writeFile(markerPath(ws), `${JSON.stringify(state, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function describeError(error: unknown): string {
|
|
47
|
+
if (error instanceof Error) return error.message;
|
|
48
|
+
return String(error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The outcome of an {@link applyUpgrades} pass.
|
|
53
|
+
*
|
|
54
|
+
* - `applied` — ids of upgrades that ran successfully *this* invocation, in
|
|
55
|
+
* apply order. Empty when the profile was already current.
|
|
56
|
+
* - `warnings` — one entry per step that threw, naming the step and its error.
|
|
57
|
+
* Non-fatal: applying continues past a failed step.
|
|
58
|
+
*/
|
|
59
|
+
export interface UpgradeReport {
|
|
60
|
+
/** Ids successfully applied during this call, in order. */
|
|
61
|
+
readonly applied: string[];
|
|
62
|
+
/** Human-readable notes for steps that failed (non-fatal). */
|
|
63
|
+
readonly warnings: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply every not-yet-applied upgrade in {@link UPGRADES}, in registry order,
|
|
68
|
+
* recording each success in the marker file so it never runs again.
|
|
69
|
+
*
|
|
70
|
+
* Steps already named in the marker are skipped. A step that throws is recorded
|
|
71
|
+
* as a warning, left unmarked (so it is retried on a later launch), and does not
|
|
72
|
+
* block the remaining steps. The marker is rewritten after each success, so a
|
|
73
|
+
* crash mid-pass still preserves the progress made so far.
|
|
74
|
+
*
|
|
75
|
+
* @param ws The resolved, absolute on-disk layout to upgrade.
|
|
76
|
+
* @returns The ids applied this pass and any non-fatal warnings.
|
|
77
|
+
*/
|
|
78
|
+
export async function applyUpgrades(ws: Workspace): Promise<UpgradeReport> {
|
|
79
|
+
const alreadyApplied = await readAppliedIds(ws);
|
|
80
|
+
const applied: string[] = [];
|
|
81
|
+
const warnings: string[] = [];
|
|
82
|
+
for (const upgrade of UPGRADES) {
|
|
83
|
+
if (alreadyApplied.has(upgrade.id)) continue;
|
|
84
|
+
try {
|
|
85
|
+
await upgrade.apply(ws);
|
|
86
|
+
alreadyApplied.add(upgrade.id);
|
|
87
|
+
applied.push(upgrade.id);
|
|
88
|
+
await writeAppliedIds(ws, alreadyApplied);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
warnings.push(`upgrade "${upgrade.id}" failed: ${describeError(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { applied, warnings };
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade subsystem — public barrel.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the ordered, idempotent profile-upgrade registry and its driver.
|
|
5
|
+
* Boot consumers import {@link applyUpgrades} to run pending one-time migrations
|
|
6
|
+
* and the {@link UPGRADES} registry / {@link Upgrade} type for inspection and
|
|
7
|
+
* testing. The marker-file bookkeeping is an internal detail of the driver and
|
|
8
|
+
* is not re-exported.
|
|
9
|
+
*/
|
|
10
|
+
export type { Upgrade } from "./upgrades.js";
|
|
11
|
+
export { UPGRADES, foldCredentials, reshelveTranscripts, relocateBinaries, renamePromptDir, projectTranscriptDirName } from "./upgrades.js";
|
|
12
|
+
export type { UpgradeReport } from "./apply.js";
|
|
13
|
+
export { applyUpgrades } from "./apply.js";
|