@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,179 @@
|
|
|
1
|
+
import { t as readConfig } from "./config-C-z7st1X.mjs";
|
|
2
|
+
import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region src/docs.ts
|
|
9
|
+
/**
|
|
10
|
+
* canopy docs — user/agent-facing guides, served live by the platform
|
|
11
|
+
* (GET /v1/docs) so they never go stale, with a local cache and a bundled
|
|
12
|
+
* snapshot as offline fallbacks.
|
|
13
|
+
*
|
|
14
|
+
* The platform requires a login for docs (internal platform), so fetches carry
|
|
15
|
+
* the CLI token when there is one; without it we simply fall back to the
|
|
16
|
+
* bundled snapshot, so `canopy docs` still works before sign-in.
|
|
17
|
+
*
|
|
18
|
+
* Resolution order for content:
|
|
19
|
+
* 1. platform (unless a fresh cache exists and --refresh wasn't passed)
|
|
20
|
+
* 2. local cache (~/.canopy/docs-cache/)
|
|
21
|
+
* 3. bundled snapshot shipped with the CLI (dist/docs, repo docs/guides in dev)
|
|
22
|
+
*/
|
|
23
|
+
const cacheDir = join(homedir(), ".canopy", "docs-cache");
|
|
24
|
+
const indexPath = join(cacheDir, "index.json");
|
|
25
|
+
const CACHE_TTL_MS = 3600 * 1e3;
|
|
26
|
+
const FETCH_TIMEOUT_MS = 4e3;
|
|
27
|
+
/** The bundled snapshot dir: dist/docs when published, repo docs/guides in dev. */
|
|
28
|
+
function bundledDocsDir() {
|
|
29
|
+
const base = fileURLToPath(new URL(".", import.meta.url));
|
|
30
|
+
for (const candidate of [join(base, "docs"), join(base, "..", "..", "..", "docs", "guides")]) if (existsSync(candidate)) return candidate;
|
|
31
|
+
}
|
|
32
|
+
/** Parse `---\ntitle: …\nsummary: …\n---\n<body>` frontmatter. */
|
|
33
|
+
function parseFrontmatter(raw) {
|
|
34
|
+
const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
|
|
35
|
+
if (!match?.[1] || match[2] === void 0) return { body: raw.trim() };
|
|
36
|
+
const meta = {};
|
|
37
|
+
for (const line of match[1].split("\n")) {
|
|
38
|
+
const kv = /^([a-zA-Z0-9_-]+):\s*(.*)$/.exec(line);
|
|
39
|
+
if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
title: meta["title"],
|
|
43
|
+
summary: meta["summary"],
|
|
44
|
+
body: match[2].trim()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Bearer header from the deploy token (CI) or the CLI login, if either exists. */
|
|
48
|
+
function authHeaders() {
|
|
49
|
+
const token = process.env["CANOPY_DEPLOY_TOKEN"] || readConfig().token;
|
|
50
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
51
|
+
}
|
|
52
|
+
async function fetchRes(url) {
|
|
53
|
+
try {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
headers: {
|
|
59
|
+
accept: "*/*",
|
|
60
|
+
...authHeaders()
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
return res;
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function fetchJson(url) {
|
|
70
|
+
const res = await fetchRes(url);
|
|
71
|
+
return res?.ok ? await res.json() : void 0;
|
|
72
|
+
}
|
|
73
|
+
async function fetchText(url) {
|
|
74
|
+
const res = await fetchRes(url);
|
|
75
|
+
return res?.ok ? await res.text() : void 0;
|
|
76
|
+
}
|
|
77
|
+
function cacheFresh() {
|
|
78
|
+
try {
|
|
79
|
+
const { fetchedAt } = JSON.parse(readFileSync(indexPath, "utf8"));
|
|
80
|
+
return fetchedAt !== void 0 && Date.now() - Date.parse(fetchedAt) < CACHE_TTL_MS;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function writeCache(file, content) {
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
88
|
+
writeFileSync(join(cacheDir, file), content);
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
/** Topics from cache, else from the bundled snapshot's frontmatter. */
|
|
92
|
+
function topicsFromFallback() {
|
|
93
|
+
try {
|
|
94
|
+
const cached = JSON.parse(readFileSync(indexPath, "utf8"));
|
|
95
|
+
if (cached.topics?.length) return cached.topics;
|
|
96
|
+
} catch {}
|
|
97
|
+
const dir = bundledDocsDir();
|
|
98
|
+
if (!dir) return [];
|
|
99
|
+
return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
100
|
+
const slug = f.replace(/\.md$/, "");
|
|
101
|
+
const { title, summary } = parseFrontmatter(readFileSync(join(dir, f), "utf8"));
|
|
102
|
+
return {
|
|
103
|
+
slug,
|
|
104
|
+
title: title ?? slug,
|
|
105
|
+
summary: summary ?? ""
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** A single doc's body from cache, else from the bundled snapshot. */
|
|
110
|
+
function bodyFromFallback(slug) {
|
|
111
|
+
const cached = join(cacheDir, `${slug}.md`);
|
|
112
|
+
if (existsSync(cached)) return readFileSync(cached, "utf8");
|
|
113
|
+
const dir = bundledDocsDir();
|
|
114
|
+
const file = dir && join(dir, `${slug}.md`);
|
|
115
|
+
if (file && existsSync(file)) return parseFrontmatter(readFileSync(file, "utf8")).body;
|
|
116
|
+
}
|
|
117
|
+
async function listTopics(refresh) {
|
|
118
|
+
const { apiUrl } = readConfig();
|
|
119
|
+
if (refresh || !cacheFresh()) {
|
|
120
|
+
const data = await fetchJson(`${apiUrl}/v1/docs`);
|
|
121
|
+
if (data?.topics) {
|
|
122
|
+
writeCache("index.json", JSON.stringify({
|
|
123
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
|
+
...data
|
|
125
|
+
}));
|
|
126
|
+
return data.topics;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return topicsFromFallback();
|
|
130
|
+
}
|
|
131
|
+
async function readTopic(slug, refresh) {
|
|
132
|
+
const { apiUrl } = readConfig();
|
|
133
|
+
if (refresh || !existsSync(join(cacheDir, `${slug}.md`))) {
|
|
134
|
+
const body = await fetchText(`${apiUrl}/v1/docs/${encodeURIComponent(slug)}`);
|
|
135
|
+
if (body !== void 0) {
|
|
136
|
+
writeCache(`${slug}.md`, body);
|
|
137
|
+
return body;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return bodyFromFallback(slug);
|
|
141
|
+
}
|
|
142
|
+
const docsCommand = defineCommand({
|
|
143
|
+
meta: {
|
|
144
|
+
name: "docs",
|
|
145
|
+
description: "Read the Canopy guides (offline-capable)"
|
|
146
|
+
},
|
|
147
|
+
args: {
|
|
148
|
+
topic: {
|
|
149
|
+
type: "positional",
|
|
150
|
+
required: false,
|
|
151
|
+
description: "Topic to print (omit to list)"
|
|
152
|
+
},
|
|
153
|
+
refresh: {
|
|
154
|
+
type: "boolean",
|
|
155
|
+
default: false,
|
|
156
|
+
description: "Re-fetch from the platform"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
run: reportErrors("docs", async ({ args }) => {
|
|
160
|
+
const refresh = args.refresh;
|
|
161
|
+
const topic = args.topic;
|
|
162
|
+
if (!topic) {
|
|
163
|
+
const topics = await listTopics(refresh);
|
|
164
|
+
if (topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
|
|
165
|
+
const width = Math.max(...topics.map((t) => t.slug.length));
|
|
166
|
+
console.log("Canopy docs — run `canopy docs <topic>`:\n");
|
|
167
|
+
for (const t of topics) console.log(` ${t.slug.padEnd(width)} ${t.summary}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const body = await readTopic(topic, refresh);
|
|
171
|
+
if (body === void 0) {
|
|
172
|
+
const known = (await listTopics(false)).map((t) => t.slug).join(", ");
|
|
173
|
+
throw new Error(`No doc "${topic}".${known ? ` Available: ${known}.` : ""}`);
|
|
174
|
+
}
|
|
175
|
+
console.log(body);
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
//#endregion
|
|
179
|
+
export { docsCommand };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { t as version } from "./package-OcWG7JAW.mjs";
|
|
3
|
+
import { i as runRefresh, r as notifyOfUpdate } from "./version-check-CoWgQs2h.mjs";
|
|
2
4
|
import { defineCommand, runMain } from "citty";
|
|
3
|
-
//#region package.json
|
|
4
|
-
var version = "0.0.1";
|
|
5
|
-
//#endregion
|
|
6
5
|
//#region src/index.ts
|
|
7
6
|
/**
|
|
8
7
|
* canopy — the Canopy CLI.
|
|
@@ -13,10 +12,15 @@ var version = "0.0.1";
|
|
|
13
12
|
* create scaffold + register a project [phase 1]
|
|
14
13
|
* dev [--local [--pull]] local dev: remote-first, or offline sandbox [phase 1/2]
|
|
15
14
|
* deploy build & deploy assets (+ worker) [phase 1/2]
|
|
15
|
+
* token create|list|revoke deploy tokens for CI [phase 2]
|
|
16
16
|
* db pull|snapshot|restore [phase 2]
|
|
17
17
|
* logs tail the project's worker logs [phase 2]
|
|
18
18
|
* open open the deployed project / dashboard [phase 1]
|
|
19
19
|
*/
|
|
20
|
+
if (process.env["CANOPY_UPDATE_REFRESH"] === "1") {
|
|
21
|
+
await runRefresh();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
20
24
|
/** A roadmap command that has not landed yet (docs/04-roadmap.md). */
|
|
21
25
|
function planned(name, description, phase) {
|
|
22
26
|
return defineCommand({
|
|
@@ -30,26 +34,32 @@ function planned(name, description, phase) {
|
|
|
30
34
|
}
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
|
-
|
|
37
|
+
const main = defineCommand({
|
|
34
38
|
meta: {
|
|
35
39
|
name: "canopy",
|
|
36
40
|
version,
|
|
37
41
|
description: "canopy — create and deploy vibecoded projects"
|
|
38
42
|
},
|
|
39
43
|
subCommands: {
|
|
40
|
-
login: () => import("./login-
|
|
41
|
-
whoami: () => import("./login-
|
|
42
|
-
logout: () => import("./login-
|
|
43
|
-
create: () => import("./projects-
|
|
44
|
-
projects: () => import("./projects-
|
|
45
|
-
delete: () => import("./projects-
|
|
46
|
-
dev: () => import("./dev-
|
|
47
|
-
deploy: () => import("./deploy-
|
|
48
|
-
rollback: () => import("./deploy-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
login: () => import("./login-Dif9Y32q.mjs").then((m) => m.loginCommand),
|
|
45
|
+
whoami: () => import("./login-Dif9Y32q.mjs").then((m) => m.whoamiCommand),
|
|
46
|
+
logout: () => import("./login-Dif9Y32q.mjs").then((m) => m.logoutCommand),
|
|
47
|
+
create: () => import("./projects-DJT_mJDR.mjs").then((m) => m.createCommand),
|
|
48
|
+
projects: () => import("./projects-DJT_mJDR.mjs").then((m) => m.projectsCommand),
|
|
49
|
+
delete: () => import("./projects-DJT_mJDR.mjs").then((m) => m.deleteCommand),
|
|
50
|
+
dev: () => import("./dev-D_GlUULk.mjs").then((m) => m.devCommand),
|
|
51
|
+
deploy: () => import("./deploy-N88l4Kt3.mjs").then((m) => m.deployCommand),
|
|
52
|
+
rollback: () => import("./deploy-N88l4Kt3.mjs").then((m) => m.rollbackCommand),
|
|
53
|
+
token: () => import("./token-rTD9jxPZ.mjs").then((m) => m.tokenCommand),
|
|
54
|
+
db: () => import("./db-CtUG4vfG.mjs").then((m) => m.dbCommand),
|
|
55
|
+
open: () => import("./open-xLiGUz_W.mjs").then((m) => m.openCommand),
|
|
56
|
+
docs: () => import("./docs-C3aOAx_A.mjs").then((m) => m.docsCommand),
|
|
57
|
+
skill: () => import("./skill-jbmERP1X.mjs").then((m) => m.skillCommand),
|
|
58
|
+
upgrade: () => import("./upgrade-Y8ejvoeg.mjs").then((m) => m.upgradeCommand),
|
|
51
59
|
logs: planned("logs", "Tail worker logs", "2")
|
|
52
60
|
}
|
|
53
|
-
})
|
|
61
|
+
});
|
|
62
|
+
if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
|
|
63
|
+
await runMain(main);
|
|
54
64
|
//#endregion
|
|
55
65
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as apiRequest } from "./api-
|
|
2
|
-
import { n as rewriteApiPath } from "./dev-
|
|
1
|
+
import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
|
|
2
|
+
import { n as rewriteApiPath } from "./dev-DTGW0lm0.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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as writeConfig, t as readConfig } from "./config-C-z7st1X.mjs";
|
|
2
|
+
import { n as reportErrors, t as openBrowser } from "./ui-C8VuPzGp.mjs";
|
|
2
3
|
import { defineCommand } from "citty";
|
|
3
4
|
import * as p from "@clack/prompts";
|
|
4
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -78,10 +79,26 @@ const loginCommand = defineCommand({
|
|
|
78
79
|
name: "login",
|
|
79
80
|
description: "Sign in to Canopy via OneLogin"
|
|
80
81
|
},
|
|
81
|
-
|
|
82
|
+
args: { json: {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
default: false,
|
|
85
|
+
description: "Emit machine-readable status to stdout (for agents/CI)"
|
|
86
|
+
} },
|
|
87
|
+
run: reportErrors("login", async ({ args }) => {
|
|
88
|
+
const json = args.json;
|
|
89
|
+
try {
|
|
90
|
+
await login(json);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (json) console.log(JSON.stringify({
|
|
93
|
+
status: "error",
|
|
94
|
+
message: err instanceof Error ? err.message : String(err)
|
|
95
|
+
}));
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
})
|
|
82
99
|
});
|
|
83
|
-
async function login() {
|
|
84
|
-
p.intro("canopy login");
|
|
100
|
+
async function login(json) {
|
|
101
|
+
if (!json) p.intro("canopy login");
|
|
85
102
|
const config = readConfig();
|
|
86
103
|
const { issuer, cliClientId, apiUrl } = config;
|
|
87
104
|
if (cliClientId.includes("REPLACE")) throw new Error("The CLI's OneLogin app is not configured yet (set CANOPY_OIDC_CLIENT_ID or packages/cli/src/config.ts).");
|
|
@@ -101,18 +118,24 @@ async function login() {
|
|
|
101
118
|
code_challenge_method: "S256"
|
|
102
119
|
}).toString();
|
|
103
120
|
const authorizeHref = authorizeUrl.toString();
|
|
104
|
-
p.note(`If nothing opens, visit:\n${authorizeHref}`, "Opening OneLogin in your browser");
|
|
105
121
|
openBrowser(authorizeHref);
|
|
106
|
-
const spin = p.spinner();
|
|
107
|
-
|
|
122
|
+
const spin = json ? void 0 : p.spinner();
|
|
123
|
+
if (json) console.log(JSON.stringify({
|
|
124
|
+
status: "waiting",
|
|
125
|
+
url: authorizeHref
|
|
126
|
+
}));
|
|
127
|
+
else {
|
|
128
|
+
p.note(`If nothing opens, visit:\n${authorizeHref}`, "Opening OneLogin in your browser");
|
|
129
|
+
spin?.start("Waiting for you to sign in in the browser…");
|
|
130
|
+
}
|
|
108
131
|
let code;
|
|
109
132
|
try {
|
|
110
133
|
code = await waitForCallback(state);
|
|
111
134
|
} catch (err) {
|
|
112
|
-
spin
|
|
135
|
+
spin?.error("Sign-in failed.");
|
|
113
136
|
throw err;
|
|
114
137
|
}
|
|
115
|
-
spin
|
|
138
|
+
spin?.stop("Signed in with OneLogin.");
|
|
116
139
|
const tokenRes = await fetch(discovery.token_endpoint, {
|
|
117
140
|
method: "POST",
|
|
118
141
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -140,7 +163,12 @@ async function login() {
|
|
|
140
163
|
tokenExpiresAt: expires_at,
|
|
141
164
|
email: user.email
|
|
142
165
|
});
|
|
143
|
-
|
|
166
|
+
if (json) console.log(JSON.stringify({
|
|
167
|
+
status: "ok",
|
|
168
|
+
email: user.email,
|
|
169
|
+
expiresAt: expires_at
|
|
170
|
+
}));
|
|
171
|
+
else p.outro(`Logged in as ${user.name} <${user.email}> (token expires ${expires_at}).`);
|
|
144
172
|
}
|
|
145
173
|
//#endregion
|
|
146
174
|
export { loginCommand, logoutCommand, whoamiCommand };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as resolveProject, r as projectUrl, t as projectDirArgs } from "./project-CpiPLNby.mjs";
|
|
2
|
+
import { n as reportErrors, t as openBrowser } from "./ui-C8VuPzGp.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/open.ts
|
|
5
5
|
/**
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
|
|
2
|
+
import { r as projectUrl } from "./project-CpiPLNby.mjs";
|
|
3
|
+
import { i as unwrap, n as reportErrors, r as requireTty } from "./ui-C8VuPzGp.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
//#region src/template.ts
|
|
10
|
+
/**
|
|
11
|
+
* Template registry + scaffolder for `canopy create`. Templates are real,
|
|
12
|
+
* runnable project directories in packages/templates — linted and built like
|
|
13
|
+
* any code — copied into the CLI's dist/templates at build time
|
|
14
|
+
* (scripts/sync-templates.mjs) so the published tarball is self-contained.
|
|
15
|
+
*
|
|
16
|
+
* Scaffolding copies a template recursively, substitutes {{name}}/{{slug}},
|
|
17
|
+
* and renames _gitignore → .gitignore (npm strips .gitignore files from
|
|
18
|
+
* published tarballs, so templates can't ship one under its real name).
|
|
19
|
+
*/
|
|
20
|
+
const TEMPLATES = [
|
|
21
|
+
{
|
|
22
|
+
id: "static",
|
|
23
|
+
label: "Static site",
|
|
24
|
+
hint: "plain html/css/js, no build step (default)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "spa",
|
|
28
|
+
label: "React SPA",
|
|
29
|
+
hint: "Vite + React + TypeScript, SDK from npm",
|
|
30
|
+
needsInstall: true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "worker",
|
|
34
|
+
label: "React SPA + backend Worker",
|
|
35
|
+
hint: "adds Hono handlers in worker/ — Phase 2 preview",
|
|
36
|
+
needsInstall: true,
|
|
37
|
+
hidden: true
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
function templatesRoot() {
|
|
41
|
+
const base = fileURLToPath(new URL(".", import.meta.url));
|
|
42
|
+
for (const candidate of [join(base, "templates"), join(base, "..", "..", "templates")]) if (existsSync(join(candidate, "static"))) return candidate;
|
|
43
|
+
throw new Error("Template files not found — the CLI build is incomplete (missing templates/).");
|
|
44
|
+
}
|
|
45
|
+
function scaffoldTemplate(templateId, targetDir, vars) {
|
|
46
|
+
const source = join(templatesRoot(), templateId);
|
|
47
|
+
mkdirSync(targetDir, { recursive: true });
|
|
48
|
+
for (const entry of readdirSync(source, {
|
|
49
|
+
recursive: true,
|
|
50
|
+
withFileTypes: true
|
|
51
|
+
})) {
|
|
52
|
+
const dest = join(targetDir, relative(source, join(entry.parentPath, entry.name)).split(sep).map((part) => part === "_gitignore" ? ".gitignore" : part).join(sep));
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
mkdirSync(dest, { recursive: true });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
writeFileSync(dest, readFileSync(join(entry.parentPath, entry.name), "utf8").replaceAll("{{name}}", vars.name).replaceAll("{{slug}}", vars.slug));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/projects.ts
|
|
62
|
+
/**
|
|
63
|
+
* canopy create | projects | delete — the project registry from the terminal
|
|
64
|
+
* (docs/04-roadmap.md Phase 1 item 2). `create` registers the project, then
|
|
65
|
+
* scaffolds ./<slug>/ from the starter template (--no-scaffold to skip, e.g.
|
|
66
|
+
* for an existing directory).
|
|
67
|
+
*/
|
|
68
|
+
function slugify(input) {
|
|
69
|
+
return input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(^-+|-+$)/g, "").slice(0, 40);
|
|
70
|
+
}
|
|
71
|
+
const createCommand = defineCommand({
|
|
72
|
+
meta: {
|
|
73
|
+
name: "create",
|
|
74
|
+
description: "Create a new Canopy project"
|
|
75
|
+
},
|
|
76
|
+
args: {
|
|
77
|
+
name: {
|
|
78
|
+
type: "positional",
|
|
79
|
+
description: "Project name (prompted for when omitted)",
|
|
80
|
+
required: false
|
|
81
|
+
},
|
|
82
|
+
slug: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "URL slug (defaults to a slugified name)"
|
|
85
|
+
},
|
|
86
|
+
template: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: `Template to scaffold: ${TEMPLATES.map((t) => t.id).join(" | ")} (picker when omitted)`
|
|
89
|
+
},
|
|
90
|
+
scaffold: {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
default: true,
|
|
93
|
+
description: "Scaffold ./<slug>/ from the starter template",
|
|
94
|
+
negativeDescription: "Only register — no files written"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
run: reportErrors("create", async ({ args }) => {
|
|
98
|
+
let name = args._.join(" ").trim();
|
|
99
|
+
p.intro("canopy create");
|
|
100
|
+
if (!name) {
|
|
101
|
+
requireTty("pass the project name as an argument.");
|
|
102
|
+
name = unwrap(await p.text({
|
|
103
|
+
message: "Project name",
|
|
104
|
+
validate: (value) => value?.trim() ? void 0 : "A project name is required."
|
|
105
|
+
})).trim();
|
|
106
|
+
}
|
|
107
|
+
let slug = args.slug;
|
|
108
|
+
if (!slug && process.stdin.isTTY) slug = unwrap(await p.text({
|
|
109
|
+
message: "Slug",
|
|
110
|
+
defaultValue: slugify(name),
|
|
111
|
+
placeholder: slugify(name),
|
|
112
|
+
validate: (value) => !value || /^[a-z0-9][a-z0-9-]*$/.test(value) ? void 0 : "Lowercase letters, digits and dashes only."
|
|
113
|
+
})).trim();
|
|
114
|
+
slug ||= slugify(name);
|
|
115
|
+
let template = TEMPLATES.find((t) => t.id === (args.template ?? "static"));
|
|
116
|
+
if (!template) throw new Error(`Unknown template "${args.template}" — available: ${TEMPLATES.map((t) => t.id).join(", ")}.`);
|
|
117
|
+
if (args.scaffold && !args.template && process.stdin.isTTY) {
|
|
118
|
+
const id = unwrap(await p.select({
|
|
119
|
+
message: "Template",
|
|
120
|
+
options: TEMPLATES.filter((t) => !t.hidden).map((t) => ({
|
|
121
|
+
value: t.id,
|
|
122
|
+
label: t.label,
|
|
123
|
+
hint: t.hint
|
|
124
|
+
}))
|
|
125
|
+
}));
|
|
126
|
+
template = TEMPLATES.find((t) => t.id === id) ?? template;
|
|
127
|
+
}
|
|
128
|
+
if (template.hidden) p.log.warn("Per-project Workers are not deployable yet (Phase 2) — `canopy deploy` publishes the frontend only for now.");
|
|
129
|
+
const targetDir = resolve(slug);
|
|
130
|
+
if (args.scaffold && existsSync(targetDir)) throw new Error(`Directory ./${slug} already exists — remove it, or use --no-scaffold to only register.`);
|
|
131
|
+
const spin = p.spinner();
|
|
132
|
+
spin.start(`Registering ${slug}…`);
|
|
133
|
+
let project;
|
|
134
|
+
try {
|
|
135
|
+
({project} = await apiRequest("POST", "/projects", {
|
|
136
|
+
slug,
|
|
137
|
+
name
|
|
138
|
+
}));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
spin.error("Create failed.");
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
spin.stop(`Created ${project.name} — ${projectUrl(project.slug)}`);
|
|
144
|
+
if (args.scaffold) {
|
|
145
|
+
scaffoldTemplate(template.id, targetDir, {
|
|
146
|
+
name,
|
|
147
|
+
slug
|
|
148
|
+
});
|
|
149
|
+
const steps = [
|
|
150
|
+
`cd ${slug}`,
|
|
151
|
+
...template.needsInstall ? ["npm install"] : [],
|
|
152
|
+
"canopy dev (or deploy right away: canopy deploy)"
|
|
153
|
+
];
|
|
154
|
+
p.note(steps.join("\n"), `Scaffolded ./${slug}/ (${template.id}) — next`);
|
|
155
|
+
p.outro(`Live URL (after first deploy): ${projectUrl(project.slug)}`);
|
|
156
|
+
} else p.outro("Registered without scaffolding — add a canopy.json to deploy an existing app.");
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
const projectsCommand = defineCommand({
|
|
160
|
+
meta: {
|
|
161
|
+
name: "projects",
|
|
162
|
+
description: "List your projects"
|
|
163
|
+
},
|
|
164
|
+
args: { all: {
|
|
165
|
+
type: "boolean",
|
|
166
|
+
default: false,
|
|
167
|
+
description: "Include projects you can't edit"
|
|
168
|
+
} },
|
|
169
|
+
run: reportErrors("projects", async ({ args }) => {
|
|
170
|
+
const { projects, is_admin: isAdmin } = await apiRequest("GET", "/projects");
|
|
171
|
+
const mine = args.all || isAdmin ? projects : projects.filter((project) => project.role);
|
|
172
|
+
if (mine.length === 0) {
|
|
173
|
+
console.log(projects.length === 0 ? "No projects yet — run `canopy create`." : `No projects of your own yet — run \`canopy create\`, or --all to list all ${projects.length}.`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const width = Math.max(...mine.map((project) => project.slug.length));
|
|
177
|
+
for (const project of mine) {
|
|
178
|
+
const role = project.role ?? (isAdmin ? "admin" : `owner: ${project.owner_email}`);
|
|
179
|
+
console.log(`${project.slug.padEnd(width)} ${project.status.padEnd(7)} ${role.padEnd(6)} ${projectUrl(project.slug)} ${project.name}`);
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
const deleteCommand = defineCommand({
|
|
184
|
+
meta: {
|
|
185
|
+
name: "delete",
|
|
186
|
+
description: "Delete a project (full teardown)"
|
|
187
|
+
},
|
|
188
|
+
args: {
|
|
189
|
+
slug: {
|
|
190
|
+
type: "positional",
|
|
191
|
+
description: "Project slug (picked from a list when omitted)",
|
|
192
|
+
required: false
|
|
193
|
+
},
|
|
194
|
+
yes: {
|
|
195
|
+
type: "boolean",
|
|
196
|
+
default: false,
|
|
197
|
+
description: "Skip the type-the-slug confirmation"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
run: reportErrors("delete", async ({ args }) => {
|
|
201
|
+
let slug = args.slug;
|
|
202
|
+
if (!slug) {
|
|
203
|
+
requireTty("pass the project slug as an argument.");
|
|
204
|
+
const listing = await apiRequest("GET", "/projects");
|
|
205
|
+
const projects = listing.is_admin ? listing.projects : listing.projects.filter((project) => project.role === "owner");
|
|
206
|
+
if (projects.length === 0) throw new Error("No projects to delete.");
|
|
207
|
+
slug = unwrap(await p.select({
|
|
208
|
+
message: "Project to delete",
|
|
209
|
+
options: projects.map((project) => ({
|
|
210
|
+
value: project.slug,
|
|
211
|
+
label: project.slug,
|
|
212
|
+
hint: project.name
|
|
213
|
+
}))
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
if (!args.yes) {
|
|
217
|
+
requireTty("pass --yes to delete without confirmation.");
|
|
218
|
+
if (unwrap(await p.text({ message: `Delete "${slug}" permanently? This tears down its database and files. Type the slug to confirm.` })).trim() !== slug) {
|
|
219
|
+
p.cancel("Aborted.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
await apiRequest("DELETE", `/projects/${slug}`);
|
|
224
|
+
p.log.success(`Deleted ${slug}.`);
|
|
225
|
+
})
|
|
226
|
+
});
|
|
227
|
+
//#endregion
|
|
228
|
+
export { createCommand, deleteCommand, projectsCommand };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: canopy
|
|
3
|
+
description: >-
|
|
4
|
+
Build, run, and deploy projects on Canopy — Monks' managed platform for
|
|
5
|
+
vibecoded apps (hosting + database + blob storage + AI). Use this whenever the
|
|
6
|
+
user wants to create a Canopy project, run/deploy one, manage its data or
|
|
7
|
+
deploy tokens, or asks how to use the `canopy` CLI. All platform docs are
|
|
8
|
+
available offline via `canopy docs`.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Canopy
|
|
12
|
+
|
|
13
|
+
Canopy is Monks' managed platform for vibecoded projects. Everything is driven
|
|
14
|
+
by one CLI: **`canopy`**. This skill tells you how to drive it; the CLI itself
|
|
15
|
+
carries the full, always-current documentation.
|
|
16
|
+
|
|
17
|
+
## First: is the CLI installed and signed in?
|
|
18
|
+
|
|
19
|
+
Run these before anything else, in order:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
canopy --version # is the CLI installed?
|
|
23
|
+
canopy whoami # is there a valid login?
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **`canopy --version` fails / command not found** → install it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @mediamonks/canopy-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
(Needs Node.js 20+. If `npm` is missing, tell the user to install Node first.)
|
|
33
|
+
|
|
34
|
+
- **`canopy whoami` says not logged in / expired** → run:
|
|
35
|
+
```bash
|
|
36
|
+
canopy login
|
|
37
|
+
```
|
|
38
|
+
This opens the user's browser. **Then STOP and tell the user to complete the
|
|
39
|
+
sign-in in their browser.** The command waits for them and finishes on its own.
|
|
40
|
+
- Never ask for or type the user's password, OneLogin codes, or any
|
|
41
|
+
credentials yourself.
|
|
42
|
+
- Login only works on the user's own machine (it uses a `127.0.0.1` redirect).
|
|
43
|
+
If you're on a remote/headless box, tell the user — it can't complete there.
|
|
44
|
+
- `canopy login --json` gives machine-readable status if you need to parse it.
|
|
45
|
+
|
|
46
|
+
Skip login if `canopy whoami` already shows a valid, unexpired token.
|
|
47
|
+
|
|
48
|
+
## Then: read the docs for whatever you're doing
|
|
49
|
+
|
|
50
|
+
The CLI ships the full documentation and keeps it fresh from the platform. List
|
|
51
|
+
the topics and read the relevant one _before_ running unfamiliar commands:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
canopy docs # list all topics
|
|
55
|
+
canopy docs deploy # print one topic
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Available topics include: `getting-started`, `cli` (full command reference),
|
|
59
|
+
`deploy`, `dev`, `data`, `ci`, `troubleshooting`. Always prefer `canopy docs`
|
|
60
|
+
and `canopy <command> --help` over guessing flags — they are the source of
|
|
61
|
+
truth and never go stale.
|
|
62
|
+
|
|
63
|
+
## Command map (quick orientation — see `canopy docs cli` for detail)
|
|
64
|
+
|
|
65
|
+
- Create a project: `canopy create`
|
|
66
|
+
- Run locally: `canopy dev` (live data) or `canopy dev --local` (offline sandbox)
|
|
67
|
+
- Deploy: `canopy deploy` (add `--build` if the project has a build step)
|
|
68
|
+
- Roll back: `canopy rollback`
|
|
69
|
+
- Open the live site: `canopy open`
|
|
70
|
+
- Manage data: `canopy db pull | snapshot | snapshots | restore`
|
|
71
|
+
- CI credentials: `canopy token create | list | revoke`
|
|
72
|
+
|
|
73
|
+
## Safety rules
|
|
74
|
+
|
|
75
|
+
- **Destructive commands** — `canopy db restore` and `canopy delete` wipe data.
|
|
76
|
+
Confirm intent with the user first, and take a `canopy db snapshot` before
|
|
77
|
+
restores.
|
|
78
|
+
- **Deploys are public.** `canopy deploy` makes a version live. Confirm before
|
|
79
|
+
deploying unless the user clearly asked you to.
|
|
80
|
+
- Keep the CLI current: `canopy upgrade` (this also refreshes this skill).
|