@yolo-labs/yolo-cli 0.23.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/README.md +57 -0
- package/dist/artifact-get.js +131 -0
- package/dist/artifact-list.js +133 -0
- package/dist/auth-context.js +60 -0
- package/dist/canonicalizer.js +208 -0
- package/dist/cli.js +1403 -0
- package/dist/context.js +58 -0
- package/dist/deploy-bundle.js +394 -0
- package/dist/deploy-cli.js +1341 -0
- package/dist/deploy-client.js +460 -0
- package/dist/deploy-config.js +301 -0
- package/dist/deploy-detect.js +498 -0
- package/dist/deploy-ship.js +389 -0
- package/dist/lockfile.js +203 -0
- package/dist/plan-diff.js +162 -0
- package/dist/plan-export.js +261 -0
- package/dist/plan-frontmatter.schema.json +184 -0
- package/dist/plan-get.js +244 -0
- package/dist/plan-import.js +420 -0
- package/dist/plan-list.js +153 -0
- package/dist/plan-open.js +100 -0
- package/dist/plan-state.js +228 -0
- package/dist/plan-validate.js +270 -0
- package/dist/revision.js +43 -0
- package/dist/run-get.js +130 -0
- package/dist/run-lifecycle.js +133 -0
- package/dist/run-list.js +148 -0
- package/dist/run-start.js +110 -0
- package/dist/run-transfer.js +128 -0
- package/dist/serve.js +227 -0
- package/dist/tileapp-developer.js +222 -0
- package/dist/tileapp-oci-assembler.js +591 -0
- package/dist/tileapp-personal.js +461 -0
- package/dist/tileapp-publisher.js +134 -0
- package/dist/tileapp-validator.js +239 -0
- package/dist/vendored/flexdb.mjs +1087 -0
- package/dist/webapp-url.js +61 -0
- package/dist/work-client.js +195 -0
- package/package.json +39 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tile-app manifest + bundle validator for the LOCAL dev harness (M1 slice 6).
|
|
3
|
+
*
|
|
4
|
+
* This is a faithful, dependency-free MIRROR of the server's authoritative
|
|
5
|
+
* validator at `common-api/src/types/tileapp.ts` (`validateManifest` /
|
|
6
|
+
* `parsePermission`) — the yolo-cli is a standalone package and cannot import
|
|
7
|
+
* common-api. Keep the two in lockstep: the structural rules + error strings
|
|
8
|
+
* here intentionally match the server so `yolo tileapp validate` reports the
|
|
9
|
+
* SAME taxonomy a partner will hit at `POST /v1/publisher/publish`.
|
|
10
|
+
*
|
|
11
|
+
* One deliberate difference: this offline validator checks permission SHAPE
|
|
12
|
+
* (namespace + segments) but NOT membership in the server's `VALID_MCP_SCOPES`
|
|
13
|
+
* / `SECRET_PRESET_KEYS` enums (those live server-side). An `mcp:<scope>` or
|
|
14
|
+
* `secret:<KEY>` with a well-formed but unknown arg passes the lint and is
|
|
15
|
+
* confirmed authoritatively at publish — `validate` says so in its output.
|
|
16
|
+
*/
|
|
17
|
+
const ID_RE = /^[a-z0-9][a-z0-9-]{1,63}$/;
|
|
18
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].+)?$/;
|
|
19
|
+
function isObj(v) {
|
|
20
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
function isStr(v) {
|
|
23
|
+
return typeof v === 'string' && v.length > 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decompose a permission string by SHAPE. Returns null for an unrecognized
|
|
27
|
+
* shape. Mirrors the server's `parsePermission` minus the enum-membership
|
|
28
|
+
* checks (mcp scope / secret key) which are server-authoritative.
|
|
29
|
+
*/
|
|
30
|
+
export function parsePermissionShape(raw) {
|
|
31
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const [head, ...argParts] = raw.split(':');
|
|
34
|
+
const arg = argParts.length > 0 ? argParts.join(':') : undefined;
|
|
35
|
+
const [ns, action] = head.split('.');
|
|
36
|
+
switch (ns) {
|
|
37
|
+
case 'fs':
|
|
38
|
+
if (action !== 'read' && action !== 'readwrite')
|
|
39
|
+
return null;
|
|
40
|
+
if (!arg)
|
|
41
|
+
return null;
|
|
42
|
+
return { raw, namespace: 'fs', action, arg };
|
|
43
|
+
case 'net':
|
|
44
|
+
if (action || !arg)
|
|
45
|
+
return null;
|
|
46
|
+
return { raw, namespace: 'net', arg };
|
|
47
|
+
case 'mcp':
|
|
48
|
+
if (action || !arg)
|
|
49
|
+
return null; // arg = McpScope (membership checked server-side)
|
|
50
|
+
return { raw, namespace: 'mcp', arg };
|
|
51
|
+
case 'llm':
|
|
52
|
+
if (action !== 'invoke' || !arg)
|
|
53
|
+
return null;
|
|
54
|
+
return { raw, namespace: 'llm', action, arg };
|
|
55
|
+
case 'media':
|
|
56
|
+
if (action !== 'upload' && action !== 'download')
|
|
57
|
+
return null;
|
|
58
|
+
if (!arg)
|
|
59
|
+
return null;
|
|
60
|
+
return { raw, namespace: 'media', action, arg };
|
|
61
|
+
case 'secret':
|
|
62
|
+
if (action || !arg)
|
|
63
|
+
return null; // arg = KEY_NAME (preset membership checked server-side)
|
|
64
|
+
return { raw, namespace: 'secret', action: arg };
|
|
65
|
+
case 'device':
|
|
66
|
+
if (!action)
|
|
67
|
+
return null;
|
|
68
|
+
return { raw, namespace: 'device', action, arg };
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validate a parsed-JSON value as a tile-app manifest. Collects EVERY error
|
|
75
|
+
* (not fail-fast) so the author sees all problems at once — matching the
|
|
76
|
+
* server. Returns `{ ok, errors }`.
|
|
77
|
+
*/
|
|
78
|
+
export function validateManifest(raw) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
if (!isObj(raw))
|
|
81
|
+
return { ok: false, errors: ['manifest is not an object'] };
|
|
82
|
+
if (!isStr(raw.id) || !ID_RE.test(raw.id))
|
|
83
|
+
errors.push('id must be a lowercase kebab-case slug (2-64 chars)');
|
|
84
|
+
if (!isStr(raw.version) || !SEMVER_RE.test(raw.version))
|
|
85
|
+
errors.push('version must be semver (e.g. 1.0.0)');
|
|
86
|
+
if (!isStr(raw.displayName))
|
|
87
|
+
errors.push('displayName is required');
|
|
88
|
+
if (!isStr(raw.publisher))
|
|
89
|
+
errors.push('publisher is required');
|
|
90
|
+
if (!isStr(raw.description))
|
|
91
|
+
errors.push('description is required');
|
|
92
|
+
if (!isObj(raw.ui) || !isStr(raw.ui.icon) || !isStr(raw.ui.color) || !isStr(raw.ui.label)) {
|
|
93
|
+
errors.push('ui must have string icon, color, label');
|
|
94
|
+
}
|
|
95
|
+
if (!isObj(raw.surface)) {
|
|
96
|
+
errors.push('surface is required');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (raw.surface.kind !== 'iframe')
|
|
100
|
+
errors.push("surface.kind must be 'iframe' (v1)");
|
|
101
|
+
if (!isStr(raw.surface.entry))
|
|
102
|
+
errors.push('surface.entry is required');
|
|
103
|
+
if (raw.surface.tilePrefersSize !== undefined) {
|
|
104
|
+
const s = raw.surface.tilePrefersSize;
|
|
105
|
+
if (!isObj(s) || typeof s.rowSpan !== 'number' || typeof s.colSpan !== 'number') {
|
|
106
|
+
errors.push('surface.tilePrefersSize must be { rowSpan, colSpan }');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (raw.runtime !== undefined) {
|
|
111
|
+
if (!isObj(raw.runtime)) {
|
|
112
|
+
errors.push('runtime must be an object when present');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (typeof raw.runtime.port !== 'number' || raw.runtime.port <= 0)
|
|
116
|
+
errors.push('runtime.port must be a positive number');
|
|
117
|
+
if (raw.runtime.exec !== undefined && !isStr(raw.runtime.exec))
|
|
118
|
+
errors.push('runtime.exec must be a non-empty string when present');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (raw.image !== undefined) {
|
|
122
|
+
if (!isObj(raw.image)) {
|
|
123
|
+
errors.push('image must be an object when present');
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (!isStr(raw.image.ref))
|
|
127
|
+
errors.push('image.ref is required (registry path without tag)');
|
|
128
|
+
if (!isStr(raw.image.tag))
|
|
129
|
+
errors.push('image.tag is required');
|
|
130
|
+
if (raw.image.digest !== undefined && !isStr(raw.image.digest))
|
|
131
|
+
errors.push('image.digest must be a string when present');
|
|
132
|
+
if (raw.image.sizeBytes !== undefined && (typeof raw.image.sizeBytes !== 'number' || raw.image.sizeBytes < 0)) {
|
|
133
|
+
errors.push('image.sizeBytes must be a non-negative number when present');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (isObj(raw.runtime) && raw.image === undefined && !isStr(raw.runtime.exec)) {
|
|
138
|
+
errors.push('runtime requires either `image` (OCI image, recommended) or `runtime.exec` (bare-process override)');
|
|
139
|
+
}
|
|
140
|
+
const allPerms = [];
|
|
141
|
+
if (!isObj(raw.permissions)) {
|
|
142
|
+
errors.push('permissions is required');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const { required, optional } = raw.permissions;
|
|
146
|
+
if (!Array.isArray(required))
|
|
147
|
+
errors.push('permissions.required must be an array');
|
|
148
|
+
else
|
|
149
|
+
allPerms.push(...required);
|
|
150
|
+
if (optional !== undefined) {
|
|
151
|
+
if (!Array.isArray(optional))
|
|
152
|
+
errors.push('permissions.optional must be an array when present');
|
|
153
|
+
else
|
|
154
|
+
allPerms.push(...optional);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const p of allPerms) {
|
|
158
|
+
if (typeof p !== 'string') {
|
|
159
|
+
errors.push(`permission must be a string: ${JSON.stringify(p)}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const parsed = parsePermissionShape(p);
|
|
163
|
+
if (!parsed) {
|
|
164
|
+
errors.push(`unrecognized permission: '${p}'`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (parsed.namespace === 'net' && (parsed.arg === '*' || parsed.arg?.includes('*'))) {
|
|
168
|
+
errors.push(`net permission must name a specific host (no wildcards): '${p}'`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (raw.category !== undefined && !isStr(raw.category))
|
|
172
|
+
errors.push('category must be a string when present');
|
|
173
|
+
if (raw.screenshots !== undefined && (!Array.isArray(raw.screenshots) || !raw.screenshots.every(isStr))) {
|
|
174
|
+
errors.push('screenshots must be an array of strings when present');
|
|
175
|
+
}
|
|
176
|
+
if (raw.changelog !== undefined && !isStr(raw.changelog))
|
|
177
|
+
errors.push('changelog must be a string when present');
|
|
178
|
+
if (raw.featured !== undefined && typeof raw.featured !== 'boolean')
|
|
179
|
+
errors.push('featured must be a boolean when present');
|
|
180
|
+
if (raw.pricing !== undefined) {
|
|
181
|
+
const p = raw.pricing;
|
|
182
|
+
if (!isObj(p)) {
|
|
183
|
+
errors.push('pricing must be an object when present');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (!['free', 'purchase', 'subscription'].includes(p.model))
|
|
187
|
+
errors.push("pricing.model must be 'free' | 'purchase' | 'subscription'");
|
|
188
|
+
if (!['byo', 'platform-metered', 'hybrid'].includes(p.costMode))
|
|
189
|
+
errors.push("pricing.costMode must be 'byo' | 'platform-metered' | 'hybrid'");
|
|
190
|
+
for (const k of ['priceUsdMonthly', 'priceUsdOnce', 'trialDays']) {
|
|
191
|
+
if (p[k] !== undefined && (typeof p[k] !== 'number' || p[k] < 0))
|
|
192
|
+
errors.push(`pricing.${k} must be a non-negative number when present`);
|
|
193
|
+
}
|
|
194
|
+
if (p.model === 'subscription' && !(typeof p.priceUsdMonthly === 'number' && p.priceUsdMonthly > 0))
|
|
195
|
+
errors.push('pricing.priceUsdMonthly is required (> 0) for a subscription');
|
|
196
|
+
if (p.model === 'purchase' && !(typeof p.priceUsdOnce === 'number' && p.priceUsdOnce > 0))
|
|
197
|
+
errors.push('pricing.priceUsdOnce is required (> 0) for a purchase');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ok: errors.length === 0, errors };
|
|
201
|
+
}
|
|
202
|
+
/** A real OCI content digest — `sha256:<64 hex>` or `sha512:<128 hex>`. Mirrors
|
|
203
|
+
* the publish endpoint's `isValidOciDigest` (a tag / `latest` / short hash is
|
|
204
|
+
* NOT immutable content addressing). */
|
|
205
|
+
export function isValidOciDigest(d) {
|
|
206
|
+
return /^sha256:[a-f0-9]{64}$/.test(d) || /^sha512:[a-f0-9]{128}$/.test(d);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* PUBLISH-gate checks that go beyond the schema `validateManifest` — the extra
|
|
210
|
+
* rules `POST /v1/publisher/publish` enforces, surfaced offline so a clean
|
|
211
|
+
* `validate` predicts a clean publish. Currently: a runtime app (anything with
|
|
212
|
+
* `runtime` OR `image`, matching the endpoint's `hasRuntime`) MUST pin
|
|
213
|
+
* `image.digest` to a valid content digest (`DIGEST_REQUIRED`).
|
|
214
|
+
*/
|
|
215
|
+
export function publishGateErrors(manifest) {
|
|
216
|
+
const errors = [];
|
|
217
|
+
if (!isObj(manifest))
|
|
218
|
+
return errors; // a non-object is reported by validateManifest; don't deref
|
|
219
|
+
const hasRuntime = isObj(manifest.runtime) || isObj(manifest.image);
|
|
220
|
+
if (hasRuntime) {
|
|
221
|
+
const digest = isObj(manifest.image) ? manifest.image.digest : undefined;
|
|
222
|
+
if (!(typeof digest === 'string' && isValidOciDigest(digest))) {
|
|
223
|
+
errors.push('a runtime app must ship a signed image pinned to a valid content digest (image.digest = sha256:<64 hex>)');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return errors;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* True if the manifest declares a server-side runtime. Keyed on `runtime` ONLY
|
|
230
|
+
* — matching the launcher, which serves a static surface bundle for any manifest
|
|
231
|
+
* WITHOUT `runtime` (`common-api/src/services/tileapp/launcher.ts`: `if
|
|
232
|
+
* (!manifest.runtime) → staticSurfaceUrl`). An `image`-only manifest (no
|
|
233
|
+
* `runtime`) is therefore pure-UI in production and STILL needs `surface.entry`
|
|
234
|
+
* on disk — so it must NOT be classified runtime here (else `validate` would
|
|
235
|
+
* skip the bundle check on an app that fails to render in prod).
|
|
236
|
+
*/
|
|
237
|
+
export function isRuntimeManifest(manifest) {
|
|
238
|
+
return isObj(manifest) && isObj(manifest.runtime);
|
|
239
|
+
}
|