@yagni-app/code-staging 0.0.0 → 0.1.0-staging.998.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/launch.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the configured pi launch. `buildLaunch` is pure (no spawn, no fs) so it
|
|
3
|
+
* is fully unit-testable: it returns the child environment and the pi argv that
|
|
4
|
+
* registers pi-extension-yagni and defaults the provider to `yagni`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Credentials } from "./credentials.js";
|
|
7
|
+
export interface LaunchPlan {
|
|
8
|
+
env: NodeJS.ProcessEnv;
|
|
9
|
+
argv: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Non-fatal launch-time notices for the caller to surface (e.g. an
|
|
12
|
+
* expiry-approaching warning). Empty when there is nothing to say.
|
|
13
|
+
*/
|
|
14
|
+
warnings: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How close to expiry the token can be before launch warns. A coding session
|
|
18
|
+
* easily outlives a token, so warn early enough that re-logging in before a long
|
|
19
|
+
* task is worthwhile, without nagging on every launch.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EXPIRY_WARNING_THRESHOLD_MS: number;
|
|
22
|
+
export type TokenExpiryStatus = {
|
|
23
|
+
kind: "unknown";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "valid";
|
|
26
|
+
} | {
|
|
27
|
+
kind: "approaching";
|
|
28
|
+
minutesRemaining: number;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "expired";
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Pure classifier for a token's ISO `expiresAt` against the current time. A
|
|
34
|
+
* missing or unparseable expiry is "unknown" (older profiles predate the field,
|
|
35
|
+
* so we proceed rather than block). Never throws.
|
|
36
|
+
*/
|
|
37
|
+
export declare function classifyTokenExpiry(expiresAt: string | undefined, nowMs: number, thresholdMs?: number): TokenExpiryStatus;
|
|
38
|
+
export interface BuildLaunchOptions {
|
|
39
|
+
/** Absolute path to pi-extension-yagni's entry (resolved by the caller). */
|
|
40
|
+
extensionPath: string;
|
|
41
|
+
/**
|
|
42
|
+
* Isolated pi config dir. Pinning this keeps YAGNI Code hermetic: it never
|
|
43
|
+
* reads the user's machine-wide `~/.pi/agent`, so local providers
|
|
44
|
+
* (Together/OpenRouter), stored keys, a saved default model, and theme cannot
|
|
45
|
+
* bleed in or route around the YAGNI proxy.
|
|
46
|
+
*/
|
|
47
|
+
agentDir: string;
|
|
48
|
+
/**
|
|
49
|
+
* Shadow pi package dir to expose as `PI_PACKAGE_DIR` so pi rebrands its
|
|
50
|
+
* chrome to "YAGNI Code". Optional: if the shadow package could not be
|
|
51
|
+
* generated, we omit it and pi runs un-rebranded but still hermetic.
|
|
52
|
+
*/
|
|
53
|
+
piPackageDir?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Absolute path of the active profile's JSON file. Forwarded to the child as
|
|
56
|
+
* `YAGNI_PROFILE_PATH` so the extension can persist a mid-session token
|
|
57
|
+
* rotation back to the same profile the launcher read. Optional: omitted when
|
|
58
|
+
* the caller cannot resolve it (the extension then keeps its rotation
|
|
59
|
+
* in-process only, still fail-soft).
|
|
60
|
+
*/
|
|
61
|
+
profilePath?: string;
|
|
62
|
+
/** Distribution-specific root shared with the bundled extension. */
|
|
63
|
+
stateDir?: string;
|
|
64
|
+
/** Base environment to extend (defaults to process.env at call sites). */
|
|
65
|
+
baseEnv?: NodeJS.ProcessEnv;
|
|
66
|
+
/** Clock seam for the token-expiry preflight (defaults to Date.now). */
|
|
67
|
+
now?: () => number;
|
|
68
|
+
/**
|
|
69
|
+
* Launcher-computed agent flags (e.g. Claude-compat `--skill` /
|
|
70
|
+
* `--prompt-template` paths). Inserted before the user's passthrough args so
|
|
71
|
+
* anything the user passes explicitly still wins.
|
|
72
|
+
*/
|
|
73
|
+
extraAgentArgs?: string[];
|
|
74
|
+
}
|
|
75
|
+
export declare function buildLaunch(creds: Credentials | null, passthroughArgs: string[], opts: BuildLaunchOptions): LaunchPlan;
|
|
76
|
+
//# sourceMappingURL=launch.d.ts.map
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the configured pi launch. `buildLaunch` is pure (no spawn, no fs) so it
|
|
3
|
+
* is fully unit-testable: it returns the child environment and the pi argv that
|
|
4
|
+
* registers pi-extension-yagni and defaults the provider to `yagni`.
|
|
5
|
+
*/
|
|
6
|
+
import { agentDirEnvVar } from "./branding.js";
|
|
7
|
+
/**
|
|
8
|
+
* How close to expiry the token can be before launch warns. A coding session
|
|
9
|
+
* easily outlives a token, so warn early enough that re-logging in before a long
|
|
10
|
+
* task is worthwhile, without nagging on every launch.
|
|
11
|
+
*/
|
|
12
|
+
export const EXPIRY_WARNING_THRESHOLD_MS = 30 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Pure classifier for a token's ISO `expiresAt` against the current time. A
|
|
15
|
+
* missing or unparseable expiry is "unknown" (older profiles predate the field,
|
|
16
|
+
* so we proceed rather than block). Never throws.
|
|
17
|
+
*/
|
|
18
|
+
export function classifyTokenExpiry(expiresAt, nowMs, thresholdMs = EXPIRY_WARNING_THRESHOLD_MS) {
|
|
19
|
+
if (!expiresAt)
|
|
20
|
+
return { kind: "unknown" };
|
|
21
|
+
const expiryMs = Date.parse(expiresAt);
|
|
22
|
+
if (Number.isNaN(expiryMs))
|
|
23
|
+
return { kind: "unknown" };
|
|
24
|
+
const remaining = expiryMs - nowMs;
|
|
25
|
+
if (remaining <= 0)
|
|
26
|
+
return { kind: "expired" };
|
|
27
|
+
if (remaining <= thresholdMs) {
|
|
28
|
+
return { kind: "approaching", minutesRemaining: Math.max(1, Math.ceil(remaining / 60_000)) };
|
|
29
|
+
}
|
|
30
|
+
return { kind: "valid" };
|
|
31
|
+
}
|
|
32
|
+
export function buildLaunch(creds, passthroughArgs, opts) {
|
|
33
|
+
if (!creds?.token) {
|
|
34
|
+
throw new Error("Not logged in. Run `yagni login` first.");
|
|
35
|
+
}
|
|
36
|
+
// Launch-time expiry preflight. An already-expired token would spawn a session
|
|
37
|
+
// that 401s on its very first completion and on every grounding tool, so block
|
|
38
|
+
// it here with an actionable login prompt instead. An approaching expiry is a
|
|
39
|
+
// non-fatal warning the caller surfaces; the session still launches.
|
|
40
|
+
const nowMs = opts.now ? opts.now() : Date.now();
|
|
41
|
+
const expiry = classifyTokenExpiry(creds.expiresAt, nowMs);
|
|
42
|
+
if (expiry.kind === "expired") {
|
|
43
|
+
throw new Error("Your YAGNI Code session has expired. Run `yagni login` to re-authenticate.");
|
|
44
|
+
}
|
|
45
|
+
const warnings = [];
|
|
46
|
+
if (expiry.kind === "approaching") {
|
|
47
|
+
const m = expiry.minutesRemaining;
|
|
48
|
+
warnings.push(`Your YAGNI Code session expires in about ${m} minute${m === 1 ? "" : "s"}. ` +
|
|
49
|
+
"Run `yagni login` to refresh it before it lapses.");
|
|
50
|
+
}
|
|
51
|
+
const env = {
|
|
52
|
+
...(opts.baseEnv ?? {}),
|
|
53
|
+
YAGNI_TOKEN: creds.token,
|
|
54
|
+
YAGNI_BASE_URL: creds.baseUrl,
|
|
55
|
+
// Forward the token's expiry (when known) so the extension can surface a
|
|
56
|
+
// single in-session "expiring soon" notice. Read-only signal: no refresh.
|
|
57
|
+
...(creds.expiresAt ? { YAGNI_TOKEN_EXPIRES_AT: creds.expiresAt } : {}),
|
|
58
|
+
// Forward the bound workspace id (when known) so the extension can key its
|
|
59
|
+
// one-time init-pass marker per workspace (Onramp Door B idempotency).
|
|
60
|
+
...(creds.workspaceId ? { YAGNI_WORKSPACE_ID: creds.workspaceId } : {}),
|
|
61
|
+
// Forward the active profile's file path (when known) so the extension can
|
|
62
|
+
// atomically persist a mid-session token rotation back to the same profile
|
|
63
|
+
// the launcher read (0600). Absent when the caller can't resolve it.
|
|
64
|
+
...(opts.profilePath ? { YAGNI_PROFILE_PATH: opts.profilePath } : {}),
|
|
65
|
+
...(opts.stateDir ? { YAGNI_CODE_HOME: opts.stateDir } : {}),
|
|
66
|
+
// Rebrand pi's chrome ("pi"/"π" → "YAGNI Code"): pi reads piConfig.name from
|
|
67
|
+
// the package at PI_PACKAGE_DIR (a shadow package the launcher generates).
|
|
68
|
+
...(opts.piPackageDir ? { PI_PACKAGE_DIR: opts.piPackageDir } : {}),
|
|
69
|
+
// Hermetic config: pin pi's agent dir to a YAGNI-owned location so the
|
|
70
|
+
// user's machine-wide ~/.pi/agent (providers, stored keys, saved default
|
|
71
|
+
// model, theme) can't bleed in or bypass the YAGNI proxy. The model scope
|
|
72
|
+
// is then exactly the backend catalog under the `yagni` provider.
|
|
73
|
+
//
|
|
74
|
+
// pi derives the env var NAME from its app name
|
|
75
|
+
// (`${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`), so once rebranded it reads
|
|
76
|
+
// "YAGNI_CODING_AGENT_DIR". We set that key AND the plain
|
|
77
|
+
// PI_CODING_AGENT_DIR as defense-in-depth: if the rebrand ever fails the app
|
|
78
|
+
// name stays "pi", and the plain key keeps the hermetic boundary intact
|
|
79
|
+
// instead of silently falling back to ~/.pi/agent.
|
|
80
|
+
[agentDirEnvVar()]: opts.agentDir,
|
|
81
|
+
PI_CODING_AGENT_DIR: opts.agentDir,
|
|
82
|
+
// Match the e2b harness defaults: skip pi's update check + telemetry.
|
|
83
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
84
|
+
PI_TELEMETRY: "0",
|
|
85
|
+
};
|
|
86
|
+
// Always load our extension. Default the provider to `yagni` unless the user
|
|
87
|
+
// explicitly chose one (so power users can still point pi elsewhere).
|
|
88
|
+
const userChoseProvider = passthroughArgs.some(arg => arg === "--provider" || arg.startsWith("--provider="));
|
|
89
|
+
// Default the model to the `balanced` tier (YAG-380). Balanced drives on the
|
|
90
|
+
// judgment-grade `advanced` model — the same driver quality this default gave
|
|
91
|
+
// before — and additionally lets the session escalate hard calls to `peak`
|
|
92
|
+
// through the capped ask_advisor tool. So it is strictly additive: no session
|
|
93
|
+
// reasons worse than it did, and the hard ones can reason better. A user
|
|
94
|
+
// `--model` (e.g. `standard` or `efficient`) still wins. Without this, pi's
|
|
95
|
+
// default-model heuristic could land an interactive session on a weaker tier.
|
|
96
|
+
// Detect if the user explicitly set a model, whether via "--model" as a separate
|
|
97
|
+
// argument or using the equals form "--model=efficient". The previous check only
|
|
98
|
+
// caught the separate form, causing a duplicate "--model balanced" to be added
|
|
99
|
+
// when the equals form was used.
|
|
100
|
+
const userChoseModel = passthroughArgs.some(arg => arg === "--model" || arg.startsWith("--model="));
|
|
101
|
+
const argv = [
|
|
102
|
+
"-e",
|
|
103
|
+
opts.extensionPath,
|
|
104
|
+
...(userChoseProvider ? [] : ["--provider", "yagni"]),
|
|
105
|
+
...(userChoseModel ? [] : ["--model", "balanced"]),
|
|
106
|
+
...(opts.extraAgentArgs ?? []),
|
|
107
|
+
...passthroughArgs,
|
|
108
|
+
];
|
|
109
|
+
return { env, argv, warnings };
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=launch.js.map
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-code login flow (RFC 8628 client side):
|
|
3
|
+
* 1. POST /api/yagni-code/auth/device → show the user_code + verification URL.
|
|
4
|
+
* 2. Poll POST /api/yagni-code/auth/token until the human approves in the
|
|
5
|
+
* web app, then store the returned token.
|
|
6
|
+
*/
|
|
7
|
+
import type { Credentials } from "./credentials.js";
|
|
8
|
+
export interface LoginDeps {
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Which profile to store the minted token in. Defaults to the active one. */
|
|
11
|
+
profileName?: string;
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
sleep?: (ms: number) => Promise<void>;
|
|
15
|
+
log?: (msg: string) => void;
|
|
16
|
+
persist?: (creds: Credentials) => Promise<void>;
|
|
17
|
+
/** Open a URL in the user's default browser. Defaults to cross-platform open. */
|
|
18
|
+
openUrl?: (url: string) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Spawns a binary with a literal arg vector (no shell). */
|
|
21
|
+
type OpenRunner = (cmd: string, args: string[]) => Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the OS-native command + argv to open `url` in the default browser,
|
|
24
|
+
* cross-platform. The URL is always a standalone argv element so it is handed
|
|
25
|
+
* to the OS verbatim — it is NEVER interpolated into a shell string, where
|
|
26
|
+
* `$(...)` / backticks in a hostile verification URL could execute commands.
|
|
27
|
+
*
|
|
28
|
+
* macOS: open <url>
|
|
29
|
+
* Linux: xdg-open <url>
|
|
30
|
+
* Windows: cmd /c start "" <url> (the empty "" is start's window-title slot;
|
|
31
|
+
* without it `start` swallows the URL as the title and opens nothing)
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
34
|
+
cmd: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Open a URL in the user's default browser, best-effort. Uses `execFile` (no
|
|
39
|
+
* shell) so a hostile URL cannot inject commands. Failures are swallowed — the
|
|
40
|
+
* URL is still printed for manual copy. `runner` is injectable for tests.
|
|
41
|
+
*/
|
|
42
|
+
export declare const realOpenUrl: (url: string, runner?: OpenRunner) => Promise<void>;
|
|
43
|
+
export declare function login(deps?: LoginDeps): Promise<Credentials>;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=login.d.ts.map
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-code login flow (RFC 8628 client side):
|
|
3
|
+
* 1. POST /api/yagni-code/auth/device → show the user_code + verification URL.
|
|
4
|
+
* 2. Poll POST /api/yagni-code/auth/token until the human approves in the
|
|
5
|
+
* web app, then store the returned token.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { resolveBaseUrl } from "./config.js";
|
|
9
|
+
import { getActiveProfileName, persistProfileCredentials } from "./profiles.js";
|
|
10
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
/**
|
|
12
|
+
* Per-poll wall-clock cap so a single hung token request can't stall the whole
|
|
13
|
+
* device-code wait. A timed-out poll is swallowed and the loop tries again until
|
|
14
|
+
* the device code's own deadline.
|
|
15
|
+
*/
|
|
16
|
+
const POLL_REQUEST_TIMEOUT_MS = 10_000;
|
|
17
|
+
/**
|
|
18
|
+
* RFC 8628 `slow_down`: when the server signals we are polling too fast (HTTP
|
|
19
|
+
* 429), lengthen the interval by 5s and keep waiting rather than failing.
|
|
20
|
+
*/
|
|
21
|
+
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
22
|
+
/** Whether an error is a per-request timeout/abort (vs. a real network error). */
|
|
23
|
+
function isTimeoutError(err) {
|
|
24
|
+
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the OS-native command + argv to open `url` in the default browser,
|
|
28
|
+
* cross-platform. The URL is always a standalone argv element so it is handed
|
|
29
|
+
* to the OS verbatim — it is NEVER interpolated into a shell string, where
|
|
30
|
+
* `$(...)` / backticks in a hostile verification URL could execute commands.
|
|
31
|
+
*
|
|
32
|
+
* macOS: open <url>
|
|
33
|
+
* Linux: xdg-open <url>
|
|
34
|
+
* Windows: cmd /c start "" <url> (the empty "" is start's window-title slot;
|
|
35
|
+
* without it `start` swallows the URL as the title and opens nothing)
|
|
36
|
+
*/
|
|
37
|
+
export function resolveOpenCommand(url, platform = process.platform) {
|
|
38
|
+
if (platform === "darwin")
|
|
39
|
+
return { cmd: "open", args: [url] };
|
|
40
|
+
if (platform === "win32")
|
|
41
|
+
return { cmd: "cmd", args: ["/c", "start", "", url] };
|
|
42
|
+
return { cmd: "xdg-open", args: [url] };
|
|
43
|
+
}
|
|
44
|
+
const spawnRunner = (cmd, args) => new Promise((resolve, reject) => {
|
|
45
|
+
execFile(cmd, args, (err) => (err ? reject(err) : resolve()));
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Open a URL in the user's default browser, best-effort. Uses `execFile` (no
|
|
49
|
+
* shell) so a hostile URL cannot inject commands. Failures are swallowed — the
|
|
50
|
+
* URL is still printed for manual copy. `runner` is injectable for tests.
|
|
51
|
+
*/
|
|
52
|
+
export const realOpenUrl = (url, runner = spawnRunner) => {
|
|
53
|
+
const { cmd, args } = resolveOpenCommand(url);
|
|
54
|
+
return runner(cmd, args).catch(() => {
|
|
55
|
+
// Silently fail — the user can still copy the URL manually.
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
export async function login(deps = {}) {
|
|
59
|
+
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
60
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
61
|
+
const now = deps.now ?? Date.now;
|
|
62
|
+
const sleep = deps.sleep ?? realSleep;
|
|
63
|
+
const log = deps.log ?? ((m) => process.stdout.write(`${m}\n`));
|
|
64
|
+
// By default the token lands in the active profile; tests inject `persist`.
|
|
65
|
+
const persist = deps.persist ??
|
|
66
|
+
(async (creds) => {
|
|
67
|
+
const profileName = deps.profileName ?? (await getActiveProfileName());
|
|
68
|
+
await persistProfileCredentials(profileName, creds);
|
|
69
|
+
});
|
|
70
|
+
// Open the verification URL automatically; failures are swallowed so the
|
|
71
|
+
// user can still copy it manually.
|
|
72
|
+
const openUrl = deps.openUrl ?? realOpenUrl;
|
|
73
|
+
const startRes = await fetchImpl(`${baseUrl}/api/yagni-code/auth/device`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
body: "{}",
|
|
77
|
+
signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS),
|
|
78
|
+
});
|
|
79
|
+
if (!startRes.ok) {
|
|
80
|
+
throw new Error(`Could not start login (HTTP ${startRes.status}). Is YAGNI Code enabled for your workspace?`);
|
|
81
|
+
}
|
|
82
|
+
const start = (await startRes.json());
|
|
83
|
+
log("");
|
|
84
|
+
log("Opening your browser to authorize YAGNI Code…");
|
|
85
|
+
log(` ${start.verification_url}`);
|
|
86
|
+
log("If it doesn't open automatically, copy the URL above and enter the code:");
|
|
87
|
+
log(` ${start.user_code}`);
|
|
88
|
+
log("");
|
|
89
|
+
log("Waiting for approval…");
|
|
90
|
+
// Best-effort auto-open; the URL is already printed above.
|
|
91
|
+
await openUrl(start.verification_url).catch(() => { });
|
|
92
|
+
const deadline = now() + start.expires_in * 1000;
|
|
93
|
+
// Interval can grow across the loop when the server asks us to slow down, so
|
|
94
|
+
// it's mutable rather than a one-shot constant.
|
|
95
|
+
let intervalMs = Math.max(1, start.interval) * 1000;
|
|
96
|
+
while (now() < deadline) {
|
|
97
|
+
await sleep(intervalMs);
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await fetchImpl(`${baseUrl}/api/yagni-code/auth/token`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ device_code: start.device_code }),
|
|
104
|
+
signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// A per-request timeout (or transient blip) must not abandon a long
|
|
109
|
+
// device-code wait — keep polling until the code's own deadline. A
|
|
110
|
+
// genuine, non-timeout error still propagates.
|
|
111
|
+
if (isTimeoutError(err))
|
|
112
|
+
continue;
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
if (res.status === 200) {
|
|
116
|
+
const data = (await res.json());
|
|
117
|
+
const creds = {
|
|
118
|
+
token: data.token,
|
|
119
|
+
baseUrl,
|
|
120
|
+
workspaceId: data.workspaceId,
|
|
121
|
+
expiresAt: data.expiresAt,
|
|
122
|
+
};
|
|
123
|
+
await persist(creds);
|
|
124
|
+
log("✓ Logged in. Run `yagni` in a repo to start.");
|
|
125
|
+
return creds;
|
|
126
|
+
}
|
|
127
|
+
if (res.status === 428) {
|
|
128
|
+
continue; // authorization_pending — keep polling
|
|
129
|
+
}
|
|
130
|
+
if (res.status === 429) {
|
|
131
|
+
// slow_down — back off by 5s (RFC 8628) and keep polling.
|
|
132
|
+
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (res.status === 410) {
|
|
136
|
+
throw new Error("The login code expired before approval. Run `yagni login` again.");
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Login failed (HTTP ${res.status}).`);
|
|
139
|
+
}
|
|
140
|
+
throw new Error("Login timed out before approval. Run `yagni login` again.");
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=login.js.map
|
package/dist/logout.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logout: revoke the active profile's token server-side (best-effort) and clear
|
|
3
|
+
* it locally. The environment binding (name + base URL) is kept so a later
|
|
4
|
+
* `yagni login` re-authenticates the same environment without re-`use`.
|
|
5
|
+
*/
|
|
6
|
+
import { type Credentials } from "./credentials.js";
|
|
7
|
+
export interface LogoutDeps {
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
read?: () => Promise<Credentials | null>;
|
|
10
|
+
remove?: () => Promise<void>;
|
|
11
|
+
log?: (msg: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function logout(deps?: LogoutDeps): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=logout.d.ts.map
|
package/dist/logout.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logout: revoke the active profile's token server-side (best-effort) and clear
|
|
3
|
+
* it locally. The environment binding (name + base URL) is kept so a later
|
|
4
|
+
* `yagni login` re-authenticates the same environment without re-`use`.
|
|
5
|
+
*/
|
|
6
|
+
import { clearProfileToken, credentialsFromProfile, getActiveProfileName, readActiveProfile, } from "./profiles.js";
|
|
7
|
+
export async function logout(deps = {}) {
|
|
8
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
9
|
+
const read = deps.read ?? (async () => credentialsFromProfile(await readActiveProfile()));
|
|
10
|
+
const remove = deps.remove ??
|
|
11
|
+
(async () => {
|
|
12
|
+
await clearProfileToken(await getActiveProfileName());
|
|
13
|
+
});
|
|
14
|
+
const log = deps.log ?? ((m) => process.stdout.write(`${m}\n`));
|
|
15
|
+
const creds = await read();
|
|
16
|
+
if (creds?.token) {
|
|
17
|
+
try {
|
|
18
|
+
await fetchImpl(`${creds.baseUrl}/api/yagni-code/auth/revoke`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"content-type": "application/json",
|
|
22
|
+
authorization: `Bearer ${creds.token}`,
|
|
23
|
+
},
|
|
24
|
+
body: "{}",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Best-effort: a network failure shouldn't block local logout.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
await remove();
|
|
32
|
+
log("✓ Logged out.");
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=logout.js.map
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the dependency entry points the launcher needs: pi's CLI and the
|
|
3
|
+
* YAGNI extension. pi is a package dependency, so we resolve it from this module
|
|
4
|
+
* rather than assuming a global install; the extension ships inside our own dist.
|
|
5
|
+
*
|
|
6
|
+
* We use the ESM-native `import.meta.resolve` (NOT `createRequire().resolve`):
|
|
7
|
+
* pi is an ESM-only package whose `exports` map declares only the `import`
|
|
8
|
+
* condition, so the CommonJS `require` resolver throws ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
9
|
+
* on it. pi's package.json is also not an exported subpath, so we read it from
|
|
10
|
+
* disk after resolving the package's main entry.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Absolute path to the extension's built entry.
|
|
14
|
+
*
|
|
15
|
+
* Published installs read the copy bundled into this package's own
|
|
16
|
+
* `dist/extension/` (see scripts/bundle-extension.mjs) — pi-extension-yagni is a
|
|
17
|
+
* private workspace package and is never on the registry, so module resolution
|
|
18
|
+
* would fail. In the monorepo the bundled copy only exists after a build, so we
|
|
19
|
+
* fall back to resolving the workspace-linked package, which keeps `pnpm test`
|
|
20
|
+
* and a fresh checkout working before anything is bundled.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveExtensionPath(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Absolute path to pi's package root — the dir whose package.json names the
|
|
25
|
+
* package. The shadow package dir is built from this (we read its package.json
|
|
26
|
+
* and symlink its `dist/`). Uses the same upward walk as resolvePiCliPath.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolvePiPackageDir(): string;
|
|
29
|
+
/** Absolute path to pi's CLI entry (its package.json `bin.pi`). */
|
|
30
|
+
export declare function resolvePiCliPath(): string;
|
|
31
|
+
//# sourceMappingURL=paths.d.ts.map
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the dependency entry points the launcher needs: pi's CLI and the
|
|
3
|
+
* YAGNI extension. pi is a package dependency, so we resolve it from this module
|
|
4
|
+
* rather than assuming a global install; the extension ships inside our own dist.
|
|
5
|
+
*
|
|
6
|
+
* We use the ESM-native `import.meta.resolve` (NOT `createRequire().resolve`):
|
|
7
|
+
* pi is an ESM-only package whose `exports` map declares only the `import`
|
|
8
|
+
* condition, so the CommonJS `require` resolver throws ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
9
|
+
* on it. pi's package.json is also not an exported subpath, so we read it from
|
|
10
|
+
* disk after resolving the package's main entry.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
function resolveModulePath(specifier) {
|
|
16
|
+
return fileURLToPath(import.meta.resolve(specifier));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Absolute path to the extension's built entry.
|
|
20
|
+
*
|
|
21
|
+
* Published installs read the copy bundled into this package's own
|
|
22
|
+
* `dist/extension/` (see scripts/bundle-extension.mjs) — pi-extension-yagni is a
|
|
23
|
+
* private workspace package and is never on the registry, so module resolution
|
|
24
|
+
* would fail. In the monorepo the bundled copy only exists after a build, so we
|
|
25
|
+
* fall back to resolving the workspace-linked package, which keeps `pnpm test`
|
|
26
|
+
* and a fresh checkout working before anything is bundled.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveExtensionPath() {
|
|
29
|
+
const bundled = fileURLToPath(new URL("./extension/index.js", import.meta.url));
|
|
30
|
+
if (existsSync(bundled))
|
|
31
|
+
return bundled;
|
|
32
|
+
return resolveModulePath("pi-extension-yagni");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Absolute path to pi's package root — the dir whose package.json names the
|
|
36
|
+
* package. The shadow package dir is built from this (we read its package.json
|
|
37
|
+
* and symlink its `dist/`). Uses the same upward walk as resolvePiCliPath.
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePiPackageDir() {
|
|
40
|
+
const mainEntry = resolveModulePath("@earendil-works/pi-coding-agent");
|
|
41
|
+
let dir = dirname(mainEntry);
|
|
42
|
+
for (let i = 0; i < 6; i++) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
45
|
+
if (pkg.name === "@earendil-works/pi-coding-agent") {
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// No package.json here (or unreadable) — keep walking up.
|
|
51
|
+
}
|
|
52
|
+
const parent = dirname(dir);
|
|
53
|
+
if (parent === dir)
|
|
54
|
+
break;
|
|
55
|
+
dir = parent;
|
|
56
|
+
}
|
|
57
|
+
// Fallback: pi ships its main entry under <root>/dist/, so the root is two
|
|
58
|
+
// levels up from the resolved entry.
|
|
59
|
+
return dirname(dirname(mainEntry));
|
|
60
|
+
}
|
|
61
|
+
/** Absolute path to pi's CLI entry (its package.json `bin.pi`). */
|
|
62
|
+
export function resolvePiCliPath() {
|
|
63
|
+
// Resolves pi's main entry (e.g. <root>/@earendil-works/pi-coding-agent/dist/index.js).
|
|
64
|
+
const mainEntry = resolveModulePath("@earendil-works/pi-coding-agent");
|
|
65
|
+
// Walk up to the package root (the dir whose package.json names the package)
|
|
66
|
+
// and read `bin.pi` from disk.
|
|
67
|
+
let dir = dirname(mainEntry);
|
|
68
|
+
for (let i = 0; i < 6; i++) {
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
71
|
+
if (pkg.name === "@earendil-works/pi-coding-agent") {
|
|
72
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi ?? "dist/cli.js";
|
|
73
|
+
return join(dir, binRel);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// No package.json here (or unreadable) — keep walking up.
|
|
78
|
+
}
|
|
79
|
+
const parent = dirname(dir);
|
|
80
|
+
if (parent === dir)
|
|
81
|
+
break;
|
|
82
|
+
dir = parent;
|
|
83
|
+
}
|
|
84
|
+
// Fallback: pi ships its CLI alongside its main entry under dist/.
|
|
85
|
+
return join(dirname(mainEntry), "cli.js");
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow pi package dir.
|
|
3
|
+
*
|
|
4
|
+
* pi reads its identity (`piConfig.name`) and all bundled assets (theme, TUI
|
|
5
|
+
* assets, export-html, CHANGELOG) relative to the package dir returned by
|
|
6
|
+
* `getPackageDir()`, which honors the `PI_PACKAGE_DIR` env var. To rebrand
|
|
7
|
+
* "pi"/"π" out of the chrome WITHOUT modifying `node_modules` (lost on
|
|
8
|
+
* reinstall) or copying pi wholesale, we generate a tiny shadow dir that:
|
|
9
|
+
*
|
|
10
|
+
* - owns its own `package.json` (pi's, with `piConfig.name` injected), and
|
|
11
|
+
* - symlinks `dist/` (+ CHANGELOG/README/docs/examples) back to the real pi
|
|
12
|
+
* package, so asset/theme resolution still finds the real files.
|
|
13
|
+
*
|
|
14
|
+
* The launcher points `PI_PACKAGE_DIR` at this dir. Regenerated every launch so
|
|
15
|
+
* a pi upgrade or store-path move can never leave a stale package.json or a
|
|
16
|
+
* dangling symlink behind.
|
|
17
|
+
*/
|
|
18
|
+
export interface ShadowPiPackageOptions {
|
|
19
|
+
/** The real pi package root (the dir whose package.json names the package). */
|
|
20
|
+
realPiDir: string;
|
|
21
|
+
/** Where to materialize the shadow package (a hermetic, YAGNI-owned dir). */
|
|
22
|
+
shadowDir: string;
|
|
23
|
+
/** The `piConfig.name` to inject — what pi shows as its app name. */
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Materialize (idempotently) the shadow pi package and return its path.
|
|
28
|
+
*
|
|
29
|
+
* @throws if the real pi package.json cannot be read — the launcher needs it to
|
|
30
|
+
* derive pi's version/name, and a silent fallback would re-leak "pi".
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureShadowPiPackage(opts: ShadowPiPackageOptions): string;
|
|
33
|
+
//# sourceMappingURL=piPackage.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow pi package dir.
|
|
3
|
+
*
|
|
4
|
+
* pi reads its identity (`piConfig.name`) and all bundled assets (theme, TUI
|
|
5
|
+
* assets, export-html, CHANGELOG) relative to the package dir returned by
|
|
6
|
+
* `getPackageDir()`, which honors the `PI_PACKAGE_DIR` env var. To rebrand
|
|
7
|
+
* "pi"/"π" out of the chrome WITHOUT modifying `node_modules` (lost on
|
|
8
|
+
* reinstall) or copying pi wholesale, we generate a tiny shadow dir that:
|
|
9
|
+
*
|
|
10
|
+
* - owns its own `package.json` (pi's, with `piConfig.name` injected), and
|
|
11
|
+
* - symlinks `dist/` (+ CHANGELOG/README/docs/examples) back to the real pi
|
|
12
|
+
* package, so asset/theme resolution still finds the real files.
|
|
13
|
+
*
|
|
14
|
+
* The launcher points `PI_PACKAGE_DIR` at this dir. Regenerated every launch so
|
|
15
|
+
* a pi upgrade or store-path move can never leave a stale package.json or a
|
|
16
|
+
* dangling symlink behind.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
// Entries pi resolves relative to its package dir. `dist` carries theme + TUI
|
|
21
|
+
// assets + export templates (the load-bearing ones); the rest back /changelog,
|
|
22
|
+
// /about, etc. Each is symlinked only if it exists in the real package.
|
|
23
|
+
const LINKED_ENTRIES = ["dist", "CHANGELOG.md", "README.md", "docs", "examples"];
|
|
24
|
+
function linkInto(shadowDir, realPiDir, entry) {
|
|
25
|
+
const target = join(realPiDir, entry);
|
|
26
|
+
if (!existsSync(target))
|
|
27
|
+
return;
|
|
28
|
+
const linkPath = join(shadowDir, entry);
|
|
29
|
+
// Clear any prior entry so we never stack or dangle. A symlink must be
|
|
30
|
+
// unlinked directly: rmSync stats THROUGH it, so on a dangling link (target
|
|
31
|
+
// checkout deleted) it sees ENOENT, force-swallows it, and leaves the link
|
|
32
|
+
// in place - then symlinkSync throws EEXIST and the rebrand silently drops.
|
|
33
|
+
if (isSymlink(linkPath)) {
|
|
34
|
+
unlinkSync(linkPath);
|
|
35
|
+
}
|
|
36
|
+
else if (existsSync(linkPath)) {
|
|
37
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
const type = statSync(target).isDirectory() ? "dir" : "file";
|
|
40
|
+
symlinkSync(target, linkPath, type);
|
|
41
|
+
}
|
|
42
|
+
function isSymlink(p) {
|
|
43
|
+
try {
|
|
44
|
+
return lstatSync(p).isSymbolicLink();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Materialize (idempotently) the shadow pi package and return its path.
|
|
52
|
+
*
|
|
53
|
+
* @throws if the real pi package.json cannot be read — the launcher needs it to
|
|
54
|
+
* derive pi's version/name, and a silent fallback would re-leak "pi".
|
|
55
|
+
*/
|
|
56
|
+
export function ensureShadowPiPackage(opts) {
|
|
57
|
+
const { realPiDir, shadowDir, name } = opts;
|
|
58
|
+
const realPkgRaw = readFileSync(join(realPiDir, "package.json"), "utf8");
|
|
59
|
+
const realPkg = JSON.parse(realPkgRaw);
|
|
60
|
+
mkdirSync(shadowDir, { recursive: true });
|
|
61
|
+
const shadowPkg = {
|
|
62
|
+
...realPkg,
|
|
63
|
+
piConfig: { ...(realPkg.piConfig ?? {}), name },
|
|
64
|
+
};
|
|
65
|
+
writeFileSync(join(shadowDir, "package.json"), `${JSON.stringify(shadowPkg, null, 2)}\n`);
|
|
66
|
+
for (const entry of LINKED_ENTRIES) {
|
|
67
|
+
linkInto(shadowDir, realPiDir, entry);
|
|
68
|
+
}
|
|
69
|
+
return shadowDir;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=piPackage.js.map
|