@nodii/dev 0.6.1 → 0.6.3

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
@@ -24,9 +24,15 @@ live `devops.*` key except `devops.releases.promote`, which is never granted.
24
24
  Login uses the RFC 8628 device flow and opens the verification page on an
25
25
  interactive terminal unless `--no-browser` is passed. Refresh tokens are
26
26
  stored only in macOS Keychain or Linux Secret Service (`secret-tool`); the CLI
27
- refuses plaintext fallback. Access tokens stay in memory. Every API call goes
28
- to the edge's `/devops` prefix with the OAuth bearer and the registered
29
- `x-agent-id`. `logout` revokes the refresh chain before erasing local
27
+ refuses plaintext fallback. The current access token is kept there too, so
28
+ every nodii-dev process on the machine shares it instead of refreshing on each
29
+ run; refreshes take turns under a per-user lock. A refresh clears the saved
30
+ sign-in only when nodii-auth answers `invalid_grant` (expired, revoked or
31
+ replayed). A 429, 5xx, timeout, network error or proxy error page keeps it and
32
+ fails with `OAUTH_REFRESH_UNAVAILABLE` (exit 1) after a few retries. Every API
33
+ call goes to the edge's `/devops` prefix with the OAuth bearer and the
34
+ registered `x-agent-id`; signing in again as the same person keeps that
35
+ registration. `logout` revokes the refresh chain before erasing local
30
36
  credentials. `--auth-url`, `--edge-url`, and `--tenant-hint` select another real
31
37
  deployment.
32
38
 
@@ -98,7 +104,34 @@ nodii-dev heartbeat audit [SERVICE]
98
104
  nodii-dev design entries [--kind KIND] [--group GROUP] [--q TEXT]
99
105
  nodii-dev design entry KEY
100
106
  nodii-dev design guide PAGE_TYPE
107
+ ```
108
+
109
+ ### Design-system guidelines
110
+
111
+ Edit an entry's guideline through Dev, not with signed scripts. A proposal is
112
+ a guideline change set; a person approves or rejects it in Dev (nobody decides
113
+ their own), and the approved edit becomes a design-system PR.
114
+
115
+ ```bash
116
+ nodii-dev design guideline list [--drafted] [--kind KIND] [--q TEXT] [--all]
117
+ nodii-dev design guideline show ENTRY > ENTRY.md # the editable Markdown form
118
+ nodii-dev design guideline propose ENTRY --file ENTRY.md [--summary TEXT] [--dry-run]
119
+ nodii-dev design changeset list [--entry KEY] [--state in_review] [--limit N]
120
+ nodii-dev design changeset show GCS-001
121
+ ```
101
122
 
123
+ The file has one `## <list>` section per statement list (`whenToUse`,
124
+ `whenNotToUse`, `dos`, `donts`, and for an active guideline `anatomy`,
125
+ `states.states`, `states.loading`, `states.empty`, `states.error`, `copy`,
126
+ `composition`, `data.notes`), one `- ` bullet per item, and `(drafted)` after
127
+ the list name when the text still awaits a real source. A line that is an
128
+ HTML comment is ignored (`<!--` inside an item stays in its text), and a line
129
+ break inside an item reads back as a space. The `#` title, as `show` prints it,
130
+ names the entry: `propose` refuses a file titled for another one. `propose` sends
131
+ only the lists that differ from Dev's current guideline, and `--dry-run`
132
+ prints them without creating anything.
133
+
134
+ ```bash
102
135
  nodii-dev figma lock FILE_KEY --title TEXT
103
136
  nodii-dev figma check FILE_KEY
104
137
  nodii-dev figma log-write CHANGE_SET --tool TOOL --nodes NODE[,NODE]
package/dist/api.js CHANGED
@@ -100,7 +100,7 @@ export const GROUPS = {
100
100
  decisions: "Decisions", drift: "Drift", requests: "Requests", pipelines: "Pipelines", slices: "Slices", "sql-lookups": "SqlLookups",
101
101
  runners: "Runners", routing: "Routing", usage: "Usage", audit: "AuditLog", reconciliation: "Reconciliation", cron: "Cron", health: "Health",
102
102
  composite: "Composite", "service-edges": "ServiceEdges", lanes: "Lanes",
103
- swaps: "Swaps", ideas: "Ideas", terminology: "Terminology",
103
+ swaps: "Swaps", ideas: "Ideas", terminology: "Terminology", credentials: "Credentials",
104
104
  rulebook: "ad-documents", "ad-decisions": "ad-decisions", domains: "ad-domains", standardizations: "standardizations",
105
105
  archive: "archive", harness: "harness",
106
106
  };
@@ -154,11 +154,19 @@ export const ALIASES = {
154
154
  "terminology:list": "listTerminology", "terminology:show": "getTerminologyTerm", "terminology:create": "createTerminologyTerm", "terminology:update": "updateTerminologyTerm",
155
155
  };
156
156
 
157
+ // Decisions a person makes in Dev (operator authority): nodii-dev never sends them.
158
+ export const PERSON_ONLY = new Set(["approveDesignGuidelineChangeSet", "rejectDesignGuidelineChangeSet", "mergeDesignRelease"]);
159
+ export function refusePersonOnly(operationId) {
160
+ if (PERSON_ONLY.has(operationId))
161
+ throw new CliError("PERSON_ONLY", `${operationId} is a person's decision in Dev; nodii-dev does not send it.`, 2);
162
+ }
163
+
157
164
  export function findFirstClass(operations, group, action) {
158
165
  const aliased = ALIASES[`${group}:${action}`];
159
166
  if (aliased) return operations.find(x => x.operationId === aliased);
160
167
  const tags = Array.isArray(GROUPS[group]) ? GROUPS[group] : [GROUPS[group]];
161
- const candidates = operations.filter(x => x.tags.some(t => tags.includes(t)));
168
+ // A person-only operation is never picked by name, so a guessed verb cannot reach it.
169
+ const candidates = operations.filter(x => !PERSON_ONLY.has(x.operationId) && x.tags.some(t => tags.includes(t)));
162
170
  const normalized = action?.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).toLowerCase();
