@nodii/dev 0.6.12 → 0.6.14

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
@@ -147,6 +147,7 @@ prints them without creating anything.
147
147
  nodii-dev figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
148
148
  nodii-dev figma lock FILE_KEY --title TEXT [--scope KIND:ID ...]
149
149
  nodii-dev figma scope add CHANGE_SET --scope KIND:ID [--scope ...]
150
+ nodii-dev figma scope set CHANGE_SET --scope KIND:ID [--scope ...]
150
151
  nodii-dev figma publish-plan FILE_KEY
151
152
  nodii-dev figma check FILE_KEY
152
153
  nodii-dev figma log-write CHANGE_SET --tool TOOL --nodes NODE[,NODE]
@@ -190,10 +191,21 @@ refuses an overlap with `SCOPE_CONFLICT`; the CLI prints Dev's message, then
190
191
  the set holding the overlapping scope. `figma lock` without `--scope` locks the
191
192
  whole file, as older CLIs do, and that conflicts with every other set.
192
193
  `figma scope add` widens a set you hold, with the same check; nothing is ever
193
- widened silently.
194
+ widened silently. `figma scope set` narrows a set you hold to exactly the
195
+ scopes you give, and releases the rest for other sets: for example, a set
196
+ migrated as a whole-file lock can keep just its page. It only narrows. Each
197
+ scope must sit inside one the set holds (the whole file holds everything, and
198
+ a page holds its components and creates); anything else is
199
+ `SCOPE_NOT_NARROWER`. Dev refuses with `WRITE_OUT_OF_SCOPE` when a logged
200
+ write or a submitted node would fall outside the narrower scopes, so keep a
201
+ scope that holds them.
194
202
 
195
203
  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`,
204
+ logs each write's node ids, in colon form. It also logs the variable,
205
+ collection and style ids (`VariableID:…`, `VariableCollectionId:…`, `S:…`)
206
+ that a write returns, including those in a `use_figma` script's JSON
207
+ result, so Dev can tell a foundations write. Dev checks containment at
208
+ `submit`, `resubmit` and `apply`. When Dev answers `WRITE_OUT_OF_SCOPE`,
197
209
  the hook exits `2`, so Claude Code shows the agent why: a write that
198
210
  returned no node ids (return them from the script, then run `figma log-write`),
199
211
  or ids outside the scopes. Dev refuses `figma submit` and `figma resubmit`
@@ -205,11 +217,14 @@ base version.
205
217
  Scopes release when a set reaches `awaiting_publish`. One library publish
206
218
  then covers every set awaiting publish. Before you publish in Figma,
207
219
  `figma publish-plan FILE_KEY` lists the components changed since the last
208
- publish in two groups. **publish** belongs to sets awaiting publish. **leave
220
+ publish in two groups, and warns about any component that sets from both
221
+ groups changed. **publish** belongs to sets awaiting publish. **leave
209
222
  unchecked** belongs to sets still editing or in review: untick those in
210
223
  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.
224
+ marks every set applied before that version as published (pass the
225
+ library publish's version id). Dev holds back a set whose component an
226
+ unfinished set now holds, and flags any published component from an
227
+ unfinished set. `figma published CS-123 ...` keeps the per-set route.
213
228
 
214
229
  `figma withdraw` moves a change set you hold from `in_review` back to
215
230
  `editing`, as long as no item has been decided (Dev's
package/dist/api.js CHANGED
@@ -102,7 +102,7 @@ export const GROUPS = {
102
102
  composite: "Composite", "service-edges": "ServiceEdges", lanes: "Lanes",
103
103
  swaps: "Swaps", ideas: "Ideas", terminology: "Terminology", credentials: "Credentials",
104
104
  rulebook: "ad-documents", "ad-decisions": "ad-decisions", domains: "ad-domains", standardizations: "standardizations",
105
- archive: "archive", harness: "harness",
105
+ archive: "archive", harness: "harness", utils: "utils",
106
106
  // DEC-0020 rule delivery; `rules core|report|cap` are first-class (upload.js).
107
107
  rules: "rulebook",
108
108
  };
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  figmaChangeSetText,
32
32
  figmaFileKey,
33
33
  figmaFilePublished,
34
+ figmaFilePublishedText,
34
35
  figmaLock,
35
36
  figmaLockCheck,
36
37
  figmaLockText,
@@ -39,6 +40,8 @@ import {
39
40
  figmaPublishPlanText,
40
41
  figmaResubmit,
41
42
  figmaScopeAdd,
43
+ figmaScopeSet,
44
+ figmaScopeSetText,
42
45
  figmaSubmit,
43
46
  isChangeSetId,
44
47
  scopeText,
@@ -91,6 +94,7 @@ Usage: nodii-dev <command> [options] [--json]
91
94
  # scopes: component:NODE page:NODE foundations create:PAGE:NAME;
92
95
  # no --scope locks the whole file (DEC-0032)
93
96
  figma scope add CHANGE_SET --scope KIND:ID [--scope ...] # widen a held set's scopes
97
+ figma scope set CHANGE_SET --scope KIND:ID [--scope ...] # narrow to exactly these; the rest frees
94
98
  figma publish-plan FILE_KEY # what to publish, and what to leave unchecked in Figma's dialog
95
99
  figma published FILE_KEY|CHANGE_SET --figma-version V # a file key marks every awaiting set published
96
100
  figma check|log-write|submit|apply|abandon|release
@@ -139,7 +143,7 @@ contracts, sagas, outbox-topics, tables, libraries, workers, projects, issues,
139
143
  backlogs, builds, changes, deploy-reports, runtime, people, agents, inbox,
140
144
  gate, design, runbooks, doctrine, changelog, health-verdicts, approval-kinds,
141
145
  notification-registry, skills, open-questions, decisions, drift, requests,
142
- rulebook, ad-decisions, domains, standardizations, archive, and harness.
146
+ rulebook, ad-decisions, domains, standardizations, archive, harness, and utils.
143
147
 
144
148
  Run a command with --help or see the package README for all flags.`;
