@mediamonks/canopy-cli 0.4.4 → 0.4.5
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-Bkq-_qkd.mjs → db-PpaU3V-B.mjs} +5 -5
- package/dist/{deploy-BO_-gK32.mjs → deploy-B064KXtr.mjs} +3 -4
- package/dist/dev-BYv6BmcI.mjs +2 -0
- package/dist/{dev-pPVCUaFp.mjs → dev-CuvlJpcO.mjs} +19 -9
- package/dist/{docs-C3aOAx_A.mjs → docs-UYzmzpz-.mjs} +1 -2
- package/dist/index.mjs +18 -18
- package/dist/{local-MLfuFVZb.mjs → local-B3dyT2fh.mjs} +2 -2
- package/dist/{login-Dif9Y32q.mjs → login-DdNc9OjU.mjs} +1 -2
- package/dist/{open-xLiGUz_W.mjs → open-CBuZBJlv.mjs} +2 -2
- package/dist/{package-BHlmIdUm.mjs → package-Bnu23N0A.mjs} +1 -1
- package/dist/{project-CpiPLNby.mjs → project-DKbgJACM.mjs} +1 -1
- package/dist/{projects-oyjC4ZGb.mjs → projects-CK4DNxgZ.mjs} +2 -3
- package/dist/{skill-BsZKAgGI.mjs → skill-h7QlCMhc.mjs} +2 -2
- package/dist/{token-BXc2klNm.mjs → token-yuwQTQB8.mjs} +2 -3
- package/dist/ui-D1DXoDN1.mjs +308 -0
- package/dist/{upgrade-nYLjXxPk.mjs → upgrade-sXoqYLRu.mjs} +2 -2
- package/package.json +3 -3
- package/dist/api-BcX_w6Xf.mjs +0 -58
- package/dist/config-C-z7st1X.mjs +0 -33
- package/dist/dev-R6nzDtA-.mjs +0 -2
- package/dist/ui-C8VuPzGp.mjs +0 -40
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { n as apiRequest } from "./
|
|
2
|
-
import { i as resolveProject, n as projectFlagArgs } from "./project-
|
|
3
|
-
import { i as unwrap, n as reportErrors, r as requireTty } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { c as withContext, i as unwrap, n as reportErrors, r as requireTty, s as apiRequest } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { i as resolveProject, n as projectFlagArgs } from "./project-DKbgJACM.mjs";
|
|
4
3
|
import { defineCommand } from "citty";
|
|
5
4
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
6
5
|
import * as p from "@clack/prompts";
|
|
@@ -137,10 +136,11 @@ const dbCommand = defineCommand({
|
|
|
137
136
|
return;
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
|
-
|
|
139
|
+
const restoreContext = { action: `restore "${slug}" from ${snapshotKey ?? file}` };
|
|
140
|
+
if (snapshotKey) await withContext(apiRequest("POST", `/projects/${slug}/restore`, { snapshot: snapshotKey }), restoreContext);
|
|
141
141
|
else if (file) {
|
|
142
142
|
const sql = readFileSync(file, "utf8");
|
|
143
|
-
await apiRequest("POST", `/projects/${slug}/restore`, sql, "application/sql");
|
|
143
|
+
await withContext(apiRequest("POST", `/projects/${slug}/restore`, sql, "application/sql"), restoreContext);
|
|
144
144
|
}
|
|
145
145
|
p.log.success(`Restored ${slug}.`);
|
|
146
146
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { a as tryReadCanopyJson, i as resolveProject, n as projectFlagArgs, r as projectUrl, t as projectDirArgs } from "./project-
|
|
3
|
-
import { i as unwrap, n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { c as withContext, i as unwrap, n as reportErrors, o as apiDownload, s as apiRequest } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { a as tryReadCanopyJson, i as resolveProject, n as projectFlagArgs, r as projectUrl, t as projectDirArgs } from "./project-DKbgJACM.mjs";
|
|
4
3
|
import { defineCommand } from "citty";
|
|
5
4
|
import { spawn } from "node:child_process";
|
|
6
5
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -89,7 +88,7 @@ const deployCommand = defineCommand({
|
|
|
89
88
|
spin.start(`Deploying ${paths.length} files (${(total / 1024).toFixed(0)} kB) → ${slug}`);
|
|
90
89
|
let result;
|
|
91
90
|
try {
|
|
92
|
-
({deploy: result} = await apiRequest("POST", `/projects/${slug}/deploys`, form, void 0, process.env["GITHUB_ACTIONS"] ? { "x-canopy-source": "github-action" } : void 0));
|
|
91
|
+
({deploy: result} = await withContext(apiRequest("POST", `/projects/${slug}/deploys`, form, void 0, process.env["GITHUB_ACTIONS"] ? { "x-canopy-source": "github-action" } : void 0), { action: `deploy "${slug}"` }));
|
|
93
92
|
} catch (err) {
|
|
94
93
|
spin.error("Deploy failed.");
|
|
95
94
|
throw err;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { i as resolveProject, t as projectDirArgs } from "./project-CpiPLNby.mjs";
|
|
4
|
-
import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { a as ApiError, c as withContext, l as readConfig, n as reportErrors, s as apiRequest } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { i as resolveProject, t as projectDirArgs } from "./project-DKbgJACM.mjs";
|
|
5
3
|
import { createRequire } from "node:module";
|
|
6
4
|
import { defineCommand } from "citty";
|
|
7
5
|
import { join } from "node:path";
|
|
@@ -76,8 +74,19 @@ async function startVite(flags, serverOverrides) {
|
|
|
76
74
|
await server.listen();
|
|
77
75
|
return server;
|
|
78
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Context for the dev-token lookup — the first (and most common) place a stale
|
|
79
|
+
* `canopy.json` slug surfaces. Turns a bare `not_found` into a message that
|
|
80
|
+
* names the slug and both ways out (rename locally, or register online).
|
|
81
|
+
*/
|
|
82
|
+
function devTokenContext(slug) {
|
|
83
|
+
return {
|
|
84
|
+
action: `look up project "${slug}" on Canopy`,
|
|
85
|
+
fixes: { not_found: `No project "${slug}" is registered on Canopy (or your account can't access it). Either rename "slug" in canopy.json to match an existing project (see \`canopy projects\`), or register this one — create it in the dashboard, then set that slug in canopy.json.` }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
79
88
|
async function devRemote(flags, slug) {
|
|
80
|
-
const { token, expires_at } = await apiRequest("POST", `/projects/${slug}/dev-token`);
|
|
89
|
+
const { token, expires_at } = await withContext(apiRequest("POST", `/projects/${slug}/dev-token`), devTokenContext(slug));
|
|
81
90
|
const { apiUrl } = readConfig();
|
|
82
91
|
const server = await startVite(flags, { proxy: { "/_api": {
|
|
83
92
|
target: apiUrl,
|
|
@@ -94,21 +103,22 @@ async function devRemote(flags, slug) {
|
|
|
94
103
|
async function devLocal(flags, slug) {
|
|
95
104
|
let dump;
|
|
96
105
|
if (flags.pull) {
|
|
97
|
-
const { fetchProjectDump } = await import("./local-
|
|
106
|
+
const { fetchProjectDump } = await import("./local-B3dyT2fh.mjs");
|
|
98
107
|
console.log(`Pulling a fresh export of ${slug}…`);
|
|
99
108
|
dump = await fetchProjectDump(slug);
|
|
100
109
|
}
|
|
101
110
|
let remote;
|
|
102
111
|
try {
|
|
103
|
-
const { token } = await apiRequest("POST", `/projects/${slug}/dev-token`);
|
|
112
|
+
const { token } = await withContext(apiRequest("POST", `/projects/${slug}/dev-token`), devTokenContext(slug));
|
|
104
113
|
remote = {
|
|
105
114
|
target: readConfig().apiUrl,
|
|
106
115
|
token
|
|
107
116
|
};
|
|
108
117
|
} catch (err) {
|
|
109
|
-
|
|
118
|
+
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
119
|
+
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
110
120
|
}
|
|
111
|
-
const { createSandbox } = await import("./local-
|
|
121
|
+
const { createSandbox } = await import("./local-B3dyT2fh.mjs");
|
|
112
122
|
const sandbox = await createSandbox({
|
|
113
123
|
slug,
|
|
114
124
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { l as readConfig, n as reportErrors } from "./ui-D1DXoDN1.mjs";
|
|
3
2
|
import { defineCommand } from "citty";
|
|
4
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { homedir } from "node:os";
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as version } from "./package-
|
|
2
|
+
import { t as version } from "./package-Bnu23N0A.mjs";
|
|
3
3
|
import { i as runRefresh, r as notifyOfUpdate } from "./version-check-CoWgQs2h.mjs";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
//#region src/index.ts
|
|
@@ -41,23 +41,23 @@ const main = defineCommand({
|
|
|
41
41
|
description: "canopy — create and deploy vibecoded projects"
|
|
42
42
|
},
|
|
43
43
|
subCommands: {
|
|
44
|
-
login: () => import("./login-
|
|
45
|
-
whoami: () => import("./login-
|
|
46
|
-
logout: () => import("./login-
|
|
47
|
-
create: () => import("./projects-
|
|
48
|
-
init: () => import("./projects-
|
|
49
|
-
projects: () => import("./projects-
|
|
50
|
-
delete: () => import("./projects-
|
|
51
|
-
dev: () => import("./dev-
|
|
52
|
-
deploy: () => import("./deploy-
|
|
53
|
-
rollback: () => import("./deploy-
|
|
54
|
-
download: () => import("./deploy-
|
|
55
|
-
token: () => import("./token-
|
|
56
|
-
db: () => import("./db-
|
|
57
|
-
open: () => import("./open-
|
|
58
|
-
docs: () => import("./docs-
|
|
59
|
-
skill: () => import("./skill-
|
|
60
|
-
upgrade: () => import("./upgrade-
|
|
44
|
+
login: () => import("./login-DdNc9OjU.mjs").then((m) => m.loginCommand),
|
|
45
|
+
whoami: () => import("./login-DdNc9OjU.mjs").then((m) => m.whoamiCommand),
|
|
46
|
+
logout: () => import("./login-DdNc9OjU.mjs").then((m) => m.logoutCommand),
|
|
47
|
+
create: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.createCommand),
|
|
48
|
+
init: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.initCommand),
|
|
49
|
+
projects: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.projectsCommand),
|
|
50
|
+
delete: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.deleteCommand),
|
|
51
|
+
dev: () => import("./dev-BYv6BmcI.mjs").then((m) => m.devCommand),
|
|
52
|
+
deploy: () => import("./deploy-B064KXtr.mjs").then((m) => m.deployCommand),
|
|
53
|
+
rollback: () => import("./deploy-B064KXtr.mjs").then((m) => m.rollbackCommand),
|
|
54
|
+
download: () => import("./deploy-B064KXtr.mjs").then((m) => m.downloadCommand),
|
|
55
|
+
token: () => import("./token-yuwQTQB8.mjs").then((m) => m.tokenCommand),
|
|
56
|
+
db: () => import("./db-PpaU3V-B.mjs").then((m) => m.dbCommand),
|
|
57
|
+
open: () => import("./open-CBuZBJlv.mjs").then((m) => m.openCommand),
|
|
58
|
+
docs: () => import("./docs-UYzmzpz-.mjs").then((m) => m.docsCommand),
|
|
59
|
+
skill: () => import("./skill-h7QlCMhc.mjs").then((m) => m.skillCommand),
|
|
60
|
+
upgrade: () => import("./upgrade-sXoqYLRu.mjs").then((m) => m.upgradeCommand),
|
|
61
61
|
logs: planned("logs", "Tail worker logs", "2")
|
|
62
62
|
}
|
|
63
63
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as rewriteApiPath } from "./dev-
|
|
1
|
+
import { s as apiRequest } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { n as rewriteApiPath } from "./dev-CuvlJpcO.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { n as reportErrors, t as openBrowser } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { l as readConfig, n as reportErrors, t as openBrowser, u as writeConfig } from "./ui-D1DXoDN1.mjs";
|
|
3
2
|
import { defineCommand } from "citty";
|
|
4
3
|
import * as p from "@clack/prompts";
|
|
5
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { n as reportErrors, t as openBrowser } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { i as resolveProject, r as projectUrl, t as projectDirArgs } from "./project-DKbgJACM.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/open.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { n as apiRequest } from "./
|
|
2
|
-
import { a as tryReadCanopyJson, r as projectUrl } from "./project-
|
|
3
|
-
import { i as unwrap, n as reportErrors, r as requireTty } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { i as unwrap, n as reportErrors, r as requireTty, s as apiRequest } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { a as tryReadCanopyJson, r as projectUrl } from "./project-DKbgJACM.mjs";
|
|
4
3
|
import { defineCommand } from "citty";
|
|
5
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
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-Bnu23N0A.mjs";
|
|
2
|
+
import { n as reportErrors } from "./ui-D1DXoDN1.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,6 +1,5 @@
|
|
|
1
|
-
import { n as apiRequest } from "./
|
|
2
|
-
import { i as resolveProject, n as projectFlagArgs, t as projectDirArgs } from "./project-
|
|
3
|
-
import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
1
|
+
import { n as reportErrors, s as apiRequest } from "./ui-D1DXoDN1.mjs";
|
|
2
|
+
import { i as resolveProject, n as projectFlagArgs, t as projectDirArgs } from "./project-DKbgJACM.mjs";
|
|
4
3
|
import { defineCommand } from "citty";
|
|
5
4
|
const tokenCommand = defineCommand({
|
|
6
5
|
meta: {
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cancel, isCancel } from "@clack/prompts";
|
|
6
|
+
//#region src/config.ts
|
|
7
|
+
/**
|
|
8
|
+
* CLI config at ~/.canopy/config.json — platform endpoints and the current
|
|
9
|
+
* CLI token. Env overrides: CANOPY_API_URL, CANOPY_OIDC_ISSUER,
|
|
10
|
+
* CANOPY_OIDC_CLIENT_ID.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_API_URL = "https://canopy-api.arjan-van-wijk.workers.dev";
|
|
13
|
+
const DEFAULT_ISSUER = "https://s4capital.onelogin.com/oidc/2";
|
|
14
|
+
const DEFAULT_CLI_CLIENT_ID = "b69755d0-6678-013f-b5ce-09fe44a57aa0180501";
|
|
15
|
+
const configDir = join(homedir(), ".canopy");
|
|
16
|
+
const configPath = join(configDir, "config.json");
|
|
17
|
+
function readConfig() {
|
|
18
|
+
let stored = {};
|
|
19
|
+
try {
|
|
20
|
+
stored = JSON.parse(readFileSync(configPath, "utf8"));
|
|
21
|
+
} catch {}
|
|
22
|
+
return {
|
|
23
|
+
...stored,
|
|
24
|
+
apiUrl: process.env["CANOPY_API_URL"] ?? stored.apiUrl ?? DEFAULT_API_URL,
|
|
25
|
+
issuer: process.env["CANOPY_OIDC_ISSUER"] ?? stored.issuer ?? DEFAULT_ISSUER,
|
|
26
|
+
cliClientId: process.env["CANOPY_OIDC_CLIENT_ID"] ?? stored.cliClientId ?? DEFAULT_CLI_CLIENT_ID
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function writeConfig(config) {
|
|
30
|
+
mkdirSync(configDir, { recursive: true });
|
|
31
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
32
|
+
chmodSync(configPath, 384);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/api.ts
|
|
36
|
+
/**
|
|
37
|
+
* Authenticated requests to the Canopy control plane. Credentials, in order:
|
|
38
|
+
* CANOPY_DEPLOY_TOKEN (CI — a project-scoped deploy token, docs/02-decisions.md
|
|
39
|
+
* D11), then the CLI token from `canopy login` (~/.canopy/config.json).
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Human copy for the error codes the control plane returns (apps/api): `why`
|
|
43
|
+
* explains what the code means, `fix` is the default next step. A command can
|
|
44
|
+
* override `fix` per code via ErrorContext.fixes; unlisted codes fall back to
|
|
45
|
+
* status-based copy (statusWhy/statusFix).
|
|
46
|
+
*/
|
|
47
|
+
const ERROR_COPY = {
|
|
48
|
+
not_found: {
|
|
49
|
+
why: "the resource does not exist there, or your account can't access it",
|
|
50
|
+
fix: "Double-check the slug/id and that it exists on Canopy under your account (`canopy projects`)."
|
|
51
|
+
},
|
|
52
|
+
project_not_found: {
|
|
53
|
+
why: "no project with that slug exists on Canopy (or your account can't access it)",
|
|
54
|
+
fix: "Run `canopy projects` to list the projects you can access, then use one of those slugs."
|
|
55
|
+
},
|
|
56
|
+
slug_taken: {
|
|
57
|
+
why: "another project already uses that slug",
|
|
58
|
+
fix: "Pick a different slug, or reuse the existing project instead of creating a new one."
|
|
59
|
+
},
|
|
60
|
+
reserved_slug: {
|
|
61
|
+
why: "that slug is reserved",
|
|
62
|
+
fix: "Choose a different slug."
|
|
63
|
+
},
|
|
64
|
+
invalid_slug: {
|
|
65
|
+
why: "the slug is not a valid format",
|
|
66
|
+
fix: "Use lowercase letters, digits and hyphens only (e.g. my-project)."
|
|
67
|
+
},
|
|
68
|
+
project_paused: {
|
|
69
|
+
why: "the project is paused",
|
|
70
|
+
fix: "Resume it from the dashboard, then retry."
|
|
71
|
+
},
|
|
72
|
+
no_database: {
|
|
73
|
+
why: "the project has no database yet",
|
|
74
|
+
fix: "Deploy the project once (`canopy deploy`) to provision its database, then retry."
|
|
75
|
+
},
|
|
76
|
+
ai_budget_exceeded: {
|
|
77
|
+
why: "the project has hit its AI spend limit",
|
|
78
|
+
fix: "Raise the budget in the dashboard, or wait for the next billing cycle."
|
|
79
|
+
},
|
|
80
|
+
ai_unavailable: {
|
|
81
|
+
why: "the AI service is temporarily unavailable",
|
|
82
|
+
fix: "This is usually transient — wait a moment and try again."
|
|
83
|
+
},
|
|
84
|
+
deploy_too_large: {
|
|
85
|
+
why: "the deploy bundle exceeds the 25 MB limit",
|
|
86
|
+
fix: "Trim large assets from the build output (or move them to blob storage), then redeploy."
|
|
87
|
+
},
|
|
88
|
+
too_many_files: {
|
|
89
|
+
why: "the deploy has more than 500 files",
|
|
90
|
+
fix: "Reduce the file count — bundle/minify assets or drop what isn't served — then redeploy."
|
|
91
|
+
},
|
|
92
|
+
empty_deploy: {
|
|
93
|
+
why: "the deploy contained no files",
|
|
94
|
+
fix: "Check your build produced output before deploying (is the build dir correct?)."
|
|
95
|
+
},
|
|
96
|
+
missing_index_html: {
|
|
97
|
+
why: "the deploy has no index.html at its root",
|
|
98
|
+
fix: "Ensure your build output has an index.html at the top level, then redeploy."
|
|
99
|
+
},
|
|
100
|
+
dump_too_large: {
|
|
101
|
+
why: "the SQL dump exceeds the 25 MB limit",
|
|
102
|
+
fix: "Split the import into smaller batches, or trim the data before restoring."
|
|
103
|
+
},
|
|
104
|
+
invalid_dump: {
|
|
105
|
+
why: "the SQL dump is empty or malformed",
|
|
106
|
+
fix: "Check the file is non-empty, valid SQL (e.g. a `canopy db export`), then retry."
|
|
107
|
+
},
|
|
108
|
+
restore_failed: {
|
|
109
|
+
why: "the database rejected the restore while executing the SQL",
|
|
110
|
+
fix: "Fix the SQL the server reported above, then retry. A snapshot (`canopy db snapshot`) first is safest."
|
|
111
|
+
},
|
|
112
|
+
invalid_description: {
|
|
113
|
+
why: "the description is invalid (at most 1000 characters)",
|
|
114
|
+
fix: "Shorten the description to 1000 characters or fewer."
|
|
115
|
+
},
|
|
116
|
+
snapshot_not_found: {
|
|
117
|
+
why: "no snapshot with that id exists for this project",
|
|
118
|
+
fix: "Run `canopy db snapshots` to list available snapshots."
|
|
119
|
+
},
|
|
120
|
+
version_not_found: {
|
|
121
|
+
why: "no deploy with that version exists for this project",
|
|
122
|
+
fix: "Run `canopy deploys` to list versions you can roll back to."
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** Generic explanation for a status code when we have no per-code copy. */
|
|
126
|
+
function statusWhy(status) {
|
|
127
|
+
if (status === 401 || status === 403) return "your account is not allowed to do that";
|
|
128
|
+
if (status === 404) return "the resource does not exist there, or your account can't access it";
|
|
129
|
+
if (status === 409) return "the request conflicts with the current state";
|
|
130
|
+
if (status >= 500) return "the platform hit an internal problem";
|
|
131
|
+
if (status >= 400) return "the request was rejected";
|
|
132
|
+
return "the request did not succeed";
|
|
133
|
+
}
|
|
134
|
+
/** Generic remediation for a status code when we have no per-code fix. */
|
|
135
|
+
function statusFix(status) {
|
|
136
|
+
if (status === 401 || status === 403) return "Check you're logged in as the right account (`canopy whoami`).";
|
|
137
|
+
if (status >= 500) return "This is usually temporary — wait a moment and try again.";
|
|
138
|
+
return "Re-check the command arguments and try again.";
|
|
139
|
+
}
|
|
140
|
+
var ApiError = class extends Error {
|
|
141
|
+
status;
|
|
142
|
+
code;
|
|
143
|
+
method;
|
|
144
|
+
path;
|
|
145
|
+
detail;
|
|
146
|
+
/** Command-specific copy, layered on by `withContext` (or `fail` for 401s). */
|
|
147
|
+
context;
|
|
148
|
+
constructor(status, code, method, path, detail) {
|
|
149
|
+
super(detail ? `${code}: ${detail}` : code);
|
|
150
|
+
this.status = status;
|
|
151
|
+
this.code = code;
|
|
152
|
+
this.method = method;
|
|
153
|
+
this.path = path;
|
|
154
|
+
this.detail = detail;
|
|
155
|
+
this.name = "ApiError";
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The three things a caller needs: what we were doing (`headline`), why it
|
|
159
|
+
* failed (`why`), and how to fix it (`fix`). See `reportErrors` for layout.
|
|
160
|
+
*/
|
|
161
|
+
explain() {
|
|
162
|
+
const copy = ERROR_COPY[this.code];
|
|
163
|
+
const headline = this.context?.action ? `could not ${this.context.action}` : `${this.method} ${this.path} failed`;
|
|
164
|
+
const detail = this.detail ? `: ${this.detail}` : "";
|
|
165
|
+
return {
|
|
166
|
+
headline,
|
|
167
|
+
why: `${this.status} ${this.code}${detail} — ${copy?.why ?? statusWhy(this.status)}`,
|
|
168
|
+
fix: this.context?.fixes?.[this.code] ?? copy?.fix ?? statusFix(this.status)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Attach command-specific context to any ApiError thrown by `request`, so the
|
|
174
|
+
* generic client failure becomes an actionable, command-aware message. Other
|
|
175
|
+
* errors pass through untouched.
|
|
176
|
+
*/
|
|
177
|
+
async function withContext(request, context) {
|
|
178
|
+
try {
|
|
179
|
+
return await request;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err instanceof ApiError) err.context = {
|
|
182
|
+
action: context.action ?? err.context?.action,
|
|
183
|
+
fixes: {
|
|
184
|
+
...err.context?.fixes,
|
|
185
|
+
...context.fixes
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Resolve the bearer token: CI deploy token first (D11), then the login token. */
|
|
192
|
+
function credentials() {
|
|
193
|
+
const config = readConfig();
|
|
194
|
+
const deployToken = process.env["CANOPY_DEPLOY_TOKEN"];
|
|
195
|
+
const token = deployToken || config.token;
|
|
196
|
+
if (!token) throw new Error("Not logged in — run `canopy login` first.");
|
|
197
|
+
return {
|
|
198
|
+
apiUrl: config.apiUrl,
|
|
199
|
+
token,
|
|
200
|
+
isDeployToken: Boolean(deployToken)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Turn a non-2xx response into a thrown ApiError (friendlier copy on 401). */
|
|
204
|
+
async function fail(response, isDeployToken, method, path) {
|
|
205
|
+
const payload = await response.json().catch(() => ({}));
|
|
206
|
+
const code = payload.error ?? (response.status === 401 ? "unauthenticated" : `http_${response.status}`);
|
|
207
|
+
const err = new ApiError(response.status, code, method, path, payload.detail);
|
|
208
|
+
if (response.status === 401) err.context = {
|
|
209
|
+
action: "authenticate with Canopy",
|
|
210
|
+
fixes: { [code]: isDeployToken ? "The deploy token was rejected — it may have been revoked. Create a new one in the dashboard (Settings → Deploy tokens) or with `canopy token create`." : "Your session has expired — run `canopy login` again." }
|
|
211
|
+
};
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
async function apiRequest(method, path, body, rawContentType, headers) {
|
|
215
|
+
const { apiUrl, token, isDeployToken } = credentials();
|
|
216
|
+
const isForm = body instanceof FormData;
|
|
217
|
+
const response = await fetch(`${apiUrl}/v1${path}`, {
|
|
218
|
+
method,
|
|
219
|
+
headers: {
|
|
220
|
+
authorization: `Bearer ${token}`,
|
|
221
|
+
...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" },
|
|
222
|
+
...headers
|
|
223
|
+
},
|
|
224
|
+
body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
|
|
225
|
+
});
|
|
226
|
+
if (!response.ok) return fail(response, isDeployToken, method, path);
|
|
227
|
+
return await response.json();
|
|
228
|
+
}
|
|
229
|
+
/** Authenticated GET returning raw bytes (e.g. a deploy zip). */
|
|
230
|
+
async function apiDownload(path) {
|
|
231
|
+
const { apiUrl, token, isDeployToken } = credentials();
|
|
232
|
+
const response = await fetch(`${apiUrl}/v1${path}`, { headers: { authorization: `Bearer ${token}` } });
|
|
233
|
+
if (!response.ok) return fail(response, isDeployToken, "GET", path);
|
|
234
|
+
return Buffer.from(await response.arrayBuffer());
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/ui.ts
|
|
238
|
+
/**
|
|
239
|
+
* Shared terminal UX plumbing: clack prompt unwrapping and uniform
|
|
240
|
+
* command error reporting.
|
|
241
|
+
*/
|
|
242
|
+
/** Unwrap a clack prompt result, exiting cleanly when the user cancels (ctrl-c). */
|
|
243
|
+
function unwrap(result) {
|
|
244
|
+
if (isCancel(result)) {
|
|
245
|
+
cancel("Aborted.");
|
|
246
|
+
process.exit(130);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
/** Terminal width to wrap error copy to, bounded so it stays readable. */
|
|
251
|
+
const TEXT_WIDTH = Math.min(process.stdout.columns || 80, 88);
|
|
252
|
+
/** Render ` Label text…`, wrapping and aligning continuation lines under the text. */
|
|
253
|
+
function labeled(label, text) {
|
|
254
|
+
const lead = ` ${label} `;
|
|
255
|
+
const indent = " ".repeat(lead.length);
|
|
256
|
+
const max = Math.max(TEXT_WIDTH - lead.length, 24);
|
|
257
|
+
const lines = [];
|
|
258
|
+
let line = "";
|
|
259
|
+
for (const word of text.split(/\s+/)) if (line && line.length + 1 + word.length > max) {
|
|
260
|
+
lines.push(line);
|
|
261
|
+
line = word;
|
|
262
|
+
} else line = line ? `${line} ${word}` : word;
|
|
263
|
+
if (line) lines.push(line);
|
|
264
|
+
return lines.map((l, i) => (i === 0 ? lead : indent) + l).join("\n");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Compose a command failure. API failures get a three-part block — what we
|
|
268
|
+
* tried (headline), why it failed, and how to fix it — everything else prints
|
|
269
|
+
* as a single `canopy <name>: <message>` line.
|
|
270
|
+
*/
|
|
271
|
+
function formatCliError(name, err) {
|
|
272
|
+
if (err instanceof ApiError) {
|
|
273
|
+
const { headline, why, fix } = err.explain();
|
|
274
|
+
return [
|
|
275
|
+
`canopy ${name}: ${headline}`,
|
|
276
|
+
"",
|
|
277
|
+
labeled("Why", why),
|
|
278
|
+
labeled("Fix", fix),
|
|
279
|
+
""
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
return `canopy ${name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
283
|
+
}
|
|
284
|
+
/** Wrap a command action so failures print an actionable report and exit 1. */
|
|
285
|
+
function reportErrors(name, action) {
|
|
286
|
+
return async (...args) => {
|
|
287
|
+
try {
|
|
288
|
+
await action(...args);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error(formatCliError(name, err));
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** Guard interactive prompts: bail with a actionable message on non-TTY runs. */
|
|
296
|
+
function requireTty(hint) {
|
|
297
|
+
if (!process.stdin.isTTY) throw new Error(`This needs an interactive terminal — ${hint}`);
|
|
298
|
+
}
|
|
299
|
+
/** Open a URL in the default browser, fire-and-forget. */
|
|
300
|
+
function openBrowser(url) {
|
|
301
|
+
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open", [url], {
|
|
302
|
+
stdio: "ignore",
|
|
303
|
+
detached: true,
|
|
304
|
+
shell: process.platform === "win32"
|
|
305
|
+
}).on("error", () => {}).unref();
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
export { ApiError as a, withContext as c, unwrap as i, readConfig as l, reportErrors as n, apiDownload as o, requireTty as r, apiRequest as s, openBrowser as t, writeConfig as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as PACKAGE_NAME } from "./version-check-CoWgQs2h.mjs";
|
|
2
|
-
import { n as reportErrors } from "./ui-
|
|
2
|
+
import { n as reportErrors } from "./ui-D1DXoDN1.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-h7QlCMhc.mjs");
|
|
116
116
|
const refreshSkill = skillInstalled(process.cwd());
|
|
117
117
|
console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
|
|
118
118
|
await run(command, cmd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediamonks/canopy-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Canopy CLI — create, develop and deploy Canopy projects.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"canopy": "./dist/index.mjs"
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"hono": "^4.8.0",
|
|
20
20
|
"miniflare": "^4.20260714.0",
|
|
21
21
|
"vite-server": "npm:vite@^7",
|
|
22
|
-
"@mediamonks/canopy
|
|
23
|
-
"@mediamonks/canopy": "0.0.
|
|
22
|
+
"@mediamonks/canopy": "0.0.2",
|
|
23
|
+
"@mediamonks/canopy-data-plane": "0.0.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^26.1.1",
|
package/dist/api-BcX_w6Xf.mjs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { t as readConfig } from "./config-C-z7st1X.mjs";
|
|
2
|
-
//#region src/api.ts
|
|
3
|
-
/**
|
|
4
|
-
* Authenticated requests to the Canopy control plane. Credentials, in order:
|
|
5
|
-
* CANOPY_DEPLOY_TOKEN (CI — a project-scoped deploy token, docs/02-decisions.md
|
|
6
|
-
* D11), then the CLI token from `canopy login` (~/.canopy/config.json).
|
|
7
|
-
*/
|
|
8
|
-
var ApiError = class extends Error {
|
|
9
|
-
status;
|
|
10
|
-
code;
|
|
11
|
-
constructor(status, code, detail) {
|
|
12
|
-
super(detail ? `${code}: ${detail}` : code);
|
|
13
|
-
this.status = status;
|
|
14
|
-
this.code = code;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
/** Resolve the bearer token: CI deploy token first (D11), then the login token. */
|
|
18
|
-
function credentials() {
|
|
19
|
-
const config = readConfig();
|
|
20
|
-
const deployToken = process.env["CANOPY_DEPLOY_TOKEN"];
|
|
21
|
-
const token = deployToken || config.token;
|
|
22
|
-
if (!token) throw new Error("Not logged in — run `canopy login` first.");
|
|
23
|
-
return {
|
|
24
|
-
apiUrl: config.apiUrl,
|
|
25
|
-
token,
|
|
26
|
-
isDeployToken: Boolean(deployToken)
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/** Turn a non-2xx response into a thrown error (friendlier copy on 401). */
|
|
30
|
-
async function fail(response, isDeployToken) {
|
|
31
|
-
const payload = await response.json().catch(() => ({}));
|
|
32
|
-
if (response.status === 401) throw new Error(isDeployToken ? "Deploy token rejected — it may have been revoked. Create a new one in the dashboard (Settings → Deploy tokens) or with `canopy token create`." : "Session expired — run `canopy login` again.");
|
|
33
|
-
throw new ApiError(response.status, payload.error ?? `http_${response.status}`, payload.detail);
|
|
34
|
-
}
|
|
35
|
-
async function apiRequest(method, path, body, rawContentType, headers) {
|
|
36
|
-
const { apiUrl, token, isDeployToken } = credentials();
|
|
37
|
-
const isForm = body instanceof FormData;
|
|
38
|
-
const response = await fetch(`${apiUrl}/v1${path}`, {
|
|
39
|
-
method,
|
|
40
|
-
headers: {
|
|
41
|
-
authorization: `Bearer ${token}`,
|
|
42
|
-
...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" },
|
|
43
|
-
...headers
|
|
44
|
-
},
|
|
45
|
-
body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
|
|
46
|
-
});
|
|
47
|
-
if (!response.ok) return fail(response, isDeployToken);
|
|
48
|
-
return await response.json();
|
|
49
|
-
}
|
|
50
|
-
/** Authenticated GET returning raw bytes (e.g. a deploy zip). */
|
|
51
|
-
async function apiDownload(path) {
|
|
52
|
-
const { apiUrl, token, isDeployToken } = credentials();
|
|
53
|
-
const response = await fetch(`${apiUrl}/v1${path}`, { headers: { authorization: `Bearer ${token}` } });
|
|
54
|
-
if (!response.ok) return fail(response, isDeployToken);
|
|
55
|
-
return Buffer.from(await response.arrayBuffer());
|
|
56
|
-
}
|
|
57
|
-
//#endregion
|
|
58
|
-
export { apiRequest as n, apiDownload as t };
|
package/dist/config-C-z7st1X.mjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
//#region src/config.ts
|
|
5
|
-
/**
|
|
6
|
-
* CLI config at ~/.canopy/config.json — platform endpoints and the current
|
|
7
|
-
* CLI token. Env overrides: CANOPY_API_URL, CANOPY_OIDC_ISSUER,
|
|
8
|
-
* CANOPY_OIDC_CLIENT_ID.
|
|
9
|
-
*/
|
|
10
|
-
const DEFAULT_API_URL = "https://canopy-api.arjan-van-wijk.workers.dev";
|
|
11
|
-
const DEFAULT_ISSUER = "https://s4capital.onelogin.com/oidc/2";
|
|
12
|
-
const DEFAULT_CLI_CLIENT_ID = "b69755d0-6678-013f-b5ce-09fe44a57aa0180501";
|
|
13
|
-
const configDir = join(homedir(), ".canopy");
|
|
14
|
-
const configPath = join(configDir, "config.json");
|
|
15
|
-
function readConfig() {
|
|
16
|
-
let stored = {};
|
|
17
|
-
try {
|
|
18
|
-
stored = JSON.parse(readFileSync(configPath, "utf8"));
|
|
19
|
-
} catch {}
|
|
20
|
-
return {
|
|
21
|
-
...stored,
|
|
22
|
-
apiUrl: process.env["CANOPY_API_URL"] ?? stored.apiUrl ?? DEFAULT_API_URL,
|
|
23
|
-
issuer: process.env["CANOPY_OIDC_ISSUER"] ?? stored.issuer ?? DEFAULT_ISSUER,
|
|
24
|
-
cliClientId: process.env["CANOPY_OIDC_CLIENT_ID"] ?? stored.cliClientId ?? DEFAULT_CLI_CLIENT_ID
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function writeConfig(config) {
|
|
28
|
-
mkdirSync(configDir, { recursive: true });
|
|
29
|
-
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
30
|
-
chmodSync(configPath, 384);
|
|
31
|
-
}
|
|
32
|
-
//#endregion
|
|
33
|
-
export { writeConfig as n, readConfig as t };
|
package/dist/dev-R6nzDtA-.mjs
DELETED
package/dist/ui-C8VuPzGp.mjs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { cancel, isCancel } from "@clack/prompts";
|
|
3
|
-
//#region src/ui.ts
|
|
4
|
-
/**
|
|
5
|
-
* Shared terminal UX plumbing: clack prompt unwrapping and uniform
|
|
6
|
-
* command error reporting.
|
|
7
|
-
*/
|
|
8
|
-
/** Unwrap a clack prompt result, exiting cleanly when the user cancels (ctrl-c). */
|
|
9
|
-
function unwrap(result) {
|
|
10
|
-
if (isCancel(result)) {
|
|
11
|
-
cancel("Aborted.");
|
|
12
|
-
process.exit(130);
|
|
13
|
-
}
|
|
14
|
-
return result;
|
|
15
|
-
}
|
|
16
|
-
/** Wrap a command action so failures print as `canopy <name>: <message>` and exit 1. */
|
|
17
|
-
function reportErrors(name, action) {
|
|
18
|
-
return async (...args) => {
|
|
19
|
-
try {
|
|
20
|
-
await action(...args);
|
|
21
|
-
} catch (err) {
|
|
22
|
-
console.error(`canopy ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/** Guard interactive prompts: bail with a actionable message on non-TTY runs. */
|
|
28
|
-
function requireTty(hint) {
|
|
29
|
-
if (!process.stdin.isTTY) throw new Error(`This needs an interactive terminal — ${hint}`);
|
|
30
|
-
}
|
|
31
|
-
/** Open a URL in the default browser, fire-and-forget. */
|
|
32
|
-
function openBrowser(url) {
|
|
33
|
-
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open", [url], {
|
|
34
|
-
stdio: "ignore",
|
|
35
|
-
detached: true,
|
|
36
|
-
shell: process.platform === "win32"
|
|
37
|
-
}).on("error", () => {}).unref();
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
export { unwrap as i, reportErrors as n, requireTty as r, openBrowser as t };
|