@vincentt-xr/cli 0.1.5 → 0.2.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 +94 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +41 -0
- package/dist/cli/index.js +362 -92
- package/dist/lib/backend.d.ts +45 -5
- package/dist/lib/backend.js +99 -18
- package/dist/lib/diagnostics.d.ts +27 -2
- package/dist/lib/diagnostics.js +73 -8
- package/dist/lib/initCommand.d.ts +14 -0
- package/dist/lib/initCommand.js +75 -0
- package/dist/lib/lifecycle.d.ts +73 -15
- package/dist/lib/lifecycle.js +111 -114
- package/dist/lib/org.d.ts +29 -0
- package/dist/lib/org.js +45 -0
- package/dist/lib/projects.d.ts +82 -0
- package/dist/lib/projects.js +217 -0
- package/dist/lib/relayInfo.d.ts +41 -3
- package/dist/lib/relayInfo.js +94 -0
- package/dist/lib/whoami.d.ts +11 -0
- package/dist/lib/whoami.js +38 -0
- package/dist/login/authBackend.d.ts +30 -0
- package/dist/login/authBackend.js +80 -0
- package/dist/login/guard.d.ts +13 -0
- package/dist/login/guard.js +17 -0
- package/dist/login/loginCommand.d.ts +39 -0
- package/dist/login/loginCommand.js +72 -0
- package/dist/login/pkce.d.ts +47 -0
- package/dist/login/pkce.js +106 -0
- package/dist/login/pkceLogin.d.ts +53 -0
- package/dist/login/pkceLogin.js +152 -0
- package/dist/mcp/server.js +47 -20
- package/dist/preview/api.d.ts +84 -0
- package/dist/preview/api.js +174 -0
- package/dist/preview/command.d.ts +96 -0
- package/dist/preview/command.js +276 -0
- package/dist/preview/copy.d.ts +209 -0
- package/dist/preview/copy.js +444 -0
- package/dist/preview/index.d.ts +2 -3
- package/dist/preview/index.js +5 -6
- package/dist/preview/proxy.d.ts +12 -1
- package/dist/preview/proxy.js +17 -7
- package/dist/preview/qr.d.ts +30 -0
- package/dist/preview/qr.js +89 -0
- package/dist/preview/runner.d.ts +155 -30
- package/dist/preview/runner.js +219 -68
- package/dist/preview/stream.d.ts +52 -0
- package/dist/preview/stream.js +171 -0
- package/dist/preview/viewers.d.ts +113 -0
- package/dist/preview/viewers.js +242 -0
- package/dist/relay/server.d.ts +32 -0
- package/dist/relay/server.js +80 -12
- package/dist/relay/store.d.ts +32 -3
- package/dist/relay/store.js +76 -22
- package/dist/relay/testers.d.ts +90 -0
- package/dist/relay/testers.js +144 -0
- package/dist/shared/authError.d.ts +1 -0
- package/dist/shared/authError.js +11 -0
- package/dist/shared/config.d.ts +73 -5
- package/dist/shared/config.js +133 -20
- package/package.json +2 -3
- package/dist/login/login.d.ts +0 -34
- package/dist/login/login.js +0 -148
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -46
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
package/dist/lib/backend.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promises as fs } from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
+
import { authFailureMessage } from "../shared/authError.js";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
export class BackendError extends Error {
|
|
11
12
|
status;
|
|
@@ -29,29 +30,106 @@ async function safeText(res) {
|
|
|
29
30
|
return "";
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
/** The address a caller asked for is not available. Carries the server's suggestions. */
|
|
34
|
+
export class SlugTakenError extends BackendError {
|
|
35
|
+
suggestions;
|
|
36
|
+
constructor(suggestions) {
|
|
37
|
+
super("That address is taken.", 409);
|
|
38
|
+
this.suggestions = suggestions;
|
|
39
|
+
this.name = "SlugTakenError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The uniform 404 for every unreachable-project cause. The message is the
|
|
44
|
+
* server's, verbatim: the console shows the same six words, so a creator moving
|
|
45
|
+
* between surfaces sees one product and support can match a screenshot to a log
|
|
46
|
+
* line without a translation table.
|
|
47
|
+
*/
|
|
48
|
+
export class ProjectUnreachableError extends BackendError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message, 404);
|
|
51
|
+
this.name = "ProjectUnreachableError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function errorBody(res) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = await res.clone().json();
|
|
57
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
32
63
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
64
|
+
* Register a project in an org. `slug` is optional and the server NEVER assigns
|
|
65
|
+
* one on a collision — it returns 409 with suggestions and the caller re-asks, so
|
|
66
|
+
* a creator's address is always a name they chose.
|
|
36
67
|
*/
|
|
37
|
-
export async function createProject(cfg, name, slug) {
|
|
38
|
-
const res = await authFetch(cfg,
|
|
68
|
+
export async function createProject(cfg, orgSlug, name, slug) {
|
|
69
|
+
const res = await authFetch(cfg, `/orgs/${encodeURIComponent(orgSlug)}/projects`, {
|
|
39
70
|
method: "POST",
|
|
40
71
|
headers: { "content-type": "application/json" },
|
|
41
72
|
body: JSON.stringify({ name, ...(slug ? { slug } : {}) }),
|
|
42
73
|
});
|
|
43
74
|
if (res.status === 409) {
|
|
44
|
-
|
|
75
|
+
const body = await errorBody(res);
|
|
76
|
+
if (body.code === "slug_taken") {
|
|
77
|
+
throw new SlugTakenError(Array.isArray(body.suggestions) ? body.suggestions : []);
|
|
78
|
+
}
|
|
45
79
|
}
|
|
46
80
|
if (!res.ok) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
const body = await errorBody(res);
|
|
82
|
+
// A 401 is the auth cutover, not a project error: authFailureMessage owns
|
|
83
|
+
// that wording. Every other status prefers the server's own message.
|
|
84
|
+
throw new BackendError(res.status === 401
|
|
85
|
+
? authFailureMessage("create project failed", res.status, await safeText(res))
|
|
86
|
+
: (body.message ??
|
|
87
|
+
`create project failed (${res.status}): ${await safeText(res)}`), res.status);
|
|
88
|
+
}
|
|
89
|
+
const p = parseProjectView(await res.json(), "create project");
|
|
90
|
+
return {
|
|
91
|
+
projectId: p.id,
|
|
92
|
+
slug: p.slug,
|
|
93
|
+
name: p.name,
|
|
94
|
+
orgId: p.orgId,
|
|
95
|
+
liveUrl: p.liveUrl,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Read a project by id. Backs `link`; the org is derived server-side from the row. */
|
|
99
|
+
export async function getProject(cfg, projectId) {
|
|
100
|
+
const res = await authFetch(cfg, `/projects/${encodeURIComponent(projectId)}`);
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await errorBody(res);
|
|
103
|
+
if (res.status === 404) {
|
|
104
|
+
throw new ProjectUnreachableError(body.message ?? "This project is not available to you.");
|
|
105
|
+
}
|
|
106
|
+
throw new BackendError(res.status === 401
|
|
107
|
+
? authFailureMessage("get project failed", res.status, await safeText(res))
|
|
108
|
+
: (body.message ?? `get project failed (${res.status}): ${await safeText(res)}`), res.status);
|
|
53
109
|
}
|
|
54
|
-
return
|
|
110
|
+
return parseProjectView(await res.json(), "get project");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The wire shape is `id`, not `_id`. The legacy tolerance (`p._id ?? p.id`) is
|
|
114
|
+
* deliberately gone: it would have let an iteration-2-shaped response through
|
|
115
|
+
* silently, which is exactly how a contract break survives two green suites.
|
|
116
|
+
*/
|
|
117
|
+
function parseProjectView(raw, what) {
|
|
118
|
+
const p = (raw ?? {});
|
|
119
|
+
const str = (k) => typeof p[k] === "string" ? p[k] : undefined;
|
|
120
|
+
const id = str("id");
|
|
121
|
+
const slug = str("slug");
|
|
122
|
+
if (!id || !slug) {
|
|
123
|
+
throw new BackendError(`${what}: response missing id/slug`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
slug,
|
|
128
|
+
name: str("name") ?? slug,
|
|
129
|
+
orgId: str("orgId") ?? "",
|
|
130
|
+
liveUrl: str("liveUrl") ?? "",
|
|
131
|
+
publishState: str("publishState") ?? "never",
|
|
132
|
+
};
|
|
55
133
|
}
|
|
56
134
|
/** Mint a per-session dev tunnel for `localPort` (the backend holds CF creds). */
|
|
57
135
|
export async function mintTunnel(cfg, projectId, localPort) {
|
|
@@ -61,7 +139,7 @@ export async function mintTunnel(cfg, projectId, localPort) {
|
|
|
61
139
|
body: JSON.stringify({ port: localPort }),
|
|
62
140
|
});
|
|
63
141
|
if (!res.ok) {
|
|
64
|
-
throw new BackendError(
|
|
142
|
+
throw new BackendError(authFailureMessage("tunnel mint failed", res.status, await safeText(res)), res.status);
|
|
65
143
|
}
|
|
66
144
|
return (await res.json());
|
|
67
145
|
}
|
|
@@ -130,18 +208,21 @@ export async function publishUpload(cfg, projectId, distDir, opts = {}) {
|
|
|
130
208
|
body: form,
|
|
131
209
|
});
|
|
132
210
|
if (!res.ok) {
|
|
133
|
-
throw new BackendError(
|
|
211
|
+
throw new BackendError(authFailureMessage("publish failed", res.status, await safeText(res)), res.status);
|
|
134
212
|
}
|
|
135
213
|
return (await res.json());
|
|
136
214
|
}
|
|
137
215
|
/** Resolve the signed-in identity for `vincentt whoami`. Throws on any non-2xx so
|
|
138
216
|
* the caller can degrade to "token present, unverified". */
|
|
139
217
|
export async function getMe(cfg) {
|
|
140
|
-
const res = await authFetch(cfg, "/
|
|
218
|
+
const res = await authFetch(cfg, "/me");
|
|
141
219
|
if (!res.ok) {
|
|
142
|
-
throw new BackendError(
|
|
220
|
+
throw new BackendError(authFailureMessage("Identity check failed", res.status, await safeText(res)), res.status);
|
|
143
221
|
}
|
|
144
|
-
|
|
222
|
+
// GET /me returns { me: {...}, csrfToken }; unwrap it, but tolerate a bare user
|
|
223
|
+
// object so a contract tweak doesn't break whoami.
|
|
224
|
+
const body = (await res.json());
|
|
225
|
+
return (body.me ?? body);
|
|
145
226
|
}
|
|
146
227
|
/** The working tree's current commit, for publish provenance. Undefined if not a repo. */
|
|
147
228
|
export async function gitHeadSha(cwd) {
|
|
@@ -7,7 +7,32 @@ export declare function httpRelayClient(baseUrl: string): RelayClient;
|
|
|
7
7
|
export declare function formatLog(e: LogEvent, now: number): string;
|
|
8
8
|
export declare function formatNetwork(e: NetworkEvent, now: number): string;
|
|
9
9
|
export declare function formatTrace(e: TraceEvent, now: number): string;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Render a RelayResult for one kind into the text block a tool returns.
|
|
12
|
+
*
|
|
13
|
+
* EVERY field is read defensively, because the CLI and the relay are versioned
|
|
14
|
+
* SEPARATELY on a user's machine even though they ship from one monorepo:
|
|
15
|
+
* `npx vincentt` fetches whatever the registry has, and points it at whatever
|
|
16
|
+
* relay `vincentt preview` happens to be running. A field this renderer reads
|
|
17
|
+
* unguarded is a field whose removal from the relay CRASHES an older CLI.
|
|
18
|
+
*
|
|
19
|
+
* That is not hypothetical — it is exactly what removing `sessions` did:
|
|
20
|
+
* `result.sessions.length` on a response that no longer carried the field took
|
|
21
|
+
* down the whole command, while `--json` kept working because it never touched
|
|
22
|
+
* it. A missing optional array must degrade to a missing LINE, never to an
|
|
23
|
+
* exception that hides the events the creator asked for.
|
|
24
|
+
*/
|
|
11
25
|
export declare function renderResult(result: RelayResult, kind: DiagEvent["kind"], now: number): string;
|
|
12
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Only-errors convenience filter applied on top of a log query result.
|
|
28
|
+
*
|
|
29
|
+
* EVERY index-parallel array is filtered with the SAME predicate rather than
|
|
30
|
+
* passed through: dropping events without dropping their tags would shift each
|
|
31
|
+
* remaining tag by one and attribute every error to the wrong tester — which is
|
|
32
|
+
* worse than no attribution, because it looks right.
|
|
33
|
+
*
|
|
34
|
+
* Adding a parallel array to RelayResult means adding it here too. `eventTesters`
|
|
35
|
+
* arrived with tester attribution and was missed on the first pass, which is
|
|
36
|
+
* exactly the failure this comment already described.
|
|
37
|
+
*/
|
|
13
38
|
export declare function filterErrors(result: RelayResult): RelayResult;
|
package/dist/lib/diagnostics.js
CHANGED
|
@@ -36,26 +36,91 @@ export function formatTrace(e, now) {
|
|
|
36
36
|
return (`${e.fps.toFixed(0)}fps over ${e.windowMs}ms · longest task ${e.longestTaskMs}ms · ` +
|
|
37
37
|
`${e.longTaskCount} long task(s)${marks} · ${ago(e.t, now)} #${e.seq}`);
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Render a RelayResult for one kind into the text block a tool returns.
|
|
41
|
+
*
|
|
42
|
+
* EVERY field is read defensively, because the CLI and the relay are versioned
|
|
43
|
+
* SEPARATELY on a user's machine even though they ship from one monorepo:
|
|
44
|
+
* `npx vincentt` fetches whatever the registry has, and points it at whatever
|
|
45
|
+
* relay `vincentt preview` happens to be running. A field this renderer reads
|
|
46
|
+
* unguarded is a field whose removal from the relay CRASHES an older CLI.
|
|
47
|
+
*
|
|
48
|
+
* That is not hypothetical — it is exactly what removing `sessions` did:
|
|
49
|
+
* `result.sessions.length` on a response that no longer carried the field took
|
|
50
|
+
* down the whole command, while `--json` kept working because it never touched
|
|
51
|
+
* it. A missing optional array must degrade to a missing LINE, never to an
|
|
52
|
+
* exception that hides the events the creator asked for.
|
|
53
|
+
*/
|
|
40
54
|
export function renderResult(result, kind, now) {
|
|
41
|
-
const
|
|
55
|
+
const events = result.events ?? [];
|
|
56
|
+
const latestSeq = typeof result.latestSeq === "number" ? result.latestSeq : -1;
|
|
57
|
+
const lines = events.map((e) => {
|
|
42
58
|
if (e.kind === "log")
|
|
43
59
|
return formatLog(e, now);
|
|
44
60
|
if (e.kind === "network")
|
|
45
61
|
return formatNetwork(e, now);
|
|
46
62
|
return formatTrace(e, now);
|
|
47
63
|
});
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
// Named whenever there is more than one tester, because a merged stream from
|
|
65
|
+
// four phones is not debuggable and the agent has to be told the filter
|
|
66
|
+
// exists.
|
|
67
|
+
//
|
|
68
|
+
// There is NO `sessions:` line. It printed the raw client-supplied session key
|
|
69
|
+
// to the terminal and into MCP tool output — a viewer-controlled string with
|
|
70
|
+
// no validation, on the path the viewer label is carefully guarded on. The
|
|
71
|
+
// tester id replaces it and is minted here, on this machine.
|
|
72
|
+
const header = [testersLine(result.testers), viewersLine(result.viewers)]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join("\n");
|
|
75
|
+
const prefix = header ? `${header}\n` : "";
|
|
76
|
+
const cursor = `\n\n(latestSeq ${latestSeq} — pass since=${latestSeq} to get only newer ${kind} events)`;
|
|
50
77
|
if (lines.length === 0) {
|
|
51
|
-
return `${
|
|
78
|
+
return `${prefix}No ${kind} events${latestSeq >= 0 ? " matched" : " yet — is the preview app open on the device?"}.${latestSeq >= 0 ? cursor : ""}`;
|
|
52
79
|
}
|
|
53
|
-
return `${
|
|
80
|
+
return `${prefix}${lines.join("\n")}${cursor}`;
|
|
54
81
|
}
|
|
55
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* The tester line leads, because the tester is what an agent should filter on:
|
|
84
|
+
* it survives the reload a tester does before reporting a bug, so it is the
|
|
85
|
+
* coordinate that still resolves by the time anyone reads this.
|
|
86
|
+
*/
|
|
87
|
+
function testersLine(testers) {
|
|
88
|
+
if (!testers || testers.length < 2)
|
|
89
|
+
return "";
|
|
90
|
+
return `testers: ${testers.join(", ")} (pass tester to filter to one)`;
|
|
91
|
+
}
|
|
92
|
+
function viewersLine(viewers) {
|
|
93
|
+
if (!viewers || viewers.length < 2)
|
|
94
|
+
return "";
|
|
95
|
+
// "connection", not "device": one tab opens several of these, so calling a
|
|
96
|
+
// viewer a device is the wrong count in the same words that made "2 viewers"
|
|
97
|
+
// wrong on the roster.
|
|
98
|
+
return `viewers: ${viewers.join(", ")} (pass viewer to filter to one connection)`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Only-errors convenience filter applied on top of a log query result.
|
|
102
|
+
*
|
|
103
|
+
* EVERY index-parallel array is filtered with the SAME predicate rather than
|
|
104
|
+
* passed through: dropping events without dropping their tags would shift each
|
|
105
|
+
* remaining tag by one and attribute every error to the wrong tester — which is
|
|
106
|
+
* worse than no attribution, because it looks right.
|
|
107
|
+
*
|
|
108
|
+
* Adding a parallel array to RelayResult means adding it here too. `eventTesters`
|
|
109
|
+
* arrived with tester attribution and was missed on the first pass, which is
|
|
110
|
+
* exactly the failure this comment already described.
|
|
111
|
+
*/
|
|
56
112
|
export function filterErrors(result) {
|
|
113
|
+
const events = result.events ?? [];
|
|
114
|
+
const keep = events.map((e) => e.kind === "log" && e.level === "error");
|
|
115
|
+
const parallel = (a) => a === undefined ? undefined : a.filter((_, i) => keep[i]);
|
|
57
116
|
return {
|
|
58
117
|
...result,
|
|
59
|
-
events:
|
|
118
|
+
events: events.filter((_, i) => keep[i]),
|
|
119
|
+
...(result.eventViewers === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { eventViewers: parallel(result.eventViewers) }),
|
|
122
|
+
...(result.eventTesters === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { eventTesters: parallel(result.eventTesters) }),
|
|
60
125
|
};
|
|
61
126
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface InitDeps {
|
|
2
|
+
print: (line: string) => void;
|
|
3
|
+
cwd: string;
|
|
4
|
+
scaffold?: (targetDir: string) => Promise<void>;
|
|
5
|
+
install?: (targetDir: string) => Promise<string>;
|
|
6
|
+
isEmpty?: (targetDir: string) => Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export interface InitOptions {
|
|
9
|
+
/** Positional target directory; defaults to the cwd. */
|
|
10
|
+
dir?: string;
|
|
11
|
+
/** Skip the dependency install (CI, or a creator who wants to pick a manager). */
|
|
12
|
+
skipInstall?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function initCommand(deps: InitDeps, opts?: InitOptions): Promise<number>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// `vincentt init [dir]` — scaffold a Vincentt XR app. NO account, NO network call
|
|
2
|
+
// to the API, NO binding written.
|
|
3
|
+
//
|
|
4
|
+
// This is the acquisition wedge: local-first is a smaller room, not a wall, so the
|
|
5
|
+
// template must be reachable without ever touching the auth gate. `create` (which
|
|
6
|
+
// does need an account) is offered at the end as the next thing AVAILABLE, never as
|
|
7
|
+
// a requirement.
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { detectPackageManager, installDependencies, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
10
|
+
export async function initCommand(deps, opts = {}) {
|
|
11
|
+
const target = path.resolve(deps.cwd, opts.dir ?? ".");
|
|
12
|
+
const label = opts.dir ? `./${opts.dir}` : ".";
|
|
13
|
+
const isEmpty = deps.isEmpty ?? needsScaffold;
|
|
14
|
+
if (!(await isEmpty(target))) {
|
|
15
|
+
deps.print(`✗ ${label} already contains an app (package.json).`);
|
|
16
|
+
deps.print("");
|
|
17
|
+
deps.print(" vincentt init <new-dir> Scaffold somewhere else");
|
|
18
|
+
deps.print(" vincentt create Register THIS folder as a project");
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
deps.print(`→ Scaffolding a Vincentt XR app in ${label}…`);
|
|
22
|
+
deps.print(" · React + react-three-fiber + @vincentt-xr/sdk");
|
|
23
|
+
const scaffold = deps.scaffold ?? ((dir) => scaffoldFromTemplate(dir));
|
|
24
|
+
try {
|
|
25
|
+
await scaffold(target);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
deps.print("");
|
|
29
|
+
deps.print(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
30
|
+
deps.print("");
|
|
31
|
+
deps.print(" Run `vincentt init` again once that is resolved.");
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
let installed = true;
|
|
35
|
+
if (!opts.skipInstall) {
|
|
36
|
+
deps.print(" · installing dependencies");
|
|
37
|
+
const install = deps.install ?? ((dir) => installDependencies(dir));
|
|
38
|
+
try {
|
|
39
|
+
await install(target);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
installed = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// The template ships a pnpm lockfile and a hoisted node-linker, so telling a
|
|
46
|
+
// creator to `npm run dev` in it produces a second lockfile and an unproven
|
|
47
|
+
// node_modules layout. Name the manager the scaffold is actually set up for.
|
|
48
|
+
const pm = await detectPackageManager(target).catch(() => "npm");
|
|
49
|
+
deps.print("");
|
|
50
|
+
deps.print(`✓ Created ${label}`);
|
|
51
|
+
deps.print("");
|
|
52
|
+
const steps = [];
|
|
53
|
+
if (opts.dir)
|
|
54
|
+
steps.push([`cd ${opts.dir}`, ""]);
|
|
55
|
+
if (!installed || opts.skipInstall) {
|
|
56
|
+
steps.push([`${pm} install`, "Install dependencies"]);
|
|
57
|
+
}
|
|
58
|
+
steps.push([`${pm} run dev`, "Start the local dev server"]);
|
|
59
|
+
for (const line of alignedSteps(steps))
|
|
60
|
+
deps.print(line);
|
|
61
|
+
deps.print("");
|
|
62
|
+
deps.print(" No account needed. When you're ready to put it online:");
|
|
63
|
+
for (const line of alignedSteps([
|
|
64
|
+
["vincentt login", "Connect this machine to Vincentt"],
|
|
65
|
+
["vincentt create", "Register the project and reserve its address"],
|
|
66
|
+
])) {
|
|
67
|
+
deps.print(line);
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
/** Two columns, aligned on the widest command, so the block reads as a table. */
|
|
72
|
+
function alignedSteps(steps) {
|
|
73
|
+
const width = Math.max(...steps.map(([cmd]) => cmd.length));
|
|
74
|
+
return steps.map(([cmd, note]) => note ? ` ${cmd.padEnd(width)} ${note}` : ` ${cmd}`);
|
|
75
|
+
}
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -1,25 +1,64 @@
|
|
|
1
|
+
import { type ResolvedOrg } from "./org.js";
|
|
1
2
|
import { type RelayClient } from "./diagnostics.js";
|
|
2
|
-
import { type
|
|
3
|
+
import { type ResolvedConfig } from "../shared/config.js";
|
|
4
|
+
import { type RunningPreview, type StartPreviewOptions } from "../preview/index.js";
|
|
3
5
|
import type { DiagEventKind, RelayResult } from "@vincentt-xr/harness/events";
|
|
4
6
|
export interface CreateOptions {
|
|
5
7
|
name?: string;
|
|
6
8
|
slug?: string;
|
|
7
|
-
|
|
8
|
-
/** Rebind a directory that already has a binding (drops the old one). */
|
|
9
|
+
/** Replace a binding this folder already has. */
|
|
9
10
|
force?: boolean;
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/** --org <slug>; else VINCENTT_ORG; else the machine config's active org. */
|
|
12
|
+
org?: string;
|
|
13
|
+
/** Honor a binding's apiUrl override (--allow-custom-api). */
|
|
14
|
+
allowCustomApi?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Stream the transcript as it happens. The CLI passes console.log so a slow
|
|
17
|
+
* create shows progress and the apiUrl advisory lands before the request; MCP
|
|
18
|
+
* omits it and reads the buffered `message`.
|
|
19
|
+
*/
|
|
20
|
+
onLine?: (line: string) => void;
|
|
12
21
|
}
|
|
13
22
|
export interface CreateResult {
|
|
23
|
+
/** The full transcript, exactly as the CLI prints it. */
|
|
14
24
|
message: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
projectId?: string;
|
|
25
|
+
/** Non-zero when nothing was created (already linked, address taken, no org). */
|
|
26
|
+
exitCode: number;
|
|
18
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The shared `create` orchestration behind both `vincentt create` and the MCP
|
|
30
|
+
* `project_create` tool, so the two surfaces can never diverge in behavior OR in
|
|
31
|
+
* what they tell the caller.
|
|
32
|
+
*
|
|
33
|
+
* It does NOT scaffold — that is `vincentt init`, which needs no account. See
|
|
34
|
+
* `initCommand`.
|
|
35
|
+
*/
|
|
19
36
|
export declare function projectCreate(projectCwd: string, opts?: CreateOptions): Promise<CreateResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve which org a create acts in: the named one if it is an ACTIVE membership,
|
|
39
|
+
* else the personal org when nothing is named. A named org the caller is not an
|
|
40
|
+
* active member of resolves to nothing rather than silently falling back — acting
|
|
41
|
+
* in an org the creator did not ask for is the retargeting the binding rules forbid.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveOrgForCommand(cfg: ResolvedConfig, input: {
|
|
44
|
+
flag?: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
ok: true;
|
|
47
|
+
org: ResolvedOrg;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
message: string;
|
|
51
|
+
}>;
|
|
20
52
|
export interface PublishOptions {
|
|
21
53
|
distDir?: string;
|
|
22
54
|
note?: string;
|
|
55
|
+
allowCustomApi?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Where the apiUrl advisory goes. It must reach the creator BEFORE the request,
|
|
58
|
+
* so it cannot ride the returned message — that is only rendered after the call
|
|
59
|
+
* has already happened (or thrown).
|
|
60
|
+
*/
|
|
61
|
+
warn?: (line: string) => void;
|
|
23
62
|
}
|
|
24
63
|
export interface PublishOutcome {
|
|
25
64
|
message: string;
|
|
@@ -35,7 +74,14 @@ export interface PreviewStartResult {
|
|
|
35
74
|
/** false when a preview was already running (the existing one is returned). */
|
|
36
75
|
started: boolean;
|
|
37
76
|
}
|
|
38
|
-
|
|
77
|
+
/** Everything the runner needs that this process does not already know. */
|
|
78
|
+
export type PreviewStartOptions = Omit<StartPreviewOptions, "projectCwd" | "config" | "projectId">;
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the binding and the config, then start. The binding is a HINT and
|
|
81
|
+
* never an authority (B12): the projectId it carries is presented to the API,
|
|
82
|
+
* which resolves the org itself and answers 404 for every unreachable cause.
|
|
83
|
+
*/
|
|
84
|
+
export declare function previewStart(projectCwd: string, onLog: (msg: string) => void, extra?: PreviewStartOptions): Promise<PreviewStartResult>;
|
|
39
85
|
export declare function previewStop(): Promise<boolean>;
|
|
40
86
|
export interface PreviewStopOutcome {
|
|
41
87
|
stopped: boolean;
|
|
@@ -44,18 +90,30 @@ export interface PreviewStopOutcome {
|
|
|
44
90
|
/**
|
|
45
91
|
* Stop a preview started by a *different* process (the common case: `vincentt
|
|
46
92
|
* preview` runs in the background and the agent later wants it down). Reads the
|
|
47
|
-
* pidfile and signals the recorded
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* the
|
|
93
|
+
* pidfile and signals the recorded pid rather than the in-process
|
|
94
|
+
* `activePreview`.
|
|
95
|
+
*
|
|
96
|
+
* There is NO orphaned-tunnel case any more, and that is the point of the edge
|
|
97
|
+
* repoint: the tunnel is a socket this process owns rather than a child process
|
|
98
|
+
* that could be reparented to init. When the preview process dies the socket
|
|
99
|
+
* closes, the edge sees the tunnel go, and the session drains on its own — so
|
|
100
|
+
* SIGTERM plus clearing the stale relay info is the whole of the cleanup.
|
|
52
101
|
*/
|
|
53
102
|
export declare function previewStopSignal(projectCwd: string): Promise<PreviewStopOutcome>;
|
|
54
103
|
export interface DiagQueryOptions {
|
|
55
|
-
sessionId?: string;
|
|
56
104
|
since?: number;
|
|
57
105
|
limit?: number;
|
|
58
106
|
errorsOnly?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* One tester — one browser tab, across its refreshes. The filter that answers
|
|
109
|
+
* "it breaks on their phone but not mine". Absent = every tester.
|
|
110
|
+
*/
|
|
111
|
+
tester?: string;
|
|
112
|
+
/**
|
|
113
|
+
* One CONNECTION (2.8), the sub-coordinate below the tester. Absent = every
|
|
114
|
+
* viewer, which is the default read.
|
|
115
|
+
*/
|
|
116
|
+
viewer?: string;
|
|
59
117
|
}
|
|
60
118
|
/** Raw diagnostics query against the running relay (used by CLI --json). */
|
|
61
119
|
export declare function diagQuery(projectCwd: string, kind: DiagEventKind, opts?: DiagQueryOptions, relay?: RelayClient): Promise<RelayResult>;
|