@nurix/etna 0.20.22 → 0.20.24

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.
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "rules": [
3
3
  "changelog",
4
+ "cli",
4
5
  "cloudflare",
5
6
  "code-eval-batcher",
6
7
  "database",
@@ -0,0 +1,68 @@
1
+ ---
2
+ paths:
3
+ - "**/packages/cli/**"
4
+ - "**/packages/*-cli/**"
5
+ - "**/bin/*.js"
6
+ - "**/bin/*.mjs"
7
+ - "**/src/cli/**"
8
+ - "**/src/commands/**"
9
+ ---
10
+ # CLI — credentials, login doors, and the machine contract
11
+
12
+ What a `@nurix/*` command-line tool holds true about **how it authenticates, where it keeps what it is given, and how a script or an agent drives it**. Packaging, `npx` distribution, self-update, managed file blocks and publishing are the `nustack` skill's `cli-design.md` reference; the product API's own gate is the product's feature doc. A CLI that talks to no authenticated service reads only §5.
13
+
14
+ ## 1. Two credentials, and the order they are read in
15
+
16
+ | Credential | Held by | Arrives by | Lives | Dies by | Verified by |
17
+ | --- | --- | --- | --- | --- | --- |
18
+ | **Grant** — an OAuth refresh token, rotated on every use | a person's machine | a login door (§2) | until revoked | revocation at the auth service | the auth service; the CLI never sees a secret it can check |
19
+ | **Token** — an opaque, prefixed secret (`nst_pat_…`) | a pipeline, an agent, a headless host | the `<TOOL>_TOKEN` environment variable | until revoked or expired | a column on its row | the product API, locally, against a stored hash |
20
+
21
+ - **Resolution order is fixed and never prompts: env token → OS store → file store → signed out.** A `<TOOL>_TOKEN` in the environment wins outright and touches no store; `login`, `logout` and `doctor` say so instead of acting on the store behind it.
22
+ - **The grant refreshes; the token never does.** A grant spends the refresh token for a fifteen-minute access JWT per resource and writes the rotated pair back. A token is presented as-is on every call, and a `401` against it is final — no refresh, no retry, no browser.
23
+ - **Only the refresh token and the cached access tokens are stored — never a password, never an ID token, never the token a pipeline injected.**
24
+ - **Never a `--token` flag.** `argv` is readable in `ps` and shell history on every host the command runs on; the environment is the only way a secret enters a process. A flag that exists for a legacy caller is documented as deprecated, never added new.
25
+ - **A token cannot mint a token.** Minting, listing and revoking are person-only routes; a credential that can create credentials turns one leak into an unbounded family.
26
+
27
+ ## 2. Three login doors, one per situation
28
+
29
+ | Door | When | Shape |
30
+ | --- | --- | --- |
31
+ | **Loopback** | a browser runs on the same machine | authorization code + PKCE; `redirect_uri` is `http://127.0.0.1:<ephemeral>/callback`; the listener answers one matching `state` and closes |
32
+ | **Pasted code** | the browser cannot reach the terminal — SSH, a container | the same authorize URL with `redirect_uri` set to the auth service's own code page; the person pastes the code the page shows |
33
+ | **Device grant** (RFC 8628) | a GUI app with no listener of its own | `/device/code`, a `user_code`, polling `/oauth2/token` |
34
+
35
+ - **The CLI hardcodes one hostname — the product's.** The auth service's base URL, the client id and the resource audiences come from the product's discovery route (`GET /api/v1/auth/config`); a literal auth URL in a client pins every installed copy to one deployment.
36
+ - **Send `provider=<id>` on the authorize URL when the client already knows the provider** — the auth service's sign-in page starts that provider server-side with no paint when there is no session. Omitting it is the "click Google, then click Google again" defect.
37
+ - **Never open a browser, and never prompt, when stdin or stdout is not a TTY or when the env token is set.** The refusal is the CLI's own `SIGNED_OUT` code naming the login command, exit 1 — never a bare HTTP status, never a hang waiting on a page nobody can see.
38
+ - **Refusals are coded, and only two spend the credential.** `invalid_grant` is a dead grant: clear the item, run the login door, resume the command. `invalid_target` is a live grant asked for a resource it never covered: re-authorize wider, keep the item. A network failure, a `5xx` or a `429` keeps the item and raises — the item is shared with every client on the machine, and clearing it on a blip signs all of them out.
39
+
40
+ ## 3. Where a credential lives
41
+
42
+ - **OS store where one is probed present, a `0600` file where none is, and the absence of an OS store is never a reason to refuse login.** macOS `security`, Linux `secret-tool` (libsecret); otherwise `~/.<tool>/credentials.json`, mode `0600`, in a `0700` directory, written atomically (temp file + rename). `<TOOL>_CREDENTIAL_STORE=auto|keychain|file` overrides the probe.
43
+ - **Never `localStorage`, never a world-readable file, never the project directory.** The link file a project carries (`.<tool>/project.json`) holds identifiers only — a project id, a host, a stack — and is committed; a secret beside it ships with the repo.
44
+ - **One item, several clients — read-modify-write with the schema versioned.** A CLI and a desktop app sharing an item merge into it, preserve keys they do not know, and stamp `schema_version`; a newer schema is refused whole and an older one reads as *sign in again*. Concurrent refreshes race by design — the auth service's reuse window absorbs the honest double refresh, so the second loser is a rotation, never a wipe.
45
+ - **Logout is revoke-then-clear, best effort on the revoke.** An offline machine still clears its local material.
46
+
47
+ ## 4. The server's doors
48
+
49
+ - **Every machine credential carries a prefix the gate routes by, and the prefixed branches are ordered after the JWT branch.** Two credentials both presented as `Authorization: Bearer <opaque>` with no prefix cannot be told apart; a prefixed branch ahead of the JWT branch would have to decline every JWT correctly on every path, while one placed last cannot shadow a credential that already resolved.
50
+ - **One credential, one door.** A token minted for one surface is refused at every other, whatever it is worth — the audience rule is what bounds a leak.
51
+ - **A token row is `id`, owner, `label`, `token_hash`, `token_prefix`, `expires_at`, `revoked_at`, `last_used_at`.** The raw value appears in the `201` that minted it and nowhere else; the prefix is what a list shows; revocation sets a column and keeps the row, so the audit trail stays readable and the hash can never be re-issued; an expired, a revoked and an unknown token all answer the same `401`, so a caller never learns which.
52
+
53
+ ## 5. The machine contract
54
+
55
+ A script or an agent drives the CLI as often as a person does, and it reads three channels: **stdout carries the one datum** — the URL, the id, or the `--json` envelope — **stderr carries everything else, and the exit code is the verdict.** `cmd > out.txt` with a `0` exit is the whole contract for the caller; a progress line on stdout breaks it. TTY detection, `--yes`, and failing loudly outside a TTY are `cli-design.md` §5; on top of it:
56
+
57
+ - **`--json` implies non-interactive**, and every error is a coded envelope, never prose.
58
+ - **A command that waits on a remote job offers `--no-wait`** (return the id at once) **and, where the job streams a log, `--ci`** (stream the build log, exit with the job's verdict, fall back to polling when the stream dies). The default waits and streams.
59
+ - **Upload and go-live are separate verbs when the platform has a pointer.** `deploy --no-promote` stages, `promote` and `rollback` move the pointer, and a `deploy` without the flag does both. A rollback that rebuilds is a redeploy, not a rollback.
60
+ - **Pack what git would ship.** `.gitignore` plus a `.<tool>ignore` decide what leaves the machine; `--no-gitignore` is the escape hatch, named, never the default.
61
+
62
+ ## 6. Pitfalls — symptom → cause → fix
63
+
64
+ - **The sign-in page painted although the client sent `provider=`** → the hint fell off a hop, or the URL carries `error=` from an earlier attempt (the page always paints on `error=`) → follow the chain hop by hop with `curl -s -o /dev/null -D - <url>` and read each `location:`; the hint must survive every hop.
65
+ - **Works on macOS, asks to log in on every run elsewhere** → OS-store-only storage with no file fallback → §3.
66
+ - **CI hangs at the first authenticated command** → a prompt or a browser opened outside a TTY → §2; the fix is the `SIGNED_OUT` refusal, not a longer timeout.
67
+ - **Two commands at once, and the machine is "signed out"** → a concurrent refresh treated `invalid_grant` on the loser as a dead grant → check the service's reuse window before clearing anything (§3).
68
+ - **A `401` on a `<TOOL>_TOKEN` call loops or opens a browser** → the token took the grant's refresh path → §1: a token's `401` is final.
@@ -9,7 +9,7 @@ paths:
9
9
  ---
10
10
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
11
11
 
12
- How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
12
+ How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and a CLI's packaging live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
13
13
 
14
14
  ---
15
15
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "e02eaf6819db",
2
+ "fingerprint": "73da5d8c28fc",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.18.1",
4
+ "version": "3.19.2",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -25,5 +25,5 @@
25
25
  }
26
26
  ],
