@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment profiles for the yagni-code CLI.
|
|
3
|
+
*
|
|
4
|
+
* A profile is a named binding of an environment (base URL) to its own stored
|
|
5
|
+
* credentials, so you can point YAGNI Code at prod, a local backend, or a
|
|
6
|
+
* staging host and switch between them with a sticky `yagni use <name>`.
|
|
7
|
+
*
|
|
8
|
+
* Storage (each file 0600, the dir 0700, since profiles hold live API tokens):
|
|
9
|
+
* ~/.yagni-code/profiles/<name>.json one profile (name, baseUrl, token, …)
|
|
10
|
+
* ~/.yagni-code/config.json { activeProfile }
|
|
11
|
+
*
|
|
12
|
+
* The model is deliberately small but multi-account-ready: a second login to a
|
|
13
|
+
* different workspace/host is just another profile file. `prod` is the default
|
|
14
|
+
* active profile and the only built-in preset that needs no `--base-url`.
|
|
15
|
+
*/
|
|
16
|
+
import { type Credentials } from "./credentials.js";
|
|
17
|
+
export interface Profile {
|
|
18
|
+
name: string;
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
workspaceId?: string;
|
|
22
|
+
expiresAt?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The default active profile when nothing has been configured yet. */
|
|
25
|
+
export declare const DEFAULT_PROFILE: string;
|
|
26
|
+
export declare function isValidProfileName(name: string): boolean;
|
|
27
|
+
export declare function profilesDir(): string;
|
|
28
|
+
export declare function profilePath(name: string): string;
|
|
29
|
+
export declare function configPath(): string;
|
|
30
|
+
export declare function readProfile(name: string): Promise<Profile | null>;
|
|
31
|
+
export declare function writeProfile(profile: Profile): Promise<void>;
|
|
32
|
+
export declare function deleteProfile(name: string): Promise<void>;
|
|
33
|
+
export declare function listProfileNames(): Promise<string[]>;
|
|
34
|
+
export declare function listProfiles(): Promise<Profile[]>;
|
|
35
|
+
export declare function getActiveProfileName(): Promise<string>;
|
|
36
|
+
export declare function setActiveProfileName(name: string): Promise<void>;
|
|
37
|
+
/** A profile is a superset of Credentials; this yields creds iff it has a token. */
|
|
38
|
+
export declare function credentialsFromProfile(profile: Profile): Credentials | null;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the active profile, with its base URL. Always returns a profile (so
|
|
41
|
+
* callers get a base URL even before a first login); `token` is undefined when
|
|
42
|
+
* the environment has not been authenticated. `YAGNI_BASE_URL` overrides the
|
|
43
|
+
* stored/preset base URL for this run only.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readActiveProfile(env?: NodeJS.ProcessEnv): Promise<Profile>;
|
|
46
|
+
/** Persist freshly-minted credentials into a profile (keeping the name). */
|
|
47
|
+
export declare function persistProfileCredentials(name: string, creds: Credentials): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Persist a launch-time token ROTATION into an existing profile without
|
|
50
|
+
* touching its stored base URL. The refresh runs against `creds.baseUrl`, which
|
|
51
|
+
* may be a per-run `YAGNI_BASE_URL` override (readActiveProfile bakes that in);
|
|
52
|
+
* persisting it would make a documented single-run override sticky, silently
|
|
53
|
+
* retargeting every later `yagni` launch at the wrong host. So we rotate only
|
|
54
|
+
* the token fields and keep the profile's own stored base URL. Falls back to the
|
|
55
|
+
* passed base URL only when no profile file exists yet (nothing stored to keep).
|
|
56
|
+
*/
|
|
57
|
+
export declare function persistProfileTokenRotation(name: string, creds: Credentials): Promise<void>;
|
|
58
|
+
/** Clear the stored token for a profile but keep the environment binding. */
|
|
59
|
+
export declare function clearProfileToken(name: string): Promise<void>;
|
|
60
|
+
export declare class UnknownEnvironmentError extends Error {
|
|
61
|
+
constructor(name: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Switch the sticky active profile to `name`, creating it if needed. Base URL
|
|
65
|
+
* is resolved from (in order): an explicit `--base-url`, the profile's existing
|
|
66
|
+
* stored URL, or a built-in preset. A name with none of those is an error
|
|
67
|
+
* rather than a silent default. When `--base-url` changes an existing profile's
|
|
68
|
+
* host, the stored token is dropped (it belonged to the old environment).
|
|
69
|
+
*/
|
|
70
|
+
export declare function useProfile(name: string, opts?: {
|
|
71
|
+
baseUrl?: string;
|
|
72
|
+
}): Promise<Profile>;
|
|
73
|
+
/**
|
|
74
|
+
* One-shot migration: adopt a pre-profiles `~/.yagni-code/credentials.json` as
|
|
75
|
+
* a named profile so existing logged-in users keep working. No-op once profiles
|
|
76
|
+
* exist (config.json present or any profile file on disk) or when there is no
|
|
77
|
+
* legacy file. The legacy file is removed only after the profile is written.
|
|
78
|
+
*/
|
|
79
|
+
export declare function migrateLegacyCredentials(): Promise<void>;
|
|
80
|
+
//# sourceMappingURL=profiles.d.ts.map
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment profiles for the yagni-code CLI.
|
|
3
|
+
*
|
|
4
|
+
* A profile is a named binding of an environment (base URL) to its own stored
|
|
5
|
+
* credentials, so you can point YAGNI Code at prod, a local backend, or a
|
|
6
|
+
* staging host and switch between them with a sticky `yagni use <name>`.
|
|
7
|
+
*
|
|
8
|
+
* Storage (each file 0600, the dir 0700, since profiles hold live API tokens):
|
|
9
|
+
* ~/.yagni-code/profiles/<name>.json one profile (name, baseUrl, token, …)
|
|
10
|
+
* ~/.yagni-code/config.json { activeProfile }
|
|
11
|
+
*
|
|
12
|
+
* The model is deliberately small but multi-account-ready: a second login to a
|
|
13
|
+
* different workspace/host is just another profile file. `prod` is the default
|
|
14
|
+
* active profile and the only built-in preset that needs no `--base-url`.
|
|
15
|
+
*/
|
|
16
|
+
import { chmod, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { credentialsDir, deleteCredentials, readCredentials } from "./credentials.js";
|
|
19
|
+
import { DEFAULT_BASE_URL, PRESET_BASE_URLS, presetBaseUrl } from "./config.js";
|
|
20
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
21
|
+
/** The default active profile when nothing has been configured yet. */
|
|
22
|
+
export const DEFAULT_PROFILE = DISTRIBUTION.defaultProfile;
|
|
23
|
+
/**
|
|
24
|
+
* Allowed profile-name shape. Restricts to a safe filename so a name can never
|
|
25
|
+
* traverse out of the profiles dir or collide with `config.json`.
|
|
26
|
+
*/
|
|
27
|
+
const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
28
|
+
export function isValidProfileName(name) {
|
|
29
|
+
return NAME_RE.test(name) && name !== "config";
|
|
30
|
+
}
|
|
31
|
+
export function profilesDir() {
|
|
32
|
+
return join(credentialsDir(), "profiles");
|
|
33
|
+
}
|
|
34
|
+
export function profilePath(name) {
|
|
35
|
+
return join(profilesDir(), `${name}.json`);
|
|
36
|
+
}
|
|
37
|
+
export function configPath() {
|
|
38
|
+
return join(credentialsDir(), "config.json");
|
|
39
|
+
}
|
|
40
|
+
async function writeJson(path, value) {
|
|
41
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
42
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
43
|
+
// writeFile honors mode only on create; chmod guarantees 0600 on overwrite.
|
|
44
|
+
await chmod(path, 0o600);
|
|
45
|
+
}
|
|
46
|
+
async function readJson(path) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function readProfile(name) {
|
|
55
|
+
const parsed = await readJson(profilePath(name));
|
|
56
|
+
if (!parsed || typeof parsed.baseUrl !== "string")
|
|
57
|
+
return null;
|
|
58
|
+
// The on-disk name is advisory; the filename is authoritative.
|
|
59
|
+
return { ...parsed, name };
|
|
60
|
+
}
|
|
61
|
+
export async function writeProfile(profile) {
|
|
62
|
+
await writeJson(profilePath(profile.name), profile);
|
|
63
|
+
}
|
|
64
|
+
export async function deleteProfile(name) {
|
|
65
|
+
await rm(profilePath(name), { force: true });
|
|
66
|
+
}
|
|
67
|
+
export async function listProfileNames() {
|
|
68
|
+
try {
|
|
69
|
+
const entries = await readdir(profilesDir());
|
|
70
|
+
return entries
|
|
71
|
+
.filter((f) => f.endsWith(".json"))
|
|
72
|
+
.map((f) => f.slice(0, -".json".length))
|
|
73
|
+
.sort();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function listProfiles() {
|
|
80
|
+
const names = await listProfileNames();
|
|
81
|
+
const profiles = await Promise.all(names.map((n) => readProfile(n)));
|
|
82
|
+
return profiles.filter((p) => p !== null);
|
|
83
|
+
}
|
|
84
|
+
export async function getActiveProfileName() {
|
|
85
|
+
const config = await readJson(configPath());
|
|
86
|
+
const active = config?.activeProfile;
|
|
87
|
+
// Ensure the name conforms to the allowed profile name pattern to prevent
|
|
88
|
+
// path traversal when the config file is corrupted or maliciously edited.
|
|
89
|
+
// If the stored name is invalid, fall back to the safe default profile.
|
|
90
|
+
return typeof active === "string" && active.length > 0 && isValidProfileName(active)
|
|
91
|
+
? active
|
|
92
|
+
: DEFAULT_PROFILE;
|
|
93
|
+
}
|
|
94
|
+
export async function setActiveProfileName(name) {
|
|
95
|
+
await writeJson(configPath(), { activeProfile: name });
|
|
96
|
+
}
|
|
97
|
+
/** A profile is a superset of Credentials; this yields creds iff it has a token. */
|
|
98
|
+
export function credentialsFromProfile(profile) {
|
|
99
|
+
if (!profile.token)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
token: profile.token,
|
|
103
|
+
baseUrl: profile.baseUrl,
|
|
104
|
+
workspaceId: profile.workspaceId ?? "",
|
|
105
|
+
expiresAt: profile.expiresAt,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the active profile, with its base URL. Always returns a profile (so
|
|
110
|
+
* callers get a base URL even before a first login); `token` is undefined when
|
|
111
|
+
* the environment has not been authenticated. `YAGNI_BASE_URL` overrides the
|
|
112
|
+
* stored/preset base URL for this run only.
|
|
113
|
+
*/
|
|
114
|
+
export async function readActiveProfile(env = process.env) {
|
|
115
|
+
const name = await getActiveProfileName();
|
|
116
|
+
const stored = await readProfile(name);
|
|
117
|
+
const baseUrl = (env.YAGNI_BASE_URL?.trim() ||
|
|
118
|
+
stored?.baseUrl ||
|
|
119
|
+
presetBaseUrl(name) ||
|
|
120
|
+
DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
121
|
+
return {
|
|
122
|
+
name,
|
|
123
|
+
baseUrl,
|
|
124
|
+
token: stored?.token,
|
|
125
|
+
workspaceId: stored?.workspaceId,
|
|
126
|
+
expiresAt: stored?.expiresAt,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Persist freshly-minted credentials into a profile (keeping the name). */
|
|
130
|
+
export async function persistProfileCredentials(name, creds) {
|
|
131
|
+
await writeProfile({
|
|
132
|
+
name,
|
|
133
|
+
baseUrl: creds.baseUrl,
|
|
134
|
+
token: creds.token,
|
|
135
|
+
workspaceId: creds.workspaceId,
|
|
136
|
+
expiresAt: creds.expiresAt,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Persist a launch-time token ROTATION into an existing profile without
|
|
141
|
+
* touching its stored base URL. The refresh runs against `creds.baseUrl`, which
|
|
142
|
+
* may be a per-run `YAGNI_BASE_URL` override (readActiveProfile bakes that in);
|
|
143
|
+
* persisting it would make a documented single-run override sticky, silently
|
|
144
|
+
* retargeting every later `yagni` launch at the wrong host. So we rotate only
|
|
145
|
+
* the token fields and keep the profile's own stored base URL. Falls back to the
|
|
146
|
+
* passed base URL only when no profile file exists yet (nothing stored to keep).
|
|
147
|
+
*/
|
|
148
|
+
export async function persistProfileTokenRotation(name, creds) {
|
|
149
|
+
const stored = await readProfile(name);
|
|
150
|
+
await writeProfile({
|
|
151
|
+
name,
|
|
152
|
+
baseUrl: stored?.baseUrl ?? creds.baseUrl,
|
|
153
|
+
token: creds.token,
|
|
154
|
+
workspaceId: creds.workspaceId,
|
|
155
|
+
expiresAt: creds.expiresAt,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Clear the stored token for a profile but keep the environment binding. */
|
|
159
|
+
export async function clearProfileToken(name) {
|
|
160
|
+
const stored = await readProfile(name);
|
|
161
|
+
if (!stored)
|
|
162
|
+
return;
|
|
163
|
+
await writeProfile({ name, baseUrl: stored.baseUrl });
|
|
164
|
+
}
|
|
165
|
+
export class UnknownEnvironmentError extends Error {
|
|
166
|
+
constructor(name) {
|
|
167
|
+
super(`Unknown environment "${name}". Pass --base-url <url> to define it ` +
|
|
168
|
+
`(or use a preset: ${Object.keys(PRESET_BASE_URLS).join(", ")}).`);
|
|
169
|
+
this.name = "UnknownEnvironmentError";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Switch the sticky active profile to `name`, creating it if needed. Base URL
|
|
174
|
+
* is resolved from (in order): an explicit `--base-url`, the profile's existing
|
|
175
|
+
* stored URL, or a built-in preset. A name with none of those is an error
|
|
176
|
+
* rather than a silent default. When `--base-url` changes an existing profile's
|
|
177
|
+
* host, the stored token is dropped (it belonged to the old environment).
|
|
178
|
+
*/
|
|
179
|
+
export async function useProfile(name, opts = {}) {
|
|
180
|
+
if (!isValidProfileName(name)) {
|
|
181
|
+
throw new Error(`Invalid profile name "${name}". Use letters, numbers, dashes or underscores.`);
|
|
182
|
+
}
|
|
183
|
+
const existing = await readProfile(name);
|
|
184
|
+
const explicit = opts.baseUrl?.trim();
|
|
185
|
+
const resolvedBaseUrl = (explicit || existing?.baseUrl || presetBaseUrl(name))?.replace(/\/$/, "");
|
|
186
|
+
if (!resolvedBaseUrl) {
|
|
187
|
+
throw new UnknownEnvironmentError(name);
|
|
188
|
+
}
|
|
189
|
+
// A host change invalidates any token minted for the old host. Compare the
|
|
190
|
+
// already-normalized resolvedBaseUrl (trailing slash stripped) against the
|
|
191
|
+
// stored baseUrl (also stripped) so re-using the same host with a trailing
|
|
192
|
+
// slash does NOT read as a change and drop a still-valid token.
|
|
193
|
+
const hostChanged = Boolean(existing?.token) && Boolean(explicit) && resolvedBaseUrl !== existing?.baseUrl;
|
|
194
|
+
const profile = hostChanged
|
|
195
|
+
? { name, baseUrl: resolvedBaseUrl }
|
|
196
|
+
: { ...(existing ?? {}), name, baseUrl: resolvedBaseUrl };
|
|
197
|
+
await writeProfile(profile);
|
|
198
|
+
await setActiveProfileName(name);
|
|
199
|
+
return profile;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* One-shot migration: adopt a pre-profiles `~/.yagni-code/credentials.json` as
|
|
203
|
+
* a named profile so existing logged-in users keep working. No-op once profiles
|
|
204
|
+
* exist (config.json present or any profile file on disk) or when there is no
|
|
205
|
+
* legacy file. The legacy file is removed only after the profile is written.
|
|
206
|
+
*/
|
|
207
|
+
export async function migrateLegacyCredentials() {
|
|
208
|
+
const alreadyMigrated = (await readJson(configPath())) !== null ||
|
|
209
|
+
(await listProfileNames()).length > 0;
|
|
210
|
+
if (alreadyMigrated)
|
|
211
|
+
return;
|
|
212
|
+
const legacy = await readCredentials();
|
|
213
|
+
if (!legacy?.token)
|
|
214
|
+
return;
|
|
215
|
+
const name = legacy.baseUrl.replace(/\/$/, "") === DEFAULT_BASE_URL
|
|
216
|
+
? DISTRIBUTION.defaultProfile
|
|
217
|
+
: "default";
|
|
218
|
+
await persistProfileCredentials(name, legacy);
|
|
219
|
+
await setActiveProfileName(name);
|
|
220
|
+
await deleteCredentials();
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh-at-launch client.
|
|
3
|
+
*
|
|
4
|
+
* When the active profile's token is inside the refresh window (but not yet
|
|
5
|
+
* expired), the launcher proactively rotates it via `POST /auth/refresh` before
|
|
6
|
+
* spawning pi, so a long coding session doesn't 401 mid-flight. Everything here
|
|
7
|
+
* is FAIL-SOFT: a refresh failure (network error, older backend that 404s, a
|
|
8
|
+
* revoked/expired token → 401) leaves the existing token in place and launches
|
|
9
|
+
* with a non-fatal warning. An already-expired token is NOT refreshed here — it
|
|
10
|
+
* stays the login flow's job (`buildLaunch` throws its own login prompt).
|
|
11
|
+
*
|
|
12
|
+
* The pure decision (`shouldRefresh`) is separated from the impure POST
|
|
13
|
+
* (`refreshToken`) so both are unit-testable without a live backend or clock.
|
|
14
|
+
*/
|
|
15
|
+
import type { Credentials } from "./credentials.js";
|
|
16
|
+
/**
|
|
17
|
+
* How close to expiry (7 days) a token must be before launch proactively
|
|
18
|
+
* refreshes it. Wide enough that a normal cadence of use keeps the token fresh
|
|
19
|
+
* without a refresh on every single launch.
|
|
20
|
+
*/
|
|
21
|
+
export declare const REFRESH_WINDOW_MS: number;
|
|
22
|
+
/**
|
|
23
|
+
* Pure: should we attempt a launch-time refresh for a token with this expiry?
|
|
24
|
+
*
|
|
25
|
+
* - unknown/unparseable expiry (older profiles) → no (nothing to reason about)
|
|
26
|
+
* - already expired → no (the login flow owns that case)
|
|
27
|
+
* - within the window → yes
|
|
28
|
+
* - comfortably valid → no
|
|
29
|
+
*
|
|
30
|
+
* Never throws.
|
|
31
|
+
*/
|
|
32
|
+
export declare function shouldRefresh(expiresAt: string | undefined, nowMs: number, windowMs?: number): boolean;
|
|
33
|
+
/** The rotated token payload from `POST /auth/refresh` (contract §2). */
|
|
34
|
+
export interface RefreshResult {
|
|
35
|
+
token: string;
|
|
36
|
+
expiresAt?: string;
|
|
37
|
+
workspaceId?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface RefreshTokenDeps {
|
|
40
|
+
fetchImpl?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Impure: `POST /api/yagni-code/auth/refresh` with the current bearer token.
|
|
44
|
+
* Returns the rotated credentials, or `null` on ANY failure (non-2xx, malformed
|
|
45
|
+
* body, network error, or a 404 from an older backend that lacks the endpoint).
|
|
46
|
+
* Never throws — the caller degrades to the existing token.
|
|
47
|
+
*/
|
|
48
|
+
export declare function refreshToken(creds: Credentials, deps?: RefreshTokenDeps): Promise<RefreshResult | null>;
|
|
49
|
+
export interface MaybeRefreshDeps {
|
|
50
|
+
fetchImpl?: typeof fetch;
|
|
51
|
+
now?: () => number;
|
|
52
|
+
/** Persist the rotated credentials (0600). Omit to skip persistence. */
|
|
53
|
+
persist?: (creds: Credentials) => Promise<void>;
|
|
54
|
+
windowMs?: number;
|
|
55
|
+
}
|
|
56
|
+
export interface MaybeRefreshOutcome {
|
|
57
|
+
/** The rotated credentials when refreshed, otherwise the originals verbatim. */
|
|
58
|
+
creds: Credentials;
|
|
59
|
+
refreshed: boolean;
|
|
60
|
+
/** Non-fatal notices for the launcher to surface (e.g. a soft-failure). */
|
|
61
|
+
warnings: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Orchestrate the launch-time refresh decision + attempt. Fail-soft throughout:
|
|
65
|
+
* outside the window → no-op; inside the window but the POST fails → keep the
|
|
66
|
+
* current token and warn. On success, the rotated token is persisted (when a
|
|
67
|
+
* `persist` seam is provided) and returned for the spawn.
|
|
68
|
+
*/
|
|
69
|
+
export declare function maybeRefreshAtLaunch(creds: Credentials, deps?: MaybeRefreshDeps): Promise<MaybeRefreshOutcome>;
|
|
70
|
+
//# sourceMappingURL=refresh.d.ts.map
|
package/dist/refresh.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh-at-launch client.
|
|
3
|
+
*
|
|
4
|
+
* When the active profile's token is inside the refresh window (but not yet
|
|
5
|
+
* expired), the launcher proactively rotates it via `POST /auth/refresh` before
|
|
6
|
+
* spawning pi, so a long coding session doesn't 401 mid-flight. Everything here
|
|
7
|
+
* is FAIL-SOFT: a refresh failure (network error, older backend that 404s, a
|
|
8
|
+
* revoked/expired token → 401) leaves the existing token in place and launches
|
|
9
|
+
* with a non-fatal warning. An already-expired token is NOT refreshed here — it
|
|
10
|
+
* stays the login flow's job (`buildLaunch` throws its own login prompt).
|
|
11
|
+
*
|
|
12
|
+
* The pure decision (`shouldRefresh`) is separated from the impure POST
|
|
13
|
+
* (`refreshToken`) so both are unit-testable without a live backend or clock.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* How close to expiry (7 days) a token must be before launch proactively
|
|
17
|
+
* refreshes it. Wide enough that a normal cadence of use keeps the token fresh
|
|
18
|
+
* without a refresh on every single launch.
|
|
19
|
+
*/
|
|
20
|
+
export const REFRESH_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
|
+
/** Per-request wall-clock cap so a hung refresh never delays the spawn. */
|
|
22
|
+
const REFRESH_REQUEST_TIMEOUT_MS = 10_000;
|
|
23
|
+
/**
|
|
24
|
+
* Pure: should we attempt a launch-time refresh for a token with this expiry?
|
|
25
|
+
*
|
|
26
|
+
* - unknown/unparseable expiry (older profiles) → no (nothing to reason about)
|
|
27
|
+
* - already expired → no (the login flow owns that case)
|
|
28
|
+
* - within the window → yes
|
|
29
|
+
* - comfortably valid → no
|
|
30
|
+
*
|
|
31
|
+
* Never throws.
|
|
32
|
+
*/
|
|
33
|
+
export function shouldRefresh(expiresAt, nowMs, windowMs = REFRESH_WINDOW_MS) {
|
|
34
|
+
if (!expiresAt)
|
|
35
|
+
return false;
|
|
36
|
+
const expiryMs = Date.parse(expiresAt);
|
|
37
|
+
if (Number.isNaN(expiryMs))
|
|
38
|
+
return false;
|
|
39
|
+
const remaining = expiryMs - nowMs;
|
|
40
|
+
if (remaining <= 0)
|
|
41
|
+
return false;
|
|
42
|
+
return remaining <= windowMs;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Impure: `POST /api/yagni-code/auth/refresh` with the current bearer token.
|
|
46
|
+
* Returns the rotated credentials, or `null` on ANY failure (non-2xx, malformed
|
|
47
|
+
* body, network error, or a 404 from an older backend that lacks the endpoint).
|
|
48
|
+
* Never throws — the caller degrades to the existing token.
|
|
49
|
+
*/
|
|
50
|
+
export async function refreshToken(creds, deps = {}) {
|
|
51
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetchImpl(`${creds.baseUrl}/api/yagni-code/auth/refresh`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"content-type": "application/json",
|
|
57
|
+
authorization: `Bearer ${creds.token}`,
|
|
58
|
+
},
|
|
59
|
+
body: "{}",
|
|
60
|
+
signal: AbortSignal.timeout(REFRESH_REQUEST_TIMEOUT_MS),
|
|
61
|
+
});
|
|
62
|
+
// 404 = older backend without /auth/refresh; 401/403 = revoked/expired.
|
|
63
|
+
// All non-2xx degrade to the existing token.
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
return null;
|
|
66
|
+
const data = (await res.json());
|
|
67
|
+
if (!data || typeof data.token !== "string")
|
|
68
|
+
return null;
|
|
69
|
+
return {
|
|
70
|
+
token: data.token,
|
|
71
|
+
expiresAt: typeof data.expiresAt === "string" ? data.expiresAt : undefined,
|
|
72
|
+
workspaceId: typeof data.workspaceId === "string" ? data.workspaceId : undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Orchestrate the launch-time refresh decision + attempt. Fail-soft throughout:
|
|
81
|
+
* outside the window → no-op; inside the window but the POST fails → keep the
|
|
82
|
+
* current token and warn. On success, the rotated token is persisted (when a
|
|
83
|
+
* `persist` seam is provided) and returned for the spawn.
|
|
84
|
+
*/
|
|
85
|
+
export async function maybeRefreshAtLaunch(creds, deps = {}) {
|
|
86
|
+
const now = deps.now ?? Date.now;
|
|
87
|
+
if (!shouldRefresh(creds.expiresAt, now(), deps.windowMs)) {
|
|
88
|
+
return { creds, refreshed: false, warnings: [] };
|
|
89
|
+
}
|
|
90
|
+
const rotated = await refreshToken(creds, { fetchImpl: deps.fetchImpl });
|
|
91
|
+
if (!rotated) {
|
|
92
|
+
return {
|
|
93
|
+
creds,
|
|
94
|
+
refreshed: false,
|
|
95
|
+
warnings: [
|
|
96
|
+
"Could not refresh your YAGNI Code session; continuing with the current token.",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const next = {
|
|
101
|
+
token: rotated.token,
|
|
102
|
+
baseUrl: creds.baseUrl,
|
|
103
|
+
workspaceId: rotated.workspaceId ?? creds.workspaceId,
|
|
104
|
+
expiresAt: rotated.expiresAt ?? creds.expiresAt,
|
|
105
|
+
};
|
|
106
|
+
if (deps.persist) {
|
|
107
|
+
try {
|
|
108
|
+
await deps.persist(next);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A failed write shouldn't block the spawn; the rotated token is still
|
|
112
|
+
// used for this session, and a later launch re-attempts persistence.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { creds: next, refreshed: true, warnings: [] };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni upgrade` + the launch-time update nudge.
|
|
3
|
+
*
|
|
4
|
+
* npm is the only distribution channel, so "upgrade" means re-running the
|
|
5
|
+
* user's own global installer (`npm`/`pnpm`/`bun`, detected from where the
|
|
6
|
+
* running bin lives) against the published package. The nudge follows the
|
|
7
|
+
* desktop updater's posture: it must never cost anybody a launch. It reads
|
|
8
|
+
* only a local cache (`~/.yagni-code/update-check.json`) — zero network
|
|
9
|
+
* before the agent starts — and refreshes that cache in the background while
|
|
10
|
+
* the session runs, so the NEXT launch nudges from fresh data. Every network
|
|
11
|
+
* path fails soft to "say nothing".
|
|
12
|
+
*
|
|
13
|
+
* Set YAGNI_DISABLE_UPDATE_CHECK=1 to silence the nudge and the background
|
|
14
|
+
* check entirely (managed/enterprise environments); an explicit
|
|
15
|
+
* `yagni upgrade` still works there.
|
|
16
|
+
*/
|
|
17
|
+
export declare const PACKAGE_NAME: string;
|
|
18
|
+
export declare const UPDATE_CHECK_DISABLE_ENV = "YAGNI_DISABLE_UPDATE_CHECK";
|
|
19
|
+
export type InstallMethod = "npm" | "pnpm" | "bun";
|
|
20
|
+
/**
|
|
21
|
+
* The CLI's own version, read from this package's package.json. Resolved
|
|
22
|
+
* relative to the module (src/ and dist/ both sit one level below the package
|
|
23
|
+
* root), so it is correct under tsx in tests and in the published tarball.
|
|
24
|
+
*/
|
|
25
|
+
export declare function currentCliVersion(): string;
|
|
26
|
+
/** `"1"`/anything truthy disables; unset, empty, and `"0"` keep checks on. */
|
|
27
|
+
export declare function updateChecksDisabled(env: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
/** Honors a private-registry override (proxy/mirror setups) like npm does. */
|
|
29
|
+
export declare function registryUrl(env: NodeJS.ProcessEnv): string;
|
|
30
|
+
/** Whether `candidate` is strictly newer than `current`. */
|
|
31
|
+
export declare function isNewerVersion(current: string, candidate: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Extract `dist-tags.latest` from a registry packument. Strict: this string
|
|
34
|
+
* gets printed to the terminal and interpolated into an install command, so
|
|
35
|
+
* anything that is not a plain version is treated as absent.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parsePublishedVersion(body: unknown, tag: string): string | null;
|
|
38
|
+
export declare function parseLatestVersion(body: unknown): string | null;
|
|
39
|
+
export interface FetchLatestDeps {
|
|
40
|
+
env?: NodeJS.ProcessEnv;
|
|
41
|
+
fetchImpl?: typeof fetch;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
/** One fail-soft registry lookup: the latest published version, or null. */
|
|
45
|
+
export declare function fetchLatestVersion(deps?: FetchLatestDeps): Promise<string | null>;
|
|
46
|
+
export interface UpdateCache {
|
|
47
|
+
checkedAt: string;
|
|
48
|
+
latest: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function updateCheckCachePath(): string;
|
|
51
|
+
export declare function readUpdateCache(): Promise<UpdateCache | null>;
|
|
52
|
+
export declare function writeUpdateCache(latest: string): Promise<void>;
|
|
53
|
+
/** The one line the nudge is allowed, or null when there is nothing to say. */
|
|
54
|
+
export declare function updateNudge(current: string, latest: string | null | undefined): string | null;
|
|
55
|
+
export interface NudgeDeps {
|
|
56
|
+
current: string;
|
|
57
|
+
env?: NodeJS.ProcessEnv;
|
|
58
|
+
/** Where the nudge line goes; stderr so it never pollutes piped stdout. */
|
|
59
|
+
log?: (line: string) => void;
|
|
60
|
+
fetchLatest?: () => Promise<string | null>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Print the (cache-backed) nudge, then kick off the background cache refresh.
|
|
64
|
+
* The caller awaits this function — a local file read — but must NOT await
|
|
65
|
+
* `refresh`; it completes during the session and is fire-and-forget.
|
|
66
|
+
*/
|
|
67
|
+
export declare function maybeNudgeAndRefresh(deps: NudgeDeps): Promise<{
|
|
68
|
+
nudged: boolean;
|
|
69
|
+
refresh: Promise<void>;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Which global installer owns the running bin, from its (real) path. The
|
|
73
|
+
* heuristic covers the three installers the package supports; `--method` is
|
|
74
|
+
* the escape hatch when a layout defeats it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function detectInstallMethod(binPath: string): InstallMethod;
|
|
77
|
+
export type ParsedUpgradeArgs = {
|
|
78
|
+
ok: true;
|
|
79
|
+
target?: string;
|
|
80
|
+
method?: InstallMethod;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function parseUpgradeArgs(args: string[]): ParsedUpgradeArgs;
|
|
86
|
+
export interface UpgradeDeps {
|
|
87
|
+
current: string;
|
|
88
|
+
/** The running bin's path, for installer detection. */
|
|
89
|
+
binPath?: string;
|
|
90
|
+
env?: NodeJS.ProcessEnv;
|
|
91
|
+
fetchLatest?: () => Promise<string | null>;
|
|
92
|
+
runInstall?: (command: string, args: string[]) => Promise<number>;
|
|
93
|
+
log?: (line: string) => void;
|
|
94
|
+
logError?: (line: string) => void;
|
|
95
|
+
}
|
|
96
|
+
export declare function upgradeCommand(args: string[], deps: UpgradeDeps): Promise<number>;
|
|
97
|
+
//# sourceMappingURL=upgrade.d.ts.map
|