@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.1
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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yagni — the thin launcher (package `yagni-code`, command `yagni`).
|
|
4
|
+
*
|
|
5
|
+
* yagni login device-code auth, stores a scoped token
|
|
6
|
+
* yagni logout revoke + delete the local token
|
|
7
|
+
* yagni doctor green/red readiness checklist
|
|
8
|
+
* yagni [pi args…] launch pi configured with the yagni provider +
|
|
9
|
+
* the ask_yagni tool (default command)
|
|
10
|
+
*
|
|
11
|
+
* The launcher holds NO model or business logic — it is credential plumbing
|
|
12
|
+
* plus a configured pi spawn. Everything that makes this "YAGNI Code" lives in
|
|
13
|
+
* pi-extension-yagni and the YAGNI backend.
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { mkdirSync, realpathSync } from "node:fs";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { PI_CONFIG_NAME } from "./branding.js";
|
|
20
|
+
import { claudeCompatArgs } from "./claudeCompat.js";
|
|
21
|
+
import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
|
|
22
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
23
|
+
import { login } from "./login.js";
|
|
24
|
+
import { logout } from "./logout.js";
|
|
25
|
+
import { buildLaunch } from "./launch.js";
|
|
26
|
+
import { runDoctor } from "./doctor.js";
|
|
27
|
+
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
28
|
+
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
29
|
+
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
30
|
+
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
31
|
+
import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
|
|
32
|
+
// Present as "yagni" in process listings, not "node".
|
|
33
|
+
process.title = DISTRIBUTION.commandName;
|
|
34
|
+
/** The one-time Claude-compat trust question, asked on the launch TTY. */
|
|
35
|
+
async function confirmOnTty(question) {
|
|
36
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
37
|
+
try {
|
|
38
|
+
const answer = await rl.question(`${question} [Y/n] `);
|
|
39
|
+
return !/^n/i.test(answer.trim());
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rl.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function runDefault(passthroughArgs) {
|
|
46
|
+
// Cache-backed update nudge (never a network wait), then a background cache
|
|
47
|
+
// refresh that completes while the session runs. Both fail soft.
|
|
48
|
+
await maybeNudgeAndRefresh({ current: cliVersion() });
|
|
49
|
+
// Load the active environment's credentials (base URL respects a per-run
|
|
50
|
+
// YAGNI_BASE_URL override). The token must belong to this environment.
|
|
51
|
+
const profile = await readActiveProfile();
|
|
52
|
+
let creds = credentialsFromProfile(profile);
|
|
53
|
+
if (!creds?.token) {
|
|
54
|
+
process.stderr.write(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
// Refresh-at-launch: if the token is inside the 7-day refresh window (and not
|
|
58
|
+
// yet expired), rotate it before spawning so a long session doesn't 401
|
|
59
|
+
// mid-flight. Fail-soft: a refresh failure keeps the current token and warns.
|
|
60
|
+
// Persist ONLY the token rotation — never the (possibly YAGNI_BASE_URL-
|
|
61
|
+
// overridden) base URL, so a single-run override can't become sticky.
|
|
62
|
+
const refresh = await maybeRefreshAtLaunch(creds, {
|
|
63
|
+
persist: (c) => persistProfileTokenRotation(profile.name, c),
|
|
64
|
+
});
|
|
65
|
+
for (const warning of refresh.warnings) {
|
|
66
|
+
process.stderr.write(`${warning}\n`);
|
|
67
|
+
}
|
|
68
|
+
creds = refresh.creds;
|
|
69
|
+
// Hermetic config dir, scoped per environment — created up front (0700) so pi
|
|
70
|
+
// reads/writes its settings, auth, model scope, and theme here (per profile)
|
|
71
|
+
// instead of ~/.pi/agent, and prod state never bleeds into staging.
|
|
72
|
+
const piAgentDir = agentDir(profile.name);
|
|
73
|
+
mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
|
|
74
|
+
// Generate the shadow pi package so the terminal title/process name read
|
|
75
|
+
// "YAGNI Code" instead of "pi"/"π". Best-effort: if it can't be built we
|
|
76
|
+
// still launch (un-rebranded but hermetic), never blocking the agent.
|
|
77
|
+
let shadowPiDir;
|
|
78
|
+
try {
|
|
79
|
+
shadowPiDir = ensureShadowPiPackage({
|
|
80
|
+
realPiDir: resolvePiPackageDir(),
|
|
81
|
+
shadowDir: piPackageDir(),
|
|
82
|
+
name: PI_CONFIG_NAME,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
shadowPiDir = undefined;
|
|
87
|
+
}
|
|
88
|
+
// Zero-config Claude Code compat: wire this repo's (and the user's)
|
|
89
|
+
// .claude/skills and .claude/commands into the agent, asking the one-time
|
|
90
|
+
// per-folder trust question when needed. Fail-soft: compat can only add
|
|
91
|
+
// flags, never block a launch.
|
|
92
|
+
let compatArgs = [];
|
|
93
|
+
try {
|
|
94
|
+
compatArgs = await claudeCompatArgs({
|
|
95
|
+
cwd: process.cwd(),
|
|
96
|
+
agentDir: piAgentDir,
|
|
97
|
+
confirm: confirmOnTty,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
compatArgs = [];
|
|
102
|
+
}
|
|
103
|
+
// buildLaunch runs the token-expiry preflight: it throws (with an actionable
|
|
104
|
+
// login prompt) on an already-expired token so we never spawn a session that
|
|
105
|
+
// immediately 401s, and returns non-fatal warnings (e.g. expiry approaching).
|
|
106
|
+
// `creds` was read fresh from the profile file above, so an out-of-band
|
|
107
|
+
// re-login in another terminal is picked up here with no stale cached token.
|
|
108
|
+
let plan;
|
|
109
|
+
try {
|
|
110
|
+
plan = buildLaunch(creds, passthroughArgs, {
|
|
111
|
+
extensionPath: resolveExtensionPath(),
|
|
112
|
+
agentDir: piAgentDir,
|
|
113
|
+
piPackageDir: shadowPiDir,
|
|
114
|
+
extraAgentArgs: compatArgs,
|
|
115
|
+
// Forward the active profile's file path so the extension can persist a
|
|
116
|
+
// mid-session token rotation back to the same profile the launcher read.
|
|
117
|
+
profilePath: profilePath(profile.name),
|
|
118
|
+
stateDir: credentialsDir(),
|
|
119
|
+
baseEnv: process.env,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
for (const warning of plan.warnings) {
|
|
127
|
+
process.stderr.write(`${warning}\n`);
|
|
128
|
+
}
|
|
129
|
+
const { env, argv } = plan;
|
|
130
|
+
const piCli = resolvePiCliPath();
|
|
131
|
+
return await new Promise((resolve) => {
|
|
132
|
+
const child = spawn(process.execPath, [piCli, ...argv], {
|
|
133
|
+
stdio: "inherit",
|
|
134
|
+
env,
|
|
135
|
+
});
|
|
136
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
137
|
+
child.on("error", (err) => {
|
|
138
|
+
process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
|
|
139
|
+
resolve(1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export const HELP_TEXT = [
|
|
144
|
+
"YAGNI Code — a business-context-grounded terminal coding agent.",
|
|
145
|
+
"",
|
|
146
|
+
"Usage:",
|
|
147
|
+
" yagni [args…] Launch the agent in the current repo.",
|
|
148
|
+
" yagni -c Continue the most recent session.",
|
|
149
|
+
" yagni -r Browse and resume a previous session.",
|
|
150
|
+
' yagni -p "prompt" Print one response and exit (reads piped stdin too).',
|
|
151
|
+
" yagni login Authorize the active environment (device-code flow).",
|
|
152
|
+
" yagni logout Revoke and clear the active environment's token.",
|
|
153
|
+
" yagni doctor Check that everything is ready (green/red checklist).",
|
|
154
|
+
" yagni use <name> Switch the active environment (sticky).",
|
|
155
|
+
" Presets: prod, local. Others need --base-url <url>.",
|
|
156
|
+
" yagni profiles List saved environments; the active one is marked.",
|
|
157
|
+
" yagni upgrade [x.y.z] Upgrade the CLI to the latest (or a given) version.",
|
|
158
|
+
" yagni version Print the CLI version.",
|
|
159
|
+
"",
|
|
160
|
+
"Common agent flags (passed straight through):",
|
|
161
|
+
" --model <tier> Pick the model tier (balanced by default).",
|
|
162
|
+
" --thinking <level> off | minimal | low | medium | high | xhigh | max",
|
|
163
|
+
" --session <id> Open a specific session; --fork <id> branches one.",
|
|
164
|
+
" --mode json Emit machine-readable events (for scripts and CI).",
|
|
165
|
+
"",
|
|
166
|
+
"In a session:",
|
|
167
|
+
" /name Runs this repo's .claude/commands/<name>.md template.",
|
|
168
|
+
" /mode plan|review|auto Hold writes for planning, or confirm each change.",
|
|
169
|
+
" /todos Show the agent's live task list.",
|
|
170
|
+
" /cost Session usage and credit headroom.",
|
|
171
|
+
"",
|
|
172
|
+
"The active environment is sticky; `use` switches it (prod is the default).",
|
|
173
|
+
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
174
|
+
"Set YAGNI_DISABLE_UPDATE_CHECK=1 to silence the new-version notice.",
|
|
175
|
+
"Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to skip loading .claude assets.",
|
|
176
|
+
].join("\n");
|
|
177
|
+
/** Parse `use <name> [--base-url <url>]` argv into its parts. */
|
|
178
|
+
export function parseUseArgs(args) {
|
|
179
|
+
let name;
|
|
180
|
+
let baseUrl;
|
|
181
|
+
for (let i = 0; i < args.length; i++) {
|
|
182
|
+
const a = args[i];
|
|
183
|
+
if (a === "--base-url") {
|
|
184
|
+
baseUrl = args[++i];
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (a.startsWith("--base-url=")) {
|
|
188
|
+
baseUrl = a.slice("--base-url=".length);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!a.startsWith("-") && name === undefined)
|
|
192
|
+
name = a;
|
|
193
|
+
}
|
|
194
|
+
return { name, baseUrl };
|
|
195
|
+
}
|
|
196
|
+
/** `yagni use <name> [--base-url <url>]` — switch the sticky environment. */
|
|
197
|
+
export async function useCommand(args) {
|
|
198
|
+
const { name, baseUrl } = parseUseArgs(args);
|
|
199
|
+
if (!name) {
|
|
200
|
+
process.stderr.write("Usage: yagni use <name> [--base-url <url>]\n");
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const profile = await useProfile(name, { baseUrl });
|
|
205
|
+
const hint = profile.token ? "" : " — run `yagni login` to authorize it";
|
|
206
|
+
process.stdout.write(`Active environment: ${profile.name} → ${profile.baseUrl}${hint}\n`);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** `yagni profiles` — list saved environments, marking the active one. */
|
|
215
|
+
export async function profilesCommand() {
|
|
216
|
+
const active = await getActiveProfileName();
|
|
217
|
+
const stored = await listProfiles();
|
|
218
|
+
const rows = [...stored];
|
|
219
|
+
// Surface the active environment even if it has no saved file yet.
|
|
220
|
+
if (!rows.some((p) => p.name === active))
|
|
221
|
+
rows.push(await readActiveProfile());
|
|
222
|
+
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
223
|
+
for (const p of rows) {
|
|
224
|
+
const marker = p.name === active ? "*" : " ";
|
|
225
|
+
const status = p.token ? "logged in" : "not logged in";
|
|
226
|
+
process.stdout.write(`${marker} ${p.name.padEnd(12)} ${p.baseUrl} (${status})\n`);
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Whether to show YAGNI Code's own help instead of launching the agent.
|
|
232
|
+
*
|
|
233
|
+
* `--help`/`-h` are intercepted ANYWHERE in argv (not just position 0) so the
|
|
234
|
+
* underlying agent's branded help never leaks (e.g. `yagni <flag> --help`).
|
|
235
|
+
*/
|
|
236
|
+
export function wantsHelp(argv) {
|
|
237
|
+
return argv[0] === "help" || argv.includes("--help") || argv.includes("-h");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether to print YAGNI Code's own version instead of launching the agent.
|
|
241
|
+
*
|
|
242
|
+
* `--version` AND `-v` are intercepted ANYWHERE in argv for the same reason as
|
|
243
|
+
* `--help`: pi parses both as its version flag, so letting either through
|
|
244
|
+
* leaks the agent's branded version line (or a login error before it).
|
|
245
|
+
*/
|
|
246
|
+
export function wantsVersion(argv) {
|
|
247
|
+
return argv[0] === "version" || argv.includes("--version") || argv.includes("-v");
|
|
248
|
+
}
|
|
249
|
+
/** The CLI's own version (see `currentCliVersion` — upgrade.ts owns it). */
|
|
250
|
+
export function cliVersion() {
|
|
251
|
+
return currentCliVersion();
|
|
252
|
+
}
|
|
253
|
+
export async function main(argv) {
|
|
254
|
+
const [command, ...rest] = argv;
|
|
255
|
+
// Help never touches storage; everything else first adopts any pre-profiles
|
|
256
|
+
// credentials.json as a named profile (one-shot, no-op once migrated).
|
|
257
|
+
if (wantsHelp(argv)) {
|
|
258
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
// Bare semver on stdout (script-friendly); like help, never touches storage.
|
|
262
|
+
if (wantsVersion(argv)) {
|
|
263
|
+
process.stdout.write(`${cliVersion()}\n`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
await migrateLegacyCredentials();
|
|
267
|
+
// login/logout act on the active environment; use/profiles manage which one
|
|
268
|
+
// that is. Anything else is passed straight through to the agent.
|
|
269
|
+
if (command === "login") {
|
|
270
|
+
const profile = await readActiveProfile();
|
|
271
|
+
await login({ baseUrl: profile.baseUrl, profileName: profile.name });
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
if (command === "logout") {
|
|
275
|
+
await logout();
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
if (command === "doctor") {
|
|
279
|
+
return runDoctor();
|
|
280
|
+
}
|
|
281
|
+
if (command === "upgrade") {
|
|
282
|
+
return upgradeCommand(rest, { current: cliVersion() });
|
|
283
|
+
}
|
|
284
|
+
if (command === "use") {
|
|
285
|
+
return useCommand(rest);
|
|
286
|
+
}
|
|
287
|
+
if (command === "profiles") {
|
|
288
|
+
return profilesCommand();
|
|
289
|
+
}
|
|
290
|
+
return runDefault(command === undefined ? [] : [command, ...rest]);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Whether this module is the process entrypoint (vs. imported by a test).
|
|
294
|
+
*
|
|
295
|
+
* Symlink-safe: when `yagni-code` is installed globally, the shell runs it
|
|
296
|
+
* through a symlink in the bin dir, so `process.argv[1]` is that symlink path.
|
|
297
|
+
* Node resolves symlinks for `import.meta.url`, so a raw `argv[1] === metaPath`
|
|
298
|
+
* compare is ALWAYS false for a symlinked install and `main()` never runs (the
|
|
299
|
+
* "nothing happens" bug). We realpath both sides so the symlinked bin and a
|
|
300
|
+
* direct `node dist/cli.js` invocation both count as the entrypoint, while a
|
|
301
|
+
* test import (argv[1] points at the test runner) does not.
|
|
302
|
+
*/
|
|
303
|
+
export function isEntrypoint(argv1, moduleUrl) {
|
|
304
|
+
if (argv1 === undefined)
|
|
305
|
+
return false;
|
|
306
|
+
const resolve = (p) => {
|
|
307
|
+
try {
|
|
308
|
+
return realpathSync(p);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return p; // path may not exist on disk; fall back to the raw value
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
return resolve(argv1) === resolve(fileURLToPath(moduleUrl));
|
|
315
|
+
}
|
|
316
|
+
// Only auto-run when invoked as the CLI entry, so tests can import this module
|
|
317
|
+
// (e.g. to exercise wantsHelp) without spawning the agent.
|
|
318
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
319
|
+
main(process.argv.slice(2))
|
|
320
|
+
.then((code) => process.exit(code))
|
|
321
|
+
.catch((err) => {
|
|
322
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Shared CLI config: where the YAGNI backend lives + the environment presets. */
|
|
2
|
+
export declare const DEFAULT_BASE_URL: string;
|
|
3
|
+
/**
|
|
4
|
+
* Built-in environment presets. These only SEED a profile's base URL when you
|
|
5
|
+
* `yagni use <name>` without `--base-url`; once stored, the profile's own
|
|
6
|
+
* baseUrl wins. `prod` is the default profile; `local` points at the dev backend
|
|
7
|
+
* (`pnpm dev` serves /api + /v1 on 3456). Any other environment is user-defined
|
|
8
|
+
* (pass `--base-url`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const PRESET_BASE_URLS: Record<string, string>;
|
|
11
|
+
/** The base URL a named preset resolves to, or undefined for a custom name. */
|
|
12
|
+
export declare function presetBaseUrl(name: string): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The YAGNI_BASE_URL env override (back-compat / per-run escape hatch). Profiles
|
|
15
|
+
* are the primary mechanism now; this still wins when set so a one-off run can
|
|
16
|
+
* target an ad-hoc host without flipping the active profile.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
19
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Shared CLI config: where the YAGNI backend lives + the environment presets. */
|
|
2
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
3
|
+
export const DEFAULT_BASE_URL = DISTRIBUTION.defaultBaseUrl;
|
|
4
|
+
/**
|
|
5
|
+
* Built-in environment presets. These only SEED a profile's base URL when you
|
|
6
|
+
* `yagni use <name>` without `--base-url`; once stored, the profile's own
|
|
7
|
+
* baseUrl wins. `prod` is the default profile; `local` points at the dev backend
|
|
8
|
+
* (`pnpm dev` serves /api + /v1 on 3456). Any other environment is user-defined
|
|
9
|
+
* (pass `--base-url`).
|
|
10
|
+
*/
|
|
11
|
+
export const PRESET_BASE_URLS = {
|
|
12
|
+
prod: "https://yagni.app",
|
|
13
|
+
staging: "https://yagni-staging.app",
|
|
14
|
+
local: "http://localhost:3456",
|
|
15
|
+
};
|
|
16
|
+
/** The base URL a named preset resolves to, or undefined for a custom name. */
|
|
17
|
+
export function presetBaseUrl(name) {
|
|
18
|
+
return PRESET_BASE_URLS[name];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The YAGNI_BASE_URL env override (back-compat / per-run escape hatch). Profiles
|
|
22
|
+
* are the primary mechanism now; this still wins when set so a one-off run can
|
|
23
|
+
* target an ad-hoc host without flipping the active profile.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveBaseUrl(env = process.env) {
|
|
26
|
+
return (env.YAGNI_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage for the yagni-code CLI: `~/.yagni-code/credentials.json`,
|
|
3
|
+
* written 0600 (owner read/write only) since it holds a live API token.
|
|
4
|
+
*/
|
|
5
|
+
export interface Credentials {
|
|
6
|
+
token: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
workspaceId: string;
|
|
9
|
+
expiresAt?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function _setYagniCodeHomeForTest(dir: string | null): void;
|
|
12
|
+
export declare function credentialsDir(): string;
|
|
13
|
+
export declare function credentialsPath(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Isolated pi config/state dir for YAGNI Code: `~/.yagni-code/agent/<profile>`.
|
|
16
|
+
*
|
|
17
|
+
* The launcher pins pi's `PI_CODING_AGENT_DIR` here so YAGNI Code never reads
|
|
18
|
+
* the user's machine-wide `~/.pi/agent` — keeping its own providers (only
|
|
19
|
+
* `yagni`), stored auth, model scope (the backend catalog), and theme out of
|
|
20
|
+
* the agent. Everything routes through the YAGNI proxy; the user's local
|
|
21
|
+
* Together/OpenRouter keys and saved default model cannot bleed in or bypass it.
|
|
22
|
+
*
|
|
23
|
+
* Scoping the dir per profile keeps each environment's pi state (session
|
|
24
|
+
* history, theme, settings) separate, so pointing at staging never bleeds prod
|
|
25
|
+
* state. Called with no profile it returns the legacy un-scoped dir.
|
|
26
|
+
*/
|
|
27
|
+
export declare function agentDir(profileName?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Hermetic shadow pi package dir: `~/.yagni-code/pi-package`.
|
|
30
|
+
*
|
|
31
|
+
* Holds a generated `package.json` (pi's, with `piConfig.name` set so the
|
|
32
|
+
* terminal title/process name read "YAGNI Code" instead of "pi"/"π") plus
|
|
33
|
+
* symlinks back to the real pi package's assets. The launcher pins pi's
|
|
34
|
+
* `PI_PACKAGE_DIR` here. Kept under the YAGNI-owned dir so it never collides
|
|
35
|
+
* with the user's machine-wide pi install.
|
|
36
|
+
*/
|
|
37
|
+
export declare function piPackageDir(): string;
|
|
38
|
+
export declare function writeCredentials(creds: Credentials, path?: string): Promise<void>;
|
|
39
|
+
export declare function readCredentials(path?: string): Promise<Credentials | null>;
|
|
40
|
+
export declare function deleteCredentials(path?: string): Promise<void>;
|
|
41
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage for the yagni-code CLI: `~/.yagni-code/credentials.json`,
|
|
3
|
+
* written 0600 (owner read/write only) since it holds a live API token.
|
|
4
|
+
*/
|
|
5
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
9
|
+
/**
|
|
10
|
+
* Test seam: when set, all path resolution roots here instead of
|
|
11
|
+
* `~/.yagni-code`, so tests can exercise profile/credential storage against a
|
|
12
|
+
* tmpdir without touching the real home dir. Mirrors the `_setTestPool` style
|
|
13
|
+
* used elsewhere in the repo (no public env var). Pass `null` to restore.
|
|
14
|
+
*/
|
|
15
|
+
let homeOverride = null;
|
|
16
|
+
export function _setYagniCodeHomeForTest(dir) {
|
|
17
|
+
homeOverride = dir;
|
|
18
|
+
}
|
|
19
|
+
export function credentialsDir() {
|
|
20
|
+
return homeOverride ?? join(homedir(), DISTRIBUTION.stateDirName);
|
|
21
|
+
}
|
|
22
|
+
export function credentialsPath() {
|
|
23
|
+
return join(credentialsDir(), "credentials.json");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Isolated pi config/state dir for YAGNI Code: `~/.yagni-code/agent/<profile>`.
|
|
27
|
+
*
|
|
28
|
+
* The launcher pins pi's `PI_CODING_AGENT_DIR` here so YAGNI Code never reads
|
|
29
|
+
* the user's machine-wide `~/.pi/agent` — keeping its own providers (only
|
|
30
|
+
* `yagni`), stored auth, model scope (the backend catalog), and theme out of
|
|
31
|
+
* the agent. Everything routes through the YAGNI proxy; the user's local
|
|
32
|
+
* Together/OpenRouter keys and saved default model cannot bleed in or bypass it.
|
|
33
|
+
*
|
|
34
|
+
* Scoping the dir per profile keeps each environment's pi state (session
|
|
35
|
+
* history, theme, settings) separate, so pointing at staging never bleeds prod
|
|
36
|
+
* state. Called with no profile it returns the legacy un-scoped dir.
|
|
37
|
+
*/
|
|
38
|
+
export function agentDir(profileName) {
|
|
39
|
+
const base = join(credentialsDir(), "agent");
|
|
40
|
+
return profileName ? join(base, profileName) : base;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Hermetic shadow pi package dir: `~/.yagni-code/pi-package`.
|
|
44
|
+
*
|
|
45
|
+
* Holds a generated `package.json` (pi's, with `piConfig.name` set so the
|
|
46
|
+
* terminal title/process name read "YAGNI Code" instead of "pi"/"π") plus
|
|
47
|
+
* symlinks back to the real pi package's assets. The launcher pins pi's
|
|
48
|
+
* `PI_PACKAGE_DIR` here. Kept under the YAGNI-owned dir so it never collides
|
|
49
|
+
* with the user's machine-wide pi install.
|
|
50
|
+
*/
|
|
51
|
+
export function piPackageDir() {
|
|
52
|
+
return join(credentialsDir(), "pi-package");
|
|
53
|
+
}
|
|
54
|
+
export async function writeCredentials(creds, path = credentialsPath()) {
|
|
55
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
56
|
+
await writeFile(path, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
+
// writeFile honors mode only on create; chmod guarantees 0600 on overwrite too.
|
|
58
|
+
await chmod(path, 0o600);
|
|
59
|
+
}
|
|
60
|
+
export async function readCredentials(path = credentialsPath()) {
|
|
61
|
+
try {
|
|
62
|
+
const raw = await readFile(path, "utf8");
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (!parsed || typeof parsed.token !== "string")
|
|
65
|
+
return null;
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function deleteCredentials(path = credentialsPath()) {
|
|
73
|
+
await rm(path, { force: true });
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DistributionChannel = "production" | "staging";
|
|
2
|
+
export interface CodeDistribution {
|
|
3
|
+
channel: DistributionChannel;
|
|
4
|
+
packageName: string;
|
|
5
|
+
commandName: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
defaultBaseUrl: string;
|
|
8
|
+
defaultProfile: string;
|
|
9
|
+
stateDirName: string;
|
|
10
|
+
updateTag: "latest" | "staging";
|
|
11
|
+
}
|
|
12
|
+
export declare function distributionForPackageName(packageName: string): CodeDistribution;
|
|
13
|
+
export declare function resolveDistribution(env?: NodeJS.ProcessEnv, packageName?: string): CodeDistribution;
|
|
14
|
+
export declare const DISTRIBUTION: CodeDistribution;
|
|
15
|
+
//# sourceMappingURL=distribution.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const PRODUCTION = {
|
|
5
|
+
channel: "production",
|
|
6
|
+
packageName: "@yagni-app/code",
|
|
7
|
+
commandName: "yagni",
|
|
8
|
+
displayName: "YAGNI Code",
|
|
9
|
+
defaultBaseUrl: "https://yagni.app",
|
|
10
|
+
defaultProfile: "prod",
|
|
11
|
+
stateDirName: ".yagni-code",
|
|
12
|
+
updateTag: "latest",
|
|
13
|
+
};
|
|
14
|
+
const STAGING = {
|
|
15
|
+
channel: "staging",
|
|
16
|
+
packageName: "@yagni-app/code-staging",
|
|
17
|
+
commandName: "yagni-staging",
|
|
18
|
+
displayName: "YAGNI Code Staging",
|
|
19
|
+
defaultBaseUrl: "https://yagni-staging.app",
|
|
20
|
+
defaultProfile: "staging",
|
|
21
|
+
stateDirName: ".yagni-code-staging",
|
|
22
|
+
updateTag: "staging",
|
|
23
|
+
};
|
|
24
|
+
export function distributionForPackageName(packageName) {
|
|
25
|
+
return packageName === STAGING.packageName ? { ...STAGING } : { ...PRODUCTION };
|
|
26
|
+
}
|
|
27
|
+
function installedPackageName() {
|
|
28
|
+
try {
|
|
29
|
+
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
30
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
return parsed.name ?? PRODUCTION.packageName;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return PRODUCTION.packageName;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function resolveDistribution(env = process.env, packageName = installedPackageName()) {
|
|
38
|
+
if (env.YAGNI_CODE_DISTRIBUTION === "staging")
|
|
39
|
+
return { ...STAGING };
|
|
40
|
+
if (env.YAGNI_CODE_DISTRIBUTION === "production")
|
|
41
|
+
return { ...PRODUCTION };
|
|
42
|
+
return distributionForPackageName(packageName);
|
|
43
|
+
}
|
|
44
|
+
export const DISTRIBUTION = resolveDistribution();
|
|
45
|
+
//# sourceMappingURL=distribution.js.map
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni doctor` — a green/red readiness checklist.
|
|
3
|
+
*
|
|
4
|
+
* Diagnoses whether a `yagni` launch will actually work: the pi engine is built,
|
|
5
|
+
* the extension is built, the active profile has a live (unexpired) token, the
|
|
6
|
+
* backend is reachable with YAGNI Code enabled, the state dir is locked down, and
|
|
7
|
+
* `gh` is available for `/go --pr`. Every failing/soft check carries a one-line
|
|
8
|
+
* actionable fix.
|
|
9
|
+
*
|
|
10
|
+
* The check LOGIC is pure (given probe inputs) so it is fully unit-testable; the
|
|
11
|
+
* impure probes (fs, fetch, profile read) are injectable seams. `runDoctor`
|
|
12
|
+
* returns the process exit code: 0 when all REQUIRED checks pass, 1 otherwise.
|
|
13
|
+
* Advisory checks (loose perms, missing `gh`) never flip the exit code.
|
|
14
|
+
*/
|
|
15
|
+
import { type TokenExpiryStatus } from "./launch.js";
|
|
16
|
+
import { type Profile } from "./profiles.js";
|
|
17
|
+
export type CheckStatus = "ok" | "warn" | "fail";
|
|
18
|
+
export interface CheckResult {
|
|
19
|
+
/** Short column label, e.g. "pi engine". */
|
|
20
|
+
name: string;
|
|
21
|
+
status: CheckStatus;
|
|
22
|
+
/** One-line current state. */
|
|
23
|
+
detail: string;
|
|
24
|
+
/** Actionable fix, shown when the check is not `ok`. */
|
|
25
|
+
hint?: string;
|
|
26
|
+
/** Only `required` failures flip the exit code to 1. */
|
|
27
|
+
required: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface PiEngineProbe {
|
|
30
|
+
binPath?: string;
|
|
31
|
+
binExists: boolean;
|
|
32
|
+
version?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ExtensionProbe {
|
|
35
|
+
path?: string;
|
|
36
|
+
exists: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface StateDirProbe {
|
|
39
|
+
path: string;
|
|
40
|
+
exists: boolean;
|
|
41
|
+
/** The dir's permission bits (mode & 0o777), or null when it doesn't exist. */
|
|
42
|
+
mode: number | null;
|
|
43
|
+
}
|
|
44
|
+
export type BackendProbe = {
|
|
45
|
+
kind: "skipped";
|
|
46
|
+
} | {
|
|
47
|
+
kind: "status";
|
|
48
|
+
status: number;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "network";
|
|
51
|
+
};
|
|
52
|
+
export declare function checkPiEngine(probe: PiEngineProbe): CheckResult;
|
|
53
|
+
export declare function checkExtension(probe: ExtensionProbe): CheckResult;
|
|
54
|
+
export declare function checkProfileToken(profile: Pick<Profile, "name" | "token">): CheckResult;
|
|
55
|
+
export declare function checkTokenExpiry(status: TokenExpiryStatus): CheckResult;
|
|
56
|
+
export declare function checkBackend(probe: BackendProbe): CheckResult;
|
|
57
|
+
export declare function checkStateDir(probe: StateDirProbe): CheckResult;
|
|
58
|
+
export declare function checkCliUpdate(probe: {
|
|
59
|
+
current: string;
|
|
60
|
+
latest: string | null;
|
|
61
|
+
}): CheckResult;
|
|
62
|
+
export declare function checkGh(onPath: boolean): CheckResult;
|
|
63
|
+
export interface DoctorReport {
|
|
64
|
+
checks: CheckResult[];
|
|
65
|
+
exitCode: number;
|
|
66
|
+
}
|
|
67
|
+
/** Exit 1 iff any REQUIRED check failed; advisory warns never flip it. */
|
|
68
|
+
export declare function buildDoctorReport(checks: CheckResult[]): DoctorReport;
|
|
69
|
+
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
70
|
+
export interface DoctorDeps {
|
|
71
|
+
now?: () => number;
|
|
72
|
+
probePiEngine?: () => PiEngineProbe;
|
|
73
|
+
probeExtension?: () => ExtensionProbe;
|
|
74
|
+
readActiveProfile?: () => Promise<Profile>;
|
|
75
|
+
probeBackend?: (baseUrl: string, token: string) => Promise<BackendProbe>;
|
|
76
|
+
probeStateDir?: () => StateDirProbe;
|
|
77
|
+
ghOnPath?: () => boolean;
|
|
78
|
+
currentVersion?: string;
|
|
79
|
+
probeLatestVersion?: () => Promise<string | null>;
|
|
80
|
+
log?: (msg: string) => void;
|
|
81
|
+
}
|
|
82
|
+
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
|
83
|
+
export declare function ghOnPathDefault(env?: NodeJS.ProcessEnv): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Gather every check result against the (injectable) probes. Pure ordering; each
|
|
86
|
+
* individual check is a pure function of its probe.
|
|
87
|
+
*/
|
|
88
|
+
export declare function gatherChecks(deps?: DoctorDeps): Promise<CheckResult[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Run the full doctor flow: gather → assemble → print → return the exit code.
|
|
91
|
+
* Never throws (a probe failure degrades to a failing/soft check, not a crash).
|
|
92
|
+
*/
|
|
93
|
+
export declare function runDoctor(deps?: DoctorDeps): Promise<number>;
|
|
94
|
+
//# sourceMappingURL=doctor.d.ts.map
|