27
27
  "docsRoot": "docs",
28
- "digest": "6c141ff0afec"
28
+ "digest": "42dd88faa621"
29
29
  }
@@ -49,6 +49,22 @@ import {
49
49
  pruneState,
50
50
  } from "./lib/claims.mjs";
51
51
  import { appendEvent } from "./lib/session-log.mjs";
52
+ import { wallsAllowlist } from "./lib/graph-store.mjs";
53
+ import { exitWallsAllowlist } from "./lib/exit-store.mjs";
54
+ import { wallEnabled } from "./lib/walls.mjs";
55
+
56
+ // Whether anything will ever read a row: a run whose allowlist switches off both walls the queue feeds —
57
+ // the Stop wall and the commit wall — has nobody to drain it, so its rows would only ride the run's output
58
+ // into the project (docs/harness/code/exit-check.md).
59
+ function queueHasReader(projectDir) {
60
+ let allowlist = null;
61
+ try {
62
+ allowlist = wallsAllowlist(projectDir) ?? exitWallsAllowlist(projectDir);
63
+ } catch (e) {
64
+ allowlist = null;
65
+ }
66
+ return wallEnabled(allowlist, "queue") || wallEnabled(allowlist, "bookkeeping");
67
+ }
52
68
 
53
69
  // ---------------------------------------------------------------------------
54
70
  // SessionStart
@@ -566,7 +582,7 @@ function postToolUse(projectDir, payload) {
566
582
  detectOutcomes(projectDir, meta, rows);
567
583
  }
568
584
  }
569
- appendRows(projectDir, rows);
585
+ if (rows.length > 0 && queueHasReader(projectDir)) appendRows(projectDir, rows);
570
586
  }
571
587
 
572
588
  // ---------------------------------------------------------------------------
@@ -25,6 +25,7 @@ import { appendEvent, unresolvedDenials, sessionLogPath, malformedEventLines } f
25
25
  import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
26
26
  import { claimActivity, releaseActivity, recentForeignTouches } from "./lib/claims.mjs";
27
27
  import { atStop, graphDenial, wallsAllowlist } from "./lib/graph-store.mjs";
28
+ import { exitAtStop, exitWallsAllowlist, recordUnreadable } from "./lib/exit-store.mjs";
28
29
  import { WALLS, wallEnabled } from "./lib/walls.mjs";
29
30
 
30
31
  // --- the build graph's switch ------------------------------------------------------------
@@ -32,12 +33,13 @@ import { WALLS, wallEnabled } from "./lib/walls.mjs";
32
33
  // allowlist over lib/walls.mjs. Every wall it does not name is off for that session — because
33
34
  // the compliance set assumes a session that can dispatch a bookkeeper and a user who can answer
34
35
  // an ask, and a generation box has neither: its walls would be unresolvable blocks, not guard
35
- // rails. No graph, or a graph with no `mode` block, leaves every wall exactly as it was.
36
+ // rails. No graph, or a graph with no `mode` block, leaves every wall exactly as it was. With no graph
37
+ // declaring one, an exit set (docs/harness/code/exit-check.md) may carry the same block.
36
38
  let allowlistCache;
