@opvs-ai/cli 0.16.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -8
- package/dist/.build-stamp.json +5 -5
- package/dist/commands/agent.js +6 -5
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/auth.js +246 -76
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/board-links.js +5 -2
- package/dist/commands/board-links.js.map +1 -1
- package/dist/commands/boards.js +26 -16
- package/dist/commands/boards.js.map +1 -1
- package/dist/commands/code.d.ts +23 -0
- package/dist/commands/code.js +90 -0
- package/dist/commands/code.js.map +1 -0
- package/dist/commands/columns.d.ts +1 -0
- package/dist/commands/columns.js +53 -4
- package/dist/commands/columns.js.map +1 -1
- package/dist/commands/config.js +5 -2
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/docs.js +17 -8
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/employees.js +4 -2
- package/dist/commands/employees.js.map +1 -1
- package/dist/commands/files.js +5 -2
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/links.js +9 -4
- package/dist/commands/links.js.map +1 -1
- package/dist/commands/mail.js +5 -2
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/markers.js +18 -5
- package/dist/commands/markers.js.map +1 -1
- package/dist/commands/members.js +4 -1
- package/dist/commands/members.js.map +1 -1
- package/dist/commands/projects.js +57 -12
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/scheduled-runs.d.ts +26 -0
- package/dist/commands/scheduled-runs.js +182 -0
- package/dist/commands/scheduled-runs.js.map +1 -0
- package/dist/commands/stages.js +5 -2
- package/dist/commands/stages.js.map +1 -1
- package/dist/commands/tasks.js +8 -5
- package/dist/commands/tasks.js.map +1 -1
- package/dist/commands/tools.js +34 -5
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/workspace.js +5 -2
- package/dist/commands/workspace.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-poll.d.ts +25 -3
- package/dist/lib/auth-poll.js +39 -6
- package/dist/lib/auth-poll.js.map +1 -1
- package/dist/lib/confirm.d.ts +14 -0
- package/dist/lib/confirm.js +17 -0
- package/dist/lib/confirm.js.map +1 -1
- package/dist/lib/health.d.ts +131 -1
- package/dist/lib/health.js +192 -3
- package/dist/lib/health.js.map +1 -1
- package/dist/lib/pending-auth.d.ts +41 -0
- package/dist/lib/pending-auth.js +69 -0
- package/dist/lib/pending-auth.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,13 +41,23 @@ Agent runs: opvs auth request -w <workspace> -e <admin-email>
|
|
|
41
41
|
--> Approval email sent to admin
|
|
42
42
|
--> CLI polls for approval every 3s
|
|
43
43
|
|
|
44
|
-
Admin clicks: [Approve] button in email
|
|
44
|
+
Admin clicks: [Approve] button in email (the link works for 60 minutes)
|
|
45
45
|
--> Token generated and delivered to CLI
|
|
46
46
|
--> Saved to ~/.opvs/config.json
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
No passwords, no copy-pasting tokens. The human stays in control.
|
|
50
50
|
|
|
51
|
+
The CLI waits 5 minutes for the approval (`--wait <minutes>` to change it). If the admin
|
|
52
|
+
approves later, the request is still open — collect the token with:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
opvs auth resume
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Exit codes for `auth request` and `auth resume`: `0` approved · `1` denied, expired or
|
|
59
|
+
failed · `2` not approved yet (run `opvs auth resume`).
|
|
60
|
+
|
|
51
61
|
## Multi-Workspace Support
|
|
52
62
|
|
|
53
63
|
The CLI supports multiple workspaces (brands) with kubectl-style context switching. Each workspace has its own token, brand, and API URL.
|
|
@@ -70,7 +80,7 @@ opvs -w my-company boards list
|
|
|
70
80
|
opvs workspace current
|
|
71
81
|
|
|
72
82
|
# Remove a workspace
|
|
73
|
-
opvs workspace remove old-brand
|
|
83
|
+
opvs workspace remove old-brand --yes
|
|
74
84
|
```
|
|
75
85
|
|
|
76
86
|
### Config File
|
|
@@ -125,12 +135,37 @@ opvs boards set-routing ASP --harness codex # Board routing defaults ('
|
|
|
125
135
|
opvs boards projects ASP # Projects claiming the board (none|resolved|ambiguous)
|
|
126
136
|
opvs boards add-to-project ASP <project-uuid> # Additive
|
|
127
137
|
opvs boards move-to-project ASP <project-uuid> --yes # REMOVES it from every other project — previews first
|
|
128
|
-
opvs boards health ASP --profile factory #
|
|
138
|
+
opvs boards health ASP --profile factory # EXITS 2 on not_healthy — see below
|
|
139
|
+
opvs boards health ASP --format json | jq .verdict # the report VERBATIM
|
|
129
140
|
opvs boards apply-preset ASP factory-full # DRY RUN by default; --yes applies
|
|
130
141
|
opvs boards apply-preset ASP factory-full --yes --json | jq .applied # ONE JSON document: the applied plan
|
|
131
142
|
opvs boards update ASP --archived true # Hidden from `boards list`; cards stay CLAIMABLE
|
|
132
143
|
```
|
|
133
144
|
|
|
145
|
+
#### Gating a script on board / project health
|
|
146
|
+
|
|
147
|
+
`boards health` and `projects health` are the same report the dashboard shows, and they set an
|
|
148
|
+
exit code so a shell can gate on them:
|
|
149
|
+
|
|
150
|
+
| Exit | Verdict | Means |
|
|
151
|
+
|-----:|---------|-------|
|
|
152
|
+
| `0` | `healthy` | every check ran, none failed |
|
|
153
|
+
| `0` | `incomplete` | **you supplied no threshold**, so some check made no finding. A fact about your REQUEST, not about the board — pass `--require-complete` to make it exit 2 |
|
|
154
|
+
| `2` | `not_healthy` | at least one check FAILED |
|
|
155
|
+
| `1` | — | transport, auth, a bad ref, or a verdict the CLI could not read |
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# A planner gate. `2` stops the run; `1` means the check itself could not be made.
|
|
159
|
+
opvs boards health "$BOARD" --profile planner-orchestration || exit $?
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
🔑 **`1` and `2` are deliberately different.** `2` is "the board failed"; `1` is "I could not
|
|
163
|
+
ask". A gate that treated them alike would pass an expired token off as a healthy board.
|
|
164
|
+
|
|
165
|
+
On `not_healthy` only the FAIL rows print — `--all-checks` brings the rest back. `--format json`
|
|
166
|
+
is the server's report verbatim (for `jq`); `--format md|yaml` is the server's own rendering, and
|
|
167
|
+
the exit code is still the verdict's.
|
|
168
|
+
|
|
134
169
|
> ⚠️ **A factory board claims NOTHING until it is in exactly ONE active project.** Every claim on a
|
|
135
170
|
> factory board in no project (`none`) or in two or more (`ambiguous`) returns
|
|
136
171
|
> `{task_id: null, reason: "board_project_unresolved"}` — the columns can be perfectly wired and the
|
|
@@ -138,13 +173,35 @@ opvs boards update ASP --archived true # Hidden from `boards list`
|
|
|
138
173
|
> `add-to-project`, `move-to-project`, `apply-preset`, `projects add-board | remove-board | archive`
|
|
139
174
|
> all check afterwards and warn on stderr when a factory board is left unresolved.
|
|
140
175
|
|
|
176
|
+
> **Destructive commands ask first (ASP-144).** Every command that deletes, revokes, cancels or
|
|
177
|
+
> uninstalls something asks `[y/N]` at a terminal, and **off a terminal (an agent loop, CI, a pipe)
|
|
178
|
+
> it refuses and exits 1 unless you pass `--yes` / `-y`**. Examples here carry `--yes` because
|
|
179
|
+
> that is how an agent runs them. Not gated, because a sibling command undoes them and nothing is
|
|
180
|
+
> deleted: `projects archive` (`--undo`), `projects remove-board` (`add-board`), `mail daemon uninstall`
|
|
181
|
+
> (`mail daemon start`). `tests/commands/destructive-gate-asp144.test.ts` walks the built command tree
|
|
182
|
+
> and fails on a new destructive-named command without `--yes`.
|
|
183
|
+
|
|
184
|
+
### Scheduled runs
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
opvs scheduled-runs list [--project <uuid>] [--board <uuid>] [--state scheduled|fired|failed|cancelled|all] [--limit 100]
|
|
188
|
+
opvs scheduled-runs get <id> # state, fired_at, last_error
|
|
189
|
+
opvs scheduled-runs create --project <uuid> --run-at 2026-09-17T08:00:00Z [--board <uuid>] [--params '{"note":"nightly"}']
|
|
190
|
+
opvs scheduled-runs cancel <id> --yes # only a run that has not fired
|
|
191
|
+
```
|
|
192
|
+
> Scheduling costs nothing. At `--run-at` (the scanner sweeps about once a minute) the project, or one
|
|
193
|
+
> board, is **armed** exactly as ▶ Play arms it (`ProjectRunService.arm`): the factory starts claiming its
|
|
194
|
+
> cards, those builds spend, and it stays armed until paused or stopped. `create` and `cancel` are brand
|
|
195
|
+
> admin only (403). `--run-at` must carry a zone (`Z` or `+02:00`); ids are full UUIDs. Cancel never disarms
|
|
196
|
+
> a run that already fired — it returns the row unchanged, and the CLI says so.
|
|
197
|
+
|
|
141
198
|
### Members
|
|
142
199
|
|
|
143
200
|
```bash
|
|
144
201
|
opvs members list <boardId>
|
|
145
202
|
opvs members add <boardId> --user <uuid> # or --agent <uuid>; a member of YOUR brand
|
|
146
203
|
opvs members invite <boardId> --email x@y.com --role commenter # outside your brand → a guest of this board
|
|
147
|
-
opvs members remove <boardId> <uuid> --type agent
|
|
204
|
+
opvs members remove <boardId> <uuid> --type agent --yes # --type defaults to user
|
|
148
205
|
```
|
|
149
206
|
|
|
150
207
|
> `--role` (viewer | commenter | editor) caps an **external guest** only; a member of your own brand
|
|
@@ -167,7 +224,7 @@ opvs columns update <columnId> --wip-limit none # 'none' clears;
|
|
|
167
224
|
|
|
168
225
|
```bash
|
|
169
226
|
opvs projects set-routing <project-uuid> --model claude-opus-5
|
|
170
|
-
opvs projects health <project-uuid> --profile planner-orchestration
|
|
227
|
+
opvs projects health <project-uuid> --profile planner-orchestration # EXITS 2 on not_healthy
|
|
171
228
|
opvs projects bundle set <project-uuid> --command ship --command-body ship=./ship.md # a body map REPLACES
|
|
172
229
|
```
|
|
173
230
|
|
|
@@ -209,6 +266,28 @@ opvs docs update <project> <slug> -f content.md
|
|
|
209
266
|
opvs docs search "query" # Search docs
|
|
210
267
|
```
|
|
211
268
|
|
|
269
|
+
### Code index
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# "Does this already exist?" — ask BEFORE writing it.
|
|
273
|
+
opvs code context "resolve_brand" --project <project-id>
|
|
274
|
+
opvs code context "resolveBrand" --project <id> --kind function --limit 5
|
|
275
|
+
opvs code summary --project <project-id> # Repo orientation: languages, entry points, top files
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Two answers you must not conflate: `indexed: false` means *there was nothing to
|
|
279
|
+
search* (no index for this project, or it is not yours), while `verdict: none`
|
|
280
|
+
means *we searched and found nothing*. Only the second is evidence of absence.
|
|
281
|
+
A capped result set says so — read `truncated`, and read
|
|
282
|
+
`freshness.index_truncated` separately: that one means the index is partial.
|
|
283
|
+
`freshness.scan_truncated` says the SCAN stopped early, so parts of the repo were
|
|
284
|
+
never read at all; `freshness.symbols_dropped_at_ingest` counts symbols the API
|
|
285
|
+
could not store, from files that were read.
|
|
286
|
+
|
|
287
|
+
Needs the `code:read` scope, which is in the default ceiling. There is no
|
|
288
|
+
`opvs code index` command on purpose — writing an index is `code:write`, which
|
|
289
|
+
a worker PAT does not carry, and the ingest caller is the sidecar on the desk.
|
|
290
|
+
|
|
212
291
|
### Session
|
|
213
292
|
|
|
214
293
|
```bash
|
|
@@ -231,8 +310,9 @@ opvs init # Print CLAUDE.md snippet
|
|
|
231
310
|
|
|
232
311
|
```bash
|
|
233
312
|
opvs auth request -w <slug> -e <email> # Request token
|
|
313
|
+
opvs auth resume # Collect a token approved after the wait
|
|
234
314
|
opvs auth status # Current auth info
|
|
235
|
-
opvs auth revoke
|
|
315
|
+
opvs auth revoke --yes # Revoke token
|
|
236
316
|
opvs auth list # List agent tokens (admin)
|
|
237
317
|
```
|
|
238
318
|
|
|
@@ -252,7 +332,7 @@ opvs tools help @opvs-ai/agentboard --section methods # Just the methods
|
|
|
252
332
|
opvs tools list # Installed packages on this brand
|
|
253
333
|
opvs tools install @opvs-ai/agentboard \
|
|
254
334
|
--grants '{"vendor.opvs-ai.agentboard.read":true}' # Install with entitlements
|
|
255
|
-
opvs tools uninstall @opvs-ai/agentboard
|
|
335
|
+
opvs tools uninstall @opvs-ai/agentboard --yes # Soft-uninstall
|
|
256
336
|
|
|
257
337
|
# Invoke
|
|
258
338
|
opvs tools call @opvs-ai/agentboard list_tasks \
|
|
@@ -270,7 +350,7 @@ gateway plugin `skill_pin` tool is the canonical surface.
|
|
|
270
350
|
opvs workspace list # List all workspaces
|
|
271
351
|
opvs workspace use <slug> # Switch current workspace
|
|
272
352
|
opvs workspace current # Show current workspace details
|
|
273
|
-
opvs workspace remove <slug>
|
|
353
|
+
opvs workspace remove <slug> --yes # Remove a saved workspace
|
|
274
354
|
```
|
|
275
355
|
|
|
276
356
|
## YAML Output
|
package/dist/.build-stamp.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stamp_version": 1,
|
|
3
3
|
"package": "@opvs-ai/cli",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"source_digest": "
|
|
6
|
-
"source_files":
|
|
7
|
-
"built_at": "2026-09-
|
|
8
|
-
"node": "v22.
|
|
4
|
+
"version": "0.20.1",
|
|
5
|
+
"source_digest": "5f2aeb431e9a054d49cf81631229a8b256b90a0120ea1b37ce860dfccf7302ca",
|
|
6
|
+
"source_files": 44,
|
|
7
|
+
"built_at": "2026-09-28T13:58:06.956Z",
|
|
8
|
+
"node": "v22.23.2"
|
|
9
9
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -71,7 +71,7 @@ export function registerAgentCommands(program) {
|
|
|
71
71
|
agent
|
|
72
72
|
.command("batch-create")
|
|
73
73
|
.description("Create multiple tasks at once from a JSON file")
|
|
74
|
-
.requiredOption("--file <path>", "JSON file with array of task objects
|
|
74
|
+
.requiredOption("--file <path>", "JSON file with an array of task objects: board_id and title required; column_id, description, priority, agent_instructions optional")
|
|
75
75
|
.action(async (opts) => {
|
|
76
76
|
const wsSlug = program.opts().workspace;
|
|
77
77
|
try {
|
|
@@ -173,14 +173,15 @@ export function registerAgentCommands(program) {
|
|
|
173
173
|
.command("mention <taskId>")
|
|
174
174
|
.description("@mention an agent on a task (triggers agent to respond)")
|
|
175
175
|
.requiredOption("--agent <slug>", "Agent slug to mention")
|
|
176
|
-
|
|
176
|
+
// Required: the gateway answers `400 agent_slug and message are required`
|
|
177
|
+
// without one, so an optional flag only moved the failure to the server
|
|
178
|
+
// (ASP-271).
|
|
179
|
+
.requiredOption("--message <text>", "Message for the agent")
|
|
177
180
|
.action(async (taskId, opts) => {
|
|
178
181
|
const wsSlug = program.opts().workspace;
|
|
179
182
|
try {
|
|
180
183
|
const client = createClient(wsSlug);
|
|
181
|
-
const body = { agent_slug: opts.agent };
|
|
182
|
-
if (opts.message)
|
|
183
|
-
body.message = opts.message;
|
|
184
|
+
const body = { agent_slug: opts.agent, message: opts.message };
|
|
184
185
|
const res = await client.post(`/api/v1/board/tasks/${taskId}/agent-mention`, body);
|
|
185
186
|
const data = await res.json();
|
|
186
187
|
printJson(data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/auth.js
CHANGED
|
@@ -2,21 +2,205 @@
|
|
|
2
2
|
* opvs auth — AI-native token provisioning + management
|
|
3
3
|
*/
|
|
4
4
|
import { getEffectiveConfig, saveWorkspace, setCurrentWorkspace, loadRawConfig, apiUnauthPost, createClient, DEFAULT_WORKSPACE, } from "@opvs-ai/core";
|
|
5
|
+
/**
|
|
6
|
+
* Point `current_workspace` at `fallback` ONLY if the pointer is currently
|
|
7
|
+
* empty or dangling.
|
|
8
|
+
*
|
|
9
|
+
* `auth request` used to call `setCurrentWorkspace(workspaces[0].brand_slug)`
|
|
10
|
+
* unconditionally, so re-authing to refresh an expiring token silently moved
|
|
11
|
+
* the CLI to whichever brand the server happened to list first. Everything
|
|
12
|
+
* after that ran against the wrong brand while still looking right: the very
|
|
13
|
+
* next command reported `skill '@opvs-ai/agentmemory' is not installed on
|
|
14
|
+
* this brand`, which is true of the brand it had been moved to and false of
|
|
15
|
+
* the one the operator thought they were on. Reported by @di-atomic
|
|
16
|
+
* 2026-09-21.
|
|
17
|
+
*
|
|
18
|
+
* The workspace pointer is sticky state. `opvs workspace use <slug>` is what
|
|
19
|
+
* changes it; re-authenticating is not. (`saveWorkspace` already has exactly
|
|
20
|
+
* these semantics — it sets current only when none is set. This makes the
|
|
21
|
+
* explicit call agree with it instead of overriding it.)
|
|
22
|
+
*
|
|
23
|
+
* Returns the slug that is current afterwards, so the caller can print it —
|
|
24
|
+
* a workspace pointer you cannot see is how this went unnoticed.
|
|
25
|
+
*/
|
|
26
|
+
function ensureCurrentWorkspace(fallback) {
|
|
27
|
+
const cfg = loadRawConfig();
|
|
28
|
+
const existing = cfg.current_workspace;
|
|
29
|
+
if (existing && cfg.workspaces[existing])
|
|
30
|
+
return existing;
|
|
31
|
+
setCurrentWorkspace(fallback);
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
5
34
|
import { buildFingerprint } from "../lib/fingerprint.js";
|
|
6
35
|
import { pollForApproval } from "../lib/auth-poll.js";
|
|
36
|
+
import { clearPendingAuth, loadPendingAuth, savePendingAuth, } from "../lib/pending-auth.js";
|
|
7
37
|
import { printJson } from "../lib/format.js";
|
|
38
|
+
import { confirmOrExit, YES_HELP } from "../lib/confirm.js";
|
|
39
|
+
const WAIT_HELP = "Minutes to wait here for the approval before handing off to `opvs auth resume` " +
|
|
40
|
+
"(default: 5). The request itself stays approvable on the server for longer.";
|
|
41
|
+
/**
|
|
42
|
+
* `--wait <minutes>` → milliseconds. `undefined` when the flag is absent (the poll
|
|
43
|
+
* loop's own default applies); `null` when it is not a positive number.
|
|
44
|
+
*/
|
|
45
|
+
function parseWaitMinutes(value) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const minutes = Number(value);
|
|
49
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
50
|
+
return null;
|
|
51
|
+
return Math.round(minutes * 60_000);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save the request for `opvs auth resume` BEFORE the first poll, so a process
|
|
55
|
+
* killed mid-wait (an agent's tool-call timeout) still leaves it collectable.
|
|
56
|
+
* A failure to save must not cost the login itself — it only loses the resume.
|
|
57
|
+
*/
|
|
58
|
+
function rememberPending(fields) {
|
|
59
|
+
const pending = { ...fields, created_at: new Date().toISOString() };
|
|
60
|
+
try {
|
|
61
|
+
savePendingAuth(pending);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
process.stderr.write(`Warning: could not save this request for \`opvs auth resume\` (${msg}). ` +
|
|
66
|
+
"If the wait below ends before it is approved, request access again.\n");
|
|
67
|
+
}
|
|
68
|
+
return pending;
|
|
69
|
+
}
|
|
70
|
+
function forgetPending() {
|
|
71
|
+
try {
|
|
72
|
+
clearPendingAuth();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* nothing to clean up is not an error */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function handoffMessage(pending, now = Date.now()) {
|
|
79
|
+
const who = pending.owner_email ? `The approval email went to ${pending.owner_email}. ` : "";
|
|
80
|
+
const expiry = new Date(pending.expires_at).getTime();
|
|
81
|
+
if (Number.isFinite(expiry) && expiry > now) {
|
|
82
|
+
const minutes = Math.max(1, Math.ceil((expiry - now) / 60_000));
|
|
83
|
+
return (`Not approved yet. The request stays open for about ${minutes} more minute${minutes === 1 ? "" : "s"} ` +
|
|
84
|
+
`(until ${pending.expires_at}).\n${who}Once it has been approved, run:\n opvs auth resume`);
|
|
85
|
+
}
|
|
86
|
+
return `Not approved yet. ${who}If it is approved in time, run:\n opvs auth resume`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Turn a poll result into saved credentials and a message — shared by
|
|
90
|
+
* `auth request` and `auth resume`, so a late approval ends exactly like a
|
|
91
|
+
* prompt one. The saved request is dropped on every final answer and kept only
|
|
92
|
+
* while the server may still approve it.
|
|
93
|
+
*/
|
|
94
|
+
function settleApproval(result, pending) {
|
|
95
|
+
const baseUrl = pending.api_url;
|
|
96
|
+
const wsSlug = pending.workspace_slug;
|
|
97
|
+
if (result.status === "active" && wsSlug && result.token) {
|
|
98
|
+
saveWorkspace(wsSlug, {
|
|
99
|
+
api_url: baseUrl,
|
|
100
|
+
token: result.token,
|
|
101
|
+
brand_id: result.brand_id ?? null,
|
|
102
|
+
brand_name: result.brand_name ?? "",
|
|
103
|
+
});
|
|
104
|
+
forgetPending();
|
|
105
|
+
const current = ensureCurrentWorkspace(wsSlug);
|
|
106
|
+
console.log(`\nAuthenticated! Token saved to workspace "${wsSlug}".`);
|
|
107
|
+
console.log(`Brand: ${result.brand_name} (ID: ${result.brand_id})`);
|
|
108
|
+
console.log(`Scopes: ${result.scopes?.join(", ")}`);
|
|
109
|
+
console.log(current === wsSlug
|
|
110
|
+
? `Current workspace: ${current}`
|
|
111
|
+
: `Current workspace: ${current} (unchanged — "opvs workspace use ${wsSlug}" to switch)`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (result.status === "active" && !wsSlug && result.workspaces?.length) {
|
|
115
|
+
// Save each workspace
|
|
116
|
+
for (const ws of result.workspaces) {
|
|
117
|
+
saveWorkspace(ws.brand_slug, {
|
|
118
|
+
api_url: baseUrl,
|
|
119
|
+
token: ws.token,
|
|
120
|
+
brand_id: ws.brand_id,
|
|
121
|
+
brand_name: ws.brand_name,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
forgetPending();
|
|
125
|
+
// Keep whatever workspace was already current — re-authing is not
|
|
126
|
+
// a request to switch brands. Falls back to the first only when
|
|
127
|
+
// there is no usable pointer yet (first auth on this machine).
|
|
128
|
+
const current = ensureCurrentWorkspace(result.workspaces[0].brand_slug);
|
|
129
|
+
const slugs = result.workspaces.map((ws) => ws.brand_slug).join(", ");
|
|
130
|
+
console.log(`\nAuthenticated! ${result.workspaces.length} workspace${result.workspaces.length !== 1 ? "s" : ""} configured: ${slugs}`);
|
|
131
|
+
for (const ws of result.workspaces) {
|
|
132
|
+
// Print the granted scopes per workspace — each token is clamped to its
|
|
133
|
+
// own workspace's policy, so they are not necessarily what was asked for
|
|
134
|
+
// and are not necessarily the same across workspaces.
|
|
135
|
+
const granted = ws.scopes?.length ? ws.scopes.join(", ") : "(none)";
|
|
136
|
+
const marker = ws.brand_slug === current ? " [current]" : "";
|
|
137
|
+
console.log(` ${ws.brand_slug}: ${ws.brand_name} (ID: ${ws.brand_id})${marker}`);
|
|
138
|
+
console.log(` scopes: ${granted}`);
|
|
139
|
+
}
|
|
140
|
+
console.log(`\nCurrent workspace: ${current} — unchanged by this command. ` +
|
|
141
|
+
`Switch with "opvs workspace use <slug>".`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (result.status === "active" && !wsSlug && result.token) {
|
|
145
|
+
// Fallback: server returned single-brand format
|
|
146
|
+
const slug = pending.brands?.[0]?.brand_slug || "default";
|
|
147
|
+
saveWorkspace(slug, {
|
|
148
|
+
api_url: baseUrl,
|
|
149
|
+
token: result.token,
|
|
150
|
+
brand_id: result.brand_id ?? null,
|
|
151
|
+
brand_name: result.brand_name ?? "",
|
|
152
|
+
});
|
|
153
|
+
forgetPending();
|
|
154
|
+
const currentFb = ensureCurrentWorkspace(slug);
|
|
155
|
+
console.log(`\nAuthenticated! Token saved to workspace "${slug}".`);
|
|
156
|
+
console.log(`Current workspace: ${currentFb}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (result.status === "denied") {
|
|
160
|
+
forgetPending();
|
|
161
|
+
console.error("\nAccess denied by the workspace admin. Check with them before requesting again.");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (result.status === "expired") {
|
|
166
|
+
forgetPending();
|
|
167
|
+
console.error("\nRequest expired before it was approved, so nothing was granted.\n" +
|
|
168
|
+
"Run `opvs auth request` again, and approve the newest email.");
|
|
169
|
+
process.exit(1);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (result.status === "invalid") {
|
|
173
|
+
forgetPending();
|
|
174
|
+
console.error("\nThe server no longer recognises this request. Run `opvs auth request` to start a new one.");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Not decided yet — or approved, but the token has not arrived yet. Either
|
|
179
|
+
// way the request can still be collected; keep it, and say how.
|
|
180
|
+
console.error(`\n${handoffMessage(pending)}`);
|
|
181
|
+
process.exit(2);
|
|
182
|
+
}
|
|
8
183
|
export function registerAuthCommands(program) {
|
|
9
184
|
const auth = program.command("auth").description("Token authentication");
|
|
10
185
|
auth
|
|
11
186
|
.command("request")
|
|
12
|
-
.description("Request a PAT token (sends approval email to admin)"
|
|
187
|
+
.description("Request a PAT token (sends an approval email to the workspace admin). Waits here for the approval, " +
|
|
188
|
+
"then hands off — the request stays approvable on the server for longer, and `opvs auth resume` " +
|
|
189
|
+
"collects the token. Exit codes: 0 approved · 1 denied, expired or failed · 2 not approved yet.")
|
|
13
190
|
.requiredOption("-e, --email <email>", "Admin email (who approves)")
|
|
14
191
|
.option("-p, --project <name>", "Project name (shown in approval email)")
|
|
15
192
|
.option("-s, --scopes <scopes>", "Comma-separated scopes to request (default: the workspace's standard agent scopes)")
|
|
16
193
|
.option("-t, --ttl <hours>", "Token TTL in hours (omit for no expiry)")
|
|
17
194
|
.option("--api-url <url>", "API base URL (overrides config)")
|
|
195
|
+
.option("--wait <minutes>", WAIT_HELP)
|
|
18
196
|
.action(async (opts) => {
|
|
19
197
|
const wsSlug = program.opts().workspace;
|
|
198
|
+
const waitMs = parseWaitMinutes(opts.wait);
|
|
199
|
+
if (waitMs === null) {
|
|
200
|
+
console.error("Error: --wait takes a number of minutes greater than 0");
|
|
201
|
+
process.exit(1);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
20
204
|
const config = loadRawConfig();
|
|
21
205
|
const fingerprint = buildFingerprint();
|
|
22
206
|
// No client-side default. This used to be hard-coded to "board:read,board:write",
|
|
@@ -47,31 +231,17 @@ export function registerAuthCommands(program) {
|
|
|
47
231
|
const { request_id, poll_token, expires_at } = data;
|
|
48
232
|
console.log(`Request ID: ${request_id}`);
|
|
49
233
|
console.log(`Expires: ${expires_at}`);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log(`Scopes: ${result.scopes?.join(", ")}`);
|
|
62
|
-
}
|
|
63
|
-
else if (result.status === "denied") {
|
|
64
|
-
console.error("\nAccess denied by workspace owner.");
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
else if (result.status === "expired") {
|
|
68
|
-
console.error("\nRequest expired. Try again.");
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
console.error("\nRequest timed out. The owner may not have seen the email.");
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
234
|
+
const pending = rememberPending({
|
|
235
|
+
request_id,
|
|
236
|
+
poll_token,
|
|
237
|
+
api_url: baseUrl,
|
|
238
|
+
expires_at,
|
|
239
|
+
owner_email: opts.email,
|
|
240
|
+
workspace_slug: wsSlug,
|
|
241
|
+
brands: [],
|
|
242
|
+
});
|
|
243
|
+
const result = await pollForApproval(baseUrl, request_id, poll_token, waitMs, expires_at);
|
|
244
|
+
settleApproval(result, pending);
|
|
75
245
|
}
|
|
76
246
|
catch (err) {
|
|
77
247
|
const e = err;
|
|
@@ -96,54 +266,17 @@ export function registerAuthCommands(program) {
|
|
|
96
266
|
console.log(`Found ${brandCount} workspace${brandCount !== 1 ? "s" : ""}. Approval email sent to ${opts.email}`);
|
|
97
267
|
console.log(`Request ID: ${request_id}`);
|
|
98
268
|
console.log(`Expires: ${expires_at}`);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Set the first workspace as current
|
|
111
|
-
setCurrentWorkspace(result.workspaces[0].brand_slug);
|
|
112
|
-
const slugs = result.workspaces.map((ws) => ws.brand_slug).join(", ");
|
|
113
|
-
console.log(`\nAuthenticated! ${result.workspaces.length} workspace${result.workspaces.length !== 1 ? "s" : ""} configured: ${slugs}`);
|
|
114
|
-
for (const ws of result.workspaces) {
|
|
115
|
-
// Print the granted scopes per workspace — each token is clamped to its
|
|
116
|
-
// own workspace's policy, so they are not necessarily what was asked for
|
|
117
|
-
// and are not necessarily the same across workspaces.
|
|
118
|
-
const granted = ws.scopes?.length ? ws.scopes.join(", ") : "(none)";
|
|
119
|
-
console.log(` ${ws.brand_slug}: ${ws.brand_name} (ID: ${ws.brand_id})`);
|
|
120
|
-
console.log(` scopes: ${granted}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else if (result.status === "active" && result.token) {
|
|
124
|
-
// Fallback: server returned single-brand format
|
|
125
|
-
const slug = brands?.[0]?.brand_slug || "default";
|
|
126
|
-
saveWorkspace(slug, {
|
|
127
|
-
api_url: baseUrl,
|
|
128
|
-
token: result.token,
|
|
129
|
-
brand_id: result.brand_id ?? null,
|
|
130
|
-
brand_name: result.brand_name ?? "",
|
|
131
|
-
});
|
|
132
|
-
setCurrentWorkspace(slug);
|
|
133
|
-
console.log(`\nAuthenticated! Token saved to workspace "${slug}".`);
|
|
134
|
-
}
|
|
135
|
-
else if (result.status === "denied") {
|
|
136
|
-
console.error("\nAccess denied by admin.");
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
else if (result.status === "expired") {
|
|
140
|
-
console.error("\nRequest expired. Try again.");
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
console.error("\nRequest timed out. The admin may not have seen the email.");
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
269
|
+
const pending = rememberPending({
|
|
270
|
+
request_id,
|
|
271
|
+
poll_token,
|
|
272
|
+
api_url: baseUrl,
|
|
273
|
+
expires_at,
|
|
274
|
+
owner_email: opts.email,
|
|
275
|
+
workspace_slug: null,
|
|
276
|
+
brands: brands ?? [],
|
|
277
|
+
});
|
|
278
|
+
const result = await pollForApproval(baseUrl, request_id, poll_token, waitMs, expires_at);
|
|
279
|
+
settleApproval(result, pending);
|
|
147
280
|
}
|
|
148
281
|
catch (err) {
|
|
149
282
|
const e = err;
|
|
@@ -152,6 +285,30 @@ export function registerAuthCommands(program) {
|
|
|
152
285
|
}
|
|
153
286
|
}
|
|
154
287
|
});
|
|
288
|
+
auth
|
|
289
|
+
.command("resume")
|
|
290
|
+
.description("Collect the token for the last `auth request` on this machine — for an approval that came after " +
|
|
291
|
+
"it stopped waiting. Same exit codes as `auth request`.")
|
|
292
|
+
.option("--wait <minutes>", WAIT_HELP)
|
|
293
|
+
.action(async (opts) => {
|
|
294
|
+
const waitMs = parseWaitMinutes(opts.wait);
|
|
295
|
+
if (waitMs === null) {
|
|
296
|
+
console.error("Error: --wait takes a number of minutes greater than 0");
|
|
297
|
+
process.exit(1);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const pending = loadPendingAuth();
|
|
301
|
+
if (!pending) {
|
|
302
|
+
console.error("No pending access request on this machine. Run `opvs auth request` to start one.");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
console.log(`Resuming request ${pending.request_id}` +
|
|
307
|
+
(pending.owner_email ? ` (approval email: ${pending.owner_email})` : "") +
|
|
308
|
+
".");
|
|
309
|
+
const result = await pollForApproval(pending.api_url, pending.request_id, pending.poll_token, waitMs, pending.expires_at);
|
|
310
|
+
settleApproval(result, pending);
|
|
311
|
+
});
|
|
155
312
|
auth
|
|
156
313
|
.command("status")
|
|
157
314
|
.description("Show current authentication status")
|
|
@@ -181,9 +338,22 @@ export function registerAuthCommands(program) {
|
|
|
181
338
|
});
|
|
182
339
|
auth
|
|
183
340
|
.command("revoke")
|
|
184
|
-
.description("Revoke the token for the current workspace")
|
|
185
|
-
.
|
|
341
|
+
.description("Revoke the token for the current workspace, on the server and on this machine. Asks first; needs --yes off a terminal.")
|
|
342
|
+
.option("-y, --yes", YES_HELP)
|
|
343
|
+
.action(async (opts) => {
|
|
186
344
|
const wsSlug = program.opts().workspace;
|
|
345
|
+
// Ask OUTSIDE the try below: its catch clears the local token, so a
|
|
346
|
+
// refusal raised inside it would be handled as a failed revoke.
|
|
347
|
+
let hasToken = false;
|
|
348
|
+
try {
|
|
349
|
+
hasToken = Boolean(getEffectiveConfig(wsSlug).token);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
/* the try below reports a config error */
|
|
353
|
+
}
|
|
354
|
+
if (hasToken) {
|
|
355
|
+
await confirmOrExit("Revoke this workspace's token?", opts.yes, "It stops working at once, and getting another needs a new `opvs auth request` and an approval");
|
|
356
|
+
}
|
|
187
357
|
try {
|
|
188
358
|
const cfg = getEffectiveConfig(wsSlug);
|
|
189
359
|
if (!cfg.token) {
|