@nodii/dev 0.6.11 → 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 CHANGED
@@ -145,12 +145,14 @@ prints them without creating anything.
145
145
 
146
146
  ```bash
147
147
  nodii-dev figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
148
- 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
149
151
  nodii-dev figma check FILE_KEY
150
152
  nodii-dev figma log-write CHANGE_SET --tool TOOL --nodes NODE[,NODE]
151
153
  nodii-dev figma submit CHANGE_SET --file SUBMISSION.json
152
154
  nodii-dev figma apply CHANGE_SET --figma-version VERSION
153
- nodii-dev figma published CHANGE_SET --figma-version VERSION
155
+ nodii-dev figma published FILE_KEY|CHANGE_SET --figma-version VERSION
154
156
  nodii-dev figma abandon CHANGE_SET
155
157
  nodii-dev figma withdraw CHANGE_SET # take back a submit before anything is decided
156
158
  nodii-dev figma resubmit CHANGE_SET PAYLOAD.json # round 2: re-submit every rejected item
@@ -170,6 +172,45 @@ that account). Registering a key Dev already has returns the stored row
170
172
  unchanged. A figma.com file link works in place of the key. `figma lock` on
171
173
  an unregistered file prints the `figma file add` command to run.
172
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
+
173
214
  `figma withdraw` moves a change set you hold from `in_review` back to
174
215
  `editing`, as long as no item has been decided (Dev's
175
216
  `withdrawDesignChangeSet`). Dev refuses when the change set doesn't exist
@@ -442,6 +483,7 @@ exits 0 only when every check is `OK`. `--json` returns the same checks as
442
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`. |
443
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. |
444
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). |
445
487
 
446
488
  Doctor may refresh the access token, as any command does. The permission table
447
489
  is kept by hand from Dev's route policy until Dev's OpenAPI names each
@@ -542,5 +584,6 @@ and `figma log-write` follows the same rule (`CHANGE_SET_NOT_EDITABLE`). The pre
542
584
  non-Figma tools and allowlisted Figma reads, but defaults to denying every
543
585
  Figma write if the file key is absent, Dev cannot be reached, or the lock
544
586
  is denied. It exits `2`, which makes Claude Code block the call. The post-hook
545
- best-effort logs the write and never blocks. Per D1433, Codex remains read-only
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
546
589
  for Figma; this package does not write to Figma itself.
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 { figmaChangeSetShow, figmaChangeSetText, figmaFileKey, figmaResubmit, notRegisteredHint } from "./figma.js";
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|check|log-write|submit|apply|published|abandon|release
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 devRequest(
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
- await devRequest(
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
- console.error(
694
- `nodii-dev post hook: ${error.code ?? "ERROR"}: ${error.message}`,
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.11";
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");