@nodii/dev 0.4.1 → 0.6.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/dist/nodii-dev.js CHANGED
@@ -1,631 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
- import { stdin, stdout } from "node:process";
4
- import {
5
- CliError,
6
- DEFAULT_HUB_URL,
7
- OPERATOR_AGENT_ID,
8
- VERSION,
9
- deleteCredentials,
10
- encode,
11
- hubRawRequest,
12
- hubRequest,
13
- parseArgs,
14
- query,
15
- readConfig,
16
- requestHash,
17
- requireFlag,
18
- secretFromAws,
19
- storeSecret,
20
- writeConfig,
21
- } from "./core.js";
22
- import {
23
- DEFAULT_AUTH_URL,
24
- DEFAULT_CLIENT_ID,
25
- DEFAULT_EDGE_URL,
26
- PLATFORM_TENANT,
27
- deviceLogin,
28
- revokeOAuth,
29
- } from "./oauth.js";
30
- import { heartbeatCurrentLane, postHook, preHook, settingsSnippet, workflowSuggestion } from "./hooks.js";
31
- import { setupCommand } from "./setup.js";
32
- import {
33
- ALIASES,
34
- GROUPS,
35
- buildOperationRequest,
36
- callOperation,
37
- findFirstClass,
38
- flagsToOperation,
39
- loadOpenApi,
40
- operationsFrom,
41
- parseParams,
42
- readBody,
43
- } from "./api.js";
44
- import {
45
- agentWorkflowCommand,
46
- builtCommand,
47
- laneCommand,
48
- machineCommand,
49
- nextCommand,
50
- peopleCommand,
51
- } from "./lanes.js";
2
+ import { fail, main } from "./cli.js";
52
3
 
