@vincentt-xr/cli 0.16.0 → 0.18.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/MIGRATION.md +24 -0
- package/README.md +94 -1
- package/dist/cli/index.js +196 -87
- package/dist/cli/usage-text.js +32 -10
- package/dist/domain/api.d.ts +10 -8
- package/dist/domain/api.js +14 -9
- package/dist/domain/command.js +25 -16
- package/dist/domain/copy.d.ts +6 -3
- package/dist/domain/copy.js +10 -6
- package/dist/dx/command.d.ts +24 -4
- package/dist/dx/command.js +80 -25
- package/dist/dx/consent.js +6 -5
- package/dist/dx/journal.d.ts +17 -2
- package/dist/dx/journal.js +19 -3
- package/dist/dx/report.js +4 -3
- package/dist/dx/reportIo.js +6 -5
- package/dist/dx/send.js +6 -5
- package/dist/lib/backend.d.ts +0 -16
- package/dist/lib/backend.js +2 -26
- package/dist/lib/detect.d.ts +87 -0
- package/dist/lib/detect.js +867 -0
- package/dist/lib/diagnostics.js +2 -2
- package/dist/lib/feedback.js +1 -1
- package/dist/lib/feedbackCopy.js +1 -1
- package/dist/lib/initFrom.js +4 -4
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +9 -3
- package/dist/lib/listProjects.js +2 -1
- package/dist/lib/projects.d.ts +37 -2
- package/dist/lib/projects.js +86 -21
- package/dist/lib/settings.d.ts +113 -0
- package/dist/lib/settings.js +284 -0
- package/dist/lib/settingsCopy.d.ts +6 -0
- package/dist/lib/settingsCopy.js +99 -0
- package/dist/lib/settle.d.ts +61 -0
- package/dist/lib/settle.js +338 -0
- package/dist/lib/template.d.ts +19 -0
- package/dist/lib/template.js +73 -0
- package/dist/lib/terminalText.d.ts +3 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/version.js +5 -11
- package/dist/lib/whoami.js +10 -3
- package/dist/library/assetsCommand.js +1 -1
- package/dist/library/client.js +8 -3
- package/dist/library/render.js +8 -6
- package/dist/login/authBackend.js +3 -2
- package/dist/login/ensure.d.ts +8 -2
- package/dist/login/ensure.js +21 -10
- package/dist/login/guard.js +5 -4
- package/dist/login/loginCommand.d.ts +2 -2
- package/dist/login/loginCommand.js +29 -5
- package/dist/login/pkce.js +1 -1
- package/dist/login/pkceLogin.js +1 -1
- package/dist/platform/outdated.js +2 -2
- package/dist/platform/upgradeReport.js +16 -12
- package/dist/preview/api.d.ts +24 -11
- package/dist/preview/api.js +42 -15
- package/dist/preview/command.d.ts +29 -0
- package/dist/preview/command.js +61 -1
- package/dist/preview/copy.d.ts +35 -3
- package/dist/preview/copy.js +102 -10
- package/dist/preview/proxy.d.ts +38 -1
- package/dist/preview/proxy.js +151 -2
- package/dist/preview/qrPage.js +1 -1
- package/dist/preview/runner.d.ts +25 -13
- package/dist/preview/runner.js +127 -26
- package/dist/preview/viewers.js +1 -1
- package/dist/publish/api.d.ts +39 -6
- package/dist/publish/api.js +80 -9
- package/dist/publish/command.d.ts +25 -0
- package/dist/publish/command.js +230 -32
- package/dist/publish/copy.d.ts +78 -5
- package/dist/publish/copy.js +271 -37
- package/dist/publish/fit.d.ts +15 -0
- package/dist/publish/fit.js +103 -0
- package/dist/publish/manifest.d.ts +44 -3
- package/dist/publish/manifest.js +172 -3
- package/dist/publish/prompt.d.ts +4 -0
- package/dist/publish/prompt.js +15 -1
- package/dist/publish/upload.d.ts +6 -0
- package/dist/publish/upload.js +11 -1
- package/dist/publish/visibility.d.ts +21 -0
- package/dist/publish/visibility.js +68 -0
- package/dist/relay/annotations.js +9 -4
- package/dist/relay/server.d.ts +21 -0
- package/dist/relay/server.js +35 -1
- package/dist/shared/authError.d.ts +27 -0
- package/dist/shared/authError.js +48 -4
- package/dist/shared/config.d.ts +21 -1
- package/dist/shared/config.js +39 -8
- package/dist/shared/countLimit.d.ts +114 -0
- package/dist/shared/countLimit.js +176 -0
- package/dist/shared/http.d.ts +23 -0
- package/dist/shared/http.js +58 -0
- package/package.json +7 -3
package/dist/lib/diagnostics.js
CHANGED
|
@@ -73,9 +73,9 @@ export function renderResult(result, kind, now) {
|
|
|
73
73
|
.filter(Boolean)
|
|
74
74
|
.join("\n");
|
|
75
75
|
const prefix = header ? `${header}\n` : "";
|
|
76
|
-
const cursor = `\n\n(latestSeq ${latestSeq}
|
|
76
|
+
const cursor = `\n\n(latestSeq ${latestSeq}; pass since=${latestSeq} to get only newer ${kind} events)`;
|
|
77
77
|
if (lines.length === 0) {
|
|
78
|
-
return `${prefix}No ${kind} events${latestSeq >= 0 ? " matched" : " yet
|
|
78
|
+
return `${prefix}No ${kind} events${latestSeq >= 0 ? " matched" : " yet (is the preview app open on the device?)"}.${latestSeq >= 0 ? cursor : ""}`;
|
|
79
79
|
}
|
|
80
80
|
return `${prefix}${lines.join("\n")}${cursor}`;
|
|
81
81
|
}
|
package/dist/lib/feedback.js
CHANGED
|
@@ -89,7 +89,7 @@ export async function fetchNext(cwd, opts) {
|
|
|
89
89
|
const info = await readRelayInfo(cwd);
|
|
90
90
|
if (!info)
|
|
91
91
|
return { kind: "closed", reason: "unreachable" };
|
|
92
|
-
const base = `http://
|
|
92
|
+
const base = `http://127.0.0.1:${info.relayPort}`;
|
|
93
93
|
const u = new URL(`${info.harnessPath}/feedback/next`, base);
|
|
94
94
|
u.searchParams.set("project", cwd);
|
|
95
95
|
u.searchParams.set("after", String(opts.after));
|
package/dist/lib/feedbackCopy.js
CHANGED
|
@@ -86,7 +86,7 @@ export function sessionEnded(handed) {
|
|
|
86
86
|
"",
|
|
87
87
|
"✗ The preview ended. Nothing more can arrive on it.",
|
|
88
88
|
"",
|
|
89
|
-
` Nothing was missed
|
|
89
|
+
` Nothing was missed: the ${handed} already handed over stay handed over.`,
|
|
90
90
|
"",
|
|
91
91
|
];
|
|
92
92
|
}
|
package/dist/lib/initFrom.js
CHANGED
|
@@ -22,7 +22,7 @@ const ORG_SOURCE_LABEL = {
|
|
|
22
22
|
};
|
|
23
23
|
/** The one sentence every registration-half failure opens with. Byte-identical across arms. */
|
|
24
24
|
function reassure(print, dirLabel) {
|
|
25
|
-
print(` Your source is fine
|
|
25
|
+
print(` Your source is fine: ${dirLabel} is on disk and it's yours.`);
|
|
26
26
|
}
|
|
27
27
|
/** The resume block: `cd <dir>` (when there is one) then the exact create command. */
|
|
28
28
|
function resume(print, dirLabel, command, needsCd = true) {
|
|
@@ -89,7 +89,7 @@ export async function registerAfterScaffold(deps, opts = {}) {
|
|
|
89
89
|
// B-F2-9(a): the open route must be named in something the creator's
|
|
90
90
|
// agent reads. On a non-TTY this output IS that artifact — the TTY
|
|
91
91
|
// branch's login prompt, which carries the same line, is unreachable here.
|
|
92
|
-
deps.print(" Or don't
|
|
92
|
+
deps.print(" Or don't: the local dev loop needs no account.");
|
|
93
93
|
}
|
|
94
94
|
deps.print("");
|
|
95
95
|
deps.print(closing);
|
|
@@ -97,7 +97,7 @@ export async function registerAfterScaffold(deps, opts = {}) {
|
|
|
97
97
|
}
|
|
98
98
|
deps.print("");
|
|
99
99
|
deps.print("→ Now registering the project so it gets an address.");
|
|
100
|
-
deps.print(" You're not signed in yet
|
|
100
|
+
deps.print(" You're not signed in yet. Opening the browser to authorize this device…");
|
|
101
101
|
// The open path's load-bearing home, and the only new sentence in this flow.
|
|
102
102
|
// The reader is a human holding working source who is being asked for an
|
|
103
103
|
// account thirty seconds after watching an app appear on their disk — the one
|
|
@@ -212,7 +212,7 @@ export async function registerAfterScaffold(deps, opts = {}) {
|
|
|
212
212
|
// not happening.
|
|
213
213
|
deps.print("");
|
|
214
214
|
deps.print(scaffolded
|
|
215
|
-
? "! That address is taken. Your source is fine
|
|
215
|
+
? "! That address is taken. Your source is fine; pick another."
|
|
216
216
|
: "! That address is taken. Pick another.");
|
|
217
217
|
if (err.suggestions.length > 0) {
|
|
218
218
|
deps.print("");
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface CreateOptions {
|
|
|
26
26
|
runLogin?: () => Promise<void>;
|
|
27
27
|
/** Where the resolved org came from, so a surprising value is diagnosable. */
|
|
28
28
|
orgSource?: () => OrgSource;
|
|
29
|
+
/** The configured console base, named by the occupied-folder refusal. */
|
|
30
|
+
consoleUrl?: string;
|
|
29
31
|
/**
|
|
30
32
|
* Stream the transcript as it happens. The CLI passes console.log so a slow
|
|
31
33
|
* create shows progress and the apiUrl advisory lands before the request; MCP
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -55,6 +55,7 @@ export async function projectCreate(projectCwd, opts = {}) {
|
|
|
55
55
|
currentToken: opts.currentToken,
|
|
56
56
|
runLogin: opts.runLogin,
|
|
57
57
|
orgSource: opts.orgSource,
|
|
58
|
+
consoleUrl: opts.consoleUrl,
|
|
58
59
|
}, {
|
|
59
60
|
name: opts.name,
|
|
60
61
|
slug: opts.slug,
|
|
@@ -116,7 +117,11 @@ export async function projectPublish(projectCwd, opts = {}) {
|
|
|
116
117
|
binding: async () => {
|
|
117
118
|
const located = await locateProjectBinding(projectCwd);
|
|
118
119
|
return located
|
|
119
|
-
? {
|
|
120
|
+
? {
|
|
121
|
+
projectId: located.binding.projectId,
|
|
122
|
+
slug: located.binding.slug,
|
|
123
|
+
dir: located.dir,
|
|
124
|
+
}
|
|
120
125
|
: undefined;
|
|
121
126
|
},
|
|
122
127
|
token: async () => process.env.VINCENTT_PAT ?? (await loadMachineConfig()).pat,
|
|
@@ -306,9 +311,10 @@ async function relayForProject(projectCwd, override) {
|
|
|
306
311
|
// error instead of the one sentence that tells the creator what to do.
|
|
307
312
|
const info = await readLivePreview(projectCwd);
|
|
308
313
|
if (!info) {
|
|
309
|
-
throw new Error("No running preview for this project
|
|
314
|
+
throw new Error("No running preview for this project. Run `vincentt preview` first.");
|
|
310
315
|
}
|
|
311
|
-
|
|
316
|
+
// The relay listens on 127.0.0.1 only; `localhost` may resolve to ::1 first.
|
|
317
|
+
return httpRelayClient(`http://127.0.0.1:${info.relayPort}`);
|
|
312
318
|
}
|
|
313
319
|
/** Raw diagnostics query against the running relay (used by CLI --json). */
|
|
314
320
|
export async function diagQuery(projectCwd, kind, opts = {}, relay) {
|
package/dist/lib/listProjects.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// branch is testable with no API, no HOME, and no network.
|
|
22
22
|
import { compactEntries, fileLocationStore, } from "../shared/locationStore.js";
|
|
23
23
|
import { joinLocations, verifyLocation } from "./locations.js";
|
|
24
|
+
import { EXIT } from "./exitCodes.js";
|
|
24
25
|
/** The unknown case, printed rather than omitted. Repeated verbatim in the scaffolded agent contract. */
|
|
25
26
|
export const NO_LOCATION = "no folder known on this machine";
|
|
26
27
|
/** The row's own two spaces, plus four. A CONSTANT — never aligned to a column. */
|
|
@@ -37,7 +38,7 @@ export async function listProjectsCommand(deps, opts = {}) {
|
|
|
37
38
|
const cfg = await deps.config();
|
|
38
39
|
if (!cfg) {
|
|
39
40
|
deps.print("Not signed in. Run `vincentt login`.");
|
|
40
|
-
return
|
|
41
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
41
42
|
}
|
|
42
43
|
const org = await deps.org();
|
|
43
44
|
if (!org.ok) {
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type FolderCase } from "./classifyTarget.js";
|
|
2
|
-
import { type OrgSource } from "./initFrom.js";
|
|
2
|
+
import { registerAfterScaffold, type OrgSource } from "./initFrom.js";
|
|
3
3
|
import type { ScaffoldCall } from "./initCommand.js";
|
|
4
4
|
import { type ScaffoldOutcome } from "../scaffold/index.js";
|
|
5
5
|
import type { Ask } from "../publish/prompt.js";
|
|
6
|
+
import { settleSettings, type JoinFlags } from "./settle.js";
|
|
6
7
|
import { type ResolvedConfig } from "../shared/config.js";
|
|
7
8
|
export interface ProjectCommandDeps {
|
|
8
9
|
/** Print a transcript line. */
|
|
@@ -45,6 +46,10 @@ export interface CreateCommandDeps extends ProjectCommandDeps {
|
|
|
45
46
|
runLogin?: () => Promise<void>;
|
|
46
47
|
/** Where the resolved org came from, so a surprising value is diagnosable (AF-3). */
|
|
47
48
|
orgSource?: () => OrgSource;
|
|
49
|
+
/** The configured console base, named by the occupied-folder refusal. */
|
|
50
|
+
consoleUrl?: string;
|
|
51
|
+
/** Injected for tests. */
|
|
52
|
+
settle?: typeof settleSettings;
|
|
48
53
|
}
|
|
49
54
|
export interface CreateCommandOptions {
|
|
50
55
|
name?: string;
|
|
@@ -67,6 +72,8 @@ export interface CreateCommandOptions {
|
|
|
67
72
|
* (registering a folder that already holds an app) or when resolution failed.
|
|
68
73
|
*/
|
|
69
74
|
templateVersion?: string;
|
|
75
|
+
/** Run detection and settle vincentt.json after the binding write (case 2). */
|
|
76
|
+
settleSettings?: boolean;
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* `vincentt create` — a TWO-HALF verb with one seam.
|
|
@@ -89,6 +96,24 @@ export interface CreateCommandOptions {
|
|
|
89
96
|
* neighbour.
|
|
90
97
|
*/
|
|
91
98
|
export declare function createCommand(deps: CreateCommandDeps, opts?: CreateCommandOptions): Promise<number>;
|
|
99
|
+
export interface ClonedRegistrationDeps {
|
|
100
|
+
/** Resolve the API host + PAT for the cloned directory. */
|
|
101
|
+
config: (dir: string) => Promise<ResolvedConfig>;
|
|
102
|
+
/** The org to register in, for the cloned directory. */
|
|
103
|
+
org: (dir: string) => ReturnType<CreateCommandDeps["org"]>;
|
|
104
|
+
currentToken: () => Promise<string | undefined>;
|
|
105
|
+
runLogin: () => Promise<void>;
|
|
106
|
+
orgSource: () => OrgSource;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The registration half of `init --from`: `registerAfterScaffold` over
|
|
110
|
+
* `registerProject`, never over `createCommand`, which would classify and scaffold
|
|
111
|
+
* a second time inside the half that already cloned.
|
|
112
|
+
*/
|
|
113
|
+
export declare function registerClonedProject(deps: ClonedRegistrationDeps, rd: Pick<Parameters<typeof registerAfterScaffold>[0], "print" | "targetDir" | "dirLabel" | "ask">, opts: {
|
|
114
|
+
name?: string;
|
|
115
|
+
slug?: string;
|
|
116
|
+
}): Promise<number>;
|
|
92
117
|
/**
|
|
93
118
|
* The registration half — the binding guard's remainder, the slug derivation, the
|
|
94
119
|
* round trip, and the binding write. Extracted from `createCommand` so
|
|
@@ -105,7 +130,17 @@ export declare function registerProject(deps: CreateCommandDeps, opts?: CreateCo
|
|
|
105
130
|
* "which folder", so linking is entirely client-side, gated only by the server's
|
|
106
131
|
* willingness to return the project at all.
|
|
107
132
|
*/
|
|
108
|
-
export
|
|
133
|
+
export interface LinkOptions {
|
|
134
|
+
/** `--dev-command`, `--dev-port`, `--build-command`, `--build-output`. */
|
|
135
|
+
flags?: JoinFlags;
|
|
136
|
+
/** Machine mode: one JSON object on stdout, never a prompt. */
|
|
137
|
+
json?: boolean;
|
|
138
|
+
/** Present only on a TTY. */
|
|
139
|
+
ask?: Ask;
|
|
140
|
+
/** Injected for tests. */
|
|
141
|
+
settle?: typeof settleSettings;
|
|
142
|
+
}
|
|
143
|
+
export declare function linkCommand(deps: ProjectCommandDeps, projectId: string | undefined, opts?: LinkOptions): Promise<number>;
|
|
109
144
|
/**
|
|
110
145
|
* The API's error messages are lowercase and unpunctuated by convention
|
|
111
146
|
* (`that handle is taken`, `this project is not available to you`), while both
|
package/dist/lib/projects.js
CHANGED
|
@@ -12,7 +12,10 @@ import { EXIT } from "./exitCodes.js";
|
|
|
12
12
|
import { registerAfterScaffold } from "./initFrom.js";
|
|
13
13
|
import { slugify } from "./slug.js";
|
|
14
14
|
import { detectPackageManager, installDependencies, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
15
|
+
import { settleSettings } from "./settle.js";
|
|
16
|
+
import { escapeControl } from "./terminalText.js";
|
|
15
17
|
import { clearProjectBinding, loadProjectBinding, locateProjectBinding, writeProjectBinding, } from "../shared/config.js";
|
|
18
|
+
const DEFAULT_CONSOLE_URL = "https://console.vincentt.studio";
|
|
16
19
|
/**
|
|
17
20
|
* `vincentt create` — a TWO-HALF verb with one seam.
|
|
18
21
|
*
|
|
@@ -69,15 +72,30 @@ export async function createCommand(deps, opts = {}) {
|
|
|
69
72
|
// unforeseen errno. The CLI could not establish that the target is safe to
|
|
70
73
|
// write into; that is one fact with one remedy pair. A per-cause message
|
|
71
74
|
// would advertise which filenames the predicate has opinions about, which is
|
|
72
|
-
// the allowlist this design refuses to open.
|
|
75
|
+
// the allowlist this design refuses to open. The one distinction drawn is
|
|
76
|
+
// whether a package.json exists, which the screen must state truthfully.
|
|
77
|
+
const hasPackageJson = await fs
|
|
78
|
+
.lstat(path.join(target, "package.json"))
|
|
79
|
+
.then((s) => s.isFile())
|
|
80
|
+
.catch(() => false);
|
|
81
|
+
const consoleUrl = (deps.consoleUrl ?? DEFAULT_CONSOLE_URL).replace(/\/$/, "");
|
|
73
82
|
deps.print("✗ This folder isn't a Vincentt app, and it isn't empty.");
|
|
74
83
|
deps.print("");
|
|
75
84
|
deps.print(" `vincentt create` scaffolds an empty folder and registers an existing app.");
|
|
76
|
-
deps.print(
|
|
77
|
-
|
|
85
|
+
deps.print(hasPackageJson
|
|
86
|
+
? " This folder is neither: nothing in its package.json comes from Vincentt."
|
|
87
|
+
: " This folder is neither: it has no package.json.");
|
|
78
88
|
deps.print("");
|
|
79
|
-
deps.print("
|
|
80
|
-
deps.print(
|
|
89
|
+
deps.print(" If this folder holds an app you want on Vincentt:");
|
|
90
|
+
deps.print(` 1. Create the project in the console: ${consoleUrl}`);
|
|
91
|
+
deps.print(" 2. Link this folder to it: vincentt link <project-id>");
|
|
92
|
+
deps.print("");
|
|
93
|
+
deps.print(" Or add the harness, then run `vincentt create` again:");
|
|
94
|
+
deps.print(" npm i @vincentt-xr/harness");
|
|
95
|
+
deps.print("");
|
|
96
|
+
deps.print(" To start from the template instead:");
|
|
97
|
+
deps.print(" vincentt create <name> Scaffold and register in a new folder");
|
|
98
|
+
deps.print(" vincentt init . Scaffold into THIS folder anyway (asks first)");
|
|
81
99
|
deps.print("");
|
|
82
100
|
deps.print(" Nothing was written.");
|
|
83
101
|
return 1;
|
|
@@ -164,6 +182,9 @@ export async function createCommand(deps, opts = {}) {
|
|
|
164
182
|
slug,
|
|
165
183
|
templateVersion,
|
|
166
184
|
rethrowSlugTaken: true,
|
|
185
|
+
// Case 2 registers an app already on disk, so it settles vincentt.json
|
|
186
|
+
// the way `link` does. A fresh scaffold IS the template.
|
|
187
|
+
settleSettings: !scaffolded,
|
|
167
188
|
}),
|
|
168
189
|
},
|
|
169
190
|
// Case 2 keeps the shipped basename default: a creator made that folder and
|
|
@@ -316,6 +337,35 @@ async function scaffoldInto(deps, target, dirLabel, opts) {
|
|
|
316
337
|
deps.print(`✓ Created ${dirLabel}`);
|
|
317
338
|
return { ok: true, installed, pm, templateVersion: scaffoldResult?.templateVersion };
|
|
318
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* The registration half of `init --from`: `registerAfterScaffold` over
|
|
342
|
+
* `registerProject`, never over `createCommand`, which would classify and scaffold
|
|
343
|
+
* a second time inside the half that already cloned.
|
|
344
|
+
*/
|
|
345
|
+
export function registerClonedProject(deps, rd, opts) {
|
|
346
|
+
let cachedOrg;
|
|
347
|
+
const org = async () => (cachedOrg ??= await deps.org(rd.targetDir));
|
|
348
|
+
return registerAfterScaffold({
|
|
349
|
+
...rd,
|
|
350
|
+
currentToken: deps.currentToken,
|
|
351
|
+
runLogin: deps.runLogin,
|
|
352
|
+
resolveOrg: async () => {
|
|
353
|
+
const resolved = await org();
|
|
354
|
+
if (!resolved.ok)
|
|
355
|
+
return resolved;
|
|
356
|
+
return { ok: true, name: resolved.org.name, source: deps.orgSource() };
|
|
357
|
+
},
|
|
358
|
+
create: async ({ name, slug }) => registerProject({
|
|
359
|
+
print: rd.print,
|
|
360
|
+
cwd: rd.targetDir,
|
|
361
|
+
config: () => deps.config(rd.targetDir),
|
|
362
|
+
org,
|
|
363
|
+
},
|
|
364
|
+
// Without this the taken-address refusal prints here and returns, and
|
|
365
|
+
// the bounded re-ask in registerAfterScaffold never sees the 409.
|
|
366
|
+
{ name, slug, rethrowSlugTaken: true }),
|
|
367
|
+
}, opts);
|
|
368
|
+
}
|
|
319
369
|
/**
|
|
320
370
|
* The registration half — the binding guard's remainder, the slug derivation, the
|
|
321
371
|
* round trip, and the binding write. Extracted from `createCommand` so
|
|
@@ -369,6 +419,14 @@ export async function registerProject(deps, opts = {}) {
|
|
|
369
419
|
deps.print(`✓ Created ${created.name} in ${org.org.name}`);
|
|
370
420
|
deps.print(` Address reserved: ${created.liveUrl}`);
|
|
371
421
|
deps.print(` Linked this folder: .vincentt/project.json (added to .gitignore)`);
|
|
422
|
+
if (opts.settleSettings) {
|
|
423
|
+
// Between the binding line and today's closing lines, so the last lines are
|
|
424
|
+
// still the ones a creator acts on. A template prints nothing here.
|
|
425
|
+
await (deps.settle ?? settleSettings)(target, await loadProjectBinding(target), {}, {
|
|
426
|
+
print: deps.print,
|
|
427
|
+
ask: deps.ask,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
372
430
|
deps.print("");
|
|
373
431
|
deps.print(" Nothing is served at that address until you publish.");
|
|
374
432
|
deps.print(" Run `vincentt preview` to try it on your phone.");
|
|
@@ -401,15 +459,7 @@ function printSlugTaken(print, suggestions, name) {
|
|
|
401
459
|
}
|
|
402
460
|
print(` Run \`vincentt create --name "${name}" --slug <address>\` with another address.`);
|
|
403
461
|
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* `vincentt link <project-id>` — one verb for four situations (console-first
|
|
407
|
-
* birth, a fresh clone, a folder that lost its binding, a re-link after
|
|
408
|
-
* `login --force`). It is a READ plus a local write: the server has no notion of
|
|
409
|
-
* "which folder", so linking is entirely client-side, gated only by the server's
|
|
410
|
-
* willingness to return the project at all.
|
|
411
|
-
*/
|
|
412
|
-
export async function linkCommand(deps, projectId) {
|
|
462
|
+
export async function linkCommand(deps, projectId, opts = {}) {
|
|
413
463
|
if (!projectId) {
|
|
414
464
|
deps.print("✗ Which project? `vincentt link` needs a project id.");
|
|
415
465
|
deps.print("");
|
|
@@ -429,16 +479,31 @@ export async function linkCommand(deps, projectId) {
|
|
|
429
479
|
}
|
|
430
480
|
throw err;
|
|
431
481
|
}
|
|
482
|
+
const text = opts.json ? () => undefined : deps.print;
|
|
432
483
|
if (existing && existing.projectId !== project.id) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
484
|
+
text(`! This folder is already linked to ${existing.slug}.`);
|
|
485
|
+
text(` Re-linking it to ${project.name}.`);
|
|
486
|
+
text("");
|
|
436
487
|
}
|
|
437
488
|
await writeProjectBinding(deps.cwd, { projectId: project.id, slug: project.slug });
|
|
438
|
-
|
|
489
|
+
text(`✓ Linked this folder to ${project.name}`);
|
|
439
490
|
if (project.liveUrl)
|
|
440
|
-
|
|
441
|
-
|
|
491
|
+
text(` Address: ${project.liveUrl}`);
|
|
492
|
+
text(" Wrote .vincentt/project.json (added to .gitignore)");
|
|
493
|
+
// Settled only after a successful binding write: a refused link writes no file.
|
|
494
|
+
const joined = await (opts.settle ?? settleSettings)(deps.cwd, await loadProjectBinding(deps.cwd), opts.flags ?? {}, { print: deps.print, ask: opts.json ? undefined : opts.ask, json: opts.json });
|
|
495
|
+
if (opts.json) {
|
|
496
|
+
// JSON escapes C0 controls itself; the C1 range still needs the same escape.
|
|
497
|
+
deps.print(escapeControl(JSON.stringify({
|
|
498
|
+
project: { id: project.id, slug: project.slug, name: project.name, liveUrl: project.liveUrl },
|
|
499
|
+
...joined.json,
|
|
500
|
+
})));
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
503
|
+
if (joined.reported) {
|
|
504
|
+
deps.print("");
|
|
505
|
+
deps.print(" Run `vincentt preview` to try it on your phone.");
|
|
506
|
+
}
|
|
442
507
|
return 0;
|
|
443
508
|
}
|
|
444
509
|
/**
|
|
@@ -487,7 +552,7 @@ export async function unlinkCommand(deps) {
|
|
|
487
552
|
}
|
|
488
553
|
deps.print("✓ Unlinked. Removed .vincentt/project.json");
|
|
489
554
|
deps.print("");
|
|
490
|
-
deps.print(" The project itself is untouched
|
|
555
|
+
deps.print(" The project itself is untouched; this only disconnects this folder.");
|
|
491
556
|
deps.print(" Run `vincentt link <project-id>` to link it again.");
|
|
492
557
|
return 0;
|
|
493
558
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export declare const SETTINGS_FILE = "vincentt.json";
|
|
2
|
+
export declare const SETTINGS_SCHEMA_URL = "https://vincentt.studio/schema/vincentt.json";
|
|
3
|
+
export declare const SETTINGS_MAX_BYTES = 4096;
|
|
4
|
+
export type SettingsField = "dev.command" | "dev.port" | "build.command" | "build.output";
|
|
5
|
+
export declare const SETTINGS_FIELDS: readonly SettingsField[];
|
|
6
|
+
export interface VincenttJson {
|
|
7
|
+
$schema?: string;
|
|
8
|
+
dev?: {
|
|
9
|
+
command?: string;
|
|
10
|
+
port?: number;
|
|
11
|
+
};
|
|
12
|
+
build?: {
|
|
13
|
+
command?: string;
|
|
14
|
+
output?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export type Source = "flag" | "vincentt.json" | "default";
|
|
18
|
+
export interface Resolved<T> {
|
|
19
|
+
value: T;
|
|
20
|
+
source: Source;
|
|
21
|
+
/** The flag that supplied it, when source is "flag". */
|
|
22
|
+
flag?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ResolvedSettings {
|
|
25
|
+
devCommand: Resolved<string>;
|
|
26
|
+
devPort: Resolved<number>;
|
|
27
|
+
buildCommand?: Resolved<string>;
|
|
28
|
+
buildOutput: Resolved<string>;
|
|
29
|
+
}
|
|
30
|
+
export interface SettingsDefaults {
|
|
31
|
+
devCommand: string;
|
|
32
|
+
devPort: number;
|
|
33
|
+
buildOutput: string;
|
|
34
|
+
}
|
|
35
|
+
export declare const TEMPLATE_DEFAULTS: SettingsDefaults;
|
|
36
|
+
export type SettingsProblem = {
|
|
37
|
+
kind: "missing";
|
|
38
|
+
field: SettingsField;
|
|
39
|
+
flag: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "unknown_key";
|
|
42
|
+
key: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "invalid_json";
|
|
45
|
+
line?: number;
|
|
46
|
+
column?: number;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "bad_value";
|
|
49
|
+
field: SettingsField | "$schema";
|
|
50
|
+
value: unknown;
|
|
51
|
+
message: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "not_object";
|
|
54
|
+
} | {
|
|
55
|
+
kind: "too_large";
|
|
56
|
+
} | {
|
|
57
|
+
kind: "not_file";
|
|
58
|
+
};
|
|
59
|
+
export declare class SettingsError extends Error {
|
|
60
|
+
readonly problem: SettingsProblem;
|
|
61
|
+
constructor(problem: SettingsProblem);
|
|
62
|
+
}
|
|
63
|
+
/** One sentence naming what is wrong, for advisories that are not the full refusal. */
|
|
64
|
+
export declare function describeProblem(p: SettingsProblem): string;
|
|
65
|
+
/**
|
|
66
|
+
* The schema rule for one field's value, as the message a prompt or a refusal
|
|
67
|
+
* prints. `undefined` means the value is acceptable. Shared by the file parser,
|
|
68
|
+
* the joining prompts and the flag values, so there is one rule.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateField(field: SettingsField, value: unknown): string | undefined;
|
|
71
|
+
/** Parse a typed or flagged value into the field's type, then validate it. */
|
|
72
|
+
export declare function coerceField(field: SettingsField, raw: string): {
|
|
73
|
+
ok: true;
|
|
74
|
+
value: string | number;
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
message: string;
|
|
78
|
+
};
|
|
79
|
+
/** Parse the file's text. Throws SettingsError; never returns a partial object. */
|
|
80
|
+
export declare function parseSettings(text: string): VincenttJson;
|
|
81
|
+
export declare function fieldOf(file: VincenttJson, field: SettingsField): string | number | undefined;
|
|
82
|
+
export interface SettingsIo {
|
|
83
|
+
readFile: (p: string) => Promise<string>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read `vincentt.json` without trusting what it is. Opened non-blocking so a
|
|
87
|
+
* FIFO cannot park the open, then checked on the OPEN descriptor to be a
|
|
88
|
+
* regular file of at most 4 KB, and read no further than one byte past the cap.
|
|
89
|
+
* A symlink to /dev/zero is a character device and is refused before any read.
|
|
90
|
+
* Throws ENOENT (absent) or SettingsError (present but unusable).
|
|
91
|
+
*/
|
|
92
|
+
export declare function readSettingsText(abs: string): Promise<string>;
|
|
93
|
+
/**
|
|
94
|
+
* The file at `root`, or undefined when there is none. A present file that
|
|
95
|
+
* cannot be used throws: an unreadable file is never treated as absent, or a
|
|
96
|
+
* typo would silently restore the template defaults.
|
|
97
|
+
*/
|
|
98
|
+
export declare function readSettings(root: string, io?: SettingsIo): Promise<VincenttJson | undefined>;
|
|
99
|
+
export interface SettingsFlags {
|
|
100
|
+
devCommand?: string;
|
|
101
|
+
devPort?: number;
|
|
102
|
+
buildOutput?: string;
|
|
103
|
+
}
|
|
104
|
+
type Need = "dev" | "build";
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the fields a verb needs. `need` scopes the refusal: preview never
|
|
107
|
+
* refuses over a missing build.output, and publish never over a missing dev
|
|
108
|
+
* port. build.command is never refused anywhere, because nothing runs it.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveSettings(flags: SettingsFlags, file: VincenttJson | undefined, defaults?: SettingsDefaults, need?: readonly Need[]): ResolvedSettings;
|
|
111
|
+
/** The file as joining writes it: 2-space indentation and a trailing newline. */
|
|
112
|
+
export declare function serializeSettings(values: Partial<Record<SettingsField, string | number>>): string;
|
|
113
|
+
export {};
|