@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yolo tileapp validate|dev` — the LOCAL developer harness (M1 slice 6).
|
|
3
|
+
*
|
|
4
|
+
* yolo tileapp validate <manifest.json> [--bundle-dir <dir>]
|
|
5
|
+
* → Offline lint: manifest schema (mirrors the server's validateManifest)
|
|
6
|
+
* + bundle layout (a pure-UI app's `surface.entry` must exist on disk).
|
|
7
|
+
* Exit 0 clean, 65 on validation errors, 64 usage, 66 I/O. Prints every
|
|
8
|
+
* error at once — the same taxonomy `POST /v1/publisher/publish` enforces.
|
|
9
|
+
*
|
|
10
|
+
* yolo tileapp dev <manifest.json> [--port N] [--host H] [--bundle-dir <dir>] [--deny]
|
|
11
|
+
* → Validate, then serve the bundle on a local HTTP server with a MOCK
|
|
12
|
+
* broker at `/__yolo/broker` (POST) so a partner can run + click through
|
|
13
|
+
* their app offline, without production review or a session pod. Allow-all
|
|
14
|
+
* by default; `--deny` makes the mock broker deny every request (to test
|
|
15
|
+
* the app's graceful-degradation path). Long-running until killed.
|
|
16
|
+
*
|
|
17
|
+
* Offline + auth-free by design (unlike sign/publish): a partner iterates here
|
|
18
|
+
* with no token, no SESSION_ID, no network.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import * as http from 'node:http';
|
|
23
|
+
import { createServeHandler } from './serve.js';
|
|
24
|
+
import { validateManifest, isRuntimeManifest, publishGateErrors } from './tileapp-validator.js';
|
|
25
|
+
/** The mock-broker + helper routes the dev server mounts (under a reserved
|
|
26
|
+
* prefix so they can't collide with a real bundle asset path). */
|
|
27
|
+
export const DEV_BROKER_PATH = '/__yolo/broker';
|
|
28
|
+
export const DEV_MANIFEST_PATH = '/__yolo/manifest';
|
|
29
|
+
/** Map a failure kind to a process exit code. */
|
|
30
|
+
export function devExitCode(kind) {
|
|
31
|
+
if (kind === 'usage')
|
|
32
|
+
return 64; // EX_USAGE
|
|
33
|
+
if (kind === 'io')
|
|
34
|
+
return 66; // EX_NOINPUT
|
|
35
|
+
return 65; // EX_DATAERR (validation)
|
|
36
|
+
}
|
|
37
|
+
function readManifestFile(p) {
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return { ok: false, message: `cannot read manifest '${p}': ${e.message}` };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return { ok: true, manifest: JSON.parse(raw) };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { ok: false, message: `manifest '${p}' is not valid JSON: ${e.message}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the static-bundle directory for a pure-UI manifest. Tries, in order:
|
|
54
|
+
* an explicit `--bundle-dir`, `<manifestDir>/bundles/<id>` (the registry layout),
|
|
55
|
+
* then `<manifestDir>` (a flat project where index.html sits beside the
|
|
56
|
+
* manifest). Returns the first that EXISTS and contains `surface.entry`.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveBundleDir(manifestPath, manifest, override) {
|
|
59
|
+
const manifestDir = path.dirname(path.resolve(manifestPath));
|
|
60
|
+
const id = typeof manifest.id === 'string' ? manifest.id : '';
|
|
61
|
+
const entry = manifest.surface?.entry || 'index.html';
|
|
62
|
+
// An EXPLICIT --bundle-dir is authoritative: it's the ONLY candidate, so a
|
|
63
|
+
// missing entry there fails loudly rather than silently validating/serving a
|
|
64
|
+
// stale fallback bundle. Only auto-discover when no override is given.
|
|
65
|
+
const candidates = override
|
|
66
|
+
? [path.resolve(override)]
|
|
67
|
+
: [
|
|
68
|
+
id ? path.join(manifestDir, 'bundles', id) : null,
|
|
69
|
+
manifestDir,
|
|
70
|
+
].filter((c) => !!c);
|
|
71
|
+
for (const dir of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const root = path.resolve(dir);
|
|
74
|
+
if (!fs.statSync(root).isDirectory())
|
|
75
|
+
continue;
|
|
76
|
+
// The entry must stay WITHIN the bundle root — production's bundle route
|
|
77
|
+
// rejects `..`-escaping paths with 403, so an entry like `../index.html`
|
|
78
|
+
// that resolves outside must not pass here.
|
|
79
|
+
const entryAbs = path.resolve(root, entry);
|
|
80
|
+
if (entryAbs !== root && !entryAbs.startsWith(root + path.sep))
|
|
81
|
+
continue;
|
|
82
|
+
// …and it must be a FILE (production 404s a directory entry).
|
|
83
|
+
if (fs.statSync(entryAbs).isFile())
|
|
84
|
+
return { ok: true, dir };
|
|
85
|
+
}
|
|
86
|
+
catch { /* try next */ }
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
candidates,
|
|
91
|
+
message: `could not find bundle entry '${entry}' in any of: ${candidates.join(', ')}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The mock broker's reply to a dev-time capability request. Returns the SAME
|
|
96
|
+
* shape + HTTP status the production broker does (`BrokerResult`:
|
|
97
|
+
* `{ ok: true, payload }` on allow; `{ ok: false, reason, message }` with a 4xx
|
|
98
|
+
* on deny) so an app written against the mock behaves identically against the
|
|
99
|
+
* real host bridge — including the deny/error path. Allow echoes the request as
|
|
100
|
+
* the payload; deny uses `permission-not-granted` (→ 403), matching the real
|
|
101
|
+
* broker's deny-reason → status mapping.
|
|
102
|
+
*/
|
|
103
|
+
export function mockBrokerResponse(body, opts) {
|
|
104
|
+
if (opts.allow) {
|
|
105
|
+
return { httpStatus: 200, result: { ok: true, payload: { echo: body ?? null } } };
|
|
106
|
+
}
|
|
107
|
+
return { httpStatus: 403, result: { ok: false, reason: 'permission-not-granted', message: 'dev-mock: deny-all (--deny)' } };
|
|
108
|
+
}
|
|
109
|
+
export function runTileAppValidate(opts) {
|
|
110
|
+
const read = readManifestFile(opts.manifestPath);
|
|
111
|
+
if (!read.ok)
|
|
112
|
+
return { ok: false, kind: 'io', message: read.message };
|
|
113
|
+
const v = validateManifest(read.manifest);
|
|
114
|
+
const errors = [...v.errors, ...publishGateErrors(read.manifest)];
|
|
115
|
+
// A non-object manifest is fully reported by validateManifest; bail before the
|
|
116
|
+
// bundle/runtime checks (which deref manifest fields) so we never crash on it.
|
|
117
|
+
const manifestIsObject = typeof read.manifest === 'object' && read.manifest !== null && !Array.isArray(read.manifest);
|
|
118
|
+
if (!manifestIsObject) {
|
|
119
|
+
return { ok: false, kind: 'validation', message: `manifest invalid:\n - ${errors.join('\n - ')}` };
|
|
120
|
+
}
|
|
121
|
+
// Bundle layout: a PURE-UI app must ship its `surface.entry` on disk. A
|
|
122
|
+
// runtime/image app's surface comes from its container, so skip the disk check.
|
|
123
|
+
let bundleNote = '';
|
|
124
|
+
if (!isRuntimeManifest(read.manifest)) {
|
|
125
|
+
const resolved = resolveBundleDir(opts.manifestPath, read.manifest, opts.bundleDir);
|
|
126
|
+
if (!resolved.ok)
|
|
127
|
+
errors.push(`bundle: ${resolved.message}`);
|
|
128
|
+
else
|
|
129
|
+
bundleNote = `\n bundle: ${resolved.dir}`;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
bundleNote = '\n runtime app — surface served from its container image (no local bundle checked)';
|
|
133
|
+
}
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
return { ok: false, kind: 'validation', message: `manifest invalid (${errors.length} issue${errors.length === 1 ? '' : 's'}):\n - ${errors.join('\n - ')}` };
|
|
136
|
+
}
|
|
137
|
+
const id = String(read.manifest.id);
|
|
138
|
+
const version = String(read.manifest.version);
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
output: `OK: ${id}@${version} is valid.${bundleNote}\n note: mcp-scope + secret-key NAMES are confirmed server-side at publish.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build the dev HTTP handler: the mock-broker + manifest helper routes, with
|
|
146
|
+
* everything else delegated to the static bundle server. Exported for tests.
|
|
147
|
+
*/
|
|
148
|
+
export function createDevHandler(opts) {
|
|
149
|
+
const staticHandler = createServeHandler({ dir: opts.dir, port: 0, host: '', spa: false, noCache: true });
|
|
150
|
+
return (req, res) => {
|
|
151
|
+
const url = (req.url ?? '/').split('?')[0];
|
|
152
|
+
if (url === DEV_MANIFEST_PATH) {
|
|
153
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
154
|
+
res.end(JSON.stringify(opts.manifest));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (url === DEV_BROKER_PATH) {
|
|
158
|
+
if (req.method !== 'POST') {
|
|
159
|
+
res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8', Allow: 'POST' });
|
|
160
|
+
res.end(JSON.stringify({ error: 'mock broker accepts POST only' }));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const chunks = [];
|
|
164
|
+
req.on('data', (c) => chunks.push(c));
|
|
165
|
+
req.on('end', () => {
|
|
166
|
+
let parsed = null;
|
|
167
|
+
try {
|
|
168
|
+
parsed = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf-8')) : null;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
172
|
+
res.end(JSON.stringify({ ok: false, reason: 'invalid-args', message: 'invalid JSON body' }));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const { httpStatus, result } = mockBrokerResponse(parsed, { allow: opts.allow });
|
|
176
|
+
res.writeHead(httpStatus, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
177
|
+
res.end(JSON.stringify(result));
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
staticHandler(req, res);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function runTileAppDev(opts) {
|
|
185
|
+
const read = readManifestFile(opts.manifestPath);
|
|
186
|
+
if (!read.ok) {
|
|
187
|
+
process.stderr.write(`FAIL [io]: ${read.message}\n`);
|
|
188
|
+
return devExitCode('io');
|
|
189
|
+
}
|
|
190
|
+
// Block on schema AND publish-gate errors before serving — `dev` claims to
|
|
191
|
+
// validate before serving, so it must reject anything publish would (e.g. an
|
|
192
|
+
// image-only manifest missing its pinned digest).
|
|
193
|
+
const gateErrors = [...validateManifest(read.manifest).errors, ...publishGateErrors(read.manifest)];
|
|
194
|
+
if (gateErrors.length) {
|
|
195
|
+
process.stderr.write(`FAIL [validation]: manifest invalid:\n - ${gateErrors.join('\n - ')}\n`);
|
|
196
|
+
return devExitCode('validation');
|
|
197
|
+
}
|
|
198
|
+
if (isRuntimeManifest(read.manifest)) {
|
|
199
|
+
process.stderr.write('FAIL [validation]: `yolo tileapp dev` serves a static (pure-UI) bundle; this manifest declares a runtime/image app — run your container locally and preview it yourself.\n');
|
|
200
|
+
return devExitCode('validation');
|
|
201
|
+
}
|
|
202
|
+
const resolved = resolveBundleDir(opts.manifestPath, read.manifest, opts.bundleDir);
|
|
203
|
+
if (!resolved.ok) {
|
|
204
|
+
process.stderr.write(`FAIL [io]: ${resolved.message}\n`);
|
|
205
|
+
return devExitCode('io');
|
|
206
|
+
}
|
|
207
|
+
const handler = createDevHandler({ dir: resolved.dir, manifest: read.manifest, allow: opts.allow });
|
|
208
|
+
const server = http.createServer(handler);
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
server.on('error', (err) => {
|
|
211
|
+
process.stderr.write(`yolo tileapp dev: ${err.code === 'EADDRINUSE' ? `port ${opts.port} is already in use` : err.message}\n`);
|
|
212
|
+
resolve(70); // EX_SOFTWARE
|
|
213
|
+
});
|
|
214
|
+
server.listen(opts.port, opts.host, () => {
|
|
215
|
+
const entry = read.manifest.surface.entry || 'index.html';
|
|
216
|
+
process.stdout.write(`yolo tileapp dev: serving ${String(read.manifest.id)} from ${resolved.dir}\n`);
|
|
217
|
+
process.stdout.write(` open http://${opts.host}:${opts.port}/${entry}\n`);
|
|
218
|
+
process.stdout.write(` broker POST http://${opts.host}:${opts.port}${DEV_BROKER_PATH} (mock: ${opts.allow ? 'allow-all' : 'deny-all'})\n`);
|
|
219
|
+
// Long-running — never resolve; the process runs until killed.
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|