@vincentt-xr/cli 0.8.0 → 0.10.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 (64) hide show
  1. package/README.md +40 -13
  2. package/dist/cli/args.d.ts +22 -0
  3. package/dist/cli/args.js +71 -0
  4. package/dist/cli/index.d.ts +1 -1
  5. package/dist/cli/index.js +359 -91
  6. package/dist/cli/usage-text.d.ts +1 -0
  7. package/dist/cli/usage-text.js +97 -0
  8. package/dist/cli/verb-surface.d.ts +24 -0
  9. package/dist/cli/verb-surface.js +49 -0
  10. package/dist/dx/clusters.d.ts +0 -17
  11. package/dist/dx/clusters.js +50 -1
  12. package/dist/dx/skeleton.js +1 -0
  13. package/dist/lib/backend.d.ts +11 -0
  14. package/dist/lib/backend.js +23 -0
  15. package/dist/lib/classifyTarget.d.ts +42 -0
  16. package/dist/lib/classifyTarget.js +83 -0
  17. package/dist/lib/feedback.d.ts +46 -5
  18. package/dist/lib/feedback.js +76 -19
  19. package/dist/lib/feedbackCopy.d.ts +70 -0
  20. package/dist/lib/feedbackCopy.js +163 -0
  21. package/dist/lib/initCommand.d.ts +28 -1
  22. package/dist/lib/initCommand.js +91 -16
  23. package/dist/lib/initFrom.d.ts +70 -0
  24. package/dist/lib/initFrom.js +222 -0
  25. package/dist/lib/lifecycle.d.ts +28 -3
  26. package/dist/lib/lifecycle.js +99 -7
  27. package/dist/lib/listProjects.d.ts +41 -0
  28. package/dist/lib/listProjects.js +77 -0
  29. package/dist/lib/projects.d.ts +64 -8
  30. package/dist/lib/projects.js +316 -24
  31. package/dist/lib/sourceRef.d.ts +46 -0
  32. package/dist/lib/sourceRef.js +107 -0
  33. package/dist/lib/sourceRefCopy.d.ts +30 -0
  34. package/dist/lib/sourceRefCopy.js +116 -0
  35. package/dist/preview/command.d.ts +11 -0
  36. package/dist/preview/command.js +19 -1
  37. package/dist/preview/copy.d.ts +7 -0
  38. package/dist/preview/copy.js +8 -1
  39. package/dist/preview/runner.d.ts +6 -0
  40. package/dist/preview/runner.js +1 -0
  41. package/dist/preview/stream.d.ts +11 -0
  42. package/dist/preview/stream.js +13 -0
  43. package/dist/publish/api.d.ts +20 -0
  44. package/dist/publish/command.d.ts +3 -0
  45. package/dist/publish/command.js +39 -4
  46. package/dist/publish/copy.d.ts +29 -0
  47. package/dist/publish/copy.js +86 -0
  48. package/dist/relay/annotations.d.ts +28 -4
  49. package/dist/relay/annotations.js +23 -7
  50. package/dist/relay/qa.f12.fixtures.d.ts +154 -0
  51. package/dist/relay/qa.f12.fixtures.js +358 -0
  52. package/dist/relay/server.d.ts +41 -3
  53. package/dist/relay/server.js +90 -32
  54. package/dist/scaffold/index.d.ts +9 -0
  55. package/dist/scaffold/index.js +13 -1
  56. package/package.json +2 -2
  57. package/dist/library/qa.f5-b32.spec.d.ts +0 -1
  58. package/dist/library/qa.f5-b32.spec.js +0 -202
  59. package/dist/library/qa.f5-cli.spec.d.ts +0 -1
  60. package/dist/library/qa.f5-cli.spec.js +0 -433
  61. package/dist/library/qa.f5-contract.spec.d.ts +0 -1
  62. package/dist/library/qa.f5-contract.spec.js +0 -206
  63. package/dist/relay/injectors.d.ts +0 -29
  64. package/dist/relay/injectors.js +0 -72
