@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 +55 -0
- package/README.md +161 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +277 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +26 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +74 -0
- package/dist/doctor.d.ts +88 -0
- package/dist/doctor.js +317 -0
- package/dist/launch.d.ts +68 -0
- package/dist/launch.js +106 -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 +22 -0
- package/dist/paths.js +75 -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 +219 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/package.json +46 -0
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
|
package/dist/branding.js
ADDED
|
@@ -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
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Shared CLI config: where the YAGNI backend lives + the environment presets. */
|
|
2
|
+
export declare const DEFAULT_BASE_URL = "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
|