@vincentt-xr/cli 0.21.0 → 0.23.0

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.
Files changed (51) hide show
  1. package/MIGRATION.md +26 -0
  2. package/dist/cli/args.js +2 -0
  3. package/dist/cli/index.js +50 -8
  4. package/dist/cli/usage-text.js +10 -1
  5. package/dist/cli/verb-surface.d.ts +2 -1
  6. package/dist/cli/verb-surface.js +5 -0
  7. package/dist/dx/command.d.ts +74 -18
  8. package/dist/dx/command.js +137 -86
  9. package/dist/dx/consent.d.ts +25 -6
  10. package/dist/dx/consent.js +58 -25
  11. package/dist/dx/enablement.d.ts +63 -11
  12. package/dist/dx/enablement.js +93 -21
  13. package/dist/dx/journal.d.ts +3 -0
  14. package/dist/dx/journal.js +6 -1
  15. package/dist/dx/report.d.ts +4 -24
  16. package/dist/dx/report.js +32 -25
  17. package/dist/dx/skeleton.js +3 -0
  18. package/dist/editor/app.d.ts +61 -0
  19. package/dist/editor/app.js +180 -0
  20. package/dist/editor/copy.d.ts +22 -0
  21. package/dist/editor/copy.js +95 -0
  22. package/dist/editor/handoffs.d.ts +12 -0
  23. package/dist/editor/handoffs.js +93 -0
  24. package/dist/editor/launcher.d.ts +86 -0
  25. package/dist/editor/launcher.js +255 -0
  26. package/dist/editor/localFiles.d.ts +31 -0
  27. package/dist/editor/localFiles.js +100 -0
  28. package/dist/editor/reads.d.ts +47 -0
  29. package/dist/editor/reads.js +269 -0
  30. package/dist/editor/text.d.ts +7 -0
  31. package/dist/editor/text.js +58 -0
  32. package/dist/editor/tools.d.ts +5 -0
  33. package/dist/editor/tools.js +20 -0
  34. package/dist/lib/version.d.ts +28 -2
  35. package/dist/lib/version.js +30 -3
  36. package/dist/preview/command.js +2 -0
  37. package/dist/preview/copy.d.ts +5 -0
  38. package/dist/preview/copy.js +15 -0
  39. package/dist/preview/runner.d.ts +6 -0
  40. package/dist/preview/runner.js +1 -0
  41. package/dist/publish/command.js +5 -2
  42. package/dist/publish/copy.d.ts +6 -0
  43. package/dist/publish/copy.js +27 -6
  44. package/dist/publish/manifest.d.ts +11 -2
  45. package/dist/publish/manifest.js +45 -9
  46. package/dist/publish/upload.d.ts +10 -0
  47. package/dist/publish/upload.js +23 -0
  48. package/dist/shared/config.d.ts +6 -6
  49. package/dist/tunnel/client.d.ts +11 -3
  50. package/dist/tunnel/client.js +11 -8
  51. package/package.json +2 -2
package/MIGRATION.md CHANGED
@@ -3,6 +3,32 @@
3
3
  Per version, newest first. Read by the platform's own version surface and served to
4
4
  creators' agents. Accurate or absent, never speculative (B-F5-2).
5
5
 
