@vincentt-xr/cli 0.15.1 → 0.16.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/dist/cli/args.js +1 -0
- package/dist/cli/index.js +177 -4
- package/dist/cli/usage-text.js +27 -0
- package/dist/cli/verb-surface.d.ts +3 -1
- package/dist/cli/verb-surface.js +12 -0
- package/dist/domain/api.d.ts +51 -0
- package/dist/domain/api.js +145 -0
- package/dist/domain/command.d.ts +75 -0
- package/dist/domain/command.js +467 -0
- package/dist/domain/copy.d.ts +334 -0
- package/dist/domain/copy.js +952 -0
- package/dist/domain/hostname.d.ts +57 -0
- package/dist/domain/hostname.js +116 -0
- package/dist/domain/names.d.ts +78 -0
- package/dist/domain/names.js +113 -0
- package/dist/domain/routedVerbs.d.ts +10 -0
- package/dist/domain/routedVerbs.js +30 -0
- package/dist/domain/wire.d.ts +140 -0
- package/dist/domain/wire.js +174 -0
- package/dist/dx/clusters.js +10 -1
- package/dist/dx/command.d.ts +13 -4
- package/dist/dx/command.js +26 -5
- package/dist/dx/report.d.ts +39 -7
- package/dist/dx/report.js +71 -20
- package/dist/dx/send.d.ts +22 -4
- package/dist/dx/send.js +29 -8
- package/dist/dx/skeleton.js +19 -0
- package/dist/dx/unbiddenVerbs.d.ts +25 -0
- package/dist/dx/unbiddenVerbs.js +39 -0
- package/dist/lib/exitCodes.d.ts +24 -0
- package/dist/lib/exitCodes.js +28 -0
- package/dist/lib/initCommand.d.ts +3 -1
- package/dist/lib/initCommand.js +10 -0
- package/dist/lib/initFrom.js +12 -2
- package/dist/lib/lifecycle.d.ts +5 -1
- package/dist/lib/projects.d.ts +8 -1
- package/dist/lib/projects.js +21 -4
- package/dist/platform/allowlist.d.ts +36 -0
- package/dist/platform/allowlist.js +34 -0
- package/dist/platform/behind.d.ts +41 -0
- package/dist/platform/behind.js +82 -0
- package/dist/platform/command.d.ts +16 -0
- package/dist/platform/command.js +90 -0
- package/dist/platform/footer.d.ts +60 -0
- package/dist/platform/footer.js +101 -0
- package/dist/platform/notice.d.ts +29 -0
- package/dist/platform/notice.js +66 -0
- package/dist/platform/offSwitch.d.ts +19 -0
- package/dist/platform/offSwitch.js +21 -0
- package/dist/platform/outdated.d.ts +25 -0
- package/dist/platform/outdated.js +246 -0
- package/dist/platform/releaseCache.d.ts +69 -0
- package/dist/platform/releaseCache.js +140 -0
- package/dist/platform/releaseFetch.d.ts +51 -0
- package/dist/platform/releaseFetch.js +150 -0
- package/dist/platform/resolve.d.ts +30 -0
- package/dist/platform/resolve.js +128 -0
- package/dist/platform/templateUpgrade.d.ts +56 -0
- package/dist/platform/templateUpgrade.js +476 -0
- package/dist/platform/upgradeReport.d.ts +132 -0
- package/dist/platform/upgradeReport.js +258 -0
- package/dist/platform/versionSummary.d.ts +82 -0
- package/dist/platform/versionSummary.js +181 -0
- package/dist/platform/zonePath.d.ts +154 -0
- package/dist/platform/zonePath.js +286 -0
- package/dist/platform/zones.d.ts +64 -0
- package/dist/platform/zones.js +79 -0
- package/dist/publish/api.d.ts +15 -4
- package/dist/publish/api.js +19 -5
- package/dist/publish/command.d.ts +10 -0
- package/dist/publish/command.js +7 -1
- package/dist/publish/copy.d.ts +14 -1
- package/dist/publish/copy.js +32 -3
- package/dist/scaffold/index.d.ts +50 -3
- package/dist/scaffold/index.js +61 -1
- package/dist/shared/config.d.ts +16 -0
- package/dist/shared/config.js +9 -1
- package/package.json +1 -1
- package/dist/dx/qa.f7.fixtures.d.ts +0 -73
- package/dist/dx/qa.f7.fixtures.js +0 -58
- package/dist/publish/qa.f4.fixtures.d.ts +0 -25
- package/dist/publish/qa.f4.fixtures.js +0 -140
- package/dist/relay/qa.f12.fixtures.d.ts +0 -154
- package/dist/relay/qa.f12.fixtures.js +0 -358
package/dist/cli/args.js
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { ensureLoggedIn } from "../login/ensure.js";
|
|
|
21
21
|
import { clearMachineToken, loadMachineConfig, locateProjectBinding, resolveConfig, writeDxEnablement, MACHINE_CONFIG_PATH, } from "../shared/config.js";
|
|
22
22
|
import { fileLocationStore } from "../shared/locationStore.js";
|
|
23
23
|
import { getMe, listProjects, ProjectUnreachableError } from "../lib/backend.js";
|
|
24
|
+
import { EXIT } from "../lib/exitCodes.js";
|
|
24
25
|
import { listProjectsCommand } from "../lib/listProjects.js";
|
|
25
26
|
import { initCommand } from "../lib/initCommand.js";
|
|
26
27
|
import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
|
|
@@ -50,6 +51,10 @@ import { nudgeFor } from "../dx/nudge.js";
|
|
|
50
51
|
import { FETCH_TIMEOUT_MS, isFresh, updateNotice } from "../dx/updateCheck.js";
|
|
51
52
|
import { fetchLatest, loadCache, saveCache } from "../dx/updateCheckIo.js";
|
|
52
53
|
import { verbOf } from "../dx/skeleton.js";
|
|
54
|
+
import { outdatedCommand } from "../platform/command.js";
|
|
55
|
+
import { behindFooterLines, needsRefresh, refreshReleaseCache } from "../platform/footer.js";
|
|
56
|
+
import { loadReleaseCache } from "../platform/releaseCache.js";
|
|
57
|
+
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
53
58
|
import { createDirCollision, createSourceFlagRefusal, fromFlagConflict, isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag, } from "./args.js";
|
|
54
59
|
import { parseSourceRef } from "../lib/sourceRef.js";
|
|
55
60
|
import { printCreateSourceFlagRefusal, printFlagConflict, printSourceRefFailure, } from "../lib/sourceRefCopy.js";
|
|
@@ -400,6 +405,29 @@ async function projects() {
|
|
|
400
405
|
store: fileLocationStore,
|
|
401
406
|
}, { json: has("--json") });
|
|
402
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* `vincentt outdated` — what this project is behind on.
|
|
410
|
+
*
|
|
411
|
+
* NEEDS NO ACCOUNT and never prompts for one: the endpoint it reads is public,
|
|
412
|
+
* and a creator with no login is exactly the population this verb exists to
|
|
413
|
+
* serve. It is a listing verb, so it reports and never asks, and its output is
|
|
414
|
+
* routinely piped.
|
|
415
|
+
*/
|
|
416
|
+
async function outdated() {
|
|
417
|
+
const cwd = projectDir();
|
|
418
|
+
process.exitCode = await outdatedCommand({
|
|
419
|
+
print: (l) => console.log(l),
|
|
420
|
+
cwd,
|
|
421
|
+
now: () => Date.now(),
|
|
422
|
+
apiUrl: async () => process.env.VINCENTT_API_URL ?? (await loadMachineConfig()).apiUrl ?? DEFAULT_API_URL,
|
|
423
|
+
inProject: async () => (await locateProjectBinding(cwd)) !== undefined,
|
|
424
|
+
projectLine: async () => {
|
|
425
|
+
const name = await projectDisplayName(cwd);
|
|
426
|
+
const org = await activeOrgName();
|
|
427
|
+
return org ? `${name} · ${org}` : name;
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
403
431
|
/**
|
|
404
432
|
* `vincentt publish` — Flows L22-L31.
|
|
405
433
|
*
|
|
@@ -813,7 +841,7 @@ async function feedbackWait(cwd, timeoutMs, verbose) {
|
|
|
813
841
|
break;
|
|
814
842
|
for (const l of feedbackCopy.windowElapsed(timeoutMs))
|
|
815
843
|
console.error(l);
|
|
816
|
-
return
|
|
844
|
+
return EXIT.WINDOW_ELAPSED;
|
|
817
845
|
}
|
|
818
846
|
if (result.kind === "closed") {
|
|
819
847
|
if (handed > 0)
|
|
@@ -825,7 +853,7 @@ async function feedbackWait(cwd, timeoutMs, verbose) {
|
|
|
825
853
|
console.error(l);
|
|
826
854
|
}
|
|
827
855
|
}
|
|
828
|
-
return
|
|
856
|
+
return EXIT.CHANNEL_ENDED;
|
|
829
857
|
}
|
|
830
858
|
await handOver(cwd, result.annotation);
|
|
831
859
|
after = result.annotation.seq;
|
|
@@ -882,7 +910,7 @@ async function feedbackWatch(cwd, timeoutMs, verbose) {
|
|
|
882
910
|
console.error(l);
|
|
883
911
|
}
|
|
884
912
|
}
|
|
885
|
-
return
|
|
913
|
+
return EXIT.CHANNEL_ENDED;
|
|
886
914
|
}
|
|
887
915
|
const a = result.annotation;
|
|
888
916
|
await handOver(cwd, a);
|
|
@@ -1031,6 +1059,9 @@ async function dx() {
|
|
|
1031
1059
|
// first two have not already allowed the send, and allowed to throw — a /me that
|
|
1032
1060
|
// is unreachable falls back to the other two arms rather than blocking.
|
|
1033
1061
|
accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
|
|
1062
|
+
// `--dir`-or-cwd, the same resolution every other verb uses, so `--dir`
|
|
1063
|
+
// works on a send as it does everywhere else.
|
|
1064
|
+
cwd: projectDir(),
|
|
1034
1065
|
}, options);
|
|
1035
1066
|
}
|
|
1036
1067
|
// ---- report -----------------------------------------------------------------
|
|
@@ -1069,6 +1100,9 @@ async function report() {
|
|
|
1069
1100
|
interactive,
|
|
1070
1101
|
enablement: () => enablement,
|
|
1071
1102
|
accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
|
|
1103
|
+
// `--dir`-or-cwd, the same resolution every other verb uses, so `--dir`
|
|
1104
|
+
// works on a report as it does everywhere else.
|
|
1105
|
+
cwd: projectDir(),
|
|
1072
1106
|
}, parseReportOptions(argv.slice(1)));
|
|
1073
1107
|
}
|
|
1074
1108
|
// ---- assets (f5) ------------------------------------------------------------
|
|
@@ -1110,7 +1144,7 @@ async function assets() {
|
|
|
1110
1144
|
for (const l of unreachableLines("auth", 'vincentt assets search "<what you want>"')) {
|
|
1111
1145
|
console.error(l);
|
|
1112
1146
|
}
|
|
1113
|
-
process.exitCode =
|
|
1147
|
+
process.exitCode = EXIT.NOT_AUTHENTICATED;
|
|
1114
1148
|
return;
|
|
1115
1149
|
}
|
|
1116
1150
|
const deps = {
|
|
@@ -1152,6 +1186,77 @@ async function assets() {
|
|
|
1152
1186
|
dir: flag("--dir"),
|
|
1153
1187
|
});
|
|
1154
1188
|
}
|
|
1189
|
+
// ---- custom domains (f16) ---------------------------------------------------
|
|
1190
|
+
/**
|
|
1191
|
+
* `vincentt domain <attach|list|verify|detach>` — Flows C1-C7.
|
|
1192
|
+
*
|
|
1193
|
+
* The aliases `add` and `remove` resolve inside `resolveSub` and NOTHING IS
|
|
1194
|
+
* PRINTED ABOUT THEM: no "did you mean", no deprecation, no echo of the canonical
|
|
1195
|
+
* name. `resolveSub` returns only the canonical verb, so this function cannot
|
|
1196
|
+
* tell an alias was used — which is the structural form of the rule.
|
|
1197
|
+
*
|
|
1198
|
+
* `renew` gets a FACT rather than a redirect, because the honest answer is that
|
|
1199
|
+
* the act does not exist: certificates renew on their own.
|
|
1200
|
+
*/
|
|
1201
|
+
async function domain() {
|
|
1202
|
+
const { domainCommand, resolveSub, } = await import("../domain/command.js");
|
|
1203
|
+
const domainCopy = await import("../domain/copy.js");
|
|
1204
|
+
const api = await import("../domain/api.js");
|
|
1205
|
+
const sub = positional(1);
|
|
1206
|
+
if (sub === undefined) {
|
|
1207
|
+
console.error("Usage: vincentt domain <attach|list|verify|detach> [domain]");
|
|
1208
|
+
process.exitCode = 1;
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (sub === "renew") {
|
|
1212
|
+
for (const l of domainCopy.noRenewVerb())
|
|
1213
|
+
console.log(l);
|
|
1214
|
+
process.exitCode = 1;
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const resolved = resolveSub(sub);
|
|
1218
|
+
if (resolved === undefined) {
|
|
1219
|
+
for (const l of domainCopy.unknownDomainSub(sub))
|
|
1220
|
+
console.log(l);
|
|
1221
|
+
process.exitCode = 1;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const cwd = projectDir();
|
|
1225
|
+
const machine = await loadMachineConfig();
|
|
1226
|
+
const config = async () => {
|
|
1227
|
+
const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
|
|
1228
|
+
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
1229
|
+
return pat ? { apiUrl: apiUrl.replace(/\/$/, ""), pat } : undefined;
|
|
1230
|
+
};
|
|
1231
|
+
process.exitCode = await domainCommand({
|
|
1232
|
+
print: (l) => console.log(l),
|
|
1233
|
+
config,
|
|
1234
|
+
binding: async () => {
|
|
1235
|
+
const located = await locateProjectBinding(cwd);
|
|
1236
|
+
return located
|
|
1237
|
+
? { projectId: located.binding.projectId, slug: located.binding.slug }
|
|
1238
|
+
: undefined;
|
|
1239
|
+
},
|
|
1240
|
+
org: async () => {
|
|
1241
|
+
const cfg = await config();
|
|
1242
|
+
if (!cfg)
|
|
1243
|
+
return { ok: false, message: "Not signed in. Run `vincentt login`." };
|
|
1244
|
+
return resolveOrgForCommand(cfg, { flag: flag("--org") });
|
|
1245
|
+
},
|
|
1246
|
+
attach: api.attachDomain,
|
|
1247
|
+
verify: api.verifyDomain,
|
|
1248
|
+
listOrg: api.listOrgDomains,
|
|
1249
|
+
detach: api.detachDomain,
|
|
1250
|
+
confirm: stdinConfirm(),
|
|
1251
|
+
isTTY: Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
|
|
1252
|
+
}, resolved, {
|
|
1253
|
+
// The hostname is the bare argument after the subcommand. `list` takes
|
|
1254
|
+
// none and ignores it rather than erroring: a listing verb reports.
|
|
1255
|
+
hostname: positional(2),
|
|
1256
|
+
json: has("--json"),
|
|
1257
|
+
yes: has("--yes"),
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1155
1260
|
// ---- servers ----------------------------------------------------------------
|
|
1156
1261
|
async function relay() {
|
|
1157
1262
|
const { startRelay } = await import("../relay/server.js");
|
|
@@ -1188,6 +1293,19 @@ async function main() {
|
|
|
1188
1293
|
// to answer a question it knows the answer to.
|
|
1189
1294
|
if (cmd !== undefined && isConsoleOnlyVerb(cmd))
|
|
1190
1295
|
return consoleOnly(cmd);
|
|
1296
|
+
// f16: `domains`, `dns`, `custom-domain` are the plausible spellings of the
|
|
1297
|
+
// real verb. An agent that gets "unknown command" retries or guesses a
|
|
1298
|
+
// neighbour; naming the command terminates.
|
|
1299
|
+
if (cmd !== undefined) {
|
|
1300
|
+
const { isDomainRoutedVerb } = await import("../domain/routedVerbs.js");
|
|
1301
|
+
if (isDomainRoutedVerb(cmd)) {
|
|
1302
|
+
const { domainVerbRedirect } = await import("../domain/copy.js");
|
|
1303
|
+
for (const l of domainVerbRedirect(cmd))
|
|
1304
|
+
console.log(l);
|
|
1305
|
+
process.exitCode = 1;
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1191
1309
|
console.error(`Unknown command: ${cmd}\n`);
|
|
1192
1310
|
usage();
|
|
1193
1311
|
process.exitCode = 1;
|
|
@@ -1232,6 +1350,10 @@ async function main() {
|
|
|
1232
1350
|
return dx();
|
|
1233
1351
|
case "report":
|
|
1234
1352
|
return report();
|
|
1353
|
+
case "outdated":
|
|
1354
|
+
return outdated();
|
|
1355
|
+
case "domain":
|
|
1356
|
+
return domain();
|
|
1235
1357
|
}
|
|
1236
1358
|
}
|
|
1237
1359
|
// Every invocation is journaled here, at the one site that sees all of them —
|
|
@@ -1282,6 +1404,57 @@ function journal(exitCode, error) {
|
|
|
1282
1404
|
catch {
|
|
1283
1405
|
// Same rule as the nudge: never break a run over a footnote.
|
|
1284
1406
|
}
|
|
1407
|
+
// The behind-notice, on the same terms as the two footnotes above: stderr,
|
|
1408
|
+
// after the run it is a footnote to, from the PREVIOUS run's cache, and never
|
|
1409
|
+
// awaited. It rides only on commands a creator initiated — there is no timer
|
|
1410
|
+
// and no background poll anywhere in this feature.
|
|
1411
|
+
//
|
|
1412
|
+
// `preview` is the carrier that matters: it is the command a creator runs many
|
|
1413
|
+
// times a day for the life of a project, which closes the gap where someone
|
|
1414
|
+
// iterates for weeks without publishing and hears nothing.
|
|
1415
|
+
//
|
|
1416
|
+
// NEVER ON `outdated` ITSELF, for the reason the nudge is never printed on
|
|
1417
|
+
// `dx`: appending an unbidden summary of what the creator just asked for and
|
|
1418
|
+
// read in full is noise, and worse than noise here — it would BURN the
|
|
1419
|
+
// once-per-release announcement on a run where the creator was already told
|
|
1420
|
+
// everything, so the next `preview` would stay silent about a release they
|
|
1421
|
+
// saw only because they went looking.
|
|
1422
|
+
if (verbOf(argv) !== "outdated")
|
|
1423
|
+
void announceBehind();
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Print the behind-notice and schedule the refresh.
|
|
1427
|
+
*
|
|
1428
|
+
* Its own function because it needs an await (the version resolve reads a few
|
|
1429
|
+
* small files) while `journal` is synchronous, and because every fault here must
|
|
1430
|
+
* be swallowed: the worst outcome of this whole surface going dark is that
|
|
1431
|
+
* nobody is told they are behind, which is exactly where they were before it
|
|
1432
|
+
* existed. `preview` must not acquire a new way to look broken.
|
|
1433
|
+
*/
|
|
1434
|
+
async function announceBehind() {
|
|
1435
|
+
try {
|
|
1436
|
+
const cwd = projectDir();
|
|
1437
|
+
const builtWith = await resolveBuiltWith(cwd);
|
|
1438
|
+
if (builtWith !== undefined) {
|
|
1439
|
+
for (const l of behindFooterLines({
|
|
1440
|
+
installed: { packages: builtWith.packages, template: builtWith.template },
|
|
1441
|
+
now: () => Date.now(),
|
|
1442
|
+
})) {
|
|
1443
|
+
console.error(l);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (needsRefresh(loadReleaseCache(), Date.now())) {
|
|
1447
|
+
refreshReleaseCache({
|
|
1448
|
+
apiUrl: process.env.VINCENTT_API_URL ??
|
|
1449
|
+
(await loadMachineConfig()).apiUrl ??
|
|
1450
|
+
DEFAULT_API_URL,
|
|
1451
|
+
now: () => Date.now(),
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
catch {
|
|
1456
|
+
// A footnote that can break a publish is not worth having.
|
|
1457
|
+
}
|
|
1285
1458
|
}
|
|
1286
1459
|
/**
|
|
1287
1460
|
* Refresh the cached `latest` without holding the process open.
|
package/dist/cli/usage-text.js
CHANGED
|
@@ -41,6 +41,28 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
41
41
|
` preview Serve the app at an https link a phone can open\n` +
|
|
42
42
|
` [--port <n>] [--tester <id>] [--no-qr] [--stop]\n` +
|
|
43
43
|
` Re-running prints the live link; --restart mints a new one.\n\n` +
|
|
44
|
+
// f16. HELP TEACHES THE CANONICAL VERBS ONLY: `add` and `remove` are accepted
|
|
45
|
+
// as silent aliases and are deliberately ABSENT here, because help, docs and
|
|
46
|
+
// every error string are where the vocabulary is actually taught. Listing the
|
|
47
|
+
// aliases would teach two words for one act, which is the thing D-45 refuses.
|
|
48
|
+
//
|
|
49
|
+
// "Detaching stops it serving; the name stays yours" is the load-bearing
|
|
50
|
+
// clause: a detached custom domain is NOT burned, and a reader who assumed
|
|
51
|
+
// f4's permanence rule applied would never try the verb.
|
|
52
|
+
`Custom domains (a paid-plan capability)\n` +
|
|
53
|
+
` domain attach <domain> Claim a domain for this folder's project and print\n` +
|
|
54
|
+
` the three DNS records to create. Prints each name\n` +
|
|
55
|
+
` in both the short and the full form, because\n` +
|
|
56
|
+
` registrars disagree about which they want.\n` +
|
|
57
|
+
` domain list This workspace's domains and the count its plan\n` +
|
|
58
|
+
` carries [--org <s>] [--json]\n` +
|
|
59
|
+
` domain verify <domain> Check the DNS now and report what was OBSERVED\n` +
|
|
60
|
+
` beside what was expected [--json]\n` +
|
|
61
|
+
` Exit 0 serving (findings that block nothing may\n` +
|
|
62
|
+
` still be reported); exit 1 not serving yet\n` +
|
|
63
|
+
` domain detach <domain> Stop serving the domain. Asks first; [--yes] to\n` +
|
|
64
|
+
` skip the question. The name stays yours and the\n` +
|
|
65
|
+
` project's vincentt.app address is unaffected.\n\n` +
|
|
44
66
|
`Diagnostics (need a running preview)\n` +
|
|
45
67
|
` logs [--errors] Console logs from the device [--json] [--tester <id>]\n` +
|
|
46
68
|
` network fetch/XHR requests [--json] [--tester <id>]\n` +
|
|
@@ -92,6 +114,11 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
92
114
|
` relay Run the diagnostics/annotation relay\n\n` +
|
|
93
115
|
`Meta\n` +
|
|
94
116
|
` version CLI version, and the API this machine resolves to\n` +
|
|
117
|
+
` outdated What this project is behind on, and what Vincentt currently\n` +
|
|
118
|
+
` publishes. Reads a public endpoint; no account needed.\n` +
|
|
119
|
+
` Prints the installed and current version of each Vincentt\n` +
|
|
120
|
+
` package, the string to place in package.json, and any\n` +
|
|
121
|
+
` migration note the platform published. Writes nothing.\n` +
|
|
95
122
|
` dx Where this dev loop fought you [--days <n>] [--json]\n` +
|
|
96
123
|
` Reads a local journal of your own runs. Local by default.\n` +
|
|
97
124
|
` --send reports the findings to the Vincentt team; it\n` +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects", "report"];
|
|
1
|
+
export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects", "report", "outdated", "domain"];
|
|
2
2
|
export type PinnedVerb = (typeof PINNED_VERBS)[number];
|
|
3
3
|
export declare const VERB_HANDLERS: {
|
|
4
4
|
readonly version: true;
|
|
@@ -20,6 +20,8 @@ export declare const VERB_HANDLERS: {
|
|
|
20
20
|
readonly dx: true;
|
|
21
21
|
readonly projects: true;
|
|
22
22
|
readonly report: true;
|
|
23
|
+
readonly outdated: true;
|
|
24
|
+
readonly domain: true;
|
|
23
25
|
};
|
|
24
26
|
export type DispatchVerb = keyof typeof VERB_HANDLERS;
|
|
25
27
|
export declare function resolveVerb(cmd: string | undefined): DispatchVerb | undefined;
|
package/dist/cli/verb-surface.js
CHANGED
|
@@ -16,6 +16,16 @@ export const PINNED_VERBS = [
|
|
|
16
16
|
// and refuses when it finds none; this carries what a creator TYPED, which is
|
|
17
17
|
// the half a journal keyed on exit codes cannot see. Also an addition.
|
|
18
18
|
"report",
|
|
19
|
+
// Appended: what this project is behind on, and what the platform currently
|
|
20
|
+
// publishes. Named for the QUESTION and never for the act — `update` and
|
|
21
|
+
// `upgrade` would imply the CLI performs the upgrade, which it does not, and
|
|
22
|
+
// an agent reading `vincentt update` in any text will run it.
|
|
23
|
+
"outdated",
|
|
24
|
+
// Appended (f16): custom domains. SINGULAR, because every subcommand acts on
|
|
25
|
+
// one domain (`domain attach ar.acme.com`) and the plural would read as a
|
|
26
|
+
// collection verb. `domains` is routed to an answer, not accepted as a verb —
|
|
27
|
+
// see domainVerbRedirect. An addition, so B-F3-8 permits it.
|
|
28
|
+
"domain",
|
|
19
29
|
];
|
|
20
30
|
// The dispatch's verb set, as data. `main()` switches over this, so the two can
|
|
21
31
|
// never disagree: a verb added to the CLI is a key added here, and QA-AHG-01
|
|
@@ -40,6 +50,8 @@ export const VERB_HANDLERS = {
|
|
|
40
50
|
dx: true,
|
|
41
51
|
projects: true,
|
|
42
52
|
report: true,
|
|
53
|
+
outdated: true,
|
|
54
|
+
domain: true,
|
|
43
55
|
};
|
|
44
56
|
// Answers "is this a verb this CLI accepts" without running it. The
|
|
45
57
|
// console-redirect strings are NOT accepted here: they reach their explanation
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
|
+
import { type DomainListView, type DomainView } from "./wire.js";
|
|
3
|
+
export interface DomainApiErrorDetail {
|
|
4
|
+
/** The plan's ceiling, on a ceiling refusal. */
|
|
5
|
+
limit?: number;
|
|
6
|
+
/** The plan in force, when the API names one. */
|
|
7
|
+
planName?: string;
|
|
8
|
+
planTier?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class DomainApiError extends Error {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly detail: DomainApiErrorDetail;
|
|
14
|
+
readonly retryAfterSec?: number | undefined;
|
|
15
|
+
constructor(status: number, code: string, message: string, detail?: DomainApiErrorDetail, retryAfterSec?: number | undefined);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The record's role, when the API names one, parallel to `records`.
|
|
19
|
+
*
|
|
20
|
+
* The CLI does NOT decide which record is the certificate-validation one: the
|
|
21
|
+
* caption whose absence is expensive (D-44) must not depend on a client-side
|
|
22
|
+
* heuristic over a CA hostname. When the API sends no roles the array is empty
|
|
23
|
+
* and the caption is simply absent — visibly missing rather than wrongly placed.
|
|
24
|
+
*/
|
|
25
|
+
export interface AttachResult {
|
|
26
|
+
domain: DomainView;
|
|
27
|
+
roles: (string | undefined)[];
|
|
28
|
+
droppedFindings: number;
|
|
29
|
+
}
|
|
30
|
+
/** POST /projects/{id}/domains — returns the records to publish. Rate-limited. */
|
|
31
|
+
export declare function attachDomain(cfg: ResolvedConfig, projectId: string, domain: string, fetchImpl?: typeof fetch): Promise<AttachResult>;
|
|
32
|
+
/**
|
|
33
|
+
* POST /projects/{id}/domains/{domain}/verify — the on-demand re-check.
|
|
34
|
+
*
|
|
35
|
+
* Rate-limited per `(org, domain)`, so a 429 here is expected traffic rather than
|
|
36
|
+
* a defect, and its copy names no limit family.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verifyDomain(cfg: ResolvedConfig, projectId: string, domain: string, fetchImpl?: typeof fetch): Promise<AttachResult>;
|
|
39
|
+
/** GET /projects/{id}/domains — this project's domains with state + findings. */
|
|
40
|
+
export declare function listProjectDomains(cfg: ResolvedConfig, projectId: string, fetchImpl?: typeof fetch): Promise<DomainListView>;
|
|
41
|
+
/**
|
|
42
|
+
* GET /orgs/{id}/domains — the workspace's whole fleet and the count against the
|
|
43
|
+
* limit.
|
|
44
|
+
*
|
|
45
|
+
* This is what `vincentt domain list` reads, and it is why the anti-oracle
|
|
46
|
+
* collision copy is safe: a creator who genuinely holds a contested name finds it
|
|
47
|
+
* here, and this route only ever returns their own.
|
|
48
|
+
*/
|
|
49
|
+
export declare function listOrgDomains(cfg: ResolvedConfig, orgSlug: string, fetchImpl?: typeof fetch): Promise<DomainListView>;
|
|
50
|
+
/** DELETE /projects/{id}/domains/{domain} — detach. The row is never deleted server-side. */
|
|
51
|
+
export declare function detachDomain(cfg: ResolvedConfig, projectId: string, domain: string, fetchImpl?: typeof fetch): Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The f16 domain calls against the Go API, built against implementation.md
|
|
2
|
+
// § API surface.
|
|
3
|
+
//
|
|
4
|
+
// THE ROUTES ARE HOSTNAME-KEYED and normalized in the path. A hostname path
|
|
5
|
+
// segment CONTAINS DOTS, which some routers treat as a file extension — the API
|
|
6
|
+
// side pins a four-label round trip for exactly that reason. Every hostname is
|
|
7
|
+
// `encodeURIComponent`d here, which is what keeps a name with an unexpected
|
|
8
|
+
// character from changing the shape of the path.
|
|
9
|
+
//
|
|
10
|
+
// The project is resolved and authorized FIRST, by the same class check every
|
|
11
|
+
// other project route uses, so a caller who does not hold the project gets the
|
|
12
|
+
// ordinary indistinguishable 404 before the hostname is read at all. The CLI
|
|
13
|
+
// therefore cannot distinguish "no such project" from "not your project" — and
|
|
14
|
+
// must not try.
|
|
15
|
+
import { clientHeaders } from "../shared/clientHeader.js";
|
|
16
|
+
import { parseDomain, parseDomainList, } from "./wire.js";
|
|
17
|
+
export class DomainApiError extends Error {
|
|
18
|
+
status;
|
|
19
|
+
code;
|
|
20
|
+
detail;
|
|
21
|
+
retryAfterSec;
|
|
22
|
+
constructor(status, code, message, detail = {}, retryAfterSec) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.detail = detail;
|
|
27
|
+
this.retryAfterSec = retryAfterSec;
|
|
28
|
+
this.name = "DomainApiError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function readError(res) {
|
|
32
|
+
let body = {};
|
|
33
|
+
try {
|
|
34
|
+
body = (await res.json());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// A non-JSON error body is still an error; the status carries the meaning.
|
|
38
|
+
}
|
|
39
|
+
const retryAfter = res.headers.get("retry-after");
|
|
40
|
+
return new DomainApiError(res.status, body.code ?? `http_${res.status}`, body.message ?? `request failed (${res.status})`, body.detail ?? {}, retryAfter ? Number(retryAfter) : undefined);
|
|
41
|
+
}
|
|
42
|
+
function authHeaders(cfg) {
|
|
43
|
+
return { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
44
|
+
}
|
|
45
|
+
function rolesOf(raw) {
|
|
46
|
+
const o = typeof raw === "object" && raw !== null ? raw : {};
|
|
47
|
+
const rows = Array.isArray(o.records) ? o.records : [];
|
|
48
|
+
return rows.map((r) => {
|
|
49
|
+
const rec = typeof r === "object" && r !== null ? r : {};
|
|
50
|
+
return typeof rec.role === "string" ? rec.role : undefined;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** POST /projects/{id}/domains — returns the records to publish. Rate-limited. */
|
|
54
|
+
export async function attachDomain(cfg, projectId, domain, fetchImpl = fetch) {
|
|
55
|
+
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { ...authHeaders(cfg), "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ domain }),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
throw await readError(res);
|
|
62
|
+
const raw = await res.json();
|
|
63
|
+
const parsed = parseDomain(unwrap(raw));
|
|
64
|
+
if (!parsed) {
|
|
65
|
+
throw new DomainApiError(res.status, "malformed_response", "attach: response carried no domain");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
domain: parsed.domain,
|
|
69
|
+
roles: rolesOf(unwrap(raw)),
|
|
70
|
+
droppedFindings: parsed.droppedFindings,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* POST /projects/{id}/domains/{domain}/verify — the on-demand re-check.
|
|
75
|
+
*
|
|
76
|
+
* Rate-limited per `(org, domain)`, so a 429 here is expected traffic rather than
|
|
77
|
+
* a defect, and its copy names no limit family.
|
|
78
|
+
*/
|
|
79
|
+
export async function verifyDomain(cfg, projectId, domain, fetchImpl = fetch) {
|
|
80
|
+
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}/verify`, { method: "POST", headers: authHeaders(cfg) });
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
throw await readError(res);
|
|
83
|
+
const raw = await res.json();
|
|
84
|
+
const parsed = parseDomain(unwrap(raw));
|
|
85
|
+
if (!parsed) {
|
|
86
|
+
throw new DomainApiError(res.status, "malformed_response", "verify: response carried no domain");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
domain: parsed.domain,
|
|
90
|
+
roles: rolesOf(unwrap(raw)),
|
|
91
|
+
droppedFindings: parsed.droppedFindings,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** GET /projects/{id}/domains — this project's domains with state + findings. */
|
|
95
|
+
export async function listProjectDomains(cfg, projectId, fetchImpl = fetch) {
|
|
96
|
+
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains`, { headers: authHeaders(cfg) });
|
|
97
|
+
if (!res.ok)
|
|
98
|
+
throw await readError(res);
|
|
99
|
+
return parseDomainList(await res.json());
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* GET /orgs/{id}/domains — the workspace's whole fleet and the count against the
|
|
103
|
+
* limit.
|
|
104
|
+
*
|
|
105
|
+
* This is what `vincentt domain list` reads, and it is why the anti-oracle
|
|
106
|
+
* collision copy is safe: a creator who genuinely holds a contested name finds it
|
|
107
|
+
* here, and this route only ever returns their own.
|
|
108
|
+
*/
|
|
109
|
+
export async function listOrgDomains(cfg, orgSlug, fetchImpl = fetch) {
|
|
110
|
+
const res = await fetchImpl(`${cfg.apiUrl}/orgs/${encodeURIComponent(orgSlug)}/domains`, { headers: authHeaders(cfg) });
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
throw await readError(res);
|
|
113
|
+
return parseDomainList(await res.json());
|
|
114
|
+
}
|
|
115
|
+
/** DELETE /projects/{id}/domains/{domain} — detach. The row is never deleted server-side. */
|
|
116
|
+
export async function detachDomain(cfg, projectId, domain, fetchImpl = fetch) {
|
|
117
|
+
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`, { method: "DELETE", headers: authHeaders(cfg) });
|
|
118
|
+
// 404 is a SILENT RESOLVE on detach: the creator asked for a state the world is
|
|
119
|
+
// already in. It is not distinguishable from "not your project", and it does
|
|
120
|
+
// not need to be — either way this project is not serving that name.
|
|
121
|
+
if (res.status === 404)
|
|
122
|
+
return;
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
throw await readError(res);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Unwrap a `{domain: {...}}` envelope if the API sends one, else treat the body as
|
|
128
|
+
* the row.
|
|
129
|
+
*
|
|
130
|
+
* Tolerating both is NOT the legacy-tolerance anti-pattern `backend.ts` removed:
|
|
131
|
+
* that one accepted two spellings of a FIELD (`_id` vs `id`), where silently
|
|
132
|
+
* accepting the old shape hides a contract break. This tolerates an ENVELOPE,
|
|
133
|
+
* where both readings name the same field set and the ambiguity is resolved by
|
|
134
|
+
* looking for `domain` as a string — the row's own required field.
|
|
135
|
+
*/
|
|
136
|
+
function unwrap(raw) {
|
|
137
|
+
if (typeof raw !== "object" || raw === null)
|
|
138
|
+
return raw;
|
|
139
|
+
const o = raw;
|
|
140
|
+
if (typeof o.domain === "string")
|
|
141
|
+
return o;
|
|
142
|
+
if (typeof o.domain === "object" && o.domain !== null)
|
|
143
|
+
return o.domain;
|
|
144
|
+
return o;
|
|
145
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
|
+
import { type DomainListView, type DomainView } from "./wire.js";
|
|
3
|
+
import type { AttachResult } from "./api.js";
|
|
4
|
+
/** The canonical subcommands. */
|
|
5
|
+
export declare const DOMAIN_SUBCOMMANDS: readonly ["attach", "list", "verify", "detach"];
|
|
6
|
+
export type DomainSub = (typeof DOMAIN_SUBCOMMANDS)[number];
|
|
7
|
+
/**
|
|
8
|
+
* The silent aliases. `add`→`attach`, `remove`→`detach`.
|
|
9
|
+
*
|
|
10
|
+
* A map, not a chain of `if`s, so the pair is data a test can enumerate and the
|
|
11
|
+
* set cannot drift from what `resolveSub` accepts.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DOMAIN_ALIASES: Readonly<Record<string, DomainSub>>;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a typed subcommand to its canonical verb, or undefined.
|
|
16
|
+
*
|
|
17
|
+
* RETURNS ONLY THE CANONICAL VERB. It deliberately does not report whether an
|
|
18
|
+
* alias was used: a caller that cannot tell cannot print a correction, which is
|
|
19
|
+
* the structural form of "the aliases are silent". Making the alias invisible at
|
|
20
|
+
* the type level is stronger than remembering not to print it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveSub(typed: string | undefined): DomainSub | undefined;
|
|
23
|
+
export interface DomainDeps {
|
|
24
|
+
print: (line: string) => void;
|
|
25
|
+
config: () => Promise<ResolvedConfig | undefined>;
|
|
26
|
+
/** The linked project for this folder, or undefined when nothing is linked. */
|
|
27
|
+
binding: () => Promise<{
|
|
28
|
+
projectId: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
} | undefined>;
|
|
31
|
+
/** The workspace to list. Resolved by the caller's own precedence chain. */
|
|
32
|
+
org: () => Promise<{
|
|
33
|
+
ok: true;
|
|
34
|
+
org: {
|
|
35
|
+
slug: string;
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
message: string;
|
|
41
|
+
}>;
|
|
42
|
+
attach: (cfg: ResolvedConfig, projectId: string, domain: string) => Promise<AttachResult>;
|
|
43
|
+
verify: (cfg: ResolvedConfig, projectId: string, domain: string) => Promise<AttachResult>;
|
|
44
|
+
listOrg: (cfg: ResolvedConfig, orgSlug: string) => Promise<DomainListView>;
|
|
45
|
+
detach: (cfg: ResolvedConfig, projectId: string, domain: string) => Promise<void>;
|
|
46
|
+
confirm: (prompt: string, defaultYes: boolean) => Promise<boolean>;
|
|
47
|
+
isTTY: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface DomainOptions {
|
|
50
|
+
/** The hostname argument, as typed. */
|
|
51
|
+
hostname?: string;
|
|
52
|
+
json?: boolean;
|
|
53
|
+
/** `--yes` — the explicit non-interactive consent for detach. */
|
|
54
|
+
yes?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The dispatch. Returns the process exit code.
|
|
58
|
+
*
|
|
59
|
+
* Every arm returns explicitly rather than falling through to a shared tail: an
|
|
60
|
+
* exit code assembled at the end of a long function is an exit code a later
|
|
61
|
+
* branch changes by accident, and the exit code is the thing an agent branches
|
|
62
|
+
* on.
|
|
63
|
+
*/
|
|
64
|
+
export declare function domainCommand(deps: DomainDeps, sub: DomainSub, opts?: DomainOptions): Promise<number>;
|
|
65
|
+
/**
|
|
66
|
+
* The verify render, split out so the state→transcript mapping is one readable
|
|
67
|
+
* table and the exit code is computed in exactly one place per arm.
|
|
68
|
+
*/
|
|
69
|
+
export declare function renderVerify(deps: Pick<DomainDeps, "print">, view: DomainView, roles: readonly (string | undefined)[]): number;
|
|
70
|
+
/**
|
|
71
|
+
* The states where a creator returns to re-read the record values: pending,
|
|
72
|
+
* failing and held (QA-F16-C1). A creator whose brand is dark should not have to
|
|
73
|
+
* navigate to find the values again.
|
|
74
|
+
*/
|
|
75
|
+
export declare function needsRecordsReprinted(view: Pick<DomainView, "state">): boolean;
|