@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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Settle `vincentt.json` after a joining verb wrote its binding: recognise a
|
|
2
|
+
// template (and stop, printing nothing), else detect, report, optionally prompt,
|
|
3
|
+
// and write the file. Shared by `link` and `create`'s register path so both
|
|
4
|
+
// joining routes end in the same state.
|
|
5
|
+
//
|
|
6
|
+
// Rules this keeps:
|
|
7
|
+
// - a present file is never overwritten; differences are reported, not applied;
|
|
8
|
+
// - a report is byte-identical with and without a TTY; the prompt block is the
|
|
9
|
+
// only TTY difference, and `--json` never prompts;
|
|
10
|
+
// - nothing is run: detection reads files only (detect.ts).
|
|
11
|
+
import { promises as fs } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { DETECT_MAX_BYTES, detect, } from "./detect.js";
|
|
14
|
+
import { coerceField, describeProblem, fieldOf, parseSettings, readSettingsText, serializeSettings, SETTINGS_FIELDS, SETTINGS_FILE, SettingsError, TEMPLATE_DEFAULTS, } from "./settings.js";
|
|
15
|
+
import { isTemplateProject } from "./template.js";
|
|
16
|
+
import { escapeControl } from "./terminalText.js";
|
|
17
|
+
const FLAG_OF = {
|
|
18
|
+
"dev.command": "devCommand",
|
|
19
|
+
"dev.port": "devPort",
|
|
20
|
+
"build.command": "buildCommand",
|
|
21
|
+
"build.output": "buildOutput",
|
|
22
|
+
};
|
|
23
|
+
const FLAG_NAME = {
|
|
24
|
+
"dev.command": "--dev-command",
|
|
25
|
+
"dev.port": "--dev-port",
|
|
26
|
+
"build.command": "--build-command",
|
|
27
|
+
"build.output": "--build-output",
|
|
28
|
+
};
|
|
29
|
+
const LABEL = {
|
|
30
|
+
"dev.command": "Dev command",
|
|
31
|
+
"dev.port": "Dev port",
|
|
32
|
+
"build.command": "Build command",
|
|
33
|
+
"build.output": "Build output",
|
|
34
|
+
};
|
|
35
|
+
const PLACEHOLDER = {
|
|
36
|
+
"dev.command": '"<command>"',
|
|
37
|
+
"dev.port": "<port>",
|
|
38
|
+
"build.command": '"<command>"',
|
|
39
|
+
"build.output": '"<folder>"',
|
|
40
|
+
};
|
|
41
|
+
const VERB_REMEDY = {
|
|
42
|
+
"dev.command": { verb: "preview", run: 'vincentt preview --dev-command "<command>"' },
|
|
43
|
+
"dev.port": { verb: "preview", run: "vincentt preview --port <port>" },
|
|
44
|
+
"build.output": { verb: "publish", run: "vincentt publish --dist <folder>" },
|
|
45
|
+
};
|
|
46
|
+
/** The real probes: read-only, capped, and confined to the project's own tree. */
|
|
47
|
+
export function fsProbes(root) {
|
|
48
|
+
const confined = async (rel) => {
|
|
49
|
+
const abs = path.join(root, rel);
|
|
50
|
+
try {
|
|
51
|
+
const [real, realRoot] = await Promise.all([fs.realpath(abs), fs.realpath(root)]);
|
|
52
|
+
const inside = path.relative(realRoot, real);
|
|
53
|
+
if (inside.startsWith("..") || path.isAbsolute(inside))
|
|
54
|
+
return undefined;
|
|
55
|
+
return real;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
exists: async (rel) => (await confined(rel)) !== undefined,
|
|
63
|
+
readText: async (rel) => {
|
|
64
|
+
const real = await confined(rel);
|
|
65
|
+
if (!real)
|
|
66
|
+
return { missing: true };
|
|
67
|
+
const stat = await fs.stat(real).catch(() => undefined);
|
|
68
|
+
if (!stat)
|
|
69
|
+
return { missing: true };
|
|
70
|
+
// Never open anything but a regular file: a FIFO or a device would block.
|
|
71
|
+
if (!stat.isFile())
|
|
72
|
+
return { notFile: true };
|
|
73
|
+
if (stat.size > DETECT_MAX_BYTES)
|
|
74
|
+
return { tooLarge: true };
|
|
75
|
+
return { text: await fs.readFile(real, "utf8") };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function wordCount(n) {
|
|
80
|
+
return ["No", "One", "Two", "Three", "Four"][n] ?? String(n);
|
|
81
|
+
}
|
|
82
|
+
function snippet(field, written) {
|
|
83
|
+
const section = field.startsWith("dev.") ? "dev" : "build";
|
|
84
|
+
const parts = [];
|
|
85
|
+
for (const f of SETTINGS_FIELDS) {
|
|
86
|
+
if (!f.startsWith(`${section}.`))
|
|
87
|
+
continue;
|
|
88
|
+
const key = f.split(".")[1];
|
|
89
|
+
if (f === field)
|
|
90
|
+
parts.push(`"${key}": ${PLACEHOLDER[f]}`);
|
|
91
|
+
else if (written[f] !== undefined)
|
|
92
|
+
parts.push(`"${key}": ${JSON.stringify(written[f])}`);
|
|
93
|
+
}
|
|
94
|
+
return `"${section}": { ${parts.join(", ")} }`;
|
|
95
|
+
}
|
|
96
|
+
function tableLines(rows) {
|
|
97
|
+
const values = SETTINGS_FIELDS.map((f) => rows[f].value === undefined ? "not found" : String(rows[f].value));
|
|
98
|
+
const width = Math.max(17, ...values.map((v) => v.length + 2));
|
|
99
|
+
const out = [];
|
|
100
|
+
SETTINGS_FIELDS.forEach((f, i) => {
|
|
101
|
+
const sentences = rows[f].evidence.split(/(?<=\.) (?=[A-Z])/);
|
|
102
|
+
out.push(` ${LABEL[f].padEnd(16)}${values[i].padEnd(width)}${sentences[0]}`);
|
|
103
|
+
for (const s of sentences.slice(1))
|
|
104
|
+
out.push(`${" ".repeat(2 + 16 + width)}${s}`);
|
|
105
|
+
});
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function readingLine(read) {
|
|
109
|
+
const line = `→ Reading how this app runs: ${read.join(", ")}.`;
|
|
110
|
+
return line.length + " Nothing was run.".length <= 80
|
|
111
|
+
? [`${line} Nothing was run.`]
|
|
112
|
+
: [line, " Nothing was run."];
|
|
113
|
+
}
|
|
114
|
+
async function hasLfs(probes) {
|
|
115
|
+
const r = await probes.readText(".gitattributes");
|
|
116
|
+
return "text" in r && r.text.includes("filter=lfs");
|
|
117
|
+
}
|
|
118
|
+
function lfsAdvisory() {
|
|
119
|
+
return [
|
|
120
|
+
"",
|
|
121
|
+
"! This repo stores assets in Git LFS. Run `git lfs pull` before a build you mean",
|
|
122
|
+
" to publish, or the build will hold pointer files instead of the assets.",
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
export async function settleSettings(root, binding, flags, io) {
|
|
126
|
+
const state = { reported: false };
|
|
127
|
+
const json = await settle(root, binding, flags, io, state);
|
|
128
|
+
return { json, reported: state.reported };
|
|
129
|
+
}
|
|
130
|
+
async function settle(root, binding, flags, io, state) {
|
|
131
|
+
const out = {
|
|
132
|
+
settings: {
|
|
133
|
+
template: false,
|
|
134
|
+
written: false,
|
|
135
|
+
file: SETTINGS_FILE,
|
|
136
|
+
fields: {},
|
|
137
|
+
missing: [],
|
|
138
|
+
differs: [],
|
|
139
|
+
},
|
|
140
|
+
advisories: [],
|
|
141
|
+
};
|
|
142
|
+
if (await isTemplateProject(root, binding)) {
|
|
143
|
+
out.settings.template = true;
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
// Every line here can carry repository text (script bodies, config paths,
|
|
147
|
+
// file values), so the one sink escapes control characters.
|
|
148
|
+
const print = io.json
|
|
149
|
+
? () => undefined
|
|
150
|
+
: (line) => io.print(escapeControl(line));
|
|
151
|
+
const probes = io.probes ?? fsProbes(root);
|
|
152
|
+
// A folder with no package.json is not an app to detect (an empty folder
|
|
153
|
+
// linked before `init .`, most often). Writing a file there would make the
|
|
154
|
+
// template scaffolded into it later refuse its own preview.
|
|
155
|
+
if (!(await probes.exists("package.json"))) {
|
|
156
|
+
out.settings.missing = SETTINGS_FIELDS.map((field) => ({
|
|
157
|
+
field,
|
|
158
|
+
reason: "there is no package.json",
|
|
159
|
+
}));
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
const detected = await detect(probes);
|
|
163
|
+
// Flags over detection, each through the schema.
|
|
164
|
+
const rows = {};
|
|
165
|
+
for (const f of SETTINGS_FIELDS) {
|
|
166
|
+
const flagValue = flags[FLAG_OF[f]];
|
|
167
|
+
const found = detected.fields[f];
|
|
168
|
+
const reason = detected.missing.find((m) => m.field === f)?.reason ?? "not found";
|
|
169
|
+
if (flagValue !== undefined) {
|
|
170
|
+
const coerced = coerceField(f, flagValue);
|
|
171
|
+
rows[f] = coerced.ok
|
|
172
|
+
? { value: coerced.value, source: "flag", evidence: `from ${FLAG_NAME[f]}` }
|
|
173
|
+
: { evidence: `${FLAG_NAME[f]} ${coerced.message}` };
|
|
174
|
+
}
|
|
175
|
+
else if (found) {
|
|
176
|
+
rows[f] = { value: found.value, source: "detected", evidence: found.evidence };
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
rows[f] = { evidence: reason };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const f of SETTINGS_FIELDS) {
|
|
183
|
+
const r = rows[f];
|
|
184
|
+
if (r.value !== undefined && (r.source === "detected" || r.source === "flag")) {
|
|
185
|
+
out.settings.fields[f] = { value: r.value, source: r.source, evidence: r.evidence };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const lfs = await hasLfs(probes);
|
|
189
|
+
if (lfs)
|
|
190
|
+
out.advisories.push("git_lfs");
|
|
191
|
+
state.reported = true;
|
|
192
|
+
print("");
|
|
193
|
+
for (const l of readingLine(detected.read))
|
|
194
|
+
print(l);
|
|
195
|
+
// A present file is the creator's decision: never rewritten, only compared.
|
|
196
|
+
const settingsPath = path.join(root, SETTINGS_FILE);
|
|
197
|
+
// Bounded and non-blocking: a symlink to /dev/zero or a FIFO is refused
|
|
198
|
+
// before any read. Anything present but unusable is reported, never rewritten.
|
|
199
|
+
let existing;
|
|
200
|
+
try {
|
|
201
|
+
existing = { text: await readSettingsText(settingsPath) };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
if (err.code === "ENOENT")
|
|
205
|
+
existing = undefined;
|
|
206
|
+
else if (err instanceof SettingsError)
|
|
207
|
+
existing = { error: err };
|
|
208
|
+
else
|
|
209
|
+
existing = { error: new SettingsError({ kind: "not_file" }) };
|
|
210
|
+
}
|
|
211
|
+
if (existing !== undefined) {
|
|
212
|
+
print("");
|
|
213
|
+
try {
|
|
214
|
+
if ("error" in existing)
|
|
215
|
+
throw existing.error;
|
|
216
|
+
const file = parseSettings(existing.text);
|
|
217
|
+
for (const f of SETTINGS_FIELDS) {
|
|
218
|
+
const inFile = fieldOf(file, f);
|
|
219
|
+
const r = rows[f];
|
|
220
|
+
if (inFile === undefined || r.value === undefined || inFile === r.value)
|
|
221
|
+
continue;
|
|
222
|
+
out.settings.differs.push({
|
|
223
|
+
field: f,
|
|
224
|
+
file: inFile,
|
|
225
|
+
folder: r.value,
|
|
226
|
+
evidence: r.evidence,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (out.settings.differs.length === 0) {
|
|
230
|
+
print(" Using vincentt.json. It matches this folder.");
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const n = out.settings.differs.length;
|
|
234
|
+
print(`! vincentt.json is already here and was left as it is. ${wordCount(n)} setting${n === 1 ? "" : "s"} differ${n === 1 ? "s" : ""} from`);
|
|
235
|
+
print(" what this folder looks like:");
|
|
236
|
+
for (const d of out.settings.differs) {
|
|
237
|
+
const folder = String(d.folder);
|
|
238
|
+
print("");
|
|
239
|
+
print(` ${d.field}`);
|
|
240
|
+
print(` vincentt.json ${d.file}`);
|
|
241
|
+
print(` this folder ${folder.padEnd(Math.max(9, folder.length + 2))}${d.evidence}`);
|
|
242
|
+
}
|
|
243
|
+
print("");
|
|
244
|
+
print(" vincentt.json is what preview and publish use. If it is out of date, edit it.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
if (!(err instanceof SettingsError))
|
|
249
|
+
throw err;
|
|
250
|
+
print(`! vincentt.json is already here but can't be read: ${describeProblem(err.problem)}.`);
|
|
251
|
+
print(" Preview and publish will stop until it is fixed.");
|
|
252
|
+
}
|
|
253
|
+
if (lfs)
|
|
254
|
+
for (const l of lfsAdvisory())
|
|
255
|
+
print(l);
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
print("");
|
|
259
|
+
for (const l of tableLines(rows))
|
|
260
|
+
print(l);
|
|
261
|
+
const matchesDefaults = rows["dev.command"].value === TEMPLATE_DEFAULTS.devCommand &&
|
|
262
|
+
rows["dev.port"].value === TEMPLATE_DEFAULTS.devPort &&
|
|
263
|
+
rows["build.output"].value === TEMPLATE_DEFAULTS.buildOutput;
|
|
264
|
+
if (matchesDefaults) {
|
|
265
|
+
print("");
|
|
266
|
+
print(" This app runs like the Vincentt template, so no vincentt.json is needed.");
|
|
267
|
+
if (lfs)
|
|
268
|
+
for (const l of lfsAdvisory())
|
|
269
|
+
print(l);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
print("");
|
|
273
|
+
print(" This app does not run like the Vincentt template, so these go in vincentt.json.");
|
|
274
|
+
if (io.ask && !io.json) {
|
|
275
|
+
print(" Press Enter to keep a value, or type a new one.");
|
|
276
|
+
print("");
|
|
277
|
+
for (const f of SETTINGS_FIELDS) {
|
|
278
|
+
const r = rows[f];
|
|
279
|
+
if (r.source === "flag")
|
|
280
|
+
continue;
|
|
281
|
+
const fallback = r.value === undefined ? "" : String(r.value);
|
|
282
|
+
const prompt = fallback ? ` ${LABEL[f]} (${fallback}): ` : ` ${LABEL[f]}: `;
|
|
283
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
284
|
+
const answer = (await io.ask(escapeControl(prompt), fallback)).trim();
|
|
285
|
+
if (answer === "" || answer === fallback)
|
|
286
|
+
break;
|
|
287
|
+
const coerced = coerceField(f, answer);
|
|
288
|
+
if (coerced.ok) {
|
|
289
|
+
rows[f] = {
|
|
290
|
+
value: coerced.value,
|
|
291
|
+
source: "typed",
|
|
292
|
+
evidence: "typed at the prompt",
|
|
293
|
+
};
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
print(` ${LABEL[f]} ${coerced.message}.`);
|
|
297
|
+
if (attempt === 1)
|
|
298
|
+
rows[f] = { evidence: `${LABEL[f].toLowerCase()} ${coerced.message}` };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
print("");
|
|
302
|
+
}
|
|
303
|
+
const values = {};
|
|
304
|
+
for (const f of SETTINGS_FIELDS)
|
|
305
|
+
if (rows[f].value !== undefined)
|
|
306
|
+
values[f] = rows[f].value;
|
|
307
|
+
const count = Object.keys(values).length;
|
|
308
|
+
const write = io.writeFile ??
|
|
309
|
+
((abs, text) => fs.writeFile(abs, text, { encoding: "utf8", flag: "wx" }));
|
|
310
|
+
await write(settingsPath, serializeSettings(values));
|
|
311
|
+
out.settings.written = true;
|
|
312
|
+
if (!io.ask || io.json)
|
|
313
|
+
print("");
|
|
314
|
+
print(count === SETTINGS_FIELDS.length
|
|
315
|
+
? "✓ Wrote vincentt.json"
|
|
316
|
+
: `✓ Wrote vincentt.json with ${count} of 4 settings.`);
|
|
317
|
+
print(" Commit it, so every clone of this repo previews and publishes the same way.");
|
|
318
|
+
for (const f of SETTINGS_FIELDS) {
|
|
319
|
+
if (values[f] !== undefined)
|
|
320
|
+
continue;
|
|
321
|
+
const remedy = VERB_REMEDY[f];
|
|
322
|
+
const reason = rows[f].evidence;
|
|
323
|
+
if (!remedy) {
|
|
324
|
+
out.settings.missing.push({ field: f, reason });
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const edit = snippet(f, values);
|
|
328
|
+
out.settings.missing.push({ field: f, reason, remedy: { edit, flag: remedy.run } });
|
|
329
|
+
print("");
|
|
330
|
+
print(`! vincentt.json has no ${f}, so \`vincentt ${remedy.verb}\` will stop until it is set.`);
|
|
331
|
+
print(` Add it to vincentt.json: ${edit}`);
|
|
332
|
+
print(` Or pass it for one run: ${remedy.run}`);
|
|
333
|
+
}
|
|
334
|
+
if (lfs)
|
|
335
|
+
for (const l of lfsAdvisory())
|
|
336
|
+
print(l);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProjectBinding } from "../shared/config.js";
|
|
2
|
+
export declare const TEMPLATE_MARKER = ".vincentt-template.json";
|
|
3
|
+
export interface PackageJsonShape {
|
|
4
|
+
scripts?: Record<string, unknown>;
|
|
5
|
+
dependencies?: Record<string, unknown>;
|
|
6
|
+
devDependencies?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface TemplateIo {
|
|
9
|
+
readText: (p: string) => Promise<string | undefined>;
|
|
10
|
+
exists: (p: string) => Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export declare const fsTemplateIo: TemplateIo;
|
|
13
|
+
/** The parsed package.json, or undefined when absent, too large or unparseable. */
|
|
14
|
+
export declare function readPackageJson(root: string, io?: TemplateIo): Promise<PackageJsonShape | undefined>;
|
|
15
|
+
/** `dependencies` or `devDependencies` names the package. */
|
|
16
|
+
export declare function listsDependency(pkg: PackageJsonShape | undefined, name: string): boolean;
|
|
17
|
+
export declare function isTemplateProject(root: string, binding: Pick<ProjectBinding, "template"> | undefined, io?: TemplateIo): Promise<boolean>;
|
|
18
|
+
/** The static no-diagnostics check: does package.json list the harness? */
|
|
19
|
+
export declare function listsHarness(root: string, io?: TemplateIo): Promise<boolean>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Is this folder a Vincentt template project? A LOCAL classification, like
|
|
2
|
+
// `looksLikeVincenttApp`; the platform holds no notion of template versus
|
|
3
|
+
// outside app.
|
|
4
|
+
//
|
|
5
|
+
// Both legs are required: `package.json` lists `@vincentt-xr/sdk`, AND the
|
|
6
|
+
// committed zone manifest `.vincentt-template.json` sits at the root or the
|
|
7
|
+
// binding records a scaffolded template version. The marker is what recognises a
|
|
8
|
+
// fresh clone (no binding yet); the SDK leg keeps a stray marker from switching
|
|
9
|
+
// detection off for an app that is not the template.
|
|
10
|
+
//
|
|
11
|
+
// Reads only: one existence check and one package.json read. Nothing is run.
|
|
12
|
+
import { promises as fs } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
export const TEMPLATE_MARKER = ".vincentt-template.json";
|
|
15
|
+
const PACKAGE_JSON_MAX_BYTES = 256 * 1024;
|
|
16
|
+
export const fsTemplateIo = {
|
|
17
|
+
readText: async (p) => {
|
|
18
|
+
try {
|
|
19
|
+
const stat = await fs.stat(p);
|
|
20
|
+
if (!stat.isFile() || stat.size > PACKAGE_JSON_MAX_BYTES)
|
|
21
|
+
return undefined;
|
|
22
|
+
return await fs.readFile(p, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
exists: async (p) => {
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(p);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/** The parsed package.json, or undefined when absent, too large or unparseable. */
|
|
39
|
+
export async function readPackageJson(root, io = fsTemplateIo) {
|
|
40
|
+
const text = await io.readText(path.join(root, "package.json"));
|
|
41
|
+
if (text === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(text);
|
|
45
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
46
|
+
? parsed
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** `dependencies` or `devDependencies` names the package. */
|
|
54
|
+
export function listsDependency(pkg, name) {
|
|
55
|
+
if (!pkg)
|
|
56
|
+
return false;
|
|
57
|
+
const has = (deps) => typeof deps === "object" &&
|
|
58
|
+
deps !== null &&
|
|
59
|
+
Object.prototype.hasOwnProperty.call(deps, name);
|
|
60
|
+
return has(pkg.dependencies) || has(pkg.devDependencies);
|
|
61
|
+
}
|
|
62
|
+
export async function isTemplateProject(root, binding, io = fsTemplateIo) {
|
|
63
|
+
const pkg = await readPackageJson(root, io);
|
|
64
|
+
if (!listsDependency(pkg, "@vincentt-xr/sdk"))
|
|
65
|
+
return false;
|
|
66
|
+
if (binding?.template?.version)
|
|
67
|
+
return true;
|
|
68
|
+
return io.exists(path.join(root, TEMPLATE_MARKER));
|
|
69
|
+
}
|
|
70
|
+
/** The static no-diagnostics check: does package.json list the harness? */
|
|
71
|
+
export async function listsHarness(root, io = fsTemplateIo) {
|
|
72
|
+
return listsDependency(await readPackageJson(root, io), "@vincentt-xr/harness");
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Repository-derived text on its way to the creator's terminal. A carriage
|
|
2
|
+
// return or an ANSI sequence in a script body, a config value or a file name
|
|
3
|
+
// could redraw a line the creator is trusting, such as the disclosure of the dev
|
|
4
|
+
// command that is about to run. Every such string passes through here.
|
|
5
|
+
export const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
6
|
+
const CONTROL_CHARS_G = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
7
|
+
/** Each C0/C1 control character as a visible `\uXXXX`, so nothing is hidden or redrawn. */
|
|
8
|
+
export function escapeControl(text) {
|
|
9
|
+
return text.replace(CONTROL_CHARS_G, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
10
|
+
}
|
package/dist/lib/version.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// talk to.
|
|
3
3
|
//
|
|
4
4
|
// The second half is the point. A creator can have the right CLI pointed at the
|
|
5
|
-
// wrong API (a leftover VINCENTT_API_URL
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// wrong API (a leftover VINCENTT_API_URL or a machine config written months
|
|
6
|
+
// ago), and every failure that follows reads like a server outage rather than a
|
|
7
|
+
// misconfiguration. So this reports the resolved host AND its provenance, using
|
|
8
|
+
// the same decideApiUrl the real verbs use; a version command that re-derived
|
|
9
|
+
// the URL could disagree with them.
|
|
10
10
|
import { enablementLine, loadEnablement } from "../dx/enablement.js";
|
|
11
11
|
import { decideApiUrl, loadMachineConfig, locateProjectBinding, } from "../shared/config.js";
|
|
12
12
|
/**
|
|
@@ -60,12 +60,6 @@ export function renderVersion(r) {
|
|
|
60
60
|
` api ${r.apiUrl}`,
|
|
61
61
|
` from ${r.apiSource}`,
|
|
62
62
|
];
|
|
63
|
-
// A localhost default on a machine that never configured anything is the
|
|
64
|
-
// single most confusing state to be in: every verb fails with a connection
|
|
65
|
-
// error that looks like the platform is down.
|
|
66
|
-
if (r.isDefault && /localhost|127\.0\.0\.1/.test(r.apiUrl)) {
|
|
67
|
-
out.push("", " ! Nothing is configured, so this points at a local backend.", " Set VINCENTT_API_URL, or run `vincentt login` to save one.");
|
|
68
|
-
}
|
|
69
63
|
if (r.ignoredBindingApiUrl) {
|
|
70
64
|
out.push("", ` ! This folder's binding names a different API (${r.ignoredBindingApiUrl})`, " and was ignored. Re-run with --allow-custom-api to use it.");
|
|
71
65
|
}
|
package/dist/lib/whoami.js
CHANGED
|
@@ -6,21 +6,28 @@
|
|
|
6
6
|
// holds if the command actually answers, so its correctness is load-bearing for
|
|
7
7
|
// f2's hardest copy rather than cosmetic.
|
|
8
8
|
import { resolveActiveOrg } from "./org.js";
|
|
9
|
+
import { authExpiredLines, isAuthRefused } from "../shared/authError.js";
|
|
10
|
+
import { EXIT } from "./exitCodes.js";
|
|
9
11
|
export async function whoamiCommand(deps) {
|
|
10
12
|
const cfg = await deps.config();
|
|
11
13
|
if (!cfg) {
|
|
12
14
|
deps.print("Not signed in. Run `vincentt login`.");
|
|
13
|
-
return
|
|
15
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
14
16
|
}
|
|
15
17
|
let me;
|
|
16
18
|
try {
|
|
17
19
|
me = await deps.getMe(cfg);
|
|
18
20
|
}
|
|
19
21
|
catch (err) {
|
|
22
|
+
if (isAuthRefused(err)) {
|
|
23
|
+
for (const l of authExpiredLines())
|
|
24
|
+
deps.print(l);
|
|
25
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
26
|
+
}
|
|
20
27
|
// Reserved for a GENUINE transport failure. The two lines a creator can act on
|
|
21
28
|
// are named, because "unverified" alone tells someone who does not know who
|
|
22
29
|
// they are that we do not either.
|
|
23
|
-
deps.print(`! Could not reach ${cfg.apiUrl}
|
|
30
|
+
deps.print(`! Could not reach ${cfg.apiUrl}. Your token is present but unverified.`);
|
|
24
31
|
deps.print(" Check your connection, or `vincentt login` if you believe it expired.");
|
|
25
32
|
void err;
|
|
26
33
|
return 1;
|
|
@@ -32,7 +39,7 @@ export async function whoamiCommand(deps) {
|
|
|
32
39
|
// from a bug on exactly the path where the creator is already confused.
|
|
33
40
|
deps.print(org
|
|
34
41
|
? ` Active workspace: ${org.name} (${org.slug})`
|
|
35
|
-
: " Active workspace: none
|
|
42
|
+
: " Active workspace: none (pass --org <slug>)");
|
|
36
43
|
deps.print(` API: ${cfg.apiUrl}`);
|
|
37
44
|
return 0;
|
|
38
45
|
}
|
|
@@ -229,7 +229,7 @@ async function runDownload(deps, records, opts) {
|
|
|
229
229
|
deps.err("");
|
|
230
230
|
deps.err(`✗ Couldn't download ${a.id}.`);
|
|
231
231
|
deps.err("");
|
|
232
|
-
deps.err(" The URLs above are correct and still work
|
|
232
|
+
deps.err(" The URLs above are correct and still work; reference them directly");
|
|
233
233
|
deps.err(" instead, or try --download again in a moment.");
|
|
234
234
|
deps.err("");
|
|
235
235
|
if (written.length > 0) {
|
package/dist/library/client.js
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
// (D-add-prints-urls degraded the no-invented-URLs rule from prevention to
|
|
14
14
|
// detection, and this is the one arm that stayed prevention).
|
|
15
15
|
import { clientHeaders } from "../shared/clientHeader.js";
|
|
16
|
+
import { authFailureMessage } from "../shared/authError.js";
|
|
17
|
+
import { apiFetch, withDeadline } from "../shared/http.js";
|
|
16
18
|
import { parseBatchResponse, parseFetchResponse, parseSearchResponse, } from "./wire.js";
|
|
19
|
+
// An asset file can be tens of megabytes, so its download gets longer than an api call.
|
|
20
|
+
const downloadFetch = withDeadline((input, init) => globalThis.fetch(input, init), 5 * 60_000);
|
|
17
21
|
/** The API said no, with a code we can act on. */
|
|
18
22
|
export class LibraryError extends Error {
|
|
19
23
|
status;
|
|
@@ -65,7 +69,7 @@ export function httpLibraryClient(cfg) {
|
|
|
65
69
|
const auth = { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
66
70
|
const call = async (pathname, init) => {
|
|
67
71
|
try {
|
|
68
|
-
return await
|
|
72
|
+
return await apiFetch(`${base}${pathname}`, {
|
|
69
73
|
...init,
|
|
70
74
|
headers: { ...(init?.headers ?? {}), ...auth },
|
|
71
75
|
});
|
|
@@ -75,7 +79,8 @@ export function httpLibraryClient(cfg) {
|
|
|
75
79
|
}
|
|
76
80
|
};
|
|
77
81
|
const fail = async (res, what) => {
|
|
78
|
-
|
|
82
|
+
// A 401 keeps its status: assetsCommand maps it to the sign-in screen and exit 4.
|
|
83
|
+
throw new LibraryError(authFailureMessage(`${what} failed`, res.status, ""), res.status, await errorCode(res));
|
|
79
84
|
};
|
|
80
85
|
return {
|
|
81
86
|
async search(params) {
|
|
@@ -124,7 +129,7 @@ export function httpLibraryClient(cfg) {
|
|
|
124
129
|
}
|
|
125
130
|
let res;
|
|
126
131
|
try {
|
|
127
|
-
res = await
|
|
132
|
+
res = await downloadFetch(url);
|
|
128
133
|
}
|
|
129
134
|
catch (err) {
|
|
130
135
|
throw new LibraryUnreachableError(err);
|
package/dist/library/render.js
CHANGED
|
@@ -16,7 +16,9 @@ export function contractLine(a) {
|
|
|
16
16
|
}
|
|
17
17
|
/** `pumpkin · prop · 1:1 · 1024x1024 · alpha` — `add`'s confirmation line (byte facts). */
|
|
18
18
|
export function byteFactsLine(a) {
|
|
19
|
-
const dims = a.width !== undefined && a.height !== undefined
|
|
19
|
+
const dims = a.width !== undefined && a.height !== undefined
|
|
20
|
+
? `${a.width}x${a.height}`
|
|
21
|
+
: undefined;
|
|
20
22
|
const parts = [
|
|
21
23
|
a.subject,
|
|
22
24
|
a.kind,
|
|
@@ -74,9 +76,9 @@ export function searchRecordLines(a) {
|
|
|
74
76
|
const description = a.description;
|
|
75
77
|
if (subject !== undefined && description !== undefined) {
|
|
76
78
|
// The description is lowercased at the join only when it starts a sentence
|
|
77
|
-
// after the
|
|
79
|
+
// after the "subject: " lead, matching the approved transcript.
|
|
78
80
|
const d = description.charAt(0).toLowerCase() + description.slice(1);
|
|
79
|
-
out.push(...wrap(`${subject}
|
|
81
|
+
out.push(...wrap(`${subject}: ${d}`, 72, " "));
|
|
80
82
|
}
|
|
81
83
|
else if (subject !== undefined) {
|
|
82
84
|
out.push(` ${subject}`);
|
|
@@ -225,7 +227,7 @@ export function showLines(a) {
|
|
|
225
227
|
// that a project already references it, and the useful instruction is to do
|
|
226
228
|
// nothing.
|
|
227
229
|
out.push("! This asset is no longer in search results. The URL below keeps working");
|
|
228
|
-
out.push(" forever
|
|
230
|
+
out.push(" forever. If you are already using it, nothing needs to change.");
|
|
229
231
|
out.push("");
|
|
230
232
|
}
|
|
231
233
|
if (a.subject !== undefined)
|
|
@@ -272,7 +274,7 @@ export function unreachableLines(kind, retry) {
|
|
|
272
274
|
"✗ You're not signed in.",
|
|
273
275
|
"",
|
|
274
276
|
" The library is a platform call, so it needs an account. Your project is",
|
|
275
|
-
" unaffected
|
|
277
|
+
" unaffected: build, preview and publish don't use the library.",
|
|
276
278
|
"",
|
|
277
279
|
" Sign in: vincentt login",
|
|
278
280
|
"",
|
|
@@ -296,7 +298,7 @@ export function unreachableLines(kind, retry) {
|
|
|
296
298
|
"",
|
|
297
299
|
"✗ Can't reach the asset library.",
|
|
298
300
|
"",
|
|
299
|
-
" Your project is unaffected
|
|
301
|
+
" Your project is unaffected: build, preview and publish don't use the",
|
|
300
302
|
" library. Any library URL you already have keeps working.",
|
|
301
303
|
"",
|
|
302
304
|
` Try again in a moment: ${retry}`,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// GET /me Bearer <PAT> → identity + orgs (confirm)
|
|
9
9
|
import { authFailureMessage } from "../shared/authError.js";
|
|
10
10
|
import { clientHeaders } from "../shared/clientHeader.js";
|
|
11
|
+
import { apiFetch } from "../shared/http.js";
|
|
11
12
|
class AuthError extends Error {
|
|
12
13
|
status;
|
|
13
14
|
constructor(message, status) {
|
|
@@ -29,7 +30,7 @@ export function httpAuthBackend(apiBase) {
|
|
|
29
30
|
const base = apiBase.replace(/\/$/, "");
|
|
30
31
|
return {
|
|
31
32
|
async exchange(code, codeVerifier) {
|
|
32
|
-
const res = await
|
|
33
|
+
const res = await apiFetch(`${base}/access-tokens/exchange`, {
|
|
33
34
|
method: "POST",
|
|
34
35
|
headers: { ...clientHeaders(), "content-type": "application/json" },
|
|
35
36
|
body: JSON.stringify({ code, code_verifier: codeVerifier }),
|
|
@@ -43,7 +44,7 @@ export function httpAuthBackend(apiBase) {
|
|
|
43
44
|
return body.token;
|
|
44
45
|
},
|
|
45
46
|
async me(pat) {
|
|
46
|
-
const res = await
|
|
47
|
+
const res = await apiFetch(`${base}/me`, {
|
|
47
48
|
headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
|
|
48
49
|
});
|
|
49
50
|
if (!res.ok) {
|
package/dist/login/ensure.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ export interface EnsureLoginDeps {
|
|
|
5
5
|
runLogin: () => Promise<void>;
|
|
6
6
|
/** Surface a line to the user (stderr, so it never pollutes machine-readable stdout). */
|
|
7
7
|
notify: (message: string) => void;
|
|
8
|
+
/** Whether a person is at a terminal who can approve a browser sign-in. */
|
|
9
|
+
interactive: boolean;
|
|
8
10
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* True when a token is present, or a login at the terminal produced one. False
|
|
13
|
+
* after printing the not-signed-in lines: the caller exits EXIT.NOT_AUTHENTICATED.
|
|
14
|
+
* A login that throws (cancelled, timed out) propagates.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ensureLoggedIn(deps: EnsureLoginDeps): Promise<boolean>;
|