@mediamonks/canopy-cli 0.0.1 → 0.2.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/dist/api-CyJ-wBZh.mjs +40 -0
- package/dist/config-C-z7st1X.mjs +33 -0
- package/dist/{db-BBcgT1md.mjs → db-CtUG4vfG.mjs} +3 -3
- package/dist/{deploy-BfhAlb91.mjs → deploy-N88l4Kt3.mjs} +7 -6
- package/dist/{dev-BB7CwqoI.mjs → dev-DTGW0lm0.mjs} +6 -5
- package/dist/dev-D_GlUULk.mjs +2 -0
- package/dist/docs/ci.md +48 -0
- package/dist/docs/cli.md +67 -0
- package/dist/docs/data.md +49 -0
- package/dist/docs/deploy.md +50 -0
- package/dist/docs/dev.md +44 -0
- package/dist/docs/getting-started.md +100 -0
- package/dist/docs/troubleshooting.md +61 -0
- package/dist/docs-C3aOAx_A.mjs +179 -0
- package/dist/index.mjs +26 -16
- package/dist/{local-DhufX0eJ.mjs → local-951zc9qJ.mjs} +2 -2
- package/dist/{login-DrXKOEmr.mjs → login-Dif9Y32q.mjs} +38 -10
- package/dist/{open-DFWvrLFc.mjs → open-xLiGUz_W.mjs} +2 -2
- package/dist/package-OcWG7JAW.mjs +4 -0
- package/dist/{project-CrrpLEPs.mjs → project-CpiPLNby.mjs} +1 -1
- package/dist/projects-DJT_mJDR.mjs +228 -0
- package/dist/skill/canopy/SKILL.md +80 -0
- package/dist/skill-jbmERP1X.mjs +84 -0
- package/dist/templates/spa/.github/workflows/canopy-deploy.yml +34 -0
- package/dist/templates/spa/AGENTS.md +43 -0
- package/dist/templates/spa/README.md +14 -0
- package/dist/templates/spa/_gitignore +4 -0
- package/dist/templates/spa/canopy.json +5 -0
- package/dist/templates/spa/index.html +12 -0
- package/dist/templates/spa/package.json +21 -0
- package/dist/templates/spa/src/app.tsx +25 -0
- package/dist/templates/spa/src/canopy.ts +5 -0
- package/dist/templates/spa/src/main.tsx +10 -0
- package/dist/templates/spa/src/style.css +29 -0
- package/dist/templates/spa/tsconfig.json +16 -0
- package/dist/templates/spa/vite.config.ts +7 -0
- package/dist/templates/static/AGENTS.md +41 -0
- package/dist/templates/static/README.md +9 -0
- package/dist/templates/static/_gitignore +2 -0
- package/dist/templates/static/canopy.json +4 -0
- package/dist/templates/static/index.html +21 -0
- package/dist/templates/static/main.js +9 -0
- package/dist/templates/static/style.css +29 -0
- package/dist/templates/worker/.github/workflows/canopy-deploy.yml +34 -0
- package/dist/templates/worker/AGENTS.md +51 -0
- package/dist/templates/worker/README.md +18 -0
- package/dist/templates/worker/_gitignore +4 -0
- package/dist/templates/worker/canopy.json +6 -0
- package/dist/templates/worker/index.html +12 -0
- package/dist/templates/worker/package.json +22 -0
- package/dist/templates/worker/src/app.tsx +25 -0
- package/dist/templates/worker/src/canopy.ts +5 -0
- package/dist/templates/worker/src/main.tsx +10 -0
- package/dist/templates/worker/src/style.css +29 -0
- package/dist/templates/worker/tsconfig.json +16 -0
- package/dist/templates/worker/vite.config.ts +7 -0
- package/dist/templates/worker/worker/index.ts +29 -0
- package/dist/token-rTD9jxPZ.mjs +75 -0
- package/dist/ui-C8VuPzGp.mjs +40 -0
- package/dist/upgrade-Y8ejvoeg.mjs +99 -0
- package/dist/version-check-CoWgQs2h.mjs +128 -0
- package/package.json +2 -2
- package/dist/api-BEY4O4ap.mjs +0 -36
- package/dist/dev-Bvy21srH.mjs +0 -2
- package/dist/projects-f_KocDae.mjs +0 -295
- package/dist/ui-Cl5P_yQ6.mjs +0 -72
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: system-ui, sans-serif;
|
|
3
|
+
color-scheme: light dark;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
display: grid;
|
|
9
|
+
place-items: center;
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
main {
|
|
14
|
+
max-width: 40rem;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
text-align: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.status {
|
|
20
|
+
font-size: 0.9rem;
|
|
21
|
+
opacity: 0.7;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.placeholder {
|
|
25
|
+
border: 1px dashed color-mix(in srgb, currentColor 30%, transparent);
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
padding: 1.5rem;
|
|
28
|
+
opacity: 0.6;
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src", "worker", "vite.config.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project's own backend Worker (Hono-style handlers). It runs behind the
|
|
3
|
+
* Canopy gateway — every request is already authenticated — and reaches the
|
|
4
|
+
* platform (db/blob/ai) through the CANOPY service binding, so budgets and
|
|
5
|
+
* data isolation still apply.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: per-project Workers are a Canopy Phase 2 capability. `canopy deploy`
|
|
8
|
+
* does not upload this Worker yet — the frontend deploys like any SPA, and
|
|
9
|
+
* this directory is picked up automatically once the platform supports it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createCanopy } from "@mediamonks/canopy";
|
|
13
|
+
import { Hono } from "hono";
|
|
14
|
+
|
|
15
|
+
interface Env {
|
|
16
|
+
// Service binding to the platform API (canopy-api), scoped to this project.
|
|
17
|
+
CANOPY: { fetch: typeof fetch };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const app = new Hono<{ Bindings: Env }>();
|
|
21
|
+
|
|
22
|
+
// Routes under /api/* reach this Worker; everything else serves the SPA.
|
|
23
|
+
app.get("/api/hello", async (c) => {
|
|
24
|
+
const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
|
|
25
|
+
const user = await canopy.auth.user();
|
|
26
|
+
return c.json({ message: `Hello from the backend, ${user.name}!` });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export default app;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
|
|
2
|
+
import { i as resolveProject, n as projectFlagArgs, t as projectDirArgs } from "./project-CpiPLNby.mjs";
|
|
3
|
+
import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
const tokenCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "token",
|
|
8
|
+
description: "Manage deploy tokens for push-to-main deploys (CI)"
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
create: defineCommand({
|
|
12
|
+
meta: {
|
|
13
|
+
name: "create",
|
|
14
|
+
description: "Create a deploy token for CI"
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
...projectDirArgs,
|
|
18
|
+
name: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Label shown in the dashboard, e.g. \"GitHub Actions\""
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
run: reportErrors("token create", async ({ args }) => {
|
|
24
|
+
const { slug } = resolveProject(args);
|
|
25
|
+
const { token } = await apiRequest("POST", `/projects/${slug}/deploy-tokens`, { name: args.name });
|
|
26
|
+
console.log(`Deploy token for ${slug} (shown once — store it now):\n`);
|
|
27
|
+
console.log(` ${token}\n`);
|
|
28
|
+
console.log("Add it to the GitHub repo as secret CANOPY_DEPLOY_TOKEN");
|
|
29
|
+
console.log("(gh secret set CANOPY_DEPLOY_TOKEN, or repo Settings → Secrets).");
|
|
30
|
+
console.log("Pushes to main then deploy via .github/workflows/canopy-deploy.yml.");
|
|
31
|
+
})
|
|
32
|
+
}),
|
|
33
|
+
list: defineCommand({
|
|
34
|
+
meta: {
|
|
35
|
+
name: "list",
|
|
36
|
+
description: "List the project's deploy tokens"
|
|
37
|
+
},
|
|
38
|
+
args: { ...projectDirArgs },
|
|
39
|
+
run: reportErrors("token list", async ({ args }) => {
|
|
40
|
+
const { slug } = resolveProject(args);
|
|
41
|
+
const { tokens } = await apiRequest("GET", `/projects/${slug}/deploy-tokens`);
|
|
42
|
+
if (tokens.length === 0) {
|
|
43
|
+
console.log(`No deploy tokens for ${slug} — create one with \`canopy token create\`.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const t of tokens) console.log(`${t.id} ${t.name ?? "(unnamed)"} created ${t.created_at} by ${t.created_by ?? "?"}`);
|
|
47
|
+
})
|
|
48
|
+
}),
|
|
49
|
+
revoke: defineCommand({
|
|
50
|
+
meta: {
|
|
51
|
+
name: "revoke",
|
|
52
|
+
description: "Revoke a deploy token (id from `canopy token list`)"
|
|
53
|
+
},
|
|
54
|
+
args: {
|
|
55
|
+
id: {
|
|
56
|
+
type: "positional",
|
|
57
|
+
description: "Token id to revoke",
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
...projectFlagArgs,
|
|
61
|
+
dir: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Project directory (defaults to the current directory)"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
run: reportErrors("token revoke", async ({ args }) => {
|
|
67
|
+
const { slug } = resolveProject(args);
|
|
68
|
+
await apiRequest("DELETE", `/projects/${slug}/deploy-tokens/${args.id}`);
|
|
69
|
+
console.log(`Revoked ${args.id} — deploys using it stop working immediately.`);
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
export { tokenCommand };
|
|
@@ -0,0 +1,40 @@
|
|
|
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 };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { t as PACKAGE_NAME } from "./version-check-CoWgQs2h.mjs";
|
|
2
|
+
import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
//#region src/upgrade.ts
|
|
6
|
+
/**
|
|
7
|
+
* canopy upgrade — install the latest published CLI globally.
|
|
8
|
+
*
|
|
9
|
+
* We shell out to whichever package manager we can detect (npm/pnpm/yarn/bun),
|
|
10
|
+
* defaulting to npm, and let it do the actual install so global prefixes,
|
|
11
|
+
* permissions and lockfiles stay the manager's concern. `--pm` overrides the
|
|
12
|
+
* guess; `--version` pins a specific release.
|
|
13
|
+
*/
|
|
14
|
+
/** Best-effort guess of the package manager that installed this CLI. */
|
|
15
|
+
function detectPackageManager() {
|
|
16
|
+
const ua = process.env["npm_config_user_agent"] ?? "";
|
|
17
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
18
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
19
|
+
if (ua.startsWith("bun")) return "bun";
|
|
20
|
+
const path = process.argv[1] ?? process.execPath;
|
|
21
|
+
if (/[\\/](?:pnpm|\.pnpm)[\\/]/.test(path)) return "pnpm";
|
|
22
|
+
if (/[\\/]\.bun[\\/]/.test(path)) return "bun";
|
|
23
|
+
if (/[\\/](?:Yarn|\.yarn)[\\/]/i.test(path)) return "yarn";
|
|
24
|
+
return "npm";
|
|
25
|
+
}
|
|
26
|
+
/** The global-install argv for `pm`, targeting `spec` (e.g. name@latest). */
|
|
27
|
+
function installArgs(pm, spec) {
|
|
28
|
+
switch (pm) {
|
|
29
|
+
case "pnpm": return [
|
|
30
|
+
"add",
|
|
31
|
+
"-g",
|
|
32
|
+
spec
|
|
33
|
+
];
|
|
34
|
+
case "yarn": return [
|
|
35
|
+
"global",
|
|
36
|
+
"add",
|
|
37
|
+
spec
|
|
38
|
+
];
|
|
39
|
+
case "bun": return [
|
|
40
|
+
"add",
|
|
41
|
+
"-g",
|
|
42
|
+
spec
|
|
43
|
+
];
|
|
44
|
+
default: return [
|
|
45
|
+
"install",
|
|
46
|
+
"-g",
|
|
47
|
+
spec
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function run(command, args) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const child = spawn(command, args, {
|
|
54
|
+
stdio: "inherit",
|
|
55
|
+
shell: process.platform === "win32"
|
|
56
|
+
});
|
|
57
|
+
child.on("error", (err) => reject(err.code === "ENOENT" ? /* @__PURE__ */ new Error(`\`${command}\` was not found on your PATH.`) : err));
|
|
58
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`${command} exited with code ${code ?? "unknown"}.`)));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const upgradeCommand = defineCommand({
|
|
62
|
+
meta: {
|
|
63
|
+
name: "upgrade",
|
|
64
|
+
description: "Update the Canopy CLI to the latest version"
|
|
65
|
+
},
|
|
66
|
+
args: {
|
|
67
|
+
version: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Install this exact version instead of the latest"
|
|
70
|
+
},
|
|
71
|
+
pm: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Package manager to use: npm, pnpm, yarn or bun (auto-detected by default)"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
run: reportErrors("upgrade", async ({ args }) => {
|
|
77
|
+
const chosen = args.pm;
|
|
78
|
+
if (chosen && ![
|
|
79
|
+
"npm",
|
|
80
|
+
"pnpm",
|
|
81
|
+
"yarn",
|
|
82
|
+
"bun"
|
|
83
|
+
].includes(chosen)) throw new Error(`Unknown --pm "${chosen}" — use npm, pnpm, yarn or bun.`);
|
|
84
|
+
const pm = chosen ?? detectPackageManager();
|
|
85
|
+
const cmd = installArgs(pm, `${PACKAGE_NAME}@${args.version ?? "latest"}`);
|
|
86
|
+
console.log(`Upgrading via ${pm}: ${pm} ${cmd.join(" ")}`);
|
|
87
|
+
await run(pm, cmd);
|
|
88
|
+
console.log(`\nDone. Run \`canopy --version\` to confirm.`);
|
|
89
|
+
const { skillInstalled } = await import("./skill-jbmERP1X.mjs");
|
|
90
|
+
if (skillInstalled(process.cwd())) {
|
|
91
|
+
console.log("Refreshing the Canopy agent skill in this repo…");
|
|
92
|
+
await run("canopy", ["skill", "install"]).catch((err) => {
|
|
93
|
+
console.warn(`Could not auto-refresh the skill (${err instanceof Error ? err.message : String(err)}). Run \`canopy skill install\` yourself.`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
//#endregion
|
|
99
|
+
export { upgradeCommand };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/version-check.ts
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort "you're behind" notice for the CLI.
|
|
8
|
+
*
|
|
9
|
+
* On startup we read a small cache at ~/.canopy/version-check.json and, if a
|
|
10
|
+
* newer version is already known, print a one-line hint to stderr. Refreshing
|
|
11
|
+
* that cache from the npm registry happens in a detached background process so
|
|
12
|
+
* it never blocks (or delays) the command the user actually ran. The check is
|
|
13
|
+
* silent on any failure and can be disabled with CANOPY_NO_UPDATE_CHECK.
|
|
14
|
+
*/
|
|
15
|
+
const PACKAGE_NAME = "@mediamonks/canopy-cli";
|
|
16
|
+
const CACHE_PATH = join(homedir(), ".canopy", "version-check.json");
|
|
17
|
+
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
18
|
+
const REFRESH_TIMEOUT_MS = 3e3;
|
|
19
|
+
const REFRESH_ENV = "CANOPY_UPDATE_REFRESH";
|
|
20
|
+
/** The npm registry to query, honouring an npm/pnpm registry override. */
|
|
21
|
+
function registryUrl() {
|
|
22
|
+
return `${(process.env["npm_config_registry"] ?? "https://registry.npmjs.org").replace(/\/$/, "")}/${PACKAGE_NAME}/latest`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compare two semver strings. Returns -1 if a < b, 1 if a > b, 0 if equal.
|
|
26
|
+
* Handles `x.y.z` with an optional `-prerelease`; a prerelease sorts below its
|
|
27
|
+
* release (1.2.0-rc.1 < 1.2.0). Good enough for a behind/ahead check.
|
|
28
|
+
*/
|
|
29
|
+
function compareSemver(a, b) {
|
|
30
|
+
const parse = (v) => {
|
|
31
|
+
const [core = "", pre = ""] = v.replace(/^v/, "").split("-", 2);
|
|
32
|
+
return {
|
|
33
|
+
nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0),
|
|
34
|
+
pre: pre ? pre.split(".") : []
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const pa = parse(a);
|
|
38
|
+
const pb = parse(b);
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
const diff = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
|
|
41
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
42
|
+
}
|
|
43
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
44
|
+
if (pa.pre.length === 0) return 1;
|
|
45
|
+
if (pb.pre.length === 0) return -1;
|
|
46
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
47
|
+
const x = pa.pre[i];
|
|
48
|
+
const y = pb.pre[i];
|
|
49
|
+
if (x === y) continue;
|
|
50
|
+
if (x === void 0) return -1;
|
|
51
|
+
if (y === void 0) return 1;
|
|
52
|
+
const nx = Number(x);
|
|
53
|
+
const ny = Number(y);
|
|
54
|
+
return (!Number.isNaN(nx) && !Number.isNaN(ny) ? nx - ny : x < y ? -1 : 1) < 0 ? -1 : 1;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
function readCache() {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(readFileSync(CACHE_PATH, "utf8"));
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function writeCache(cache) {
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(join(homedir(), ".canopy"), { recursive: true });
|
|
68
|
+
writeFileSync(CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`);
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
/** Should we stay out of the way entirely (opt-out, CI, or piped output)? */
|
|
72
|
+
function disabled() {
|
|
73
|
+
return process.env["CANOPY_NO_UPDATE_CHECK"] === "1" || process.env["CI"] !== void 0 || !process.stderr.isTTY;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Print an update hint from cache (if we're behind), then kick off a detached
|
|
77
|
+
* background refresh when the cache is stale. Never throws, never blocks.
|
|
78
|
+
*/
|
|
79
|
+
function notifyOfUpdate(currentVersion) {
|
|
80
|
+
if (disabled()) return;
|
|
81
|
+
const cache = readCache();
|
|
82
|
+
if (cache?.latest && compareSemver(currentVersion, cache.latest) < 0) process.stderr.write(`\nUpdate available: ${currentVersion} → ${cache.latest}. Run \`canopy upgrade\` to update.\n\n`);
|
|
83
|
+
if (!(cache && Date.now() - Date.parse(cache.lastCheck) < CHECK_INTERVAL_MS)) spawnBackgroundRefresh();
|
|
84
|
+
}
|
|
85
|
+
/** Spawn `canopy` again, flagged to only refresh the cache, fully detached. */
|
|
86
|
+
function spawnBackgroundRefresh() {
|
|
87
|
+
try {
|
|
88
|
+
const entry = process.argv[1];
|
|
89
|
+
if (entry === void 0) return;
|
|
90
|
+
const child = spawn(process.execPath, [entry], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: "ignore",
|
|
93
|
+
env: {
|
|
94
|
+
...process.env,
|
|
95
|
+
[REFRESH_ENV]: "1"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
child.on("error", () => {});
|
|
99
|
+
child.unref();
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The detached process's whole job: fetch the latest version and update the
|
|
104
|
+
* cache. Always records lastCheck so a flaky registry doesn't cause a fetch on
|
|
105
|
+
* every single invocation. Exits the process when done.
|
|
106
|
+
*/
|
|
107
|
+
async function runRefresh() {
|
|
108
|
+
const cache = {
|
|
109
|
+
lastCheck: (/* @__PURE__ */ new Date()).toISOString(),
|
|
110
|
+
latest: readCache()?.latest
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
|
|
115
|
+
const res = await fetch(registryUrl(), {
|
|
116
|
+
headers: { accept: "application/json" },
|
|
117
|
+
signal: controller.signal
|
|
118
|
+
});
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
if (res.ok) {
|
|
121
|
+
const { version } = await res.json();
|
|
122
|
+
if (version) cache.latest = version;
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
writeCache(cache);
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { runRefresh as i, REFRESH_ENV as n, notifyOfUpdate as r, PACKAGE_NAME as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediamonks/canopy-cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Canopy CLI — create, develop and deploy Canopy projects.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"canopy": "./dist/index.mjs"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"vite-plus": "0.2.5"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "vp pack",
|
|
31
|
+
"build": "vp pack && node ./scripts/sync-templates.mjs",
|
|
32
32
|
"dev": "vp pack --watch",
|
|
33
33
|
"test": "vp test",
|
|
34
34
|
"check": "vp check"
|
package/dist/api-BEY4O4ap.mjs
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { a as readConfig } from "./ui-Cl5P_yQ6.mjs";
|
|
2
|
-
//#region src/api.ts
|
|
3
|
-
/**
|
|
4
|
-
* Authenticated requests to the Canopy control plane, using the CLI token
|
|
5
|
-
* from `canopy login` (~/.canopy/config.json).
|
|
6
|
-
*/
|
|
7
|
-
var ApiError = class extends Error {
|
|
8
|
-
status;
|
|
9
|
-
code;
|
|
10
|
-
constructor(status, code, detail) {
|
|
11
|
-
super(detail ? `${code}: ${detail}` : code);
|
|
12
|
-
this.status = status;
|
|
13
|
-
this.code = code;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
async function apiRequest(method, path, body, rawContentType) {
|
|
17
|
-
const config = readConfig();
|
|
18
|
-
if (!config.token) throw new Error("Not logged in — run `canopy login` first.");
|
|
19
|
-
const isForm = body instanceof FormData;
|
|
20
|
-
const response = await fetch(`${config.apiUrl}/v1${path}`, {
|
|
21
|
-
method,
|
|
22
|
-
headers: {
|
|
23
|
-
authorization: `Bearer ${config.token}`,
|
|
24
|
-
...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" }
|
|
25
|
-
},
|
|
26
|
-
body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
|
|
27
|
-
});
|
|
28
|
-
if (!response.ok) {
|
|
29
|
-
const payload = await response.json().catch(() => ({}));
|
|
30
|
-
if (response.status === 401) throw new Error("Session expired — run `canopy login` again.");
|
|
31
|
-
throw new ApiError(response.status, payload.error ?? `http_${response.status}`, payload.detail);
|
|
32
|
-
}
|
|
33
|
-
return await response.json();
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
|
-
export { apiRequest as t };
|
package/dist/dev-Bvy21srH.mjs
DELETED