@vincentt-xr/cli 0.2.0 → 0.4.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.
- package/README.md +3 -1
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +20 -0
- package/dist/cli/index.js +114 -25
- package/dist/lib/backend.d.ts +0 -16
- package/dist/lib/backend.js +0 -34
- package/dist/lib/initCommand.d.ts +18 -1
- package/dist/lib/initCommand.js +70 -7
- package/dist/lib/lifecycle.d.ts +12 -4
- package/dist/lib/lifecycle.js +36 -30
- package/dist/lib/version.d.ts +34 -0
- package/dist/lib/version.js +82 -0
- package/dist/publish/api.d.ts +67 -0
- package/dist/publish/api.js +96 -0
- package/dist/publish/bounds.d.ts +25 -0
- package/dist/publish/bounds.js +53 -0
- package/dist/publish/classify.d.ts +51 -0
- package/dist/publish/classify.js +138 -0
- package/dist/publish/command.d.ts +27 -0
- package/dist/publish/command.js +328 -0
- package/dist/publish/consoleVerbs.d.ts +17 -0
- package/dist/publish/consoleVerbs.js +26 -0
- package/dist/publish/copy.d.ts +180 -0
- package/dist/publish/copy.js +606 -0
- package/dist/publish/defaults.d.ts +16 -0
- package/dist/publish/defaults.js +52 -0
- package/dist/publish/manifest.d.ts +27 -0
- package/dist/publish/manifest.js +81 -0
- package/dist/publish/paths.d.ts +25 -0
- package/dist/publish/paths.js +78 -0
- package/dist/publish/progress.d.ts +17 -0
- package/dist/publish/progress.js +45 -0
- package/dist/publish/prompt.d.ts +7 -0
- package/dist/publish/prompt.js +43 -0
- package/dist/publish/qa.f4.fixtures.d.ts +25 -0
- package/dist/publish/qa.f4.fixtures.js +140 -0
- package/dist/publish/upload.d.ts +30 -0
- package/dist/publish/upload.js +93 -0
- package/dist/scaffold/index.d.ts +26 -0
- package/dist/scaffold/index.js +43 -1
- package/package.json +2 -3
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.js +0 -215
package/README.md
CHANGED
|
@@ -68,7 +68,9 @@ Reverse channel
|
|
|
68
68
|
|
|
69
69
|
Servers
|
|
70
70
|
relay Run the diagnostics/annotation relay
|
|
71
|
-
|
|
71
|
+
|
|
72
|
+
Meta
|
|
73
|
+
version CLI version, and the API this machine resolves to
|
|
72
74
|
|
|
73
75
|
Common options: --dir <path> (project dir, default cwd)
|
|
74
76
|
```
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* usage instead of running the (side-effectful) verb.
|
|
6
6
|
*/
|
|
7
7
|
export declare function isHelpInvocation(args: string[]): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True for `vincentt --version` / `-v` — the flag form, as the FIRST argument
|
|
10
|
+
* only. Unlike `--help`, this is deliberately not honored anywhere in the args:
|
|
11
|
+
* `--help` is safe to obey late because it cancels the action, whereas treating
|
|
12
|
+
* a trailing `-v` on `vincentt publish -v` as "print the version" would silently
|
|
13
|
+
* skip a publish the creator asked for. Anywhere else it stays an unknown flag.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isVersionInvocation(args: string[]): boolean;
|
|
16
|
+
export declare function refusedFlag(args: string[]): string | undefined;
|
|
8
17
|
/**
|
|
9
18
|
* The bare arguments, with flag VALUES removed. Without this, `vincentt link --dir
|
|
10
19
|
* /tmp 66f1` reads `/tmp` as the project id — a wrong-but-plausible value, which is
|
package/dist/cli/args.js
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
export function isHelpInvocation(args) {
|
|
8
8
|
return args[0] === undefined || args.includes("--help") || args.includes("-h");
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* True for `vincentt --version` / `-v` — the flag form, as the FIRST argument
|
|
12
|
+
* only. Unlike `--help`, this is deliberately not honored anywhere in the args:
|
|
13
|
+
* `--help` is safe to obey late because it cancels the action, whereas treating
|
|
14
|
+
* a trailing `-v` on `vincentt publish -v` as "print the version" would silently
|
|
15
|
+
* skip a publish the creator asked for. Anywhere else it stays an unknown flag.
|
|
16
|
+
*/
|
|
17
|
+
export function isVersionInvocation(args) {
|
|
18
|
+
return args[0] === "--version" || args[0] === "-v";
|
|
19
|
+
}
|
|
10
20
|
/** Flags that stand alone; every other `--flag` consumes the next argument. */
|
|
11
21
|
const BOOLEAN_FLAGS = new Set([
|
|
12
22
|
"--force",
|
|
@@ -18,9 +28,19 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
18
28
|
"--stop",
|
|
19
29
|
"--no-install",
|
|
20
30
|
"--allow-custom-api",
|
|
31
|
+
"--yes",
|
|
21
32
|
"--help",
|
|
22
33
|
"-h",
|
|
23
34
|
]);
|
|
35
|
+
/**
|
|
36
|
+
* Flags the CLI deliberately does NOT have. The checklist reports and never
|
|
37
|
+
* writes: a suggested value is one keystroke from a tool that writes it, and
|
|
38
|
+
* writing to the creator's source is a named boundary.
|
|
39
|
+
*/
|
|
40
|
+
const REFUSED_FLAGS = new Set(["--fix", "--write-meta"]);
|
|
41
|
+
export function refusedFlag(args) {
|
|
42
|
+
return args.find((a) => REFUSED_FLAGS.has(a.split("=")[0]));
|
|
43
|
+
}
|
|
24
44
|
/**
|
|
25
45
|
* The bare arguments, with flag VALUES removed. Without this, `vincentt link --dir
|
|
26
46
|
* /tmp 66f1` reads `/tmp` as the project id — a wrong-but-plausible value, which is
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `vincentt` — the
|
|
3
|
-
// bash-capable coding agent drive the same verbs
|
|
4
|
-
//
|
|
2
|
+
// `vincentt` — the CLI for the local-first dev loop, and the only surface. A human
|
|
3
|
+
// and a bash-capable coding agent drive the same verbs, so there is nothing to
|
|
4
|
+
// register: an agent that can run a shell command already has the whole product.
|
|
5
5
|
//
|
|
6
6
|
// Verb groups:
|
|
7
7
|
// auth login · logout · whoami
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// dev loop publish · preview
|
|
10
10
|
// diagnostics logs · network · trace
|
|
11
11
|
// reverse feedback (--wait | --watch)
|
|
12
|
-
// servers relay
|
|
12
|
+
// servers relay
|
|
13
|
+
// meta version
|
|
14
|
+
import { createRequire } from "node:module";
|
|
13
15
|
import os from "node:os";
|
|
14
16
|
import path from "node:path";
|
|
15
17
|
import { runPkceLogin } from "../login/pkceLogin.js";
|
|
@@ -21,7 +23,11 @@ import { getMe, ProjectUnreachableError } from "../lib/backend.js";
|
|
|
21
23
|
import { initCommand } from "../lib/initCommand.js";
|
|
22
24
|
import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
|
|
23
25
|
import { whoamiCommand } from "../lib/whoami.js";
|
|
24
|
-
import {
|
|
26
|
+
import { renderVersion, versionReport } from "../lib/version.js";
|
|
27
|
+
import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, projectCreate, } from "../lib/lifecycle.js";
|
|
28
|
+
import { publishCommand } from "../publish/command.js";
|
|
29
|
+
import { stdinAsk, stdinConfirm } from "../publish/prompt.js";
|
|
30
|
+
import { consoleVerbCommand, isConsoleOnlyVerb } from "../publish/consoleVerbs.js";
|
|
25
31
|
import { PreviewUi } from "../preview/command.js";
|
|
26
32
|
import { httpRelayClient } from "../lib/diagnostics.js";
|
|
27
33
|
import { readLivePreview } from "../lib/relayInfo.js";
|
|
@@ -29,7 +35,7 @@ import { mintRateLimited, notSignedIn, previewConflict, startBlock, } from "../p
|
|
|
29
35
|
import { renderTerminalQr, shouldRenderQr } from "../preview/qr.js";
|
|
30
36
|
import { PreviewConflictError, PreviewRateLimitedError } from "../preview/api.js";
|
|
31
37
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
32
|
-
import { isHelpInvocation, positionalArgs } from "./args.js";
|
|
38
|
+
import { isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag } from "./args.js";
|
|
33
39
|
// The console origin serves /cli-auth (the browser target for `vincentt login`). VINCENTT_
|
|
34
40
|
// CONSOLE_URL overrides for dev/dogfood; VINCENTT_EDITOR_URL is honored as a legacy alias.
|
|
35
41
|
const DEFAULT_CONSOLE_URL = "https://vincentt.app";
|
|
@@ -143,7 +149,20 @@ async function whoami() {
|
|
|
143
149
|
}
|
|
144
150
|
// ---- project ----------------------------------------------------------------
|
|
145
151
|
async function init() {
|
|
146
|
-
|
|
152
|
+
// Both prompts are omitted when stdin is not a TTY, and initCommand reads their
|
|
153
|
+
// ABSENCE as "may not ask" — so a piped or CI invocation refuses rather than
|
|
154
|
+
// blocking forever on a question nobody can see.
|
|
155
|
+
const interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
156
|
+
process.exitCode = await initCommand({
|
|
157
|
+
print: (l) => console.log(l),
|
|
158
|
+
cwd: process.cwd(),
|
|
159
|
+
ask: interactive ? stdinAsk() : undefined,
|
|
160
|
+
confirm: interactive ? stdinConfirm() : undefined,
|
|
161
|
+
}, {
|
|
162
|
+
dir: positional(1),
|
|
163
|
+
skipInstall: has("--no-install"),
|
|
164
|
+
template: flag("--template"),
|
|
165
|
+
});
|
|
147
166
|
}
|
|
148
167
|
async function create() {
|
|
149
168
|
await requireLogin();
|
|
@@ -186,17 +205,54 @@ async function unlink() {
|
|
|
186
205
|
config: () => resolveConfig(cwd),
|
|
187
206
|
});
|
|
188
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* `vincentt publish` — Flows L22-L31.
|
|
210
|
+
*
|
|
211
|
+
* The whole gate lives here: the checklist, the permanence disclosure, the
|
|
212
|
+
* confirmation, the upload and the live URL. There is no browser step in
|
|
213
|
+
* publishing, not on the first publish, not ever.
|
|
214
|
+
*/
|
|
189
215
|
async function publish() {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
console.log(result.message);
|
|
198
|
-
if (result.notBound)
|
|
216
|
+
const refused = refusedFlag(argv);
|
|
217
|
+
if (refused) {
|
|
218
|
+
console.log(`✗ There's no \`${refused}\` option.`);
|
|
219
|
+
console.log("");
|
|
220
|
+
console.log(" The link-preview check reports what it found; it never edits your");
|
|
221
|
+
console.log(" source. Fix the tags in your own files, rebuild, then publish again.");
|
|
199
222
|
process.exitCode = 1;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const cwd = projectDir();
|
|
226
|
+
process.exitCode = await publishCommand({
|
|
227
|
+
print: (l) => console.log(l),
|
|
228
|
+
write: (s) => process.stdout.write(s),
|
|
229
|
+
confirm: stdinConfirm(),
|
|
230
|
+
cwd,
|
|
231
|
+
config: () => resolveConfig(cwd, {
|
|
232
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
233
|
+
warn: (l) => console.log(l),
|
|
234
|
+
}),
|
|
235
|
+
binding: async () => {
|
|
236
|
+
const located = await locateProjectBinding(cwd);
|
|
237
|
+
return located
|
|
238
|
+
? { projectId: located.binding.projectId, slug: located.binding.slug }
|
|
239
|
+
: undefined;
|
|
240
|
+
},
|
|
241
|
+
token: currentToken,
|
|
242
|
+
isTTY: Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
|
|
243
|
+
now: () => Date.now(),
|
|
244
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
245
|
+
orgName: activeOrgName,
|
|
246
|
+
}, { distDir: flag("--dist"), note: flag("--note"), yes: has("--yes") });
|
|
247
|
+
}
|
|
248
|
+
/** Flow L30 — the six verbs that are console actions, answered rather than rejected. */
|
|
249
|
+
async function consoleOnly(verb) {
|
|
250
|
+
const cwd = projectDir();
|
|
251
|
+
process.exitCode = await consoleVerbCommand({
|
|
252
|
+
print: (l) => console.log(l),
|
|
253
|
+
projectId: async () => (await locateProjectBinding(cwd))?.binding.projectId,
|
|
254
|
+
consoleUrl: flag("--console") ?? process.env.VINCENTT_CONSOLE_URL,
|
|
255
|
+
}, verb);
|
|
200
256
|
}
|
|
201
257
|
/**
|
|
202
258
|
* `vincentt preview` — Flows L13-L21.
|
|
@@ -458,11 +514,32 @@ async function feedback() {
|
|
|
458
514
|
await writeCursor(cwd, annotation.seq);
|
|
459
515
|
} while (watch);
|
|
460
516
|
}
|
|
461
|
-
// ----
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
517
|
+
// ---- meta -------------------------------------------------------------------
|
|
518
|
+
/**
|
|
519
|
+
* The running package's version, read from the manifest rather than baked in by
|
|
520
|
+
* the build — a hardcoded string is one forgotten edit away from a CLI that
|
|
521
|
+
* misreports itself, which is worse than not having the command at all.
|
|
522
|
+
* `src/cli/` and the emitted `dist/cli/` sit at the same depth, so one relative
|
|
523
|
+
* path serves both.
|
|
524
|
+
*/
|
|
525
|
+
function cliVersion() {
|
|
526
|
+
try {
|
|
527
|
+
const manifest = createRequire(import.meta.url)("../../package.json");
|
|
528
|
+
return manifest.version ?? "unknown";
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return "unknown";
|
|
532
|
+
}
|
|
465
533
|
}
|
|
534
|
+
async function version() {
|
|
535
|
+
const report = await versionReport(projectDir(), {
|
|
536
|
+
cliVersion: cliVersion(),
|
|
537
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
538
|
+
});
|
|
539
|
+
for (const line of renderVersion(report))
|
|
540
|
+
console.log(line);
|
|
541
|
+
}
|
|
542
|
+
// ---- servers ----------------------------------------------------------------
|
|
466
543
|
async function relay() {
|
|
467
544
|
const { startRelay } = await import("../relay/server.js");
|
|
468
545
|
const port = numFlag("--port") ?? 7331;
|
|
@@ -490,13 +567,14 @@ function usage() {
|
|
|
490
567
|
` whoami Show the signed-in account\n\n` +
|
|
491
568
|
`Project\n` +
|
|
492
569
|
` init [dir] Scaffold a Vincentt XR app — no account needed\n` +
|
|
493
|
-
` [--no-install]\n` +
|
|
570
|
+
` [--no-install] [--template <version>]\n` +
|
|
494
571
|
` create Register a project and reserve its address\n` +
|
|
495
572
|
` [--name <s>] [--slug <s>] [--org <s>] [--preview] [--force]\n` +
|
|
496
573
|
` link <project-id> Link this folder to an existing project\n` +
|
|
497
574
|
` unlink Disconnect this folder from its project\n\n` +
|
|
498
575
|
`Dev loop\n` +
|
|
499
576
|
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
577
|
+
` [--dist <path>] [--note <s>] [--yes]\n` +
|
|
500
578
|
` preview Serve the app at an https link a phone can open\n` +
|
|
501
579
|
` [--port <n>] [--tester <id>] [--no-qr] [--stop]\n\n` +
|
|
502
580
|
`Diagnostics (need a running preview)\n` +
|
|
@@ -510,13 +588,19 @@ function usage() {
|
|
|
510
588
|
` feedback --wait Block for the next phone annotation, print it, exit\n` +
|
|
511
589
|
` feedback --watch Stream annotations as they arrive\n\n` +
|
|
512
590
|
`Servers\n` +
|
|
513
|
-
` relay Run the diagnostics/annotation relay\n` +
|
|
514
|
-
`
|
|
591
|
+
` relay Run the diagnostics/annotation relay\n\n` +
|
|
592
|
+
`Meta\n` +
|
|
593
|
+
` version CLI version, and the API this machine resolves to\n\n` +
|
|
515
594
|
`Common options: --dir <path> (project dir, default cwd)\n` +
|
|
516
595
|
` --allow-custom-api honor a binding's apiUrl override\n`);
|
|
517
596
|
}
|
|
518
597
|
async function main() {
|
|
519
598
|
const cmd = argv[0];
|
|
599
|
+
// Checked BEFORE the help gate: `--version` is a lone flag with no verb, which
|
|
600
|
+
// the help gate would otherwise answer with usage — the one response that does
|
|
601
|
+
// not contain the version.
|
|
602
|
+
if (isVersionInvocation(argv))
|
|
603
|
+
return version();
|
|
520
604
|
// `--help`/`-h` anywhere means "explain, don't act" — never let it fall through to
|
|
521
605
|
// a verb. Otherwise `login --help` runs login, `create --help` scaffolds, etc.
|
|
522
606
|
if (isHelpInvocation(argv)) {
|
|
@@ -524,6 +608,8 @@ async function main() {
|
|
|
524
608
|
return;
|
|
525
609
|
}
|
|
526
610
|
switch (cmd) {
|
|
611
|
+
case "version":
|
|
612
|
+
return version();
|
|
527
613
|
case "login":
|
|
528
614
|
return login();
|
|
529
615
|
case "logout":
|
|
@@ -550,11 +636,14 @@ async function main() {
|
|
|
550
636
|
return diag("trace");
|
|
551
637
|
case "feedback":
|
|
552
638
|
return feedback();
|
|
553
|
-
case "mcp":
|
|
554
|
-
return mcp();
|
|
555
639
|
case "relay":
|
|
556
640
|
return relay();
|
|
557
641
|
default:
|
|
642
|
+
// Flow L30: rolling back and taking an address offline are console
|
|
643
|
+
// actions, and a bare "unknown command" here would be the product failing
|
|
644
|
+
// to answer a question it knows the answer to.
|
|
645
|
+
if (cmd !== undefined && isConsoleOnlyVerb(cmd))
|
|
646
|
+
return consoleOnly(cmd);
|
|
558
647
|
console.error(`Unknown command: ${cmd}\n`);
|
|
559
648
|
usage();
|
|
560
649
|
process.exitCode = 1;
|
package/dist/lib/backend.d.ts
CHANGED
|
@@ -19,13 +19,6 @@ export interface CreatedProject {
|
|
|
19
19
|
orgId: string;
|
|
20
20
|
liveUrl: string;
|
|
21
21
|
}
|
|
22
|
-
export interface PublishResult {
|
|
23
|
-
version: number;
|
|
24
|
-
/** Immutable versioned URL (<slug>.<apex>/v<n>/). */
|
|
25
|
-
url: string;
|
|
26
|
-
/** Stable live alias (<slug>.<apex>/). */
|
|
27
|
-
liveUrl: string;
|
|
28
|
-
}
|
|
29
22
|
export declare class BackendError extends Error {
|
|
30
23
|
readonly status?: number | undefined;
|
|
31
24
|
constructor(message: string, status?: number | undefined);
|
|
@@ -70,15 +63,6 @@ export declare function reapTunnel(cfg: ResolvedConfig, projectId: string, ref:
|
|
|
70
63
|
}): Promise<void>;
|
|
71
64
|
/** Recursively list files under `dir`, returning dir-relative POSIX paths. */
|
|
72
65
|
export declare function walkDir(dir: string, base?: string): Promise<string[]>;
|
|
73
|
-
/**
|
|
74
|
-
* Upload a locally-built dist and make it live. Each file rides as a multipart
|
|
75
|
-
* part whose filename is its dist-relative path (the backend reconstructs the
|
|
76
|
-
* tree). Requires an index.html at the dist root.
|
|
77
|
-
*/
|
|
78
|
-
export declare function publishUpload(cfg: ResolvedConfig, projectId: string, distDir: string, opts?: {
|
|
79
|
-
note?: string;
|
|
80
|
-
commitSha?: string;
|
|
81
|
-
}): Promise<PublishResult>;
|
|
82
66
|
export interface MeMembership {
|
|
83
67
|
orgId: string;
|
|
84
68
|
orgSlug: string;
|
package/dist/lib/backend.js
CHANGED
|
@@ -178,40 +178,6 @@ export async function walkDir(dir, base = dir) {
|
|
|
178
178
|
}
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
181
|
-
/**
|
|
182
|
-
* Upload a locally-built dist and make it live. Each file rides as a multipart
|
|
183
|
-
* part whose filename is its dist-relative path (the backend reconstructs the
|
|
184
|
-
* tree). Requires an index.html at the dist root.
|
|
185
|
-
*/
|
|
186
|
-
export async function publishUpload(cfg, projectId, distDir, opts = {}) {
|
|
187
|
-
const files = await walkDir(distDir);
|
|
188
|
-
if (!files.includes("index.html")) {
|
|
189
|
-
throw new BackendError(`dist at ${distDir} has no index.html at its root — did the build run?`);
|
|
190
|
-
}
|
|
191
|
-
const form = new FormData();
|
|
192
|
-
// The real dist-relative paths ride as an ordered manifest — a multipart filename
|
|
193
|
-
// can't carry a directory (FormData strips it to a basename), so the backend maps
|
|
194
|
-
// paths[i] → files[i] to rebuild the tree.
|
|
195
|
-
form.append("paths", JSON.stringify(files));
|
|
196
|
-
for (const rel of files) {
|
|
197
|
-
const bytes = await fs.readFile(path.join(distDir, rel));
|
|
198
|
-
// Uint8Array (Buffer) is a valid BlobPart; the filename is cosmetic (the backend
|
|
199
|
-
// uses the manifest), but pass the basename so server logs read sensibly.
|
|
200
|
-
form.append("files", new Blob([bytes]), path.basename(rel));
|
|
201
|
-
}
|
|
202
|
-
if (opts.note)
|
|
203
|
-
form.append("note", opts.note);
|
|
204
|
-
if (opts.commitSha)
|
|
205
|
-
form.append("commitSha", opts.commitSha);
|
|
206
|
-
const res = await authFetch(cfg, `/projects/${projectId}/publish/upload`, {
|
|
207
|
-
method: "POST",
|
|
208
|
-
body: form,
|
|
209
|
-
});
|
|
210
|
-
if (!res.ok) {
|
|
211
|
-
throw new BackendError(authFailureMessage("publish failed", res.status, await safeText(res)), res.status);
|
|
212
|
-
}
|
|
213
|
-
return (await res.json());
|
|
214
|
-
}
|
|
215
181
|
/** Resolve the signed-in identity for `vincentt whoami`. Throws on any non-2xx so
|
|
216
182
|
* the caller can degrade to "token present, unverified". */
|
|
217
183
|
export async function getMe(cfg) {
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
+
import type { Ask, Confirm } from "../publish/prompt.js";
|
|
1
2
|
export interface InitDeps {
|
|
2
3
|
print: (line: string) => void;
|
|
3
4
|
cwd: string;
|
|
4
|
-
scaffold?: (targetDir: string) => Promise<void>;
|
|
5
|
+
scaffold?: (targetDir: string, ref?: string) => Promise<void>;
|
|
5
6
|
install?: (targetDir: string) => Promise<string>;
|
|
6
7
|
isEmpty?: (targetDir: string) => Promise<boolean>;
|
|
8
|
+
/** Free-text prompt. Absent → not a TTY, so nothing may be asked. */
|
|
9
|
+
ask?: Ask;
|
|
10
|
+
/** y/N prompt. Absent → not a TTY. */
|
|
11
|
+
confirm?: Confirm;
|
|
12
|
+
/** Non-empty check for the resolved target; injected in tests. */
|
|
13
|
+
isNonEmpty?: (targetDir: string) => Promise<boolean>;
|
|
7
14
|
}
|
|
15
|
+
/** The folder name offered when `init` is run bare and the creator just hits Enter. */
|
|
16
|
+
export declare const DEFAULT_APP_NAME = "my-ar-app";
|
|
17
|
+
/**
|
|
18
|
+
* A folder name safe to create from a typed answer. Keeps this from becoming a
|
|
19
|
+
* path-traversal seam: the answer names a child of the cwd, never `../` upward and
|
|
20
|
+
* never an absolute path.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sanitizeAppName(answer: string): string | null;
|
|
8
23
|
export interface InitOptions {
|
|
9
24
|
/** Positional target directory; defaults to the cwd. */
|
|
10
25
|
dir?: string;
|
|
11
26
|
/** Skip the dependency install (CI, or a creator who wants to pick a manager). */
|
|
12
27
|
skipInstall?: boolean;
|
|
28
|
+
/** Template version to scaffold from; defaults to the latest release. */
|
|
29
|
+
template?: string;
|
|
13
30
|
}
|
|
14
31
|
export declare function initCommand(deps: InitDeps, opts?: InitOptions): Promise<number>;
|
package/dist/lib/initCommand.js
CHANGED
|
@@ -6,10 +6,49 @@
|
|
|
6
6
|
// does need an account) is offered at the end as the next thing AVAILABLE, never as
|
|
7
7
|
// a requirement.
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { detectPackageManager, installDependencies, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
9
|
+
import { detectPackageManager, installDependencies, isNonEmptyDir, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
10
|
+
/** The folder name offered when `init` is run bare and the creator just hits Enter. */
|
|
11
|
+
export const DEFAULT_APP_NAME = "my-ar-app";
|
|
12
|
+
/**
|
|
13
|
+
* A folder name safe to create from a typed answer. Keeps this from becoming a
|
|
14
|
+
* path-traversal seam: the answer names a child of the cwd, never `../` upward and
|
|
15
|
+
* never an absolute path.
|
|
16
|
+
*/
|
|
17
|
+
export function sanitizeAppName(answer) {
|
|
18
|
+
const trimmed = answer.trim();
|
|
19
|
+
if (trimmed === "" || trimmed === "." || trimmed === "..")
|
|
20
|
+
return null;
|
|
21
|
+
if (trimmed.includes("/") || trimmed.includes("\\"))
|
|
22
|
+
return null;
|
|
23
|
+
if (trimmed.startsWith("-"))
|
|
24
|
+
return null;
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
10
27
|
export async function initCommand(deps, opts = {}) {
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
// Resolve WHERE before checking what is there. Bare `init` used to mean "here",
|
|
29
|
+
// which is the surprising reading — a creator typing it in their home directory
|
|
30
|
+
// meant "make me an app", not "fill this folder with one".
|
|
31
|
+
let dir = opts.dir;
|
|
32
|
+
if (dir === undefined) {
|
|
33
|
+
if (!deps.ask) {
|
|
34
|
+
deps.print("✗ init needs a target directory when it can't ask.");
|
|
35
|
+
deps.print("");
|
|
36
|
+
deps.print(" vincentt init <dir> Scaffold into a new folder");
|
|
37
|
+
deps.print(" vincentt init . Scaffold into the current folder");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const answer = await deps.ask(`Project name: (${DEFAULT_APP_NAME}) `, DEFAULT_APP_NAME);
|
|
41
|
+
const name = sanitizeAppName(answer);
|
|
42
|
+
if (name === null) {
|
|
43
|
+
deps.print(`✗ "${answer.trim()}" is not a folder name.`);
|
|
44
|
+
deps.print("");
|
|
45
|
+
deps.print(" Use a plain name (my-ar-app), or `vincentt init .` for this folder.");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
dir = name;
|
|
49
|
+
}
|
|
50
|
+
const target = path.resolve(deps.cwd, dir);
|
|
51
|
+
const label = dir === "." ? "." : `./${dir}`;
|
|
13
52
|
const isEmpty = deps.isEmpty ?? needsScaffold;
|
|
14
53
|
if (!(await isEmpty(target))) {
|
|
15
54
|
deps.print(`✗ ${label} already contains an app (package.json).`);
|
|
@@ -18,11 +57,34 @@ export async function initCommand(deps, opts = {}) {
|
|
|
18
57
|
deps.print(" vincentt create Register THIS folder as a project");
|
|
19
58
|
return 1;
|
|
20
59
|
}
|
|
60
|
+
// No package.json, but not empty either — the case the old guard waved through.
|
|
61
|
+
const isNonEmpty = deps.isNonEmpty ?? isNonEmptyDir;
|
|
62
|
+
if (await isNonEmpty(target)) {
|
|
63
|
+
if (!deps.confirm) {
|
|
64
|
+
deps.print(`✗ ${label} is not empty, and I can't ask whether to add to it.`);
|
|
65
|
+
deps.print("");
|
|
66
|
+
deps.print(" vincentt init <dir> Scaffold into a new folder instead");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
deps.print(`! ${label} is not empty. Scaffolding adds the template's files alongside what's there.`);
|
|
70
|
+
deps.print(" Existing files are never overwritten.");
|
|
71
|
+
if (!(await deps.confirm("? Continue? (y/N) ", false))) {
|
|
72
|
+
deps.print("");
|
|
73
|
+
deps.print(" Nothing was written.");
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
21
77
|
deps.print(`→ Scaffolding a Vincentt XR app in ${label}…`);
|
|
22
78
|
deps.print(" · React + react-three-fiber + @vincentt-xr/sdk");
|
|
23
|
-
|
|
79
|
+
// Only announce the version when it was asked for. On the default path the ref
|
|
80
|
+
// is an implementation detail, and naming it would invite creators to pin a
|
|
81
|
+
// version they have no reason to care about.
|
|
82
|
+
if (opts.template)
|
|
83
|
+
deps.print(` · template ${opts.template}`);
|
|
84
|
+
const scaffold = deps.scaffold ??
|
|
85
|
+
((dir, ref) => scaffoldFromTemplate(dir, { ref }));
|
|
24
86
|
try {
|
|
25
|
-
await scaffold(target);
|
|
87
|
+
await scaffold(target, opts.template);
|
|
26
88
|
}
|
|
27
89
|
catch (err) {
|
|
28
90
|
deps.print("");
|
|
@@ -50,8 +112,9 @@ export async function initCommand(deps, opts = {}) {
|
|
|
50
112
|
deps.print(`✓ Created ${label}`);
|
|
51
113
|
deps.print("");
|
|
52
114
|
const steps = [];
|
|
53
|
-
|
|
54
|
-
|
|
115
|
+
// Keyed off the RESOLVED dir, not the flag — a prompted name needs the cd too.
|
|
116
|
+
if (dir !== ".")
|
|
117
|
+
steps.push([`cd ${dir}`, ""]);
|
|
55
118
|
if (!installed || opts.skipInstall) {
|
|
56
119
|
steps.push([`${pm} install`, "Install dependencies"]);
|
|
57
120
|
}
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -59,14 +59,22 @@ export interface PublishOptions {
|
|
|
59
59
|
* has already happened (or thrown).
|
|
60
60
|
*/
|
|
61
61
|
warn?: (line: string) => void;
|
|
62
|
+
/** Skip the confirmation. REFUSED on a first publish; see publishCommand. */
|
|
63
|
+
yes?: boolean;
|
|
62
64
|
}
|
|
63
65
|
export interface PublishOutcome {
|
|
66
|
+
/** The full transcript, exactly as the CLI prints it. */
|
|
64
67
|
message: string;
|
|
65
|
-
|
|
66
|
-
version?: number;
|
|
67
|
-
liveUrl?: string;
|
|
68
|
-
url?: string;
|
|
68
|
+
exitCode: number;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* The shared `publish` orchestration behind both `vincentt publish` and the MCP
|
|
72
|
+
* `project_publish` tool, so the two surfaces can never diverge.
|
|
73
|
+
*
|
|
74
|
+
* MCP has no terminal, so it is a non-TTY caller: the command refuses without
|
|
75
|
+
* `--yes` rather than assuming consent, and refuses `--yes` outright on a first
|
|
76
|
+
* publish. An agent cannot make an address permanent.
|
|
77
|
+
*/
|
|
70
78
|
export declare function projectPublish(projectCwd: string, opts?: PublishOptions): Promise<PublishOutcome>;
|
|
71
79
|
export declare function getActivePreview(): RunningPreview | null;
|
|
72
80
|
export interface PreviewStartResult {
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// lower-level pieces (backend calls, scaffold, preview, diagnostics); no CLI or
|
|
4
4
|
// MCP SDK concerns leak in here. Both surfaces render the returned data their
|
|
5
5
|
// own way.
|
|
6
|
-
import
|
|
7
|
-
import { getMe, gitHeadSha, publishUpload } from "./backend.js";
|
|
6
|
+
import { getMe } from "./backend.js";
|
|
8
7
|
import { createCommand } from "./projects.js";
|
|
8
|
+
import { publishCommand } from "../publish/command.js";
|
|
9
9
|
import { activeOrgSlug, resolveActiveOrg } from "./org.js";
|
|
10
10
|
import { filterErrors, httpRelayClient, renderResult, } from "./diagnostics.js";
|
|
11
11
|
import { clearRelayInfo, readLivePreview, readRelayInfo, writeRelayInfo, } from "./relayInfo.js";
|
|
@@ -68,36 +68,42 @@ export async function resolveOrgForCommand(cfg, input) {
|
|
|
68
68
|
: "✗ No active organization.\n\n Run `vincentt whoami` to see which organizations you belong to,\n then re-run with `--org <slug>`.",
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The shared `publish` orchestration behind both `vincentt publish` and the MCP
|
|
73
|
+
* `project_publish` tool, so the two surfaces can never diverge.
|
|
74
|
+
*
|
|
75
|
+
* MCP has no terminal, so it is a non-TTY caller: the command refuses without
|
|
76
|
+
* `--yes` rather than assuming consent, and refuses `--yes` outright on a first
|
|
77
|
+
* publish. An agent cannot make an address permanent.
|
|
78
|
+
*/
|
|
71
79
|
export async function projectPublish(projectCwd, opts = {}) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
const located = await locateProjectBinding(projectCwd);
|
|
77
|
-
if (!located) {
|
|
78
|
-
return {
|
|
79
|
-
message: [
|
|
80
|
-
"✗ This folder isn't linked to a project.",
|
|
81
|
-
"",
|
|
82
|
-
" vincentt link <project-id> (copy it from the console)",
|
|
83
|
-
" vincentt create Register a new project for this folder",
|
|
84
|
-
].join("\n"),
|
|
85
|
-
notBound: true,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
const dist = path.resolve(projectCwd, opts.distDir ?? "dist");
|
|
89
|
-
const commitSha = await gitHeadSha(projectCwd);
|
|
90
|
-
const result = await publishUpload(cfg, located.binding.projectId, dist, {
|
|
91
|
-
note: opts.note,
|
|
92
|
-
commitSha,
|
|
93
|
-
});
|
|
94
|
-
return {
|
|
95
|
-
message: `Published v${result.version}. Live at ${result.liveUrl}`,
|
|
96
|
-
notBound: false,
|
|
97
|
-
version: result.version,
|
|
98
|
-
liveUrl: result.liveUrl,
|
|
99
|
-
url: result.url,
|
|
80
|
+
const lines = [];
|
|
81
|
+
const print = (line) => {
|
|
82
|
+
lines.push(line);
|
|
100
83
|
};
|
|
84
|
+
const exitCode = await publishCommand({
|
|
85
|
+
print,
|
|
86
|
+
write: () => {
|
|
87
|
+
// No redraw off a terminal; the degraded path emits whole lines.
|
|
88
|
+
},
|
|
89
|
+
confirm: async () => false,
|
|
90
|
+
cwd: projectCwd,
|
|
91
|
+
config: () => resolveConfig(projectCwd, {
|
|
92
|
+
allowCustomApi: opts.allowCustomApi,
|
|
93
|
+
warn: opts.warn ?? print,
|
|
94
|
+
}),
|
|
95
|
+
binding: async () => {
|
|
96
|
+
const located = await locateProjectBinding(projectCwd);
|
|
97
|
+
return located
|
|
98
|
+
? { projectId: located.binding.projectId, slug: located.binding.slug }
|
|
99
|
+
: undefined;
|
|
100
|
+
},
|
|
101
|
+
token: async () => process.env.VINCENTT_PAT ?? (await loadMachineConfig()).pat,
|
|
102
|
+
isTTY: false,
|
|
103
|
+
now: () => Date.now(),
|
|
104
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
105
|
+
}, { distDir: opts.distDir, note: opts.note, yes: opts.yes });
|
|
106
|
+
return { message: lines.join("\n"), exitCode };
|
|
101
107
|
}
|
|
102
108
|
// ---- preview (one per process) ----------------------------------------------
|
|
103
109
|
let activePreview = null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface VersionReport {
|
|
2
|
+
cliVersion: string;
|
|
3
|
+
nodeVersion: string;
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
/** Where apiUrl came from, in creator-facing words. */
|
|
6
|
+
apiSource: string;
|
|
7
|
+
/** Set when a binding names a different API that was NOT used. */
|
|
8
|
+
ignoredBindingApiUrl?: string;
|
|
9
|
+
/** True when the resolved host is the built-in fallback (nothing configured). */
|
|
10
|
+
isDefault: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface VersionEnv {
|
|
13
|
+
envApiUrl?: string;
|
|
14
|
+
machineApiUrl?: string;
|
|
15
|
+
bindingApiUrl?: string;
|
|
16
|
+
allowCustomApi?: boolean;
|
|
17
|
+
nodeVersion: string;
|
|
18
|
+
cliVersion: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide what to report. Pure, so the transcript is testable without a home
|
|
22
|
+
* directory, an env var, or a built package.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildVersionReport(env: VersionEnv): VersionReport;
|
|
25
|
+
/**
|
|
26
|
+
* The transcript. Two aligned columns so the host and its provenance read as one
|
|
27
|
+
* fact, not two.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderVersion(r: VersionReport): string[];
|
|
30
|
+
/** Read the machine's real state and produce the report. */
|
|
31
|
+
export declare function versionReport(cwd: string, opts: {
|
|
32
|
+
cliVersion: string;
|
|
33
|
+
allowCustomApi?: boolean;
|
|
34
|
+
}): Promise<VersionReport>;
|