@nodii/dev 0.6.10 → 0.6.12
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 +54 -4
- package/bin/nodii-dev +11 -1
- package/dist/cli.js +53 -33
- package/dist/core.js +1 -1
- package/dist/devops-openapi.json +1 -1
- package/dist/doctor.js +14 -1
- package/dist/figma.js +175 -1
- package/dist/hooks.js +26 -15
- package/dist/oauth.js +163 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,14 @@ every nodii-dev process on the machine shares it instead of refreshing on each
|
|
|
29
29
|
run; refreshes take turns under a per-user lock. A refresh clears the saved
|
|
30
30
|
sign-in only when nodii-auth answers `invalid_grant` (expired, revoked or
|
|
31
31
|
replayed). A 429, 5xx, timeout, network error or proxy error page keeps it and
|
|
32
|
-
fails with `OAUTH_REFRESH_UNAVAILABLE` (exit 1)
|
|
32
|
+
fails with `OAUTH_REFRESH_UNAVAILABLE` (exit 1). Only answers that show the
|
|
33
|
+
token was not used are retried at once (a connection that never opened, 429,
|
|
34
|
+
500, 503, a proxy's non-JSON 4xx). After a timeout, a dropped connection, a
|
|
35
|
+
gateway timeout or a 2xx without tokens, nodii-auth may already have rotated
|
|
36
|
+
the token; it is not sent again by that command, though the next one will
|
|
37
|
+
present it, and a rotation whose answer was lost does end the sign-in.
|
|
38
|
+
SIGINT, SIGTERM and SIGHUP wait while a token is in flight and until its
|
|
39
|
+
successor is stored. Every API
|
|
33
40
|
call goes to the edge's `/devops` prefix with the OAuth bearer and the
|
|
34
41
|
registered `x-agent-id`; signing in again as the same person keeps that
|
|
35
42
|
registration. `logout` revokes the refresh chain before erasing local
|
|
@@ -138,12 +145,14 @@ prints them without creating anything.
|
|
|
138
145
|
|
|
139
146
|
```bash
|
|
140
147
|
nodii-dev figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
|
|
141
|
-
nodii-dev figma lock FILE_KEY --title TEXT
|
|
148
|
+
nodii-dev figma lock FILE_KEY --title TEXT [--scope KIND:ID ...]
|
|
149
|
+
nodii-dev figma scope add CHANGE_SET --scope KIND:ID [--scope ...]
|
|
150
|
+
nodii-dev figma publish-plan FILE_KEY
|
|
142
151
|
nodii-dev figma check FILE_KEY
|
|
143
152
|
nodii-dev figma log-write CHANGE_SET --tool TOOL --nodes NODE[,NODE]
|
|
144
153
|
nodii-dev figma submit CHANGE_SET --file SUBMISSION.json
|
|
145
154
|
nodii-dev figma apply CHANGE_SET --figma-version VERSION
|
|
146
|
-
nodii-dev figma published CHANGE_SET --figma-version VERSION
|
|
155
|
+
nodii-dev figma published FILE_KEY|CHANGE_SET --figma-version VERSION
|
|
147
156
|
nodii-dev figma abandon CHANGE_SET
|
|
148
157
|
nodii-dev figma withdraw CHANGE_SET # take back a submit before anything is decided
|
|
149
158
|
nodii-dev figma resubmit CHANGE_SET PAYLOAD.json # round 2: re-submit every rejected item
|
|
@@ -163,6 +172,45 @@ that account). Registering a key Dev already has returns the stored row
|
|
|
163
172
|
unchanged. A figma.com file link works in place of the key. `figma lock` on
|
|
164
173
|
an unregistered file prints the `figma file add` command to run.
|
|
165
174
|
|
|
175
|
+
**Scoped locks (DEC-0032).** A change set locks scopes inside a file, not
|
|
176
|
+
the whole file, so lanes can change one file in parallel. Give `figma lock`
|
|
177
|
+
one `--scope` per scope:
|
|
178
|
+
|
|
179
|
+
| `--scope` | Claims |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `component:NODE_ID` | a component set or standalone component, at its top-level node (a variant id is taken as its set) |
|
|
182
|
+
| `page:NODE_ID` | a whole Figma page: new screens, specimens, docs pages |
|
|
183
|
+
| `foundations` | the file's variables, text styles and effect styles; one holder at a time |
|
|
184
|
+
| `create:PAGE_ID:NAME` | a new component, made on that page |
|
|
185
|
+
|
|
186
|
+
A node id from a figma.com link (`12-34`) is read as Figma's `12:34`. Two
|
|
187
|
+
sets conflict only when their scopes overlap: the same node, a page that holds
|
|
188
|
+
a claimed component (or the reverse), or foundations already held. Dev
|
|
189
|
+
refuses an overlap with `SCOPE_CONFLICT`; the CLI prints Dev's message, then
|
|
190
|
+
the set holding the overlapping scope. `figma lock` without `--scope` locks the
|
|
191
|
+
whole file, as older CLIs do, and that conflicts with every other set.
|
|
192
|
+
`figma scope add` widens a set you hold, with the same check; nothing is ever
|
|
193
|
+
widened silently.
|
|
194
|
+
|
|
195
|
+
Every write must fall inside the set's scopes (MW1-57.7). The post-hook
|
|
196
|
+
logs each write's node ids as before. When Dev answers `WRITE_OUT_OF_SCOPE`,
|
|
197
|
+
the hook exits `2`, so Claude Code shows the agent why: a write that
|
|
198
|
+
returned no node ids (return them from the script, then run `figma log-write`),
|
|
199
|
+
or ids outside the scopes. Dev refuses `figma submit` and `figma resubmit`
|
|
200
|
+
with the same code until it's fixed. You can widen the scope with
|
|
201
|
+
`figma scope add` if it's free (styles and variables need
|
|
202
|
+
`--scope foundations`), or revert the node exactly to how it was at the set's
|
|
203
|
+
base version.
|
|
204
|
+
|
|
205
|
+
Scopes release when a set reaches `awaiting_publish`. One library publish
|
|
206
|
+
then covers every set awaiting publish. Before you publish in Figma,
|
|
207
|
+
`figma publish-plan FILE_KEY` lists the components changed since the last
|
|
208
|
+
publish in two groups. **publish** belongs to sets awaiting publish. **leave
|
|
209
|
+
unchecked** belongs to sets still editing or in review: untick those in
|
|
210
|
+
Figma's publish dialog. Then `figma published FILE_KEY --figma-version VERSION`
|
|
211
|
+
marks every awaiting set published, and Dev flags any published component
|
|
212
|
+
from an unfinished set. `figma published CS-123 ...` keeps the per-set route.
|
|
213
|
+
|
|
166
214
|
`figma withdraw` moves a change set you hold from `in_review` back to
|
|
167
215
|
`editing`, as long as no item has been decided (Dev's
|
|
168
216
|
`withdrawDesignChangeSet`). Dev refuses when the change set doesn't exist
|
|
@@ -435,6 +483,7 @@ exits 0 only when every check is `OK`. `--json` returns the same checks as
|
|
|
435
483
|
| `repo` | Whether the repository in this directory is the agent's home repository or granted to it. Repositories are compared by short name (`nodii-clis`, not `owner/nodii-clis`), as Dev compares them. Dev does not let an OAuth agent read its own grants yet (requested in Dev), so a repository other than the home one is `CANNOT CHECK`. |
|
|
436
484
|
| `lane_limit` | The agent's active lanes against its lane limit. A full limit is `OK` when a lane on this branch holds the slot. |
|
|
437
485
|
| `figma_file` | Only with `--figma-file KEY` (a key or figma.com link), or `NODII_DEV_FIGMA_FILE`: whether Dev has the brief's Figma file registered. `MISSING` names the `figma file add` command, since `figma lock` would fail with `DESIGN_FILE_NOT_FOUND`. |
|
|
486
|
+
| `figma_scopes` | With `figma_file`, when the file is registered: the change set this agent holds in the file and its lock scopes (or the whole file), or that it holds none (DEC-0032). |
|
|
438
487
|
|
|
439
488
|
Doctor may refresh the access token, as any command does. The permission table
|
|
440
489
|
is kept by hand from Dev's route policy until Dev's OpenAPI names each
|
|
@@ -535,5 +584,6 @@ and `figma log-write` follows the same rule (`CHANGE_SET_NOT_EDITABLE`). The pre
|
|
|
535
584
|
non-Figma tools and allowlisted Figma reads, but defaults to denying every
|
|
536
585
|
Figma write if the file key is absent, Dev cannot be reached, or the lock
|
|
537
586
|
is denied. It exits `2`, which makes Claude Code block the call. The post-hook
|
|
538
|
-
best-effort logs the write and never blocks
|
|
587
|
+
best-effort logs the write and never blocks; it exits `2` only for
|
|
588
|
+
`WRITE_OUT_OF_SCOPE`, after the write is logged, so the agent sees it. Per D1433, Codex remains read-only
|
|
539
589
|
for Figma; this package does not write to Figma itself.
|
package/bin/nodii-dev
CHANGED
|
@@ -7,6 +7,16 @@ while [ -L "$self" ]; do
|
|
|
7
7
|
link=$(readlink "$self")
|
|
8
8
|
case "$link" in /*) self="$link" ;; *) self="$(dirname "$self")/$link" ;; esac
|
|
9
9
|
done
|
|
10
|
-
|
|
10
|
+
root="$(cd "$(dirname "$self")/.." && pwd)"
|
|
11
|
+
entry="$root/dist/nodii-dev.js"
|
|
12
|
+
# A checkout's own build can be older than its package.json (Dev issue 88: a
|
|
13
|
+
# stale 0.3.1 build once answered `npx @nodii/dev@0.6.10` inside a nodii-clis
|
|
14
|
+
# worktree). Refuse to run one.
|
|
15
|
+
built=$(sed -n 's/^export const VERSION = "\(.*\)";$/\1/p' "$root/dist/core.js" 2>/dev/null)
|
|
16
|
+
wanted=$(sed -n 's/^ "version": "\(.*\)",$/\1/p' "$root/package.json" 2>/dev/null)
|
|
17
|
+
if [ -n "$built" ] && [ -n "$wanted" ] && [ "$built" != "$wanted" ]; then
|
|
18
|
+
echo "nodii-dev: the build at $root is $built but its package.json says $wanted. Run 'npm run build' there, or run npx outside the nodii-clis repo." >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
11
21
|
if command -v bun >/dev/null 2>&1; then exec bun "$entry" "$@"; fi
|
|
12
22
|
exec node "$entry" "$@"
|
package/dist/cli.js
CHANGED
|
@@ -22,11 +22,29 @@ import {
|
|
|
22
22
|
deviceLogin,
|
|
23
23
|
revokeOAuth,
|
|
24
24
|
} from "./oauth.js";
|
|
25
|
-
import { heartbeatCurrentLane, postHook, preHook, settingsSnippet, workflowSuggestion } from "./hooks.js";
|
|
25
|
+
import { heartbeatCurrentLane, postHook, postHookFailure, preHook, settingsSnippet, workflowSuggestion } from "./hooks.js";
|
|
26
26
|
import { setupCommand } from "./setup.js";
|
|
27
27
|
import { harnessCommand } from "./harness.js";
|
|
28
28
|
import { doctorCommand } from "./doctor.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
figmaChangeSetShow,
|
|
31
|
+
figmaChangeSetText,
|
|
32
|
+
figmaFileKey,
|
|
33
|
+
figmaFilePublished,
|
|
34
|
+
figmaLock,
|
|
35
|
+
figmaLockCheck,
|
|
36
|
+
figmaLockText,
|
|
37
|
+
figmaLogWrite,
|
|
38
|
+
figmaPublishPlan,
|
|
39
|
+
figmaPublishPlanText,
|
|
40
|
+
figmaResubmit,
|
|
41
|
+
figmaScopeAdd,
|
|
42
|
+
figmaSubmit,
|
|
43
|
+
isChangeSetId,
|
|
44
|
+
scopeText,
|
|
45
|
+
scopesFrom,
|
|
46
|
+
} from "./figma.js";
|
|
47
|
+
export { figmaLock } from "./figma.js";
|
|
30
48
|
import { docsCommand, rulebookCommand, rulesCommand } from "./upload.js";
|
|
31
49
|
import { changeSetCommand, guidelineCommand } from "./guidelines.js";
|
|
32
50
|
import {
|
|
@@ -69,7 +87,13 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
69
87
|
design guideline list [--drafted] [--all] | show ENTRY
|
|
70
88
|
design guideline propose ENTRY --file FILE.md [--summary TEXT] [--dry-run]
|
|
71
89
|
design changeset list [--entry KEY] [--state STATE] | show ID
|
|
72
|
-
figma lock
|
|
90
|
+
figma lock FILE_KEY --title TEXT [--scope KIND:ID ...]
|
|
91
|
+
# scopes: component:NODE page:NODE foundations create:PAGE:NAME;
|
|
92
|
+
# no --scope locks the whole file (DEC-0032)
|
|
93
|
+
figma scope add CHANGE_SET --scope KIND:ID [--scope ...] # widen a held set's scopes
|
|
94
|
+
figma publish-plan FILE_KEY # what to publish, and what to leave unchecked in Figma's dialog
|
|
95
|
+
figma published FILE_KEY|CHANGE_SET --figma-version V # a file key marks every awaiting set published
|
|
96
|
+
figma check|log-write|submit|apply|abandon|release
|
|
73
97
|
figma withdraw CHANGE_SET # holder takes back a submit: in_review -> editing (or a re-submit -> applying)
|
|
74
98
|
figma resubmit CHANGE_SET PAYLOAD.json
|
|
75
99
|
# holder re-submits every rejected item: applying -> in_review, round + 1
|
|
@@ -321,16 +345,6 @@ export async function figmaFileAdd(value, flags, { request = devRequest } = {})
|
|
|
321
345
|
}
|
|
322
346
|
}
|
|
323
347
|
|
|
324
|
-
export async function figmaLock(value, title, { request = devRequest } = {}) {
|
|
325
|
-
const key = figmaFileKey(value);
|
|
326
|
-
try {
|
|
327
|
-
return await request("POST", `/api/v1/design/files/${encode(key)}/lock`, { changeSetTitle: title });
|
|
328
|
-
} catch (error) {
|
|
329
|
-
if (error?.code !== "DESIGN_FILE_NOT_FOUND") throw error;
|
|
330
|
-
throw new CliError(error.code, notRegisteredHint(key), error.exitCode ?? 1, error.details);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
348
|
export async function main(argv = process.argv.slice(2)) {
|
|
335
349
|
if (argv.length === 1 && ["--version", "-v"].includes(argv[0]))
|
|
336
350
|
return console.log(VERSION);
|
|
@@ -589,7 +603,26 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
589
603
|
}
|
|
590
604
|
if (command === "figma") {
|
|
591
605
|
if (action === "lock")
|
|
592
|
-
return emit(await figmaLock(rest[0], requireFlag(f, "title")), json);
|
|
606
|
+
return emit(await figmaLock(rest[0], requireFlag(f, "title"), { scopes: scopesFrom(f) }), json, (p) => figmaLockText(p.data));
|
|
607
|
+
if (action === "scope") {
|
|
608
|
+
if (rest[0] !== "add" || !rest[1] || rest.length > 2) throw new CliError("USAGE", "figma scope add CHANGE_SET --scope KIND:ID [--scope ...]", 2);
|
|
609
|
+
return emit(await figmaScopeAdd(rest[1], scopesFrom(f)), json, (p) =>
|
|
610
|
+
`${rest[1]} holds ${p.data?.scopes?.length ?? "its"} scope(s)${p.data?.scopes?.length ? `: ${p.data.scopes.map(scopeText).join(", ")}` : ""}`);
|
|
611
|
+
}
|
|
612
|
+
if (action === "publish-plan") {
|
|
613
|
+
if (!rest[0] || rest.length > 1) throw new CliError("USAGE", "figma publish-plan FILE_KEY", 2);
|
|
614
|
+
return emit(await figmaPublishPlan(rest[0]), json, (p) => figmaPublishPlanText(p.data));
|
|
615
|
+
}
|
|
616
|
+
// One publish covers every set awaiting publish on the file (MW1-57.8); a
|
|
617
|
+
// change set id keeps the per-set route.
|
|
618
|
+
if (action === "published" && rest[0] && !isChangeSetId(rest[0]))
|
|
619
|
+
return emit(await figmaFilePublished(rest[0], f.file ? JSON.parse(await readFile(f.file, "utf8")) : { figmaVersion: requireFlag(f, "figma-version") }), json, (p) =>
|
|
620
|
+
`Published ${p.data.published?.length ?? 0} change set(s): ${(p.data.published ?? []).join(", ") || "none awaiting publish"}` +
|
|
621
|
+
(p.data.flagged?.length ? `\nFLAGGED: published components from unfinished sets: ${JSON.stringify(p.data.flagged)}` : ""));
|
|
622
|
+
if (action === "submit") {
|
|
623
|
+
const payload = f.file ? JSON.parse(await readFile(f.file, "utf8")) : { figmaVersion: requireFlag(f, "figma-version") };
|
|
624
|
+
return emit(await figmaSubmit(rest[0], payload), json);
|
|
625
|
+
}
|
|
593
626
|
if (action === "file") {
|
|
594
627
|
if (rest[0] !== "add") throw new CliError("USAGE", "figma file add FILE_KEY --name TEXT --kind KIND [--module M]", 2);
|
|
595
628
|
return emit(await figmaFileAdd(rest[1], f), json, (p) =>
|
|
@@ -597,10 +630,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
597
630
|
}
|
|
598
631
|
if (action === "check") {
|
|
599
632
|
const c = await readConfig();
|
|
600
|
-
const result = await
|
|
601
|
-
"GET",
|
|
602
|
-
`/api/v1/design/files/${encode(rest[0])}/lock/check?agentId=${encode(c.agentId)}`,
|
|
603
|
-
);
|
|
633
|
+
const result = await figmaLockCheck(figmaFileKey(rest[0]), c.agentId);
|
|
604
634
|
emit(
|
|
605
635
|
result,
|
|
606
636
|
json,
|
|
@@ -611,18 +641,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
611
641
|
return;
|
|
612
642
|
}
|
|
613
643
|
if (action === "log-write")
|
|
614
|
-
return emit(
|
|
615
|
-
|
|
616
|
-
"POST",
|
|
617
|
-
`/api/v1/design/change-sets/${encode(rest[0])}/writes`,
|
|
618
|
-
{
|
|
619
|
-
tool: requireFlag(f, "tool"),
|
|
620
|
-
nodeIds: requireFlag(f, "nodes").split(",").filter(Boolean),
|
|
621
|
-
},
|
|
622
|
-
),
|
|
623
|
-
json,
|
|
624
|
-
);
|
|
625
|
-
if (["submit", "apply", "published", "abandon"].includes(action)) {
|
|
644
|
+
return emit(await figmaLogWrite(rest[0], requireFlag(f, "tool"), requireFlag(f, "nodes").split(",").filter(Boolean)), json);
|
|
645
|
+
if (["apply", "published", "abandon"].includes(action)) {
|
|
626
646
|
const payload = f.file
|
|
627
647
|
? JSON.parse(await readFile(f.file, "utf8"))
|
|
628
648
|
: action === "abandon"
|
|
@@ -690,9 +710,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
690
710
|
try {
|
|
691
711
|
return emit(await postHook(payload), json, "logged");
|
|
692
712
|
} catch (error) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
);
|
|
713
|
+
const outcome = postHookFailure(error);
|
|
714
|
+
console.error(outcome.message);
|
|
715
|
+
if (outcome.exitCode) process.exitCode = outcome.exitCode;
|
|
696
716
|
return;
|
|
697
717
|
}
|
|
698
718
|
}
|
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.12";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|