@halyard/cli 0.1.2 → 0.2.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/README.md +88 -1
- package/dist/cli.js +27 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.d.ts.map +1 -0
- package/dist/commands/api-keys.js +90 -0
- package/dist/commands/api-keys.js.map +1 -0
- package/dist/commands/hook.d.ts +6 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +106 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +111 -14
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.d.ts.map +1 -0
- package/dist/commands/orgs.js +102 -0
- package/dist/commands/orgs.js.map +1 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +29 -6
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/sessions.d.ts +3 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +139 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +28 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +171 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +51 -49
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/users.d.ts +13 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +205 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +77 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/lib/api-client.d.ts +64 -2
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +32 -7
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/auth.d.ts +42 -6
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +101 -27
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config-files.d.ts +6 -0
- package/dist/lib/config-files.d.ts.map +1 -0
- package/dist/lib/config-files.js +27 -0
- package/dist/lib/config-files.js.map +1 -0
- package/dist/lib/config.d.ts +13 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +22 -8
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot-export.d.ts +31 -0
- package/dist/lib/copilot-export.d.ts.map +1 -0
- package/dist/lib/copilot-export.js +129 -0
- package/dist/lib/copilot-export.js.map +1 -0
- package/dist/lib/discovery.d.ts +60 -2
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +187 -15
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/hook-discovery.d.ts +43 -0
- package/dist/lib/hook-discovery.d.ts.map +1 -0
- package/dist/lib/hook-discovery.js +90 -0
- package/dist/lib/hook-discovery.js.map +1 -0
- package/dist/lib/hook-input.d.ts +36 -0
- package/dist/lib/hook-input.d.ts.map +1 -0
- package/dist/lib/hook-input.js +161 -0
- package/dist/lib/hook-input.js.map +1 -0
- package/dist/lib/hook-runner.d.ts +48 -0
- package/dist/lib/hook-runner.d.ts.map +1 -0
- package/dist/lib/hook-runner.js +306 -0
- package/dist/lib/hook-runner.js.map +1 -0
- package/dist/lib/http.d.ts +18 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +54 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/opencode-export.d.ts +22 -0
- package/dist/lib/opencode-export.d.ts.map +1 -0
- package/dist/lib/opencode-export.js +53 -0
- package/dist/lib/opencode-export.js.map +1 -0
- package/dist/lib/output.d.ts +27 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +74 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/preflight.d.ts +35 -0
- package/dist/lib/preflight.d.ts.map +1 -0
- package/dist/lib/preflight.js +54 -0
- package/dist/lib/preflight.js.map +1 -0
- package/dist/lib/repo-binding.d.ts +43 -0
- package/dist/lib/repo-binding.d.ts.map +1 -0
- package/dist/lib/repo-binding.js +158 -0
- package/dist/lib/repo-binding.js.map +1 -0
- package/dist/lib/repo-identity.d.ts +29 -0
- package/dist/lib/repo-identity.d.ts.map +1 -0
- package/dist/lib/repo-identity.js +113 -0
- package/dist/lib/repo-identity.js.map +1 -0
- package/dist/lib/setup-hooks.d.ts +41 -0
- package/dist/lib/setup-hooks.d.ts.map +1 -0
- package/dist/lib/setup-hooks.js +276 -0
- package/dist/lib/setup-hooks.js.map +1 -0
- package/dist/lib/setup.d.ts +4 -1
- package/dist/lib/setup.d.ts.map +1 -1
- package/dist/lib/setup.js +20 -31
- package/dist/lib/setup.js.map +1 -1
- package/dist/lib/skill-batches.d.ts +16 -0
- package/dist/lib/skill-batches.d.ts.map +1 -0
- package/dist/lib/skill-batches.js +35 -0
- package/dist/lib/skill-batches.js.map +1 -0
- package/dist/lib/skill-files.d.ts +4 -0
- package/dist/lib/skill-files.d.ts.map +1 -0
- package/dist/lib/skill-files.js +36 -0
- package/dist/lib/skill-files.js.map +1 -0
- package/dist/lib/skill-fingerprint.d.ts +55 -0
- package/dist/lib/skill-fingerprint.d.ts.map +1 -0
- package/dist/lib/skill-fingerprint.js +204 -0
- package/dist/lib/skill-fingerprint.js.map +1 -0
- package/dist/lib/skills-discovery.d.ts +57 -0
- package/dist/lib/skills-discovery.d.ts.map +1 -0
- package/dist/lib/skills-discovery.js +157 -0
- package/dist/lib/skills-discovery.js.map +1 -0
- package/dist/lib/skills-sync.d.ts +66 -0
- package/dist/lib/skills-sync.d.ts.map +1 -0
- package/dist/lib/skills-sync.js +251 -0
- package/dist/lib/skills-sync.js.map +1 -0
- package/dist/lib/sync-runner.d.ts +56 -0
- package/dist/lib/sync-runner.d.ts.map +1 -0
- package/dist/lib/sync-runner.js +106 -0
- package/dist/lib/sync-runner.js.map +1 -0
- package/dist/lib/sync-state.d.ts +36 -6
- package/dist/lib/sync-state.d.ts.map +1 -1
- package/dist/lib/sync-state.js +41 -11
- package/dist/lib/sync-state.js.map +1 -1
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @halyard/cli
|
|
2
2
|
|
|
3
|
-
Command-line companion for [Halyard](https://usehalyard.ai) — scaffold Halyard into your repo for Claude Code, Cursor, and Codex, and sync your coding
|
|
3
|
+
Command-line companion for [Halyard](https://usehalyard.ai) — scaffold Halyard into your repo for Claude Code, Cursor, and Codex, and sync your coding-agent sessions back to the platform.
|
|
4
|
+
|
|
5
|
+
`halyard sync` discovers sessions from **Claude Code** (`~/.claude/projects/`), **Codex CLI** (`~/.codex/sessions/`), **Gemini CLI** (`~/.gemini/tmp/*/chats/`), **OpenCode** (via `opencode export`), and **GitHub Copilot CLI** (`~/.copilot/session-store.db`, Node ≥ 22.13). Each session becomes searchable knowledge plus per-person work metrics — cost (USD), turns, duration, tokens, and tool activity.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -18,10 +20,95 @@ halyard setup # Scaffold Halyard into the current repo (alias: init)
|
|
|
18
20
|
halyard login # Authenticate via your browser
|
|
19
21
|
halyard sync # Upload new session logs to Halyard
|
|
20
22
|
halyard push # Push a single session file
|
|
23
|
+
halyard whoami # Show the authenticated user, org, and repo binding
|
|
24
|
+
halyard skills # Discover local agent skills (SKILL.md folders) and sync them to Halyard
|
|
21
25
|
```
|
|
22
26
|
|
|
23
27
|
Run `halyard --help` for the full list of flags.
|
|
24
28
|
|
|
29
|
+
## Repo binding (`.halyard.json`)
|
|
30
|
+
|
|
31
|
+
A committed `.halyard.json` at the repo root binds the repo to one organization:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "org": "halyard-studio" }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Create it with `halyard setup --org <slug>` (add `--force` to rebind an already-bound
|
|
38
|
+
repo). The binding is discovered by walking up from the current directory; detached
|
|
39
|
+
git worktrees fall back to the main checkout's binding via `git rev-parse
|
|
40
|
+
--git-common-dir`.
|
|
41
|
+
|
|
42
|
+
In a bound repo:
|
|
43
|
+
|
|
44
|
+
- `halyard sync` uploads only sessions whose working directory resolves to the same
|
|
45
|
+
repository (matched by git common dir, then by normalized `remote.origin.url`).
|
|
46
|
+
Sessions whose repo can't be determined are skipped and counted; `halyard sync --all`
|
|
47
|
+
restores machine-wide sync.
|
|
48
|
+
- `halyard sync` and `halyard push` print `Syncing <scope> → <org>` before uploading
|
|
49
|
+
and hard-fail if your credentials belong to a different org than the binding —
|
|
50
|
+
re-run `halyard login` inside the repo to fix that.
|
|
51
|
+
- `halyard login` stores workspace-scoped credentials for the bound org (see below).
|
|
52
|
+
|
|
53
|
+
Without a binding, `sync` keeps the old machine-wide behavior and prints a notice.
|
|
54
|
+
|
|
55
|
+
## Credentials
|
|
56
|
+
|
|
57
|
+
Resolution order for every command:
|
|
58
|
+
|
|
59
|
+
1. `HALYARD_TOKEN` env var — an `sk_halyard_...` API key (or raw token) for
|
|
60
|
+
headless/CI use. API keys are bound to one org server-side; the CLI cannot
|
|
61
|
+
verify that org against a repo binding, so it warns instead of hard-failing.
|
|
62
|
+
2. `~/.halyard/credentials/<orgId>.json` — per-org OAuth chains, matched against
|
|
63
|
+
the repo binding's org slug. Written by `halyard login` inside a bound repo.
|
|
64
|
+
3. `~/.halyard/credentials.json` — the legacy/default chain, written by
|
|
65
|
+
`halyard login` outside a bound repo and used by `halyard orgs switch`.
|
|
66
|
+
|
|
67
|
+
Token refresh always writes back to the same file the credentials were read from.
|
|
68
|
+
Set `HALYARD_HOME` to relocate the state directory (defaults to `~/.halyard`).
|
|
69
|
+
|
|
70
|
+
## Environment variables
|
|
71
|
+
|
|
72
|
+
| Variable | Purpose |
|
|
73
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
74
|
+
| `HALYARD_TOKEN` | API key / token override for headless use (beats stored auth) |
|
|
75
|
+
| `HALYARD_API_URL` | Default for `--api-url` (the flag still wins) |
|
|
76
|
+
| `HALYARD_HOME` | State directory override (default `~/.halyard`) |
|
|
77
|
+
|
|
78
|
+
## Upload every session automatically
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
halyard setup --org <your-org-slug>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`setup --org` binds the repo (see above) and writes a launcher, `.halyard/hook.sh`, plus session-start / session-end hook config for **Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, OpenCode and Grok Build**. Each hook runs `halyard hook`, which uploads the transcript that just finished at session end and sweeps this repo's earlier transcripts that never uploaded at session start. Commit the generated files and every clone gets the behaviour. Re-running `setup` is idempotent; `--no-hooks` skips this part.
|
|
85
|
+
|
|
86
|
+
`halyard hook` reads the client's hook payload from stdin, resolves credentials the same way `sync` and `push` do (`HALYARD_TOKEN` from the environment or the repo's `.env`, then the per-org login for the bound org, then the default login), skips instead of uploading when login credentials belong to another org than `.halyard.json`, never blocks the agent, and logs to `~/.halyard/hooks.log`. Claude and Codex sweeps use the same repo-scoped matching as `halyard sync`. Run `halyard hook --help` for the flags.
|
|
87
|
+
|
|
88
|
+
Upload happens at session **end**, not on every turn: the ingest endpoint keeps the first upload it sees for a session id. See [`docs/agent-session-upload.md`](https://github.com/halyard-labs/halyard/blob/main/docs/agent-session-upload.md) for the per-client details.
|
|
89
|
+
|
|
90
|
+
## Sync your agent skills
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
halyard skills list # every SKILL.md folder on this machine, with its fingerprint
|
|
94
|
+
halyard skills sync # upload new or changed skills to the bound org
|
|
95
|
+
halyard skills sync --scope project --dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`skills sync` scans the directories every harness reads — user-level (`~/.claude/skills`, `~/.agents/skills`, `~/.codex/skills`, `~/.cursor/skills`, `~/.gemini/skills`, `~/.copilot/skills`, OpenCode, Amp, Windsurf, Grok, Kiro, Cline, Factory) and repo-level (`.claude/skills`, `.agents/skills`, `.github/skills`, …) — de-duplicates symlink aliases by real path, fingerprints each folder (sha256 over every file, not just `SKILL.md`), and uploads what changed. Each skill becomes a `PROCESS` knowledge entry, searchable by every agent connected to the org. Re-runs are free: unchanged skills are skipped from the local ledger, and the server compares the fingerprint again for anything the ledger missed. The session-start hook syncs the repo's committed skills automatically (`HALYARD_SKILLS_SYNC=0` turns that off); personal skills only go up when you run the command. See [`docs/agent-skills-sync.md`](https://github.com/halyard-labs/halyard/blob/main/docs/agent-skills-sync.md).
|
|
99
|
+
|
|
100
|
+
### Headless and cloud environments
|
|
101
|
+
|
|
102
|
+
Set `HALYARD_TOKEN` to an API key in the environment (one key per environment; keys are bound to one organization):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
halyard api-keys create --name "my-repo-ci" # prints sk_halyard_… once
|
|
106
|
+
export HALYARD_TOKEN=sk_halyard_…
|
|
107
|
+
halyard sync --provider claude # or push <file>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Where to set it: Claude Code on the web → environment variables; Codex Cloud → environment variables (not secrets, which are removed before the agent phase); Cursor cloud agents → dashboard Secrets; GitHub Copilot coding agent → Agents secrets; GitHub Actions → `secrets.HALYARD_TOKEN` and a final `if: always()` step running `halyard sync`.
|
|
111
|
+
|
|
25
112
|
## Learn more
|
|
26
113
|
|
|
27
114
|
- Product: [usehalyard.ai](https://usehalyard.ai)
|
package/dist/cli.js
CHANGED
|
@@ -6,16 +6,39 @@ import { loginCommand } from './commands/login.js';
|
|
|
6
6
|
import { syncCommand } from './commands/sync.js';
|
|
7
7
|
import { pushCommand } from './commands/push.js';
|
|
8
8
|
import { setupCommand } from './commands/setup.js';
|
|
9
|
-
|
|
9
|
+
import { whoamiCommand } from './commands/whoami.js';
|
|
10
|
+
import { apiKeysCommand } from './commands/api-keys.js';
|
|
11
|
+
import { usersCommand } from './commands/users.js';
|
|
12
|
+
import { orgsCommand } from './commands/orgs.js';
|
|
13
|
+
import { sessionsCommand } from './commands/sessions.js';
|
|
14
|
+
import { hookCommand } from './commands/hook.js';
|
|
15
|
+
import { skillsCommand } from './commands/skills.js';
|
|
16
|
+
function resolveVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
19
|
+
return packageJson.version;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return typeof __HALYARD_CLI_VERSION__ === 'string' ? __HALYARD_CLI_VERSION__ : '0.0.0-dev';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
10
25
|
const program = new Command();
|
|
11
26
|
program
|
|
12
27
|
.name('halyard')
|
|
13
|
-
.description('Set up Halyard in your repo and sync coding session logs')
|
|
14
|
-
.version(
|
|
15
|
-
.option('--api-url <url>', 'Halyard API URL', DEFAULT_API_URL)
|
|
28
|
+
.description('Set up Halyard in your repo and sync coding session logs and agent skills')
|
|
29
|
+
.version(resolveVersion())
|
|
30
|
+
.option('--api-url <url>', 'Halyard API URL (env: HALYARD_API_URL)', process.env['HALYARD_API_URL'] ?? DEFAULT_API_URL)
|
|
31
|
+
.option('--json', 'Output raw JSON');
|
|
16
32
|
program.addCommand(setupCommand());
|
|
17
33
|
program.addCommand(loginCommand());
|
|
18
34
|
program.addCommand(syncCommand());
|
|
19
35
|
program.addCommand(pushCommand());
|
|
36
|
+
program.addCommand(whoamiCommand());
|
|
37
|
+
program.addCommand(apiKeysCommand());
|
|
38
|
+
program.addCommand(usersCommand());
|
|
39
|
+
program.addCommand(orgsCommand());
|
|
40
|
+
program.addCommand(sessionsCommand());
|
|
41
|
+
program.addCommand(hookCommand());
|
|
42
|
+
program.addCommand(skillsCommand());
|
|
20
43
|
program.parse();
|
|
21
44
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAMpD,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAC5C,CAAA;QACxB,OAAO,WAAW,CAAC,OAAO,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,uBAAuB,KAAK,QAAQ,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,WAAW,CAAA;IAC5F,CAAC;AACH,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,2EAA2E,CAAC;KACxF,OAAO,CAAC,cAAc,EAAE,CAAC;KACzB,MAAM,CACL,iBAAiB,EACjB,wCAAwC,EACxC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,eAAe,CAClD;KACA,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAA;AAEtC,OAAO,CAAC,UAAU,CAAC,YAAY,EAAE,CAAC,CAAA;AAClC,OAAO,CAAC,UAAU,CAAC,YAAY,EAAE,CAAC,CAAA;AAClC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAA;AACjC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAA;AACjC,OAAO,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC,CAAA;AACnC,OAAO,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC,CAAA;AACpC,OAAO,CAAC,UAAU,CAAC,YAAY,EAAE,CAAC,CAAA;AAClC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAA;AACjC,OAAO,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAA;AACrC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAA;AACjC,OAAO,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC,CAAA;AAEnC,OAAO,CAAC,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../../src/commands/api-keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAsBnC,wBAAgB,cAAc,IAAI,OAAO,CAkGxC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { apiFetch, ApiError } from '../lib/http.js';
|
|
3
|
+
import { printTable, printJson, formatDate, confirm, fail } from '../lib/output.js';
|
|
4
|
+
export function apiKeysCommand() {
|
|
5
|
+
const apiKeys = new Command('api-keys').description('Manage API keys (requires `halyard login`; keys belong to the current organization)');
|
|
6
|
+
apiKeys
|
|
7
|
+
.command('create')
|
|
8
|
+
.description('Create a new API key. The secret is shown once and cannot be retrieved again.')
|
|
9
|
+
.requiredOption('--name <name>', 'Display name for the key')
|
|
10
|
+
.action(async function () {
|
|
11
|
+
const globalOpts = this.optsWithGlobals();
|
|
12
|
+
const apiUrl = globalOpts.apiUrl;
|
|
13
|
+
const name = this.opts().name;
|
|
14
|
+
let key;
|
|
15
|
+
try {
|
|
16
|
+
key = await apiFetch(apiUrl, '/api/v1/api-keys', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
body: { name },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
23
|
+
}
|
|
24
|
+
if (globalOpts.json) {
|
|
25
|
+
printJson(key);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`Created API key "${key.name}" (${key.id})`);
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(` ${key.secret}`);
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log('Store this secret now - it will not be shown again.');
|
|
33
|
+
console.log('Use it via the HALYARD_TOKEN environment variable.');
|
|
34
|
+
});
|
|
35
|
+
apiKeys
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('List your active API keys')
|
|
38
|
+
.action(async function () {
|
|
39
|
+
const globalOpts = this.optsWithGlobals();
|
|
40
|
+
const apiUrl = globalOpts.apiUrl;
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await apiFetch(apiUrl, '/api/v1/api-keys');
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
47
|
+
}
|
|
48
|
+
if (globalOpts.json) {
|
|
49
|
+
printJson(response);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (response.keys.length === 0) {
|
|
53
|
+
console.log('No API keys. Create one with `halyard api-keys create --name <name>`.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
printTable(response.keys, [
|
|
57
|
+
{ key: 'id', header: 'ID' },
|
|
58
|
+
{ key: 'name', header: 'NAME' },
|
|
59
|
+
{ key: 'prefix', header: 'PREFIX', format: (v) => `${String(v)}...` },
|
|
60
|
+
{ key: 'lastUsedAt', header: 'LAST USED', format: formatDate },
|
|
61
|
+
{ key: 'createdAt', header: 'CREATED', format: formatDate },
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
apiKeys
|
|
65
|
+
.command('revoke')
|
|
66
|
+
.description('Revoke an API key (immediate; the key stops authenticating on its next use)')
|
|
67
|
+
.argument('<id>', 'API key ID (from `halyard api-keys list`)')
|
|
68
|
+
.option('-f, --force', 'Skip the confirmation prompt')
|
|
69
|
+
.action(async function (id) {
|
|
70
|
+
const globalOpts = this.optsWithGlobals();
|
|
71
|
+
const apiUrl = globalOpts.apiUrl;
|
|
72
|
+
const force = !!this.opts().force;
|
|
73
|
+
const confirmed = await confirm(`Revoke API key ${id}? This cannot be undone.`, { force });
|
|
74
|
+
if (!confirmed) {
|
|
75
|
+
fail('Aborted.');
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await apiFetch(apiUrl, `/api/v1/api-keys/${id}`, { method: 'DELETE' });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
82
|
+
fail('API key not found (or not yours).');
|
|
83
|
+
}
|
|
84
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
85
|
+
}
|
|
86
|
+
console.log('Key revoked.');
|
|
87
|
+
});
|
|
88
|
+
return apiKeys;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=api-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../../src/commands/api-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/** The parent passes the hook payload to its detached child through this
|
|
3
|
+
* variable, because the child has no stdin. */
|
|
4
|
+
export declare const HOOK_INPUT_ENV = "HALYARD_HOOK_INPUT";
|
|
5
|
+
export declare function hookCommand(): Command;
|
|
6
|
+
//# sourceMappingURL=hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../src/commands/hook.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAYnC;gDACgD;AAChD,eAAO,MAAM,cAAc,uBAAuB,CAAA;AAoFlD,wBAAgB,WAAW,IAAI,OAAO,CA0BrC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { halyardDir } from '../lib/config.js';
|
|
6
|
+
import { HOOK_CLIENTS, isHookClient, normalizeEvent, parseHookInput, } from '../lib/hook-input.js';
|
|
7
|
+
import { HookLogger, runHook } from '../lib/hook-runner.js';
|
|
8
|
+
/** The parent passes the hook payload to its detached child through this
|
|
9
|
+
* variable, because the child has no stdin. */
|
|
10
|
+
export const HOOK_INPUT_ENV = 'HALYARD_HOOK_INPUT';
|
|
11
|
+
const STDIN_TIMEOUT_MS = 2000;
|
|
12
|
+
function readStdin(timeoutMs) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
let data = '';
|
|
15
|
+
const finish = () => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
process.stdin.pause();
|
|
18
|
+
resolve(data);
|
|
19
|
+
};
|
|
20
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
21
|
+
process.stdin.setEncoding('utf8');
|
|
22
|
+
process.stdin.on('data', (chunk) => {
|
|
23
|
+
data += chunk;
|
|
24
|
+
});
|
|
25
|
+
process.stdin.on('end', finish);
|
|
26
|
+
process.stdin.on('error', finish);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function parseEvent(value) {
|
|
30
|
+
if (value === 'start' || value === 'end' || value === 'sweep')
|
|
31
|
+
return value;
|
|
32
|
+
return value ? normalizeEvent(value) : null;
|
|
33
|
+
}
|
|
34
|
+
function childArgs(opts, apiUrl, event) {
|
|
35
|
+
const args = [process.argv[1] ?? '', '--api-url', apiUrl, 'hook', '--event', event];
|
|
36
|
+
if (opts.client)
|
|
37
|
+
args.push('--client', opts.client);
|
|
38
|
+
if (opts.root)
|
|
39
|
+
args.push('--root', opts.root);
|
|
40
|
+
if (opts.transcript)
|
|
41
|
+
args.push('--transcript', opts.transcript);
|
|
42
|
+
if (opts.dryRun)
|
|
43
|
+
args.push('--dry-run');
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
/** Run the upload in a background process so the host's hook budget is never
|
|
47
|
+
* hit (Codex caps SessionEnd at 3s) and session start is never delayed. */
|
|
48
|
+
function detach(args, input) {
|
|
49
|
+
const child = spawn(process.execPath, args, {
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: 'ignore',
|
|
52
|
+
env: { ...process.env, [HOOK_INPUT_ENV]: input },
|
|
53
|
+
});
|
|
54
|
+
child.unref();
|
|
55
|
+
}
|
|
56
|
+
async function execute(opts, apiUrl) {
|
|
57
|
+
if (process.env['HALYARD_SESSION_UPLOAD'] === '0')
|
|
58
|
+
return;
|
|
59
|
+
const input = process.env[HOOK_INPUT_ENV] ?? (process.stdin.isTTY ? '' : await readStdin(STDIN_TIMEOUT_MS));
|
|
60
|
+
const event = parseEvent(opts.event) ?? normalizeEvent(parseHookInput(input).event);
|
|
61
|
+
if (!event)
|
|
62
|
+
return;
|
|
63
|
+
const client = opts.client && isHookClient(opts.client) ? opts.client : undefined;
|
|
64
|
+
if (event === 'start' || (event === 'end' && opts.detach)) {
|
|
65
|
+
detach(childArgs(opts, apiUrl, event === 'start' ? 'sweep' : 'end'), input);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await runHook({
|
|
69
|
+
input,
|
|
70
|
+
client,
|
|
71
|
+
event,
|
|
72
|
+
root: opts.root,
|
|
73
|
+
transcript: opts.transcript,
|
|
74
|
+
dryRun: !!opts.dryRun,
|
|
75
|
+
apiUrl,
|
|
76
|
+
halyardDir: halyardDir(),
|
|
77
|
+
homeDir: homedir(),
|
|
78
|
+
env: process.env,
|
|
79
|
+
cwd: process.cwd(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function hookCommand() {
|
|
83
|
+
return new Command('hook')
|
|
84
|
+
.description('Agent lifecycle hook: upload the session that just ended, or sweep unsynced ones (wired by `halyard setup`)')
|
|
85
|
+
.option('--client <name>', `Client (${HOOK_CLIENTS.join(', ')}); auto-detected when omitted`)
|
|
86
|
+
.option('--event <event>', 'start | end | sweep (defaults to the event in the hook payload)')
|
|
87
|
+
.option('--root <dir>', "Repository root (defaults to the client's project dir or cwd)")
|
|
88
|
+
.option('--transcript <path>', 'Transcript to upload (defaults to the hook payload)')
|
|
89
|
+
.option('--detach', 'Upload in a background process and return immediately')
|
|
90
|
+
.option('--dry-run', 'Log what would be uploaded without uploading')
|
|
91
|
+
.action(async function () {
|
|
92
|
+
const opts = this.opts();
|
|
93
|
+
const apiUrl = this.optsWithGlobals().apiUrl;
|
|
94
|
+
try {
|
|
95
|
+
await execute(opts, apiUrl);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
new HookLogger(join(halyardDir(), 'hooks.log'), `${opts.client ?? '?'}/${opts.event ?? '?'}`).log(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
}
|
|
100
|
+
// Gemini CLI blocks unless a hook prints JSON on stdout.
|
|
101
|
+
if (opts.client === 'gemini' || process.env['GEMINI_PROJECT_DIR'])
|
|
102
|
+
console.log('{}');
|
|
103
|
+
process.exitCode = 0;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../../src/commands/hook.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { refreshToken, type Credentials } from '../lib/auth.js';
|
|
3
|
+
import { type RepoBinding } from '../lib/repo-binding.js';
|
|
2
4
|
export declare function loginCommand(): Command;
|
|
5
|
+
export interface PersistLoginOptions {
|
|
6
|
+
creds: Credentials;
|
|
7
|
+
binding: RepoBinding | null;
|
|
8
|
+
/** Injectable for tests */
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
refreshImpl?: typeof refreshToken;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Persist freshly exchanged tokens. In an unbound repo this is the legacy
|
|
14
|
+
* behavior (write ~/.halyard/credentials.json). In a bound repo, resolve the
|
|
15
|
+
* binding's org slug against /api/v1/organizations, reissue workspace-scoped
|
|
16
|
+
* tokens when needed, and save to the per-org store.
|
|
17
|
+
* Returns the success message to print; throws on failure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function persistLogin(options: PersistLoginOptions): Promise<string>;
|
|
3
20
|
//# sourceMappingURL=login.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAEL,YAAY,EAGZ,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAA;AA8D1E,wBAAgB,YAAY,IAAI,OAAO,CA6KtC;AASD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,2BAA2B;IAC3B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,WAAW,CAAC,EAAE,OAAO,YAAY,CAAA;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsChF"}
|
package/dist/commands/login.js
CHANGED
|
@@ -2,7 +2,8 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { randomBytes, createHash } from 'node:crypto';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import open from 'open';
|
|
5
|
-
import { saveCredentials } from '../lib/auth.js';
|
|
5
|
+
import { perOrgCredentialsPath, refreshToken, saveCredentials, savePerOrgCredentials, } from '../lib/auth.js';
|
|
6
|
+
import { findRepoBinding } from '../lib/repo-binding.js';
|
|
6
7
|
/**
|
|
7
8
|
* Generate a PKCE code verifier (RFC 7636)
|
|
8
9
|
*/
|
|
@@ -15,6 +16,51 @@ function generateCodeVerifier() {
|
|
|
15
16
|
function generateCodeChallenge(verifier) {
|
|
16
17
|
return createHash('sha256').update(verifier).digest('base64url');
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Branded OAuth return page. Fully self-contained (inline sail mark, system
|
|
21
|
+
* font fallbacks, prefers-color-scheme dark) because it is served from the
|
|
22
|
+
* loopback listener with no asset pipeline. Values mirror the design tokens in
|
|
23
|
+
* packages/ui/src/styles/brand-tokens.css; the desktop app serves the same page
|
|
24
|
+
* from apps/desktop/src-tauri/src/oauth.rs — keep the two in step.
|
|
25
|
+
*/
|
|
26
|
+
function returnPage(ok, msg) {
|
|
27
|
+
const escaped = msg.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
28
|
+
return `<!doctype html>
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head>
|
|
31
|
+
<meta charset="utf-8">
|
|
32
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
33
|
+
<title>${ok ? 'Halyard — signed in' : 'Halyard — sign-in failed'}</title>
|
|
34
|
+
<style>
|
|
35
|
+
:root { color-scheme: light dark; }
|
|
36
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #fdfbf7; color: #2b180a; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; -webkit-font-smoothing: antialiased; }
|
|
37
|
+
main { text-align: center; padding: 56px 64px; max-width: 420px; border: 1px solid #ede4da; border-radius: 14px; background: #faf7f1; box-shadow: 0 1px 3px rgba(43, 24, 10, 0.05); }
|
|
38
|
+
h1 { font-family: 'Playfair Display', Georgia, 'Times New Roman', serif; font-weight: 500; font-size: 30px; letter-spacing: -0.01em; margin: 24px 0 10px; }
|
|
39
|
+
p { color: #796758; font-size: 15px; line-height: 1.55; margin: 0; }
|
|
40
|
+
.rule { width: 40px; height: 2px; border-radius: 1px; background: #8b6914; opacity: 0.55; margin: 26px auto 0; }
|
|
41
|
+
@media (prefers-color-scheme: dark) {
|
|
42
|
+
body { background: #131210; color: #ebe5db; }
|
|
43
|
+
main { border-color: #312e28; background: #1a1815; box-shadow: none; }
|
|
44
|
+
p { color: #7d766a; }
|
|
45
|
+
.rule { background: #d4a017; }
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<main>
|
|
51
|
+
<svg width="56" height="56" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Halyard">
|
|
52
|
+
<rect width="48" height="48" rx="10" fill="#1C1A16"/>
|
|
53
|
+
<path d="M 18.000,24.750 Q 18.000,22.500 16.821,24.416 L 7.179,40.085 Q 6.000,42.000 8.250,42.000 L 15.750,42.000 Q 18.000,42.000 18.000,39.750 Z" fill="#E8DFD0" opacity="0.8"/>
|
|
54
|
+
<path d="M 42.000,8.250 Q 42.000,6.000 40.808,7.908 L 20.693,40.092 Q 19.500,42.000 21.750,42.000 L 39.750,42.000 Q 42.000,42.000 42.000,39.750 Z" fill="#E8DFD0"/>
|
|
55
|
+
</svg>
|
|
56
|
+
<h1>${ok ? 'You’re signed in' : 'Sign-in didn’t complete'}</h1>
|
|
57
|
+
<p>${escaped}</p>
|
|
58
|
+
<div class="rule"></div>
|
|
59
|
+
</main>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
18
64
|
export function loginCommand() {
|
|
19
65
|
return new Command('login')
|
|
20
66
|
.description('Authenticate with Halyard via browser login')
|
|
@@ -72,7 +118,7 @@ export function loginCommand() {
|
|
|
72
118
|
const error = url.searchParams.get('error');
|
|
73
119
|
if (error) {
|
|
74
120
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
75
|
-
res.end('
|
|
121
|
+
res.end(returnPage(false, 'You can close this tab and try again from the terminal.'));
|
|
76
122
|
clearTimeout(timeout);
|
|
77
123
|
server.close();
|
|
78
124
|
reject(new Error(`OAuth error: ${error}`));
|
|
@@ -80,7 +126,7 @@ export function loginCommand() {
|
|
|
80
126
|
}
|
|
81
127
|
if (returnedState !== state) {
|
|
82
128
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
83
|
-
res.end('
|
|
129
|
+
res.end(returnPage(false, 'State mismatch. You can close this tab.'));
|
|
84
130
|
clearTimeout(timeout);
|
|
85
131
|
server.close();
|
|
86
132
|
reject(new Error('OAuth state mismatch'));
|
|
@@ -88,14 +134,14 @@ export function loginCommand() {
|
|
|
88
134
|
}
|
|
89
135
|
if (!code) {
|
|
90
136
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
91
|
-
res.end('
|
|
137
|
+
res.end(returnPage(false, 'No auth code received. You can close this tab.'));
|
|
92
138
|
clearTimeout(timeout);
|
|
93
139
|
server.close();
|
|
94
140
|
reject(new Error('No authorization code received'));
|
|
95
141
|
return;
|
|
96
142
|
}
|
|
97
143
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
98
|
-
res.end('
|
|
144
|
+
res.end(returnPage(true, 'You can close this tab and return to the terminal.'));
|
|
99
145
|
clearTimeout(timeout);
|
|
100
146
|
server.close();
|
|
101
147
|
resolve(code);
|
|
@@ -124,6 +170,7 @@ export function loginCommand() {
|
|
|
124
170
|
}
|
|
125
171
|
// Step 5: Exchange auth code for tokens
|
|
126
172
|
console.log('Exchanging authorization code...');
|
|
173
|
+
let tokenData;
|
|
127
174
|
try {
|
|
128
175
|
const tokenRes = await fetch(`${apiUrl}/oauth/token`, {
|
|
129
176
|
method: 'POST',
|
|
@@ -140,21 +187,71 @@ export function loginCommand() {
|
|
|
140
187
|
const body = await tokenRes.text();
|
|
141
188
|
throw new Error(`Token exchange failed (${tokenRes.status}): ${body}`);
|
|
142
189
|
}
|
|
143
|
-
|
|
144
|
-
saveCredentials({
|
|
145
|
-
accessToken: tokenData.access_token,
|
|
146
|
-
refreshToken: tokenData.refresh_token,
|
|
147
|
-
clientId,
|
|
148
|
-
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
149
|
-
apiUrl,
|
|
150
|
-
});
|
|
151
|
-
console.log('Login successful! Credentials saved to ~/.halyard/credentials.json');
|
|
190
|
+
tokenData = (await tokenRes.json());
|
|
152
191
|
}
|
|
153
192
|
catch (err) {
|
|
154
193
|
console.error('Token exchange failed:', err instanceof Error ? err.message : err);
|
|
155
194
|
process.exit(1);
|
|
156
195
|
}
|
|
196
|
+
// Step 6: Persist credentials (org-aware when the repo is bound)
|
|
197
|
+
try {
|
|
198
|
+
const message = await persistLogin({
|
|
199
|
+
creds: {
|
|
200
|
+
accessToken: tokenData.access_token,
|
|
201
|
+
refreshToken: tokenData.refresh_token,
|
|
202
|
+
clientId,
|
|
203
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
204
|
+
apiUrl,
|
|
205
|
+
},
|
|
206
|
+
binding: findRepoBinding(),
|
|
207
|
+
});
|
|
208
|
+
console.log(message);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Persist freshly exchanged tokens. In an unbound repo this is the legacy
|
|
218
|
+
* behavior (write ~/.halyard/credentials.json). In a bound repo, resolve the
|
|
219
|
+
* binding's org slug against /api/v1/organizations, reissue workspace-scoped
|
|
220
|
+
* tokens when needed, and save to the per-org store.
|
|
221
|
+
* Returns the success message to print; throws on failure.
|
|
222
|
+
*/
|
|
223
|
+
export async function persistLogin(options) {
|
|
224
|
+
const { creds, binding, fetchImpl = fetch, refreshImpl = refreshToken } = options;
|
|
225
|
+
if (!binding) {
|
|
226
|
+
saveCredentials(creds);
|
|
227
|
+
return 'Login successful! Credentials saved to ~/.halyard/credentials.json';
|
|
228
|
+
}
|
|
229
|
+
const res = await fetchImpl(`${creds.apiUrl}/api/v1/organizations`, {
|
|
230
|
+
headers: { Authorization: `Bearer ${creds.accessToken}` },
|
|
157
231
|
});
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
throw new Error(`Could not list organizations (${res.status}): ${await res.text()}`);
|
|
234
|
+
}
|
|
235
|
+
const { organizations } = (await res.json());
|
|
236
|
+
const target = organizations.find((org) => org.slug === binding.org);
|
|
237
|
+
if (!target) {
|
|
238
|
+
const available = organizations.map((org) => ` ${org.slug}`).join('\n');
|
|
239
|
+
throw new Error(`This repo is bound to organization "${binding.org}" (${binding.rootDir}/.halyard.json), ` +
|
|
240
|
+
`but your account has no access to it.\nAvailable organizations:\n${available}`);
|
|
241
|
+
}
|
|
242
|
+
if (target.isCurrent) {
|
|
243
|
+
// Token is already scoped to the bound org - save it as-is.
|
|
244
|
+
savePerOrgCredentials({ ...creds, orgId: target.id, orgSlug: target.slug });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// Workspace-scoped reissue, persisted straight into the per-org store.
|
|
248
|
+
await refreshImpl(creds.refreshToken, creds.clientId, creds.apiUrl, target.id, {
|
|
249
|
+
filePath: perOrgCredentialsPath(target.id),
|
|
250
|
+
orgId: target.id,
|
|
251
|
+
orgSlug: target.slug,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return `Logged in to Halyard (${target.slug}) for this repo`;
|
|
158
255
|
}
|
|
159
256
|
/**
|
|
160
257
|
* Find an available port for the OAuth callback server.
|