@vincentt-xr/cli 0.6.0 → 0.8.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.d.ts +8 -1
- package/dist/cli/args.js +10 -1
- package/dist/cli/index.js +190 -5
- package/dist/dx/command.d.ts +91 -1
- package/dist/dx/command.js +213 -2
- package/dist/dx/enablement.d.ts +51 -0
- package/dist/dx/enablement.js +97 -0
- package/dist/dx/nudge.d.ts +7 -0
- package/dist/dx/nudge.js +54 -0
- package/dist/dx/qa.f7.fixtures.d.ts +73 -0
- package/dist/dx/qa.f7.fixtures.js +58 -0
- package/dist/dx/send.d.ts +70 -0
- package/dist/dx/send.js +108 -0
- package/dist/dx/skeleton.js +88 -5
- package/dist/lib/version.d.ts +20 -1
- package/dist/lib/version.js +41 -5
- package/dist/library/assetsCommand.d.ts +32 -0
- package/dist/library/assetsCommand.js +264 -0
- package/dist/library/client.d.ts +42 -0
- package/dist/library/client.js +137 -0
- package/dist/library/download.d.ts +46 -0
- package/dist/library/download.js +98 -0
- package/dist/library/parse.d.ts +43 -0
- package/dist/library/parse.js +109 -0
- package/dist/library/qa.f5-b32.spec.d.ts +1 -0
- package/dist/library/qa.f5-b32.spec.js +202 -0
- package/dist/library/qa.f5-cli.spec.d.ts +1 -0
- package/dist/library/qa.f5-cli.spec.js +433 -0
- package/dist/library/qa.f5-contract.spec.d.ts +1 -0
- package/dist/library/qa.f5-contract.spec.js +206 -0
- package/dist/library/render.d.ts +39 -0
- package/dist/library/render.js +305 -0
- package/dist/library/wire.d.ts +96 -0
- package/dist/library/wire.js +167 -0
- package/dist/publish/prompt.js +17 -2
- package/dist/shared/config.d.ts +27 -0
- package/dist/shared/config.js +64 -9
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The product-lifetime bound on an explicitly internal-beta capability. Past this
|
|
3
|
+
* date `enable` refuses AND an existing flag stops being honored, so FORGETTING
|
|
4
|
+
* THIS TURNS THE FEATURE OFF — the safe direction.
|
|
5
|
+
*
|
|
6
|
+
* This is deliberately a constant in shipped code rather than a CI test with a
|
|
7
|
+
* hard-coded date: such a test fires in an uninvolved engineer's lap at a moment
|
|
8
|
+
* with no context, and the cheapest green is bumping the constant. Reaching this
|
|
9
|
+
* date is a decision to make (extend, or remove the feature), not a number to
|
|
10
|
+
* raise.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DX_UNATTENDED_SUNSET: number;
|
|
13
|
+
export interface Enablement {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
/** The raw ISO8601 grant time. Present only when `enabled` is true. */
|
|
16
|
+
at?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Decide enablement from an already-parsed config object. Pure, and STRICT:
|
|
20
|
+
* identity against `true`, never truthiness.
|
|
21
|
+
*
|
|
22
|
+
* Truthiness fails OPEN on `"false"`, `"0"`, `{}`, `[]`, `1`, and `"garbage"` —
|
|
23
|
+
* every one of those is truthy in JavaScript. The worst is `"false"`, which is
|
|
24
|
+
* what a person hand-editing this file writes when they are trying to turn the
|
|
25
|
+
* feature OFF; `{}` is what a refactor grouping the two fields produces. A gate
|
|
26
|
+
* that reads a creator's "off" as "on" is inverted, not merely loose.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readEnablement(cfg: unknown, now: number): Enablement;
|
|
29
|
+
/**
|
|
30
|
+
* Read enablement from the real machine config.
|
|
31
|
+
*
|
|
32
|
+
* Wrapped in its OWN try/catch, independent of the auth read, because
|
|
33
|
+
* `readJsonIfExists` rethrows everything that is not ENOENT: a truncated
|
|
34
|
+
* config.json must DISABLE the relaxation, not throw through it and take an
|
|
35
|
+
* unrelated verb down with it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadEnablement(now: number): Promise<Enablement>;
|
|
38
|
+
/** Whole days elapsed, floored, never negative — a clock skew must not print `(-2 days)`. */
|
|
39
|
+
export declare function ageInDays(at: string, now: number): number;
|
|
40
|
+
/** The calendar day of the grant, in UTC. The time of day is noise at this age scale. */
|
|
41
|
+
export declare function grantedOn(at: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* The one visibility line, in the single pinned shape every human surface uses:
|
|
44
|
+
*
|
|
45
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days)
|
|
46
|
+
*
|
|
47
|
+
* Returns undefined when not enabled. Nothing is printed in that case, on
|
|
48
|
+
* purpose: a line that prints unconditionally is a line readers learn to skip,
|
|
49
|
+
* which would cost exactly the attention the enabled line needs.
|
|
50
|
+
*/
|
|
51
|
+
export declare function enablementLine(e: Enablement, now: number): string | undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable authorization for unattended `vincentt dx --send`.
|
|
3
|
+
*
|
|
4
|
+
* `--send` transmits only when the session is interactive OR this machine has
|
|
5
|
+
* been explicitly enabled by a person, at a terminal, via `vincentt dx enable`.
|
|
6
|
+
* The state is two fields in ~/.vincentt/config.json, set by `enable`, cleared by
|
|
7
|
+
* `disable` and by `logout`, and it has NO expiry — it stays on until the creator
|
|
8
|
+
* turns it off.
|
|
9
|
+
*
|
|
10
|
+
* Because there is no expiry, VISIBILITY is the entire mitigation, and the AGE is
|
|
11
|
+
* the load-bearing half of it: `ENABLED` alone reads as configuration, while
|
|
12
|
+
* `(145 days)` reads as drift and is the number that makes someone ask whether
|
|
13
|
+
* they still want it. That is why an unparseable timestamp fails CLOSED rather
|
|
14
|
+
* than printing `since Invalid Date (NaN days)` — a degraded mitigation is worse
|
|
15
|
+
* than a visible absence.
|
|
16
|
+
*/
|
|
17
|
+
import { loadMachineConfig } from "../shared/config.js";
|
|
18
|
+
/**
|
|
19
|
+
* The product-lifetime bound on an explicitly internal-beta capability. Past this
|
|
20
|
+
* date `enable` refuses AND an existing flag stops being honored, so FORGETTING
|
|
21
|
+
* THIS TURNS THE FEATURE OFF — the safe direction.
|
|
22
|
+
*
|
|
23
|
+
* This is deliberately a constant in shipped code rather than a CI test with a
|
|
24
|
+
* hard-coded date: such a test fires in an uninvolved engineer's lap at a moment
|
|
25
|
+
* with no context, and the cheapest green is bumping the constant. Reaching this
|
|
26
|
+
* date is a decision to make (extend, or remove the feature), not a number to
|
|
27
|
+
* raise.
|
|
28
|
+
*/
|
|
29
|
+
export const DX_UNATTENDED_SUNSET = Date.parse("2027-02-24T00:00:00.000Z");
|
|
30
|
+
const DISABLED = { enabled: false };
|
|
31
|
+
/**
|
|
32
|
+
* Decide enablement from an already-parsed config object. Pure, and STRICT:
|
|
33
|
+
* identity against `true`, never truthiness.
|
|
34
|
+
*
|
|
35
|
+
* Truthiness fails OPEN on `"false"`, `"0"`, `{}`, `[]`, `1`, and `"garbage"` —
|
|
36
|
+
* every one of those is truthy in JavaScript. The worst is `"false"`, which is
|
|
37
|
+
* what a person hand-editing this file writes when they are trying to turn the
|
|
38
|
+
* feature OFF; `{}` is what a refactor grouping the two fields produces. A gate
|
|
39
|
+
* that reads a creator's "off" as "on" is inverted, not merely loose.
|
|
40
|
+
*/
|
|
41
|
+
export function readEnablement(cfg, now) {
|
|
42
|
+
if (typeof cfg !== "object" || cfg === null)
|
|
43
|
+
return DISABLED;
|
|
44
|
+
const { dxSendEnabled, dxSendEnabledAt } = cfg;
|
|
45
|
+
if (dxSendEnabled !== true)
|
|
46
|
+
return DISABLED;
|
|
47
|
+
if (typeof dxSendEnabledAt !== "string")
|
|
48
|
+
return DISABLED;
|
|
49
|
+
if (!Number.isFinite(Date.parse(dxSendEnabledAt)))
|
|
50
|
+
return DISABLED;
|
|
51
|
+
if (now >= DX_UNATTENDED_SUNSET)
|
|
52
|
+
return DISABLED;
|
|
53
|
+
return { enabled: true, at: dxSendEnabledAt };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read enablement from the real machine config.
|
|
57
|
+
*
|
|
58
|
+
* Wrapped in its OWN try/catch, independent of the auth read, because
|
|
59
|
+
* `readJsonIfExists` rethrows everything that is not ENOENT: a truncated
|
|
60
|
+
* config.json must DISABLE the relaxation, not throw through it and take an
|
|
61
|
+
* unrelated verb down with it.
|
|
62
|
+
*/
|
|
63
|
+
export async function loadEnablement(now) {
|
|
64
|
+
try {
|
|
65
|
+
return readEnablement(await loadMachineConfig(), now);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return DISABLED;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
72
|
+
/** Whole days elapsed, floored, never negative — a clock skew must not print `(-2 days)`. */
|
|
73
|
+
export function ageInDays(at, now) {
|
|
74
|
+
const t = Date.parse(at);
|
|
75
|
+
if (!Number.isFinite(t))
|
|
76
|
+
return 0;
|
|
77
|
+
return Math.max(0, Math.floor((now - t) / DAY_MS));
|
|
78
|
+
}
|
|
79
|
+
/** The calendar day of the grant, in UTC. The time of day is noise at this age scale. */
|
|
80
|
+
export function grantedOn(at) {
|
|
81
|
+
return new Date(Date.parse(at)).toISOString().slice(0, 10);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The one visibility line, in the single pinned shape every human surface uses:
|
|
85
|
+
*
|
|
86
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days)
|
|
87
|
+
*
|
|
88
|
+
* Returns undefined when not enabled. Nothing is printed in that case, on
|
|
89
|
+
* purpose: a line that prints unconditionally is a line readers learn to skip,
|
|
90
|
+
* which would cost exactly the attention the enabled line needs.
|
|
91
|
+
*/
|
|
92
|
+
export function enablementLine(e, now) {
|
|
93
|
+
if (!e.enabled || e.at === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
const days = ageInDays(e.at, now);
|
|
96
|
+
return `Unattended send: ENABLED since ${grantedOn(e.at)} (${days} ${days === 1 ? "day" : "days"})`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JournalEntry } from "./journal.js";
|
|
2
|
+
/**
|
|
3
|
+
* The nudge fires only on a cluster the CURRENT run just completed, so it
|
|
4
|
+
* appears at the moment of friction rather than as a standing complaint about
|
|
5
|
+
* something that happened this morning.
|
|
6
|
+
*/
|
|
7
|
+
export declare function nudgeFor(entries: JournalEntry[], now: number): string[];
|
package/dist/dx/nudge.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nudge: a one-line hint, printed after a command that just completed a
|
|
3
|
+
* friction cluster, telling the creator the report exists.
|
|
4
|
+
*
|
|
5
|
+
* It is here because a command nobody remembers produces no data. A creator
|
|
6
|
+
* building an AR app is not going to think "I should audit my tooling" — they
|
|
7
|
+
* are going to swear at the fourth preview and keep going. The moment the
|
|
8
|
+
* friction happens is the only moment the report is cheap to ask for.
|
|
9
|
+
*
|
|
10
|
+
* It is a HINT and never an action. Nothing is sent, nothing is queued, and the
|
|
11
|
+
* creator still has to type the command — which is what keeps `--send` an
|
|
12
|
+
* explicit act rather than a dialog they clicked through while annoyed.
|
|
13
|
+
*/
|
|
14
|
+
import { findClusters } from "./clusters.js";
|
|
15
|
+
/** How far back a nudge looks. A cluster older than this is not "right now". */
|
|
16
|
+
const WINDOW_MS = 15 * 60 * 1000;
|
|
17
|
+
/**
|
|
18
|
+
* The nudge fires only on a cluster the CURRENT run just completed, so it
|
|
19
|
+
* appears at the moment of friction rather than as a standing complaint about
|
|
20
|
+
* something that happened this morning.
|
|
21
|
+
*/
|
|
22
|
+
export function nudgeFor(entries, now) {
|
|
23
|
+
if (entries.length === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const recent = entries.filter((e) => {
|
|
26
|
+
const t = Date.parse(e.ts);
|
|
27
|
+
return Number.isFinite(t) && now - t <= WINDOW_MS;
|
|
28
|
+
});
|
|
29
|
+
if (recent.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const last = recent[recent.length - 1];
|
|
32
|
+
const clusters = findClusters(recent);
|
|
33
|
+
// Only a cluster the LAST entry belongs to. A cluster the caller already
|
|
34
|
+
// worked past is not friction they are feeling now, and nagging about it is
|
|
35
|
+
// how a hint becomes noise a reader learns to skip.
|
|
36
|
+
const live = clusters.find((c) => c.skeleton === last.skel && c.entries[c.entries.length - 1].ts === last.ts);
|
|
37
|
+
if (live === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
const what = live.kind === "repetition"
|
|
40
|
+
? `${live.entries.length} runs of \`${live.skeleton}\` in a few minutes.`
|
|
41
|
+
: `\`${live.skeleton}\` failed ${live.errors}× just now.`;
|
|
42
|
+
// STATES A FACT, NEVER AN INSTRUCTION.
|
|
43
|
+
//
|
|
44
|
+
// This line lands in an agent's transcript, because in the agent-driven loop
|
|
45
|
+
// the CLI's stdout IS the agent's input. "Run `vincentt dx --send` to report
|
|
46
|
+
// this" is a directive an autonomous caller will simply obey — and a report
|
|
47
|
+
// an agent decided to send is automation wearing consent's clothes, not the
|
|
48
|
+
// opt-in this feature is built around. Naming that a command exists is
|
|
49
|
+
// information; telling a caller to transmit is not.
|
|
50
|
+
//
|
|
51
|
+
// Note it names `vincentt dx` (which only READS, locally) and never the
|
|
52
|
+
// sending form.
|
|
53
|
+
return ["", ` ! ${what}`, " `vincentt dx` shows friction from your recent runs."];
|
|
54
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA fixtures for the dx-agent-consent delta (f7).
|
|
3
|
+
*
|
|
4
|
+
* Declared in the change's qa.md `## Fixtures` section. All three are
|
|
5
|
+
* machine-config state, which f7 has never had a fixture for — its existing
|
|
6
|
+
* fixtures (F7_JOURNAL_FRICTION, F7_REPORT_EXPIRED, …) are journal-side or
|
|
7
|
+
* server-side.
|
|
8
|
+
*
|
|
9
|
+
* F7_CONFIG_SIGNED_IN is written by driving the REAL writeMachineConfig(), never
|
|
10
|
+
* by hand-writing JSON. A hand-written config asserts against a shape the CLI
|
|
11
|
+
* never produces — the f1 orgs/memberships failure at file scope, where a mock
|
|
12
|
+
* of state the API could actually produce drifted from the API and stayed green.
|
|
13
|
+
*
|
|
14
|
+
* These are QA-owned (qa.* prefix) and deliberately separate from dev's test
|
|
15
|
+
* files in the same package.
|
|
16
|
+
*/
|
|
17
|
+
import type { JournalEntry } from "./journal.js";
|
|
18
|
+
/** The honest baseline config, as the CLI itself writes it. */
|
|
19
|
+
export declare const F7_CONFIG_SIGNED_IN: {
|
|
20
|
+
readonly apiUrl: "https://api.test";
|
|
21
|
+
readonly consoleUrl: "https://console.test";
|
|
22
|
+
readonly pat: "vct_alice";
|
|
23
|
+
readonly activeOrg: "alice";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The instant every fixture is dated against. 145 days after the grant in
|
|
27
|
+
* F7_CONFIG_ENABLED, and before DX_UNATTENDED_SUNSET.
|
|
28
|
+
*/
|
|
29
|
+
export declare const F7_NOW: number;
|
|
30
|
+
/** 145 days before F7_NOW. The AGE is the point — see qa.md on F7_CONFIG_ENABLED. */
|
|
31
|
+
export declare const F7_ENABLED_AT = "2026-04-01T00:00:00.000Z";
|
|
32
|
+
/**
|
|
33
|
+
* The single pinned human-facing shape. Every human surface must render this
|
|
34
|
+
* byte-for-byte; the age is the load-bearing half, so it is pinned as a literal
|
|
35
|
+
* rather than computed (a computed expectation would follow a broken
|
|
36
|
+
* implementation wherever it went).
|
|
37
|
+
*/
|
|
38
|
+
export declare const F7_PINNED_ENABLEMENT_LINE = "Unattended send: ENABLED since 2026-04-01 (145 days)";
|
|
39
|
+
/** F7_CONFIG_SIGNED_IN + durable enablement, aged. */
|
|
40
|
+
export declare const F7_CONFIG_ENABLED: {
|
|
41
|
+
readonly dxSendEnabled: true;
|
|
42
|
+
readonly dxSendEnabledAt: "2026-04-01T00:00:00.000Z";
|
|
43
|
+
readonly apiUrl: "https://api.test";
|
|
44
|
+
readonly consoleUrl: "https://console.test";
|
|
45
|
+
readonly pat: "vct_alice";
|
|
46
|
+
readonly activeOrg: "alice";
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Every shape the enablement read must treat as DISABLED.
|
|
50
|
+
*
|
|
51
|
+
* Rows B, C, E, F, G are TRUTHY IN JAVASCRIPT — `if (cfg.dxSendEnabled)` passes
|
|
52
|
+
* every one of them. That is the entire reason this fixture exists, and why the
|
|
53
|
+
* read is specified as `=== true`. The worst is B ("false"): what a person
|
|
54
|
+
* hand-editing this file writes when trying to turn the feature OFF.
|
|
55
|
+
*
|
|
56
|
+
* `raw` rows (K, L) are written as bytes rather than as an object, because the
|
|
57
|
+
* property under test is that an UNPARSEABLE config fails closed instead of
|
|
58
|
+
* throwing through into an unrelated verb.
|
|
59
|
+
*/
|
|
60
|
+
export interface MalformedRow {
|
|
61
|
+
id: string;
|
|
62
|
+
why: string;
|
|
63
|
+
/** A config object to serialize, or undefined when `raw` carries the bytes. */
|
|
64
|
+
patch?: Record<string, unknown>;
|
|
65
|
+
/** Literal file bytes, for the rows that are not valid JSON at all. */
|
|
66
|
+
raw?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare const F7_CONFIG_MALFORMED_ENABLEMENT: MalformedRow[];
|
|
69
|
+
/**
|
|
70
|
+
* F7_JOURNAL_FRICTION, as the record declares it: three runs of the same verb in
|
|
71
|
+
* a few minutes, which is a repetition cluster.
|
|
72
|
+
*/
|
|
73
|
+
export declare function f7JournalFriction(now?: number): JournalEntry[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The honest baseline config, as the CLI itself writes it. */
|
|
2
|
+
export const F7_CONFIG_SIGNED_IN = {
|
|
3
|
+
apiUrl: "https://api.test",
|
|
4
|
+
consoleUrl: "https://console.test",
|
|
5
|
+
pat: "vct_alice",
|
|
6
|
+
activeOrg: "alice",
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The instant every fixture is dated against. 145 days after the grant in
|
|
10
|
+
* F7_CONFIG_ENABLED, and before DX_UNATTENDED_SUNSET.
|
|
11
|
+
*/
|
|
12
|
+
export const F7_NOW = Date.parse("2026-08-24T12:00:00.000Z");
|
|
13
|
+
/** 145 days before F7_NOW. The AGE is the point — see qa.md on F7_CONFIG_ENABLED. */
|
|
14
|
+
export const F7_ENABLED_AT = "2026-04-01T00:00:00.000Z";
|
|
15
|
+
/**
|
|
16
|
+
* The single pinned human-facing shape. Every human surface must render this
|
|
17
|
+
* byte-for-byte; the age is the load-bearing half, so it is pinned as a literal
|
|
18
|
+
* rather than computed (a computed expectation would follow a broken
|
|
19
|
+
* implementation wherever it went).
|
|
20
|
+
*/
|
|
21
|
+
export const F7_PINNED_ENABLEMENT_LINE = "Unattended send: ENABLED since 2026-04-01 (145 days)";
|
|
22
|
+
/** F7_CONFIG_SIGNED_IN + durable enablement, aged. */
|
|
23
|
+
export const F7_CONFIG_ENABLED = {
|
|
24
|
+
...F7_CONFIG_SIGNED_IN,
|
|
25
|
+
dxSendEnabled: true,
|
|
26
|
+
dxSendEnabledAt: F7_ENABLED_AT,
|
|
27
|
+
};
|
|
28
|
+
export const F7_CONFIG_MALFORMED_ENABLEMENT = [
|
|
29
|
+
{ id: "A", why: 'truthy STRING "true"', patch: { dxSendEnabled: "true", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
30
|
+
{ id: "B", why: 'truthy in JS, means false to a human', patch: { dxSendEnabled: "false", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
31
|
+
{ id: "C", why: 'truthy in JS ("0")', patch: { dxSendEnabled: "0", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
32
|
+
{ id: "D", why: "truthy, not the boolean (1)", patch: { dxSendEnabled: 1, dxSendEnabledAt: F7_ENABLED_AT } },
|
|
33
|
+
{ id: "E", why: "truthy ({} — what a refactor grouping the fields produces)", patch: { dxSendEnabled: {}, dxSendEnabledAt: F7_ENABLED_AT } },
|
|
34
|
+
{ id: "F", why: "truthy ([])", patch: { dxSendEnabled: [], dxSendEnabledAt: F7_ENABLED_AT } },
|
|
35
|
+
{ id: "G", why: 'truthy ("garbage")', patch: { dxSendEnabled: "garbage", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
36
|
+
{ id: "H", why: "key absent entirely", patch: {} },
|
|
37
|
+
{ id: "I", why: "enabled true, dxSendEnabledAt absent", patch: { dxSendEnabled: true } },
|
|
38
|
+
{ id: "J", why: 'enabled true, dxSendEnabledAt "not-a-date"', patch: { dxSendEnabled: true, dxSendEnabledAt: "not-a-date" } },
|
|
39
|
+
{ id: "K", why: "file truncated mid-JSON", raw: '{"pat":"vct_alice","dxSendEn' },
|
|
40
|
+
{ id: "L", why: "file empty (0 bytes)", raw: "" },
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* F7_JOURNAL_FRICTION, as the record declares it: three runs of the same verb in
|
|
44
|
+
* a few minutes, which is a repetition cluster.
|
|
45
|
+
*/
|
|
46
|
+
export function f7JournalFriction(now = F7_NOW) {
|
|
47
|
+
const entry = (minutesAgo) => ({
|
|
48
|
+
ts: new Date(now - minutesAgo * 60_000).toISOString(),
|
|
49
|
+
v: "0.7.0",
|
|
50
|
+
verb: "preview",
|
|
51
|
+
skel: "preview",
|
|
52
|
+
exit: 0,
|
|
53
|
+
ms: 800,
|
|
54
|
+
err: null,
|
|
55
|
+
wt: "abc123",
|
|
56
|
+
});
|
|
57
|
+
return [entry(3), entry(2), entry(1)];
|
|
58
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vincentt dx --send` — the one place anything from the journal leaves the
|
|
3
|
+
* machine.
|
|
4
|
+
*
|
|
5
|
+
* The journal itself is local and unconditional. Sending is neither: it happens
|
|
6
|
+
* only when a creator types this flag, and only after they have been shown the
|
|
7
|
+
* exact payload. That ordering is the whole consent model — a report is an act,
|
|
8
|
+
* not a background transmission — so nothing in this file may be called from a
|
|
9
|
+
* path the creator did not ask for.
|
|
10
|
+
*
|
|
11
|
+
* What goes: the SHAPE of the friction (skeleton, counts, timing, error class
|
|
12
|
+
* names) plus the CLI version and platform. What never goes: the raw journal,
|
|
13
|
+
* flag values, paths, error messages, or anything about which project this was.
|
|
14
|
+
* The server re-validates all of it — a client that leaked a value cannot
|
|
15
|
+
* persist it by asking — but the boundary is drawn here first, because a payload
|
|
16
|
+
* that needs server-side scrubbing was built wrong.
|
|
17
|
+
*/
|
|
18
|
+
import type { Cluster } from "./clusters.js";
|
|
19
|
+
/** A finding as it goes on the wire. Mirrors the API's dxFindingRequest. */
|
|
20
|
+
export interface WireFinding {
|
|
21
|
+
kind: "error" | "repetition";
|
|
22
|
+
skeleton: string;
|
|
23
|
+
calls: number;
|
|
24
|
+
errors: number;
|
|
25
|
+
spanMs: number;
|
|
26
|
+
errorClasses?: string[];
|
|
27
|
+
note?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface WireReport {
|
|
30
|
+
cliVersion: string;
|
|
31
|
+
platform: string;
|
|
32
|
+
findings: WireFinding[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The server bounds a report at 50 findings and refuses the whole thing past
|
|
36
|
+
* that, so the worst clusters are sent rather than the request rejected. They
|
|
37
|
+
* arrive ranked, so this is a head, not a sample.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MAX_SENT_FINDINGS = 50;
|
|
40
|
+
export declare function platformTag(): string;
|
|
41
|
+
/**
|
|
42
|
+
* Build the wire payload from ranked clusters.
|
|
43
|
+
*
|
|
44
|
+
* Every field here is derived from the journal's own skeletonized entries — this
|
|
45
|
+
* function reads counts and timings, never values. If a future journal field
|
|
46
|
+
* carries content, it must not be added to this mapping without the same
|
|
47
|
+
* scrutiny the skeleton got.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildReport(clusters: Cluster[], cliVersion: string, note?: string): WireReport;
|
|
50
|
+
/**
|
|
51
|
+
* Render the payload for the creator to read BEFORE it is sent.
|
|
52
|
+
*
|
|
53
|
+
* This is not a summary of the payload — it is the payload, formatted. A preview
|
|
54
|
+
* that paraphrased what would be sent would be exactly the wrong thing to show
|
|
55
|
+
* someone deciding whether to send it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function renderPayload(report: WireReport): string[];
|
|
58
|
+
export interface SendResult {
|
|
59
|
+
ref: string;
|
|
60
|
+
findings: number;
|
|
61
|
+
expiresAt: string;
|
|
62
|
+
}
|
|
63
|
+
export declare class DXSendError extends Error {
|
|
64
|
+
readonly status?: number | undefined;
|
|
65
|
+
constructor(message: string, status?: number | undefined);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* POST the report. The caller has already shown the payload and had it accepted.
|
|
69
|
+
*/
|
|
70
|
+
export declare function postReport(apiUrl: string, pat: string, report: WireReport, fetchImpl?: typeof fetch): Promise<SendResult>;
|
package/dist/dx/send.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server bounds a report at 50 findings and refuses the whole thing past
|
|
3
|
+
* that, so the worst clusters are sent rather than the request rejected. They
|
|
4
|
+
* arrive ranked, so this is a head, not a sample.
|
|
5
|
+
*/
|
|
6
|
+
export const MAX_SENT_FINDINGS = 50;
|
|
7
|
+
/** Error classes per finding, matching the server's bound. */
|
|
8
|
+
const MAX_ERROR_CLASSES = 10;
|
|
9
|
+
export function platformTag() {
|
|
10
|
+
return `${process.platform}-${process.arch}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build the wire payload from ranked clusters.
|
|
14
|
+
*
|
|
15
|
+
* Every field here is derived from the journal's own skeletonized entries — this
|
|
16
|
+
* function reads counts and timings, never values. If a future journal field
|
|
17
|
+
* carries content, it must not be added to this mapping without the same
|
|
18
|
+
* scrutiny the skeleton got.
|
|
19
|
+
*/
|
|
20
|
+
export function buildReport(clusters, cliVersion, note) {
|
|
21
|
+
const findings = clusters.slice(0, MAX_SENT_FINDINGS).map((c) => {
|
|
22
|
+
const classes = [
|
|
23
|
+
...new Set(c.entries.map((e) => e.err).filter((e) => e !== null)),
|
|
24
|
+
].slice(0, MAX_ERROR_CLASSES);
|
|
25
|
+
const f = {
|
|
26
|
+
kind: c.kind,
|
|
27
|
+
skeleton: c.skeleton,
|
|
28
|
+
calls: c.entries.length,
|
|
29
|
+
errors: c.errors,
|
|
30
|
+
spanMs: Math.max(0, Math.round(c.spanMs)),
|
|
31
|
+
};
|
|
32
|
+
if (classes.length > 0)
|
|
33
|
+
f.errorClasses = classes;
|
|
34
|
+
return f;
|
|
35
|
+
});
|
|
36
|
+
// The note rides on the first finding: it is the creator's own words about the
|
|
37
|
+
// friction they are reporting, and the report is about one sitting.
|
|
38
|
+
if (note !== undefined && note.trim() !== "" && findings.length > 0) {
|
|
39
|
+
findings[0].note = note.trim();
|
|
40
|
+
}
|
|
41
|
+
return { cliVersion, platform: platformTag(), findings };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render the payload for the creator to read BEFORE it is sent.
|
|
45
|
+
*
|
|
46
|
+
* This is not a summary of the payload — it is the payload, formatted. A preview
|
|
47
|
+
* that paraphrased what would be sent would be exactly the wrong thing to show
|
|
48
|
+
* someone deciding whether to send it.
|
|
49
|
+
*/
|
|
50
|
+
export function renderPayload(report) {
|
|
51
|
+
const hasNote = report.findings.some((f) => f.note !== undefined && f.note !== "");
|
|
52
|
+
const lines = [
|
|
53
|
+
"This is what would be sent:",
|
|
54
|
+
"",
|
|
55
|
+
...JSON.stringify(report, null, 2)
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((l) => ` ${l}`),
|
|
58
|
+
"",
|
|
59
|
+
];
|
|
60
|
+
// The generated fields carry shape only. The NOTE does not, and saying
|
|
61
|
+
// otherwise above a note that may hold a path or a token would be asking the
|
|
62
|
+
// creator to consent on a false claim — the one thing this screen exists to
|
|
63
|
+
// prevent. So the sentence changes when a note is present.
|
|
64
|
+
if (hasNote) {
|
|
65
|
+
lines.push(" The generated fields carry no paths, flag values, or error messages.", " YOUR NOTE IS SENT EXACTLY AS WRITTEN — check it for paths, URLs,", " tokens, or customer names before sending.");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push(" No paths, no flag values, no error messages, and nothing about which", " project this was.");
|
|
69
|
+
}
|
|
70
|
+
lines.push(" Your journal stays on this machine.");
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
export class DXSendError extends Error {
|
|
74
|
+
status;
|
|
75
|
+
constructor(message, status) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.status = status;
|
|
78
|
+
this.name = "DXSendError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* POST the report. The caller has already shown the payload and had it accepted.
|
|
83
|
+
*/
|
|
84
|
+
export async function postReport(apiUrl, pat, report, fetchImpl = fetch) {
|
|
85
|
+
const res = await fetchImpl(`${apiUrl}/dx-reports`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${pat}` },
|
|
88
|
+
body: JSON.stringify(report),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
let detail = "";
|
|
92
|
+
try {
|
|
93
|
+
const body = (await res.json());
|
|
94
|
+
detail = body.message ?? "";
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A non-JSON body is not more informative than the status.
|
|
98
|
+
}
|
|
99
|
+
if (res.status === 401) {
|
|
100
|
+
throw new DXSendError("Your access token is no longer valid — run `vincentt login`.", 401);
|
|
101
|
+
}
|
|
102
|
+
// A 400 here means the payload this CLI built failed the server's own
|
|
103
|
+
// grammar. Surfaced verbatim rather than softened: it is a bug in this CLI,
|
|
104
|
+
// and the creator is the one who can report it.
|
|
105
|
+
throw new DXSendError(detail !== "" ? detail : `The report was not accepted (${res.status}).`, res.status);
|
|
106
|
+
}
|
|
107
|
+
return (await res.json());
|
|
108
|
+
}
|
package/dist/dx/skeleton.js
CHANGED
|
@@ -10,6 +10,83 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { BOOLEAN_FLAGS } from "../cli/args.js";
|
|
12
12
|
const ELIDED = "<v>";
|
|
13
|
+
/**
|
|
14
|
+
* The verbs this CLI dispatches. A first bare token that is not one of these is
|
|
15
|
+
* not a verb — it is whatever the caller typed, and it must not be kept.
|
|
16
|
+
*
|
|
17
|
+
* Keeping it verbatim was a leak: an agent that mistypes
|
|
18
|
+
* `vincentt publish-acme-demo-to-prod`, fails, and retries builds a top-ranked
|
|
19
|
+
* friction cluster, so a customer's name ended up at the head of both the
|
|
20
|
+
* on-disk journal and any report built from it. Lowercase kebab-case is exactly
|
|
21
|
+
* what slugs and project names are made of, so no character-class check catches
|
|
22
|
+
* this — only a list of the verbs that actually exist.
|
|
23
|
+
*/
|
|
24
|
+
const KNOWN_VERBS = new Set([
|
|
25
|
+
"create",
|
|
26
|
+
"dx",
|
|
27
|
+
"feedback",
|
|
28
|
+
"init",
|
|
29
|
+
"link",
|
|
30
|
+
"login",
|
|
31
|
+
"logout",
|
|
32
|
+
"logs",
|
|
33
|
+
"network",
|
|
34
|
+
"preview",
|
|
35
|
+
"publish",
|
|
36
|
+
"relay",
|
|
37
|
+
"trace",
|
|
38
|
+
"unlink",
|
|
39
|
+
"version",
|
|
40
|
+
"whoami",
|
|
41
|
+
"help",
|
|
42
|
+
]);
|
|
43
|
+
/** Flags the CLI defines. An unknown `--flag` name is caller text, not signal. */
|
|
44
|
+
const KNOWN_FLAGS = new Set([
|
|
45
|
+
"--allow-custom-api",
|
|
46
|
+
"--api",
|
|
47
|
+
"--console",
|
|
48
|
+
"--days",
|
|
49
|
+
"--dir",
|
|
50
|
+
"--dist",
|
|
51
|
+
"--editor",
|
|
52
|
+
"--errors",
|
|
53
|
+
"--force",
|
|
54
|
+
"--help",
|
|
55
|
+
"--json",
|
|
56
|
+
"--limit",
|
|
57
|
+
"--name",
|
|
58
|
+
"--no-install",
|
|
59
|
+
"--no-qr",
|
|
60
|
+
"--note",
|
|
61
|
+
"--org",
|
|
62
|
+
"--port",
|
|
63
|
+
"--preview",
|
|
64
|
+
"--restart",
|
|
65
|
+
"--send",
|
|
66
|
+
"--session",
|
|
67
|
+
"--since",
|
|
68
|
+
"--slug",
|
|
69
|
+
"--stop",
|
|
70
|
+
"--template",
|
|
71
|
+
"--tester",
|
|
72
|
+
"--timeout",
|
|
73
|
+
"--verbose",
|
|
74
|
+
"--version",
|
|
75
|
+
"--viewer",
|
|
76
|
+
"--wait",
|
|
77
|
+
"--watch",
|
|
78
|
+
"--yes",
|
|
79
|
+
]);
|
|
80
|
+
/**
|
|
81
|
+
* Placeholders for tokens the CLI does not recognize. Angle brackets cannot
|
|
82
|
+
* appear in a token the caller typed, so a caller cannot forge one.
|
|
83
|
+
*
|
|
84
|
+
* A mistyped verb still produces a finding — it clusters under `<verb>`, which
|
|
85
|
+
* is itself worth seeing (repeated unknown verbs mean an agent is guessing at a
|
|
86
|
+
* command that should exist).
|
|
87
|
+
*/
|
|
88
|
+
const UNKNOWN_VERB = "<verb>";
|
|
89
|
+
const UNKNOWN_FLAG = "--<flag>";
|
|
13
90
|
/**
|
|
14
91
|
* Normalize an argv into `verb --flag=<v> <v>` form — the key a retry cluster
|
|
15
92
|
* groups on. Two calls that differ only in their values share a skeleton, which
|
|
@@ -20,12 +97,18 @@ export function skeleton(args) {
|
|
|
20
97
|
for (let i = 0; i < args.length; i++) {
|
|
21
98
|
const a = args[i];
|
|
22
99
|
if (!a.startsWith("-")) {
|
|
23
|
-
// The
|
|
24
|
-
// positional is a value (a slug, an id, a path)
|
|
25
|
-
|
|
100
|
+
// The first bare token is the verb — kept only when it is one we actually
|
|
101
|
+
// dispatch. Every later positional is a value (a slug, an id, a path).
|
|
102
|
+
if (out.length === 0) {
|
|
103
|
+
out.push(KNOWN_VERBS.has(a) ? a : UNKNOWN_VERB);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
out.push(ELIDED);
|
|
107
|
+
}
|
|
26
108
|
continue;
|
|
27
109
|
}
|
|
28
|
-
const
|
|
110
|
+
const rawName = a.split("=")[0];
|
|
111
|
+
const name = KNOWN_FLAGS.has(rawName) ? rawName : UNKNOWN_FLAG;
|
|
29
112
|
if (BOOLEAN_FLAGS.has(a)) {
|
|
30
113
|
out.push(name);
|
|
31
114
|
continue;
|
|
@@ -47,7 +130,7 @@ export function verbOf(args) {
|
|
|
47
130
|
for (let i = 0; i < args.length; i++) {
|
|
48
131
|
const a = args[i];
|
|
49
132
|
if (!a.startsWith("-"))
|
|
50
|
-
return a;
|
|
133
|
+
return KNOWN_VERBS.has(a) ? a : UNKNOWN_VERB;
|
|
51
134
|
if (!BOOLEAN_FLAGS.has(a) && !a.includes("="))
|
|
52
135
|
i++;
|
|
53
136
|
}
|