@yagni-app/code 0.1.0

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 ADDED
@@ -0,0 +1,55 @@
1
+ # YAGNI Code License
2
+
3
+ Copyright (c) 2026 YAGNI, Inc. (https://yagni.app). All rights reserved.
4
+
5
+ This software is a proprietary client for the YAGNI service. It is licensed to
6
+ you, not sold. Installing it from a public package registry does not make it
7
+ open source and grants no rights beyond this license.
8
+
9
+ ## License grant
10
+
11
+ Subject to this license and the YAGNI Terms of Service, YAGNI, Inc. ("YAGNI")
12
+ grants you a limited, revocable, non-exclusive, non-transferable license to
13
+ install and run this software, in unmodified form, solely to access and use
14
+ the YAGNI service under an active YAGNI account or workspace.
15
+
16
+ ## Restrictions
17
+
18
+ You may not:
19
+
20
+ - copy, modify, adapt, or create derivative works of the software;
21
+ - distribute, sublicense, rent, lease, sell, or otherwise transfer the
22
+ software or access to it;
23
+ - reverse engineer, decompile, or disassemble the software, except to the
24
+ extent applicable law does not allow that restriction;
25
+ - remove or alter any copyright, trademark, or other proprietary notices; or
26
+ - use the software to build a product or service that competes with the YAGNI
27
+ service.
28
+
29
+ ## Ownership
30
+
31
+ The software and all intellectual property rights in it remain the exclusive
32
+ property of YAGNI and its licensors. Third-party packages installed alongside
33
+ this one are licensed under their own terms.
34
+
35
+ ## Termination
36
+
37
+ This license ends automatically if you breach it or when your access to the
38
+ YAGNI service ends. When it ends, you must stop using the software and
39
+ uninstall it.
40
+
41
+ ## Disclaimer of warranty
42
+
43
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ IMPLIED, INCLUDING WITHOUT LIMITATION THE WARRANTIES OF MERCHANTABILITY,
45
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
46
+
47
+ ## Limitation of liability
48
+
49
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, YAGNI WILL NOT BE LIABLE FOR ANY
50
+ INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES ARISING
51
+ FROM OR RELATED TO THE SOFTWARE, AND YAGNI'S TOTAL LIABILITY WILL NOT EXCEED
52
+ THE AMOUNTS YOU PAID FOR THE YAGNI SERVICE IN THE TWELVE MONTHS BEFORE THE
53
+ CLAIM AROSE.
54
+
55
+ Questions about this license: jack@yagni.app.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # YAGNI Code
2
+
3
+ A terminal coding agent that already knows your company.
4
+
5
+ YAGNI Code is a coding agent for your terminal, like Claude Code or Codex — but
6
+ it is wired into the YAGNI app, your team's shared source of truth for how your
7
+ company and codebase actually work. One YAGNI login routes everything:
8
+
9
+ - the **model** runs on YAGNI-routed open-weight inference (you bring no API
10
+ keys), and
11
+ - the agent **boots already grounded** in your company — conventions,
12
+ decisions, ownership, priorities, the "why" — and can consult it mid-task with
13
+ a built-in `ask_yagni` tool.
14
+
15
+ **The bet:** a cheaper open-weight model that is *grounded in your context*
16
+ beats a stronger model that is *guessing* on your own codebase. Because YAGNI
17
+ Code and the YAGNI app are connected, you intervene less and the agent does more
18
+ correct, autonomous work than a coding agent that starts blank.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install -g @yagni-app/code
24
+ ```
25
+
26
+ The command it installs is `yagni`:
27
+
28
+ ```bash
29
+ yagni login # device-code auth against your YAGNI workspace
30
+ yagni doctor # green/red readiness checklist
31
+ yagni # launch the agent in the current repo
32
+ ```
33
+
34
+ Your workspace must have YAGNI Code enabled (the `yagni_code.enabled` feature
35
+ flag) — ask your workspace admin. Maintainers: the alpha checklist is
36
+ `docs/yagni-code-alpha.md`; publish-day steps live in
37
+ `docs/yagni-code-publishing.md`.
38
+
39
+ ## Develop
40
+
41
+ Working in this monorepo, install from the repo instead of the registry. Build
42
+ the extension first — the CLI loads `pi-extension-yagni` at runtime, so an
43
+ unbuilt extension fails on launch — then build and link the CLI:
44
+
45
+ ```bash
46
+ pnpm --filter pi-extension-yagni build
47
+ pnpm --filter @yagni-app/code build
48
+ npm i -g ./packages/yagni-code-cli # or `pnpm --filter @yagni-app/code link --global`
49
+ ```
50
+
51
+ Both packages are plain `tsc` builds that emit to `dist/`. The global
52
+ `yagni` command is a symlink into this repo's `dist/`, so after the initial
53
+ `npm i -g` you **never reinstall** — a rebuild is enough, and the next launch
54
+ picks it up.
55
+
56
+ Iterate with watch mode (one terminal each — run the commands bare, an inline
57
+ `#` comment is passed through as a filename by zsh and breaks `tsc`):
58
+
59
+ ```bash
60
+ pnpm --filter pi-extension-yagni dev
61
+ pnpm --filter @yagni-app/code dev
62
+ ```
63
+
64
+ Notes:
65
+
66
+ - **Order matters when changing both.** The CLI bundles the extension, so build
67
+ (or have a watcher running) `pi-extension-yagni` before `@yagni-app/code`.
68
+ - **`yagni` is long-lived.** A rebuild only takes effect the next time you
69
+ launch it — restart the CLI to pick up new code.
70
+ - **Frontend/backend changes** (e.g. the `/v1` dev proxy) are served by
71
+ `pnpm dev` — Vite restarts on `vite.config.ts` changes and the backend runs
72
+ under `tsx` watch, so no manual rebuild there.
73
+ - **Tests:** `pnpm --filter @yagni-app/code test` (`node:test` via `tsx`).
74
+
75
+ ## Use
76
+
77
+ ```bash
78
+ yagni login # device-code flow: open the URL, enter the code, approve
79
+ yagni doctor # green/red readiness checklist (engine, token, backend, perms)
80
+ cd ~/code/my-repo
81
+ yagni # launch the agent in this repo (interactive)
82
+ yagni -p "explain the deploy pipeline; ask_yagni if unsure" # one-shot / print mode
83
+ yagni logout # revoke the token and clear it locally
84
+ ```
85
+
86
+ The agent runs on three opaque model tiers routed by YAGNI: `advanced` (the strongest,
87
+ for judgment), `standard` (for execution) and `efficient` (cheapest).
88
+ Interactive sessions default to `advanced`; pass `--model standard` or `--model
89
+ efficient` to switch a run. The `/go` pipeline routes its own steps automatically —
90
+ judgment steps (plan, review) on `advanced`, execution steps (map, implement, fix) on
91
+ `standard`.
92
+
93
+ `login` opens a device-code flow: it prints a short code and a URL. Open the URL
94
+ in a browser where you are signed in to YAGNI, enter the code, and approve. The
95
+ token is stored in the active environment's profile (`~/.yagni-code/profiles/<name>.json`,
96
+ mode `0600`). Re-run `login` any time to refresh it. See **Environments** below to
97
+ point the CLI at a non-prod backend.
98
+
99
+ Any arguments after `yagni` are passed straight through to the agent, so
100
+ flags like `--model` and `--print/-p` work.
101
+
102
+ ### What "knows your company" means in practice
103
+
104
+ - On startup, YAGNI Code loads a brief of your workspace (vision, goals,
105
+ conventions) and works from it immediately — no prompting required.
106
+ - When a choice depends on something organization- or codebase-specific, the
107
+ agent calls **`ask_yagni`** and gets a cited answer from the YAGNI app instead
108
+ of guessing.
109
+
110
+ ## Environments (profiles)
111
+
112
+ YAGNI Code keeps a **sticky active environment**. Each environment ("profile") binds a
113
+ base URL to its own stored token, so you can point the CLI at prod, a local backend, or
114
+ a staging host and switch between them without re-authenticating each time.
115
+
116
+ ```bash
117
+ yagni use local # switch to the local dev backend (preset)
118
+ yagni login # authorize the active environment
119
+ yagni # runs against `local` from now on (sticky)
120
+
121
+ yagni use staging --base-url https://staging.example.com # define a custom env
122
+ yagni login
123
+ yagni profiles # list environments; the active one is *-marked
124
+ yagni use prod # switch back (sticky); prod is the default
125
+ ```
126
+
127
+ - `prod` (→ `https://yagni.app`) is the default environment and a built-in preset.
128
+ - `local` (→ `http://localhost:3456`, the `pnpm dev` backend) is the other preset.
129
+ - Any other name needs `--base-url <url>` the first time you `use` it.
130
+ - Each environment gets its own isolated pi state under `~/.yagni-code/agent/<name>`,
131
+ so prod sessions never bleed into staging.
132
+ - `login` / `logout` always act on the **active** environment. `logout` clears its token
133
+ but keeps the binding, so a later `login` re-authorizes the same environment.
134
+
135
+ ## Configuration
136
+
137
+ | Variable | Default | Purpose |
138
+ | ---------------- | ------------------------ | -------------------------------------------------- |
139
+ | `YAGNI_BASE_URL` | active environment's URL | Override the base URL for a **single run** (escape hatch). Prefer `yagni use` for anything sticky. |
140
+
141
+ Credentials live in `~/.yagni-code/profiles/<name>.json` (mode `0600`); the active
142
+ environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
143
+ `~/.yagni-code/credentials.json` is migrated automatically on first run.
144
+
145
+ ## Troubleshooting
146
+
147
+ - **`Not logged in to environment "<name>" … Run \`yagni login\` first.`** — no
148
+ valid token for the active environment; run `yagni login` (or `yagni use`
149
+ a different one). Run `yagni doctor` for a full green/red readiness check.
150
+ - **`Could not start login … Is YAGNI Code enabled for your workspace?`** —
151
+ YAGNI Code is not enabled for your workspace. Ask an admin to enable it.
152
+ - **The login code expired** — approval wasn't completed in time; run
153
+ `yagni login` again.
154
+ - **Talking to staging** — `yagni use staging --base-url https://<staging-host>`,
155
+ then `yagni login`.
156
+
157
+ ---
158
+
159
+ Maintainers: YAGNI Code is built on the open-source `pi` coding agent, configured
160
+ entirely through a first-party extension (no fork). The engineering details live
161
+ in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
@@ -0,0 +1,25 @@
1
+ /**
2
+ * YAGNI Code launcher branding.
3
+ *
4
+ * pi derives its entire app identity — terminal title, `process.title`, and the
5
+ * name of the env var it reads for its config dir — from `piConfig.name` in the
6
+ * package.json it loads from `PI_PACKAGE_DIR` (pi config.ts: `APP_NAME`,
7
+ * `APP_TITLE`, `ENV_AGENT_DIR`). We rebrand "pi"/"π" out of the chrome by
8
+ * pointing pi at a shadow package whose `piConfig.name` is set to this value.
9
+ *
10
+ * Because the SAME name drives `ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}` +
11
+ * "_CODING_AGENT_DIR"`, this module is the single source of truth: the shadow
12
+ * package.json and the launcher's hermetic-dir env wiring must agree on the
13
+ * name, or pi reads a different env var than the launcher sets and silently
14
+ * falls back to `~/.pi/agent` — breaking the hermetic boundary that keeps the
15
+ * user's local provider keys from bypassing the YAGNI proxy.
16
+ */
17
+ /** The app name pi will compute as `APP_NAME` (shown in the terminal title/tab). */
18
+ export declare const PI_CONFIG_NAME = "YAGNI";
19
+ /**
20
+ * The env var name pi reads for its config dir, derived exactly as pi does
21
+ * (`${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`). For the default name this is
22
+ * `"YAGNI_CODING_AGENT_DIR"`.
23
+ */
24
+ export declare function agentDirEnvVar(name?: string): string;
25
+ //# sourceMappingURL=branding.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * YAGNI Code launcher branding.
3
+ *
4
+ * pi derives its entire app identity — terminal title, `process.title`, and the
5
+ * name of the env var it reads for its config dir — from `piConfig.name` in the
6
+ * package.json it loads from `PI_PACKAGE_DIR` (pi config.ts: `APP_NAME`,
7
+ * `APP_TITLE`, `ENV_AGENT_DIR`). We rebrand "pi"/"π" out of the chrome by
8
+ * pointing pi at a shadow package whose `piConfig.name` is set to this value.
9
+ *
10
+ * Because the SAME name drives `ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}` +
11
+ * "_CODING_AGENT_DIR"`, this module is the single source of truth: the shadow
12
+ * package.json and the launcher's hermetic-dir env wiring must agree on the
13
+ * name, or pi reads a different env var than the launcher sets and silently
14
+ * falls back to `~/.pi/agent` — breaking the hermetic boundary that keeps the
15
+ * user's local provider keys from bypassing the YAGNI proxy.
16
+ */
17
+ /** The app name pi will compute as `APP_NAME` (shown in the terminal title/tab). */
18
+ export const PI_CONFIG_NAME = "YAGNI";
19
+ /**
20
+ * The env var name pi reads for its config dir, derived exactly as pi does
21
+ * (`${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`). For the default name this is
22
+ * `"YAGNI_CODING_AGENT_DIR"`.
23
+ */
24
+ export function agentDirEnvVar(name = PI_CONFIG_NAME) {
25
+ return `${name.toUpperCase()}_CODING_AGENT_DIR`;
26
+ }
27
+ //# sourceMappingURL=branding.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,59 @@
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
+ export declare const HELP_TEXT: string;
16
+ /** Parse `use <name> [--base-url <url>]` argv into its parts. */
17
+ export declare function parseUseArgs(args: string[]): {
18
+ name?: string;
19
+ baseUrl?: string;
20
+ };
21
+ /** `yagni use <name> [--base-url <url>]` — switch the sticky environment. */
22
+ export declare function useCommand(args: string[]): Promise<number>;
23
+ /** `yagni profiles` — list saved environments, marking the active one. */
24
+ export declare function profilesCommand(): Promise<number>;
25
+ /**
26
+ * Whether to show YAGNI Code's own help instead of launching the agent.
27
+ *
28
+ * `--help`/`-h` are intercepted ANYWHERE in argv (not just position 0) so the
29
+ * underlying agent's branded help never leaks (e.g. `yagni <flag> --help`).
30
+ */
31
+ export declare function wantsHelp(argv: string[]): boolean;
32
+ /**
33
+ * Whether to print YAGNI Code's own version instead of launching the agent.
34
+ *
35
+ * `--version` AND `-v` are intercepted ANYWHERE in argv for the same reason as
36
+ * `--help`: pi parses both as its version flag, so letting either through
37
+ * leaks the agent's branded version line (or a login error before it).
38
+ */
39
+ export declare function wantsVersion(argv: string[]): boolean;
40
+ /**
41
+ * The CLI's own version, read from this package's package.json. Resolved
42
+ * relative to the module (src/ and dist/ both sit one level below the package
43
+ * root), so it is correct under tsx in tests and in the published tarball.
44
+ */
45
+ export declare function cliVersion(): string;
46
+ export declare function main(argv: string[]): Promise<number>;
47
+ /**
48
+ * Whether this module is the process entrypoint (vs. imported by a test).
49
+ *
50
+ * Symlink-safe: when `yagni-code` is installed globally, the shell runs it
51
+ * through a symlink in the bin dir, so `process.argv[1]` is that symlink path.
52
+ * Node resolves symlinks for `import.meta.url`, so a raw `argv[1] === metaPath`
53
+ * compare is ALWAYS false for a symlinked install and `main()` never runs (the
54
+ * "nothing happens" bug). We realpath both sides so the symlinked bin and a
55
+ * direct `node dist/cli.js` invocation both count as the entrypoint, while a
56
+ * test import (argv[1] points at the test runner) does not.
57
+ */
58
+ export declare function isEntrypoint(argv1: string | undefined, moduleUrl: string): boolean;
59
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,277 @@
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, readFileSync, realpathSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { PI_CONFIG_NAME } from "./branding.js";
20
+ import { agentDir, piPackageDir } from "./credentials.js";
21
+ import { login } from "./login.js";
22
+ import { logout } from "./logout.js";
23
+ import { buildLaunch } from "./launch.js";
24
+ import { runDoctor } from "./doctor.js";
25
+ import { maybeRefreshAtLaunch } from "./refresh.js";
26
+ import { ensureShadowPiPackage } from "./piPackage.js";
27
+ import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
28
+ import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
29
+ // Present as "yagni" in process listings, not "node".
30
+ process.title = "yagni";
31
+ async function runDefault(passthroughArgs) {
32
+ // Load the active environment's credentials (base URL respects a per-run
33
+ // YAGNI_BASE_URL override). The token must belong to this environment.
34
+ const profile = await readActiveProfile();
35
+ let creds = credentialsFromProfile(profile);
36
+ if (!creds?.token) {
37
+ process.stderr.write(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.\n`);
38
+ return 1;
39
+ }
40
+ // Refresh-at-launch: if the token is inside the 7-day refresh window (and not
41
+ // yet expired), rotate it before spawning so a long session doesn't 401
42
+ // mid-flight. Fail-soft: a refresh failure keeps the current token and warns.
43
+ // Persist ONLY the token rotation — never the (possibly YAGNI_BASE_URL-
44
+ // overridden) base URL, so a single-run override can't become sticky.
45
+ const refresh = await maybeRefreshAtLaunch(creds, {
46
+ persist: (c) => persistProfileTokenRotation(profile.name, c),
47
+ });
48
+ for (const warning of refresh.warnings) {
49
+ process.stderr.write(`${warning}\n`);
50
+ }
51
+ creds = refresh.creds;
52
+ // Hermetic config dir, scoped per environment — created up front (0700) so pi
53
+ // reads/writes its settings, auth, model scope, and theme here (per profile)
54
+ // instead of ~/.pi/agent, and prod state never bleeds into staging.
55
+ const piAgentDir = agentDir(profile.name);
56
+ mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
57
+ // Generate the shadow pi package so the terminal title/process name read
58
+ // "YAGNI Code" instead of "pi"/"π". Best-effort: if it can't be built we
59
+ // still launch (un-rebranded but hermetic), never blocking the agent.
60
+ let shadowPiDir;
61
+ try {
62
+ shadowPiDir = ensureShadowPiPackage({
63
+ realPiDir: resolvePiPackageDir(),
64
+ shadowDir: piPackageDir(),
65
+ name: PI_CONFIG_NAME,
66
+ });
67
+ }
68
+ catch {
69
+ shadowPiDir = undefined;
70
+ }
71
+ // buildLaunch runs the token-expiry preflight: it throws (with an actionable
72
+ // login prompt) on an already-expired token so we never spawn a session that
73
+ // immediately 401s, and returns non-fatal warnings (e.g. expiry approaching).
74
+ // `creds` was read fresh from the profile file above, so an out-of-band
75
+ // re-login in another terminal is picked up here with no stale cached token.
76
+ let plan;
77
+ try {
78
+ plan = buildLaunch(creds, passthroughArgs, {
79
+ extensionPath: resolveExtensionPath(),
80
+ agentDir: piAgentDir,
81
+ piPackageDir: shadowPiDir,
82
+ // Forward the active profile's file path so the extension can persist a
83
+ // mid-session token rotation back to the same profile the launcher read.
84
+ profilePath: profilePath(profile.name),
85
+ baseEnv: process.env,
86
+ });
87
+ }
88
+ catch (err) {
89
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
90
+ return 1;
91
+ }
92
+ for (const warning of plan.warnings) {
93
+ process.stderr.write(`${warning}\n`);
94
+ }
95
+ const { env, argv } = plan;
96
+ const piCli = resolvePiCliPath();
97
+ return await new Promise((resolve) => {
98
+ const child = spawn(process.execPath, [piCli, ...argv], {
99
+ stdio: "inherit",
100
+ env,
101
+ });
102
+ child.on("exit", (code) => resolve(code ?? 0));
103
+ child.on("error", (err) => {
104
+ process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
105
+ resolve(1);
106
+ });
107
+ });
108
+ }
109
+ export const HELP_TEXT = [
110
+ "YAGNI Code — a business-context-grounded terminal coding agent.",
111
+ "",
112
+ "Usage:",
113
+ " yagni login Authorize the active environment (device-code flow).",
114
+ " yagni logout Revoke and clear the active environment's token.",
115
+ " yagni doctor Check that everything is ready (green/red checklist).",
116
+ " yagni use <name> Switch the active environment (sticky).",
117
+ " Presets: prod, local. Others need --base-url <url>.",
118
+ " yagni profiles List saved environments; the active one is marked.",
119
+ " yagni version Print the CLI version.",
120
+ " yagni [args…] Launch the agent in the current repo.",
121
+ "",
122
+ "The active environment is sticky; `use` switches it (prod is the default).",
123
+ "Set YAGNI_BASE_URL to override the base URL for a single run.",
124
+ ].join("\n");
125
+ /** Parse `use <name> [--base-url <url>]` argv into its parts. */
126
+ export function parseUseArgs(args) {
127
+ let name;
128
+ let baseUrl;
129
+ for (let i = 0; i < args.length; i++) {
130
+ const a = args[i];
131
+ if (a === "--base-url") {
132
+ baseUrl = args[++i];
133
+ continue;
134
+ }
135
+ if (a.startsWith("--base-url=")) {
136
+ baseUrl = a.slice("--base-url=".length);
137
+ continue;
138
+ }
139
+ if (!a.startsWith("-") && name === undefined)
140
+ name = a;
141
+ }
142
+ return { name, baseUrl };
143
+ }
144
+ /** `yagni use <name> [--base-url <url>]` — switch the sticky environment. */
145
+ export async function useCommand(args) {
146
+ const { name, baseUrl } = parseUseArgs(args);
147
+ if (!name) {
148
+ process.stderr.write("Usage: yagni use <name> [--base-url <url>]\n");
149
+ return 1;
150
+ }
151
+ try {
152
+ const profile = await useProfile(name, { baseUrl });
153
+ const hint = profile.token ? "" : " — run `yagni login` to authorize it";
154
+ process.stdout.write(`Active environment: ${profile.name} → ${profile.baseUrl}${hint}\n`);
155
+ return 0;
156
+ }
157
+ catch (err) {
158
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
159
+ return 1;
160
+ }
161
+ }
162
+ /** `yagni profiles` — list saved environments, marking the active one. */
163
+ export async function profilesCommand() {
164
+ const active = await getActiveProfileName();
165
+ const stored = await listProfiles();
166
+ const rows = [...stored];
167
+ // Surface the active environment even if it has no saved file yet.
168
+ if (!rows.some((p) => p.name === active))
169
+ rows.push(await readActiveProfile());
170
+ rows.sort((a, b) => a.name.localeCompare(b.name));
171
+ for (const p of rows) {
172
+ const marker = p.name === active ? "*" : " ";
173
+ const status = p.token ? "logged in" : "not logged in";
174
+ process.stdout.write(`${marker} ${p.name.padEnd(12)} ${p.baseUrl} (${status})\n`);
175
+ }
176
+ return 0;
177
+ }
178
+ /**
179
+ * Whether to show YAGNI Code's own help instead of launching the agent.
180
+ *
181
+ * `--help`/`-h` are intercepted ANYWHERE in argv (not just position 0) so the
182
+ * underlying agent's branded help never leaks (e.g. `yagni <flag> --help`).
183
+ */
184
+ export function wantsHelp(argv) {
185
+ return argv[0] === "help" || argv.includes("--help") || argv.includes("-h");
186
+ }
187
+ /**
188
+ * Whether to print YAGNI Code's own version instead of launching the agent.
189
+ *
190
+ * `--version` AND `-v` are intercepted ANYWHERE in argv for the same reason as
191
+ * `--help`: pi parses both as its version flag, so letting either through
192
+ * leaks the agent's branded version line (or a login error before it).
193
+ */
194
+ export function wantsVersion(argv) {
195
+ return argv[0] === "version" || argv.includes("--version") || argv.includes("-v");
196
+ }
197
+ /**
198
+ * The CLI's own version, read from this package's package.json. Resolved
199
+ * relative to the module (src/ and dist/ both sit one level below the package
200
+ * root), so it is correct under tsx in tests and in the published tarball.
201
+ */
202
+ export function cliVersion() {
203
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
204
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
205
+ return pkg.version ?? "0.0.0";
206
+ }
207
+ export async function main(argv) {
208
+ const [command, ...rest] = argv;
209
+ // Help never touches storage; everything else first adopts any pre-profiles
210
+ // credentials.json as a named profile (one-shot, no-op once migrated).
211
+ if (wantsHelp(argv)) {
212
+ process.stdout.write(`${HELP_TEXT}\n`);
213
+ return 0;
214
+ }
215
+ // Bare semver on stdout (script-friendly); like help, never touches storage.
216
+ if (wantsVersion(argv)) {
217
+ process.stdout.write(`${cliVersion()}\n`);
218
+ return 0;
219
+ }
220
+ await migrateLegacyCredentials();
221
+ // login/logout act on the active environment; use/profiles manage which one
222
+ // that is. Anything else is passed straight through to the agent.
223
+ if (command === "login") {
224
+ const profile = await readActiveProfile();
225
+ await login({ baseUrl: profile.baseUrl, profileName: profile.name });
226
+ return 0;
227
+ }
228
+ if (command === "logout") {
229
+ await logout();
230
+ return 0;
231
+ }
232
+ if (command === "doctor") {
233
+ return runDoctor();
234
+ }
235
+ if (command === "use") {
236
+ return useCommand(rest);
237
+ }
238
+ if (command === "profiles") {
239
+ return profilesCommand();
240
+ }
241
+ return runDefault(command === undefined ? [] : [command, ...rest]);
242
+ }
243
+ /**
244
+ * Whether this module is the process entrypoint (vs. imported by a test).
245
+ *
246
+ * Symlink-safe: when `yagni-code` is installed globally, the shell runs it
247
+ * through a symlink in the bin dir, so `process.argv[1]` is that symlink path.
248
+ * Node resolves symlinks for `import.meta.url`, so a raw `argv[1] === metaPath`
249
+ * compare is ALWAYS false for a symlinked install and `main()` never runs (the
250
+ * "nothing happens" bug). We realpath both sides so the symlinked bin and a
251
+ * direct `node dist/cli.js` invocation both count as the entrypoint, while a
252
+ * test import (argv[1] points at the test runner) does not.
253
+ */
254
+ export function isEntrypoint(argv1, moduleUrl) {
255
+ if (argv1 === undefined)
256
+ return false;
257
+ const resolve = (p) => {
258
+ try {
259
+ return realpathSync(p);
260
+ }
261
+ catch {
262
+ return p; // path may not exist on disk; fall back to the raw value
263
+ }
264
+ };
265
+ return resolve(argv1) === resolve(fileURLToPath(moduleUrl));
266
+ }
267
+ // Only auto-run when invoked as the CLI entry, so tests can import this module
268
+ // (e.g. to exercise wantsHelp) without spawning the agent.
269
+ if (isEntrypoint(process.argv[1], import.meta.url)) {
270
+ main(process.argv.slice(2))
271
+ .then((code) => process.exit(code))
272
+ .catch((err) => {
273
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
274
+ process.exit(1);
275
+ });
276
+ }
277
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,19 @@
1
+ /** Shared CLI config: where the YAGNI backend lives + the environment presets. */
2
+ export declare const DEFAULT_BASE_URL = "https://yagni.app";
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,26 @@
1
+ /** Shared CLI config: where the YAGNI backend lives + the environment presets. */
2
+ export const DEFAULT_BASE_URL = "https://yagni.app";
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 const PRESET_BASE_URLS = {
11
+ prod: DEFAULT_BASE_URL,
12
+ local: "http://localhost:3456",
13
+ };
14
+ /** The base URL a named preset resolves to, or undefined for a custom name. */
15
+ export function presetBaseUrl(name) {
16
+ return PRESET_BASE_URLS[name];
17
+ }
18
+ /**
19
+ * The YAGNI_BASE_URL env override (back-compat / per-run escape hatch). Profiles
20
+ * are the primary mechanism now; this still wins when set so a one-off run can
21
+ * target an ad-hoc host without flipping the active profile.
22
+ */
23
+ export function resolveBaseUrl(env = process.env) {
24
+ return (env.YAGNI_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, "");
25
+ }
26
+ //# sourceMappingURL=config.js.map