@mediamonks/canopy-cli 0.7.11 → 0.7.13

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.
Files changed (29) hide show
  1. package/dist/{db-8Etjmzxh.mjs → db-DKnJQPm5.mjs} +2 -2
  2. package/dist/{deploy-BCtEY1is.mjs → deploy-CnxJ4TE6.mjs} +10 -3
  3. package/dist/dev-BqLvttKc.mjs +2 -0
  4. package/dist/{dev-DtoZrexp.mjs → dev-CyJT6EfQ.mjs} +7 -6
  5. package/dist/{dev-worker-BZgtEM9V.mjs → dev-worker-IWnHvQ1f.mjs} +42 -7
  6. package/dist/docs/index.json +5 -0
  7. package/dist/docs/platform-google.md +213 -0
  8. package/dist/docs/static-serving.md +14 -9
  9. package/dist/{docs-BV9qhLJx.mjs → docs-BBMVxVta.mjs} +2 -2
  10. package/dist/{egress-CN_axsAN.mjs → egress-BFSwa8Uu.mjs} +1 -1
  11. package/dist/{feedback-Cb8ccQ_R.mjs → feedback-BMX_d4KZ.mjs} +2 -2
  12. package/dist/index.mjs +25 -24
  13. package/dist/{local-C5AhA4GM.mjs → local-BQxsp-ZQ.mjs} +7 -470
  14. package/dist/{login-D7dI8D9N.mjs → login-BDTO3nVw.mjs} +1 -1
  15. package/dist/{logs-DNiZquki.mjs → logs-C53y2eAS.mjs} +1 -1
  16. package/dist/{migrations-C-5x6SYf.mjs → migrations-C6EcKD6a.mjs} +1 -1
  17. package/dist/{open-DRPp4QJg.mjs → open-C5x3tX6I.mjs} +1 -1
  18. package/dist/{package-BBpw-n7k.mjs → package-vJXq4RNN.mjs} +1 -1
  19. package/dist/{projects-C6JQjVqg.mjs → projects-MMbv7MmK.mjs} +1 -1
  20. package/dist/schedule-Cq9K0rnE.mjs +106 -0
  21. package/dist/{skill-DJHlTEhS.mjs → skill-Zq9aIPKf.mjs} +2 -2
  22. package/dist/src-Bu4Ze32I.mjs +470 -0
  23. package/dist/{staging-Bw1D_J8M.mjs → staging-D8G4amjO.mjs} +1 -1
  24. package/dist/{token-D7zrIF8G.mjs → token-BUxgRJVA.mjs} +1 -1
  25. package/dist/{ui-2mrJcjYC.mjs → ui-B1Px5y7q.mjs} +17 -0
  26. package/dist/{upgrade-B86Ao6T5.mjs → upgrade-ES8u8_Pn.mjs} +2 -2
  27. package/dist/{usage-CX1Izj_l.mjs → usage-DjNngyvy.mjs} +1 -1
  28. package/package.json +3 -3
  29. package/dist/dev-BYxSHUH6.mjs +0 -2