37
39
  function wallOn(projectDir, name) {
38
40
  if (allowlistCache === undefined) {
39
41
  try {
40
- allowlistCache = wallsAllowlist(projectDir);
42
+ allowlistCache = wallsAllowlist(projectDir) ?? exitWallsAllowlist(projectDir);
41
43
  } catch (e) {
42
44
  allowlistCache = null; // a broken graph never switches a wall off
43
45
  }
@@ -524,6 +526,29 @@ function graphStop(projectDir, stopHookActive) {
524
526
  };
525
527
  }
526
528
 
529
+ // The exit check (docs/harness/code/exit-check.md): authority, so a crash blocks rather than passes. It
530
+ // blocks once — the box walls `.claude/state`, so the model cannot repair the file — and the second stop
531
+ // records `blocked`, so the node still ends failed instead of met.
532
+ function exitStop(projectDir, stopHookActive) {
533
+ let step;
534
+ try {
535
+ step = exitAtStop(projectDir);
536
+ } catch (e) {
537
+ const reason = `EXIT CHECK FAILED — ${e?.message || e}`;
538
+ if (!stopHookActive) {
539
+ return { decision: "block", reason: `${reason}. This node's exit set cannot be read, so it is not finished. (Said once — the next Stop ends the node blocked.)` };
540
+ }
541
+ try {
542
+ recordUnreadable(projectDir, reason);
543
+ } catch (e2) {
544
+ /* nowhere to record it; the box seals without a verdict and the run owner's own check decides */
545
+ }
546
+ return null;
547
+ }
548
+ if (!step || step.allow) return null;
549
+ return { decision: "block", reason: step.block };
550
+ }
551
+
527
552
  function stopGate(projectDir, payload) {
528
553
  // The build graph is asked BEFORE the runtime's loop guard, and deliberately ignores it. The
529
554
  // guard exists because an unbounded Stop hook is a hang; the walker is bounded instead — every
@@ -538,6 +563,9 @@ function stopGate(projectDir, payload) {
538
563
  // hand-written graph should know it.
539
564
  const graph = graphStop(projectDir, payload.stop_hook_active);
540
565
  if (graph) return graph;
566
+ // The exit check is asked before the loop guard for the walker's reason: it is bounded by its own attempts.
567
+ const exit = exitStop(projectDir, payload.stop_hook_active);
568
+ if (exit) return exit;
541
569
 
542
570
  if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
543
571
 
@@ -0,0 +1,152 @@
1
+ // harness/code/hooks/lib/exit-store.mjs — the exit check: what a NuStack generation box hands the harness
2
+ // instead of a build graph. The box writes `.claude/state/exit.json` naming the documents a node owes; at
3
+ // every Stop this module checks that each one EXISTS — never its shape — and hands the turn back naming what
4
+ // is missing until the attempts run out, then records `blocked` (docs/harness/code/exit-check.md).
5
+ //
6
+ // This is authority, not hygiene: the caller turns a thrown error into a block, never a pass.
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { atomicWrite, stateDir } from "./state.mjs";
11
+
12
+ export const EXIT_VERSION = 1;
13
+ export const EXIT_FILE = "exit.json";
14
+ export const EXIT_VERDICT_FILE = "exit-verdict.json";
15
+
16
+ const SKIPPED_DIRS = new Set(["node_modules", ".git"]);
17
+
18
+ // Read through a plain join: absence is dormancy, and asking must never create the folder.
19
+ export function exitFile(projectDir) {
20
+ return path.join(projectDir, ".claude", "state", EXIT_FILE);
21
+ }
22
+
23
+ export function exitVerdictFile(projectDir) {
24
+ return path.join(stateDir(projectDir), EXIT_VERDICT_FILE);
25
+ }
26
+
27
+ // Absent → null. Present but not a v1 exit set → throws: a set nobody can read is not a set that is met.
28
+ export function loadExit(projectDir) {
29
+ let text;
30
+ try {
31
+ text = fs.readFileSync(exitFile(projectDir), "utf8");
32
+ } catch (e) {
33
+ return null;
34
+ }
35
+ const set = JSON.parse(text);
36
+ const attempts = set?.attempts;
37
+ if (!set || set.v !== EXIT_VERSION || !Array.isArray(set.owed) || !Number.isInteger(attempts?.used) || !Number.isInteger(attempts?.max)) {
38
+ throw new Error(`.claude/state/${EXIT_FILE} is not a v${EXIT_VERSION} exit set`);
39
+ }
40
+ return set;
41
+ }
42
+
43
+ // `*` matches within one path segment; everything else is literal.
44
+ export function globMatches(glob, rel) {
45
+ const pattern = glob
46
+ .split("*")
47
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
48
+ .join("[^/]+");
49
+ return new RegExp(`^${pattern}$`).test(rel);
50
+ }
51
+
52
+ function isFile(projectDir, rel) {
53
+ try {
54
+ return fs.statSync(path.join(projectDir, rel)).isFile();
55
+ } catch (e) {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ // How many files match a glob, walking only below its literal prefix.
61
+ function globCount(projectDir, glob) {
62
+ const segments = glob.split("/");
63
+ const wild = segments.findIndex((segment) => segment.includes("*"));
64
+ const base = wild === -1 ? path.posix.dirname(glob) : segments.slice(0, wild).join("/");
65
+ let count = 0;
66
+ const walk = (rel) => {
67
+ let entries;
68
+ try {
69
+ entries = fs.readdirSync(path.join(projectDir, rel), { withFileTypes: true });
70
+ } catch (e) {
71
+ return;
72
+ }
73
+ for (const entry of entries) {
74
+ if (SKIPPED_DIRS.has(entry.name)) continue;
75
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
76
+ if (entry.isDirectory()) walk(child);
77
+ else if (entry.isFile() && globMatches(glob, child)) count += 1;
78
+ }
79
+ };
80
+ walk(base === "." ? "" : base);
81
+ return count;
82
+ }
83
+
84
+ // The owed entries not on disk: a path that is not a file, or a glob matching fewer files than its floor.
85
+ export function missingOf(projectDir, set) {
86
+ const missing = [];
87
+ for (const entry of set.owed) {
88
+ if (typeof entry?.path === "string") {
89
+ if (!isFile(projectDir, entry.path)) missing.push({ type: entry.type, path: entry.path });
90
+ } else if (typeof entry?.glob === "string") {
91
+ const min = Number.isInteger(entry.min) && entry.min > 0 ? entry.min : 1;
92
+ const found = globCount(projectDir, entry.glob);
93
+ if (found < min) missing.push({ type: entry.type, glob: entry.glob, min, found });
94
+ } else {
95
+ throw new Error(`an owed entry names neither a path nor a glob: ${JSON.stringify(entry)}`);
96
+ }
97
+ }
98
+ return missing;
99
+ }
100
+
101
+ function describeMissing(entry) {
102
+ return entry.path
103
+ ? `\`${entry.path}\` (${entry.type})`
104
+ : `at least ${entry.min} file(s) matching \`${entry.glob}\` (${entry.type}, ${entry.found} found)`;
105
+ }
106
+
107
+ export function writeVerdict(projectDir, verdict) {
108
+ atomicWrite(exitVerdictFile(projectDir), `${JSON.stringify({ v: EXIT_VERSION, ...verdict }, null, 2)}\n`);
109
+ }
110
+
111
+ // Stop: null when dormant; `{ allow, verdict }` when the set is met or its attempts are spent; `{ block }` otherwise, charging one attempt.
112
+ export function exitAtStop(projectDir) {
113
+ const set = loadExit(projectDir);
114
+ if (!set) return null;
115
+ const missing = missingOf(projectDir, set);
116
+ const nodeSlug = set.nodeSlug ?? null;
117
+ if (missing.length === 0) {
118
+ writeVerdict(projectDir, { nodeSlug, verdict: "done", missing, attempts: set.attempts });
119
+ return { allow: true, verdict: "done", missing };
120
+ }
121
+ if (set.attempts.used >= set.attempts.max) {
122
+ writeVerdict(projectDir, { nodeSlug, verdict: "blocked", missing, attempts: set.attempts });
123
+ return { allow: true, verdict: "blocked", missing };
124
+ }
125
+ const attempts = { ...set.attempts, used: set.attempts.used + 1 };
126
+ atomicWrite(exitFile(projectDir), `${JSON.stringify({ ...set, attempts }, null, 2)}\n`);
127
+ const left = attempts.max - attempts.used;
128
+ return {
129
+ block:
130
+ `EXIT CHECK — this node owes documents that do not exist yet: ${missing.map(describeMissing).join("; ")}. Write them, then stop. ` +
131
+ (left > 0
132
+ ? `This stop was handed back; ${left} more will be, and after that the node ends blocked.`
133
+ : "The next stop ends the node blocked if any of them is still missing."),
134
+ missing,
135
+ attempts,
136
+ };
137
+ }
138
+
139
+ // A set that cannot be read still ends the node — as blocked, naming why — rather than as met.
140
+ export function recordUnreadable(projectDir, reason) {
141
+ writeVerdict(projectDir, { nodeSlug: null, verdict: "blocked", missing: [{ type: "exit-check", reason }], attempts: { used: 0, max: 0 } });
142
+ }
143
+
144
+ // The set's `mode.walls` allowlist, or null. A broken file switches no wall off: hygiene fails open.
145
+ export function exitWallsAllowlist(projectDir) {
146
+ try {
147
+ const set = loadExit(projectDir);
148
+ return Array.isArray(set?.mode?.walls) ? set.mode.walls : null;
149
+ } catch (e) {
150
+ return null;
151
+ }
152
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ paths:
3
+ - "**/packages/cli/**"
4
+ - "**/packages/*-cli/**"
5
+ - "**/bin/*.js"
6
+ - "**/bin/*.mjs"
7
+ - "**/src/cli/**"
8
+ - "**/src/commands/**"
9
+ ---
10
+ # CLI — credentials, login doors, and the machine contract
11
+
12
+ What a `@nurix/*` command-line tool holds true about **how it authenticates, where it keeps what it is given, and how a script or an agent drives it**. Packaging, `npx` distribution, self-update, managed file blocks and publishing are the `nustack` skill's `cli-design.md` reference; the product API's own gate is the product's feature doc. A CLI that talks to no authenticated service reads only §5.
13
+
14
+ ## 1. Two credentials, and the order they are read in
15
+
16
+ | Credential | Held by | Arrives by | Lives | Dies by | Verified by |
17
+ | --- | --- | --- | --- | --- | --- |
18
+ | **Grant** — an OAuth refresh token, rotated on every use | a person's machine | a login door (§2) | until revoked | revocation at the auth service | the auth service; the CLI never sees a secret it can check |
19
+ | **Token** — an opaque, prefixed secret (`nst_pat_…`) | a pipeline, an agent, a headless host | the `<TOOL>_TOKEN` environment variable | until revoked or expired | a column on its row | the product API, locally, against a stored hash |
20
+
21
+ - **Resolution order is fixed and never prompts: env token → OS store → file store → signed out.** A `<TOOL>_TOKEN` in the environment wins outright and touches no store; `login`, `logout` and `doctor` say so instead of acting on the store behind it.
22
+ - **The grant refreshes; the token never does.** A grant spends the refresh token for a fifteen-minute access JWT per resource and writes the rotated pair back. A token is presented as-is on every call, and a `401` against it is final — no refresh, no retry, no browser.
23
+ - **Only the refresh token and the cached access tokens are stored — never a password, never an ID token, never the token a pipeline injected.**
24
+ - **Never a `--token` flag.** `argv` is readable in `ps` and shell history on every host the command runs on; the environment is the only way a secret enters a process. A flag that exists for a legacy caller is documented as deprecated, never added new.
25
+ - **A token cannot mint a token.** Minting, listing and revoking are person-only routes; a credential that can create credentials turns one leak into an unbounded family.
26
+
27
+ ## 2. Three login doors, one per situation
28
+
29
+ | Door | When | Shape |
30
+ | --- | --- | --- |
31
+ | **Loopback** | a browser runs on the same machine | authorization code + PKCE; `redirect_uri` is `http://127.0.0.1:<ephemeral>/callback`; the listener answers one matching `state` and closes |
32
+ | **Pasted code** | the browser cannot reach the terminal — SSH, a container | the same authorize URL with `redirect_uri` set to the auth service's own code page; the person pastes the code the page shows |
33
+ | **Device grant** (RFC 8628) | a GUI app with no listener of its own | `/device/code`, a `user_code`, polling `/oauth2/token` |
34
+
35
+ - **The CLI hardcodes one hostname — the product's.** The auth service's base URL, the client id and the resource audiences come from the product's discovery route (`GET /api/v1/auth/config`); a literal auth URL in a client pins every installed copy to one deployment.
36
+ - **Send `provider=<id>` on the authorize URL when the client already knows the provider** — the auth service's sign-in page starts that provider server-side with no paint when there is no session. Omitting it is the "click Google, then click Google again" defect.
37
+ - **Never open a browser, and never prompt, when stdin or stdout is not a TTY or when the env token is set.** The refusal is the CLI's own `SIGNED_OUT` code naming the login command, exit 1 — never a bare HTTP status, never a hang waiting on a page nobody can see.
38
+ - **Refusals are coded, and only two spend the credential.** `invalid_grant` is a dead grant: clear the item, run the login door, resume the command. `invalid_target` is a live grant asked for a resource it never covered: re-authorize wider, keep the item. A network failure, a `5xx` or a `429` keeps the item and raises — the item is shared with every client on the machine, and clearing it on a blip signs all of them out.
39
+
40
+ ## 3. Where a credential lives
41
+
42
+ - **OS store where one is probed present, a `0600` file where none is, and the absence of an OS store is never a reason to refuse login.** macOS `security`, Linux `secret-tool` (libsecret); otherwise `~/.<tool>/credentials.json`, mode `0600`, in a `0700` directory, written atomically (temp file + rename). `<TOOL>_CREDENTIAL_STORE=auto|keychain|file` overrides the probe.
43
+ - **Never `localStorage`, never a world-readable file, never the project directory.** The link file a project carries (`.<tool>/project.json`) holds identifiers only — a project id, a host, a stack — and is committed; a secret beside it ships with the repo.
44
+ - **One item, several clients — read-modify-write with the schema versioned.** A CLI and a desktop app sharing an item merge into it, preserve keys they do not know, and stamp `schema_version`; a newer schema is refused whole and an older one reads as *sign in again*. Concurrent refreshes race by design — the auth service's reuse window absorbs the honest double refresh, so the second loser is a rotation, never a wipe.
45
+ - **Logout is revoke-then-clear, best effort on the revoke.** An offline machine still clears its local material.
46
+
47
+ ## 4. The server's doors
48
+
49
+ - **Every machine credential carries a prefix the gate routes by, and the prefixed branches are ordered after the JWT branch.** Two credentials both presented as `Authorization: Bearer <opaque>` with no prefix cannot be told apart; a prefixed branch ahead of the JWT branch would have to decline every JWT correctly on every path, while one placed last cannot shadow a credential that already resolved.
50
+ - **One credential, one door.** A token minted for one surface is refused at every other, whatever it is worth — the audience rule is what bounds a leak.
51
+ - **A token row is `id`, owner, `label`, `token_hash`, `token_prefix`, `expires_at`, `revoked_at`, `last_used_at`.** The raw value appears in the `201` that minted it and nowhere else; the prefix is what a list shows; revocation sets a column and keeps the row, so the audit trail stays readable and the hash can never be re-issued; an expired, a revoked and an unknown token all answer the same `401`, so a caller never learns which.
52
+
53
+ ## 5. The machine contract
54
+
55
+ A script or an agent drives the CLI as often as a person does, and it reads three channels: **stdout carries the one datum** — the URL, the id, or the `--json` envelope — **stderr carries everything else, and the exit code is the verdict.** `cmd > out.txt` with a `0` exit is the whole contract for the caller; a progress line on stdout breaks it. TTY detection, `--yes`, and failing loudly outside a TTY are `cli-design.md` §5; on top of it:
56
+
57
+ - **`--json` implies non-interactive**, and every error is a coded envelope, never prose.
58
+ - **A command that waits on a remote job offers `--no-wait`** (return the id at once) **and, where the job streams a log, `--ci`** (stream the build log, exit with the job's verdict, fall back to polling when the stream dies). The default waits and streams.
59
+ - **Upload and go-live are separate verbs when the platform has a pointer.** `deploy --no-promote` stages, `promote` and `rollback` move the pointer, and a `deploy` without the flag does both. A rollback that rebuilds is a redeploy, not a rollback.
60
+ - **Pack what git would ship.** `.gitignore` plus a `.<tool>ignore` decide what leaves the machine; `--no-gitignore` is the escape hatch, named, never the default.
61
+
62
+ ## 6. Pitfalls — symptom → cause → fix
63
+
64
+ - **The sign-in page painted although the client sent `provider=`** → the hint fell off a hop, or the URL carries `error=` from an earlier attempt (the page always paints on `error=`) → follow the chain hop by hop with `curl -s -o /dev/null -D - <url>` and read each `location:`; the hint must survive every hop.
65
+ - **Works on macOS, asks to log in on every run elsewhere** → OS-store-only storage with no file fallback → §3.
66
+ - **CI hangs at the first authenticated command** → a prompt or a browser opened outside a TTY → §2; the fix is the `SIGNED_OUT` refusal, not a longer timeout.
67
+ - **Two commands at once, and the machine is "signed out"** → a concurrent refresh treated `invalid_grant` on the loser as a dead grant → check the service's reuse window before clearing anything (§3).
68
+ - **A `401` on a `<TOOL>_TOKEN` call loops or opens a browser** → the token took the grant's refresh path → §1: a token's `401` is final.
@@ -9,7 +9,7 @@ paths:
9
9
  ---
10
10
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
11
11
 
12
- How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
12
+ How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and a CLI's packaging live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
13
13
 
14
14
  ---
15
15
 
@@ -14,20 +14,21 @@ button,inputs,Button (element/button),Button,"native <button>, variants via clas
14
14
  text-input,inputs,"Input (element/input) — onSave commit, regex validator",Input,"<input type=text>, commit on blur/Enter"
15
15
  number-input,inputs,"NumberInput (element/number-input) — min/max/step",—,"<input inputmode=numeric>, clamp on commit, Escape reverts"
16
16
  textarea,inputs,"Textarea (element/textarea) — Cmd/Ctrl+Enter commits",Textarea,"<textarea>, blur commits, Escape resets draft"
17
+ inline-edit,inputs,"InlineEdit (element/inline-edit) — click-to-edit contentEditable span",—,"span[contenteditable] reading as a label until focused, Enter/blur commits, Escape reverts"
17
18
  checkbox,inputs,Checkbox (element/checkbox),Checkbox,"<input type=checkbox> + label, indeterminate via JS"
18
19
  switch,inputs,Switch (element/switch),Switch,"role=switch button, aria-checked, Space toggles"
19
20
  radio-group,inputs,"RadioGroup + card/segmented variants (element/radio-group)",RadioGroup,"fieldset of <input type=radio>, arrow-key roving focus"
20
- select,inputs,"Select — searchable, multi, async (element/select)","Select; Combobox (Popover + Command)","<select> or listbox popover, typeahead, option objects not raw values"
21
- native-select,inputs,"Select* Radix family (element/native-select)",Select,"plain <select> behind a styled trigger"
21
+ combobox,inputs,"Combobox — searchable, multi, async (compound/combobox)","Combobox (Popover + Command)","input + filtered listbox popup, typeahead, option objects not raw values"
22
+ calendar,inputs,"Calendar (compound/calendar) — react-day-picker",Calendar,"month grid <table>, arrow-key day navigation"
23
+ select,inputs,"Select — closed listbox, one string value, no query field (element/select)",Select,"<select> or listbox popup, typeahead, one string value"
22
24
  date-picker,inputs,DatePicker (compound/date-picker),Date Picker (Calendar + Popover),"<input type=date> or calendar popover, ISO value"
23
25
  date-range-picker,inputs,"DateRangePicker — presets sidebar (compound/date-picker)",—,"two bound date inputs, partial selection reverts on close"
24
- calendar,inputs,"Calendar (element/calendar) — react-day-picker",Calendar,"month grid <table>, arrow-key day navigation"
25
26
  slider,inputs,Slider (element/slider),Slider,"<input type=range> with visible value readout"
26
27
  rating,inputs,Rating (element/rating),—,"radio group of icon buttons, half-step optional"
27
- toggle,inputs,"Toggle · ToggleGroup (element/toggle, element/toggle-group)","Toggle, Toggle Group","aria-pressed buttons, single or multiple selection"
28
+ toggle,inputs,"Toggle · ToggleGroup (element/toggle, compound/toggle-group)","Toggle, Toggle Group","aria-pressed buttons, single or multiple selection"
28
29
  otp-input,inputs,InputOTP (compound/input-otp),Input OTP,"segmented one-char inputs, paste distributes across slots"
29
30
  icon-picker,inputs,"IconPicker · Icon (compound/icon-picker)",—,"searchable icon grid in a popover, categorized"
30
- file-upload,inputs,MediaFilePicker (compound/media-file-picker),—,"<input type=file> + drop zone, previews, size and count caps"
31
+ file-upload,inputs,"FileUpload family — dropzone, list, item progress (compound/file-picker) · FilePreview (compound/file-preview)",—,"<input type=file> + drop zone, previews, size and count caps"
31
32
  form-field,inputs,"Cell — label/content/helper/error slots (compound/cell)",Form + FormField (react-hook-form),"label + control + helper in one block, error slot below control"
32
33
  form-validation,inputs,,Form (react-hook-form),"form element with submit-time validation and inline field errors"
33
34
  rich-text-editor,inputs,,—,"@tiptap/react editor with toolbar and markdown shortcuts"
@@ -39,19 +40,20 @@ bottom-drawer,overlays,Sheet side=bottom (block/sheet),Drawer (vaul),"fixed bott
39
40
  command-palette,overlays,Command (compound/command),Command (cmdk),"filtered list in a dialog, ⌘K opens, arrow-key + Enter"
40
41
  dropdown-menu,overlays,DropdownMenu (compound/dropdown-menu),Dropdown Menu,"role=menu under a trigger, arrow-key nav, outside click closes"
41
42
  context-menu,overlays,ContextMenu (compound/context-menu),Context Menu,"right-click menu at pointer position"
43
+ speed-dial,overlays,SpeedDial (compound/speed-dial),—,"floating trigger button that fans a bounded set of icon actions out along one axis; the trigger doubles as the close affordance"
42
44
  popover,overlays,Popover (element/popover),Popover,"anchored floating panel, dismiss on outside/ESC"
43
45
  hover-card,overlays,HoverCard (element/popover),Hover Card,"preview panel on hover intent, delayed open/close"
44
46
  tooltip,overlays,Tooltip (element/tooltip),Tooltip,"short hint on hover/focus, never load-bearing content"
45
47
  data-table,collections,"Table + defineColumn (block/table)","<Table> + TanStack Table","semantic <table>, sticky header, tabular-nums, real pagination, status chips"
46
48
  table-primitives,collections,UiTable* family (block/ui-table),Table,"bare styled <table>/<thead>/<tbody> parts"
47
49
  list,collections,"List + ListToolbar + ListPagination (block/list)",—,"<ul> of rows with toolbar and pager"
48
- record-header,collections,"Header — config-driven (block/app-header)",—,"page-top block: breadcrumbs, title, status bar, actions"
49
- page-header-stats,collections,"PageHeader — Stat/Crumb/BackLink (block/page-header)",—,"title row + KPI strip + back link"
50
- gantt-timeline,collections,GanttChart (activity-timeline),—,"time-scaled task rows, date axis, horizontal scroll"
50
+ record-header,collections,,—,"page-top block: breadcrumbs, title, status bar, actions"
51
+ page-header-stats,collections,"Heading (element/heading) · Stat (element/stat) — composed at the call site",—,"title row + KPI strip + back link"
52
+ gantt-timeline,collections,,—,"time-scaled task rows, date axis, horizontal scroll"
51
53
  changes-feed,collections,ChangesList (block/changes-list),—,"grouped change-history feed, entity + operation labels"
52
- message-thread,collections,"Message family + MessageScroller (compound/message, block/message-scroller)",—,"scrolling feed pinned to bottom, jump-to-new button"
53
- chat-bubble,collections,BubbleGroup + Bubble (element/bubble),—,"aligned bubble rows with reactions row"
54
- attachment,collections,Attachment family (compound/attachment),—,"file card: media, title, actions, error state, dashed idle border"
54
+ message-thread,collections,"MessageScroller + useMessageScroller (block/message-scroller)",—,"scrolling feed pinned to bottom, jump-to-new button"
55
+ chat-bubble,collections,,—,"aligned bubble rows with reactions row"
56
+ attachment,collections,"FilePreview (compound/file-preview)",—,"file card: media, title, actions, error state, dashed idle border"
55
57
  thread-marker,collections,"Marker — separator/border variants (element/marker)",—,"inline separator row with icon and label"
56
58
  json-viewer,collections,,—,"react18-json-view collapsible tree, mono"
57
59
  virtualized-list,collections,,—,"react-window fixed-row virtualization for 1k+ rows"
@@ -65,79 +67,79 @@ file-tree,navigation,FileTree (block/sidebar),—,"nested tree with disclosure c
65
67
  breadcrumb,navigation,Breadcrumb (compound/breadcrumb),Breadcrumb,"<nav aria-label=breadcrumb>, current page unlinked"
66
68
  tabs,navigation,Tabs (compound/tabs),Tabs,"role=tablist, arrow-key switching, one panel per tab"
67
69
  pagination,navigation,Pagination (compound/pagination),Pagination,"page buttons + prev/next, aria-current on active page"
68
- navbar,navigation,Navbar (block/app-header),—,"horizontal top <nav> with links and actions"
70
+ navbar,navigation,,—,"horizontal top <nav> with links and actions"
69
71
  navigation-menu,navigation,NavigationMenu (compound/navigation-menu),Navigation Menu,"top-level nav with flyout panels"
70
72
  menubar,navigation,Menubar (compound/menubar),Menubar,"desktop-app menu strip with full keyboard model"
71
- slide-out-menu,navigation,SlideOutMenu (block/slide-out-menu),—,"off-canvas nav panel for narrow viewports"
72
73
  icon-rail,navigation,VerticalIconBar (block/vertical-icon-bar),—,"narrow vertical icon strip with tooltips"
73
74
  stepper,navigation,,—,"ordered steps with done/current/todo states, aria-current=step"
74
- alert,feedback,Alert (element/alert),Alert,"role=alert block, tone via border + icon"
75
+ alert,feedback,Alert (compound/alert),Alert,"role=alert block, tone via border + icon"
75
76
  empty-state,feedback,EmptyState (compound/empty-state),Empty,"icon + heading + motivation line + primary action"
76
77
  error-boundary,feedback,ErrorBoundary (lib/error-boundary),—,"React boundary rendering a retry block"
77
78
  not-found,feedback,NotFound (compound/empty-state),—,"404 block with a way back"
78
79
  skeleton,feedback,Skeleton (element/skeleton),Skeleton,"pulsing placeholder matching the final layout"
79
80
  loader,feedback,Loader (element/loader),Spinner,"centered spinner with optional label"
80
- text-spinner,feedback,"Braille (braille) · spinner frames (element/loader)",—,"animated unicode frames inline with text"
81
+ text-spinner,feedback,,—,"animated unicode frames inline with text"
81
82
  progress,feedback,Progress (element/progress),Progress,"role=progressbar with aria-valuenow"
82
83
  toast,feedback,Toaster (compound/sonner),Sonner,"stacked corner notifications, auto-dismiss, action slot"
83
- typing-indicator,feedback,TypingIndicator (element/typing-indicator),—,"three-dot pulse row"
84
- sparkline,charts,"Spark — line|bar|pie (element/spark) · Sparkline (sparkline)",—,"inline no-axis SVG mark, token or self-contained color"
85
- inline-bar,charts,BarGraph (element/bar-graph),—,"compact bar SVG with skeleton, token colors"
86
- inline-line,charts,LineGraph (line-graph),—,"compact line SVG with skeleton"
87
- pie-donut,charts,"PieGraph — pie|half-pie|doughnut|half-doughnut (pie-graph)",—,"SVG arcs, max ~6 slices, center slot in doughnut"
88
- segmented-progress,charts,ProgressBar (progress-bar),—,"stacked horizontal segments with legend"
89
- area-chart,charts,"AreaChart + Area/RangeArea marks (charts)",Chart (recharts),"axes + gridlines + filled series + tooltip"
90
- bar-chart,charts,"BarChart + ParetoLine, BarSort, stack modes (charts)",Chart (recharts),"grouped/stacked bars, value labels opt-in"
91
- line-chart,charts,"LineChart + projection/reference overlays (charts)",Chart (recharts),"multi-series lines, reference lines, brush"
92
- scatter-chart,charts,ScatterChart + Scatter (charts),—,"x/y point field, hover ring, box select"
93
- composed-chart,charts,ComposedChart (charts),—,"mixed marks over one shared scale pair"
94
- histogram,charts,Histogram + computeHistogramBins (charts),—,"binned bars over a continuous axis"
84
+ typing-indicator,feedback,"Loader — pulsing dots (element/loader)",—,"three-dot pulse row"
85
+ sparkline,charts,"Spark — line|bar|pie (element/spark)",—,"inline no-axis SVG mark, token or self-contained color"
86
+ status-timeline,charts,"StatusTimeline + .Skeleton (compound/status-timeline)",—,"one stacked bar per day of uptime status, hover card per day — no axis, not a bar chart"
87
+ inline-line,charts,"Spark — line (element/spark)",—,"compact line SVG with skeleton"
88
+ pie-donut,charts,"Chart — pie spec (block/chart)",—,"SVG arcs, max ~6 slices, center slot in doughnut"
89
+ segmented-progress,charts,"Chart — progress spec (block/chart)",—,"stacked horizontal segments with legend"
90
+ area-chart,charts,"Chart — area spec (block/chart)",Chart (recharts),"axes + gridlines + filled series + tooltip"
91
+ bar-chart,charts,"Chart — bar spec (block/chart)",Chart (recharts),"grouped/stacked bars, value labels opt-in"
92
+ line-chart,charts,"Chart — line spec (block/chart)",Chart (recharts),"multi-series lines, reference lines, brush"
93
+ scatter-chart,charts,"Chart — scatter spec (block/chart)",—,"x/y point field, hover ring, box select"
94
+ composed-chart,charts,"Chart — composed spec (block/chart)",—,"mixed marks over one shared scale pair"
95
+ histogram,charts,"Chart — bar spec with mode: histogram (block/chart)",—,"binned bars over a continuous axis"
95
96
  bullet-chart,charts,"Bullet (gauge; also charts)",—,"measure bar + target tick + zone bands"
96
- funnel,charts,"Funnel + FunnelTooltip/FunnelLegend (funnel)",—,"ordered stage bands, percent-of-previous labels"
97
- gauge,charts,"Gauge — arc or notch track, zones, target (gauge)",—,"value arc with threshold zones and center readout"
98
- contribution-heatmap,charts,Heatmap + chrome (heatmap),—,"calendar cell grid with level legend"
99
- matrix-heatmap,charts,MatrixHeatmap + legend/tooltip (heatmap),—,"x-by-y cell matrix on the tokenized ramp"
100
- radar,charts,Radar (radar),—,"spider axes with series polygons"
101
- ring,charts,Ring (ring),—,"segmented circular progress with center info"
102
- chart-chrome,charts,"ChartEmpty · ChartHoverCard · ChartLegend (chart-primitives)",—,"shared empty/tooltip/legend chrome for inline charts"
97
+ funnel,charts,"Chart — funnel spec (block/chart)",—,"ordered stage bands, percent-of-previous labels"
98
+ gauge,charts,"Chart — gauge spec (block/chart)",—,"value arc with threshold zones and center readout"
99
+ contribution-heatmap,charts,"Chart — heatmap-calendar spec (block/chart)",—,"calendar cell grid with level legend"
100
+ matrix-heatmap,charts,"Chart — heatmap-matrix spec (block/chart)",—,"x-by-y cell matrix on the tokenized ramp"
101
+ radar,charts,"Chart — radar spec (block/chart)",—,"spider axes with series polygons"
102
+ ring,charts,"Chart — ring spec (block/chart)",—,"segmented circular progress with center info"
103
+ chart-chrome,charts,"Chart chrome — legend, tooltip and empty state set by the spec theme · ChartErrorCard (block/chart)",—,"shared empty/tooltip/legend chrome for inline charts"
103
104
  card,layout,Card family (block/card),Card,"bordered section: header, content, footer"
104
- stat-card,layout,Statistics family (statistics),—,"number + comparison + trend + state + door, composable regions"
105
+ stat-card,layout,"Stat (element/stat) · Chart — stat spec (block/chart)",—,"number + comparison + trend + state + door, composable regions"
105
106
  accordion,layout,Accordion (compound/accordion),Accordion,"stacked disclosure sections, single or multiple open"
107
+ collapsible-panel,layout,SidePanel (block/side-panel),—,"in-flow side region docked left or right that collapses to a rail instead of disappearing; siblings reflow, nothing overlays"
106
108
  collapsible,layout,"Collapsible (element/collapsible) · CollapsiblePanel (compound/accordion)",Collapsible,"button-controlled region with hidden state"
107
109
  split-panes,layout,ResizablePanelGroup (block/resizable),Resizable,"draggable divider panes, keyboard resize"
108
110
  scroll-area,layout,ScrollArea (element/scroll-area),Scroll Area,"styled overflow container, visible-on-hover scrollbar"
109
111
  separator,layout,"Separator (element/separator) · SeparatorWithText (element/separator)",Separator,"<hr>/role=separator, optional centered label"
110
112
  aspect-ratio,layout,AspectRatio (element/aspect-ratio),Aspect Ratio,"CSS aspect-ratio box"
111
113
  carousel,layout,Carousel (compound/carousel),Carousel,"scroll-snap track with prev/next controls"
112
- bento-grid,layout,BentoGrid + BentoCard (bento-grid),—,"asymmetric feature grid with spanning cells"
113
- editable-card,layout,EditableCard (compound/editable-card),—,"card with edit-in-place body and commit footer"
114
+ bento-grid,layout,,—,"asymmetric feature grid with spanning cells"
115
+ editable-card,layout,,—,"card with edit-in-place body and commit footer"
114
116
  drag-reorder,layout,Sortable family (compound/sortable),—,"dnd-kit list reorder, keyboard sortable, drag overlay ghost"
115
- text,typography,Text (element/text),Typography,"semantic p/span with size and weight scale"
116
- badge,typography,Badge (element/badge),Badge,"small status pill, tokenized tone"
117
+ text,typography,,Typography,"semantic p/span with size and weight scale"
118
+ badge,typography,"Chip (element/chip)",Badge,"small status pill, tokenized tone"
117
119
  chip,typography,"Chip (element/chip) · Option display (element/select)",—,"removable tag, optional color dot"
118
120
  code,typography,Code (element/code),—,"inline <code>, mono"
119
121
  kbd,typography,Kbd (element/kbd),Kbd,"<kbd> keycap"
120
122
  label,typography,Label (element/label),Label,"<label for> bound to a control"
121
123
  link,typography,Link (element/link),—,"<a> with underline discipline"
122
- secure-text,typography,SecureText (secure-text),—,"masked value with reveal toggle, mono"
124
+ secure-text,typography,"SecureText · maskText (element/secure-text)",—,"masked value with reveal toggle, mono"
123
125
  markdown,typography,Markdown (element/markdown),—,"react-markdown rendering with typographic defaults"
124
126
  avatar,media,Avatar (element/avatar),Avatar,"image with initials fallback, round"
125
127
  copy-button,media,CopyButton (compound/copy-button),—,"clipboard write with copied-state swap"
126
- icon,media,"IconRenderer (element/icon-renderer) + icons-data (lib/icons-data)",—,"lucide icon by name, consistent stroke and size grid"
127
- voice-waveform,media,"LiveWaveform (element/live-waveform) · MicIcon (element/mic-icon)",—,"animated level bars from mic input"
128
+ icon,media,"Icon · IconPicker (compound/icon-picker) + iconsData (lib/icons-data)",—,"lucide icon by name, consistent stroke and size grid"
129
+ voice-waveform,chat,"MicInput (compound/mic-input)",—,"latched dictation button plus mic level bars; button={false} for the meter alone, recorder={…} for record-then-review"
128
130
  brand-logo,media,"Logo — blue-black|white-white|lime-white|blue-white (element/logo)",—,"inline SVG logo sized by height"
129
131
  shine-border,media,ShineBorder (effect/shine-border),—,"animated gradient border wrapper, decorative"
130
- theme-toggle,media,ThemeToggler (element/theme-toggler),—,"light/dark switch writing data-theme"
132
+ theme-toggle,media,"ThemeToggle (effect/theme-toggle)",—,"light/dark switch toggling the .dark class on the root element"
131
133
  view-switcher,media,ViewSwitcher (compound/tabs),—,"segmented control switching view-type projections"
132
134
  animated-number,motion,"AnimatedNumber (element/animated-number) · SlidingNumber (element/animated-number)",—,"tick-up numerals, renders plain text until JS ready"
133
- stagger-entrance,motion,AnimatedGroup (effect/animated-group),—,"staggered child arrival, identity moments only, never on 100×/day actions"
135
+ stagger-entrance,motion,"AnimatedList (effect/animated-list)",—,"staggered child arrival, identity moments only, never on 100×/day actions"
134
136
  text-effects,motion,"TextEffect · TextMorph · TextScramble · TextShimmer (effect/*)",—,"per-character text animation, low-frequency surfaces only"
135
137
  morphing-overlay,motion,"MorphingDialog · MorphingPopover (effect/*)",—,"trigger morphs into overlay, interruptible"
136
- panel-transitions,motion,"TransitionPanel · Disclosure · motion Accordion (effect/*) · animated Tabs (animate-ui)",—,"crossfade/slide between sibling panels, height-animated reveal, interruptible"
138
+ panel-transitions,motion,"TransitionPanel (effect/transition-panel) · Accordion (compound/accordion) · Tabs (compound/tabs)",—,"crossfade/slide between sibling panels, height-animated reveal, interruptible"
137
139
  decorative-effects,motion,"Spotlight · GlowEffect · AnimatedBackground · InfiniteSlider · Highlight (effect/*, effect/*)",—,"pointer-tracked washes and loops, decorative only, never on data"
138
- dynamic-toolbar,motion,ToolbarDynamic (effect/toolbar-dynamic),—,"toolbar expanding to contextual actions"
140
+ dynamic-toolbar,motion,,—,"toolbar expanding to contextual actions"
139
141
  utility-hooks,hooks,"useClickOutside · useFileUpload · useMediaQuery · useIsMobile (lib/*)",—,"outside-click, file-upload state, matchMedia boolean, <768px boolean"
140
142
  class-merge,utils,cn (lib/utils),cn,"clsx + tailwind-merge"
141
- formatting-utils,utils,"formatDate (lib/format) · generateId (lib/id) · getErrorMessage (lib/handle-error)",—,"Intl date formatting, nanoid ids, error-to-message normalization"
143
+ formatting-utils,utils,"formatDate (lib/format) · generateId (lib/id) · formatRelativeTime (lib/time)",—,"Intl date formatting, nanoid ids, relative-time strings"
142
144
  composition-utils,utils,"getStrictContext (lib/get-strict-context) · composeRefs (lib/compose-refs)",—,"throwing Provider/hook pair, ref composition"
143
145
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.22",
3
+ "version": "0.20.24",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "e02eaf6819db"
45
+ "etnaFingerprint": "73da5d8c28fc"
46
46
  }