163
171
  return candidates.find(x => x.operationId.toLowerCase() === normalized || x.operationId.toLowerCase().endsWith(normalized));
164
172
  }
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import { heartbeatCurrentLane, postHook, preHook, settingsSnippet, workflowSugge
26
26
  import { setupCommand } from "./setup.js";
27
27
  import { harnessCommand } from "./harness.js";
28
28
  import { docsCommand, rulebookCommand } from "./upload.js";
29
+ import { changeSetCommand, guidelineCommand } from "./guidelines.js";
29
30
  import {
30
31
  ALIASES,
31
32
  GROUPS,
@@ -33,6 +34,7 @@ import {
33
34
  callOperation,
34
35
  findFirstClass,
35
36
  flagsToOperation,
37
+ refusePersonOnly,
36
38
  loadOpenApi,
37
39
  operationsFrom,
38
40
  parseParams,
@@ -58,6 +60,9 @@ Usage: nodii-dev <command> [options] [--json]
58
60
  drift file | request file
59
61
  decisions list|get|search
60
62
  design entries|entry|guide
63
+ design guideline list [--drafted] [--all] | show ENTRY
64
+ design guideline propose ENTRY --file FILE.md [--summary TEXT] [--dry-run]
65
+ design changeset list [--entry KEY] [--state STATE] | show ID
61
66
  figma lock|check|log-write|submit|apply|published|abandon|release
62
67
  hook claude-figma-pre|claude-figma-post|print-settings
63
68
  setup [--yes|--check] # dry-run by default
@@ -140,9 +145,12 @@ async function login(flags, json) {
140
145
  noBrowser: Boolean(flags["no-browser"]),
141
146
  scope: flags.scope,
142
147
  output: json ? console.error : console.log,
148
+ previous,
143
149
  });
144
- return emit({ loggedIn: true, provider: "oauth", credentialStorage: result.storage }, json,
145
- `OAuth login complete; refresh credential stored in ${result.storage}. Run \`nodii-dev agent register --name NAME\` next.`);
150
+ return emit({ loggedIn: true, provider: "oauth", credentialStorage: result.storage, agentId: result.agentId }, json,
151
+ result.agentId
152
+ ? `OAuth login complete; refresh credential stored in ${result.storage}. Kept agent registration ${result.agentId}.`
153
+ : `OAuth login complete; refresh credential stored in ${result.storage}. Run \`nodii-dev agent register --name NAME\` next.`);
146
154
  }
147
155
 
148
156
  async function issues(action, args, f, json) {
@@ -237,6 +245,7 @@ export async function main(argv = process.argv.slice(2)) {
237
245
  if (!action) throw new CliError("USAGE", "api needs an operationId or list", 2);
238
246
  const entry = operations.find((x) => x.operationId === action);
239
247
  if (!entry) throw new CliError("UNKNOWN_OPERATION", `No OpenAPI operationId: ${action}`, 2);
248
+ refusePersonOnly(entry.operationId);
240
249
  const parsed = await readBody(f.body);
241
250
  const request = buildOperationRequest(entry, parseParams(f.param), parsed?.value);
242
251
  return emit(await devRawRequest(request.method, request.path, parsed?.text), json);
@@ -270,6 +279,9 @@ export async function main(argv = process.argv.slice(2)) {
270
279
  }
271
280
  if (command === "docs") return emitCommand(await docsCommand(action, rest, f));
272
281
  if (command === "rulebook" && ["list", "show", "diff"].includes(action)) return emitCommand(await rulebookCommand(action, rest, f));
282
+ // Guideline change sets: approving or rejecting one is a person's action in Dev.
283
+ if (command === "design" && action === "guideline") return emitCommand(await guidelineCommand(rest[0], rest.slice(1), f));
284
+ if (command === "design" && action === "changeset") return emitCommand(await changeSetCommand(rest[0], rest.slice(1), f));
273
285
  if (command === "lane") return emitCommand(await laneCommand(action, rest, f));
274
286
  if (command === "next") return emitCommand(await nextCommand(action, f));
275
287
  if (command === "built") return emitCommand(await builtCommand(action, rest, f));
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.1";
7
+ export const VERSION = "0.6.3";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -162,7 +162,7 @@ export async function checkLogin() {
162
162
  export const encode = (value) => encodeURIComponent(value);
163
163
  // Switches never take the next token as their value, so
164
164
  // `docs upload --wait rulebook/` keeps rulebook/ as a path.
165
- const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts"]);
165
+ const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted"]);
166
166
  export function parseArgs(argv) {
167
167
  const flags = {};
168
168
  const args = [];