@@ -0,0 +1,70 @@
1
+ /** Flow C1 — the two lines before the park. */
2
+ export declare function waiting(): string[];
3
+ /** Flow C6 — the opening line of a watch. */
4
+ export declare function watching(): string[];
5
+ /**
6
+ * Flow C2 — the ending of a drain. A count and nothing else: a fact about what
7
+ * this process did, naming no next step.
8
+ *
9
+ * It exists because a human running this in their own terminal otherwise reads
10
+ * three dense JSON lines and cannot tell whether the command is finished. It is
11
+ * deliberately NOT one line per annotation, which would duplicate the preview
12
+ * stream's arrival line in a second place and triple the stderr noise of the
13
+ * exact case this feature exists for.
14
+ */
15
+ export declare function handedOver(n: number): string[];
16
+ /**
17
+ * Flow C3 — the bounded window elapsed. One line, one integer, no glyph.
18
+ *
19
+ * `The preview is still live` is the load-bearing half: it is the fact that
20
+ * makes exit 64 actionable without an instruction. The window is named with its
21
+ * number because an unexplained stop is worse than the calibration.
22
+ */
23
+ export declare function windowElapsed(timeoutMs: number): string[];
24
+ /**
25
+ * Flow C4 — the session ended. `✗`, because this one stops: 65 is not a fault,
26
+ * but it is the end of a channel, and the glyph separates it from 64 at a glance
27
+ * in scrollback.
28
+ *
29
+ * The last line answers the creator's actual fear ("did my annotations die with
30
+ * the preview?") before they can ask it. It does NOT say to start a preview —
31
+ * that precondition is stated in `--help`, which is where a recipe reads it.
32
+ *
33
+ * `0 already handed over` reads slightly oddly and is the price of one sentence
34
+ * covering both counts; suppressing the line at zero would make the ending's
35
+ * shape depend on the count and give a reader two endings to learn.
36
+ */
37
+ export declare function sessionEnded(handed: number): string[];
38
+ /**
39
+ * The triage detail behind a 65, one flag away.
40
+ *
41
+ * The two paths print IDENTICALLY by default: a polling host reaches 65 on every
42
+ * re-park once a preview has ended, and a creator cannot act on the difference
43
+ * between a graceful teardown and a killed process. The distinction belongs to
44
+ * whoever is diagnosing, not to the hot path.
45
+ */
46
+ export declare function sessionEndedDetail(reason: "signalled" | "unreachable", tree: string, cursor: number): string[];
47
+ /** Flow C6 — a human stopped a watch. The preview is not what ended. */
48
+ export declare function stoppedWatching(): string[];
49
+ /** Flow C5 — the usage error. */
50
+ export declare const USAGE = "Usage: vincentt feedback --wait | --watch [--timeout <seconds>]";
51
+ /**
52
+ * Flow C6's human render, on stderr, while stdout keeps the exact NDJSON. One
53
+ * observation, two fidelities — the rule that already governs the perf stream.
54
+ *
55
+ * Columns are the preview stream's columns (time, then the tester, then the
56
+ * content), so a creator who has read a preview stream reads this with nothing
57
+ * new to learn. The screenshot path is its own line, unadorned, so it is
58
+ * copy-clean. NOT rendered: id, seq, projectDir — a human can act on none of
59
+ * them.
60
+ */
61
+ export interface WatchEntry {
62
+ createdAt: number;
63
+ tester?: string;
64
+ message: string;
65
+ screenshotPath: string;
66
+ }
67
+ export declare function watchEntry(e: WatchEntry, columns: number | undefined): string[];
68
+ export declare function stripControl(s: string): string;
69
+ /** Hard wrap at `width`, breaking on spaces where one is available. */
70
+ export declare function wrap(s: string, width: number): string[];
@@ -0,0 +1,163 @@
1
+ // Every line `vincentt feedback` writes to STDERR (Flows C1-C6). Pure functions
2
+ // returning string arrays, so the transcripts are assertable without a terminal.
3
+ //
4
+ // The verb has two readers on two streams and they are different audiences:
5
+ // stdout is a MACHINE (one JSON annotation per line, nothing else, ever) and
6
+ // stderr is a HUMAN. Nothing in this file may ever be written to stdout.
7
+ //
8
+ // Two rules govern all of it:
9
+ // - UNBIDDEN OUTPUT STATES A FACT, NEVER A COMMAND. `--wait` prints on its own
10
+ // initiative into what is often an agent's input, so no line here names
11
+ // something to run. Every ending states what is true about the world
12
+ // ("The preview is still live", "The preview ended") where the CLI's older
13
+ // verbs would have ended with an instruction.
14
+ // - An exit code that means "retry is correct" carries NO GLYPH. 64 is neither
15
+ // a failure nor a success: `✗` would teach a polling host's reader that the
16
+ // normal case looks broken, and `✓` would claim something happened.
17
+ //
18
+ // The annotation's own content is never echoed here on the `--wait` path. It is
19
+ // viewer-authored and hostile, and the reader is a parked consumer that is about
20
+ // to receive the whole record on stdout anyway.
21
+ /** Flow C1 — the two lines before the park. */
22
+ export function waiting() {
23
+ return [
24
+ " Waiting for feedback from the preview in this folder.",
25
+ // The verb is ambiguous on first contact — `feedback` could plausibly mean
26
+ // "send feedback to Vincentt", which is `vincentt dx`. This disambiguates by
27
+ // naming DIRECTION rather than by issuing an instruction.
28
+ " Nothing is sent from here; this only reads what your phone sends.",
29
+ ];
30
+ }
31
+ /** Flow C6 — the opening line of a watch. */
32
+ export function watching() {
33
+ return [" Watching for feedback from the preview in this folder. Ctrl-C to stop."];
34
+ }
35
+ /**
36
+ * Flow C2 — the ending of a drain. A count and nothing else: a fact about what
37
+ * this process did, naming no next step.
38
+ *
39
+ * It exists because a human running this in their own terminal otherwise reads
40
+ * three dense JSON lines and cannot tell whether the command is finished. It is
41
+ * deliberately NOT one line per annotation, which would duplicate the preview
42
+ * stream's arrival line in a second place and triple the stderr noise of the
43
+ * exact case this feature exists for.
44
+ */
45
+ export function handedOver(n) {
46
+ return ["", ` ${n} handed over.`, ""];
47
+ }
48
+ /**
49
+ * Flow C3 — the bounded window elapsed. One line, one integer, no glyph.
50
+ *
51
+ * `The preview is still live` is the load-bearing half: it is the fact that
52
+ * makes exit 64 actionable without an instruction. The window is named with its
53
+ * number because an unexplained stop is worse than the calibration.
54
+ */
55
+ export function windowElapsed(timeoutMs) {
56
+ return [
57
+ "",
58
+ ` No feedback in the last ${windowLabel(timeoutMs)}. The preview is still live.`,
59
+ "",
60
+ ];
61
+ }
62
+ /** Whole minutes read as minutes; anything else reads as seconds. */
63
+ function windowLabel(timeoutMs) {
64
+ const seconds = Math.max(Math.round(timeoutMs / 1000), 0);
65
+ if (seconds >= 60 && seconds % 60 === 0) {
66
+ const minutes = seconds / 60;
67
+ return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
68
+ }
69
+ return `${seconds} ${seconds === 1 ? "second" : "seconds"}`;
70
+ }
71
+ /**
72
+ * Flow C4 — the session ended. `✗`, because this one stops: 65 is not a fault,
73
+ * but it is the end of a channel, and the glyph separates it from 64 at a glance
74
+ * in scrollback.
75
+ *
76
+ * The last line answers the creator's actual fear ("did my annotations die with
77
+ * the preview?") before they can ask it. It does NOT say to start a preview —
78
+ * that precondition is stated in `--help`, which is where a recipe reads it.
79
+ *
80
+ * `0 already handed over` reads slightly oddly and is the price of one sentence
81
+ * covering both counts; suppressing the line at zero would make the ending's
82
+ * shape depend on the count and give a reader two endings to learn.
83
+ */
84
+ export function sessionEnded(handed) {
85
+ return [
86
+ "",
87
+ "✗ The preview ended. Nothing more can arrive on it.",
88
+ "",
89
+ ` Nothing was missed — the ${handed} already handed over stay handed over.`,
90
+ "",
91
+ ];
92
+ }
93
+ /**
94
+ * The triage detail behind a 65, one flag away.
95
+ *
96
+ * The two paths print IDENTICALLY by default: a polling host reaches 65 on every
97
+ * re-park once a preview has ended, and a creator cannot act on the difference
98
+ * between a graceful teardown and a killed process. The distinction belongs to
99
+ * whoever is diagnosing, not to the hot path.
100
+ */
101
+ export function sessionEndedDetail(reason, tree, cursor) {
102
+ const which = reason === "signalled"
103
+ ? "The preview signalled it was ending"
104
+ : "The relay stopped answering (no teardown signal)";
105
+ return [` ${which} · ${tree}`, ` Cursor unchanged at ${cursor}.`, ""];
106
+ }
107
+ /** Flow C6 — a human stopped a watch. The preview is not what ended. */
108
+ export function stoppedWatching() {
109
+ return ["", " Stopped watching. The preview is still live.", ""];
110
+ }
111
+ /** Flow C5 — the usage error. */
112
+ export const USAGE = "Usage: vincentt feedback --wait | --watch [--timeout <seconds>]";
113
+ /** Content indent + the time column + its gutter — where the tester starts. */
114
+ const TESTER_COL = 9;
115
+ export function watchEntry(e, columns) {
116
+ const time = clockLabel(e.createdAt);
117
+ const tester = e.tester ?? "";
118
+ const head = ` ${time} ${tester}`;
119
+ const gutter = " ".repeat(TESTER_COL);
120
+ // The message is viewer-authored and hostile. A raw `\n` or an ANSI escape in
121
+ // it must not forge a second entry or repaint the screen, so control
122
+ // characters are STRIPPED (not escaped — this stream is for reading) before
123
+ // the text is wrapped. stdout's equivalent protection is JSON.stringify.
124
+ const width = wrapWidth(columns);
125
+ const indent = " ".repeat(head.length + 2);
126
+ const body = wrap(stripControl(e.message), Math.max(width - indent.length, 20));
127
+ const out = [`${head} ${body[0] ?? ""}`];
128
+ for (const line of body.slice(1))
129
+ out.push(`${indent}${line}`);
130
+ out.push(`${gutter}${stripControl(e.screenshotPath)}`);
131
+ return out;
132
+ }
133
+ function clockLabel(createdAt) {
134
+ const d = new Date(createdAt);
135
+ if (Number.isNaN(d.getTime()))
136
+ return "--:--";
137
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
138
+ }
139
+ function wrapWidth(columns) {
140
+ return columns !== undefined && columns >= 40 ? columns : 80;
141
+ }
142
+ /**
143
+ * C0 and C1 controls out, including ESC, CR, LF, NUL and the DEL range. A
144
+ * replacement character would still occupy a cell for something the tester did
145
+ * not type, so they are removed rather than substituted.
146
+ */
147
+ const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/gu;
148
+ export function stripControl(s) {
149
+ return s.replace(CONTROL_RE, "");
150
+ }
151
+ /** Hard wrap at `width`, breaking on spaces where one is available. */
152
+ export function wrap(s, width) {
153
+ const out = [];
154
+ let rest = s;
155
+ while (rest.length > width) {
156
+ const cut = rest.lastIndexOf(" ", width);
157
+ const at = cut > 0 ? cut : width;
158
+ out.push(rest.slice(0, at));
159
+ rest = rest.slice(cut > 0 ? at + 1 : at);
160
+ }
161
+ out.push(rest);
162
+ return out;
163
+ }
@@ -1,8 +1,10 @@
1
+ import type { RegisterDeps } from "./initFrom.js";
2
+ import { type SourceRef } from "./sourceRef.js";
1
3
  import type { Ask, Confirm } from "../publish/prompt.js";
