@vincentt-xr/cli 0.17.0 → 0.18.1
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 +31 -0
- package/README.md +94 -1
- package/dist/cli/index.js +170 -84
- 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 +5 -11
- package/dist/preview/api.js +13 -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 +22 -5
- package/dist/publish/api.js +80 -9
- package/dist/publish/command.d.ts +27 -0
- package/dist/publish/command.js +261 -50
- 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 +59 -5
- package/dist/publish/upload.js +138 -30
- 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 +7 -2
- package/dist/shared/countLimit.js +12 -5
- package/dist/shared/http.d.ts +23 -0
- package/dist/shared/http.js +58 -0
- package/package.json +7 -3
package/dist/cli/usage-text.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// The help text, in its own module so it can be rendered without importing the
|
|
2
2
|
// CLI entrypoint — `cli/index.ts` calls `main()` at module scope, so importing it
|
|
3
3
|
// runs the CLI, writes the dx journal and sets process.exitCode.
|
|
4
|
-
export const USAGE_TEXT = `Vincentt CLI
|
|
4
|
+
export const USAGE_TEXT = `Vincentt CLI: the local-first AR dev loop\n\n` +
|
|
5
5
|
`Usage: vincentt <command> [options]\n\n` +
|
|
6
6
|
`Getting started (building a new app with a coding agent)\n` +
|
|
7
7
|
` 1. Confirm a project name with the developer first.\n` +
|
|
8
8
|
` 2. Create: vincentt create <name> (scaffolds if the folder is empty)\n` +
|
|
9
9
|
` 3. The scaffold writes the agent contract into the project root. Read it,\n` +
|
|
10
|
-
` confirm the project is ready, and stop
|
|
10
|
+
` confirm the project is ready, and stop. Do NOT invent an app.\n` +
|
|
11
11
|
` Wait for the developer to describe what to build.\n` +
|
|
12
12
|
` Full guide: npm view @vincentt-xr/cli readme\n\n` +
|
|
13
13
|
`Auth\n` +
|
|
@@ -16,7 +16,7 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
16
16
|
` logout Remove the saved access token\n` +
|
|
17
17
|
` whoami Show the signed-in account\n\n` +
|
|
18
18
|
`Project\n` +
|
|
19
|
-
` init [dir] Scaffold a Vincentt XR app
|
|
19
|
+
` init [dir] Scaffold a Vincentt XR app; no account needed, ever\n` +
|
|
20
20
|
` [--no-install] [--template <version>]\n` +
|
|
21
21
|
` [--from <owner>/<repo>@<tag>] Clone a gallery template\n` +
|
|
22
22
|
` and register it [--name <s>] [--slug <s>]\n` +
|
|
@@ -28,7 +28,18 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
28
28
|
` Omit the name to use the current folder as-is.\n` +
|
|
29
29
|
` [--name <s>] [--slug <s>]\n` +
|
|
30
30
|
` [--org <s>] [--preview] [--force] [--no-install]\n` +
|
|
31
|
+
` In an app that already carries a @vincentt-xr package it\n` +
|
|
32
|
+
` only registers, then settles vincentt.json like link does.\n` +
|
|
31
33
|
` link <project-id> Link this folder to an existing project\n` +
|
|
34
|
+
` For an app not made from the Vincentt template, link also\n` +
|
|
35
|
+
` reads how the app runs (it runs nothing) and writes\n` +
|
|
36
|
+
` vincentt.json: dev.command, dev.port, build.command and\n` +
|
|
37
|
+
` build.output. Commit it. A present vincentt.json is used\n` +
|
|
38
|
+
` as-is, with no defaults, and is never rewritten.\n` +
|
|
39
|
+
` [--json] [--dev-command <cmd>] [--dev-port <n>]\n` +
|
|
40
|
+
` [--build-command <cmd>] [--build-output <folder>]\n` +
|
|
41
|
+
` Diagnostics for such an app: npm i @vincentt-xr/harness,\n` +
|
|
42
|
+
` then import "@vincentt-xr/harness/auto"; (see the readme).\n` +
|
|
32
43
|
` unlink Disconnect this folder from its project\n` +
|
|
33
44
|
` projects List the workspace's projects, their ids, and where each\n` +
|
|
34
45
|
` one lives on this machine [--json]\n` +
|
|
@@ -37,9 +48,17 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
37
48
|
` [--org <s>] to list a workspace other than the active one\n\n` +
|
|
38
49
|
`Dev loop\n` +
|
|
39
50
|
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
40
|
-
` [--dist <path>] [--note <s>] [--yes]\n` +
|
|
51
|
+
` [--dist <path>] [--note <s>] [--yes] [--json]\n` +
|
|
52
|
+
` Says live once the address serves the new version\n` +
|
|
53
|
+
` (waits up to 90s; --json needs --yes).\n` +
|
|
54
|
+
` The folder is build.output from vincentt.json, else dist.\n` +
|
|
55
|
+
` --dist and build.output are relative to the project\n` +
|
|
56
|
+
` folder (the one holding the link), not the working dir.\n` +
|
|
41
57
|
` preview Serve the app at an https link a phone can open\n` +
|
|
42
|
-
` [--port <n>] [--
|
|
58
|
+
` [--port <n>] [--dev-command <cmd>] [--tester <id>]\n` +
|
|
59
|
+
` [--no-qr] [--stop]\n` +
|
|
60
|
+
` Runs dev.command and forwards dev.port from vincentt.json,\n` +
|
|
61
|
+
` else npm run dev on 5173.\n` +
|
|
43
62
|
` Re-running prints the live link; --restart mints a new one.\n\n` +
|
|
44
63
|
// f16. HELP TEACHES THE CANONICAL VERBS ONLY: `add` and `remove` are accepted
|
|
45
64
|
// as silent aliases and are deliberately ABSENT here, because help, docs and
|
|
@@ -97,7 +116,7 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
97
116
|
// DELIBERATELY ABSENT: any product name; any shell idiom; any mention of a
|
|
98
117
|
// scheduled task EVEN TO FORBID ONE (naming a bad pattern is how a reader
|
|
99
118
|
// learns it exists); any "recommended for AI" framing.
|
|
100
|
-
`Reverse channel
|
|
119
|
+
`Reverse channel: feedback sent from the phone\n` +
|
|
101
120
|
` feedback --wait Block until feedback arrives, print every annotation\n` +
|
|
102
121
|
` waiting as one JSON object per line, then exit.\n` +
|
|
103
122
|
` [--timeout <seconds>, default 300]\n` +
|
|
@@ -119,8 +138,9 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
119
138
|
` Prints the installed and current version of each Vincentt\n` +
|
|
120
139
|
` package, the string to place in package.json, and any\n` +
|
|
121
140
|
` migration note the platform published. Writes nothing.\n` +
|
|
122
|
-
` dx Where this dev loop fought you [--
|
|
141
|
+
` dx Where this dev loop fought you [--since <duration>] [--json]\n` +
|
|
123
142
|
` Reads a local journal of your own runs. Local by default.\n` +
|
|
143
|
+
` Covers the last 24 hours; --since 30m, 2h or 7d changes it.\n` +
|
|
124
144
|
` --send reports the findings to the Vincentt team; it\n` +
|
|
125
145
|
` shows the exact payload and asks first. [--note <text>]\n` +
|
|
126
146
|
` dx enable Allow reports to be sent from this machine without asking.\n` +
|
|
@@ -129,9 +149,11 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
129
149
|
` \`vincentt version\` while on.\n` +
|
|
130
150
|
` dx disable Turn that off again.\n` +
|
|
131
151
|
` report "<text>" Tell us something went wrong, in your own words.\n` +
|
|
132
|
-
` For when nothing failed but something looked wrong
|
|
152
|
+
` For when nothing failed but something looked wrong,\n` +
|
|
133
153
|
` the case \`dx --send\` cannot see. Shows the exact\n` +
|
|
134
154
|
` payload and asks first. [--no-context] [--yes]\n\n` +
|
|
135
|
-
`Common options: --dir <path> (where to work, default cwd
|
|
155
|
+
`Common options: --dir <path> (where to work, default cwd; for \`create\` this is\n` +
|
|
136
156
|
` the PARENT of the new folder, not the folder itself)\n` +
|
|
137
|
-
` --allow-custom-api honor a binding's apiUrl override\n
|
|
157
|
+
` --allow-custom-api honor a binding's apiUrl override\n\n` +
|
|
158
|
+
`Exit 4 from any verb that needs an account: not signed in, or the stored token\n` +
|
|
159
|
+
`was refused. Run \`vincentt login\` at a terminal, then run the verb again.\n`;
|
package/dist/domain/api.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
2
|
import { type DomainListView, type DomainView } from "./wire.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The ceiling refusal's figures. `domain_limit_reached` carries them at the TOP
|
|
5
|
+
* of the body, beside `code` and `message`, not under `detail`. `entitled` is 0
|
|
6
|
+
* for a plan that carries no custom domains at all.
|
|
7
|
+
*/
|
|
8
|
+
export interface DomainLimitFigures {
|
|
9
|
+
attached?: number;
|
|
10
|
+
entitled?: number;
|
|
9
11
|
}
|
|
10
12
|
export declare class DomainApiError extends Error {
|
|
11
13
|
readonly status: number;
|
|
12
14
|
readonly code: string;
|
|
13
|
-
readonly
|
|
15
|
+
readonly figures: DomainLimitFigures;
|
|
14
16
|
readonly retryAfterSec?: number | undefined;
|
|
15
|
-
constructor(status: number, code: string, message: string,
|
|
17
|
+
constructor(status: number, code: string, message: string, figures?: DomainLimitFigures, retryAfterSec?: number | undefined);
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* The record's role, when the API names one, parallel to `records`.
|
package/dist/domain/api.js
CHANGED
|
@@ -13,22 +13,26 @@
|
|
|
13
13
|
// therefore cannot distinguish "no such project" from "not your project" — and
|
|
14
14
|
// must not try.
|
|
15
15
|
import { clientHeaders } from "../shared/clientHeader.js";
|
|
16
|
+
import { rejectAuthExpired } from "../shared/authError.js";
|
|
17
|
+
import { apiFetch } from "../shared/http.js";
|
|
16
18
|
import { parseDomain, parseDomainList, } from "./wire.js";
|
|
17
19
|
export class DomainApiError extends Error {
|
|
18
20
|
status;
|
|
19
21
|
code;
|
|
20
|
-
|
|
22
|
+
figures;
|
|
21
23
|
retryAfterSec;
|
|
22
|
-
constructor(status, code, message,
|
|
24
|
+
constructor(status, code, message, figures = {}, retryAfterSec) {
|
|
23
25
|
super(message);
|
|
24
26
|
this.status = status;
|
|
25
27
|
this.code = code;
|
|
26
|
-
this.
|
|
28
|
+
this.figures = figures;
|
|
27
29
|
this.retryAfterSec = retryAfterSec;
|
|
28
30
|
this.name = "DomainApiError";
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
/** Rejects with AuthExpiredError on a 401; every other status becomes a DomainApiError. */
|
|
31
34
|
async function readError(res) {
|
|
35
|
+
rejectAuthExpired(res);
|
|
32
36
|
let body = {};
|
|
33
37
|
try {
|
|
34
38
|
body = (await res.json());
|
|
@@ -36,8 +40,9 @@ async function readError(res) {
|
|
|
36
40
|
catch {
|
|
37
41
|
// A non-JSON error body is still an error; the status carries the meaning.
|
|
38
42
|
}
|
|
43
|
+
const num = (v) => (typeof v === "number" ? v : undefined);
|
|
39
44
|
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.
|
|
45
|
+
return new DomainApiError(res.status, body.code ?? `http_${res.status}`, body.message ?? `request failed (${res.status})`, { attached: num(body.attached), entitled: num(body.entitled) }, retryAfter ? Number(retryAfter) : undefined);
|
|
41
46
|
}
|
|
42
47
|
function authHeaders(cfg) {
|
|
43
48
|
return { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
@@ -51,7 +56,7 @@ function rolesOf(raw) {
|
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
/** POST /projects/{id}/domains — returns the records to publish. Rate-limited. */
|
|
54
|
-
export async function attachDomain(cfg, projectId, domain, fetchImpl =
|
|
59
|
+
export async function attachDomain(cfg, projectId, domain, fetchImpl = apiFetch) {
|
|
55
60
|
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains`, {
|
|
56
61
|
method: "POST",
|
|
57
62
|
headers: { ...authHeaders(cfg), "content-type": "application/json" },
|
|
@@ -76,7 +81,7 @@ export async function attachDomain(cfg, projectId, domain, fetchImpl = fetch) {
|
|
|
76
81
|
* Rate-limited per `(org, domain)`, so a 429 here is expected traffic rather than
|
|
77
82
|
* a defect, and its copy names no limit family.
|
|
78
83
|
*/
|
|
79
|
-
export async function verifyDomain(cfg, projectId, domain, fetchImpl =
|
|
84
|
+
export async function verifyDomain(cfg, projectId, domain, fetchImpl = apiFetch) {
|
|
80
85
|
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}/verify`, { method: "POST", headers: authHeaders(cfg) });
|
|
81
86
|
if (!res.ok)
|
|
82
87
|
throw await readError(res);
|
|
@@ -92,7 +97,7 @@ export async function verifyDomain(cfg, projectId, domain, fetchImpl = fetch) {
|
|
|
92
97
|
};
|
|
93
98
|
}
|
|
94
99
|
/** GET /projects/{id}/domains — this project's domains with state + findings. */
|
|
95
|
-
export async function listProjectDomains(cfg, projectId, fetchImpl =
|
|
100
|
+
export async function listProjectDomains(cfg, projectId, fetchImpl = apiFetch) {
|
|
96
101
|
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains`, { headers: authHeaders(cfg) });
|
|
97
102
|
if (!res.ok)
|
|
98
103
|
throw await readError(res);
|
|
@@ -106,14 +111,14 @@ export async function listProjectDomains(cfg, projectId, fetchImpl = fetch) {
|
|
|
106
111
|
* collision copy is safe: a creator who genuinely holds a contested name finds it
|
|
107
112
|
* here, and this route only ever returns their own.
|
|
108
113
|
*/
|
|
109
|
-
export async function listOrgDomains(cfg, orgSlug, fetchImpl =
|
|
114
|
+
export async function listOrgDomains(cfg, orgSlug, fetchImpl = apiFetch) {
|
|
110
115
|
const res = await fetchImpl(`${cfg.apiUrl}/orgs/${encodeURIComponent(orgSlug)}/domains`, { headers: authHeaders(cfg) });
|
|
111
116
|
if (!res.ok)
|
|
112
117
|
throw await readError(res);
|
|
113
118
|
return parseDomainList(await res.json());
|
|
114
119
|
}
|
|
115
120
|
/** DELETE /projects/{id}/domains/{domain} — detach. The row is never deleted server-side. */
|
|
116
|
-
export async function detachDomain(cfg, projectId, domain, fetchImpl =
|
|
121
|
+
export async function detachDomain(cfg, projectId, domain, fetchImpl = apiFetch) {
|
|
117
122
|
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`, { method: "DELETE", headers: authHeaders(cfg) });
|
|
118
123
|
// 404 is a SILENT RESOLVE on detach: the creator asked for a state the world is
|
|
119
124
|
// already in. It is not distinguishable from "not your project", and it does
|
package/dist/domain/command.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { EXIT } from "../lib/exitCodes.js";
|
|
22
22
|
import { VALIDATION_RECORD_GONE } from "./wire.js";
|
|
23
23
|
import { DomainApiError } from "./api.js";
|
|
24
|
+
import { AuthExpiredError, authExpiredLines } from "../shared/authError.js";
|
|
24
25
|
import { localRefusal, normalizeArgument } from "./hostname.js";
|
|
25
26
|
import * as copy from "./copy.js";
|
|
26
27
|
/** The canonical subcommands. */
|
|
@@ -170,7 +171,15 @@ async function attachVerb(deps, cfg, opts) {
|
|
|
170
171
|
}
|
|
171
172
|
return 0;
|
|
172
173
|
}
|
|
174
|
+
/** The api refused the token: the sign-in remedy, and the not-signed-in exit code. */
|
|
175
|
+
function authExpired(deps) {
|
|
176
|
+
for (const l of authExpiredLines())
|
|
177
|
+
deps.print(l);
|
|
178
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
179
|
+
}
|
|
173
180
|
function printAttachError(deps, err, hostname) {
|
|
181
|
+
if (err instanceof AuthExpiredError)
|
|
182
|
+
return authExpired(deps);
|
|
174
183
|
if (!(err instanceof DomainApiError)) {
|
|
175
184
|
deps.print("");
|
|
176
185
|
deps.print(`✗ Couldn't reach Vincentt to attach ${hostname}.`);
|
|
@@ -179,31 +188,25 @@ function printAttachError(deps, err, hostname) {
|
|
|
179
188
|
deps.print("");
|
|
180
189
|
return 1;
|
|
181
190
|
}
|
|
182
|
-
// The refusals, each by the server's own code. The ceiling figure
|
|
183
|
-
//
|
|
191
|
+
// The refusals, each by the server's own code. The ceiling figure comes from
|
|
192
|
+
// the API; the CLI holds no plan ladder. A platform hostname or a root domain
|
|
193
|
+
// the local check missed arrives as `domain_invalid` and takes the generic arm.
|
|
184
194
|
switch (err.code) {
|
|
185
|
-
case "domain_limit_reached":
|
|
186
|
-
for
|
|
195
|
+
case "domain_limit_reached": {
|
|
196
|
+
// One code for both: a plan with no custom domains is refused at a
|
|
197
|
+
// ceiling of 0, and "using all 0" would read as a bug.
|
|
198
|
+
const entitled = err.figures.entitled;
|
|
199
|
+
const lines = entitled === 0 ? copy.refusedNoEntitlement() : copy.refusedAtCeiling(entitled);
|
|
200
|
+
for (const l of lines)
|
|
187
201
|
deps.print(l);
|
|
188
202
|
return 1;
|
|
203
|
+
}
|
|
189
204
|
case "domain_taken":
|
|
190
205
|
// BYTE-IDENTICAL whether the holder is this workspace or a stranger. The
|
|
191
206
|
// CLI passes no holder information because the API sends none.
|
|
192
207
|
for (const l of copy.refusedAlreadyAttached(hostname))
|
|
193
208
|
deps.print(l);
|
|
194
209
|
return 1;
|
|
195
|
-
case "platform_hostname":
|
|
196
|
-
for (const l of copy.refusedPlatformHost())
|
|
197
|
-
deps.print(l);
|
|
198
|
-
return 1;
|
|
199
|
-
case "root_domain_unsupported":
|
|
200
|
-
for (const l of copy.refusedRootDomain(hostname))
|
|
201
|
-
deps.print(l);
|
|
202
|
-
return 1;
|
|
203
|
-
case "no_entitlement":
|
|
204
|
-
for (const l of copy.refusedNoEntitlement(err.detail.planName))
|
|
205
|
-
deps.print(l);
|
|
206
|
-
return 1;
|
|
207
210
|
default:
|
|
208
211
|
break;
|
|
209
212
|
}
|
|
@@ -236,6 +239,8 @@ async function listVerb(deps, cfg, opts) {
|
|
|
236
239
|
view = await deps.listOrg(cfg, org.org.slug);
|
|
237
240
|
}
|
|
238
241
|
catch (err) {
|
|
242
|
+
if (err instanceof AuthExpiredError)
|
|
243
|
+
return authExpired(deps);
|
|
239
244
|
deps.print("");
|
|
240
245
|
deps.print(err instanceof DomainApiError
|
|
241
246
|
? `✗ ${err.message}`
|
|
@@ -288,6 +293,8 @@ async function verifyVerb(deps, cfg, opts) {
|
|
|
288
293
|
result = await deps.verify(cfg, binding.projectId, hostname);
|
|
289
294
|
}
|
|
290
295
|
catch (err) {
|
|
296
|
+
if (err instanceof AuthExpiredError)
|
|
297
|
+
return authExpired(deps);
|
|
291
298
|
if (err instanceof DomainApiError && err.status === 404) {
|
|
292
299
|
for (const l of copy.notAttached(hostname))
|
|
293
300
|
deps.print(l);
|
|
@@ -452,6 +459,8 @@ async function detachVerb(deps, cfg, opts) {
|
|
|
452
459
|
await deps.detach(cfg, binding.projectId, hostname);
|
|
453
460
|
}
|
|
454
461
|
catch (err) {
|
|
462
|
+
if (err instanceof AuthExpiredError)
|
|
463
|
+
return authExpired(deps);
|
|
455
464
|
deps.print("");
|
|
456
465
|
deps.print(err instanceof DomainApiError
|
|
457
466
|
? `✗ ${err.message}`
|
package/dist/domain/copy.d.ts
CHANGED
|
@@ -166,7 +166,7 @@ export declare const NOTHING_WAS_CLAIMED = " Nothing was claimed.";
|
|
|
166
166
|
* No price, no plan name other than the one they are on, no URL. Clause 4 reaches
|
|
167
167
|
* the CLI too.
|
|
168
168
|
*/
|
|
169
|
-
export declare function refusedAtCeiling(
|
|
169
|
+
export declare function refusedAtCeiling(entitled: number | undefined): string[];
|
|
170
170
|
/**
|
|
171
171
|
* Already attached — THE ANTI-ORACLE ANSWER.
|
|
172
172
|
*
|
|
@@ -192,8 +192,11 @@ export declare function refusedPlatformHost(): string[];
|
|
|
192
192
|
* doesn't support it"* sounds like something we might fix on request.
|
|
193
193
|
*/
|
|
194
194
|
export declare function refusedRootDomain(domain: string): string[];
|
|
195
|
-
/**
|
|
196
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Not on a plan that carries custom domains: the ceiling refusal at
|
|
197
|
+
* `entitled: 0`. The body names no plan, so neither does this.
|
|
198
|
+
*/
|
|
199
|
+
export declare function refusedNoEntitlement(): string[];
|
|
197
200
|
/**
|
|
198
201
|
* A verify refused by the rate limit.
|
|
199
202
|
*
|
package/dist/domain/copy.js
CHANGED
|
@@ -477,10 +477,12 @@ export const NOTHING_WAS_CLAIMED = " Nothing was claimed.";
|
|
|
477
477
|
* No price, no plan name other than the one they are on, no URL. Clause 4 reaches
|
|
478
478
|
* the CLI too.
|
|
479
479
|
*/
|
|
480
|
-
export function refusedAtCeiling(
|
|
480
|
+
export function refusedAtCeiling(entitled) {
|
|
481
|
+
// A body without the figure states no number rather than inventing one.
|
|
482
|
+
const all = entitled === undefined ? "all" : `all ${entitled}`;
|
|
481
483
|
return [
|
|
482
484
|
"",
|
|
483
|
-
`✗ This workspace is using
|
|
485
|
+
`✗ This workspace is using ${all} of the custom domains its plan carries.`,
|
|
484
486
|
"",
|
|
485
487
|
" Detach one to attach this:",
|
|
486
488
|
" vincentt domain list",
|
|
@@ -550,12 +552,14 @@ export function refusedRootDomain(domain) {
|
|
|
550
552
|
"",
|
|
551
553
|
];
|
|
552
554
|
}
|
|
553
|
-
/**
|
|
554
|
-
|
|
555
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Not on a plan that carries custom domains: the ceiling refusal at
|
|
557
|
+
* `entitled: 0`. The body names no plan, so neither does this.
|
|
558
|
+
*/
|
|
559
|
+
export function refusedNoEntitlement() {
|
|
556
560
|
return [
|
|
557
561
|
"",
|
|
558
|
-
|
|
562
|
+
"✗ Custom domains are part of the paid plans.",
|
|
559
563
|
"",
|
|
560
564
|
" An owner can change the workspace's plan in the console.",
|
|
561
565
|
"",
|
package/dist/dx/command.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ConsentDecision } from "./consent.js";
|
|
2
2
|
import type { Enablement } from "./enablement.js";
|
|
3
|
-
import type { JournalEntry } from "./journal.js";
|
|
3
|
+
import type { JournalEntry, JournalWindow } from "./journal.js";
|
|
4
4
|
import type { WireReport } from "./send.js";
|
|
5
5
|
import { type BuiltWith } from "../platform/resolve.js";
|
|
6
6
|
export interface DxDeps {
|
|
7
|
-
|
|
7
|
+
/** Entries inside the window, or the whole journal when called without one. */
|
|
8
|
+
entries: (window?: JournalWindow) => JournalEntry[];
|
|
8
9
|
print: (line: string) => void;
|
|
9
10
|
now: () => number;
|
|
10
11
|
enabled: () => boolean;
|
|
@@ -15,9 +16,17 @@ export interface DxDeps {
|
|
|
15
16
|
*/
|
|
16
17
|
enablement?: () => Enablement;
|
|
17
18
|
}
|
|
19
|
+
/** How far back a report looks. */
|
|
20
|
+
export interface DxWindow {
|
|
21
|
+
ms: number;
|
|
22
|
+
/** The compact form a caller types and `--json` carries: `30m`, `24h`, `7d`. */
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
18
25
|
export interface DxOptions {
|
|
19
|
-
/** Only consider runs
|
|
20
|
-
|
|
26
|
+
/** Only consider runs inside this window. */
|
|
27
|
+
window: DxWindow;
|
|
28
|
+
/** The `--since` value that did not parse, when one was given. */
|
|
29
|
+
sinceInvalid?: string;
|
|
21
30
|
json: boolean;
|
|
22
31
|
/** Send the findings to the Vincentt team. Never implied by anything else. */
|
|
23
32
|
send: boolean;
|
|
@@ -30,6 +39,17 @@ export interface DxOptions {
|
|
|
30
39
|
/** `vincentt dx disable` — a positional subcommand, not a flag. */
|
|
31
40
|
disable: boolean;
|
|
32
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* One sitting, not the whole file. A report is read as describing the session
|
|
44
|
+
* that prompted it, so the default must not reach back into earlier ones.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_WINDOW: DxWindow;
|
|
47
|
+
/** `30m`, `2h`, `7d`: a positive whole number and one unit. Anything else is undefined. */
|
|
48
|
+
export declare function parseDuration(raw: string): DxWindow | undefined;
|
|
49
|
+
/** `the last 24 hours`, `the last hour`, `the last 7 days`. */
|
|
50
|
+
export declare function describeWindow(w: DxWindow): string;
|
|
51
|
+
/** The usage refusal for a `--since` value that did not parse. */
|
|
52
|
+
export declare function sinceUsageLines(raw: string): string[];
|
|
33
53
|
export declare function parseDxOptions(args: string[]): DxOptions;
|
|
34
54
|
export declare function dxCommand(deps: DxDeps, options: DxOptions): number;
|
|
35
55
|
/** Wire the command to the real journal. */
|
package/dist/dx/command.js
CHANGED
|
@@ -3,10 +3,64 @@ import { enablementLine } from "./enablement.js";
|
|
|
3
3
|
import { journalEnabled, journalPath, readJournal } from "./journal.js";
|
|
4
4
|
import { buildReport, renderPayload } from "./send.js";
|
|
5
5
|
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
6
|
-
|
|
6
|
+
import { EXIT } from "../lib/exitCodes.js";
|
|
7
|
+
const MINUTE_MS = 60 * 1000;
|
|
8
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
9
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
10
|
+
const UNIT_MS = { m: MINUTE_MS, h: HOUR_MS, d: DAY_MS };
|
|
11
|
+
const UNIT_WORD = { m: "minute", h: "hour", d: "day" };
|
|
12
|
+
/**
|
|
13
|
+
* One sitting, not the whole file. A report is read as describing the session
|
|
14
|
+
* that prompted it, so the default must not reach back into earlier ones.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_WINDOW = { ms: DAY_MS, label: "24h" };
|
|
17
|
+
/** `30m`, `2h`, `7d`: a positive whole number and one unit. Anything else is undefined. */
|
|
18
|
+
export function parseDuration(raw) {
|
|
19
|
+
const m = /^([1-9]\d*)([mhd])$/.exec(raw.trim());
|
|
20
|
+
if (m === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
const n = Number(m[1]);
|
|
23
|
+
const ms = n * UNIT_MS[m[2]];
|
|
24
|
+
if (!Number.isSafeInteger(ms))
|
|
25
|
+
return undefined;
|
|
26
|
+
return { ms, label: `${n}${m[2]}` };
|
|
27
|
+
}
|
|
28
|
+
/** `the last 24 hours`, `the last hour`, `the last 7 days`. */
|
|
29
|
+
export function describeWindow(w) {
|
|
30
|
+
const m = /^(\d+(?:\.\d+)?)([mhd])$/.exec(w.label);
|
|
31
|
+
if (m === null)
|
|
32
|
+
return `the last ${w.label}`;
|
|
33
|
+
const word = UNIT_WORD[m[2]];
|
|
34
|
+
return m[1] === "1" ? `the last ${word}` : `the last ${m[1]} ${word}s`;
|
|
35
|
+
}
|
|
36
|
+
/** The usage refusal for a `--since` value that did not parse. */
|
|
37
|
+
export function sinceUsageLines(raw) {
|
|
38
|
+
return [
|
|
39
|
+
raw === ""
|
|
40
|
+
? "--since needs a duration."
|
|
41
|
+
: `Invalid --since value: ${JSON.stringify(raw)}.`,
|
|
42
|
+
"Use a whole number and a unit: 30m, 2h or 7d.",
|
|
43
|
+
"Usage: vincentt dx [--since <duration>] [--json]",
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function parseWindow(args) {
|
|
47
|
+
const s = args.indexOf("--since");
|
|
48
|
+
if (s !== -1) {
|
|
49
|
+
const raw = args[s + 1];
|
|
50
|
+
const parsed = raw === undefined || raw.startsWith("-") ? undefined : parseDuration(raw);
|
|
51
|
+
return parsed !== undefined
|
|
52
|
+
? { window: parsed }
|
|
53
|
+
: { window: DEFAULT_WINDOW, sinceInvalid: raw ?? "" };
|
|
54
|
+
}
|
|
55
|
+
// `--days <n>` predates `--since` and stays accepted so scripts written
|
|
56
|
+
// against it keep their window.
|
|
57
|
+
const d = args.indexOf("--days");
|
|
58
|
+
const days = d !== -1 ? Number(args[d + 1]) : NaN;
|
|
59
|
+
return Number.isFinite(days) && days > 0
|
|
60
|
+
? { window: { ms: days * DAY_MS, label: `${days}d` } }
|
|
61
|
+
: { window: DEFAULT_WINDOW };
|
|
62
|
+
}
|
|
7
63
|
export function parseDxOptions(args) {
|
|
8
|
-
const i = args.indexOf("--days");
|
|
9
|
-
const raw = i !== -1 ? Number(args[i + 1]) : NaN;
|
|
10
64
|
const n = args.indexOf("--note");
|
|
11
65
|
const note = n !== -1 ? args[n + 1] : undefined;
|
|
12
66
|
// Positional, and read only from the slot right after the verb: `--note enable`
|
|
@@ -15,7 +69,7 @@ export function parseDxOptions(args) {
|
|
|
15
69
|
// already running, which is the shape this whole gate exists to refuse.
|
|
16
70
|
const sub = args[0] === "dx" ? args[1] : args[0];
|
|
17
71
|
return {
|
|
18
|
-
|
|
72
|
+
...parseWindow(args),
|
|
19
73
|
json: args.includes("--json"),
|
|
20
74
|
send: args.includes("--send"),
|
|
21
75
|
yes: args.includes("--yes"),
|
|
@@ -24,12 +78,8 @@ export function parseDxOptions(args) {
|
|
|
24
78
|
disable: sub === "disable",
|
|
25
79
|
};
|
|
26
80
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
return entries.filter((e) => {
|
|
30
|
-
const t = Date.parse(e.ts);
|
|
31
|
-
return Number.isFinite(t) && t >= cutoff;
|
|
32
|
-
});
|
|
81
|
+
function cutoff(options, now) {
|
|
82
|
+
return { sinceMs: now - options.window.ms };
|
|
33
83
|
}
|
|
34
84
|
function seconds(ms) {
|
|
35
85
|
if (ms < 1000)
|
|
@@ -41,8 +91,8 @@ function seconds(ms) {
|
|
|
41
91
|
}
|
|
42
92
|
function describe(c) {
|
|
43
93
|
const head = c.kind === "error"
|
|
44
|
-
? `${c.entries.length}× ${c.skeleton}
|
|
45
|
-
: `${c.entries.length}× ${c.skeleton}
|
|
94
|
+
? `${c.entries.length}× ${c.skeleton}: ${c.errors} failed${c.recovered ? ", then succeeded" : ", never recovered"}`
|
|
95
|
+
: `${c.entries.length}× ${c.skeleton}: all succeeded, within ${seconds(c.spanMs)}`;
|
|
46
96
|
const why = c.kind === "error"
|
|
47
97
|
? c.recovered
|
|
48
98
|
? " The caller got there in the end. What did the first failure not tell them?"
|
|
@@ -61,13 +111,14 @@ export function dxCommand(deps, options) {
|
|
|
61
111
|
deps.print("The DX journal is off (VINCENTT_DX_JOURNAL=0). Nothing is being recorded.");
|
|
62
112
|
return 0;
|
|
63
113
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
114
|
+
const entries = deps.entries(cutoff(options, deps.now()));
|
|
115
|
+
// Only an empty window pays for the second, unwindowed read, and it is what
|
|
116
|
+
// tells "nothing recorded yet" apart from "nothing recent".
|
|
117
|
+
if (entries.length === 0 && deps.entries().length === 0) {
|
|
118
|
+
deps.print("No journal yet. It fills as you use the CLI.");
|
|
67
119
|
deps.print(` ${deps.path()}`);
|
|
68
120
|
return 0;
|
|
69
121
|
}
|
|
70
|
-
const entries = withinDays(all, options.days, deps.now());
|
|
71
122
|
const clusters = rankClusters(findClusters(entries));
|
|
72
123
|
const enablement = deps.enablement?.() ?? { enabled: false };
|
|
73
124
|
if (options.json) {
|
|
@@ -75,7 +126,7 @@ export function dxCommand(deps, options) {
|
|
|
75
126
|
// AGENT reads, and an agent that can send unattended should be able to see
|
|
76
127
|
// that it can.
|
|
77
128
|
deps.print(JSON.stringify({
|
|
78
|
-
window:
|
|
129
|
+
window: options.window.label,
|
|
79
130
|
runs: entries.length,
|
|
80
131
|
clusters,
|
|
81
132
|
dxSendEnabled: enablement.enabled,
|
|
@@ -85,7 +136,7 @@ export function dxCommand(deps, options) {
|
|
|
85
136
|
}, null, 2));
|
|
86
137
|
return 0;
|
|
87
138
|
}
|
|
88
|
-
deps.print(`${entries.length} runs in
|
|
139
|
+
deps.print(`${entries.length} runs in ${describeWindow(options.window)}.`);
|
|
89
140
|
deps.print("");
|
|
90
141
|
if (clusters.length === 0) {
|
|
91
142
|
// A clean window is a real result. Manufacturing a finding to fill the
|
|
@@ -100,7 +151,7 @@ export function dxCommand(deps, options) {
|
|
|
100
151
|
deps.print(l);
|
|
101
152
|
deps.print("");
|
|
102
153
|
}
|
|
103
|
-
deps.print("If an agent ran these, ask it what it was trying to do
|
|
154
|
+
deps.print("If an agent ran these, ask it what it was trying to do; the journal");
|
|
104
155
|
deps.print("records the shape of a run, not the intent behind it.");
|
|
105
156
|
}
|
|
106
157
|
// Printed on every report, findings or not: where the file is and that it
|
|
@@ -119,7 +170,7 @@ export function dxCommand(deps, options) {
|
|
|
119
170
|
/** Wire the command to the real journal. */
|
|
120
171
|
export function runDx(args, print, enablement = () => ({ enabled: false })) {
|
|
121
172
|
return dxCommand({
|
|
122
|
-
entries: readJournal,
|
|
173
|
+
entries: (window) => readJournal(window),
|
|
123
174
|
print,
|
|
124
175
|
now: () => Date.now(),
|
|
125
176
|
enabled: () => journalEnabled(),
|
|
@@ -178,7 +229,7 @@ export async function dxEnableCommand(deps, sunset) {
|
|
|
178
229
|
deps.print("");
|
|
179
230
|
deps.print(" Run `vincentt login` first.");
|
|
180
231
|
deps.print("");
|
|
181
|
-
return
|
|
232
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
182
233
|
}
|
|
183
234
|
if (!deps.interactive()) {
|
|
184
235
|
return enableViaBrowser(deps);
|
|
@@ -333,12 +384,12 @@ export async function dxSendCommand(deps, options) {
|
|
|
333
384
|
deps.print("The DX journal is off (VINCENTT_DX_JOURNAL=0). There is nothing to send.");
|
|
334
385
|
return 0;
|
|
335
386
|
}
|
|
336
|
-
const entries =
|
|
387
|
+
const entries = deps.entries(cutoff(options, deps.now()));
|
|
337
388
|
const clusters = rankClusters(findClusters(entries));
|
|
338
389
|
if (clusters.length === 0) {
|
|
339
390
|
// Nothing to report is a result, not a failure. Sending an empty report
|
|
340
391
|
// would cost the creator a round trip to tell us nothing.
|
|
341
|
-
deps.print(`No friction detected in
|
|
392
|
+
deps.print(`No friction detected in ${describeWindow(options.window)}. Nothing to send.`);
|
|
342
393
|
return 0;
|
|
343
394
|
}
|
|
344
395
|
// SENDING requires either an interactive session or durable enablement — the
|
|
@@ -416,7 +467,7 @@ export async function dxSendCommand(deps, options) {
|
|
|
416
467
|
}
|
|
417
468
|
if (auth === undefined) {
|
|
418
469
|
deps.print("Not signed in. Run `vincentt login` to send a report.");
|
|
419
|
-
return
|
|
470
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
420
471
|
}
|
|
421
472
|
// RESOLVED AFTER THE GATE AND IMMEDIATELY BEFORE THE BUILD.
|
|
422
473
|
//
|
|
@@ -448,6 +499,10 @@ export async function dxSendCommand(deps, options) {
|
|
|
448
499
|
deps.print(`${line}. Not asking.`);
|
|
449
500
|
}
|
|
450
501
|
}
|
|
502
|
+
// The window is stated beside the payload rather than in it: the api refuses
|
|
503
|
+
// fields it does not know, so carrying it would be a contract change.
|
|
504
|
+
deps.print("");
|
|
505
|
+
deps.print(`Findings from ${describeWindow(options.window)}.`);
|
|
451
506
|
// renderPayload runs on EVERY path, including the unattended one. This is
|
|
452
507
|
// DISCLOSURE, NOT CONSENT: on the attended path it is what the creator answers
|
|
453
508
|
// to; unattended it is a forensic artifact that answers "what did we send"
|
|
@@ -465,7 +520,7 @@ export async function dxSendCommand(deps, options) {
|
|
|
465
520
|
try {
|
|
466
521
|
const res = await deps.post(auth.apiUrl, auth.pat, report);
|
|
467
522
|
deps.print("");
|
|
468
|
-
deps.print(`✓ Reported
|
|
523
|
+
deps.print(`✓ Reported: ${res.findings} ${res.findings === 1 ? "finding" : "findings"}, ref ${res.ref}`);
|
|
469
524
|
deps.print(` Kept until ${res.expiresAt}, then deleted.`);
|
|
470
525
|
deps.print(" Your journal stays on this machine.");
|
|
471
526
|
return 0;
|