@mediamonks/canopy-cli 0.7.7 → 0.7.9
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/{db-D3RKaLVU.mjs → db-8Etjmzxh.mjs} +2 -2
- package/dist/{deploy-ClioT9ZC.mjs → deploy-y75cgauK.mjs} +2 -2
- package/dist/dev-BYxSHUH6.mjs +2 -0
- package/dist/{dev-BbAVZcAR.mjs → dev-DtoZrexp.mjs} +63 -8
- package/dist/dev-worker-BZgtEM9V.mjs +313 -0
- package/dist/docs/cli-dev.md +19 -0
- package/dist/docs/cli-feedback.md +38 -14
- package/dist/docs/dev.md +19 -0
- package/dist/docs/overview-how-it-works.md +2 -1
- package/dist/docs/platform-client-access.md +2 -1
- package/dist/docs/platform-feedback.md +46 -11
- package/dist/docs/platform-members-and-access.md +2 -2
- package/dist/docs/platform-project-data.md +19 -9
- package/dist/{docs-DOiL2Sw-.mjs → docs-VmcoH1An.mjs} +2 -2
- package/dist/{egress-BkkG9Jzs.mjs → egress-CN_axsAN.mjs} +1 -1
- package/dist/feedback-DUuWXS_D.mjs +390 -0
- package/dist/index.mjs +24 -24
- package/dist/{local-BHxSZwUT.mjs → local-C5AhA4GM.mjs} +160 -6
- package/dist/{login-C1x1l3qg.mjs → login-D7dI8D9N.mjs} +28 -12
- package/dist/{logs-DuzM-_Rn.mjs → logs-DNiZquki.mjs} +1 -1
- package/dist/{migrations-DpWHuqkQ.mjs → migrations-C-5x6SYf.mjs} +1 -1
- package/dist/{open-CLSh6oez.mjs → open-DRPp4QJg.mjs} +1 -1
- package/dist/{package-BNiOibhj.mjs → package-CGrKr6IT.mjs} +1 -1
- package/dist/{projects-C9TBoiW7.mjs → projects-C6JQjVqg.mjs} +1 -1
- package/dist/{skill-DRTg-mJR.mjs → skill-CZP1AqQX.mjs} +2 -2
- package/dist/{staging-95xeXyZN.mjs → staging-Bw1D_J8M.mjs} +1 -1
- package/dist/templates/worker/.dev.vars.example +9 -0
- package/dist/templates/worker/AGENTS.md +7 -1
- package/dist/templates/worker/README.md +4 -1
- package/dist/templates/worker/_gitignore +1 -0
- package/dist/{token-BjlqT2Xj.mjs → token-D7zrIF8G.mjs} +1 -1
- package/dist/{ui-CpdtK7mt.mjs → ui-2mrJcjYC.mjs} +30 -1
- package/dist/{upgrade-BDaM4M6Y.mjs → upgrade-CmhSO_r6.mjs} +2 -2
- package/dist/{usage-wpew0a2M.mjs → usage-CX1Izj_l.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-D6Sy15Wv.mjs +0 -2
- package/dist/feedback-BZG8E8zG.mjs +0 -258
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as apiDataRequest } from "./ui-
|
|
2
|
-
import { n as rewriteApiPath } from "./dev-
|
|
1
|
+
import { o as apiDataRequest } from "./ui-2mrJcjYC.mjs";
|
|
2
|
+
import { n as rewriteApiPath } from "./dev-DtoZrexp.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -126,6 +126,105 @@ const META_COLUMNS = {
|
|
|
126
126
|
_updatedAt: "updated_at"
|
|
127
127
|
};
|
|
128
128
|
//#endregion
|
|
129
|
+
//#region ../data-plane/src/schema.ts
|
|
130
|
+
/** Platform/system tables never shown to users. */
|
|
131
|
+
const RESERVED = /^(_canopy_|_cf_|sqlite_|pg_)/;
|
|
132
|
+
/** Single-quoted SQL string literal (introspection args ride inline, not bound). */
|
|
133
|
+
const lit = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
134
|
+
async function sqliteSchema(store) {
|
|
135
|
+
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));
|
|
136
|
+
return Promise.all(names.map(async (name) => {
|
|
137
|
+
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)})`)]);
|
|
138
|
+
const indexes = await Promise.all(idxList.results.filter((idx) => !RESERVED.test(String(idx.name))).map(async (idx) => ({
|
|
139
|
+
name: String(idx.name),
|
|
140
|
+
unique: Boolean(Number(idx.is_unique)),
|
|
141
|
+
columns: (await store.query(`SELECT name FROM pragma_index_info(${lit(String(idx.name))})`)).results.map((r) => String(r.name))
|
|
142
|
+
})));
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
columns: cols.results.map((col) => ({
|
|
146
|
+
name: String(col.name),
|
|
147
|
+
type: String(col.type),
|
|
148
|
+
nullable: !Number(col.not_null),
|
|
149
|
+
pk: Number(col.pk) > 0
|
|
150
|
+
})),
|
|
151
|
+
indexes
|
|
152
|
+
};
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
async function postgresSchema(store) {
|
|
156
|
+
const [tables, columns, pks, indexes] = await Promise.all([
|
|
157
|
+
store.query(`SELECT table_name AS name FROM information_schema.tables
|
|
158
|
+
WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'
|
|
159
|
+
ORDER BY table_name`),
|
|
160
|
+
store.query(`SELECT table_name, column_name, data_type, is_nullable
|
|
161
|
+
FROM information_schema.columns
|
|
162
|
+
WHERE table_schema = current_schema()
|
|
163
|
+
ORDER BY table_name, ordinal_position`),
|
|
164
|
+
store.query(`SELECT tc.table_name, kcu.column_name
|
|
165
|
+
FROM information_schema.table_constraints tc
|
|
166
|
+
JOIN information_schema.key_column_usage kcu
|
|
167
|
+
ON kcu.constraint_name = tc.constraint_name
|
|
168
|
+
AND kcu.table_schema = tc.table_schema
|
|
169
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = current_schema()`),
|
|
170
|
+
store.query(`SELECT t.relname AS table_name, i.relname AS index_name,
|
|
171
|
+
ix.indisunique AS is_unique, a.attname AS column_name
|
|
172
|
+
FROM pg_index ix
|
|
173
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
174
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
175
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
176
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
177
|
+
WHERE n.nspname = current_schema() AND NOT ix.indisprimary`)
|
|
178
|
+
]);
|
|
179
|
+
const pkSet = new Set(pks.results.map((r) => `${String(r.table_name)}.${String(r.column_name)}`));
|
|
180
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const row of tables.results) {
|
|
182
|
+
const name = String(row.name);
|
|
183
|
+
if (!RESERVED.test(name)) byTable.set(name, {
|
|
184
|
+
name,
|
|
185
|
+
columns: [],
|
|
186
|
+
indexes: []
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const col of columns.results) {
|
|
190
|
+
const table = byTable.get(String(col.table_name));
|
|
191
|
+
if (!table) continue;
|
|
192
|
+
table.columns.push({
|
|
193
|
+
name: String(col.column_name),
|
|
194
|
+
type: String(col.data_type),
|
|
195
|
+
nullable: col.is_nullable === "YES",
|
|
196
|
+
pk: pkSet.has(`${String(col.table_name)}.${String(col.column_name)}`)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
for (const idx of indexes.results) {
|
|
200
|
+
const table = byTable.get(String(idx.table_name));
|
|
201
|
+
if (!table) continue;
|
|
202
|
+
const name = String(idx.index_name);
|
|
203
|
+
let index = table.indexes.find((i) => i.name === name);
|
|
204
|
+
if (!index) {
|
|
205
|
+
index = {
|
|
206
|
+
name,
|
|
207
|
+
unique: Boolean(idx.is_unique),
|
|
208
|
+
columns: []
|
|
209
|
+
};
|
|
210
|
+
table.indexes.push(index);
|
|
211
|
+
}
|
|
212
|
+
index.columns.push(String(idx.column_name));
|
|
213
|
+
}
|
|
214
|
+
return [...byTable.values()];
|
|
215
|
+
}
|
|
216
|
+
async function introspectSchema(store, dialect) {
|
|
217
|
+
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) => ({
|
|
218
|
+
name: String(row.collection),
|
|
219
|
+
docs: Number(row.docs)
|
|
220
|
+
}))).catch(() => [])]);
|
|
221
|
+
return {
|
|
222
|
+
dialect,
|
|
223
|
+
collections,
|
|
224
|
+
tables
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
129
228
|
//#region ../data-plane/src/db.ts
|
|
130
229
|
/**
|
|
131
230
|
* /db — the SDK's document API + raw-SQL escape hatch, over an injectable
|
|
@@ -143,6 +242,29 @@ const COLLECTION_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
|
143
242
|
const FIELD_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
144
243
|
const MAX_DOC_BYTES = 256 * 1024;
|
|
145
244
|
const MAX_LIMIT = 1e3;
|
|
245
|
+
/** Row cap for versioned /db/sql responses (the legacy shape is uncapped). */
|
|
246
|
+
const MAX_SQL_ROWS = 1e3;
|
|
247
|
+
/**
|
|
248
|
+
* Statement classifier for read-only /db/sql runs. First keyword must be a
|
|
249
|
+
* read; WITH additionally may not smuggle DML in a CTE (impossible on SQLite,
|
|
250
|
+
* possible on Postgres — where hosts also enforce `SET TRANSACTION READ ONLY`
|
|
251
|
+
* at the engine, making this the belt to that suspender).
|
|
252
|
+
*/
|
|
253
|
+
function readOnlySqlViolation(query) {
|
|
254
|
+
const bare = query.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"]|"")*"/g, "\"\"");
|
|
255
|
+
const first = /^\s*([A-Za-z]+)/.exec(bare)?.[1]?.toUpperCase();
|
|
256
|
+
if (first === void 0) return "empty statement";
|
|
257
|
+
if (![
|
|
258
|
+
"SELECT",
|
|
259
|
+
"EXPLAIN",
|
|
260
|
+
"VALUES",
|
|
261
|
+
"WITH"
|
|
262
|
+
].includes(first)) return `${first} is not allowed in read-only mode`;
|
|
263
|
+
const dml = /\b(INSERT|UPDATE|DELETE|MERGE|REPLACE|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|VACUUM|ATTACH|REINDEX)\b/i.exec(bare);
|
|
264
|
+
if (first === "WITH" && dml) return `${dml[1].toUpperCase()} is not allowed in read-only mode`;
|
|
265
|
+
if (first === "EXPLAIN" && dml && /\bANALYZE\b/i.test(bare)) return "EXPLAIN ANALYZE on a write is not allowed in read-only mode";
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
146
268
|
function toDoc(row) {
|
|
147
269
|
return {
|
|
148
270
|
...JSON.parse(row.data),
|
|
@@ -188,16 +310,48 @@ function dbRouter(config) {
|
|
|
188
310
|
const { store } = resolved;
|
|
189
311
|
const body = await c.req.json().catch(() => ({}));
|
|
190
312
|
const params = body.params ?? [];
|
|
191
|
-
if (typeof body.query !== "string" || body.query.trim() === "" || !Array.isArray(params) || !params.every(isScalar)) return c.json({
|
|
313
|
+
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({
|
|
192
314
|
error: "invalid_query",
|
|
193
|
-
detail: "expected { query: string, params?: (string|number|boolean|null)[] }"
|
|
315
|
+
detail: "expected { query: string, params?: (string|number|boolean|null)[], readonly?: boolean }"
|
|
194
316
|
}, 400);
|
|
195
|
-
|
|
317
|
+
if (body.readonly === void 0) {
|
|
318
|
+
const result = await store.query(body.query, params).catch((err) => err);
|
|
319
|
+
if (result instanceof Error) return c.json({
|
|
320
|
+
error: "sql_error",
|
|
321
|
+
detail: result.message
|
|
322
|
+
}, 400);
|
|
323
|
+
return c.json(result.results);
|
|
324
|
+
}
|
|
325
|
+
if (body.readonly) {
|
|
326
|
+
const violation = readOnlySqlViolation(body.query);
|
|
327
|
+
if (violation) return c.json({
|
|
328
|
+
error: "read_only",
|
|
329
|
+
detail: violation
|
|
330
|
+
}, 400);
|
|
331
|
+
}
|
|
332
|
+
const result = await store.query(body.query, params, { readonly: body.readonly }).catch((err) => err);
|
|
196
333
|
if (result instanceof Error) return c.json({
|
|
197
334
|
error: "sql_error",
|
|
198
335
|
detail: result.message
|
|
199
336
|
}, 400);
|
|
200
|
-
|
|
337
|
+
if (!body.readonly) await config.onSqlWrite?.(c, body.query);
|
|
338
|
+
const truncated = result.results.length > MAX_SQL_ROWS;
|
|
339
|
+
const rows = truncated ? result.results.slice(0, MAX_SQL_ROWS) : result.results;
|
|
340
|
+
return c.json({
|
|
341
|
+
rows,
|
|
342
|
+
rowCount: rows.length,
|
|
343
|
+
truncated
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
db.get("/db/schema", async (c) => {
|
|
347
|
+
const resolved = await resolve(c);
|
|
348
|
+
if (resolved instanceof Response) return resolved;
|
|
349
|
+
const schema = await introspectSchema(resolved.store, resolved.dialect.name).catch((err) => err);
|
|
350
|
+
if (schema instanceof Error) return c.json({
|
|
351
|
+
error: "sql_error",
|
|
352
|
+
detail: schema.message
|
|
353
|
+
}, 400);
|
|
354
|
+
return c.json(schema);
|
|
201
355
|
});
|
|
202
356
|
db.post("/db/:collection", async (c) => {
|
|
203
357
|
const resolved = await resolve(c);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as getBakedProfile, d as clearAllProfileTokens, f as listBakedProfiles, g as writeConfigFile, h as upsertProfileAuth, m as setActiveProfile, n as siteConfig, p as readConfigFile, v as isProfileId } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { n as reportErrors, t as openBrowser } from "./ui-
|
|
2
|
+
import { n as reportErrors, t as openBrowser } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -19,27 +19,43 @@ function loginCallbackPage(ok) {
|
|
|
19
19
|
<title>${title} · Canopy</title>
|
|
20
20
|
<style>
|
|
21
21
|
:root {
|
|
22
|
-
color-scheme: light;
|
|
22
|
+
color-scheme: light dark;
|
|
23
23
|
--ink: #14201a;
|
|
24
24
|
--muted: #4a5c52;
|
|
25
25
|
--line: #d5ddd7;
|
|
26
26
|
--panel: rgba(255, 255, 255, 0.82);
|
|
27
|
+
--field: rgba(255, 255, 255, 0.7);
|
|
27
28
|
--accent: #2f6b4f;
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
29
|
+
--mark-ink: #f4faf6;
|
|
30
|
+
--shadow: 0 18px 40px -28px rgba(20, 32, 26, 0.45);
|
|
31
|
+
--bg: radial-gradient(900px 480px at 12% -10%, #dfece3, transparent 60%),
|
|
32
|
+
radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%),
|
|
33
|
+
linear-gradient(165deg, #e8f0ea, #f7f5f0 55%, #eef2ef);
|
|
31
34
|
--mark-bg: ${ok ? "var(--accent)" : "#8a4b3a"};
|
|
32
35
|
}
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root {
|
|
38
|
+
--ink: #e9f0ec;
|
|
39
|
+
--muted: #a2b3aa;
|
|
40
|
+
--line: rgba(233, 240, 236, 0.14);
|
|
41
|
+
--panel: rgba(24, 31, 27, 0.72);
|
|
42
|
+
--field: rgba(233, 240, 236, 0.06);
|
|
43
|
+
--accent: #7cc39d;
|
|
44
|
+
--mark-ink: #0d1410;
|
|
45
|
+
--shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.7);
|
|
46
|
+
--bg: radial-gradient(900px 480px at 12% -10%, #16261d, transparent 60%),
|
|
47
|
+
radial-gradient(700px 420px at 100% 0%, #16221c, transparent 55%),
|
|
48
|
+
linear-gradient(165deg, #0e1512, #121a15 55%, #0d130f);
|
|
49
|
+
--mark-bg: ${ok ? "var(--accent)" : "#e0a292"};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
* { box-sizing: border-box; }
|
|
34
53
|
body {
|
|
35
54
|
margin: 0;
|
|
36
55
|
min-height: 100vh;
|
|
37
56
|
font-family: "Segoe UI", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif;
|
|
38
57
|
color: var(--ink);
|
|
39
|
-
background:
|
|
40
|
-
radial-gradient(900px 480px at 12% -10%, var(--bg2), transparent 60%),
|
|
41
|
-
radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%),
|
|
42
|
-
linear-gradient(165deg, var(--bg0), var(--bg1) 55%, #eef2ef);
|
|
58
|
+
background: var(--bg);
|
|
43
59
|
display: grid;
|
|
44
60
|
place-items: center;
|
|
45
61
|
padding: 1.5rem;
|
|
@@ -51,7 +67,7 @@ function loginCallbackPage(ok) {
|
|
|
51
67
|
border-radius: 1.25rem;
|
|
52
68
|
padding: 2rem 1.75rem 1.75rem;
|
|
53
69
|
backdrop-filter: blur(10px);
|
|
54
|
-
box-shadow:
|
|
70
|
+
box-shadow: var(--shadow);
|
|
55
71
|
}
|
|
56
72
|
.mark {
|
|
57
73
|
width: 2.75rem;
|
|
@@ -60,7 +76,7 @@ function loginCallbackPage(ok) {
|
|
|
60
76
|
display: grid;
|
|
61
77
|
place-items: center;
|
|
62
78
|
background: var(--mark-bg);
|
|
63
|
-
color:
|
|
79
|
+
color: var(--mark-ink);
|
|
64
80
|
margin-bottom: 1.25rem;
|
|
65
81
|
}
|
|
66
82
|
.mark svg { width: 1.4rem; height: 1.4rem; display: block; }
|
|
@@ -98,7 +114,7 @@ function loginCallbackPage(ok) {
|
|
|
98
114
|
padding: 0.1em 0.35em;
|
|
99
115
|
border: 1px solid var(--line);
|
|
100
116
|
border-radius: 0.35rem;
|
|
101
|
-
background:
|
|
117
|
+
background: var(--field);
|
|
102
118
|
}
|
|
103
119
|
</style>
|
|
104
120
|
</head>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { c as apiRequest,
|
|
2
|
+
import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/logs.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { o as apiDataRequest } from "./ui-
|
|
2
|
+
import { o as apiDataRequest } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
//#region src/migrations.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, l as stagingUrl, s as projectUrl } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { n as reportErrors, t as openBrowser } from "./ui-
|
|
2
|
+
import { n as reportErrors, t as openBrowser } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/open.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-
|
|
2
|
+
import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, relative, resolve, sep } from "node:path";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
2
|
-
import { n as reportErrors } from "./ui-
|
|
1
|
+
import { t as version } from "./package-CGrKr6IT.mjs";
|
|
2
|
+
import { n as reportErrors } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, relative } from "node:path";
|
|
@@ -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,
|
|
2
|
+
import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty, u as withContext } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
//#region src/staging.ts
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Local worker secrets for `canopy dev` — copy to .dev.vars (gitignored).
|
|
2
|
+
#
|
|
3
|
+
# Dashboard secrets (Settings → Secrets) are never downloaded: the platform
|
|
4
|
+
# can't hand a stored value back, and live keys don't belong on a laptop. Set
|
|
5
|
+
# local stand-ins here; the worker reads them as c.env.NAME, exactly as it
|
|
6
|
+
# reads the dashboard's in production. `canopy dev` prints which dashboard
|
|
7
|
+
# secret names this file is missing.
|
|
8
|
+
#
|
|
9
|
+
# MY_API_KEY=local-test-key
|
|
@@ -28,6 +28,10 @@ the [Canopy dashboard](https://dashboard.thanarie.dev).
|
|
|
28
28
|
`event.name`). It runs behind the Canopy gateway (HTTP requests arrive
|
|
29
29
|
authenticated) and reaches the platform through a service binding — same
|
|
30
30
|
SDK, same budgets and data isolation as the browser.
|
|
31
|
+
- `canopy dev` serves the worker as well: `/api/*` goes to
|
|
32
|
+
`worker/index.ts` with the same `env` it gets deployed, no build step. Tick a
|
|
33
|
+
schedule by hand with
|
|
34
|
+
`curl -X POST localhost:5173/__canopy/scheduled -d '{"name":"<schedule>"}'`.
|
|
31
35
|
- The SDK comes from npm (`@mediamonks/canopy`) — the browser's shared
|
|
32
36
|
instance lives in `src/canopy.ts`.
|
|
33
37
|
- `canopy dev` uses LIVE project data: read freely, but take a
|
|
@@ -50,7 +54,9 @@ the [Canopy dashboard](https://dashboard.thanarie.dev).
|
|
|
50
54
|
reaches this app or the Worker.
|
|
51
55
|
- Do not store secrets in the repo or SPA. Set them in the Canopy dashboard
|
|
52
56
|
(Settings → Secrets); the Worker reads them as `c.env.SECRET_NAME`
|
|
53
|
-
(Workers-shaped bindings — not `process.env`).
|
|
57
|
+
(Workers-shaped bindings — not `process.env`). Locally, put stand-in values
|
|
58
|
+
in `.dev.vars` (gitignored, see `.dev.vars.example`) — dashboard values are
|
|
59
|
+
never downloaded, and `canopy dev` prints which names are still missing. Platform access still goes
|
|
54
60
|
through the Canopy SDK (`canopy.auth`, `canopy.db`, `canopy.blob`,
|
|
55
61
|
`canopy.ai`) in both the browser and the Worker.
|
|
56
62
|
- The Worker is the only backend: no SSR, no long-running processes, no
|
|
@@ -5,7 +5,10 @@ backend Worker (`worker/`). Live (behind company login) at
|
|
|
5
5
|
https://{{slug}}.thanarie.dev after the first `canopy deploy`.
|
|
6
6
|
|
|
7
7
|
- `npm install` — once, then:
|
|
8
|
-
- `canopy dev` — develop locally against the live platform (real data!)
|
|
8
|
+
- `canopy dev` — develop locally against the live platform (real data!);
|
|
9
|
+
`/api/*` is served by `worker/index.ts`, everything else by the SPA
|
|
10
|
+
- `.dev.vars` — local values for the worker's secrets (copy
|
|
11
|
+
`.dev.vars.example`); dashboard secrets are never downloaded
|
|
9
12
|
- `canopy dev --local [--pull]` — offline sandbox with local data instead
|
|
10
13
|
- `canopy deploy` — build and publish the frontend and the `worker/` backend
|
|
11
14
|
- `AGENTS.md` — project guardrails and skill setup for coding agents
|
|
@@ -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-
|
|
2
|
+
import { c as apiRequest, n as reportErrors } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
const tokenCommand = defineCommand({
|
|
5
5
|
meta: {
|
|
@@ -102,6 +102,18 @@ const ERROR_COPY = {
|
|
|
102
102
|
why: "the allowlist is at its 32-entry limit",
|
|
103
103
|
fix: "Remove hosts you no longer need (`canopy egress list` / `canopy egress remove`), or cover several subdomains with one *. wildcard."
|
|
104
104
|
},
|
|
105
|
+
unsupported_image: {
|
|
106
|
+
why: "the file isn't an image Canopy stores (PNG, JPEG, WebP or GIF)",
|
|
107
|
+
fix: "Attach a screenshot in one of those formats — the file's real contents are what count, not its extension."
|
|
108
|
+
},
|
|
109
|
+
attachment_too_large: {
|
|
110
|
+
why: "the screenshot is over the 5 MB limit",
|
|
111
|
+
fix: "Scale the image down (or export it as JPEG/WebP) and attach it again."
|
|
112
|
+
},
|
|
113
|
+
too_many_attachments: {
|
|
114
|
+
why: "the report already has the maximum number of screenshots",
|
|
115
|
+
fix: "Remove one from the report on the dashboard before attaching another."
|
|
116
|
+
},
|
|
105
117
|
worker_egress_sync_failed: {
|
|
106
118
|
why: "the list was saved but redeploying the running worker with it failed",
|
|
107
119
|
fix: "Retry, or redeploy the project (`canopy deploy`) to apply the new allowlist."
|
|
@@ -211,6 +223,23 @@ async function apiRequest(method, path, body, rawContentType, headers) {
|
|
|
211
223
|
if (!response.ok) return fail(response, isDeployToken, method, path);
|
|
212
224
|
return await response.json();
|
|
213
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Authenticated POST of raw bytes (a screenshot attachment). The server types
|
|
228
|
+
* the bytes itself, so `contentType` is a hint, not a claim it trusts.
|
|
229
|
+
*/
|
|
230
|
+
async function apiUpload(path, bytes, contentType) {
|
|
231
|
+
const { apiUrl, token, isDeployToken } = credentials();
|
|
232
|
+
const response = await fetch(`${apiUrl}/control/v1${path}`, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: {
|
|
235
|
+
authorization: `Bearer ${token}`,
|
|
236
|
+
"content-type": contentType
|
|
237
|
+
},
|
|
238
|
+
body: bytes
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) return fail(response, isDeployToken, "POST", path);
|
|
241
|
+
return await response.json();
|
|
242
|
+
}
|
|
214
243
|
/** Authenticated GET returning raw bytes (e.g. a deploy zip). */
|
|
215
244
|
async function apiDownload(path) {
|
|
216
245
|
const { apiUrl, token, isDeployToken } = credentials();
|
|
@@ -346,4 +375,4 @@ function openBrowser(url) {
|
|
|
346
375
|
}).on("error", () => {}).unref();
|
|
347
376
|
}
|
|
348
377
|
//#endregion
|
|
349
|
-
export { ApiError as a, apiRequest as c, unwrap as i,
|
|
378
|
+
export { ApiError as a, apiRequest as c, unwrap as i, apiUpload as l, reportErrors as n, apiDataRequest as o, requireTty as r, apiDownload as s, openBrowser as t, withContext as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
|
|
2
|
-
import { n as reportErrors } from "./ui-
|
|
2
|
+
import { n as reportErrors } from "./ui-2mrJcjYC.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-
|
|
115
|
+
const { skillInstalled } = await import("./skill-CZP1AqQX.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,
|
|
2
|
+
import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/usage.ts
|
|
5
5
|
/**
|
package/package.json
CHANGED
package/dist/dev-D6Sy15Wv.mjs
DELETED