6
+ ## 0.23.0
7
+
8
+ required: false
9
+ No action required. New verb `vincentt editor`, which hands the project to the Vincentt Editor desktop app.
10
+ - `vincentt editor` (macOS 14 or later, no account) finds `Vincentt Editor.app` in `/Applications`, then `~/Applications`, checks it is signed by Vincentt, hands it the project folder and exits 0 with `✓ Sent <name> to Vincentt Editor.` It does not wait for the app. The app is a separate download from https://vincentt.studio/editor; the CLI installs no editor binary and has no port option for it.
11
+ - It exits 1, last line `Nothing was opened.`, when the app is not installed, the installed copy is not signed by Vincentt, the Mac is not on macOS 14 or later, or another copy of the app is running from a different folder.
12
+ - `vincentt editor --json` prints one `vincentt.editor-open/1` object: `{"schema","handedOff":true,"project","app","appVersion"}`, or `handedOff: false` with a `reason` (`not_installed`, `bad_signature`, `unsupported_os`, `other_copy_running`, `open_failed`) and, where known, `app` and `detail`. The sentence goes to stderr.
13
+ - `vincentt editor --selection [--json]` prints what is selected in the editor (`vincentt.editor-selection/1`), and `vincentt editor --status --json` the editor's state for the folder (`vincentt.editor-state/1`, including `app.lastOpen`). Both work on any OS and exit 1, with one JSON object still on stdout, when no editor is running for the folder.
14
+ - In `--status --json`, `app` is always `{"version","lastOpen"}`; when no editor is running, `lastOpen` is reported only for a folder this machine handed over with `vincentt editor`, from a state file written after that hand-off, and is `null` otherwise.
15
+ - The CLI keeps those hand-offs in `~/.vincentt/editor-handoffs.json`, as a hash of each folder's path and a time, never the path itself. A file written in a newer format than this CLI reads is refused with an upgrade line on stderr and nothing on stdout.
16
+
17
+ ## 0.22.0
18
+
19
+ required: false
20
+ No action required. Behaviour that changed:
21
+ - `vincentt dx enable` at a terminal now asks `Allow unattended reports on your account? [y/N]` and, on `y`, records the grant on your Vincentt account first, then sets this machine. Success reads `Enabled for your account. Reports may now be sent without a prompt from any machine signed in to it.`
22
+ - If the account does not record the grant, `vincentt dx enable` changes nothing and exits 1 with `✗ Vincentt did not record your consent.`, `✗ Too many consent changes. Wait a minute and try again.`, or `✗ Could not reach Vincentt to record your consent.` A refused sign-in exits 4 and a too-old CLI exits 5, as on other verbs.
23
+ - Unattended `vincentt dx --send` and `vincentt report` now check the account first. An account grant sends; an account that is not granted refuses (exit 1) and clears this machine's setting, so turning reports off in the console or on another machine reaches this one. This machine's setting is used on its own only when the account cannot be reached. An interactive session still asks and makes no account check.
24
+ - The `Unattended send: ENABLED since <day> (<n> days)` line now ends with where the answer came from: `· granted from the CLI`, `· granted in the console`, `· on your account`, or `· this machine (account not checked)`. `vincentt dx` and `vincentt version` check the account for up to 3 seconds first; `vincentt version` never fails because of that check.
25
+ - `vincentt dx --json` adds `dxConsent`: `{"state": "on" | "off" | "unknown", "checked": "account" | "machine", "at"?, "via"?}`. `dxSendEnabled` and `dxSendEnabledAt` still describe this machine's setting.
26
+ - `vincentt preview` has a new ending: when the preview server answers the tunnel with a reply this CLI cannot accept, the preview ends once with `✗ Preview stopped: the preview server sent an invalid reply. Update the CLI or try again later.` (exit 1) instead of reconnecting every few seconds indefinitely. The screen names the upgrade command and `vincentt preview`.
27
+ - `vincentt publish` refuses the whole publish, listing the files, when a folder in the build output turns into a link to somewhere outside it while publish reads it. Let the build finish, then publish again.
28
+ - `vincentt publish` refuses to upload when the platform's upload link does not carry the checksum publish computed for that file; nothing is sent, and the screen names the file.
29
+ - A folder or file in the build output that publish cannot read now ends the publish with the "changed while publish was reading it" screen naming the path inside the build output, instead of a raw error message holding the full path.
30
+ - File names printed on publish refusal screens show bidirectional and direction-mark characters as visible `\uXXXX` escapes.
31
+
6
32
  ## 0.21.0
7
33
 
8
34
  required: false
package/dist/cli/args.js CHANGED
@@ -42,6 +42,8 @@ export const BOOLEAN_FLAGS = new Set([
42
42
  "--yes",
43
43
  "--download",
44
44
  "--no-context",
45
+ "--selection",
46
+ "--status",
45
47
  "--help",
46
48
  "-h",
47
49
  ]);
package/dist/cli/index.js CHANGED
@@ -29,7 +29,7 @@ import { listProjectsCommand } from "../lib/listProjects.js";
29
29
  import { initCommand } from "../lib/initCommand.js";