145
149
 
@@ -605,7 +609,8 @@ export async function main(argv = process.argv.slice(2)) {
605
609
  if (action === "lock")
606
610
  return emit(await figmaLock(rest[0], requireFlag(f, "title"), { scopes: scopesFrom(f) }), json, (p) => figmaLockText(p.data));
607
611
  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);
612
+ if (!["add", "set"].includes(rest[0]) || !rest[1] || rest.length > 2) throw new CliError("USAGE", "figma scope add|set CHANGE_SET --scope KIND:ID [--scope ...]", 2);
613
+ if (rest[0] === "set") return emit(await figmaScopeSet(rest[1], scopesFrom(f)), json, (p) => figmaScopeSetText(rest[1], p.data));
609
614
  return emit(await figmaScopeAdd(rest[1], scopesFrom(f)), json, (p) =>
610
615
  `${rest[1]} holds ${p.data?.scopes?.length ?? "its"} scope(s)${p.data?.scopes?.length ? `: ${p.data.scopes.map(scopeText).join(", ")}` : ""}`);
611
616
  }
@@ -616,9 +621,7 @@ export async function main(argv = process.argv.slice(2)) {
616
621
  // One publish covers every set awaiting publish on the file (MW1-57.8); a
617
622
  // change set id keeps the per-set route.
618
623
  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)}` : ""));
624
+ return emit(await figmaFilePublished(rest[0], f.file ? JSON.parse(await readFile(f.file, "utf8")) : { figmaVersion: requireFlag(f, "figma-version") }), json, (p) => figmaFilePublishedText(p.data));
622
625
  if (action === "submit") {
623
626
  const payload = f.file ? JSON.parse(await readFile(f.file, "utf8")) : { figmaVersion: requireFlag(f, "figma-version") };
624
627
  return emit(await figmaSubmit(rest[0], payload), json);
@@ -670,7 +673,7 @@ export async function main(argv = process.argv.slice(2)) {
670
673
  return emit(
671
674
  await devRequest(
672
675
  "POST",
673
- `/api/v1/design/files/${encode(rest[0])}/lock/release`,
676
+ `/api/v1/design/files/${encode(figmaFileKey(rest[0]))}/lock/release`,
674
677
  {},
675
678
  ),
676
679
  json,
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.12";
7
+ export const VERSION = "0.6.14";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -105,20 +105,42 @@ export function requestUrl(config, path) {
105
105
  return `${config.edgeUrl.replace(/\/$/, "")}/devops${path}`;
106
106
  }
107
107
 
108
+ const staleCode = (raw) => {
109
+ try {
110
+ return JSON.parse(raw)?.code === "token_stale";
111
+ } catch {
112
+ return false;
113
+ }
114
+ };
115
+
108
116
  async function devFetch(method, path, bodyText, contentType, accept) {
109
117
  const config = await readConfig();
110
118
  const auth = await authProvider();
111
- const headers = { accept };
112
- if (bodyText !== undefined) headers["content-type"] = contentType;
113
- Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
114
119
  const url = requestUrl(config, path);
115
- let response;
116
- try {
117
- response = await fetch(url, { method, headers, body: bodyText });
118
- } catch (error) {
119
- throw new CliError("DEV_UNREACHABLE", `Cannot reach Dev at ${new URL(url).origin}: ${error.message}`);
120
+ // The edge refuses a token minted before the principal's claims epoch with
121
+ // 401 token_stale (refresh and retry once). The request never reached Dev,
122
+ // so it is sent again: with a token another process cached since, or with
123
+ // one refresh. A token_stale after that refresh is surfaced.
124
+ let refreshed = false;
125
+ let adopted = false;
126
+ for (;;) {
127
+ const headers = { accept };
128
+ if (bodyText !== undefined) headers["content-type"] = contentType;
129
+ Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
130
+ let response;
131
+ try {
132
+ response = await fetch(url, { method, headers, body: bodyText });
133
+ } catch (error) {
134
+ throw new CliError("DEV_UNREACHABLE", `Cannot reach Dev at ${new URL(url).origin}: ${error.message}`);
135
+ }
136
+ const raw = await response.text();
137
+ if (response.status !== 401 || typeof auth.refreshStale !== "function" || !staleCode(raw)) return { response, raw };
138
+ if (refreshed) return { response, raw };
139
+ // A cached token that is stale too is not adopted twice: the next one is refreshed.
140
+ const used = String(headers.authorization ?? "").replace(/^Bearer /, "");
141
+ if ((await auth.refreshStale(used, { force: adopted })) === "refreshed") refreshed = true;
142
+ else adopted = true;
120
143
  }
121
- return { response, raw: await response.text() };
122
144
  }
123
145
 
124
146
  export async function devRawRequest(method, path, bodyText, contentType = "application/json") {