@@ -0,0 +1,470 @@
1
+ import { Hono } from "hono";
2
+ //#region ../data-plane/src/blob.ts
3
+ /**
4
+ * /blob — the SDK's file storage over an injectable BlobStore (a bucket
5
+ * plus the project's isolated key prefix). Same single-implementation deal as
6
+ * db.ts: canopy-api and the CLI's local sandbox mount this exact router.
7
+ *
8
+ * Authorization is the HOST's job (canopy-api enforces session-or-signature
9
+ * ahead of this router; the local sandbox has no auth wall). This router only
10
+ * mints download URLs via the injected signer — verification lives with the
11
+ * host's auth middleware (see signing.ts).
12
+ */
13
+ const MAX_BLOB_BYTES = 25 * 1024 * 1024;
14
+ const MAX_LIST_KEYS = 1e3;
15
+ const URL_TTL_SECONDS = 600;
16
+ /** Keys may nest with `/`, but never escape the prefix or surprise a URL. */
17
+ function safeBlobKey(raw) {
18
+ if (raw.length === 0 || raw.length > 512) return null;
19
+ if (raw.includes("\\") || raw.includes("\0")) return null;
20
+ if (raw.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) return null;
21
+ return raw;
22
+ }
23
+ function blobRouter(config) {
24
+ const blob = new Hono();
25
+ const resolve = async (c) => config.store(c);
26
+ blob.put("/blob/:key", async (c) => {
27
+ const resolved = await resolve(c);
28
+ if (resolved instanceof Response) return resolved;
29
+ const { store } = resolved;
30
+ const key = safeBlobKey(c.req.param("key"));
31
+ if (!key) return c.json({ error: "invalid_key" }, 400);
32
+ if (Number(c.req.header("content-length") ?? 0) > MAX_BLOB_BYTES) return c.json({
33
+ error: "blob_too_large",
34
+ maxBytes: MAX_BLOB_BYTES
35
+ }, 413);
36
+ const body = await c.req.raw.arrayBuffer();
37
+ if (body.byteLength > MAX_BLOB_BYTES) return c.json({
38
+ error: "blob_too_large",
39
+ maxBytes: MAX_BLOB_BYTES
40
+ }, 413);
41
+ const contentType = c.req.header("content-type") ?? "application/octet-stream";
42
+ await store.bucket.put(store.prefix + key, body, { httpMetadata: { contentType } });
43
+ return c.json({
44
+ key,
45
+ size: body.byteLength,
46
+ contentType,
47
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
48
+ }, 201);
49
+ });
50
+ blob.get("/blob", async (c) => {
51
+ const resolved = await resolve(c);
52
+ if (resolved instanceof Response) return resolved;
53
+ const { store } = resolved;
54
+ const prefix = c.req.query("prefix") ?? "";
55
+ if (prefix.length > 512 || prefix.includes("\0")) return c.json({ error: "invalid_prefix" }, 400);
56
+ const base = store.prefix;
57
+ const objects = [];
58
+ let cursor;
59
+ do {
60
+ const page = await store.bucket.list({
61
+ prefix: base + prefix,
62
+ cursor,
63
+ include: ["httpMetadata"]
64
+ });
65
+ for (const o of page.objects) objects.push({
66
+ key: o.key.slice(base.length),
67
+ size: o.size,
68
+ contentType: o.httpMetadata?.contentType ?? "application/octet-stream",
69
+ uploadedAt: o.uploaded.toISOString()
70
+ });
71
+ cursor = page.truncated && objects.length < MAX_LIST_KEYS ? page.cursor : void 0;
72
+ } while (cursor);
73
+ return c.json(objects.slice(0, MAX_LIST_KEYS));
74
+ });
75
+ blob.get("/blob/:key/url", async (c) => {
76
+ const resolved = await resolve(c);
77
+ if (resolved instanceof Response) return resolved;
78
+ const { slug, store } = resolved;
79
+ const key = safeBlobKey(c.req.param("key"));
80
+ if (!key) return c.json({ error: "invalid_key" }, 400);
81
+ if (!await store.bucket.head(store.prefix + key)) return c.json({ error: "not_found" }, 404);
82
+ const exp = Math.floor(Date.now() / 1e3) + URL_TTL_SECONDS;
83
+ const sig = await config.signDownload(c, slug, key, exp);
84
+ return c.json({
85
+ url: `/_api/data/v1/blob/${encodeURIComponent(key)}?project=${encodeURIComponent(slug)}&exp=${exp}&sig=${sig}`,
86
+ expiresAt: (/* @__PURE__ */ new Date(exp * 1e3)).toISOString()
87
+ });
88
+ });
89
+ blob.get("/blob/:key", async (c) => {
90
+ const resolved = await resolve(c);
91
+ if (resolved instanceof Response) return resolved;
92
+ const { store } = resolved;
93
+ const key = safeBlobKey(c.req.param("key"));
94
+ if (!key) return c.json({ error: "invalid_key" }, 400);
95
+ const object = await store.bucket.get(store.prefix + key);
96
+ if (!object) return c.json({ error: "not_found" }, 404);
97
+ return new Response(object.body, { headers: {
98
+ "content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
99
+ "cache-control": "private, no-store"
100
+ } });
101
+ });
102
+ blob.delete("/blob/:key", async (c) => {
103
+ const resolved = await resolve(c);
104
+ if (resolved instanceof Response) return resolved;
105
+ const { store } = resolved;
106
+ const key = safeBlobKey(c.req.param("key"));
107
+ if (!key) return c.json({ error: "invalid_key" }, 400);
108
+ const fullKey = store.prefix + key;
109
+ const existed = await store.bucket.head(fullKey) !== null;
110
+ await store.bucket.delete(fullKey);
111
+ return c.json({ deleted: existed });
112
+ });
113
+ return blob;
114
+ }
115
+ //#endregion
116
+ //#region ../data-plane/src/dialect.ts
117
+ /** Platform-managed fields map to real columns instead of JSON extraction. */
118
+ const META_COLUMNS = {
119
+ _id: "id",
120
+ _createdAt: "created_at",
121
+ _updatedAt: "updated_at"
122
+ };
123
+ //#endregion
124
+ //#region ../data-plane/src/schema.ts
125
+ /** Platform/system tables never shown to users. */
126
+ const RESERVED = /^(_canopy_|_cf_|sqlite_|pg_)/;
127
+ /** Single-quoted SQL string literal (introspection args ride inline, not bound). */
128
+ const lit = (value) => `'${value.replaceAll("'", "''")}'`;
129
+ async function sqliteSchema(store) {
130
+ const names = (await store.query(`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)).results.map((r) => String(r.name)).filter((n) => !RESERVED.test(n));
131
+ return Promise.all(names.map(async (name) => {
132
+ const [cols, idxList] = await Promise.all([store.query(`SELECT name, type, "notnull" AS not_null, pk FROM pragma_table_info(${lit(name)})`), store.query(`SELECT name, "unique" AS is_unique FROM pragma_index_list(${lit(name)})`)]);
133
+ const indexes = await Promise.all(idxList.results.filter((idx) => !RESERVED.test(String(idx.name))).map(async (idx) => ({
134
+ name: String(idx.name),
135
+ unique: Boolean(Number(idx.is_unique)),
136
+ columns: (await store.query(`SELECT name FROM pragma_index_info(${lit(String(idx.name))})`)).results.map((r) => String(r.name))
137
+ })));
138
+ return {
139
+ name,
140
+ columns: cols.results.map((col) => ({
141
+ name: String(col.name),
142
+ type: String(col.type),
143
+ nullable: !Number(col.not_null),
144
+ pk: Number(col.pk) > 0
145
+ })),
146
+ indexes
147
+ };
148
+ }));
149
+ }
150
+ async function postgresSchema(store) {
151
+ const [tables, columns, pks, indexes] = await Promise.all([
152
+ store.query(`SELECT table_name AS name FROM information_schema.tables
153
+ WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'
154
+ ORDER BY table_name`),
155
+ store.query(`SELECT table_name, column_name, data_type, is_nullable
156
+ FROM information_schema.columns
157
+ WHERE table_schema = current_schema()
158
+ ORDER BY table_name, ordinal_position`),
159
+ store.query(`SELECT tc.table_name, kcu.column_name
160
+ FROM information_schema.table_constraints tc
161
+ JOIN information_schema.key_column_usage kcu
162
+ ON kcu.constraint_name = tc.constraint_name
163
+ AND kcu.table_schema = tc.table_schema
164
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = current_schema()`),
165
+ store.query(`SELECT t.relname AS table_name, i.relname AS index_name,
166
+ ix.indisunique AS is_unique, a.attname AS column_name
167
+ FROM pg_index ix
168
+ JOIN pg_class i ON i.oid = ix.indexrelid
169
+ JOIN pg_class t ON t.oid = ix.indrelid
170
+ JOIN pg_namespace n ON n.oid = t.relnamespace
171
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
172
+ WHERE n.nspname = current_schema() AND NOT ix.indisprimary`)
173
+ ]);
174
+ const pkSet = new Set(pks.results.map((r) => `${String(r.table_name)}.${String(r.column_name)}`));
175
+ const byTable = /* @__PURE__ */ new Map();
176
+ for (const row of tables.results) {
177
+ const name = String(row.name);
178
+ if (!RESERVED.test(name)) byTable.set(name, {
179
+ name,
180
+ columns: [],
181
+ indexes: []
182
+ });
183
+ }
184
+ for (const col of columns.results) {
185
+ const table = byTable.get(String(col.table_name));
186
+ if (!table) continue;
187
+ table.columns.push({
188
+ name: String(col.column_name),
189
+ type: String(col.data_type),
190
+ nullable: col.is_nullable === "YES",
191
+ pk: pkSet.has(`${String(col.table_name)}.${String(col.column_name)}`)
192
+ });
193
+ }
194
+ for (const idx of indexes.results) {
195
+ const table = byTable.get(String(idx.table_name));
196
+ if (!table) continue;
197
+ const name = String(idx.index_name);
198
+ let index = table.indexes.find((i) => i.name === name);
199
+ if (!index) {
200
+ index = {
201
+ name,
202
+ unique: Boolean(idx.is_unique),
203
+ columns: []
204
+ };
205
+ table.indexes.push(index);
206
+ }
207
+ index.columns.push(String(idx.column_name));
208
+ }
209
+ return [...byTable.values()];
210
+ }
211
+ async function introspectSchema(store, dialect) {
212
+ const [tables, collections] = await Promise.all([dialect === "postgres" ? postgresSchema(store) : sqliteSchema(store), store.query("SELECT collection, COUNT(*) AS docs FROM _canopy_docs GROUP BY collection ORDER BY collection").then((r) => r.results.map((row) => ({
213
+ name: String(row.collection),
214
+ docs: Number(row.docs)
215
+ }))).catch(() => [])]);
216
+ return {
217
+ dialect,
218
+ collections,
219
+ tables
220
+ };
221
+ }
222
+ //#endregion
223
+ //#region ../data-plane/src/db.ts
224
+ /**
225
+ * /db — the SDK's document API + raw-SQL escape hatch, over an injectable
226
+ * DocStore. This is THE data-plane implementation: canopy-api mounts it under
227
+ * /data/v1 against project D1s (or Postgres on GCP), and `canopy dev --local`
228
+ * mounts the very same router against PGlite — one place to change when the
229
+ * API surface moves.
230
+ *
231
+ * Paths carry no project: hosts wrap the router with their own auth/scope
232
+ * middleware, resolve the project themselves (gateway-stamped
233
+ * X-Canopy-Project or token scope — see canopy-api's session.ts), and hand
234
+ * back the store in `config.store` (returning a Response for not-found/paused…).
235
+ */
236
+ const COLLECTION_RE = /^[a-z][a-z0-9_]{0,63}$/;
237
+ const FIELD_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
238
+ const MAX_DOC_BYTES = 256 * 1024;
239
+ const MAX_LIMIT = 1e3;
240
+ /** Row cap for versioned /db/sql responses (the legacy shape is uncapped). */
241
+ const MAX_SQL_ROWS = 1e3;
242
+ /**
243
+ * Statement classifier for read-only /db/sql runs. First keyword must be a
244
+ * read; WITH additionally may not smuggle DML in a CTE (impossible on SQLite,
245
+ * possible on Postgres — where hosts also enforce `SET TRANSACTION READ ONLY`
246
+ * at the engine, making this the belt to that suspender).
247
+ */
248
+ function readOnlySqlViolation(query) {
249
+ const bare = query.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"]|"")*"/g, "\"\"");
250
+ const first = /^\s*([A-Za-z]+)/.exec(bare)?.[1]?.toUpperCase();
251
+ if (first === void 0) return "empty statement";
252
+ if (![
253
+ "SELECT",
254
+ "EXPLAIN",
255
+ "VALUES",
256
+ "WITH"
257
+ ].includes(first)) return `${first} is not allowed in read-only mode`;
258
+ const dml = /\b(INSERT|UPDATE|DELETE|MERGE|REPLACE|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|VACUUM|ATTACH|REINDEX)\b/i.exec(bare);
259
+ if (first === "WITH" && dml) return `${dml[1].toUpperCase()} is not allowed in read-only mode`;
260
+ if (first === "EXPLAIN" && dml && /\bANALYZE\b/i.test(bare)) return "EXPLAIN ANALYZE on a write is not allowed in read-only mode";
261
+ return null;
262
+ }
263
+ function toDoc(row) {
264
+ return {
265
+ ...JSON.parse(row.data),
266
+ _id: row.id,
267
+ _createdAt: row.created_at,
268
+ _updatedAt: row.updated_at
269
+ };
270
+ }
271
+ function isPlainObject(value) {
272
+ return typeof value === "object" && value !== null && !Array.isArray(value);
273
+ }
274
+ /** User fields only — `_`-prefixed keys are platform-managed. */
275
+ function stripMetaKeys(doc) {
276
+ return Object.fromEntries(Object.entries(doc).filter(([key]) => !key.startsWith("_")));
277
+ }
278
+ function isScalar(value) {
279
+ return value === null || [
280
+ "string",
281
+ "number",
282
+ "boolean"
283
+ ].includes(typeof value);
284
+ }
285
+ var BadQuery = class extends Error {};
286
+ /** Validate a where object: known meta keys / dot-path fields, scalar values. */
287
+ function validateWhere(where) {
288
+ for (const [field, value] of Object.entries(where)) {
289
+ if (!isScalar(value)) throw new BadQuery(`where.${field}: only string/number/boolean/null values are supported`);
290
+ if (!META_COLUMNS[field] && !FIELD_RE.test(field)) throw new BadQuery(`where.${field}: invalid field name`);
291
+ }
292
+ return where;
293
+ }
294
+ function intParam(raw, name, fallback, max) {
295
+ if (raw === void 0) return fallback;
296
+ if (!/^\d{1,9}$/.test(raw)) throw new BadQuery(`${name} must be a non-negative integer`);
297
+ return Math.min(Number(raw), max);
298
+ }
299
+ function dbRouter(config) {
300
+ const db = new Hono();
301
+ const resolve = async (c) => config.store(c);
302
+ db.post("/db/sql", async (c) => {
303
+ const resolved = await resolve(c);
304
+ if (resolved instanceof Response) return resolved;
305
+ const { store } = resolved;
306
+ const body = await c.req.json().catch(() => ({}));
307
+ const params = body.params ?? [];
308
+ if (typeof body.query !== "string" || body.query.trim() === "" || !Array.isArray(params) || !params.every(isScalar) || !(body.readonly === void 0 || typeof body.readonly === "boolean")) return c.json({
309
+ error: "invalid_query",
310
+ detail: "expected { query: string, params?: (string|number|boolean|null)[], readonly?: boolean }"
311
+ }, 400);
312
+ if (body.readonly === void 0) {
313
+ const result = await store.query(body.query, params).catch((err) => err);
314
+ if (result instanceof Error) return c.json({
315
+ error: "sql_error",
316
+ detail: result.message
317
+ }, 400);
318
+ return c.json(result.results);
319
+ }
320
+ if (body.readonly) {
321
+ const violation = readOnlySqlViolation(body.query);
322
+ if (violation) return c.json({
323
+ error: "read_only",
324
+ detail: violation
325
+ }, 400);
326
+ }
327
+ const result = await store.query(body.query, params, { readonly: body.readonly }).catch((err) => err);
328
+ if (result instanceof Error) return c.json({
329
+ error: "sql_error",
330
+ detail: result.message
331
+ }, 400);
332
+ if (!body.readonly) await config.onSqlWrite?.(c, body.query);
333
+ const truncated = result.results.length > MAX_SQL_ROWS;
334
+ const rows = truncated ? result.results.slice(0, MAX_SQL_ROWS) : result.results;
335
+ return c.json({
336
+ rows,
337
+ rowCount: rows.length,
338
+ truncated
339
+ });
340
+ });
341
+ db.get("/db/schema", async (c) => {
342
+ const resolved = await resolve(c);
343
+ if (resolved instanceof Response) return resolved;
344
+ const schema = await introspectSchema(resolved.store, resolved.dialect.name).catch((err) => err);
345
+ if (schema instanceof Error) return c.json({
346
+ error: "sql_error",
347
+ detail: schema.message
348
+ }, 400);
349
+ return c.json(schema);
350
+ });
351
+ db.post("/db/:collection", async (c) => {
352
+ const resolved = await resolve(c);
353
+ if (resolved instanceof Response) return resolved;
354
+ const { store, dialect } = resolved;
355
+ const collection = c.req.param("collection");
356
+ if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
357
+ const body = await c.req.json().catch(() => void 0);
358
+ if (!isPlainObject(body)) return c.json({
359
+ error: "invalid_document",
360
+ detail: "expected a JSON object"
361
+ }, 400);
362
+ const fields = stripMetaKeys(body);
363
+ const data = JSON.stringify(fields);
364
+ if (data.length > MAX_DOC_BYTES) return c.json({
365
+ error: "document_too_large",
366
+ maxBytes: MAX_DOC_BYTES
367
+ }, 413);
368
+ const id = crypto.randomUUID();
369
+ const now = (/* @__PURE__ */ new Date()).toISOString();
370
+ const insert = dialect.insert(collection, id, data, now);
371
+ await store.query(insert.sql, insert.params);
372
+ return c.json({
373
+ ...fields,
374
+ _id: id,
375
+ _createdAt: now,
376
+ _updatedAt: now
377
+ }, 201);
378
+ });
379
+ db.get("/db/:collection", async (c) => {
380
+ const resolved = await resolve(c);
381
+ if (resolved instanceof Response) return resolved;
382
+ const { store, dialect } = resolved;
383
+ const collection = c.req.param("collection");
384
+ if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
385
+ const q = c.req.query();
386
+ try {
387
+ const limit = intParam(q.limit, "limit", 100, MAX_LIMIT);
388
+ const offset = intParam(q.offset, "offset", 0, 1e9);
389
+ let whereRaw = {};
390
+ if (q.where !== void 0) {
391
+ try {
392
+ whereRaw = JSON.parse(q.where);
393
+ } catch {
394
+ throw new BadQuery("where must be a JSON object");
395
+ }
396
+ if (!isPlainObject(whereRaw)) throw new BadQuery("where must be a JSON object");
397
+ }
398
+ const where = validateWhere(whereRaw);
399
+ let query;
400
+ if (q.search !== void 0) {
401
+ query = dialect.search({
402
+ collection,
403
+ text: q.search,
404
+ where,
405
+ limit,
406
+ offset
407
+ });
408
+ if (query === null) return c.json([]);
409
+ } else {
410
+ if (q.orderBy !== void 0 && !META_COLUMNS[q.orderBy] && !FIELD_RE.test(q.orderBy)) throw new BadQuery("orderBy: invalid field name");
411
+ if (q.order !== void 0 && q.order !== "asc" && q.order !== "desc") throw new BadQuery("order must be 'asc' or 'desc'");
412
+ query = dialect.find({
413
+ collection,
414
+ where,
415
+ orderBy: q.orderBy,
416
+ order: q.order === "desc" ? "desc" : "asc",
417
+ limit,
418
+ offset
419
+ });
420
+ }
421
+ const result = await store.query(query.sql, query.params);
422
+ return c.json(result.results.map(toDoc));
423
+ } catch (err) {
424
+ if (err instanceof BadQuery) return c.json({
425
+ error: "invalid_query",
426
+ detail: err.message
427
+ }, 400);
428
+ throw err;
429
+ }
430
+ });
431
+ db.get("/db/:collection/:id", async (c) => {
432
+ const resolved = await resolve(c);
433
+ if (resolved instanceof Response) return resolved;
434
+ const { store, dialect } = resolved;
435
+ const collection = c.req.param("collection");
436
+ if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
437
+ const get = dialect.get(collection, c.req.param("id"));
438
+ const row = (await store.query(get.sql, get.params)).results[0];
439
+ return c.json(row ? toDoc(row) : null);
440
+ });
441
+ db.patch("/db/:collection/:id", async (c) => {
442
+ const resolved = await resolve(c);
443
+ if (resolved instanceof Response) return resolved;
444
+ const { store, dialect } = resolved;
445
+ const collection = c.req.param("collection");
446
+ if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
447
+ const body = await c.req.json().catch(() => void 0);
448
+ if (!isPlainObject(body)) return c.json({
449
+ error: "invalid_patch",
450
+ detail: "expected a JSON object"
451
+ }, 400);
452
+ const patch = dialect.patch(collection, c.req.param("id"), JSON.stringify(stripMetaKeys(body)), (/* @__PURE__ */ new Date()).toISOString());
453
+ const row = (await store.query(patch.sql, patch.params)).results[0];
454
+ if (!row) return c.json({ error: "not_found" }, 404);
455
+ return c.json(toDoc(row));
456
+ });
457
+ db.delete("/db/:collection/:id", async (c) => {
458
+ const resolved = await resolve(c);
459
+ if (resolved instanceof Response) return resolved;
460
+ const { store, dialect } = resolved;
461
+ const collection = c.req.param("collection");
462
+ if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
463
+ const del = dialect.del(collection, c.req.param("id"));
464
+ const result = await store.query(del.sql, del.params);
465
+ return c.json({ deleted: (result.meta.changes ?? 0) > 0 });
466
+ });
467
+ return db;
468
+ }
469
+ //#endregion
470
+ export { META_COLUMNS as n, blobRouter as r, dbRouter as t };
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty, u as withContext } from "./ui-2mrJcjYC.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty, u as withContext } from "./ui-B1Px5y7q.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/staging.ts
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors } from "./ui-2mrJcjYC.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-B1Px5y7q.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -114,6 +114,22 @@ const ERROR_COPY = {
114
114
  why: "the report already has the maximum number of screenshots",
115
115
  fix: "Remove one from the report on the dashboard before attaching another."
116
116
  },
117
+ invalid_name: {
118
+ why: "the schedule name is not a valid format",
119
+ fix: "Use lowercase letters, digits and hyphens, starting with a letter (e.g. refresh-feeds)."
120
+ },
121
+ invalid_cron: {
122
+ why: "the cron expression is not a valid five-field cron",
123
+ fix: "Use five space-separated fields, quoted — minute hour day month weekday (e.g. \"0 5 * * *\")."
124
+ },
125
+ too_many_schedules: {
126
+ why: "the project is at its schedule limit",
127
+ fix: "Remove a schedule you no longer need (`canopy schedule list` / `canopy schedule remove`)."
128
+ },
129
+ scheduler_sync_failed: {
130
+ why: "the schedule was saved but syncing it to the platform scheduler failed",
131
+ fix: "Retry the same command — it re-syncs the saved schedule."
132
+ },
117
133
  worker_egress_sync_failed: {
118
134
  why: "the list was saved but redeploying the running worker with it failed",
119
135
  fix: "Retry, or redeploy the project (`canopy deploy`) to apply the new allowlist."
@@ -221,6 +237,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
221
237
  body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
222
238
  });
223
239
  if (!response.ok) return fail(response, isDeployToken, method, path);
240
+ if (response.status === 204) return void 0;
224
241
  return await response.json();
225
242
  }
226
243
  /**
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { n as reportErrors } from "./ui-2mrJcjYC.mjs";
2
+ import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
112
112
  const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
113
113
  const command = pmCommand(pm);
114
114
  const cmd = installArgs(pm, spec);
115
- const { skillInstalled } = await import("./skill-DJHlTEhS.mjs");
115
+ const { skillInstalled } = await import("./skill-Zq9aIPKf.mjs");
116
116
  const refreshSkill = skillInstalled(process.cwd());
117
117
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
118
118
  await run(command, cmd);
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.mjs";
2
+ import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/usage.ts
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -20,8 +20,8 @@
20
20
  "esbuild": "^0.28.1",
21
21
  "hono": "^4.8.0",
22
22
  "vite-server": "npm:vite@^7",
23
- "@mediamonks/canopy": "0.0.4",
24
- "@mediamonks/canopy-data-plane": "0.0.3"
23
+ "@mediamonks/canopy": "0.0.5",
24
+ "@mediamonks/canopy-data-plane": "0.0.4"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.1",
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-DtoZrexp.mjs";
2
- export { devCommand };