30
30
  import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
31
31
  import { whoamiCommand } from "../lib/whoami.js";
32
- import { renderVersion, versionReport } from "../lib/version.js";
32
+ import { renderVersion, VERSION_ACCOUNT_TIMEOUT_MS, versionReport, } from "../lib/version.js";
33
33
  import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, projectCreate, } from "../lib/lifecycle.js";
34
34
  import { publishCommand } from "../publish/command.js";
35
35
  import { stdinAsk, stdinConfirm } from "../publish/prompt.js";
@@ -53,7 +53,7 @@ import * as feedbackCopy from "../lib/feedbackCopy.js";
53
53
  import { record as recordInvocation } from "../dx/journal.js";
54
54
  import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, sinceUsageLines, } from "../dx/command.js";
55
55
  import { httpConsentBackend, runConsentInBrowser } from "../dx/consent.js";
56
- import { DX_UNATTENDED_SUNSET, loadEnablement } from "../dx/enablement.js";
56
+ import { DX_UNATTENDED_SUNSET, loadEnablement, resolveConsentView, } from "../dx/enablement.js";
57
57
  import { journalEnabled, journalPath, readJournal } from "../dx/journal.js";
58
58
  import { postReport } from "../dx/send.js";
59
59
  import { nudgeFor } from "../dx/nudge.js";
@@ -1139,14 +1139,22 @@ function cliVersion() {
1139
1139
  }
1140
1140
  }
1141
1141
  async function version() {
1142
+ const now = Date.now();
1142
1143
  const report = await versionReport(projectDir(), {
1143
1144
  cliVersion: cliVersion(),
1144
1145
  allowCustomApi: has("--allow-custom-api"),
1146
+ consent: {
1147
+ auth: dxAuth,
1148
+ readAccount: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now, VERSION_ACCOUNT_TIMEOUT_MS),
1149
+ clearLocal: () => writeDxEnablement(undefined),
1150
+ },
1145
1151
  });
1146
1152
  for (const line of renderVersion(report))
1147
1153
  console.log(line);
1148
1154
  }
1149
1155
  // ---- dx ---------------------------------------------------------------------