53
- const HELP = `nodii-dev ${VERSION}
54
-
55
- Usage: nodii-dev <command> [options] [--json]
56
-
57
- login | logout | whoami | config show
58
- agent register --name NAME | agent heartbeat
59
- issues list|show|create|update|claim|release
60
- inbox list|show
61
- drift file | request file
62
- decisions list|get|search
63
- design entries|entry|guide
64
- figma lock|check|log-write|submit|apply|published|abandon|release
65
- hook claude-figma-pre|claude-figma-post|print-settings
66
- setup [--yes|--check] # dry-run by default
67
-
68
- lane start <key> --title TEXT --kind KIND --side SIDE [--repo REPO] [--issue ISSUE]
69
- lane step <key> --side SIDE --start|--done|--block <step>
70
- lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
71
- lane heartbeat|review|land|verify|prioritize|deprioritize|stop <key>
72
- lane list [--kind KIND] [--status STATUS] [--mine] [--cursor CURSOR]
73
- lane show <key> | next <key>
74
- built list | built ack <key...>
75
- machine list|show|set-coordinator|assign|report
76
- agent lane-limit|grant|revoke
77
- people reveal <id> --reason TEXT | people refresh-names
78
-
79
- api list [--tag TAG]
80
- api <operationId> [--param k=v] [--body @file|-|JSON]
81
- <resource> <action> [ID ...] [--field VALUE]
82
-
83
- Resource commands cover every OpenAPI tag, including services, feature-docs,
84
- contracts, sagas, outbox-topics, tables, libraries, workers, projects, issues,
85
- backlogs, builds, changes, deploy-reports, runtime, people, agents, inbox,
86
- gate, design, runbooks, doctrine, changelog, health-verdicts, approval-kinds,
87
- notification-registry, skills, open-questions, decisions, drift, and requests.
88
-
89
- Run a command with --help or see the package README for all flags.`;
90
-
91
- async function stdinText() {
92
- let value = "";
93
- for await (const chunk of stdin) value += chunk;
94
- return value;
95
- }
96
- const bodyFlag = async (f, name = "body") =>
97
- f[`${name}-file`] ? readFile(f[`${name}-file`], "utf8") : f[name];
98
- function emit(payload, json, human) {
99
- if (json) console.log(JSON.stringify(payload));
100
- else
101
- console.log(
102
- typeof human === "function"
103
- ? human(payload)
104
- : (human ?? JSON.stringify(payload.data ?? payload, null, 2)),
105
- );
106
- }
107
- const qsFlags = (f, mapping) =>
108
- Object.fromEntries(
109
- Object.entries(mapping).map(([to, from]) => [to, f[from]]),
110
- );
111
-
112
- async function login(flags, json) {
113
- const previous = await readConfig(false);
114
- const configuredClientId = flags["client-id"] ?? process.env.NODII_DEV_OAUTH_CLIENT_ID ?? previous?.clientId ?? DEFAULT_CLIENT_ID;
115
- if (configuredClientId && !flags.hmac) {
116
- const result = await deviceLogin({
117
- clientId: configuredClientId,
118
- authUrl: flags["auth-url"] ?? process.env.NODII_DEV_AUTH_URL ?? DEFAULT_AUTH_URL,
119
- edgeUrl: flags["edge-url"] ?? process.env.NODII_DEV_EDGE_URL ?? DEFAULT_EDGE_URL,
120
- tenantHint:
121
- flags["tenant-hint"] ??
122
- process.env.NODII_DEV_PLATFORM_TENANT_ID ??
123
- PLATFORM_TENANT,
124
- noBrowser: Boolean(flags["no-browser"]),
125
- scope: flags.scope,
126
- output: json ? console.error : console.log,
127
- });
128
- return emit({ loggedIn: true, provider: "oauth", credentialStorage: result.storage }, json,
129
- `OAuth login complete; refresh credential stored in ${result.storage}. Run \`nodii-dev agent register --name NAME\` next.`);
130
- }
131
- const hubUrl = flags["hub-url"] ?? DEFAULT_HUB_URL;
132
- const agentId = flags["agent-id"] ?? OPERATOR_AGENT_ID;
133
- const person = requireFlag(flags, "person");
134
- if (flags["secret-stdin"] && flags["from-aws"])
135
- throw new CliError(
136
- "USAGE",
137
- "Choose either --secret-stdin or --from-aws, not both.",
138
- 2,
139
- );
140
- if (
141
- !flags["secret-stdin"] &&
142
- !flags["from-aws"] &&
143
- !process.env.PLANNING_HUB_HMAC_SECRET
144
- )
145
- throw new CliError(
146
- "USAGE",
147
- "Provide --secret-stdin, --from-aws, or PLANNING_HUB_HMAC_SECRET.",
148
- 2,
149
- );
150
- const secret = flags["secret-stdin"]
151
- ? (await stdinText()).trim()
152
- : flags["from-aws"]
153
- ? await secretFromAws()
154
- : process.env.PLANNING_HUB_HMAC_SECRET;
155
- if (!secret)
156
- throw new CliError("EMPTY_SECRET", "The supplied secret is empty.", 2);
157
- const storage = await storeSecret(secret);
158
- await writeConfig({ hubUrl, agentId, person });
159
- emit(
160
- { hubUrl, agentId, person, secretStorage: storage },
161
- json,
162
- `Logged in as ${person} (${agentId}); secret stored in ${storage}.`,
163
- );
164
- }
165
-
166
- async function issues(action, args, f, json) {
167
- if (action === "list") {
168
- const payload = await hubRequest(
169
- "GET",
170
- `/api/v1/issues${query(qsFlags(f, { serviceId: "service", project: "project", status: "status", kind: "kind", priority: "priority", claimed: "claimed", limit: "limit", cursor: "cursor" }))}`,
171
- );
172
- return emit(
173
- payload,
174
- json,
175
- (p) =>
176
- p.data
177
- .map((x) => `${x.key}\t${x.status}\t${x.priority}\t${x.title}`)
178
- .join("\n") || "No issues.",
179
- );
180
- }
181
- const ref = args[0];
182
- if (!ref && action !== "create")
183
- throw new CliError("USAGE", `${action} needs an issue key or id`, 2);
184
- if (action === "show")
185
- return emit(await hubRequest("GET", `/api/v1/issues/${encode(ref)}`), json);
186
- const config = await readConfig();
187
- if (action === "create" || action === "update") {
188
- const bodyMd = await bodyFlag(f);
189
- const payload = {
190
- ...(action === "create"
191
- ? {
192
- serviceId: requireFlag(f, "service"),
193
- title: requireFlag(f, "title"),
194
- }
195
- : {}),
196
- ...Object.fromEntries(
197
- [
198
- ["title", f.title],
199
- ["kind", f.kind],
200
- ["priority", f.priority],
201
- ["project", f.project],
202
- ["bodyMd", bodyMd],
203
- ["onBehalfOf", config.person],
204
- ].filter(([, v]) => v !== undefined),
205
- ),
206
- };
207
- const result = await hubRequest(
208
- action === "create" ? "POST" : "PATCH",
209
- action === "create" ? "/api/v1/issues" : `/api/v1/issues/${encode(ref)}`,
210
- payload,
211
- );
212
- return emit(
213
- result,
214
- json,
215
- (p) => `${p.data.key} ${p.data.status} — ${p.data.title}`,
216
- );
217
- }
218
- if (action === "claim")
219
- return emit(
220
- await hubRequest("POST", `/api/v1/issues/${encode(ref)}/claim`, {
221
- onBehalfOf: config.person,
222
- ...(f.ttl ? { ttlMinutes: Number(f.ttl) } : {}),
223
- }),
224
- json,
225
- );
226
- if (action === "release")
227
- return emit(
228
- await hubRequest("POST", `/api/v1/issues/${encode(ref)}/release`, {
229
- onBehalfOf: config.person,
230
- ...(f.note ? { note: f.note } : {}),
231
- }),
232
- json,
233
- );
234
- throw new CliError("USAGE", `Unknown issues action: ${action}`, 2);
235
- }
236
-
237
- async function main(argv = process.argv.slice(2)) {
238
- if (argv.length === 1 && ["--version", "-v"].includes(argv[0]))
239
- return console.log(VERSION);
240
- if (argv.length === 1 && ["--help", "-h"].includes(argv[0]))
241
- return console.log(HELP);
242
- const { args, flags: f } = parseArgs(argv);
243
- const json = Boolean(f.json);
244
- const [command, action, ...rest] = args;
245
- if (!command || f.help || command === "help") return console.log(HELP);
246
- if (command === "api") {
247
- const operations = operationsFrom(await loadOpenApi());
248
- if (action === "list") {
249
- const selected = f.tag === undefined || f.tag === true
250
- ? operations
251
- : operations.filter((x) => x.tags.some((t) => t.toLowerCase() === String(f.tag).toLowerCase()));
252
- return emit(selected.map(({ operationId, method, path, tags }) => ({ operationId, method, path, tags })), json,
253
- (rows) => rows.map((x) => `${x.operationId}\t${x.method}\t${x.path}\t${x.tags.join(",")}`).join("\n"));
254
- }
255
- if (!action) throw new CliError("USAGE", "api needs an operationId or list", 2);
256
- const entry = operations.find((x) => x.operationId === action);
257
- if (!entry) throw new CliError("UNKNOWN_OPERATION", `No OpenAPI operationId: ${action}`, 2);
258
- const parsed = await readBody(f.body);
259
- const request = buildOperationRequest(entry, parseParams(f.param), parsed?.value);
260
- return emit(await hubRawRequest(request.method, request.path, parsed?.text), json);
261
- }
262
- if (command === "login") return login(f, json);
263
- if (command === "logout") {
264
- const c = await readConfig(false);
265
- if (c?.authProvider === "oauth") {
266
- try { await revokeOAuth(c); } finally { await deleteCredentials(); }
267
- } else await deleteCredentials();
268
- return emit({ loggedOut: true }, json, "Logged out.");
269
- }
270
- if (command === "config" && action === "show") {
271
- const c = await readConfig();
272
- if (c.authProvider === "oauth")
273
- return emit({ provider: "oauth", authUrl: c.authUrl, edgeUrl: c.edgeUrl, clientId: c.clientId, tenantHint: c.tenantHint, agentId: c.agentId }, json,
274
- `${c.edgeUrl}\nprovider: oauth\napp: ${c.clientId}\nagent: ${c.agentId ?? "not registered"}`);
275
- return emit(
276
- c,
277
- json,
278
- `${c.hubUrl}\nagent: ${c.agentId}\nperson: ${c.person}`,
279
- );
280
- }
281
- if (command === "whoami") {
282
- const c = await readConfig();
283
- if (c.authProvider === "oauth") {
284
- try { return emit(await hubRequest("GET", "/api/v1/whoami"), json); }
285
- catch (error) {
286
- if (!["HTTP_404", "NOT_FOUND"].includes(error.code)) throw error;
287
- return emit({ data: { person: c.person, app: c.clientId, installation: c.installationId, agent: c.agentId } }, json);
288
- }
289
- }
290
- const remote = await hubRequest(
291
- "GET",
292
- `/api/v1/agents/${encode(c.agentId)}`,
293
- );
294
- return emit({ data: { ...c, agent: remote.data } }, json);
295
- }
296
- const emitCommand = async (result) => emit(result.payload, json, result.human);
297
- if (command === "setup") return emitCommand(await setupCommand(f));
298
- if (command === "lane") return emitCommand(await laneCommand(action, rest, f));
299
- if (command === "next") return emitCommand(await nextCommand(action, f));
300
- if (command === "built") return emitCommand(await builtCommand(action, rest, f));
301
- if (command === "machine") return emitCommand(await machineCommand(action, rest, f));
302
- if (command === "people" && ["reveal", "refresh-names"].includes(action))
303
- return emitCommand(await peopleCommand(action, rest, f));
304
- if (command === "state" && action === "diff") {
305
- const [health, services, decisions, docs, requests, drift] = await Promise.all([
306
- hubRequest("GET", "/api/v1/health"), hubRequest("GET", "/api/v1/services"),
307
- hubRequest("GET", "/api/v1/decisions"), hubRequest("GET", "/api/v1/feature-docs"),
308
- hubRequest("GET", "/api/v1/requests"), hubRequest("GET", "/api/v1/drift"),
309
- ]);
310
- const count = (x) => Array.isArray(x.data) ? x.data.length : undefined;
311
- return emit({ data: { health: health.data ?? health, counts: { services: count(services), decisions: count(decisions), featureDocs: count(docs), requests: count(requests), drift: count(drift) } } }, json);
312
- }
313
- if (command === "heartbeat" && action === "audit") {
314
- const ids = rest[0] ? [rest[0]] : (await hubRequest("GET", "/api/v1/services?limit=1000")).data.map((x) => x.id);
315
- const rows = await Promise.all(ids.map(async (id) => ({ serviceId: id, ...(await hubRequest("GET", `/api/v1/services/${encode(id)}/unblock-surface`)).data })));
316
- const failed = rows.some((x) => (x.openDriftCount ?? 0) > 0 || (x.openInboxRequestCount ?? 0) > 0);
317
- emit({ data: rows, clean: !failed }, json, (p) => p.data.map((x) => `${x.serviceId}\t${(x.openDriftCount ?? 0) || (x.openInboxRequestCount ?? 0) ? "ATTENTION" : "PASS"}\tdrift=${x.openDriftCount ?? 0}\tinbox=${x.openInboxRequestCount ?? 0}`).join("\n"));
318
- if (failed) process.exitCode = 1;
319
- return;
320
- }
321
- if (command === "agents" && action === "spawn") {
322
- const serviceId = rest[0] ?? requireFlag(f, "service");
323
- const docs = await hubRequest("GET", `/api/v1/feature-docs${query({ serviceId, status: "approved", limit: 200 })}`);
324
- if (!docs.data?.length) throw new CliError("NO_APPROVED_WORK", `No approved feature docs for ${serviceId}`, 1);
325
- const repo = `${f["repo-root"] ?? `${process.env.HOME}/Projects`}/${serviceId}`;
326
- return emit({ data: { serviceId, repo, approvedFeatureDocs: docs.data.length, next: `cd ${repo} && nodii-dev agent register --name <name>` } }, json);
327
- }
328
- if (command === "agents" && action === "cowork-tick") {
329
- if (f.plist) {
330
- const interval = Number(f.interval ?? 1800);
331
- return console.log(`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>com.nodii.cowork</string><key>ProgramArguments</key><array><string>nodii-dev</string><string>agents</string><string>cowork-tick</string></array><key>StartInterval</key><integer>${interval}</integer><key>RunAtLoad</key><true/></dict></plist>`);
332
- }
333
- const docs = await hubRequest("GET", "/api/v1/feature-docs?status=implementing&limit=1000");
334
- const claims = docs.data.filter((x) => x.claimedByAgentId && (!x.claimExpiresAt || Date.parse(x.claimExpiresAt) > Date.now()));
335
- return emit({ data: claims, dryRun: true, note: "Wakeup execution is workstation-local and has no hub endpoint; this tick reports eligible claims without invoking an external agent runtime." }, json,
336
- (p) => p.data.map((x) => `${x.serviceId}/${x.docKey}\t${x.claimedByAgentId}`).join("\n") || "No active claims.");
337
- }
338
- if (command === "agent") {
339
- if (["lane-limit", "grant", "revoke"].includes(action))
340
- return emitCommand(await agentWorkflowCommand(action, rest, f));
341
- const c = await readConfig();
342
- if (action === "register") {
343
- const result = await hubRequest("POST", c.authProvider === "oauth" ? "/api/v1/agents/register" : "/api/v1/agents", {
344
- sessionId: f.session ?? crypto.randomUUID(),
345
- name: requireFlag(f, "name"),
346
- ...(c.authProvider === "oauth" ? {} : { personId: c.person }),
347
- });
348
- await writeConfig({ ...c, agentId: result.data.id });
349
- // The hub ignores a self-claimed service: staff bind agents to services
350
- // on the Dev Agents page (devops.agents.bind).
351
- if (f.service)
352
- process.stderr.write(
353
- "--service is ignored: staff bind agents to a service on the Dev Agents page.\n",
354
- );
355
- await hubRequest(
356
- "POST",
357
- `/api/v1/agents/${encode(result.data.id)}/heartbeat`,
358
- {},
359
- );
360
- return emit(
361
- result,
362
- json,
363
- `Registered ${result.data.name}: ${result.data.id}`,
364
- );
365
- }
366
- if (action === "heartbeat")
367
- return emit(
368
- await hubRequest(
369
- "POST",
370
- `/api/v1/agents/${encode(c.agentId)}/heartbeat`,
371
- {},
372
- ),
373
- json,
374
- "Heartbeat recorded.",
375
- );
376
- }
377
- if (command === "issues") return issues(action, rest, f, json);
378
- if (command === "inbox") {
379
- if (action === "list")
380
- return emit(
381
- await hubRequest(
382
- "GET",
383
- `/api/v1/inbox${query({ serviceId: f.service, kind: f.kind, limit: f.limit, cursor: f.cursor })}`,
384
- ),
385
- json,
386
- );
387
- if (action === "show") {
388
- const [kind, id] = rest;
389
- if (!kind || !id)
390
- throw new CliError(
391
- "USAGE",
392
- "inbox show needs <drift|request|question> <id>",
393
- 2,
394
- );
395
- return emit(
396
- await hubRequest(
397
- "GET",
398
- `/api/v1/${kind === "question" ? "open-questions" : `${kind}s`}/${encode(id)}`,
399
- ),
400
- json,
401
- );
402
- }
403
- }
404
- if (command === "drift" && action === "file") {
405
- const service = requireFlag(f, "service");
406
- const bodyMd = await bodyFlag(f);
407
- return emit(
408
- await hubRequest("POST", `/api/v1/services/${encode(service)}/drift`, {
409
- severity: requireFlag(f, "severity"),
410
- summary: requireFlag(f, "summary"),
411
- bodyMd: bodyMd ?? "",
412
- relatedArtifactType: f["related-artifact-type"] ?? null,
413
- relatedArtifactId: f["related-artifact-id"] ?? null,
414
- }),
415
- json,
416
- );
417
- }
418
- if (command === "request" && action === "file") {
419
- const specMd = f["spec-file"]
420
- ? await readFile(f["spec-file"], "utf8")
421
- : requireFlag(f, "spec");
422
- const body = {
423
- fromService: requireFlag(f, "from"),
424
- toService: requireFlag(f, "to"),
425
- kind: requireFlag(f, "kind"),
426
- specMd,
427
- requestHash: requestHash({ toService: f.to, kind: f.kind, specMd }),
428
- ...(f["required-capability"]
429
- ? { requiredCapability: f["required-capability"] }
430
- : {}),
431
- targetArtifactType: f["target-artifact-type"] ?? null,
432
- targetArtifactId: f["target-artifact-id"] ?? null,
433
- };
434
- return emit(await hubRequest("POST", "/api/v1/requests", body), json);
435
- }
436
- if (command === "requests" && action === "file") {
437
- const specMd = f["spec-file"] ? await readFile(f["spec-file"], "utf8") : requireFlag(f, "spec");
438
- f.fromService = requireFlag(f, "from"); f.toService = requireFlag(f, "to"); f.specMd = specMd;
439
- f.requestHash = requestHash({ toService: f.toService, kind: requireFlag(f, "kind"), specMd });
440
- }
441
- if (command === "decisions") {
442
- if (action === "get")
443
- return emit(
444
- await hubRequest("GET", `/api/v1/decisions/${encode(rest[0])}`),
445
- json,
446
- );
447
- const q = action === "search" ? rest.join(" ") || f.q : undefined;
448
- const payload = await hubRequest(
449
- "GET",
450
- `/api/v1/decisions${query({ status: f.status, module: f.module, tag: f.tag, limit: f.limit ?? 200 })}`,
451
- );
452
- if (q)
453
- payload.data = payload.data.filter((x) =>
454
- `${x.id} ${x.title} ${(x.topicTags ?? []).join(" ")}`
455
- .toLowerCase()
456
- .includes(q.toLowerCase()),
457
- );
458
- return emit(
459
- payload,
460
- json,
461
- (p) =>
462
- p.data.map((x) => `${x.id}\t${x.status}\t${x.title}`).join("\n") ||
463
- "No decisions.",
464
- );
465
- }
466
- if (command === "design") {
467
- if (action === "entries")
468
- return emit(
469
- await hubRequest(
470
- "GET",
471
- `/api/v1/design/entries${query({ kind: f.kind, group: f.group, q: f.q, limit: f.limit })}`,
472
- ),
473
- json,
474
- );
475
- if (action === "entry")
476
- return emit(
477
- await hubRequest("GET", `/api/v1/design/entries/${encode(rest[0])}`),
478
- json,
479
- );
480
- if (action === "guide")
481
- return emit(
482
- await hubRequest("GET", `/api/v1/design/guides/${encode(rest[0])}`),
483
- json,
484
- );
485
- }
486
- if (command === "figma") {
487
- if (action === "lock")
488
- return emit(
489
- await hubRequest(
490
- "POST",
491
- `/api/v1/design/files/${encode(rest[0])}/lock`,
492
- { changeSetTitle: requireFlag(f, "title") },
493
- ),
494
- json,
495
- );
496
- if (action === "check") {
497
- const c = await readConfig();
498
- const result = await hubRequest(
499
- "GET",
500
- `/api/v1/design/files/${encode(rest[0])}/lock/check?agentId=${encode(c.agentId)}`,
501
- );
502
- emit(
503
- result,
504
- json,
505
- (p) =>
506
- `${p.data.allowed ? "allowed" : "denied"}: ${p.data.reason ?? "lock held"}`,
507
- );
508
- if (!result.data.allowed) process.exitCode = 3;
509
- return;
510
- }
511
- if (action === "log-write")
512
- return emit(
513
- await hubRequest(
514
- "POST",
515
- `/api/v1/design/change-sets/${encode(rest[0])}/writes`,
516
- {
517
- tool: requireFlag(f, "tool"),
518
- nodeIds: requireFlag(f, "nodes").split(",").filter(Boolean),
519
- },
520
- ),
521
- json,
522
- );
523
- if (["submit", "apply", "published", "abandon"].includes(action)) {
524
- const payload = f.file
525
- ? JSON.parse(await readFile(f.file, "utf8"))
526
- : action === "abandon"
527
- ? {}
528
- : { figmaVersion: requireFlag(f, "figma-version") };
529
- return emit(
530
- await hubRequest(
531
- "POST",
532
- `/api/v1/design/change-sets/${encode(rest[0])}/${action}`,
533
- payload,
534
- ),
535
- json,
536
- );
537
- }
538
- if (action === "release")
539
- return emit(
540
- await hubRequest(
541
- "POST",
542
- `/api/v1/design/files/${encode(rest[0])}/lock/release`,
543
- {},
544
- ),
545
- json,
546
- );
547
- }
548
- if (command === "hook") {
549
- if (action === "print-settings")
550
- return console.log(JSON.stringify(settingsSnippet(), null, 2));
551
- if (action === "claude-figma-pre") {
552
- // Fail closed: Claude Code only blocks a tool call on exit code 2, and
553
- // treats any other non-zero exit as a non-blocking error that lets the
554
- // write through. Every failure here (bad stdin, missing config, hub
555
- // error, lock denied) must therefore exit 2.
556
- try {
557
- const payload = JSON.parse(await stdinText());
558
- return emit(await preHook(payload), json, "allowed");
559
- } catch (error) {
560
- throw new CliError(
561
- error?.code ?? "FIGMA_HOOK_FAILED",
562
- `Blocked Figma write: ${error?.message ?? String(error)}`,
563
- 2,
564
- error?.details,
565
- );
566
- }
567
- }
568
- if (action === "lane-heartbeat") {
569
- const result = await heartbeatCurrentLane();
570
- return emit(result, json, result.heartbeated ? `Heartbeat recorded for ${result.key}.` : "No current-branch lane; no heartbeat sent.");
571
- }
572
- const payload = JSON.parse(await stdinText());
573
- if (action === "workflow-post") {
574
- const suggestion = workflowSuggestion(payload);
575
- if (suggestion) console.log(suggestion);
576
- return;
577
- }
578
- if (action === "claude-figma-post") {
579
- try {
580
- return emit(await postHook(payload), json, "logged");
581
- } catch (error) {
582
- console.error(
583
- `nodii-dev post hook: ${error.code ?? "ERROR"}: ${error.message}`,
584
- );
585
- return;
586
- }
587
- }
588
- }
589
- if (Object.hasOwn(GROUPS, command)) {
590
- const operations = operationsFrom(await loadOpenApi());
591
- let entry = findFirstClass(operations, command, action);
592
- if (command === "drift" && action === "resolve" && f.status === "accepted-as-canonical")
593
- entry = operations.find((x) => x.operationId === "acceptDrift");
594
- if (command === "contracts") {
595
- const [kind, verb, ...ids] = [action, ...rest];
596
- const type = { grpc: "GrpcInterface", rest: "RestInterface", outbox: "OutboxTopic", saga: "Saga", table: "Table" }[kind];
597
- const op = verb === "publish" ? `create${type}` : verb === "status" && f.status === "implemented" ? `implement${type}` : null;
598
- if (!op) throw new CliError("USAGE", "contracts: use <grpc|rest|outbox|saga|table> publish|status; status currently supports --status implemented", 2);
599
- entry = operations.find((x) => x.operationId === op);
600
- rest.splice(0, rest.length, ...ids);
601
- }
602
- if (!entry) {
603
- const tags = Array.isArray(GROUPS[command]) ? GROUPS[command] : [GROUPS[command]];
604
- const available = operations.filter((x) => x.tags.some((t) => tags.includes(t))).map((x) => x.operationId).join(", ");
605
- throw new CliError("USAGE", `Unknown ${command} action: ${action}. OpenAPI operations: ${available}`, 2);
606
- }
607
- const built = flagsToOperation(entry, rest, f);
608
- if (command === "requests" && action === "answer")
609
- Object.assign(built.params, { kind: "request", id: rest[0] });
610
- if (command === "drift" && entry.operationId === "acceptDrift" && !built.body?.relatedDecisionId && !built.body?.newDecision)
611
- throw new CliError("USAGE", "accepted-as-canonical drift requires --related-decision-id or --new-decision JSON", 2);
612
- const explicit = await readBody(f.body);
613
- return emit(await callOperation(entry, built.params, explicit?.value ?? built.body), json);
614
- }
615
- throw new CliError(
616
- "USAGE",
617
- `Unknown command: ${[command, action].filter(Boolean).join(" ")}`,
618
- 2,
619
- );
620
- }
621
-
622
- main().catch((error) => {
623
- const e =
624
- error instanceof CliError
625
- ? error
626
- : new CliError("UNEXPECTED", error?.message ?? String(error));
627
- console.error(
628
- `${e.code}: ${e.message}${e.details ? `\n${JSON.stringify(e.details)}` : ""}`,
629
- );
630
- process.exitCode = e.exitCode;
631
- });
4
+ main().catch(fail);
package/dist/oauth.js CHANGED
@@ -19,7 +19,7 @@ export const FORBIDDEN_SCOPE = "devops.releases.promote";
19
19
  * The scope sent at device authorization. By default none is sent, and
20
20
  * nodii-auth grants the app's provisioned ceiling (every live devops.* key
21
21
  * except promotion, D1368). An explicit --scope narrows it; promotion is
22
- * refused locally as well as by auth and the hub.
22
+ * refused locally as well as by auth and by Dev.
23
23
  */
24
24
  export function requestedScope(scope) {
25
25
  const keys = (scope ?? "").split(/[\s,]+/).filter(Boolean);