@vincentt-xr/cli 0.7.0 → 0.9.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 +18 -1
- package/dist/cli/args.js +40 -1
- package/dist/cli/index.js +208 -10
- package/dist/dx/command.d.ts +59 -2
- package/dist/dx/command.js +150 -11
- package/dist/dx/enablement.d.ts +51 -0
- package/dist/dx/enablement.js +97 -0
- package/dist/dx/qa.f7.fixtures.d.ts +73 -0
- package/dist/dx/qa.f7.fixtures.js +58 -0
- package/dist/dx/skeleton.js +1 -0
- package/dist/lib/initCommand.d.ts +28 -1
- package/dist/lib/initCommand.js +91 -16
- package/dist/lib/initFrom.d.ts +48 -0
- package/dist/lib/initFrom.js +198 -0
- package/dist/lib/sourceRef.d.ts +46 -0
- package/dist/lib/sourceRef.js +107 -0
- package/dist/lib/sourceRefCopy.d.ts +11 -0
- package/dist/lib/sourceRefCopy.js +80 -0
- 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/api.d.ts +20 -0
- package/dist/publish/command.d.ts +3 -0
- package/dist/publish/command.js +39 -4
- package/dist/publish/copy.d.ts +29 -0
- package/dist/publish/copy.js +86 -0
- package/dist/publish/prompt.js +17 -2
- package/dist/scaffold/index.d.ts +9 -0
- package/dist/scaffold/index.js +13 -1
- package/dist/shared/config.d.ts +27 -0
- package/dist/shared/config.js +64 -9
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AssetRecord, Gap, SearchResponse } from "./wire.js";
|
|
2
|
+
/** `prop · 1:1 · alpha` — the placement contract, from facts the bytes cannot get wrong. */
|
|
3
|
+
export declare function contractLine(a: AssetRecord): string;
|
|
4
|
+
/** `pumpkin · prop · 1:1 · 1024x1024 · alpha` — `add`'s confirmation line (byte facts). */
|
|
5
|
+
export declare function byteFactsLine(a: AssetRecord): string;
|
|
6
|
+
/** Human byte size, e.g. `180 KB`. Absent bytes render as an absent size. */
|
|
7
|
+
export declare function humanBytes(bytes: number | undefined): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* One search result, in the five-line layout the design fixes:
|
|
10
|
+
* 1 id + the placement contract (byte facts — decides usability)
|
|
11
|
+
* 2 subject — description (the noun leads; the agent matched on a noun)
|
|
12
|
+
* 3 usage_hint, UNLABELLED (it is an instruction and reads as one)
|
|
13
|
+
* 4 style · render then palette (the NEXT query's vocabulary)
|
|
14
|
+
* 5 the URL, whole and alone (the payload)
|
|
15
|
+
* Any line whose fields are absent is simply not emitted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function searchRecordLines(a: AssetRecord): string[];
|
|
18
|
+
/** The `Narrow it:` footer — a vocabulary with real example values, not a usage string. */
|
|
19
|
+
export declare function narrowFooter(results: AssetRecord[]): string;
|
|
20
|
+
/** The zero-result screen. Exit 0, and DELIBERATELY no glyph. */
|
|
21
|
+
export declare function zeroResultLines(query: string, gaps: Gap[], revision?: string): string[];
|
|
22
|
+
/** The full human search screen. */
|
|
23
|
+
export declare function searchLines(res: SearchResponse, query: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* `add`'s success block. The URL is the FIRST line of each entry because `add`
|
|
26
|
+
* exists to produce URLs; the byte facts follow as confirmation that the right one
|
|
27
|
+
* arrived.
|
|
28
|
+
*/
|
|
29
|
+
export declare function addLines(resolved: AssetRecord[], unknownCount: number, requestedCount: number): string[];
|
|
30
|
+
/**
|
|
31
|
+
* The failure block for unresolved ids. Last, `✗`, and the offending string echoed
|
|
32
|
+
* VERBATIM on its own indented line so an agent can diff its own input without
|
|
33
|
+
* parsing our sentence.
|
|
34
|
+
*/
|
|
35
|
+
export declare function unknownBlockLines(unknown: string[], resolvedCount: number, malformed: boolean): string[];
|
|
36
|
+
/** `show`'s stacked key/value block — the "tell me everything" verb. */
|
|
37
|
+
export declare function showLines(a: AssetRecord): string[];
|
|
38
|
+
/** The three exit-4 screens. Each states that the creator's PROJECT is unaffected. */
|
|
39
|
+
export declare function unreachableLines(kind: "transport" | "auth" | "unavailable", retry: string): string[];
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// The human renderers for `vincentt assets`. Pure string builders — no I/O, no
|
|
2
|
+
// exit codes — so every line the designer specified is unit-assertable.
|
|
3
|
+
//
|
|
4
|
+
// The reader is a LANGUAGE MODEL with a human over its shoulder, so everything
|
|
5
|
+
// here is designed to be true when read as plain text with color stripped. There
|
|
6
|
+
// is no color, no spinner and no box drawing in this file.
|
|
7
|
+
//
|
|
8
|
+
// Every field read is guarded (see wire.ts). A record missing `usage_hint` loses
|
|
9
|
+
// that LINE; it does not lose the record, and it never throws.
|
|
10
|
+
/** `prop · 1:1 · alpha` — the placement contract, from facts the bytes cannot get wrong. */
|
|
11
|
+
export function contractLine(a) {
|
|
12
|
+
// `kind` may legitimately be ABSENT (unclassifiable). It is omitted rather than
|
|
13
|
+
// printed as "unknown", which would read as a classification we did not make.
|
|
14
|
+
const parts = [a.kind, a.aspect, a.has_alpha === true ? "alpha" : undefined].filter((p) => typeof p === "string" && p.length > 0);
|
|
15
|
+
return parts.join(" · ");
|
|
16
|
+
}
|
|
17
|
+
/** `pumpkin · prop · 1:1 · 1024x1024 · alpha` — `add`'s confirmation line (byte facts). */
|
|
18
|
+
export function byteFactsLine(a) {
|
|
19
|
+
const dims = a.width !== undefined && a.height !== undefined ? `${a.width}x${a.height}` : undefined;
|
|
20
|
+
const parts = [
|
|
21
|
+
a.subject,
|
|
22
|
+
a.kind,
|
|
23
|
+
a.aspect,
|
|
24
|
+
dims,
|
|
25
|
+
a.has_alpha === true ? "alpha" : undefined,
|
|
26
|
+
].filter((p) => typeof p === "string" && p.length > 0);
|
|
27
|
+
return parts.join(" · ");
|
|
28
|
+
}
|
|
29
|
+
/** Human byte size, e.g. `180 KB`. Absent bytes render as an absent size. */
|
|
30
|
+
export function humanBytes(bytes) {
|
|
31
|
+
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (bytes < 1024)
|
|
34
|
+
return `${bytes} B`;
|
|
35
|
+
const kb = bytes / 1024;
|
|
36
|
+
if (kb < 1024)
|
|
37
|
+
return `${Math.round(kb)} KB`;
|
|
38
|
+
return `${(kb / 1024).toFixed(1)} MB`;
|
|
39
|
+
}
|
|
40
|
+
function wrap(text, width, indent) {
|
|
41
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
42
|
+
if (words.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const lines = [];
|
|
45
|
+
let line = "";
|
|
46
|
+
for (const w of words) {
|
|
47
|
+
if (line.length === 0)
|
|
48
|
+
line = w;
|
|
49
|
+
else if (`${line} ${w}`.length <= width)
|
|
50
|
+
line = `${line} ${w}`;
|
|
51
|
+
else {
|
|
52
|
+
lines.push(indent + line);
|
|
53
|
+
line = w;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (line.length > 0)
|
|
57
|
+
lines.push(indent + line);
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One search result, in the five-line layout the design fixes:
|
|
62
|
+
* 1 id + the placement contract (byte facts — decides usability)
|
|
63
|
+
* 2 subject — description (the noun leads; the agent matched on a noun)
|
|
64
|
+
* 3 usage_hint, UNLABELLED (it is an instruction and reads as one)
|
|
65
|
+
* 4 style · render then palette (the NEXT query's vocabulary)
|
|
66
|
+
* 5 the URL, whole and alone (the payload)
|
|
67
|
+
* Any line whose fields are absent is simply not emitted.
|
|
68
|
+
*/
|
|
69
|
+
export function searchRecordLines(a) {
|
|
70
|
+
const out = [];
|
|
71
|
+
const contract = contractLine(a);
|
|
72
|
+
out.push(` ${a.id}${contract ? ` ${contract}` : ""}`);
|
|
73
|
+
const subject = a.subject;
|
|
74
|
+
const description = a.description;
|
|
75
|
+
if (subject !== undefined && description !== undefined) {
|
|
76
|
+
// The description is lowercased at the join only when it starts a sentence
|
|
77
|
+
// after the em-less "subject — " lead, matching the approved transcript.
|
|
78
|
+
const d = description.charAt(0).toLowerCase() + description.slice(1);
|
|
79
|
+
out.push(...wrap(`${subject} — ${d}`, 72, " "));
|
|
80
|
+
}
|
|
81
|
+
else if (subject !== undefined) {
|
|
82
|
+
out.push(` ${subject}`);
|
|
83
|
+
}
|
|
84
|
+
else if (description !== undefined) {
|
|
85
|
+
out.push(...wrap(description, 72, " "));
|
|
86
|
+
}
|
|
87
|
+
if (a.usage_hint !== undefined)
|
|
88
|
+
out.push(...wrap(a.usage_hint, 72, " "));
|
|
89
|
+
const styleTokens = [...(a.style ?? [])];
|
|
90
|
+
if (a.render !== undefined)
|
|
91
|
+
styleTokens.unshift(a.render);
|
|
92
|
+
const left = styleTokens.join(" · ");
|
|
93
|
+
const right = (a.palette ?? []).join(", ");
|
|
94
|
+
if (left || right) {
|
|
95
|
+
out.push(` ${left}${left && right ? " · " : ""}${right}`);
|
|
96
|
+
}
|
|
97
|
+
out.push(` ${a.url}`);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** The `Narrow it:` footer — a vocabulary with real example values, not a usage string. */
|
|
101
|
+
export function narrowFooter(results) {
|
|
102
|
+
// Real values are pulled from the results the caller is looking at: an agent that
|
|
103
|
+
// has never seen the corpus does not know `--render` takes `3d-clay` and not
|
|
104
|
+
// `clay`. This is the cheapest schema disclosure available and it costs one line.
|
|
105
|
+
const render = results.find((r) => r.render !== undefined)?.render ?? "3d-clay";
|
|
106
|
+
const palette = results.flatMap((r) => r.palette ?? [])[0] ?? "sage";
|
|
107
|
+
return ` Narrow it: --kind prop|frame --render ${render} --palette ${palette} --limit 20`;
|
|
108
|
+
}
|
|
109
|
+
/** The zero-result screen. Exit 0, and DELIBERATELY no glyph. */
|
|
110
|
+
export function zeroResultLines(query, gaps, revision) {
|
|
111
|
+
const out = ["", ` No assets match "${query}".`, ""];
|
|
112
|
+
// The inventory sentence is served VERBATIM from the corpus's gaps array — it is
|
|
113
|
+
// a fact about what we own, not a phrase the CLI composed. Closing the gap is a
|
|
114
|
+
// corpus change, not a code change. De-duplicated because two tags legitimately
|
|
115
|
+
// carry the same message ("wearable" and "face-filter" both explain one absence).
|
|
116
|
+
// VERBATIM — not re-cased, not re-punctuated. The moment the CLI edits this
|
|
117
|
+
// string it is composing an inventory claim rather than reporting one, and the
|
|
118
|
+
// whole point is that closing a gap is a CORPUS change, not a code change.
|
|
119
|
+
const messages = [...new Set(gaps.map((g) => g.message))];
|
|
120
|
+
for (const m of messages) {
|
|
121
|
+
out.push(...wrap(m, 70, " "));
|
|
122
|
+
out.push("");
|
|
123
|
+
}
|
|
124
|
+
// The anti-"Did you mean". When the miss is an inventory fact, a suggested
|
|
125
|
+
// rephrasing would imply one exists that works. It does not, and implying it is
|
|
126
|
+
// a lie of exactly the class this feature exists to prevent.
|
|
127
|
+
out.push(" There is nothing to try differently on this query. What the library has:");
|
|
128
|
+
out.push("");
|
|
129
|
+
out.push(" --kind prop objects to place in a scene 1:1, transparent");
|
|
130
|
+
out.push(" --kind frame full-screen camera overlays 9:16, transparent centre");
|
|
131
|
+
if (revision !== undefined) {
|
|
132
|
+
out.push("");
|
|
133
|
+
out.push(` library revision ${revision}`);
|
|
134
|
+
}
|
|
135
|
+
out.push("");
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/** The full human search screen. */
|
|
139
|
+
export function searchLines(res, query) {
|
|
140
|
+
if (res.resultCount === 0 || res.results.length === 0) {
|
|
141
|
+
return zeroResultLines(query, res.gaps, res.revision);
|
|
142
|
+
}
|
|
143
|
+
const out = [""];
|
|
144
|
+
const noun = res.resultCount === 1 ? "asset" : "assets";
|
|
145
|
+
// The revision is the one piece of platform bookkeeping on the screen: it makes
|
|
146
|
+
// "the library changed under me" answerable without SSH. Absent on an older API,
|
|
147
|
+
// in which case the header degrades to just the count.
|
|
148
|
+
out.push(` ${res.resultCount} ${noun}${res.revision !== undefined ? ` · library revision ${res.revision}` : ""}`);
|
|
149
|
+
out.push("");
|
|
150
|
+
for (const a of res.results) {
|
|
151
|
+
out.push(...searchRecordLines(a));
|
|
152
|
+
out.push("");
|
|
153
|
+
}
|
|
154
|
+
const shown = res.results.length;
|
|
155
|
+
if (res.resultCount > shown) {
|
|
156
|
+
out.push(` … ${res.resultCount - shown} more`);
|
|
157
|
+
out.push("");
|
|
158
|
+
}
|
|
159
|
+
out.push(narrowFooter(res.results));
|
|
160
|
+
out.push("");
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* `add`'s success block. The URL is the FIRST line of each entry because `add`
|
|
165
|
+
* exists to produce URLs; the byte facts follow as confirmation that the right one
|
|
166
|
+
* arrived.
|
|
167
|
+
*/
|
|
168
|
+
export function addLines(resolved, unknownCount, requestedCount) {
|
|
169
|
+
const out = [""];
|
|
170
|
+
// The header changes SHAPE when something is missing — "4 assets" vs
|
|
171
|
+
// "4 of 5 assets" — because a different shape survives a skim and a different
|
|
172
|
+
// number does not.
|
|
173
|
+
out.push(unknownCount > 0
|
|
174
|
+
? ` ${resolved.length} of ${requestedCount} assets`
|
|
175
|
+
: ` ${resolved.length} ${resolved.length === 1 ? "asset" : "assets"}`);
|
|
176
|
+
out.push("");
|
|
177
|
+
for (const a of resolved) {
|
|
178
|
+
out.push(` ${a.url}`);
|
|
179
|
+
const facts = byteFactsLine(a);
|
|
180
|
+
if (facts)
|
|
181
|
+
out.push(` ${facts}`);
|
|
182
|
+
out.push("");
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The failure block for unresolved ids. Last, `✗`, and the offending string echoed
|
|
188
|
+
* VERBATIM on its own indented line so an agent can diff its own input without
|
|
189
|
+
* parsing our sentence.
|
|
190
|
+
*/
|
|
191
|
+
export function unknownBlockLines(unknown, resolvedCount, malformed) {
|
|
192
|
+
const out = [];
|
|
193
|
+
const n = unknown.length;
|
|
194
|
+
out.push(`✗ ${n} ${n === 1 ? "id is" : "ids are"} not in the library:`);
|
|
195
|
+
out.push("");
|
|
196
|
+
for (const id of unknown)
|
|
197
|
+
out.push(` ${id}`);
|
|
198
|
+
out.push("");
|
|
199
|
+
// Printed ONLY when a passed id was not even id-shaped. This is f5's closest
|
|
200
|
+
// thing to teaching the invented-URL rule, at the moment the agent is actually
|
|
201
|
+
// making that mistake — and it is advice that AGREES with a control, never the
|
|
202
|
+
// control itself (the control is that no verb accepts a URL).
|
|
203
|
+
if (malformed) {
|
|
204
|
+
out.push(" Ids look like lib_7f3a2c67d4e5b6a7. Search returns them; nothing else does.");
|
|
205
|
+
}
|
|
206
|
+
if (resolvedCount > 0) {
|
|
207
|
+
// The sentence that prevents the expensive mistake: an agent that reads exit 1
|
|
208
|
+
// and nothing else discards the whole turn; one that reads this keeps its good
|
|
209
|
+
// resolutions and re-searches for the one that missed.
|
|
210
|
+
out.push(malformed
|
|
211
|
+
? ` The ${resolvedCount} above resolved and ${resolvedCount === 1 ? "is" : "are"} safe to use.`
|
|
212
|
+
: ` The ${resolvedCount} above resolved and ${resolvedCount === 1 ? "is" : "are"} safe to use. Search for the missing one:`);
|
|
213
|
+
if (!malformed)
|
|
214
|
+
out.push(` vincentt assets search "<what you wanted>"`);
|
|
215
|
+
}
|
|
216
|
+
out.push("");
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/** `show`'s stacked key/value block — the "tell me everything" verb. */
|
|
220
|
+
export function showLines(a) {
|
|
221
|
+
const out = ["", ` ${a.id}`, ""];
|
|
222
|
+
if (a.retired === true) {
|
|
223
|
+
// Retirement removes an asset from SEARCH, never from SERVING (B37). The `!`
|
|
224
|
+
// is advisory and never `✗`: the only reason an agent holds a retired id is
|
|
225
|
+
// that a project already references it, and the useful instruction is to do
|
|
226
|
+
// nothing.
|
|
227
|
+
out.push("! This asset is no longer in search results. The URL below keeps working");
|
|
228
|
+
out.push(" forever — if you are already using it, nothing needs to change.");
|
|
229
|
+
out.push("");
|
|
230
|
+
}
|
|
231
|
+
if (a.subject !== undefined)
|
|
232
|
+
out.push(` ${a.subject}`);
|
|
233
|
+
if (a.description !== undefined)
|
|
234
|
+
out.push(...wrap(a.description, 74, " "));
|
|
235
|
+
if (a.usage_hint !== undefined) {
|
|
236
|
+
out.push("");
|
|
237
|
+
out.push(...wrap(a.usage_hint, 74, " "));
|
|
238
|
+
}
|
|
239
|
+
out.push("");
|
|
240
|
+
const row = (label, value) => {
|
|
241
|
+
if (value !== undefined && value.length > 0)
|
|
242
|
+
out.push(` ${label.padEnd(10)}${value}`);
|
|
243
|
+
};
|
|
244
|
+
row("kind", a.kind);
|
|
245
|
+
const size = a.width !== undefined && a.height !== undefined
|
|
246
|
+
? `${a.width} x ${a.height}${a.aspect !== undefined ? ` (${a.aspect})` : ""}`
|
|
247
|
+
: undefined;
|
|
248
|
+
row("size", size);
|
|
249
|
+
if (a.has_alpha !== undefined) {
|
|
250
|
+
const coverage = a.occupies_canvas !== undefined
|
|
251
|
+
? ` · covers ${Math.round(a.occupies_canvas * 100)}% of the canvas`
|
|
252
|
+
: "";
|
|
253
|
+
row("alpha", `${a.has_alpha ? "yes" : "no"}${coverage}`);
|
|
254
|
+
}
|
|
255
|
+
const size2 = humanBytes(a.bytes);
|
|
256
|
+
row("format", a.format !== undefined ? `${a.format}${size2 ? ` · ${size2}` : ""}` : size2);
|
|
257
|
+
row("render", a.render);
|
|
258
|
+
row("view", a.view);
|
|
259
|
+
row("text", a.text_in_image);
|
|
260
|
+
row("style", (a.style ?? []).join(", "));
|
|
261
|
+
row("palette", (a.palette ?? []).join(", "));
|
|
262
|
+
out.push("");
|
|
263
|
+
out.push(` ${a.url}`);
|
|
264
|
+
out.push("");
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
/** The three exit-4 screens. Each states that the creator's PROJECT is unaffected. */
|
|
268
|
+
export function unreachableLines(kind, retry) {
|
|
269
|
+
if (kind === "auth") {
|
|
270
|
+
return [
|
|
271
|
+
"",
|
|
272
|
+
"✗ You're not signed in.",
|
|
273
|
+
"",
|
|
274
|
+
" The library is a platform call, so it needs an account. Your project is",
|
|
275
|
+
" unaffected — build, preview and publish don't use the library.",
|
|
276
|
+
"",
|
|
277
|
+
" Sign in: vincentt login",
|
|
278
|
+
"",
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
if (kind === "unavailable") {
|
|
282
|
+
return [
|
|
283
|
+
"",
|
|
284
|
+
"✗ The library is temporarily unavailable.",
|
|
285
|
+
"",
|
|
286
|
+
" Nothing is wrong with your project. Try again in a moment:",
|
|
287
|
+
` ${retry}`,
|
|
288
|
+
"",
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
// B36 said out loud at the only moment anyone cares. A creator whose tool just
|
|
292
|
+
// failed is one thought from "is my project broken?", and the answer is no,
|
|
293
|
+
// definitively. It also pre-empts the wrong recovery: an agent that believes the
|
|
294
|
+
// library is required starts improvising URLs.
|
|
295
|
+
return [
|
|
296
|
+
"",
|
|
297
|
+
"✗ Can't reach the asset library.",
|
|
298
|
+
"",
|
|
299
|
+
" Your project is unaffected — build, preview and publish don't use the",
|
|
300
|
+
" library. Any library URL you already have keeps working.",
|
|
301
|
+
"",
|
|
302
|
+
` Try again in a moment: ${retry}`,
|
|
303
|
+
"",
|
|
304
|
+
];
|
|
305
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The asset id shape. Base32-lower, matching the server's `^lib_[a-z2-7]{16}$`
|
|
3
|
+
* exactly.
|
|
4
|
+
*
|
|
5
|
+
* The alphabet is the house convention across this API (f3 sessions {32}, f4
|
|
6
|
+
* uploads {26}, preview host labels, publish keys) and it excludes 0/1/8/9 so an
|
|
7
|
+
* id read off a terminal carries no visually ambiguous characters. 16 chars is
|
|
8
|
+
* 80 bits, which is what makes the key pattern unguessable, one of the four
|
|
9
|
+
* counters to an agent inventing a URL.
|
|
10
|
+
*
|
|
11
|
+
* The CLI briefly accepted hex as well, because every EXAMPLE id written into
|
|
12
|
+
* the design record was hex and contradicted the record's own regex. User ruling
|
|
13
|
+
* 2026-08-23: the regex is authoritative and the examples were wrong. Matching
|
|
14
|
+
* the server exactly is what keeps a client-side reject and a server-side
|
|
15
|
+
* `unknown[]` from ever disagreeing.
|
|
16
|
+
*/
|
|
17
|
+
export declare const LIB_ID_RE: RegExp;
|
|
18
|
+
export declare function isLibraryId(s: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* An asset record as the API emits it. Only `id` and `url` are structurally
|
|
21
|
+
* required; everything else is optional BECAUSE THE WIRE SAYS SO, not as a
|
|
22
|
+
* convenience:
|
|
23
|
+
* • `kind` may be ABSENT — a real state (unclassifiable), never "" and never
|
|
24
|
+
* "unknown". Coercing absence to a string would invent a classification.
|
|
25
|
+
* • `text_in_image` is omitted when not confidently read, never "".
|
|
26
|
+
* • `retired` rides fetch and batch but NOT search (a search never returns a
|
|
27
|
+
* retired asset), so its absence is normal rather than a defect.
|
|
28
|
+
*/
|
|
29
|
+
export interface AssetRecord {
|
|
30
|
+
id: string;
|
|
31
|
+
url: string;
|
|
32
|
+
kind?: "prop" | "frame";
|
|
33
|
+
width?: number;
|
|
34
|
+
height?: number;
|
|
35
|
+
aspect?: string;
|
|
36
|
+
has_alpha?: boolean;
|
|
37
|
+
bytes?: number;
|
|
38
|
+
format?: string;
|
|
39
|
+
subject?: string;
|
|
40
|
+
style?: string[];
|
|
41
|
+
palette?: string[];
|
|
42
|
+
render?: string;
|
|
43
|
+
view?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
usage_hint?: string;
|
|
46
|
+
text_in_image?: string;
|
|
47
|
+
occupies_canvas?: number;
|
|
48
|
+
retired?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface Gap {
|
|
51
|
+
tag: string;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SearchResponse {
|
|
55
|
+
query?: string;
|
|
56
|
+
/** The corpus the answer came from. Absent on an older API — the header line degrades away. */
|
|
57
|
+
revision?: string;
|
|
58
|
+
resultCount: number;
|
|
59
|
+
results: AssetRecord[];
|
|
60
|
+
gaps: Gap[];
|
|
61
|
+
}
|
|
62
|
+
export interface BatchResponse {
|
|
63
|
+
resolved: AssetRecord[];
|
|
64
|
+
/** Verbatim echoes of unresolvable inputs. Disjoint from `resolved`. */
|
|
65
|
+
unknown: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse one asset record.
|
|
69
|
+
*
|
|
70
|
+
* `kind` is admitted ONLY as "prop" or "frame". Anything else — including "" and
|
|
71
|
+
* "unknown" — parses to ABSENT rather than being passed through, because a record
|
|
72
|
+
* whose kind is the literal string "unknown" would render as a real classification
|
|
73
|
+
* on the line an agent uses to decide whether the asset is usable at all.
|
|
74
|
+
*
|
|
75
|
+
* Returns undefined when `id`/`url` are missing: a record with no URL is not a
|
|
76
|
+
* usable result, and dropping it is better than rendering an entry whose payload
|
|
77
|
+
* line is blank.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseAssetRecord(raw: unknown): AssetRecord | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Parse the search envelope.
|
|
82
|
+
*
|
|
83
|
+
* `resultCount` is taken from the wire when present and otherwise derived from the
|
|
84
|
+
* array, so a truncated envelope still renders a truthful count rather than 0 —
|
|
85
|
+
* and 0 is the one value that must never be invented, because zero results is a
|
|
86
|
+
* meaningful product answer (the honest-empty path) rather than an absence.
|
|
87
|
+
*/
|
|
88
|
+
export declare function parseSearchResponse(raw: unknown): SearchResponse;
|
|
89
|
+
/**
|
|
90
|
+
* Parse the batch envelope. `unknown[]` entries are echoed VERBATIM by the server
|
|
91
|
+
* and are kept as raw strings — they are the caller's own input coming back, which
|
|
92
|
+
* is what lets an agent diff against what it sent without parsing our prose.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseBatchResponse(raw: unknown): BatchResponse;
|
|
95
|
+
/** A single record from `GET /library/assets/{id}` — the same record shape, unwrapped. */
|
|
96
|
+
export declare function parseFetchResponse(raw: unknown): AssetRecord | undefined;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The f5 asset-library wire shapes, and the ONE parser every renderer reads through.
|
|
2
|
+
//
|
|
3
|
+
// The envelope is camelCase (resultCount, gaps, resolved, unknown) while the asset
|
|
4
|
+
// RECORD is snake_case (has_alpha, usage_hint, text_in_image, occupies_canvas).
|
|
5
|
+
// That mix is deliberate and it is the API's only mixed-convention response
|
|
6
|
+
// (implementation 2.12): the record's vocabulary sits beside English prose in an
|
|
7
|
+
// agent's context, so it reads as prose-adjacent rather than as code identifiers.
|
|
8
|
+
// NEITHER SIDE IS NORMALIZED HERE. Rewriting the record to camelCase would silently
|
|
9
|
+
// break every agent prompt in the wild that reads `usage_hint`.
|
|
10
|
+
//
|
|
11
|
+
// EVERY field is read defensively, for the reason lib/diagnostics.ts records: the
|
|
12
|
+
// CLI and the API are versioned separately on a creator's machine — `npx vincentt`
|
|
13
|
+
// fetches whatever the registry has and points it at whatever API the config
|
|
14
|
+
// resolves to. A field a renderer reads unguarded is a field whose removal from the
|
|
15
|
+
// API crashes the command, which is exactly what removing `sessions` did once.
|
|
16
|
+
// A missing optional field must degrade to a missing LINE, never to an exception.
|
|
17
|
+
/**
|
|
18
|
+
* The asset id shape. Base32-lower, matching the server's `^lib_[a-z2-7]{16}$`
|
|
19
|
+
* exactly.
|
|
20
|
+
*
|
|
21
|
+
* The alphabet is the house convention across this API (f3 sessions {32}, f4
|
|
22
|
+
* uploads {26}, preview host labels, publish keys) and it excludes 0/1/8/9 so an
|
|
23
|
+
* id read off a terminal carries no visually ambiguous characters. 16 chars is
|
|
24
|
+
* 80 bits, which is what makes the key pattern unguessable, one of the four
|
|
25
|
+
* counters to an agent inventing a URL.
|
|
26
|
+
*
|
|
27
|
+
* The CLI briefly accepted hex as well, because every EXAMPLE id written into
|
|
28
|
+
* the design record was hex and contradicted the record's own regex. User ruling
|
|
29
|
+
* 2026-08-23: the regex is authoritative and the examples were wrong. Matching
|
|
30
|
+
* the server exactly is what keeps a client-side reject and a server-side
|
|
31
|
+
* `unknown[]` from ever disagreeing.
|
|
32
|
+
*/
|
|
33
|
+
export const LIB_ID_RE = /^lib_[a-z2-7]{16}$/;
|
|
34
|
+
export function isLibraryId(s) {
|
|
35
|
+
return LIB_ID_RE.test(s);
|
|
36
|
+
}
|
|
37
|
+
function obj(v) {
|
|
38
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
39
|
+
}
|
|
40
|
+
function str(o, k) {
|
|
41
|
+
return typeof o[k] === "string" ? o[k] : undefined;
|
|
42
|
+
}
|
|
43
|
+
function num(o, k) {
|
|
44
|
+
return typeof o[k] === "number" && Number.isFinite(o[k]) ? o[k] : undefined;
|
|
45
|
+
}
|
|
46
|
+
function bool(o, k) {
|
|
47
|
+
return typeof o[k] === "boolean" ? o[k] : undefined;
|
|
48
|
+
}
|
|
49
|
+
function strArray(o, k) {
|
|
50
|
+
const v = o[k];
|
|
51
|
+
if (!Array.isArray(v))
|
|
52
|
+
return undefined;
|
|
53
|
+
return v.filter((x) => typeof x === "string");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse one asset record.
|
|
57
|
+
*
|
|
58
|
+
* `kind` is admitted ONLY as "prop" or "frame". Anything else — including "" and
|
|
59
|
+
* "unknown" — parses to ABSENT rather than being passed through, because a record
|
|
60
|
+
* whose kind is the literal string "unknown" would render as a real classification
|
|
61
|
+
* on the line an agent uses to decide whether the asset is usable at all.
|
|
62
|
+
*
|
|
63
|
+
* Returns undefined when `id`/`url` are missing: a record with no URL is not a
|
|
64
|
+
* usable result, and dropping it is better than rendering an entry whose payload
|
|
65
|
+
* line is blank.
|
|
66
|
+
*/
|
|
67
|
+
export function parseAssetRecord(raw) {
|
|
68
|
+
const o = obj(raw);
|
|
69
|
+
const id = str(o, "id");
|
|
70
|
+
const url = str(o, "url");
|
|
71
|
+
if (id === undefined || url === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
// The wire is an input too. A record whose url is not https is dropped rather
|
|
74
|
+
// than rendered, so a compromised or misconfigured API cannot point --download
|
|
75
|
+
// at file:// or a link-local address.
|
|
76
|
+
if (!url.startsWith("https://"))
|
|
77
|
+
return undefined;
|
|
78
|
+
const kindRaw = str(o, "kind");
|
|
79
|
+
const kind = kindRaw === "prop" || kindRaw === "frame" ? kindRaw : undefined;
|
|
80
|
+
// An empty string is the "not confidently read" case the API omits; treat a
|
|
81
|
+
// stray "" the same way rather than rendering an empty labelled line.
|
|
82
|
+
const textInImage = str(o, "text_in_image");
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
url,
|
|
86
|
+
...(kind !== undefined ? { kind } : {}),
|
|
87
|
+
...(num(o, "width") !== undefined ? { width: num(o, "width") } : {}),
|
|
88
|
+
...(num(o, "height") !== undefined ? { height: num(o, "height") } : {}),
|
|
89
|
+
...(str(o, "aspect") !== undefined ? { aspect: str(o, "aspect") } : {}),
|
|
90
|
+
...(bool(o, "has_alpha") !== undefined ? { has_alpha: bool(o, "has_alpha") } : {}),
|
|
91
|
+
...(num(o, "bytes") !== undefined ? { bytes: num(o, "bytes") } : {}),
|
|
92
|
+
...(str(o, "format") !== undefined ? { format: str(o, "format") } : {}),
|
|
93
|
+
...(str(o, "subject") !== undefined ? { subject: str(o, "subject") } : {}),
|
|
94
|
+
...(strArray(o, "style") !== undefined ? { style: strArray(o, "style") } : {}),
|
|
95
|
+
...(strArray(o, "palette") !== undefined ? { palette: strArray(o, "palette") } : {}),
|
|
96
|
+
...(str(o, "render") !== undefined ? { render: str(o, "render") } : {}),
|
|
97
|
+
...(str(o, "view") !== undefined ? { view: str(o, "view") } : {}),
|
|
98
|
+
...(str(o, "description") !== undefined
|
|
99
|
+
? { description: str(o, "description") }
|
|
100
|
+
: {}),
|
|
101
|
+
...(str(o, "usage_hint") !== undefined ? { usage_hint: str(o, "usage_hint") } : {}),
|
|
102
|
+
...(textInImage ? { text_in_image: textInImage } : {}),
|
|
103
|
+
...(num(o, "occupies_canvas") !== undefined
|
|
104
|
+
? { occupies_canvas: num(o, "occupies_canvas") }
|
|
105
|
+
: {}),
|
|
106
|
+
...(bool(o, "retired") !== undefined ? { retired: bool(o, "retired") } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function parseGaps(raw) {
|
|
110
|
+
if (!Array.isArray(raw))
|
|
111
|
+
return [];
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const g of raw) {
|
|
114
|
+
const o = obj(g);
|
|
115
|
+
const tag = str(o, "tag");
|
|
116
|
+
const message = str(o, "message");
|
|
117
|
+
// Both fields are load-bearing and carry different jobs: the agent BRANCHES on
|
|
118
|
+
// `tag` and QUOTES `message`. A half-formed gap is dropped rather than rendered
|
|
119
|
+
// with an invented half.
|
|
120
|
+
if (tag !== undefined && message !== undefined)
|
|
121
|
+
out.push({ tag, message });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse the search envelope.
|
|
127
|
+
*
|
|
128
|
+
* `resultCount` is taken from the wire when present and otherwise derived from the
|
|
129
|
+
* array, so a truncated envelope still renders a truthful count rather than 0 —
|
|
130
|
+
* and 0 is the one value that must never be invented, because zero results is a
|
|
131
|
+
* meaningful product answer (the honest-empty path) rather than an absence.
|
|
132
|
+
*/
|
|
133
|
+
export function parseSearchResponse(raw) {
|
|
134
|
+
const o = obj(raw);
|
|
135
|
+
const results = Array.isArray(o.results)
|
|
136
|
+
? o.results.map(parseAssetRecord).filter((r) => r !== undefined)
|
|
137
|
+
: [];
|
|
138
|
+
return {
|
|
139
|
+
...(str(o, "query") !== undefined ? { query: str(o, "query") } : {}),
|
|
140
|
+
...(str(o, "revision") !== undefined ? { revision: str(o, "revision") } : {}),
|
|
141
|
+
resultCount: num(o, "resultCount") ?? results.length,
|
|
142
|
+
results,
|
|
143
|
+
gaps: parseGaps(o.gaps),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse the batch envelope. `unknown[]` entries are echoed VERBATIM by the server
|
|
148
|
+
* and are kept as raw strings — they are the caller's own input coming back, which
|
|
149
|
+
* is what lets an agent diff against what it sent without parsing our prose.
|
|
150
|
+
*/
|
|
151
|
+
export function parseBatchResponse(raw) {
|
|
152
|
+
const o = obj(raw);
|
|
153
|
+
const resolved = Array.isArray(o.resolved)
|
|
154
|
+
? o.resolved.map(parseAssetRecord).filter((r) => r !== undefined)
|
|
155
|
+
: [];
|
|
156
|
+
const unknown = Array.isArray(o.unknown)
|
|
157
|
+
? o.unknown.filter((x) => typeof x === "string")
|
|
158
|
+
: [];
|
|
159
|
+
return { resolved, unknown };
|
|
160
|
+
}
|
|
161
|
+
/** A single record from `GET /library/assets/{id}` — the same record shape, unwrapped. */
|
|
162
|
+
export function parseFetchResponse(raw) {
|
|
163
|
+
const o = obj(raw);
|
|
164
|
+
// Tolerate both a bare record and an { asset: … } envelope: /me already taught
|
|
165
|
+
// this codebase that an envelope can appear on a route that once returned bare.
|
|
166
|
+
return parseAssetRecord(o.asset !== undefined ? o.asset : o);
|
|
167
|
+
}
|
package/dist/publish/api.d.ts
CHANGED
|
@@ -46,6 +46,26 @@ export interface ApiErrorDetail {
|
|
|
46
46
|
mismatched?: number;
|
|
47
47
|
reason?: string;
|
|
48
48
|
path?: string;
|
|
49
|
+
/**
|
|
50
|
+
* f10's plan-limit refusal. Bytes are DECIMAL on the wire (1 GB = 10^9), the
|
|
51
|
+
* same units formatBytes already renders.
|
|
52
|
+
*/
|
|
53
|
+
figureBytes?: number;
|
|
54
|
+
allowanceBytes?: number;
|
|
55
|
+
cutOffAt?: string;
|
|
56
|
+
from?: string;
|
|
57
|
+
to?: string;
|
|
58
|
+
/** The tier in force, when the API names one. */
|
|
59
|
+
planTier?: string;
|
|
60
|
+
planName?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The org that owns THIS project, as the API names it. It is not the machine's
|
|
63
|
+
* configured active org and must win over it: a project can belong to an org
|
|
64
|
+
* the CLI is not currently pointed at, and naming the wrong one in a refusal
|
|
65
|
+
* tells the creator's agent to go look at an org that was never cut off.
|
|
66
|
+
*/
|
|
67
|
+
orgName?: string;
|
|
68
|
+
orgSlug?: string;
|
|
49
69
|
}
|
|
50
70
|
export declare class PublishApiError extends Error {
|
|
51
71
|
readonly status: number;
|
|
@@ -18,6 +18,9 @@ export interface PublishDeps {
|
|
|
18
18
|
sleep: (ms: number) => Promise<void>;
|
|
19
19
|
fetchImpl?: typeof fetch;
|
|
20
20
|
orgName?: () => Promise<string>;
|
|
21
|
+
/** The active org's slug, for the usage deep link on a plan-limit refusal. */
|
|
22
|
+
orgSlug?: () => Promise<string | undefined>;
|
|
23
|
+
consoleUrl?: string;
|
|
21
24
|
}
|
|
22
25
|
export interface PublishOptions {
|
|
23
26
|
distDir?: string;
|