1156
+ /** The account bound on `vincentt dx` (f7 Scenario 3c); the send gate uses the default. */
1157
+ const DX_ACCOUNT_TIMEOUT_MS = 3_000;
1150
1158
  /**
1151
1159
  * The API host + PAT for a dx call, or undefined when signed out.
1152
1160
  *
@@ -1197,6 +1205,7 @@ async function dx() {
1197
1205
  onProgress: (l) => console.log(l),
1198
1206
  });
1199
1207
  },
1208
+ grantAccountConsent: (auth) => consent.grant(auth.apiUrl, auth.pat, now),
1200
1209
  revokeAccountConsent: (auth) => consent.revoke(auth.apiUrl, auth.pat),
1201
1210
  };
1202
1211
  process.exitCode = options.enable
@@ -1214,7 +1223,15 @@ async function dx() {
1214
1223
  // relaxation, not throw through it.
1215
1224
  const enablement = await loadEnablement(now);
1216
1225
  if (!options.send) {
1217
- process.exitCode = runDx(argv, (l) => console.log(l), () => enablement);
1226
+ // The report tail and `dxConsent` read the account first, bounded at 3 s.
1227
+ const view = await resolveConsentView({
1228
+ auth: await dxAuth(),
1229
+ readAccount: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now, DX_ACCOUNT_TIMEOUT_MS),
1230
+ local: () => enablement,
1231
+ clearLocal: () => writeDxEnablement(undefined),
1232
+ timeoutMs: DX_ACCOUNT_TIMEOUT_MS,
1233
+ });
1234
+ process.exitCode = runDx(argv, (l) => console.log(l), () => view.local, () => view);
1218
1235
  return;
1219
1236
  }
1220
1237
  // `--send` is the one path out of this machine, so it resolves auth the way
@@ -1231,10 +1248,9 @@ async function dx() {
1231
1248
  confirm: (q) => stdinConfirm()(`${q} [y/N] `, false),
1232
1249
  interactive,
1233
1250
  enablement: () => enablement,
1234
- // The third arm: the account-wide grant, read from /me. Consulted only when the
1235
- // first two have not already allowed the send, and allowed to throw — a /me that
1236
- // is unreachable falls back to the other two arms rather than blocking.
1251
+ // The account decides when it answers; never read on an interactive session.
1237
1252
  accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
1253
+ clearEnablement: () => writeDxEnablement(undefined),
1238
1254
  // `--dir`-or-cwd, the same resolution every other verb uses, so `--dir`
1239
1255
  // works on a send as it does everywhere else.
1240
1256
  cwd: projectDir(),
@@ -1276,6 +1292,7 @@ async function report() {
1276
1292
  interactive,
1277
1293
  enablement: () => enablement,
1278
1294
  accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
1295
+ clearEnablement: () => writeDxEnablement(undefined),
1279
1296
  // `--dir`-or-cwd, the same resolution every other verb uses, so `--dir`
1280
1297
  // works on a report as it does everywhere else.
1281
1298
  cwd: projectDir(),
@@ -1429,6 +1446,25 @@ async function domain() {
1429
1446
  yes: has("--yes"),
1430
1447
  });
1431
1448
  }
1449
+ // ---- Vincentt Editor (f21) --------------------------------------------------
1450
+ /** Why `editor` exited 1, for the journal (one word from a closed set, B-F7-1). */
1451
+ let editorReason;
1452
+ /**
1453
+ * `vincentt editor` hands the project folder to the installed Vincentt Editor app and exits.
1454
+ * Needs no account and makes no network call.
1455
+ */
1456
+ async function editor() {
1457
+ const { editorVerb } = await import("../editor/launcher.js");
1458
+ // The launcher's parser accepts `--dir=<path>`, so the folder it names is the one handed over.
1459
+ const dir = flag("--dir") ?? valueOfEqualsFlag(argv, "--dir");
1460
+ const run = await editorVerb(argv.slice(1), dir ? path.resolve(dir) : process.cwd());
1461
+ for (const l of run.stdout)
1462
+ console.log(l);
1463
+ for (const l of run.stderr)
1464
+ console.error(l);
1465
+ editorReason = run.reason;
1466
+ process.exitCode = run.exitCode;
1467
+ }
1432
1468
  // ---- servers ----------------------------------------------------------------
1433
1469
  async function relay() {
1434
1470
  const { startRelay } = await import("../relay/server.js");
@@ -1532,6 +1568,8 @@ async function main() {
1532
1568
  return outdated();
1533
1569
  case "domain":
1534
1570
  return domain();
1571
+ case "editor":
1572
+ return editor();
1535
1573
  }
1536
1574
  }
1537
1575
  // Every invocation is journaled here, at the one site that sees all of them —
