@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/README.md +117 -29
- package/dist/api.js +20 -11
- package/dist/cli.js +607 -0
- package/dist/core.js +80 -210
- package/dist/devops-openapi.json +1 -0
- package/dist/docs.js +204 -0
- package/dist/harness.js +343 -0
- package/dist/hooks.js +4 -4
- package/dist/lanes.js +7 -7
- package/dist/nodii-dev.js +2 -629
- package/dist/oauth.js +1 -1
- package/dist/setup.js +7 -7
- package/dist/upload.js +291 -0
- package/package.json +5 -4
package/dist/nodii-dev.js
CHANGED
|
@@ -1,631 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
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);
|