@nodii/dev 0.6.17 → 0.6.21
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 +50 -3
- package/dist/cli.js +73 -24
- package/dist/core.js +23 -8
- package/dist/design.js +15 -9
- package/dist/doctor.js +6 -3
- package/dist/figma.js +168 -8
- package/dist/lanes.js +23 -1
- package/dist/oauth.js +149 -11
- package/dist/vendor/ds-conformance.mjs +259 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,30 @@ Linux only (the launcher is a POSIX shell script).
|
|
|
12
12
|
nodii-dev login # opens the approval page in your browser
|
|
13
13
|
nodii-dev login --no-browser # prints the link and code instead
|
|
14
14
|
nodii-dev login --scope devops.issues.read,devops.issues.write # narrower grant
|
|
15
|
+
nodii-dev login --force # replace the machine's sign-in (never from a lane)
|
|
15
16
|
nodii-dev agent register --name my-agent
|
|
16
17
|
```
|
|
17
18
|
|
|
19
|
+
A machine has **one sign-in**. The refresh credential is kept in the keychain
|
|
20
|
+
under the OAuth client, not beside a config file, so every config path on the
|
|
21
|
+
machine (`NODII_DEV_CONFIG_PATH`) shares it. `login` checks it first: when the
|
|
22
|
+
shared access token is live or the refresh credential refreshes, it prints
|
|
23
|
+
"You're already signed in as PERSON on this machine." and exits 0 without a
|
|
24
|
+
device flow, from any config path, an empty or new one included (it writes
|
|
25
|
+
the routing there, without an agent). A sign-in nodii-auth no longer accepts
|
|
26
|
+
(`invalid_grant`), or a refresh it refuses outright, is named, then replaced
|
|
27
|
+
by a device flow. If nodii-auth cannot be reached, or the keychain cannot be
|
|
28
|
+
read, `login` stops: that says nothing about the sign-in. A different
|
|
29
|
+
`--scope` replaces the one sign-in, so with a valid one it needs `--force`.
|
|
30
|
+
`login --force` starts a device flow whatever the machine holds and names the
|
|
31
|
+
person it replaces. A lane's config (`lane-*.json`) never starts a device
|
|
32
|
+
flow (`--force`, or `login` over a dead sign-in: the coordinator signs in) and
|
|
33
|
+
refuses `logout`; both end the one sign-in for every lane.
|
|
34
|
+
`agent register` on a fresh, empty config path takes the machine's sign-in the
|
|
35
|
+
same way (the OAuth client and hosts from the machine's newest config, the
|
|
36
|
+
person and installation from the token; `NODII_DEV_*` variables override),
|
|
37
|
+
so a new lane needs no login and no hand-seeded config.
|
|
38
|
+
|
|
18
39
|
`nodii-dev` talks only to Dev, through the platform edge
|
|
19
40
|
(`https://platform.nucleus-cloud.in/devops/*`), signed in as the platform
|
|
20
41
|
tenant's nodii-dev OAuth app (D1368). There is no other host and no other way
|
|
@@ -62,7 +83,7 @@ Dev failures print Dev's error code to stderr and exit non-zero.
|
|
|
62
83
|
## Commands
|
|
63
84
|
|
|
64
85
|
```text
|
|
65
|
-
nodii-dev login [--no-browser] [--scope KEYS]
|
|
86
|
+
nodii-dev login [--no-browser] [--scope KEYS] [--force]
|
|
66
87
|
nodii-dev logout
|
|
67
88
|
nodii-dev whoami # OAuth identity plus the registered agent
|
|
68
89
|
nodii-dev config show
|
|
@@ -82,7 +103,8 @@ nodii-dev issues list [--service ID] [--project KEY] [--status STATUS]
|
|
|
82
103
|
nodii-dev issues show ISSUE
|
|
83
104
|
nodii-dev issues create --service ID --title TEXT [--body TEXT|--body-file FILE]
|
|
84
105
|
[--kind KIND] [--priority PRIORITY] [--project KEY]
|
|
85
|
-
nodii-dev issues update ISSUE [the same mutable fields]
|
|
106
|
+
nodii-dev issues update ISSUE [the same mutable fields] [--comment TEXT|--comment-file FILE]
|
|
107
|
+
nodii-dev issues comment ISSUE --body TEXT|--body-file FILE # commentOnIssue, on the issue's activity feed
|
|
86
108
|
nodii-dev issues claim ISSUE [--ttl MINUTES]
|
|
87
109
|
nodii-dev issues release ISSUE [--note TEXT]
|
|
88
110
|
nodii-dev issues transition ISSUE --to todo|in_progress|in_review|blocked|done|wont_do
|
|
@@ -280,7 +302,9 @@ nodii-dev hook print-settings # prints managed-settings JSON; writes not
|
|
|
280
302
|
|
|
281
303
|
nodii-dev lane start KEY --title TEXT --kind dev_fix|new_build|docs_change|uat_fix|prod_fix
|
|
282
304
|
--side backend|frontend|both [--repo REPO] [--issue ISSUE]
|
|
283
|
-
nodii-dev lane step KEY --side backend|frontend --start|--done|--block STEP
|
|
305
|
+
nodii-dev lane step KEY --side backend|frontend --start|--done|--block STEP [--reason TEXT]
|
|
306
|
+
# --reason is kept as a lane note; on a step already blocked,
|
|
307
|
+
# --block --reason records the new reason (Dev issue 125 moves it onto the step)
|
|
284
308
|
nodii-dev lane evidence KEY --pr|--commit|--figma|--deploy|--dev-check REF
|
|
285
309
|
nodii-dev lane heartbeat KEY [--note TEXT]
|
|
286
310
|
nodii-dev lane review KEY
|
|
@@ -402,6 +426,29 @@ version. A set never submitted needs `--file`: the body `figma submit --file`
|
|
|
402
426
|
would send. A cold run can outlast the edge's gateway; the CLI then says
|
|
403
427
|
`DEV_TIMEOUT` and to run it again (Dev keeps the mains it read for 5 minutes).
|
|
404
428
|
|
|
429
|
+
### Large sets: submit as a Dev job
|
|
430
|
+
|
|
431
|
+
`figma submit`, `figma resubmit` and `design check --change-set` send
|
|
432
|
+
`Prefer: respond-async`. Dev answers 202 with a job, and the CLI polls it
|
|
433
|
+
(`getDesignSubmitJob`) with backoff, printing progress on stderr, until it
|
|
434
|
+
ends. The output and exit code are the synchronous call's: a succeeded job
|
|
435
|
+
prints Dev's 200 body and warnings, a failed one its error and findings. A
|
|
436
|
+
large set's gate outlasts the edge's 30 s timeout, so this is the default.
|
|
437
|
+
|
|
438
|
+
- `a submit is already running: JOB`: Dev already runs a submit (or a dry run)
|
|
439
|
+
for this set (409 `SUBMIT_IN_PROGRESS` / `CONFORMANCE_IN_PROGRESS`). The CLI
|
|
440
|
+
polls that job instead of sending again, then exits 1 with the 409's code
|
|
441
|
+
and that job's outcome (`--json`: `details.result` or `details.error`):
|
|
442
|
+
this command's body was not sent, so read the set and run it again if it
|
|
443
|
+
still needs to go.
|
|
444
|
+
- `SUBMIT_JOB_LOST`: Dev restarted while the job ran. Run the same command again.
|
|
445
|
+
- `SUBMIT_JOB_UNREACHABLE`: polling lost the edge; the job runs on in Dev.
|
|
446
|
+
Read the set with `figma changeset show` before sending it again.
|
|
447
|
+
- `--sync` sends the old single request. If it (or a submit's 202) meets the
|
|
448
|
+
edge's 502 or 504, the CLI reads the set and says whether the submit landed;
|
|
449
|
+
it never resends. `SUBMIT_UNCONFIRMED` means it hasn't yet: check with
|
|
450
|
+
`figma changeset show` before submitting again.
|
|
451
|
+
|
|
405
452
|
When `figma submit`, `figma resubmit` or the dry run hits
|
|
406
453
|
`DS_CONFORMANCE_FAILED`, the CLI prints each finding and Dev's warnings. A
|
|
407
454
|
submit Dev accepts prints its warnings too. On a stale version it also prints
|
package/dist/cli.js
CHANGED
|
@@ -15,11 +15,9 @@ import {
|
|
|
15
15
|
writeConfig,
|
|
16
16
|
} from "./core.js";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
PLATFORM_TENANT,
|
|
22
|
-
deviceLogin,
|
|
18
|
+
adoptMachineSignIn,
|
|
19
|
+
login as signIn,
|
|
20
|
+
refuseFromLane,
|
|
23
21
|
revokeOAuth,
|
|
24
22
|
} from "./oauth.js";
|
|
25
23
|
import { heartbeatCurrentLane, postHook, postHookFailure, preHook, settingsSnippet, workflowSuggestion } from "./hooks.js";
|
|
@@ -43,12 +41,13 @@ import {
|
|
|
43
41
|
figmaScopeSet,
|
|
44
42
|
figmaScopeSetText,
|
|
45
43
|
figmaSubmit,
|
|
44
|
+
submitMode,
|
|
46
45
|
isChangeSetId,
|
|
47
46
|
scopeText,
|
|
48
47
|
scopesFrom,
|
|
49
48
|
conformanceLines,
|
|
50
49
|
} from "./figma.js";
|
|
51
|
-
export { figmaLock } from "./figma.js";
|
|
50
|
+
export { figmaLock, submitMode } from "./figma.js";
|
|
52
51
|
import { docsCommand, rulebookCommand, rulesCommand } from "./upload.js";
|
|
53
52
|
import { changeSetCommand, guidelineCommand } from "./guidelines.js";
|
|
54
53
|
import { designCheck, designCheckText, designRoles, designRolesText } from "./design.js";
|
|
@@ -78,11 +77,14 @@ const HELP = `nodii-dev ${VERSION}
|
|
|
78
77
|
|
|
79
78
|
Usage: nodii-dev <command> [options] [--json]
|
|
80
79
|
|
|
81
|
-
login | logout | whoami | config show
|
|
80
|
+
login [--force] | logout | whoami | config show
|
|
81
|
+
# login says so when the machine is already signed in; --force
|
|
82
|
+
# replaces that one sign-in (never from a lane)
|
|
82
83
|
doctor [--figma-file KEY] [--json] # preflight: sign-in, agent, permissions, machine, repo, lane slots
|
|
83
84
|
# and, given a key (or NODII_DEV_FIGMA_FILE), that Figma file's registration
|
|
84
85
|
agent register --name NAME | agent heartbeat
|
|
85
86
|
issues list|show|create|update|claim|release
|
|
87
|
+
issues comment ISSUE --body TEXT # or --body-file FILE; issues update --comment TEXT|--comment-file FILE adds one too
|
|
86
88
|
issues transition ISSUE --to STATUS [--reason TEXT] [--dev-check REF]
|
|
87
89
|
# blocked and wont_do need --reason; done needs --dev-check
|
|
88
90
|
inbox list|show
|
|
@@ -96,7 +98,7 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
96
98
|
design check FILE_KEY|LINK [--nodes A,B] [--roles R,S] [--library]
|
|
97
99
|
# the DS conformance check Dev's submit runs, before you submit;
|
|
98
100
|
# reads Figma with FIGMA_PAT; exit 1 = DS_CONFORMANCE_FAILED or an error (--json: data.passed)
|
|
99
|
-
design check --change-set CS [--file SUBMIT.json] [--figma-version V]
|
|
101
|
+
design check --change-set CS [--file SUBMIT.json] [--figma-version V] [--sync]
|
|
100
102
|
# Dev's dry run of the submit gate on a set, with the image guard;
|
|
101
103
|
# a stale version is a warning that prints when to retry
|
|
102
104
|
figma lock FILE_KEY --title TEXT [--scope KIND:ID ...]
|
|
@@ -107,8 +109,12 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
107
109
|
figma publish-plan FILE_KEY # what to publish, and what to leave unchecked in Figma's dialog
|
|
108
110
|
figma published FILE_KEY|CHANGE_SET --figma-version V # a file key marks every awaiting set published
|
|
109
111
|
figma check|log-write|submit|apply|abandon|release
|
|
112
|
+
figma submit CHANGE_SET --file SUBMIT.json [--sync]
|
|
113
|
+
# submit, resubmit and design check --change-set run as a job in Dev
|
|
114
|
+
# and poll it (a large set outlasts the edge's timeout); --sync waits
|
|
115
|
+
# on one request instead. A 502/504 is checked against the set, never resent
|
|
110
116
|
figma withdraw CHANGE_SET # holder takes back a submit: in_review -> editing (or a re-submit -> applying)
|
|
111
|
-
figma resubmit CHANGE_SET PAYLOAD.json
|
|
117
|
+
figma resubmit CHANGE_SET PAYLOAD.json [--sync]
|
|
112
118
|
# holder re-submits every rejected item: applying -> in_review, round + 1
|
|
113
119
|
figma changeset show CHANGE_SET # items, variants, round, decision and comment (with the previous round's)
|
|
114
120
|
figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
|
|
@@ -129,7 +135,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
129
135
|
rules cap TOKENS # operator: set the core token cap (default 3000)
|
|
130
136
|
|
|
131
137
|
lane start <key> --title TEXT --kind KIND --side SIDE [--repo REPO] [--issue ISSUE]
|
|
132
|
-
lane step <key> --side SIDE --start|--done|--block <step>
|
|
138
|
+
lane step <key> --side SIDE --start|--done|--block <step> [--reason TEXT]
|
|
139
|
+
# --block --reason on a blocked step records its new reason
|
|
133
140
|
lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
|
|
134
141
|
lane heartbeat|review|land|verify|prioritize|deprioritize|stop <key>
|
|
135
142
|
lane non-issue <key> --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
|
|
@@ -194,18 +201,22 @@ async function login(flags, json) {
|
|
|
194
201
|
"nodii-dev talks only to Dev, through the platform edge, signed in as the platform tenant's OAuth app. Run `nodii-dev login`.",
|
|
195
202
|
2,
|
|
196
203
|
);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
const result = await signIn({
|
|
205
|
+
force: flags.force === true,
|
|
206
|
+
previous: await readConfig(false),
|
|
207
|
+
clientId: flags["client-id"] ?? process.env.NODII_DEV_OAUTH_CLIENT_ID,
|
|
208
|
+
authUrl: flags["auth-url"] ?? process.env.NODII_DEV_AUTH_URL,
|
|
209
|
+
edgeUrl: flags["edge-url"] ?? process.env.NODII_DEV_EDGE_URL,
|
|
210
|
+
tenantHint: flags["tenant-hint"] ?? process.env.NODII_DEV_PLATFORM_TENANT_ID,
|
|
203
211
|
noBrowser: Boolean(flags["no-browser"]),
|
|
204
212
|
scope: flags.scope,
|
|
205
213
|
output: json ? console.error : console.log,
|
|
206
|
-
previous,
|
|
207
214
|
});
|
|
208
|
-
|
|
215
|
+
if (result.alreadySignedIn)
|
|
216
|
+
return emit({ loggedIn: true, alreadySignedIn: true, provider: "oauth", person: result.person, agentId: result.agentId }, json,
|
|
217
|
+
`You're already signed in${result.person ? ` as ${result.person}` : ""} on this machine.${result.agentId ? "" : " Run `nodii-dev agent register --name NAME` next."}`);
|
|
218
|
+
return emit({ loggedIn: true, alreadySignedIn: false, provider: "oauth", person: result.person, credentialStorage: result.storage, agentId: result.agentId,
|
|
219
|
+
...(result.replaced !== undefined ? { replaced: result.replaced } : {}), ...(result.replacedDeadSignIn ? { replacedDeadSignIn: result.replacedDeadSignIn } : {}) }, json,
|
|
209
220
|
result.agentId
|
|
210
221
|
? `OAuth login complete; refresh credential stored in ${result.storage}. Kept agent registration ${result.agentId}.`
|
|
211
222
|
: `OAuth login complete; refresh credential stored in ${result.storage}. Run \`nodii-dev agent register --name NAME\` next.`);
|
|
@@ -232,6 +243,18 @@ async function issues(action, args, f, json) {
|
|
|
232
243
|
if (action === "show")
|
|
233
244
|
return emit(await devRequest("GET", `/api/v1/issues/${encode(ref)}`), json);
|
|
234
245
|
const config = await readConfig();
|
|
246
|
+
// commentOnIssue. `issues update --comment` sent no field PATCH accepts (HTTP 400): a comment is its own route.
|
|
247
|
+
const commentBody = async (name) => {
|
|
248
|
+
const body = await bodyFlag(f, name);
|
|
249
|
+
if (typeof body !== "string" || !body.trim())
|
|
250
|
+
throw new CliError("USAGE", `issues ${action} ISSUE --${name} TEXT (or --${name}-file FILE)`, 2);
|
|
251
|
+
if (body.length > COMMENT_MAX) throw new CliError("USAGE", `The comment is ${body.length} characters; Dev keeps at most ${COMMENT_MAX}.`, 2);
|
|
252
|
+
return body;
|
|
253
|
+
};
|
|
254
|
+
const comment = (key, body) => devRequest("POST", `/api/v1/issues/${encode(key)}/comment`, { body, onBehalfOf: config.person });
|
|
255
|
+
if (action === "comment") return emit(await comment(ref, await commentBody("body")), json, `${ref}: comment added.`);
|
|
256
|
+
// Read and checked before anything is sent, so a bad comment changes nothing.
|
|
257
|
+
const note = ["create", "update"].includes(action) && (f.comment !== undefined || f["comment-file"] !== undefined) ? await commentBody("comment") : undefined;
|
|
235
258
|
if (action === "create" || action === "update") {
|
|
236
259
|
const bodyMd = await bodyFlag(f);
|
|
237
260
|
const payload = {
|
|
@@ -252,11 +275,23 @@ async function issues(action, args, f, json) {
|
|
|
252
275
|
].filter(([, v]) => v !== undefined),
|
|
253
276
|
),
|
|
254
277
|
};
|
|
278
|
+
// An update with nothing to PATCH but a comment is only the comment.
|
|
279
|
+
if (action === "update" && note !== undefined && Object.keys(payload).every((k) => k === "onBehalfOf"))
|
|
280
|
+
return emit(await comment(ref, note), json, `${ref}: comment added.`);
|
|
255
281
|
const result = await devRequest(
|
|
256
282
|
action === "create" ? "POST" : "PATCH",
|
|
257
283
|
action === "create" ? "/api/v1/issues" : `/api/v1/issues/${encode(ref)}`,
|
|
258
284
|
payload,
|
|
259
285
|
);
|
|
286
|
+
// After the write, so a refused create or update leaves no comment behind.
|
|
287
|
+
if (note !== undefined) {
|
|
288
|
+
const key = result.data?.key ?? ref;
|
|
289
|
+
try {
|
|
290
|
+
await comment(key, note);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new CliError("COMMENT_FAILED", `${key} was ${action}d, but the comment was not posted (${error?.code ?? "error"}: ${error?.message ?? error}). Post it with \`nodii-dev issues comment ${key} --body TEXT\`.`, 1, { issue: result.data ?? null });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
260
295
|
return emit(
|
|
261
296
|
result,
|
|
262
297
|
json,
|
|
@@ -291,6 +326,8 @@ async function issues(action, args, f, json) {
|
|
|
291
326
|
throw new CliError("USAGE", `Unknown issues action: ${action}`, 2);
|
|
292
327
|
}
|
|
293
328
|
|
|
329
|
+
// commentOnIssue's body limit.
|
|
330
|
+
const COMMENT_MAX = 10_000;
|
|
294
331
|
const ISSUE_STATUSES = ["todo", "in_progress", "in_review", "blocked", "done", "wont_do"];
|
|
295
332
|
// A blocked reason names who or what clears it: "cleared by", "unblocks", "waiting on", "until".
|
|
296
333
|
const CLEARED_BY = /\b(clear(s|ed)?|unblock(s|ed)?|wait(s|ing)? (on|for)|until)\b/i;
|
|
@@ -386,9 +423,19 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
386
423
|
}
|
|
387
424
|
if (command === "login") return login(f, json);
|
|
388
425
|
if (command === "logout") {
|
|
426
|
+
refuseFromLane("logout");
|
|
389
427
|
const c = await readConfig(false);
|
|
390
428
|
if (c) {
|
|
391
|
-
try {
|
|
429
|
+
try {
|
|
430
|
+
await revokeOAuth(c);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
// A keychain that cannot be read still holds the credential: keep the
|
|
433
|
+
// config, so logout can revoke it once the keychain is unlocked.
|
|
434
|
+
if (error?.code === "KEYCHAIN_UNAVAILABLE") throw error;
|
|
435
|
+
await deleteCredentials();
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
await deleteCredentials();
|
|
392
439
|
} else await deleteCredentials();
|
|
393
440
|
return emit({ loggedOut: true }, json, "Logged out.");
|
|
394
441
|
}
|
|
@@ -428,7 +475,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
428
475
|
if (command === "design" && action === "roles") return emit(await designRoles(rest), json, designRolesText);
|
|
429
476
|
if (command === "design" && action === "check") {
|
|
430
477
|
if ((!rest[0] && f["change-set"] === undefined) || rest.length > 1)
|
|
431
|
-
throw new CliError("USAGE", "design check FILE_KEY|LINK [--nodes A,B] [--roles R,S] [--library] | design check --change-set CS [--file SUBMIT.json] [--figma-version V]", 2);
|
|
478
|
+
throw new CliError("USAGE", "design check FILE_KEY|LINK [--nodes A,B] [--roles R,S] [--library] | design check --change-set CS [--file SUBMIT.json] [--figma-version V] [--sync]", 2);
|
|
432
479
|
const result = await designCheck(rest[0], f);
|
|
433
480
|
emit(result.payload, json, result.human ?? designCheckText);
|
|
434
481
|
if (result.exitCode) {
|
|
@@ -480,13 +527,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
480
527
|
if (command === "agent") {
|
|
481
528
|
if (["lane-limit", "grant", "revoke"].includes(action))
|
|
482
529
|
return emitCommand(await agentWorkflowCommand(action, rest, f));
|
|
483
|
-
|
|
530
|
+
// A fresh, empty config path takes the machine's sign-in (Dev issue 110).
|
|
531
|
+
const name = action === "register" ? requireFlag(f, "name") : undefined;
|
|
532
|
+
const c = action === "register" ? (await readConfig(false)) ?? (await adoptMachineSignIn()) : await readConfig();
|
|
484
533
|
if (action === "register") {
|
|
485
534
|
// POST /api/v1/agents is the registration route the edge exposes; Dev
|
|
486
535
|
// binds the agent to the OAuth person, app and installation itself.
|
|
487
536
|
const result = await devRequest("POST", "/api/v1/agents", {
|
|
488
537
|
sessionId: f.session ?? crypto.randomUUID(),
|
|
489
|
-
name
|
|
538
|
+
name,
|
|
490
539
|
});
|
|
491
540
|
await writeConfig({ ...c, agentId: result.data.id });
|
|
492
541
|
// Dev ignores a self-claimed service: staff bind agents to services
|
|
@@ -646,7 +695,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
646
695
|
if (action === "submit") {
|
|
647
696
|
const payload = f.file ? JSON.parse(await readFile(f.file, "utf8")) : { figmaVersion: requireFlag(f, "figma-version") };
|
|
648
697
|
// A 200 carries top-level warnings (STALE_FIGMA_VERSION): print them.
|
|
649
|
-
return emit(await figmaSubmit(rest[0], payload), json, (p) =>
|
|
698
|
+
return emit(await figmaSubmit(rest[0], payload, { sync: submitMode(f) }), json, (p) =>
|
|
650
699
|
[JSON.stringify(p.data, null, 2), ...conformanceLines({ warnings: p.warnings })].join("\n"));
|
|
651
700
|
}
|
|
652
701
|
if (action === "file") {
|
|
@@ -686,7 +735,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
686
735
|
if (action === "withdraw")
|
|
687
736
|
return emit(await figmaWithdraw(rest[0]), json, (p) => `Withdrew ${rest[0]}; it is back in ${p.data?.state ?? "editing"}.`);
|
|
688
737
|
if (action === "resubmit")
|
|
689
|
-
return emit(await figmaResubmit(rest[0], rest[1]), json, (p) =>
|
|
738
|
+
return emit(await figmaResubmit(rest[0], rest[1], { sync: submitMode(f) }), json, (p) =>
|
|
690
739
|
[`Re-submitted ${rest[0]}; it is ${p.data.state}.`, figmaChangeSetText(p.data), ...conformanceLines({ warnings: p.warnings })].join("\n"));
|
|
691
740
|
if (action === "changeset") {
|
|
692
741
|
if (rest[0] !== "show" || !rest[1] || rest.length > 2) throw new CliError("USAGE", "figma changeset show CHANGE_SET", 2);
|
package/dist/core.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
|
-
export const VERSION = "0.6.
|
|
7
|
+
export const VERSION = "0.6.21";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|
|
@@ -33,7 +33,13 @@ export const isOAuthConfig = (config) =>
|
|
|
33
33
|
export async function readConfig(required = true) {
|
|
34
34
|
let config;
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
const text = await readFile(configPath(), "utf8");
|
|
37
|
+
// An empty file is a config path made ready for a new agent, not a login.
|
|
38
|
+
if (!text.trim()) {
|
|
39
|
+
if (!required) return null;
|
|
40
|
+
throw new Error("empty");
|
|
41
|
+
}
|
|
42
|
+
config = JSON.parse(text);
|
|
37
43
|
} catch (error) {
|
|
38
44
|
if (!required && error?.code === "ENOENT") return null;
|
|
39
45
|
throw new CliError("NOT_LOGGED_IN", "Run `nodii-dev login` first.", 2);
|
|
@@ -113,7 +119,7 @@ const staleCode = (raw) => {
|
|
|
113
119
|
}
|
|
114
120
|
};
|
|
115
121
|
|
|
116
|
-
async function devFetch(method, path, bodyText, contentType, accept) {
|
|
122
|
+
async function devFetch(method, path, bodyText, contentType, accept, extra = {}) {
|
|
117
123
|
const config = await readConfig();
|
|
118
124
|
const auth = await authProvider();
|
|
119
125
|
const url = requestUrl(config, path);
|
|
@@ -124,7 +130,7 @@ async function devFetch(method, path, bodyText, contentType, accept) {
|
|
|
124
130
|
let refreshed = false;
|
|
125
131
|
let adopted = false;
|
|
126
132
|
for (;;) {
|
|
127
|
-
const headers = { accept };
|
|
133
|
+
const headers = { accept, ...extra };
|
|
128
134
|
if (bodyText !== undefined) headers["content-type"] = contentType;
|
|
129
135
|
Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
|
|
130
136
|
let response;
|
|
@@ -143,8 +149,9 @@ async function devFetch(method, path, bodyText, contentType, accept) {
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
/** A JSON call that keeps the status: `{ status, payload }`. Errors throw as devRawRequest's do. */
|
|
153
|
+
export async function devExchange(method, path, bodyText, { contentType = "application/json", headers = {} } = {}) {
|
|
154
|
+
const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json", headers);
|
|
148
155
|
let payload;
|
|
149
156
|
try {
|
|
150
157
|
payload = raw ? JSON.parse(raw) : {};
|
|
@@ -158,7 +165,11 @@ export async function devRawRequest(method, path, bodyText, contentType = "appli
|
|
|
158
165
|
1,
|
|
159
166
|
payload.details,
|
|
160
167
|
);
|
|
161
|
-
return payload;
|
|
168
|
+
return { status: response.status, payload };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
|
|
172
|
+
return (await devExchange(method, path, bodyText, { contentType })).payload;
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
/** A Dev route that answers with text (e.g. a document body as text/markdown). Errors are Dev's JSON. */
|
|
@@ -173,6 +184,10 @@ export async function devTextRequest(method, path) {
|
|
|
173
184
|
export const devRequest = (method, path, body) =>
|
|
174
185
|
devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body));
|
|
175
186
|
|
|
187
|
+
/** A JSON call sent with `Prefer: respond-async` (RFC 7240): `{ status, payload }`, 202 when Dev took it as a job. */
|
|
188
|
+
export const devAsyncRequest = async (method, path, body) =>
|
|
189
|
+
devExchange(method, path, body === undefined ? undefined : JSON.stringify(body), { headers: { prefer: "respond-async" } });
|
|
190
|
+
|
|
176
191
|
/** Proves the stored OAuth login works: a token refresh, then the agent read once registered. */
|
|
177
192
|
export async function checkLogin() {
|
|
178
193
|
const config = await readConfig();
|
|
@@ -184,7 +199,7 @@ export async function checkLogin() {
|
|
|
184
199
|
export const encode = (value) => encodeURIComponent(value);
|
|
185
200
|
// Switches never take the next token as their value, so
|
|
186
201
|
// `docs upload --wait rulebook/` keeps rulebook/ as a path.
|
|
187
|
-
const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library"]);
|
|
202
|
+
const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library", "async", "sync", "force"]);
|
|
188
203
|
export function parseArgs(argv) {
|
|
189
204
|
const flags = {};
|
|
190
205
|
const args = [];
|
package/dist/design.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// library main.
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
10
10
|
import { CliError, devRequest } from "./core.js";
|
|
11
|
-
import { conformanceLines, figmaConformance, figmaFileKey } from "./figma.js";
|
|
11
|
+
import { conformanceLines, figmaConformance, figmaFileKey, submitMode } from "./figma.js";
|
|
12
12
|
import { checkConformance, neededMains, VERSION as CHECK_VERSION } from "./vendor/ds-conformance.mjs";
|
|
13
13
|
|
|
14
14
|
const FIGMA_API = "https://api.figma.com/v1";
|
|
@@ -159,7 +159,7 @@ export async function lastPublish(figmaGet, fileKey) {
|
|
|
159
159
|
/**
|
|
160
160
|
* The main of each instance, keyed by the instance's componentId (this file's
|
|
161
161
|
* id for it), resolved as Dev's gate resolves it (Dev issue 92, conformance
|
|
162
|
-
* 1.0.
|
|
162
|
+
* 1.0.4), so both give one verdict:
|
|
163
163
|
* - a local component (remote: false) is read from this file;
|
|
164
164
|
* - a remote one is found in the DS file's published listing, else by Figma's
|
|
165
165
|
* lookup of its key, and is read only from a library Dev has registered
|
|
@@ -273,8 +273,10 @@ const nodeId = (x) => (/^\d+-\d+$/.test(x) ? x.replace("-", ":") : x);
|
|
|
273
273
|
* finding fails; stale overrides (a value equal to the main's) are listed but
|
|
274
274
|
* never fail (DEC-0035).
|
|
275
275
|
*/
|
|
276
|
-
export async function designCheck(target, flags = {}, { request = devRequest, env = process.env, figmaGet, read } = {}) {
|
|
277
|
-
|
|
276
|
+
export async function designCheck(target, flags = {}, { request = devRequest, send, progress, env = process.env, figmaGet, read } = {}) {
|
|
277
|
+
submitMode(flags);
|
|
278
|
+
if (flags["change-set"] !== undefined)
|
|
279
|
+
return designCheckChangeSet(target, flags, { request, ...(send ? { send } : {}), ...(progress ? { progress } : {}), ...(read ? { read } : {}) });
|
|
278
280
|
// A --nodes that names nothing must not widen to every page.
|
|
279
281
|
if (flags.nodes !== undefined && ([flags.nodes].flat().includes(true) || !list(flags.nodes).length))
|
|
280
282
|
throw new CliError("USAGE", "--nodes needs node ids, e.g. --nodes 12:34,12:56", 2);
|
|
@@ -345,6 +347,8 @@ export async function designCheck(target, flags = {}, { request = devRequest, en
|
|
|
345
347
|
// Same value through a different variable: the file is behind a library
|
|
346
348
|
// republish. Accept the update in Figma (MW6-17); never a failure.
|
|
347
349
|
libraryUpdatePending: result.libraryUpdatePending ?? [],
|
|
350
|
+
// What the app-UI gate leaves out (email), with its node count (Dev issue 116).
|
|
351
|
+
outOfScope: result.outOfScope ?? [], outOfScopeNodes: result.outOfScopeNodes ?? 0,
|
|
348
352
|
warnings,
|
|
349
353
|
};
|
|
350
354
|
return { payload: { data }, exitCode: data.passed ? 0 : 1 };
|
|
@@ -367,8 +371,9 @@ export function designCheckText({ data }) {
|
|
|
367
371
|
* plus the submit-time guards against the set's items: IMAGE_NOT_TRUECOLOR
|
|
368
372
|
* refuses; STALE_FIGMA_VERSION is a warning. Changes no state.
|
|
369
373
|
*/
|
|
370
|
-
export async function designCheckChangeSet(target, flags, { request = devRequest, read = (f) => readFile(f, "utf8") } = {}) {
|
|
374
|
+
export async function designCheckChangeSet(target, flags, { request = devRequest, send, progress, read = (f) => readFile(f, "utf8") } = {}) {
|
|
371
375
|
const id = flags["change-set"];
|
|
376
|
+
const sync = submitMode(flags);
|
|
372
377
|
if (id === true || !id) throw new CliError("USAGE", "--change-set needs a change set id, e.g. --change-set CS-123", 2);
|
|
373
378
|
if (target !== undefined) throw new CliError("USAGE", "design check --change-set CS takes no file: Dev checks the set's own items", 2);
|
|
374
379
|
if (flags["figma-version"] === true) throw new CliError("USAGE", "--figma-version needs a version id", 2);
|
|
@@ -387,17 +392,18 @@ export async function designCheckChangeSet(target, flags, { request = devRequest
|
|
|
387
392
|
if (flags["figma-version"]) body = { ...body, figmaVersion: String(flags["figma-version"]) };
|
|
388
393
|
let p;
|
|
389
394
|
try {
|
|
390
|
-
p = await figmaConformance(id, body, { request });
|
|
395
|
+
p = await figmaConformance(id, body, { request, sync, ...(send ? { send } : {}), ...(progress ? { progress } : {}) });
|
|
391
396
|
} catch (error) {
|
|
392
397
|
// A cold run reads every root and main from Figma and can outlast the
|
|
393
398
|
// edge's gateway (504, an HTML page). Dev keeps the mains it read for 5
|
|
394
399
|
// minutes, so the same run again usually answers.
|
|
395
|
-
if (error?.code !== "HTTP_504") throw error;
|
|
396
|
-
throw new CliError("DEV_TIMEOUT", `Dev's dry run of ${id} ran past the edge's gateway timeout while reading Figma; run it again (Dev keeps the library mains it read for 5 minutes)`, 1);
|
|
400
|
+
if (error?.code !== "HTTP_504" && error?.code !== "HTTP_502") throw error;
|
|
401
|
+
throw new CliError("DEV_TIMEOUT", `Dev's dry run of ${id} ran past the edge's gateway timeout while reading Figma; run it again${sync ? " without --sync, so Dev runs it as a job" : ""} (Dev keeps the library mains it read for 5 minutes)`, 1);
|
|
397
402
|
}
|
|
398
403
|
const r = p.data ?? {};
|
|
399
404
|
const data = { changeSetId: id, checkVersion: r.version, passed: Boolean(r.passed), checked: r.checked, findings: r.findings ?? [],
|
|
400
|
-
staleOverrides: r.staleOverrides ?? [], libraryUpdatePending: r.libraryUpdatePending ?? [],
|
|
405
|
+
staleOverrides: r.staleOverrides ?? [], libraryUpdatePending: r.libraryUpdatePending ?? [],
|
|
406
|
+
outOfScope: r.outOfScope ?? [], outOfScopeNodes: r.outOfScopeNodes ?? 0, warnings: r.warnings ?? [], staleVersion: r.staleVersion ?? null };
|
|
401
407
|
return { payload: { data }, exitCode: data.passed ? 0 : 1, human: designCheckChangeSetText };
|
|
402
408
|
}
|
|
403
409
|
|
package/dist/doctor.js
CHANGED
|
@@ -48,7 +48,7 @@ export function grantReason(grant = {}) {
|
|
|
48
48
|
return "unspecified";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
async function readConfigFile(path) {
|
|
51
|
+
export async function readConfigFile(path) {
|
|
52
52
|
let raw;
|
|
53
53
|
try {
|
|
54
54
|
raw = await readFile(path, "utf8");
|
|
@@ -56,6 +56,8 @@ async function readConfigFile(path) {
|
|
|
56
56
|
if (error?.code === "ENOENT") return null;
|
|
57
57
|
return { unreadable: error?.code ?? String(error) };
|
|
58
58
|
}
|
|
59
|
+
// An empty file is a path made ready for a new agent (`agent register` adopts the machine's sign-in).
|
|
60
|
+
if (!raw.trim()) return null;
|
|
59
61
|
try { return JSON.parse(raw); } catch { return {}; }
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -106,8 +108,9 @@ export async function doctorCommand(deps = {}) {
|
|
|
106
108
|
try {
|
|
107
109
|
await (deps.loadRefreshToken ?? loadRefreshToken)(config.clientId);
|
|
108
110
|
stored = true;
|
|
109
|
-
} catch {
|
|
110
|
-
checks.push(missing("signed_in", "
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error?.code === "KEYCHAIN_UNAVAILABLE") checks.push(missing("signed_in", "the keychain could not be read (locked or access denied)", "unlock the login keychain, then rerun"));
|
|
113
|
+
else checks.push(missing("signed_in", "no stored sign-in credential for this app in the keychain", loginFix));
|
|
111
114
|
}
|
|
112
115
|
if (stored) {
|
|
113
116
|
try {
|