@nodii/dev 0.6.29 → 0.6.31
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 +52 -1
- package/dist/cli.js +63 -9
- package/dist/core.js +37 -14
- package/dist/devops-openapi.json +1 -1
- package/dist/doctor.js +18 -1
- package/dist/freeze.js +328 -0
- package/dist/gather.js +241 -0
- package/dist/harness.js +2 -0
- package/dist/hooks.js +16 -42
- package/dist/lanes.js +170 -9
- package/dist/managed-hooks.js +6 -1
- package/dist/nodii-dev.js +12 -2
- package/dist/nudges.js +441 -0
- package/dist/setup.js +23 -6
- package/dist/starting.js +52 -23
- package/dist/upload.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ nodii-dev login [--no-browser] [--scope KEYS] [--force]
|
|
|
87
87
|
nodii-dev logout
|
|
88
88
|
nodii-dev whoami # OAuth identity plus the registered agent
|
|
89
89
|
nodii-dev config show
|
|
90
|
-
nodii-dev doctor [--figma-file KEY] [--json] # preflight:
|
|
90
|
+
nodii-dev doctor [--figma-file KEY] [--json] # preflight: Dev, hooks, local Ollama + Gather model, optional Figma
|
|
91
91
|
|
|
92
92
|
nodii-dev api list [--tag TAG] [--json]
|
|
93
93
|
nodii-dev api OPERATION_ID [--param name=value ...] [--body @file|-|JSON]
|
|
@@ -129,6 +129,15 @@ nodii-dev decisions get D123
|
|
|
129
129
|
nodii-dev decisions search WORDS
|
|
130
130
|
nodii-dev decisions create|lock|supersede|amend
|
|
131
131
|
|
|
132
|
+
nodii-dev lane gather [KEY] [--delta] [--objective TEXT] [--done-check TEXT]
|
|
133
|
+
[--paths A,B] [--components K,L]
|
|
134
|
+
nodii-dev lane freeze add KEY --section SECTION --file ITEM.json --reason TEXT
|
|
135
|
+
nodii-dev lane freeze drop KEY --section SECTION --item ID --reason TEXT
|
|
136
|
+
nodii-dev lane freeze swap KEY --section SECTION [--item ID] --file VALUE.json --reason TEXT
|
|
137
|
+
nodii-dev lane freeze show KEY
|
|
138
|
+
nodii-dev lane freeze history KEY
|
|
139
|
+
nodii-dev rules find "QUESTION" [--limit N]
|
|
140
|
+
|
|
132
141
|
nodii-dev open-questions list|show|answer
|
|
133
142
|
nodii-dev feature-docs list|show|claim|release|status
|
|
134
143
|
nodii-dev contracts grpc|rest|outbox|saga|table publish|status
|
|
@@ -304,6 +313,9 @@ nodii-dev hook claude-figma-pre # reads Claude Code hook JSON on stdin
|
|
|
304
313
|
nodii-dev hook claude-figma-post # reads Claude Code hook JSON on stdin
|
|
305
314
|
nodii-dev hook safety-pre # silent safety decision; blocks with exit 2
|
|
306
315
|
nodii-dev hook safety-post # silently records resources created by successful commands
|
|
316
|
+
nodii-dev hook nudge-pre # guideline nudges before a tool call; never blocks
|
|
317
|
+
nodii-dev hook nudge-stop # guideline nudges when a lane session stops; asks once
|
|
318
|
+
nodii-dev figma checked CS-n --unused-properties --stale-placeholders --dialog-body
|
|
307
319
|
nodii-dev hook print-settings # prints managed-settings JSON; writes nothing
|
|
308
320
|
|
|
309
321
|
nodii-dev lane start KEY --title TEXT --kind dev_fix|new_build|docs_change|uat_fix|prod_fix
|
|
@@ -420,6 +432,38 @@ deploy. A coordinator's exceptional `--no-verify` also requires
|
|
|
420
432
|
`NODII_DEV_NO_VERIFY_REASON`; the CLI records the reason in private Git
|
|
421
433
|
metadata. Lanes and subagents can never use `--no-verify`.
|
|
422
434
|
|
|
435
|
+
### Guideline nudges (AGENT-HARNESS-21)
|
|
436
|
+
|
|
437
|
+
`harness install` and `harness update` add two more `nodii-dev` hooks for
|
|
438
|
+
Claude Code and Codex. A nudge reminds a lane of a rule at the moment it is
|
|
439
|
+
about to be broken and **says nothing when the rule is met**. Nudges never
|
|
440
|
+
block a tool call, fail open on any error or unknown payload, and stay silent
|
|
441
|
+
in any session that is not a Dev lane. Each cites the clause it protects.
|
|
442
|
+
|
|
443
|
+
| Moment | Nudge | Silent when |
|
|
444
|
+
|---|---|---|
|
|
445
|
+
| Edit or write | A DS component or product page is used or edited and its guideline is not in the lane's gathered set (U07-19, DEC-0010) | the guideline is in the gathered set, or was read with `design guideline show` |
|
|
446
|
+
| Edit or write | `className` or `style` on a DS component (U07-19) | the tag is not a DS import, or has neither prop |
|
|
447
|
+
| Edit or write | A new migration: journal `when` = max + 1, then a Delta gather. A `_journal.json` write whose `when` values do not rise | the migration is covered by a Delta gather, or already exists |
|
|
448
|
+
| Edit or write | Code renders a person: `PiiText` without `self`, a raw `.email`/`.phone`/`.displayName`, or the page masks a value itself (R5, U05-25) | `PiiText` carries `self`, or no person is rendered |
|
|
449
|
+
| Figma read | A code lane reads by node id (`get_design_context`, `get_metadata`, `get_screenshot`, ...) instead of the published key (MW8-20) | a Figma lane, or no node id |
|
|
450
|
+
| `figma submit CS-n` | The unused-property, stale `Blocked on DS` placeholder and Dialog-without-body hand checks (F-34, CS-2453) | all three are recorded with `figma checked CS-n --unused-properties --stale-placeholders --dialog-body` |
|
|
451
|
+
| AskUserQuestion | The question is also an item in `docs/figma-ux-drive/qa-with-abhi/open.md` (R7) | open.md already holds the question |
|
|
452
|
+
| Stop | The lane is not `on_dev` but "done" was claimed (MW3); an "I couldn't" with no filed issue; Unconfirmed Freeze items still open; uncommitted work | none of those; a Stop that was already nudged once may stop |
|
|
453
|
+
|
|
454
|
+
The lane's gathered set is the private Git file `nodii-lane-knowledge.json`:
|
|
455
|
+
`lane gather --components a,b` adds the components it covered to `guidelines`,
|
|
456
|
+
`lane gather --delta` adds the paths it saw (`--paths`, else every path this
|
|
457
|
+
worktree changes against origin/main) to `deltaGathered`, and `design guideline
|
|
458
|
+
show` adds to `guidelinesRead`. A part's guideline is its root's (`dialog`
|
|
459
|
+
covers `dialog-content`). Edits are judged against the file they produce, so
|
|
460
|
+
an Edit or `apply_patch` that only changes an attribute is read in full.
|
|
461
|
+
|
|
462
|
+
The Stop nudge asks the session to continue once (`decision: block`, skipped
|
|
463
|
+
when `stop_hook_active`); Codex passes it through its managed dispatcher. A
|
|
464
|
+
nudge hook skips the amendment guard and the waiting-results banner, since
|
|
465
|
+
either would corrupt its JSON; it cannot run a Delta gather.
|
|
466
|
+
|
|
423
467
|
### DS conformance: `design roles` and `design check` (DEC-0034)
|
|
424
468
|
|
|
425
469
|
Every UI role names exactly one DS component (MW6-14). Resolve your brief's
|
|
@@ -764,6 +808,13 @@ call until `lane gather --delta` serves the current rules and acknowledges the
|
|
|
764
808
|
newest displayed version. `NODII_DEV_RUN_ID` makes this check per subagent run,
|
|
765
809
|
so one child cannot acknowledge for its siblings.
|
|
766
810
|
|
|
811
|
+
Gather also persists the lane's bounded Freeze: the objective and done check,
|
|
812
|
+
strict repository/Figma boundaries, the current pinned clauses, and the lane's
|
|
813
|
+
lookups, decisions, and unconfirmed questions. Ordinary CLI calls print and
|
|
814
|
+
acknowledge a changed Freeze before continuing, and compact SessionStart uses
|
|
815
|
+
the same backstop. `lane freeze add|drop|swap` makes a reasoned, version-checked
|
|
816
|
+
change; `show|history` reads the current contract or its append-only history.
|
|
817
|
+
|
|
767
818
|
`lane start` creates and starts a lane. It infers the repository (by short
|
|
768
819
|
name, as Dev compares it), branch and worktree from git unless overridden, and resolves `--issue` keys to the UUIDs
|
|
769
820
|
required by Dev. Lane keys are passed directly to every lane and Built
|
package/dist/cli.js
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
import { heartbeatCurrentLane, postHook, postHookFailure, preHook, sessionStartContext, settingsSnippet, workflowSuggestion } from "./hooks.js";
|
|
24
24
|
import { setupCommand } from "./setup.js";
|
|
25
25
|
import { harnessCommand } from "./harness.js";
|
|
26
|
-
import { hookRuntime, recordHookFire, safetyPostHook, safetyPreHook } from "./safety.js";
|
|
26
|
+
import { hookRuntime, recordHookFire, safetyContext, safetyPostHook, safetyPreHook } from "./safety.js";
|
|
27
|
+
import { SUBMIT_CHECK_FLAGS, changedPaths, nudgeCandidate, nudgePre, nudgeStop, recordGathered, recordGuidelineRead, recordSubmitChecks } from "./nudges.js";
|
|
27
28
|
import { doctorCommand } from "./doctor.js";
|
|
28
29
|
import { dispatchManagedHook } from "./managed-hooks.js";
|
|
30
|
+
import { freezeCommand } from "./freeze.js";
|
|
29
31
|
import {
|
|
30
32
|
figmaChangeSetShow,
|
|
31
33
|
figmaChangeSetText,
|
|
@@ -95,8 +97,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
95
97
|
login [--force] | logout | whoami | config show
|
|
96
98
|
# login says so when the machine is already signed in; --force
|
|
97
99
|
# replaces that one sign-in (never from a lane)
|
|
98
|
-
doctor [--figma-file KEY] [--json] # preflight: sign-in, agent, permissions, machine, repo, lane slots
|
|
99
|
-
#
|
|
100
|
+
doctor [--figma-file KEY] [--json] # preflight: sign-in, agent, permissions, machine, repo, lane slots,
|
|
101
|
+
# local Ollama + pinned Gather model, and optional Figma registration
|
|
100
102
|
agent register --name NAME | agent heartbeat
|
|
101
103
|
issues list|show|create|update|claim|release
|
|
102
104
|
issues comment ISSUE --body TEXT # or --body-file FILE; issues update --comment TEXT|--comment-file FILE adds one too
|
|
@@ -125,6 +127,7 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
125
127
|
figma published FILE_KEY|CHANGE_SET --figma-version V # a file key marks every awaiting set published
|
|
126
128
|
figma check|log-write|submit|apply|abandon|release
|
|
127
129
|
figma submit CHANGE_SET --file SUBMIT.json [--sync]
|
|
130
|
+
figma checked CHANGE_SET --unused-properties --stale-placeholders --dialog-body # the hand checks the submit nudge asks for
|
|
128
131
|
# submit, resubmit and design check --change-set run as a job in Dev
|
|
129
132
|
# and poll it (a large set outlasts the edge's timeout); --sync waits
|
|
130
133
|
# on one request instead. A 502/504 is checked against the set, never resent
|
|
@@ -134,8 +137,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
134
137
|
figma changeset show CHANGE_SET # items, variants, round, decision and comment (with the previous round's)
|
|
135
138
|
figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
|
|
136
139
|
# register a Figma file in Dev so it can be locked
|
|
137
|
-
hook claude-figma-pre|claude-figma-post|safety-pre|safety-post|session-start|managed-dispatch|print-settings
|
|
138
|
-
setup [--yes|--check] # dry-run by default
|
|
140
|
+
hook claude-figma-pre|claude-figma-post|safety-pre|safety-post|nudge-pre|nudge-stop|session-start|managed-dispatch|print-settings
|
|
141
|
+
setup [--yes|--check] # dry-run by default; installs/checks Ollama and pulls the Gather model
|
|
139
142
|
harness install|update [--include-drafts[=false]] [--adopt PATH,...] [--dry-run] [--system]
|
|
140
143
|
# --system stages managed hooks and only prints operator-reviewed sudo commands
|
|
141
144
|
harness check # 0 current, 1 differs from Dev, 2 not installed
|
|
@@ -150,6 +153,7 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
150
153
|
rulebook list|show CODE [--version V]|diff CODE
|
|
151
154
|
rules core # the generated core (M05-33); refused over the cap
|
|
152
155
|
rules report [--section S] [--lane KEY] # delivery totals per document; rules served per lane
|
|
156
|
+
rules find "QUESTION" [--limit N] # answer an ad-hoc rules question from the indexed sections
|
|
153
157
|
rules cap TOKENS # operator: set the core token cap (default 3000)
|
|
154
158
|
|
|
155
159
|
coordinator start --machine NAME # register this machine's one watched coordinator; refuses a live second
|
|
@@ -162,7 +166,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
162
166
|
# authoritative preflight, one worktree per repo, watched launch
|
|
163
167
|
# uses the provisioned agents/lane-<key>.json Dev identity
|
|
164
168
|
lane amend <key> --objective|--scope|--rules TEXT --reason TEXT
|
|
165
|
-
lane gather [<key>] [--delta]
|
|
169
|
+
lane gather [<key>] [--delta] [--objective TEXT] [--done-check TEXT] [--components a,b] [--paths a,b]
|
|
170
|
+
# infers a launched worktree; explicit contract inputs support manual lane start
|
|
166
171
|
run [--kind build|test_run|subagent|deploy] [--name TEXT] -- <command> [args...]
|
|
167
172
|
# watched command: registers with NODII_DEV_RUN_ID,
|
|
168
173
|
# heartbeats, and records exit code + redacted log tail
|
|
@@ -175,12 +180,18 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
175
180
|
lane heartbeat|resume|land|verify|prioritize|deprioritize|stop <key>
|
|
176
181
|
lane review <key> --file REVIEW.json # base/head, outcome and clause-cited findings
|
|
177
182
|
lane reviews <key> # stored review history for the lane
|
|
183
|
+
lane freeze add <key> --section SECTION --file ITEM.json --reason TEXT
|
|
184
|
+
lane freeze drop <key> --section SECTION --item ID --reason TEXT
|
|
185
|
+
lane freeze swap <key> --section SECTION [--item ID] --file VALUE.json --reason TEXT
|
|
186
|
+
lane freeze show|history <key> # durable compact-safe contract and all versions
|
|
178
187
|
lane non-issue <key> --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
|
|
179
188
|
--branch carried:KEY|dropped[:REASON] [--commits SHA[,SHA]]
|
|
180
189
|
# DEC-0011: terminal non_issue; issue -> wont_do; slot frees
|
|
181
190
|
lane list [--kind KIND] [--status STATUS] [--mine] [--cursor CURSOR]
|
|
182
191
|
lane show <key> | next <key> [--paths A,B] [--components K,L] [--role R]
|
|
183
192
|
# next step, plus the rules Dev serves for it (DEC-0020)
|
|
193
|
+
lane gather [key] [--delta] [--paths A,B] [--components K,L]
|
|
194
|
+
# local document pick; prints Lane contract then Clause map
|
|
184
195
|
built list | built ack <key...>
|
|
185
196
|
machine list|show|set-coordinator|assign|report
|
|
186
197
|
agent lane-limit|grant|revoke
|
|
@@ -435,7 +446,9 @@ export async function figmaFileAdd(value, flags, { request = devRequest } = {})
|
|
|
435
446
|
export async function main(argv = process.argv.slice(2)) {
|
|
436
447
|
// Amendment acknowledgement is a lane-wide backstop, including otherwise
|
|
437
448
|
// local commands such as help and version.
|
|
438
|
-
|
|
449
|
+
// A nudge hook is fail-open and writes only its own JSON: it cannot run a
|
|
450
|
+
// Delta gather, and a waiting-results banner would corrupt its output.
|
|
451
|
+
if (argv[0] !== "__run-supervisor" && !(argv[0] === "hook" && argv[1]?.startsWith("nudge-"))) {
|
|
439
452
|
await amendmentGuard(argv);
|
|
440
453
|
await showWaitingResults({ acknowledge: argv[0] !== "hook" });
|
|
441
454
|
}
|
|
@@ -556,7 +569,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
556
569
|
if (command === "rulebook" && ["list", "show", "diff"].includes(action)) return emitCommand(await rulebookCommand(action, rest, f));
|
|
557
570
|
if (command === "rules") return emitCommand(await rulesCommand(action, rest, f));
|
|
558
571
|
// Guideline change sets: approving or rejecting one is a person's action in Dev.
|
|
559
|
-
if (command === "design" && action === "guideline")
|
|
572
|
+
if (command === "design" && action === "guideline") {
|
|
573
|
+
const result = await guidelineCommand(rest[0], rest.slice(1), f);
|
|
574
|
+
// A guideline read in a lane counts toward the lane's gathered set (nudge hooks).
|
|
575
|
+
if (rest[0] === "show" && rest[1]) await recordGuidelineRead(process.cwd(), rest[1]).catch(() => {});
|
|
576
|
+
return emitCommand(result);
|
|
577
|
+
}
|
|
560
578
|
if (command === "design" && action === "changeset") return emitCommand(await changeSetCommand(rest[0], rest.slice(1), f));
|
|
561
579
|
if (command === "design" && action === "roles") return emit(await designRoles(rest), json, designRolesText);
|
|
562
580
|
if (command === "design" && action === "check") {
|
|
@@ -573,7 +591,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
573
591
|
if (command === "coordinator") return emitCommand(await coordinatorCommand(action, f));
|
|
574
592
|
if (command === "lane" && action === "launch") return emitCommand(await launchLaneCommand(rest[0], f));
|
|
575
593
|
if (command === "lane" && action === "amend") return emitCommand(await amendLaneCommand(rest[0], f));
|
|
576
|
-
if (command === "lane" && action === "gather")
|
|
594
|
+
if (command === "lane" && action === "gather") {
|
|
595
|
+
const result = await gatherLaneCommand(rest[0], f);
|
|
596
|
+
// What a gather covered is the lane's gathered set for the nudge hooks.
|
|
597
|
+
const asked = (value) => (typeof value === "string" ? value.split(",").map((item) => item.trim()).filter(Boolean) : []);
|
|
598
|
+
await (async () => recordGathered(process.cwd(), { guidelines: asked(f.components), deltaGathered: f.delta ? (f.paths ? asked(f.paths) : await changedPaths(process.cwd())) : [] }))().catch(() => {});
|
|
599
|
+
return emitCommand(result);
|
|
600
|
+
}
|
|
601
|
+
if (command === "lane" && action === "freeze")
|
|
602
|
+
return emitCommand(await freezeCommand(rest[0], rest.slice(1), f));
|
|
577
603
|
if (command === "lane") return emitCommand(await laneCommand(action, rest, f));
|
|
578
604
|
if (command === "next") return emitCommand(await nextCommand(action, f));
|
|
579
605
|
if (command === "built") return emitCommand(await builtCommand(action, rest, f));
|
|
@@ -805,6 +831,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
805
831
|
if (!result.data.allowed) process.exitCode = 3;
|
|
806
832
|
return;
|
|
807
833
|
}
|
|
834
|
+
if (action === "checked") {
|
|
835
|
+
const flagged = SUBMIT_CHECK_FLAGS.filter((check) => f[check]);
|
|
836
|
+
if (!isChangeSetId(rest[0]) || rest.length > 1 || !flagged.length)
|
|
837
|
+
throw new CliError("USAGE", `figma checked CHANGE_SET ${SUBMIT_CHECK_FLAGS.map((check) => `--${check}`).join(" ")} (at least one)`, 2);
|
|
838
|
+
await recordSubmitChecks(process.cwd(), rest[0], flagged);
|
|
839
|
+
return emit({ data: { changeSetId: rest[0], checked: flagged } }, json, `${rest[0]}: recorded ${flagged.join(", ")}.`);
|
|
840
|
+
}
|
|
808
841
|
if (action === "log-write")
|
|
809
842
|
return emit(await figmaLogWrite(rest[0], requireFlag(f, "tool"), requireFlag(f, "nodes").split(",").filter(Boolean)), json);
|
|
810
843
|
if (["apply", "published", "abandon"].includes(action)) {
|
|
@@ -910,6 +943,27 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
910
943
|
} catch {}
|
|
911
944
|
return;
|
|
912
945
|
}
|
|
946
|
+
// Guideline nudges (AGENT-HARNESS-21) never block: any error, a payload
|
|
947
|
+
// they don't know or a session outside a lane ends in no output.
|
|
948
|
+
if (action === "nudge-pre") {
|
|
949
|
+
try {
|
|
950
|
+
const payload = JSON.parse(await stdinText());
|
|
951
|
+
await recordHookFire(payload).catch(() => {});
|
|
952
|
+
if (!(await nudgeCandidate(payload))) return;
|
|
953
|
+
const nudges = await nudgePre(payload, await safetyContext(payload.cwd ?? process.cwd()));
|
|
954
|
+
if (nudges.length) console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: nudges.join("\n") } }));
|
|
955
|
+
} catch {}
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (action === "nudge-stop") {
|
|
959
|
+
try {
|
|
960
|
+
const payload = JSON.parse(await stdinText());
|
|
961
|
+
await recordHookFire(payload).catch(() => {});
|
|
962
|
+
const nudges = await nudgeStop(payload, await safetyContext(payload.cwd ?? process.cwd()));
|
|
963
|
+
if (nudges.length) console.log(JSON.stringify({ decision: "block", reason: nudges.join("\n") }));
|
|
964
|
+
} catch {}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
913
967
|
if (action === "lane-heartbeat") {
|
|
914
968
|
const result = await heartbeatCurrentLane();
|
|
915
969
|
return emit(result, json, result.heartbeated ? `Heartbeat recorded for ${result.key}.` : "No current-branch lane; no heartbeat sent.");
|
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.31";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|
|
@@ -119,9 +119,30 @@ const staleCode = (raw) => {
|
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const withSignal = (promise, signal) => {
|
|
123
|
+
if (!signal) return promise;
|
|
124
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new Error("aborted"));
|
|
125
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
126
|
+
const abort = () => rejectPromise(signal.reason ?? new Error("aborted"));
|
|
127
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
128
|
+
Promise.resolve(promise).then(
|
|
129
|
+
(value) => { signal.removeEventListener("abort", abort); resolvePromise(value); },
|
|
130
|
+
(error) => { signal.removeEventListener("abort", abort); rejectPromise(error); },
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
async function devFetch(method, path, bodyText, contentType, accept, extra = {}, signal) {
|
|
136
|
+
const wait = async (promise) => {
|
|
137
|
+
try {
|
|
138
|
+
return await withSignal(promise, signal);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (signal?.aborted) throw new CliError("DEV_TIMEOUT", `Dev request timed out: ${method} ${path}`, 1);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const config = await wait(readConfig());
|
|
145
|
+
const auth = await wait(authProvider());
|
|
125
146
|
const url = requestUrl(config, path);
|
|
126
147
|
// The edge refuses a token minted before the principal's claims epoch with
|
|
127
148
|
// 401 token_stale (refresh and retry once). The request never reached Dev,
|
|
@@ -132,26 +153,28 @@ async function devFetch(method, path, bodyText, contentType, accept, extra = {})
|
|
|
132
153
|
for (;;) {
|
|
133
154
|
const headers = { accept, ...extra };
|
|
134
155
|
if (bodyText !== undefined) headers["content-type"] = contentType;
|
|
135
|
-
Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
|
|
156
|
+
Object.assign(headers, await wait(auth.headers({ method, path, body: bodyText ?? "" })));
|
|
136
157
|
let response;
|
|
137
158
|
try {
|
|
138
|
-
response = await fetch(url, { method, headers, body: bodyText });
|
|
159
|
+
response = await wait(fetch(url, { method, headers, body: bodyText, signal }));
|
|
139
160
|
} catch (error) {
|
|
161
|
+
if (error?.code === "DEV_TIMEOUT") throw error;
|
|
162
|
+
if (signal?.aborted) throw new CliError("DEV_TIMEOUT", `Dev request timed out: ${method} ${path}`, 1);
|
|
140
163
|
throw new CliError("DEV_UNREACHABLE", `Cannot reach Dev at ${new URL(url).origin}: ${error.message}`);
|
|
141
164
|
}
|
|
142
|
-
const raw = await response.text();
|
|
165
|
+
const raw = await wait(response.text());
|
|
143
166
|
if (response.status !== 401 || typeof auth.refreshStale !== "function" || !staleCode(raw)) return { response, raw };
|
|
144
167
|
if (refreshed) return { response, raw };
|
|
145
168
|
// A cached token that is stale too is not adopted twice: the next one is refreshed.
|
|
146
169
|
const used = String(headers.authorization ?? "").replace(/^Bearer /, "");
|
|
147
|
-
if ((await auth.refreshStale(used, { force: adopted })) === "refreshed") refreshed = true;
|
|
170
|
+
if ((await wait(auth.refreshStale(used, { force: adopted }))) === "refreshed") refreshed = true;
|
|
148
171
|
else adopted = true;
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
152
175
|
/** A JSON call that keeps the status: `{ status, payload }`. Errors throw as devRawRequest's do. */
|
|
153
|
-
export async function devExchange(method, path, bodyText, { contentType = "application/json", headers = {} } = {}) {
|
|
154
|
-
const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json", headers);
|
|
176
|
+
export async function devExchange(method, path, bodyText, { contentType = "application/json", headers = {}, signal } = {}) {
|
|
177
|
+
const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json", headers, signal);
|
|
155
178
|
let payload;
|
|
156
179
|
try {
|
|
157
180
|
payload = raw ? JSON.parse(raw) : {};
|
|
@@ -168,8 +191,8 @@ export async function devExchange(method, path, bodyText, { contentType = "appli
|
|
|
168
191
|
return { status: response.status, payload };
|
|
169
192
|
}
|
|
170
193
|
|
|
171
|
-
export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
|
|
172
|
-
return (await devExchange(method, path, bodyText, { contentType })).payload;
|
|
194
|
+
export async function devRawRequest(method, path, bodyText, contentType = "application/json", options = {}) {
|
|
195
|
+
return (await devExchange(method, path, bodyText, { ...options, contentType })).payload;
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
/** A Dev route that answers with text (e.g. a document body as text/markdown). Errors are Dev's JSON. */
|
|
@@ -181,8 +204,8 @@ export async function devTextRequest(method, path) {
|
|
|
181
204
|
throw new CliError(payload.code ?? `HTTP_${response.status}`, payload.message ?? `Dev returned HTTP ${response.status}`, 1, payload.details);
|
|
182
205
|
}
|
|
183
206
|
|
|
184
|
-
export const devRequest = (method, path, body) =>
|
|
185
|
-
devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body));
|
|
207
|
+
export const devRequest = (method, path, body, options) =>
|
|
208
|
+
devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body), "application/json", options);
|
|
186
209
|
|
|
187
210
|
/** A JSON call sent with `Prefer: respond-async` (RFC 7240): `{ status, payload }`, 202 when Dev took it as a job. */
|
|
188
211
|
export const devAsyncRequest = async (method, path, body) =>
|