@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,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.yolo/deploy.json` — the durable project↔hosting link (spec
|
|
3
|
+
* `docs/MANAGED_HOSTING_CLI_SPEC.md` §3).
|
|
4
|
+
*
|
|
5
|
+
* COMMITTED by design (the wrangler-config analog): gitignored `.yolo/`
|
|
6
|
+
* entries are per-machine state; `deploy.json` is durable shared
|
|
7
|
+
* identity so future sessions/teammates/CI ship to the SAME project
|
|
8
|
+
* instead of forking a new slug. No secrets live in it — the credential
|
|
9
|
+
* is the session JWT; `projectId` is non-secret and ownership-checked
|
|
10
|
+
* server-side on every call.
|
|
11
|
+
*
|
|
12
|
+
* Canonical write shape (mirrors `lockfile.ts` write discipline):
|
|
13
|
+
* - JSON, 2-space indent, single trailing `\n`.
|
|
14
|
+
* - Fixed key order per the spec example: $version, projectId, slug,
|
|
15
|
+
* type, build{command,outputDir}, worker{entry,assetsDir},
|
|
16
|
+
* compatibilityFlags, bindings. Absent optional fields are omitted.
|
|
17
|
+
* - Same input → byte-identical output (idempotent re-write).
|
|
18
|
+
*
|
|
19
|
+
* Validation is lenient on PRESENCE (only `$version` is required —
|
|
20
|
+
* `init` may write a partial link before detection fills the rest) but
|
|
21
|
+
* strict on TYPES: a present field with the wrong shape is a structured
|
|
22
|
+
* error, never silently coerced. Unknown keys are tolerated (forward
|
|
23
|
+
* compat — a newer deploy.json must not be rejected by an older CLI) and
|
|
24
|
+
* dropped by the canonical writer, but they are NO LONGER SILENT: each is
|
|
25
|
+
* returned as a non-fatal `warning` so a misspelled or unsupported field
|
|
26
|
+
* (e.g. `compatibilityDate`, which the platform fixes and does not read)
|
|
27
|
+
* surfaces in `yolo deploy validate` instead of misleading the author.
|
|
28
|
+
*/
|
|
29
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
export class DeployConfigError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
errors;
|
|
34
|
+
constructor(message, code, errors) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'DeployConfigError';
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.errors = errors;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
42
|
+
/** Resolve the deploy.json path for a project root. */
|
|
43
|
+
export function deployConfigPath(cwd) {
|
|
44
|
+
return path.join(cwd, '.yolo', 'deploy.json');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read `.yolo/deploy.json` from a project root. Absent file is the
|
|
48
|
+
* non-error `{ ok: true, config: null }` case (detection falls through
|
|
49
|
+
* to heuristics); malformed JSON or schema violations are structured
|
|
50
|
+
* failures so the CLI can print `FAIL [<reason>]` with field paths.
|
|
51
|
+
*/
|
|
52
|
+
export function readDeployConfig(cwd, readFileImpl = defaultReadFile) {
|
|
53
|
+
const filePath = deployConfigPath(cwd);
|
|
54
|
+
const text = readFileImpl(filePath);
|
|
55
|
+
if (text === undefined)
|
|
56
|
+
return { ok: true, config: null, path: filePath, warnings: [] };
|
|
57
|
+
if (text.trim() === '')
|
|
58
|
+
return { ok: true, config: null, path: filePath, warnings: [] };
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
kind: 'malformed',
|
|
68
|
+
path: filePath,
|
|
69
|
+
message: `malformed JSON in ${filePath}: ${msg}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const validated = validateDeployConfig(parsed);
|
|
73
|
+
if (!validated.ok) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
kind: 'invalid',
|
|
77
|
+
path: filePath,
|
|
78
|
+
message: `invalid deploy config at ${filePath}: ` +
|
|
79
|
+
validated.errors.map((e) => `${e.path}: ${e.message}`).join('; '),
|
|
80
|
+
errors: validated.errors,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, config: validated.config, path: filePath, warnings: validated.warnings };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Write `.yolo/deploy.json` in canonical form (2-space JSON, fixed key
|
|
87
|
+
* order, trailing newline). Creates `.yolo/` if missing. Validates
|
|
88
|
+
* before touching disk and throws `DeployConfigError` on an invalid
|
|
89
|
+
* config or fs failure — a half-written link file is worse than none.
|
|
90
|
+
*/
|
|
91
|
+
export function writeDeployConfig(cwd, config) {
|
|
92
|
+
const validated = validateDeployConfig(config);
|
|
93
|
+
if (!validated.ok) {
|
|
94
|
+
throw new DeployConfigError('refusing to write invalid deploy config: ' +
|
|
95
|
+
validated.errors.map((e) => `${e.path}: ${e.message}`).join('; '), 'invalid', validated.errors);
|
|
96
|
+
}
|
|
97
|
+
const filePath = deployConfigPath(cwd);
|
|
98
|
+
const dir = path.dirname(filePath);
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
+
throw new DeployConfigError(`could not create ${dir}: ${msg}`, 'unwritable');
|
|
105
|
+
}
|
|
106
|
+
const text = JSON.stringify(canonicalize(validated.config), null, 2) + '\n';
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(filePath, text, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
112
|
+
throw new DeployConfigError(`could not write ${filePath}: ${msg}`, 'unwritable');
|
|
113
|
+
}
|
|
114
|
+
return filePath;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Schema validation per spec §3. Returns the value typed as
|
|
118
|
+
* `DeployConfig` on success or a full list of structured errors (all
|
|
119
|
+
* violations, not just the first) on failure. Pure — no I/O.
|
|
120
|
+
*/
|
|
121
|
+
export function validateDeployConfig(value) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
124
|
+
return { ok: false, errors: [{ path: '$', message: `must be a JSON object, got ${describe(value)}` }] };
|
|
125
|
+
}
|
|
126
|
+
const obj = value;
|
|
127
|
+
if (obj.$version !== 1) {
|
|
128
|
+
errors.push({ path: '$version', message: `must be the number 1, got ${describe(obj.$version)}` });
|
|
129
|
+
}
|
|
130
|
+
checkOptionalString(obj, 'projectId', errors);
|
|
131
|
+
checkOptionalString(obj, 'slug', errors);
|
|
132
|
+
if (obj.type !== undefined && obj.type !== 'static' && obj.type !== 'worker') {
|
|
133
|
+
errors.push({ path: 'type', message: `must be 'static' or 'worker', got ${describe(obj.type)}` });
|
|
134
|
+
}
|
|
135
|
+
if (obj.build !== undefined) {
|
|
136
|
+
if (!isPlainObject(obj.build)) {
|
|
137
|
+
errors.push({ path: 'build', message: `must be an object, got ${describe(obj.build)}` });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
checkOptionalString(obj.build, 'command', errors, 'build.');
|
|
141
|
+
checkOptionalString(obj.build, 'outputDir', errors, 'build.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (obj.worker !== undefined) {
|
|
145
|
+
if (!isPlainObject(obj.worker)) {
|
|
146
|
+
errors.push({ path: 'worker', message: `must be an object, got ${describe(obj.worker)}` });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
checkOptionalString(obj.worker, 'entry', errors, 'worker.');
|
|
150
|
+
checkOptionalString(obj.worker, 'assetsDir', errors, 'worker.');
|
|
151
|
+
if (obj.worker.prebuilt !== undefined && typeof obj.worker.prebuilt !== 'boolean') {
|
|
152
|
+
errors.push({ path: 'worker.prebuilt', message: `must be a boolean, got ${describe(obj.worker.prebuilt)}` });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (obj.compatibilityFlags !== undefined) {
|
|
157
|
+
if (!Array.isArray(obj.compatibilityFlags)) {
|
|
158
|
+
errors.push({
|
|
159
|
+
path: 'compatibilityFlags',
|
|
160
|
+
message: `must be an array of strings, got ${describe(obj.compatibilityFlags)}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
obj.compatibilityFlags.forEach((flag, i) => {
|
|
165
|
+
if (typeof flag !== 'string' || flag.trim() === '') {
|
|
166
|
+
errors.push({ path: `compatibilityFlags[${i}]`, message: `must be a non-empty string, got ${describe(flag)}` });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (obj.bindings !== undefined) {
|
|
172
|
+
if (!Array.isArray(obj.bindings)) {
|
|
173
|
+
errors.push({ path: 'bindings', message: `must be an array, got ${describe(obj.bindings)}` });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
obj.bindings.forEach((b, i) => {
|
|
177
|
+
if (!isPlainObject(b)) {
|
|
178
|
+
errors.push({ path: `bindings[${i}]`, message: `must be an object, got ${describe(b)}` });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (typeof b.kind !== 'string' || b.kind.trim() === '') {
|
|
182
|
+
errors.push({ path: `bindings[${i}].kind`, message: `must be a non-empty string, got ${describe(b.kind)}` });
|
|
183
|
+
}
|
|
184
|
+
if (typeof b.binding !== 'string' || b.binding.trim() === '') {
|
|
185
|
+
errors.push({ path: `bindings[${i}].binding`, message: `must be a non-empty string, got ${describe(b.binding)}` });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (errors.length > 0)
|
|
191
|
+
return { ok: false, errors };
|
|
192
|
+
return { ok: true, config: obj, warnings: collectUnknownKeyWarnings(obj) };
|
|
193
|
+
}
|
|
194
|
+
// Known schema keys per level. Unknown keys are tolerated (forward compat) but
|
|
195
|
+
// surfaced as warnings so a silently-ignored field doesn't mislead the author.
|
|
196
|
+
// `bindings[]` entries intentionally allow extra keys (provisioning hints), so
|
|
197
|
+
// they're excluded here.
|
|
198
|
+
const KNOWN_TOP_KEYS = new Set(['$version', 'projectId', 'slug', 'type', 'build', 'worker', 'compatibilityFlags', 'bindings']);
|
|
199
|
+
const KNOWN_BUILD_KEYS = new Set(['command', 'outputDir']);
|
|
200
|
+
const KNOWN_WORKER_KEYS = new Set(['entry', 'prebuilt', 'assetsDir']);
|
|
201
|
+
function collectUnknownKeyWarnings(obj) {
|
|
202
|
+
const warnings = [];
|
|
203
|
+
const note = (path, key) => {
|
|
204
|
+
// A targeted hint for the field a field report saw silently swallowed: the
|
|
205
|
+
// Worker compatibility DATE is platform-fixed and not read from deploy.json
|
|
206
|
+
// (only compatibilityFlags is) — see release-service `COMPATIBILITY_DATE`.
|
|
207
|
+
const message = key === 'compatibilityDate'
|
|
208
|
+
? "unknown field 'compatibilityDate' — ignored. The Worker compatibility date is fixed by the platform and is not configurable via deploy.json; use 'compatibilityFlags' for runtime flags (e.g. [\"nodejs_compat\"])."
|
|
209
|
+
: `unknown field '${key}' — ignored (not part of the deploy.json schema; check for a typo)`;
|
|
210
|
+
warnings.push({ path, message });
|
|
211
|
+
};
|
|
212
|
+
for (const k of Object.keys(obj))
|
|
213
|
+
if (!KNOWN_TOP_KEYS.has(k))
|
|
214
|
+
note(k, k);
|
|
215
|
+
if (isPlainObject(obj.build))
|
|
216
|
+
for (const k of Object.keys(obj.build))
|
|
217
|
+
if (!KNOWN_BUILD_KEYS.has(k))
|
|
218
|
+
note(`build.${k}`, k);
|
|
219
|
+
if (isPlainObject(obj.worker))
|
|
220
|
+
for (const k of Object.keys(obj.worker))
|
|
221
|
+
if (!KNOWN_WORKER_KEYS.has(k))
|
|
222
|
+
note(`worker.${k}`, k);
|
|
223
|
+
return warnings;
|
|
224
|
+
}
|
|
225
|
+
// ─── Internals ────────────────────────────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* Reorder to the spec's fixed key order, omitting absent optionals.
|
|
228
|
+
* Binding entries keep `kind`, `binding` first; extra hint keys follow
|
|
229
|
+
* lex-sorted (same rule the plan-file canonicalizer applies to
|
|
230
|
+
* free-form maps). Pure — does not mutate input.
|
|
231
|
+
*/
|
|
232
|
+
function canonicalize(config) {
|
|
233
|
+
const out = { $version: 1 };
|
|
234
|
+
if (config.projectId !== undefined)
|
|
235
|
+
out.projectId = config.projectId;
|
|
236
|
+
if (config.slug !== undefined)
|
|
237
|
+
out.slug = config.slug;
|
|
238
|
+
if (config.type !== undefined)
|
|
239
|
+
out.type = config.type;
|
|
240
|
+
if (config.build !== undefined) {
|
|
241
|
+
const build = {};
|
|
242
|
+
if (config.build.command !== undefined)
|
|
243
|
+
build.command = config.build.command;
|
|
244
|
+
if (config.build.outputDir !== undefined)
|
|
245
|
+
build.outputDir = config.build.outputDir;
|
|
246
|
+
out.build = build;
|
|
247
|
+
}
|
|
248
|
+
if (config.worker !== undefined) {
|
|
249
|
+
const worker = {};
|
|
250
|
+
if (config.worker.entry !== undefined)
|
|
251
|
+
worker.entry = config.worker.entry;
|
|
252
|
+
if (config.worker.prebuilt !== undefined)
|
|
253
|
+
worker.prebuilt = config.worker.prebuilt;
|
|
254
|
+
if (config.worker.assetsDir !== undefined)
|
|
255
|
+
worker.assetsDir = config.worker.assetsDir;
|
|
256
|
+
out.worker = worker;
|
|
257
|
+
}
|
|
258
|
+
if (config.compatibilityFlags !== undefined)
|
|
259
|
+
out.compatibilityFlags = [...config.compatibilityFlags];
|
|
260
|
+
if (config.bindings !== undefined) {
|
|
261
|
+
out.bindings = config.bindings.map((b) => {
|
|
262
|
+
const entry = { kind: b.kind, binding: b.binding };
|
|
263
|
+
for (const key of Object.keys(b).sort()) {
|
|
264
|
+
if (key !== 'kind' && key !== 'binding')
|
|
265
|
+
entry[key] = b[key];
|
|
266
|
+
}
|
|
267
|
+
return entry;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
function checkOptionalString(obj, key, errors, prefix = '') {
|
|
273
|
+
const v = obj[key];
|
|
274
|
+
if (v === undefined)
|
|
275
|
+
return;
|
|
276
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
277
|
+
errors.push({ path: `${prefix}${key}`, message: `must be a non-empty string, got ${describe(v)}` });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function isPlainObject(value) {
|
|
281
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
282
|
+
}
|
|
283
|
+
function describe(value) {
|
|
284
|
+
if (value === null)
|
|
285
|
+
return 'null';
|
|
286
|
+
if (Array.isArray(value))
|
|
287
|
+
return 'array';
|
|
288
|
+
if (typeof value === 'string')
|
|
289
|
+
return `'${value}'`;
|
|
290
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
291
|
+
return String(value);
|
|
292
|
+
return typeof value;
|
|
293
|
+
}
|
|
294
|
+
function defaultReadFile(filePath) {
|
|
295
|
+
try {
|
|
296
|
+
return readFileSync(filePath, 'utf8');
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|