@@ -1539,6 +1577,8 @@ async function main() {
1539
1577
  // command. Recording is best-effort and local-only (see dx/journal.ts); it never
1540
1578
  // changes what the CLI does or what it exits with.
1541
1579
  const startedAt = Date.now();
1580
+ /** Verbs that make no network request, footnotes included: their footnotes never refresh. */
1581
+ const NO_NETWORK_VERBS = new Set(["editor"]);
1542
1582
  function journal(exitCode, error) {
1543
1583
  recordInvocation({
1544
1584
  argv,
@@ -1548,6 +1588,7 @@ function journal(exitCode, error) {
1548
1588
  endedAt: Date.now(),
1549
1589
  exitCode,
1550
1590
  error,
1591
+ reason: editorReason,
1551
1592
  });
1552
1593
  // The hint goes out AFTER the run it is about, so it reads as a footnote to
1553
1594
  // what just happened rather than a warning about what is about to. Never on
@@ -1580,7 +1621,7 @@ function journal(exitCode, error) {
1580
1621
  for (const l of updateNotice(cliVersion(), cached?.latest))
1581
1622
  console.error(l);
1582
1623
  }
1583
- if (!isFresh(cached, Date.now()))
1624
+ if (!isFresh(cached, Date.now()) && !NO_NETWORK_VERBS.has(verbOf(argv)))
1584
1625
  refreshUpdateCache();
1585
1626
  }
1586
1627
  catch {
@@ -1625,7 +1666,8 @@ async function announceBehind() {
1625
1666
  console.error(l);
1626
1667
  }
1627
1668
  }
1628
- if (needsRefresh(loadReleaseCache(), Date.now())) {
1669
+ if (needsRefresh(loadReleaseCache(), Date.now()) &&
1670
+ !NO_NETWORK_VERBS.has(verbOf(argv))) {
1629
1671
  refreshReleaseCache({
1630
1672
  apiUrl: machineApiUrl(await loadMachineConfig()),
1631
1673
  now: () => Date.now(),
@@ -60,7 +60,16 @@ export const USAGE_TEXT = `Vincentt CLI: the local-first AR dev loop\n\n` +
60
60
  ` [--no-qr] [--stop]\n` +
61
61
  ` Runs dev.command and forwards dev.port from vincentt.json,\n` +
62
62
  ` else npm run dev on a free port (passed as PORT).\n` +
63
- ` Re-running prints the live link; --restart mints a new one.\n\n` +
63
+ ` Re-running prints the live link; --restart mints a new one.\n` +
64
+ ` editor Hand this project to the Vincentt Editor app (macOS 14 or\n` +
65
+ ` later) and exit. Opens only the app in /Applications or\n` +
66
+ ` ~/Applications, signed by Vincentt. No account needed.\n` +
67
+ ` [--json] one vincentt.editor-open/1 object; handedOff says\n` +
68
+ ` the folder reached the app, not that it opened.\n` +
69
+ ` editor --selection The object selected in the editor, with its source\n` +
70
+ ` location and values [--json]\n` +
71
+ ` editor --status --json The editor's state for this folder, including\n` +
72
+ ` app.lastOpen. Both reads exit 1 when no editor runs here.\n\n` +
64
73
  // f16. HELP TEACHES THE CANONICAL VERBS ONLY: `add` and `remove` are accepted
65
74
  // as silent aliases and are deliberately ABSENT here, because help, docs and
66
75
  // every error string are where the vocabulary is actually taught. Listing the
@@ -1,4 +1,4 @@
1
- export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects", "report", "outdated", "domain"];
1
+ export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects", "report", "outdated", "domain", "editor"];
2
2
  export type PinnedVerb = (typeof PINNED_VERBS)[number];
3
3
  export declare const VERB_HANDLERS: {
4
4
  readonly version: true;
@@ -22,6 +22,7 @@ export declare const VERB_HANDLERS: {
22
22
  readonly report: true;
23
23
  readonly outdated: true;
24
24
  readonly domain: true;
25
+ readonly editor: true;
25
26
  };
26
27
  export type DispatchVerb = keyof typeof VERB_HANDLERS;
27
28
  export declare function resolveVerb(cmd: string | undefined): DispatchVerb | undefined;
@@ -26,6 +26,10 @@ export const PINNED_VERBS = [
26
26
  // collection verb. `domains` is routed to an answer, not accepted as a verb —
27
27
  // see domainVerbRedirect. An addition, so B-F3-8 permits it.
28
28
  "domain",
29
+ // Appended (f21): the Vincentt Editor launcher. `editor`, not `edit`: an agent
30
+ // reading "vincentt edit" would take it as a command that changes files. An
31
+ // addition, so B-F3-8 permits it.
32
+ "editor",
29
33
  ];
30
34
  // The dispatch's verb set, as data. `main()` switches over this, so the two can
31
35
  // never disagree: a verb added to the CLI is a key added here, and QA-AHG-01
@@ -52,6 +56,7 @@ export const VERB_HANDLERS = {
52
56
  report: true,
53
57
  outdated: true,
54
58
  domain: true,
59
+ editor: true,
55
60
  };
56
61
  // Answers "is this a verb this CLI accepts" without running it. The
57
62
  // console-redirect strings are NOT accepted here: they reach their explanation
@@ -1,5 +1,5 @@
1
- import type { ConsentDecision } from "./consent.js";
2
- import type { Enablement } from "./enablement.js";
1
+ import type { ConsentDecision, GrantResult } from "./consent.js";
2
+ import type { AccountRead, ConsentView, Enablement } from "./enablement.js";
3
3
  import type { JournalEntry, JournalWindow } from "./journal.js";
4
4
  import type { WireReport } from "./send.js";
5
5
  import { type BuiltWith } from "../platform/resolve.js";
@@ -11,10 +11,15 @@ export interface DxDeps {
11
11
  enabled: () => boolean;
12
12
  path: () => string;
13
13
  /**
14
- * Durable unattended-send state. Optional so the many callers that only read
15
- * the journal need not resolve it; absent reads as not enabled.
14
+ * This machine's copy of the grant (the local flag). Optional so the many callers that
15
+ * only read the journal need not resolve it; absent reads as not enabled.
16
16
  */
17
17
  enablement?: () => Enablement;
18
+ /**
19
+ * The account-first answer the report tail and `dxConsent` show. Absent falls back to
20
+ * the local flag, reported as checked on this machine only.
21
+ */
22
+ consentView?: () => ConsentView;
18
23
  }
19
24
  /** How far back a report looks. */
20
25
  export interface DxWindow {
@@ -53,7 +58,7 @@ export declare function sinceUsageLines(raw: string): string[];
53
58
  export declare function parseDxOptions(args: string[]): DxOptions;
54
59
  export declare function dxCommand(deps: DxDeps, options: DxOptions): number;
55
60
  /** Wire the command to the real journal. */
56
- export declare function runDx(args: string[], print: (line: string) => void, enablement?: () => Enablement): number;
61
+ export declare function runDx(args: string[], print: (line: string) => void, enablement?: () => Enablement, consentView?: () => ConsentView): number;
57
62
  export interface DxEnableDeps {
58
63
  print: (line: string) => void;
59
64
  now: () => number;
@@ -89,6 +94,14 @@ export interface DxEnableDeps {
89
94
  * no socket, no browser, and no clock to test.
90
95
  */
91
96
  consentInBrowser?: () => Promise<ConsentDecision>;
97
+ /**
98
+ * POST /me/dx-consent with the PAT, on the terminal path. Rejects only with the version
99
+ * refusal or the sign-in error; every other outcome is a GrantResult.
100
+ */
101
+ grantAccountConsent?: (auth: {
102
+ apiUrl: string;
103
+ pat: string;
104
+ }) => Promise<GrantResult>;
92
105
  /**
93
106
  * DELETE /me/dx-consent. Used only by `disable`, and allowed to throw — the caller
94
107
  * treats a network failure as "local revoke still stands", never as a reason to leave
@@ -103,11 +116,13 @@ export interface DxEnableDeps {
103
116
  * `vincentt dx enable` — grant durable permission to send DX reports without asking
104
117
  * first.
105
118
  *
106
- * TWO PATHS, and which one runs is decided by whether there is a terminal to ask in:
119
+ * TWO PATHS to ONE account grant, and which one runs is decided by whether there is a
120
+ * terminal to ask in:
107
121
  *
108
- * - At a TTY: the original terminal prompt, unchanged. A person is already here and
109
- * can read the disclosure where they stand; sending them to a browser would be
110
- * ceremony.
122
+ * - At a TTY: the terminal prompt. A person is already here and can read the
123
+ * disclosure where they stand; sending them to a browser would be ceremony. On `y`
124
+ * the CLI POSTs /me/dx-consent with the PAT, and writes the local flag only after
125
+ * the account recorded the grant.
111
126
  * - With no TTY: the BROWSER path. This is the case the feature exists for — a
112
127
  * creator driving the CLI through a coding agent has no terminal, so the old code
113
128
  * refused and the flow dead-ended. It now opens the console, where a human can
@@ -130,8 +145,8 @@ export declare function dxEnableCommand(deps: DxEnableDeps, sunset: number): Pro
130
145
  * Clears BOTH local keys AND revokes the account grant when there is a PAT to do it
131
146
  * with. Both halves are needed: a creator who runs `disable` means "stop sending", and
132
147
  * clearing only the local flag would leave the account grant standing, so the very next
133
- * `--send` would be authorized by arm (c) and the command would appear not to have
134
- * worked.
148
+ * `--send` would be authorized by the account and the command would appear not to
149
+ * have worked.
135
150
  *
136
151
  * ORDER IS DELIBERATE: local first, network second. The local clear is the one that
137
152
  * cannot fail and the one that stops THIS machine, which is the machine the creator is
@@ -168,18 +183,19 @@ export interface DxSendDeps extends DxDeps {
168
183
  * convenience, not authorization.
169
184
  */
170
185
  interactive: () => boolean;
171
- /** Durable unattended-send state, read strictly. Absent reads as not enabled. */
186
+ /** This machine's flag, read strictly. Absent reads as not enabled. */
172
187
  enablement?: () => Enablement;
173
188
  /**
174
- * The ACCOUNT's consent, read from GET /me. The third arm of the gate.
175
- *
176
- * Allowed to throw, and its throw is not fatal — see the gate below. Absent (the dep
177
- * not wired at all) reads as not consented, never as "unknown, so allow".
189
+ * The ACCOUNT's consent, read from GET /me. Allowed to throw; a throw reads as
190
+ * unreadable. Absent reads as unreadable too: it can only let a valid local flag stand,
191
+ * never authorize on its own.
178
192
  */
179
193
  accountConsent?: (auth: {
180
194
  apiUrl: string;
181
195
  pat: string;
182
- }) => Promise<Enablement>;
196
+ }) => Promise<AccountRead>;
197
+ /** Clears this machine's flag when the account answers "not granted". */
198
+ clearEnablement?: () => Promise<unknown>;
183
199
  /**
184
200
  * The version set. Injectable so a fixed set — or a REJECTING promise, the arm
185
201
  * proving a fault omits — can be driven with no filesystem. Defaults to the one
@@ -188,6 +204,46 @@ export interface DxSendDeps extends DxDeps {
188
204
  builtWith?: () => Promise<BuiltWith | undefined>;
189
205
  cwd?: string;
190
206
  }
207
+ /** What the send gate needs. `dx --send` and `report` both satisfy it. */
208
+ export interface SendGateDeps {
209
+ interactive: () => boolean;
210
+ enablement?: () => Enablement;
211
+ accountConsent?: (auth: {
212
+ apiUrl: string;
213
+ pat: string;
214
+ }) => Promise<AccountRead>;
215
+ clearEnablement?: () => Promise<unknown>;
216
+ }
217
+ export type SendGate = {
218
+ arm: "interactive";
219
+ } | {
220
+ arm: "unattended";
221
+ enablement: Enablement;
222
+ } | {
223
+ arm: "refused";
224
+ };
225
+ /**
226
+ * The one consent gate both send paths call (f7 Scenario 3), checked in order:
227
+ *
228
+ * 1. an interactive session → ask, then send. No /me read at all, so a creator at their
229
+ * own terminal is never blocked by an API being slow or down.
230
+ * 2. signed in and GET /me answers: a strict grant sends (source: account); any other
231
+ * 200 clears this machine's flag and refuses.
232
+ * 3. /me cannot be read and this machine's flag is valid → send (source: machine).
233
+ *
234
+ * An unreadable /me never authorizes by itself; it only lets an existing local flag stand.
235
+ */
236
+ export declare function gateSend(deps: SendGateDeps, auth: {
237
+ apiUrl: string;
238
+ pat: string;
239
+ } | undefined): Promise<SendGate>;
240
+ /**
241
+ * The gate's refusal. It NAMES the enabling command: a refusal is an answer to a
242
+ * command the caller typed, which makes it SOLICITED output — the prohibition is on
243
+ * UNBIDDEN output instructing a caller. A refusal naming nothing leaves a human at
244
+ * exit 1 with no path forward, for no security gain.
245
+ */
246
+ export declare const SEND_REFUSAL_LINES: readonly string[];
191
247
  /**
192
248
  * `vincentt dx --send`.
193
249
  *
@@ -195,6 +251,6 @@ export interface DxSendDeps extends DxDeps {
195
251
  * who cannot see what leaves their machine before it leaves has not agreed to
196
252
  * anything:
197
253
  *
198
- * friction → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
254
+ * friction → auth → GATE → RESOLVE → build → SHOW → ask → post
199
255
  */
200
256
  export declare function dxSendCommand(deps: DxSendDeps, options: DxOptions): Promise<number>;