2
4
  export interface InitDeps {
3
5
  print: (line: string) => void;
4
6
  cwd: string;
5
- scaffold?: (targetDir: string, ref?: string) => Promise<void>;
7
+ scaffold?: (targetDir: string, opts: ScaffoldCall) => Promise<void>;
6
8
  install?: (targetDir: string) => Promise<string>;
7
9
  isEmpty?: (targetDir: string) => Promise<boolean>;
8
10
  /** Free-text prompt. Absent → not a TTY, so nothing may be asked. */
@@ -11,6 +13,23 @@ export interface InitDeps {
11
13
  confirm?: Confirm;
12
14
  /** Non-empty check for the resolved target; injected in tests. */
13
15
  isNonEmpty?: (targetDir: string) => Promise<boolean>;
16
+ /**
17
+ * The registration half of `--from`. Injected whole so the combined path is
18
+ * drivable without a real API, a real browser, or a real HOME.
19
+ */
20
+ register?: (deps: Pick<RegisterDeps, "print" | "targetDir" | "dirLabel" | "ask">, opts: {
21
+ name?: string;
22
+ slug?: string;
23
+ }) => Promise<number>;
24
+ }
25
+ /**
26
+ * What `InitDeps.scaffold` is called with. An OBJECT rather than a positional
27
+ * ref: `--from` needs to name a repo too, and a positional-to-object change
28
+ * compiles silently in a fake that ignores its second argument.
29
+ */
30
+ export interface ScaffoldCall {
31
+ repo?: string;
32
+ ref?: string;
14
33
  }
15
34
  /** The folder name offered when `init` is run bare and the creator just hits Enter. */
16
35
  export declare const DEFAULT_APP_NAME = "my-ar-app";
@@ -27,5 +46,13 @@ export interface InitOptions {
27
46
  skipInstall?: boolean;
28
47
  /** Template version to scaffold from; defaults to the latest release. */
29
48
  template?: string;
49
+ /**
50
+ * A gallery source, already parsed. The combined path (f6): clone this repo at
51
+ * this tag, then register the project. NEVER installs dependencies (S-1).
52
+ */
53
+ from?: SourceRef;
54
+ /** `--name` / `--slug`, accepted on `init` only when `--from` is present. */
55
+ name?: string;
56
+ slug?: string;
30
57
  }
31
58
  export declare function initCommand(deps: InitDeps, opts?: InitOptions): Promise<number>;
@@ -1,12 +1,19 @@
1
- // `vincentt init [dir]` — scaffold a Vincentt XR app. NO account, NO network call
2
- // to the API, NO binding written.
1
+ // `vincentt init [dir]` — scaffold a Vincentt XR app. On the BARE path: NO
2
+ // account, NO network call to the API, NO binding written.
3
3
  //
4
4
  // This is the acquisition wedge: local-first is a smaller room, not a wall, so the
5
5
  // template must be reachable without ever touching the auth gate. `create` (which
6
6
  // does need an account) is offered at the end as the next thing AVAILABLE, never as
7
7
  // a requirement.
8
+ //
9
+ // `--from owner/repo@tag` (f6) adds a SECOND path that clones a gallery template
10
+ // and then registers the project, so it does call the API — for its second half,
11
+ // after the source is on disk. The claim above is scoped to the bare path and
12
+ // stays true of it: no flag added to `init` may make the bare path touch the
13
+ // network (A-5.3).
8
14
  import path from "node:path";
9
- import { detectPackageManager, installDependencies, isNonEmptyDir, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
15
+ import { ScaffoldRefError, detectPackageManager, installDependencies, isNonEmptyDir, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
16
+ import { formatSourceRef, templateCloneURL } from "./sourceRef.js";
10
17
  /** The folder name offered when `init` is run bare and the creator just hits Enter. */
11
18
  export const DEFAULT_APP_NAME = "my-ar-app";
12
19
  /**
@@ -81,20 +88,36 @@ export async function initCommand(deps, opts = {}) {
81
88
  // version they have no reason to care about.
82
89
  if (opts.template)
83
90
  deps.print(` · template ${opts.template}`);
91
+ if (opts.from)
92
+ deps.print(` · from ${formatSourceRef(opts.from)}`);
84
93
  const scaffold = deps.scaffold ??
85
- ((dir, ref) => scaffoldFromTemplate(dir, { ref }));
94
+ ((dir, o) => scaffoldFromTemplate(dir, o));
86
95
  try {
87
- await scaffold(target, opts.template);
96
+ await scaffold(target, opts.from
97
+ ? { repo: templateCloneURL(opts.from.owner, opts.from.repo), ref: opts.from.tag }
98
+ : { ref: opts.template });
88
99
  }
89
100
  catch (err) {
101
+ const msg = err instanceof Error ? err.message : String(err);
90
102
  deps.print("");
91
- deps.print(`✗ ${err instanceof Error ? err.message : String(err)}`);
103
+ if (opts.from) {
104
+ printFromScaffoldFailure(deps.print, opts.from, isRefFailure(err, msg));
105
+ return 1;
106
+ }
107
+ deps.print(`✗ ${msg}`);
92
108
  deps.print("");
93
109
  deps.print(" Run `vincentt init` again once that is resolved.");
94
110
  return 1;
95
111
  }
112
+ // S-1: the combined path CLONES AND STOPS. No package manager is spawned, and
113
+ // the refusal is structural — `--from` is rejected in the argument parser
114
+ // alongside any install-enabling flag, so this branch cannot be re-enabled by
115
+ // a caller.
96
116
  let installed = true;
97
- if (!opts.skipInstall) {
117
+ if (opts.from) {
118
+ installed = false;
119
+ }
120
+ else if (!opts.skipInstall) {
98
121
  deps.print(" · installing dependencies");
99
122
  const install = deps.install ?? ((dir) => installDependencies(dir));
100
123
  try {
@@ -110,7 +133,6 @@ export async function initCommand(deps, opts = {}) {
110
133
  const pm = await detectPackageManager(target).catch(() => "npm");
111
134
  deps.print("");
112
135
  deps.print(`✓ Created ${label}`);
113
- deps.print("");
114
136
  const steps = [];
115
137
  // Keyed off the RESOLVED dir, not the flag — a prompted name needs the cd too.
116
138
  if (dir !== ".")
@@ -119,18 +141,71 @@ export async function initCommand(deps, opts = {}) {
119
141
  steps.push([`${pm} install`, "Install dependencies"]);
120
142
  }
121
143
  steps.push([`${pm} run dev`, "Start the local dev server"]);
122
- for (const line of alignedSteps(steps))
123
- deps.print(line);
144
+ if (!opts.from) {
145
+ deps.print("");
146
+ for (const line of alignedSteps(steps))
147
+ deps.print(line);
148
+ deps.print("");
149
+ deps.print(" No account needed. When you're ready to put it online:");
150
+ for (const line of alignedSteps([
151
+ ["vincentt login", "Connect this machine to Vincentt"],
152
+ ["vincentt create", "Register the project and reserve its address"],
153
+ ])) {
154
+ deps.print(line);
155
+ }
156
+ return 0;
157
+ }
158
+ // S-1's whole UX: two sentences, no apology and no threat model. The install
159
+ // appears below as an ordinary step, never as a remedy — a creator should
160
+ // never learn that it is unusual.
161
+ deps.print(" The source is yours. Dependencies are not installed yet.");
162
+ // TRAP 4: the scaffold's success is already in the output, and the exit code
163
+ // still reports the registration half. The `✗` is the last glyph, always.
164
+ const register = deps.register;
165
+ if (!register)
166
+ return 0;
167
+ const code = await register({ print: deps.print, targetDir: target, dirLabel: label, ask: deps.ask }, { name: opts.name, slug: opts.slug });
168
+ if (code !== 0)
169
+ return code;
124
170
  deps.print("");
125
- deps.print(" No account needed. When you're ready to put it online:");
126
- for (const line of alignedSteps([
127
- ["vincentt login", "Connect this machine to Vincentt"],
128
- ["vincentt create", "Register the project and reserve its address"],
129
- ])) {
171
+ deps.print(" Next:");
172
+ for (const line of alignedSteps(steps))
130
173
  deps.print(line);
131
- }
132
174
  return 0;
133
175
  }
176
+ /**
177
+ * The scaffold half's failure copy for `--from`, which branches on WHICH repo
178
+ * was cloned. The shipped "omit --template to use the latest release" advice is
179
+ * right for the built-in template and meaningless for a gallery source.
180
+ *
181
+ * Nothing was written — no `else` — because there is no second half above this.
182
+ */
183
+ function isRefFailure(err, message) {
184
+ return (err instanceof ScaffoldRefError ||
185
+ /has no version|remote branch|couldn't find remote ref/i.test(message));
186
+ }
187
+ function printFromScaffoldFailure(print, from, refMissing) {
188
+ const named = `${from.owner}/${from.repo}`;
189
+ if (refMissing) {
190
+ print(`✗ The repository ${named} has no tag "${from.tag}"`);
191
+ print("");
192
+ print(" The gallery card always shows the current reference. If you pasted this command");
193
+ print(" from somewhere else, it may be out of date.");
194
+ print("");
195
+ print(" Nothing was written.");
196
+ print("");
197
+ print(" Copy the command again from the gallery and run it.");
198
+ return;
199
+ }
200
+ print(`✗ Could not reach github.com/${named}`);
201
+ print("");
202
+ print(" The repository may have been renamed or made private, or your network may be");
203
+ print(" blocking github.com.");
204
+ print("");
205
+ print(" Nothing was written.");
206
+ print("");
207
+ print(" Check the command on the gallery card and run it again.");
208
+ }
134
209
  /** Two columns, aligned on the widest command, so the block reads as a table. */
135
210
  function alignedSteps(steps) {
136
211
  const width = Math.max(...steps.map(([cmd]) => cmd.length));
@@ -0,0 +1,70 @@
1
+ import { type SourceRef } from "./sourceRef.js";
2
+ /** How many times the address prompt re-asks before falling back to the printed block. */
3
+ export declare const ADDRESS_REASK_LIMIT = 3;
4
+ export interface RegisterDeps {
5
+ print: (line: string) => void;
6
+ /** The scaffolded directory — the cwd the registration acts in. */
7
+ targetDir: string;
8
+ /** Label for the resume command (`cd <label>`). */
9
+ dirLabel: string;
10
+ /** Free-text prompt. Absent → not a TTY, so nothing may be asked. */
11
+ ask?: (prompt: string, fallback: string) => Promise<string>;
12
+ /** Resolve the current PAT, or undefined. Never triggers a login by itself. */
13
+ currentToken: () => Promise<string | undefined>;
14
+ /** Run the interactive browser login. Only ever called on a TTY. */
15
+ runLogin: () => Promise<void>;
16
+ /** Resolve which org to register in, and where that choice came from. */
17
+ resolveOrg: () => Promise<{
18
+ ok: true;
19
+ name: string;
20
+ source: OrgSource;
21
+ } | {
22
+ ok: false;
23
+ message: string;
24
+ }>;
25
+ /**
26
+ * f2's shipped registration half, injected. Returns the exit code; it owns the
27
+ * binding write, which happens ONLY after the 201.
28
+ */
29
+ create: (opts: {
30
+ name: string;
31
+ slug: string;
32
+ }) => Promise<number>;
33
+ /**
34
+ * Whether a scaffold ran above this call. It changes exactly one word, and that
35
+ * word is the on-disk state: "Nothing else was written." (source exists) versus
36
+ * "Nothing was written." (it does not).
37
+ *
38
+ * `create` in an ALREADY-scaffolded folder (case 2) reaches this orchestrator
39
+ * without having written anything, so the shipped default — true, f6's combined
40
+ * path, where a clone always precedes it — would tell that creator their source
41
+ * was written by this command.
42
+ */
43
+ scaffolded?: boolean;
44
+ /**
45
+ * Whether the resume block carries a `cd <dir>`. `create` run in the cwd has
46
+ * nowhere to cd to, so the line would be `cd .`.
47
+ *
48
+ * Defaults to true: f6's path always scaffolds INTO a named directory.
49
+ */
50
+ resumeNeedsCd?: boolean;
51
+ }
52
+ export type OrgSource = "active" | "env" | "flag";
53
+ export interface RegisterOptions {
54
+ /** `--name`, read before prompting. */
55
+ name?: string;
56
+ /** `--slug`, read before prompting. */
57
+ slug?: string;
58
+ }
59
+ /**
60
+ * The registration half — the below-the-seam orchestrator shared by `init --from`
61
+ * (f6) and by `vincentt create` (create-scaffolds). One implementation, because a
62
+ * second implementation of f2's rules is a second place for them to be wrong.
63
+ *
64
+ * `deps.scaffolded` says whether source was written above this call, and it decides
65
+ * one word in every failure's last line: that word IS the on-disk state, read at a
66
+ * glance in scrollback by someone who wants exactly one bit of information.
67
+ */
68
+ export declare function registerAfterScaffold(deps: RegisterDeps, opts?: RegisterOptions): Promise<number>;
69
+ /** The `· from owner/repo@tag` bullet in the start block. */
70
+ export declare function sourceBullet(ref: SourceRef): string;