@mediamonks/canopy-cli 0.1.0 → 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.
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: CI and deploy tokens
3
+ summary: Push-to-main deploys using a project-scoped deploy token.
4
+ ---
5
+
6
+ # CI and deploy tokens
7
+
8
+ Interactive `canopy login` is for humans at a terminal. For automated deploys
9
+ (GitHub Actions, etc.) use a **deploy token**: a project-scoped credential that
10
+ can deploy that one project and nothing else.
11
+
12
+ ## Create a token
13
+
14
+ From inside the project directory:
15
+
16
+ ```bash
17
+ canopy token create --name "GitHub Actions"
18
+ ```
19
+
20
+ This prints the token **once** — copy it immediately and store it as a CI secret
21
+ (e.g. `CANOPY_DEPLOY_TOKEN`). It is not shown again.
22
+
23
+ Manage tokens:
24
+
25
+ ```bash
26
+ canopy token list
27
+ canopy token revoke <id>
28
+ ```
29
+
30
+ ## Use it in CI
31
+
32
+ The CLI picks up `CANOPY_DEPLOY_TOKEN` from the environment and uses it instead
33
+ of an interactive login. A minimal GitHub Actions step:
34
+
35
+ ```yaml
36
+ - run: npm install -g @mediamonks/canopy-cli
37
+ - run: canopy deploy --build
38
+ env:
39
+ CANOPY_DEPLOY_TOKEN: ${{ secrets.CANOPY_DEPLOY_TOKEN }}
40
+ ```
41
+
42
+ No `canopy login` step is needed — the token is the credential.
43
+
44
+ ## Notes
45
+
46
+ - A deploy token can only deploy its own project. If it strays to another
47
+ project's resources, the platform rejects it.
48
+ - Revoke a token the moment it might be exposed; create a fresh one to replace it.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: CLI command reference
3
+ summary: Every canopy command, its flags, and what it does.
4
+ ---
5
+
6
+ # CLI command reference
7
+
8
+ Run `canopy <command> --help` for the authoritative flags of any command. This
9
+ is the overview.
10
+
11
+ ## Auth
12
+
13
+ - `canopy login` — sign in via OneLogin (opens the browser; finish there).
14
+ Add `--json` for machine-readable status output (for agents/CI).
15
+ - `canopy whoami` — show the current login and token expiry.
16
+ - `canopy logout` — forget the local CLI token.
17
+
18
+ ## Projects
19
+
20
+ - `canopy create [name]` — create and register a project.
21
+ - `--slug <slug>` — URL slug (defaults to a slugified name).
22
+ - `--template <id>` — `static` | `spa` | `worker` (picker when omitted).
23
+ - `--scaffold` — write `./<slug>/` from the starter template.
24
+ - `canopy projects` — list your projects. `--all` includes ones you can't edit.
25
+ - `canopy delete [slug]` — full teardown of a project. `--yes` skips the
26
+ type-the-slug confirmation.
27
+
28
+ ## Develop
29
+
30
+ - `canopy dev` — run the project locally against live platform data.
31
+ - `--port <port>` — dev server port.
32
+ - `--local` — offline sandbox (Miniflare D1/R2 under `.canopy/local/`).
33
+ - `--pull` — hydrate the sandbox from a fresh prod export first (with `--local`).
34
+
35
+ ## Deploy
36
+
37
+ - `canopy deploy` — build and deploy the project.
38
+ - `--build` — run the `canopy.json` build command first.
39
+ - `canopy rollback [version]` — make an earlier deploy live again, e.g. `v3`
40
+ (picked from history when omitted). `--dir <path>` to target another directory.
41
+ - `canopy open` — open the deployed project in your browser.
42
+
43
+ ## Data
44
+
45
+ - `canopy db pull` — download a fresh SQL dump. `--out <file>` to name it.
46
+ - `canopy db snapshot` — store a dump server-side (restorable anytime).
47
+ - `canopy db snapshots` — list stored snapshots.
48
+ - `canopy db restore [file]` — replace ALL project data from a `.sql` file or
49
+ `--snapshot <key>`. `--yes` skips the confirmation. **Destructive.**
50
+
51
+ ## CI / deploy tokens
52
+
53
+ - `canopy token create` — create a project-scoped deploy token for CI.
54
+ `--name "<label>"` to label it.
55
+ - `canopy token list` — list the project's deploy tokens.
56
+ - `canopy token revoke <id>` — revoke a token.
57
+
58
+ See `canopy docs ci` for wiring push-to-main deploys.
59
+
60
+ ## Maintenance
61
+
62
+ - `canopy upgrade` — update the CLI to the latest version. `--version <v>` pins a
63
+ release; `--pm <npm|pnpm|yarn|bun>` overrides the package-manager guess.
64
+ Upgrading also refreshes the installed Canopy agent skill.
65
+ - `canopy docs [topic]` — list docs, or print one. `--refresh` re-fetches from
66
+ the platform.
67
+ - `canopy skill install` — (re)install the Canopy agent skill into this repo.
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: Working with project data
3
+ summary: Pulling, snapshotting, and restoring your project database.
4
+ ---
5
+
6
+ # Working with project data
7
+
8
+ Each Canopy project gets its own database (D1) and blob storage (R2). Your app
9
+ reads and writes them through the Canopy SDK at runtime; the CLI gives you tools
10
+ to manage the data around it.
11
+
12
+ ## Download a dump
13
+
14
+ ```bash
15
+ canopy db pull # writes <slug>-<timestamp>.sql
16
+ canopy db pull --out data.sql # choose the filename
17
+ ```
18
+
19
+ A plain SQL dump you can inspect, diff, or keep as a backup.
20
+
21
+ ## Snapshots (server-side backups)
22
+
23
+ ```bash
24
+ canopy db snapshot # store a dump on the platform
25
+ canopy db snapshots # list stored snapshots
26
+ ```
27
+
28
+ Snapshots live on the platform and can be restored at any time — handy before a
29
+ risky change.
30
+
31
+ ## Restore — destructive
32
+
33
+ Restoring **replaces ALL project data**:
34
+
35
+ ```bash
36
+ canopy db restore backup.sql # from a local dump
37
+ canopy db restore --snapshot <key> # from a server-side snapshot
38
+ ```
39
+
40
+ You'll be asked to type the project slug to confirm. `--yes` skips that prompt —
41
+ use it only in scripts you trust.
42
+
43
+ ## Safe workflow for agents
44
+
45
+ Before any destructive data change:
46
+
47
+ 1. `canopy db snapshot` — take a restore point.
48
+ 2. Make the change (or test it first with `canopy dev --local --pull`).
49
+ 3. If something goes wrong, `canopy db restore --snapshot <key>`.
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: Deploying a project
3
+ summary: How canopy deploy builds, uploads, and makes a version live.
4
+ ---
5
+
6
+ # Deploying a project
7
+
8
+ From inside a project directory:
9
+
10
+ ```bash
11
+ canopy deploy
12
+ ```
13
+
14
+ This uploads your built assets to the platform, creates a new immutable deploy
15
+ version, and makes it live. Each deploy is versioned (`v1`, `v2`, …).
16
+
17
+ ## Building first
18
+
19
+ If your project needs a build step (React SPA, anything with a bundler), declare
20
+ the build command in `canopy.json` and pass `--build`:
21
+
22
+ ```bash
23
+ canopy deploy --build
24
+ ```
25
+
26
+ `--build` runs the project's configured build command before uploading. Without
27
+ it, `canopy deploy` uploads whatever is already built (or the static files as-is
28
+ for a `static` project).
29
+
30
+ ## Rolling back
31
+
32
+ Every deploy is kept, so you can instantly revert:
33
+
34
+ ```bash
35
+ canopy rollback # pick a version from history
36
+ canopy rollback v3 # make v3 live again
37
+ ```
38
+
39
+ Rollback re-points the live version at an existing deploy — it does not rebuild.
40
+
41
+ ## Seeing the live site
42
+
43
+ ```bash
44
+ canopy open
45
+ ```
46
+
47
+ ## Deploying from CI
48
+
49
+ For automated push-to-main deploys, use a deploy token instead of an interactive
50
+ login — see `canopy docs ci`.
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Local development
3
+ summary: Running a project locally against live or sandboxed data.
4
+ ---
5
+
6
+ # Local development
7
+
8
+ From inside a project directory:
9
+
10
+ ```bash
11
+ canopy dev
12
+ ```
13
+
14
+ By default `canopy dev` runs your project locally but talks to **live platform
15
+ data** (your project's real database, blobs, and AI). This is the fastest way to
16
+ develop against realistic state.
17
+
18
+ - `--port <port>` — choose the dev-server port.
19
+
20
+ ## Offline sandbox
21
+
22
+ To develop without touching live data — for example on a plane, or before you
23
+ trust a destructive change — use a local sandbox backed by Miniflare:
24
+
25
+ ```bash
26
+ canopy dev --local
27
+ ```
28
+
29
+ This creates local D1 and R2 under `.canopy/local/` in your project. The sandbox
30
+ starts empty.
31
+
32
+ To seed the sandbox from a fresh copy of production data first:
33
+
34
+ ```bash
35
+ canopy dev --local --pull
36
+ ```
37
+
38
+ `.canopy/local/` is disposable — delete it to reset the sandbox.
39
+
40
+ ## Which mode should an agent use?
41
+
42
+ - Iterating on UI or read-only features → `canopy dev` (live data, realistic).
43
+ - Testing writes, migrations, or anything destructive → `canopy dev --local`
44
+ (optionally `--pull` to start from real data).
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Getting started
3
+ summary: Install the CLI, sign in with OneLogin, and ship your first project.
4
+ ---
5
+
6
+ # Getting started with Canopy
7
+
8
+ Canopy is Monks' managed platform for vibecoded projects. You build a small web
9
+ app, and Canopy hosts it, gives it a database, blob storage, and AI — no
10
+ infrastructure to manage.
11
+
12
+ This guide is written so an AI coding agent (Claude, Cursor, etc.) can follow it
13
+ on your behalf. Everything runs through one command-line tool: `canopy`.
14
+
15
+ ## 1. Install the CLI
16
+
17
+ The CLI is published on npm. Install it globally:
18
+
19
+ ```bash
20
+ npm install -g @mediamonks/canopy-cli
21
+ ```
22
+
23
+ Confirm it's on your PATH:
24
+
25
+ ```bash
26
+ canopy --version
27
+ ```
28
+
29
+ If `npm` isn't available, install Node.js 20+ first (https://nodejs.org), then
30
+ re-run the install.
31
+
32
+ ## 2. Sign in
33
+
34
+ Canopy authenticates through Monks' OneLogin. Run:
35
+
36
+ ```bash
37
+ canopy login
38
+ ```
39
+
40
+ This opens your browser. **Finish the sign-in there** — the CLI waits for the
41
+ browser to redirect back, then stores a token in `~/.canopy/config.json`.
42
+
43
+ > If you are an agent: run `canopy login`, then stop and tell the human to
44
+ > complete the sign-in in the browser window that just opened. Never ask for or
45
+ > type the user's password or any codes yourself.
46
+
47
+ Check who you are at any time:
48
+
49
+ ```bash
50
+ canopy whoami
51
+ ```
52
+
53
+ ## 3. Create a project
54
+
55
+ ```bash
56
+ canopy create
57
+ ```
58
+
59
+ This prompts for a name and a starter template, scaffolds a runnable project
60
+ directory, and registers the project on the platform. To skip the prompts:
61
+
62
+ ```bash
63
+ canopy create "My App" --scaffold --template static
64
+ ```
65
+
66
+ ## 4. Run it locally
67
+
68
+ ```bash
69
+ cd my-app
70
+ canopy dev
71
+ ```
72
+
73
+ Your project runs against live platform data by default. For an offline sandbox
74
+ with local data, use `canopy dev --local`.
75
+
76
+ ## 5. Deploy
77
+
78
+ ```bash
79
+ canopy deploy
80
+ ```
81
+
82
+ Then open the live site:
83
+
84
+ ```bash
85
+ canopy open
86
+ ```
87
+
88
+ ## Where to go next
89
+
90
+ Everything the CLI can do is documented and available offline. List the topics:
91
+
92
+ ```bash
93
+ canopy docs
94
+ ```
95
+
96
+ Read one, e.g.:
97
+
98
+ ```bash
99
+ canopy docs deploy
100
+ ```
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Troubleshooting
3
+ summary: Common errors and how to resolve them.
4
+ ---
5
+
6
+ # Troubleshooting
7
+
8
+ ## `command not found: canopy`
9
+
10
+ The CLI isn't installed or isn't on your PATH. Install it:
11
+
12
+ ```bash
13
+ npm install -g @mediamonks/canopy-cli
14
+ ```
15
+
16
+ If it installs but still isn't found, your npm global bin directory isn't on your
17
+ PATH. Run `npm bin -g` to find it and add that directory to your PATH.
18
+
19
+ ## `Not logged in — run canopy login`
20
+
21
+ You have no valid CLI token. Run `canopy login` and finish the sign-in in the
22
+ browser. Check status with `canopy whoami`.
23
+
24
+ ## `Session expired — run canopy login again`
25
+
26
+ Your token expired. Just run `canopy login` again.
27
+
28
+ ## The browser didn't open during login
29
+
30
+ `canopy login` prints the sign-in URL as a fallback — open it manually in a
31
+ browser on the **same machine** as the CLI (login uses a local loopback
32
+ redirect, so it must be the same machine).
33
+
34
+ ## `Port 8976 is in use`
35
+
36
+ A previous login attempt is still holding the callback port. Close it (or wait a
37
+ moment) and retry `canopy login`.
38
+
39
+ ## Login hangs and never completes
40
+
41
+ Login only works when the CLI runs on the machine whose browser you're using
42
+ (the redirect goes to `127.0.0.1`). It cannot complete on a remote/headless box.
43
+ Run it on your own laptop.
44
+
45
+ ## `Deploy token rejected`
46
+
47
+ The `CANOPY_DEPLOY_TOKEN` was revoked or is wrong. Create a new one with
48
+ `canopy token create` (or in the dashboard) and update your CI secret.
49
+
50
+ ## Docs are stale
51
+
52
+ `canopy docs` caches the platform's docs. Force a refresh:
53
+
54
+ ```bash
55
+ canopy docs <topic> --refresh
56
+ ```
57
+
58
+ ## Still stuck?
59
+
60
+ Run the failing command again and share its full output. `canopy whoami` and
61
+ `canopy --version` are useful to include.
@@ -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,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { t as version } from "./package-OcWG7JAW.mjs";
2
3
  import { i as runRefresh, r as notifyOfUpdate } from "./version-check-CoWgQs2h.mjs";
3
4
  import { defineCommand, runMain } from "citty";
4
- //#region package.json
5
- var version = "0.1.0";
6
- //#endregion
7
5
  //#region src/index.ts
8
6
  /**
9
7
  * canopy — the Canopy CLI.
@@ -43,9 +41,9 @@ const main = defineCommand({
43
41
  description: "canopy — create and deploy vibecoded projects"
44
42
  },
45
43
  subCommands: {
46
- login: () => import("./login-DsWvYc8C.mjs").then((m) => m.loginCommand),
47
- whoami: () => import("./login-DsWvYc8C.mjs").then((m) => m.whoamiCommand),
48
- logout: () => import("./login-DsWvYc8C.mjs").then((m) => m.logoutCommand),
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),
49
47
  create: () => import("./projects-DJT_mJDR.mjs").then((m) => m.createCommand),
50
48
  projects: () => import("./projects-DJT_mJDR.mjs").then((m) => m.projectsCommand),
51
49
  delete: () => import("./projects-DJT_mJDR.mjs").then((m) => m.deleteCommand),
@@ -55,7 +53,9 @@ const main = defineCommand({
55
53
  token: () => import("./token-rTD9jxPZ.mjs").then((m) => m.tokenCommand),
56
54
  db: () => import("./db-CtUG4vfG.mjs").then((m) => m.dbCommand),
57
55
  open: () => import("./open-xLiGUz_W.mjs").then((m) => m.openCommand),
58
- upgrade: () => import("./upgrade-TbMOjkLY.mjs").then((m) => m.upgradeCommand),
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),
59
59
  logs: planned("logs", "Tail worker logs", "2")
60
60
  }
61
61
  });
@@ -79,10 +79,26 @@ const loginCommand = defineCommand({
79
79
  name: "login",
80
80
  description: "Sign in to Canopy via OneLogin"
81
81
  },
82
- run: reportErrors("login", () => login())
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
+ })
83
99
  });
84
- async function login() {
85
- p.intro("canopy login");
100
+ async function login(json) {
101
+ if (!json) p.intro("canopy login");
86
102
  const config = readConfig();
87
103
  const { issuer, cliClientId, apiUrl } = config;
88
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).");
@@ -102,18 +118,24 @@ async function login() {
102
118
  code_challenge_method: "S256"
103
119
  }).toString();
104
120
  const authorizeHref = authorizeUrl.toString();
105
- p.note(`If nothing opens, visit:\n${authorizeHref}`, "Opening OneLogin in your browser");
106
121
  openBrowser(authorizeHref);
107
- const spin = p.spinner();
108
- spin.start("Waiting for you to sign in in the browser…");
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
+ }
109
131
  let code;
110
132
  try {
111
133
  code = await waitForCallback(state);
112
134
  } catch (err) {
113
- spin.error("Sign-in failed.");
135
+ spin?.error("Sign-in failed.");
114
136
  throw err;
115
137
  }
116
- spin.stop("Signed in with OneLogin.");
138
+ spin?.stop("Signed in with OneLogin.");
117
139
  const tokenRes = await fetch(discovery.token_endpoint, {
118
140
  method: "POST",
119
141
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -141,7 +163,12 @@ async function login() {
141
163
  tokenExpiresAt: expires_at,
142
164
  email: user.email
143
165
  });
144
- p.outro(`Logged in as ${user.name} <${user.email}> (token expires ${expires_at}).`);
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}).`);
145
172
  }
146
173
  //#endregion
147
174
  export { loginCommand, logoutCommand, whoamiCommand };
@@ -0,0 +1,4 @@
1
+ //#region package.json
2
+ var version = "0.2.0";
3
+ //#endregion
4
+ export { version as t };
@@ -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).
@@ -0,0 +1,84 @@
1
+ import { t as version } from "./package-OcWG7JAW.mjs";
2
+ import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
3
+ import { defineCommand } from "citty";
4
+ import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { join, relative } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ //#region src/skill.ts
8
+ /**
9
+ * canopy skill install — write the Canopy agent skill into the current repo.
10
+ *
11
+ * To avoid vendor lock-in the skill lives in the tool-neutral `.agents/skills/`
12
+ * tree, and `.claude/skills/canopy` is a symlink to it (so Claude Code picks it
13
+ * up while other agents can read `.agents/`). The skill files are bundled with
14
+ * the CLI (dist/skill in the published package, packages/cli/skill in dev), so
15
+ * `canopy upgrade` re-runs this to keep the installed skill in sync.
16
+ */
17
+ const SKILL_NAME = "canopy";
18
+ const AGENTS_REL = join(".agents", "skills", SKILL_NAME);
19
+ const CLAUDE_REL = join(".claude", "skills", SKILL_NAME);
20
+ const STAMP = ".canopy-skill-version";
21
+ /** The bundled skill source: dist/skill/canopy published, packages/cli/skill in dev. */
22
+ function bundledSkillDir() {
23
+ const base = fileURLToPath(new URL(".", import.meta.url));
24
+ for (const candidate of [join(base, "skill", SKILL_NAME), join(base, "..", "skill", SKILL_NAME)]) if (existsSync(join(candidate, "SKILL.md"))) return candidate;
25
+ throw new Error("Skill files not found — the CLI build is incomplete (missing skill/).");
26
+ }
27
+ /** Point `.claude/skills/canopy` at the `.agents` copy; copy as a fallback. */
28
+ function linkClaude(root) {
29
+ const linkPath = join(root, CLAUDE_REL);
30
+ const target = relative(join(root, ".claude", "skills"), join(root, AGENTS_REL));
31
+ mkdirSync(join(root, ".claude", "skills"), { recursive: true });
32
+ rmSync(linkPath, {
33
+ recursive: true,
34
+ force: true
35
+ });
36
+ try {
37
+ symlinkSync(target, linkPath, "dir");
38
+ return "symlink";
39
+ } catch {
40
+ cpSync(join(root, AGENTS_REL), linkPath, { recursive: true });
41
+ return "copy";
42
+ }
43
+ }
44
+ /** Install/refresh the skill under `root`. Returns the absolute `.agents` path. */
45
+ function installSkill(root) {
46
+ const source = bundledSkillDir();
47
+ const agentsDir = join(root, AGENTS_REL);
48
+ rmSync(agentsDir, {
49
+ recursive: true,
50
+ force: true
51
+ });
52
+ mkdirSync(agentsDir, { recursive: true });
53
+ cpSync(source, agentsDir, { recursive: true });
54
+ writeFileSync(join(agentsDir, STAMP), `${version}\n`);
55
+ return {
56
+ agentsDir,
57
+ claudeMode: linkClaude(root)
58
+ };
59
+ }
60
+ /** Is a Canopy skill already installed under `root`? (Used by `canopy upgrade`.) */
61
+ function skillInstalled(root) {
62
+ return existsSync(join(root, AGENTS_REL, "SKILL.md"));
63
+ }
64
+ const skillCommand = defineCommand({
65
+ meta: {
66
+ name: "skill",
67
+ description: "Manage the Canopy agent skill in this repo"
68
+ },
69
+ subCommands: { install: () => defineCommand({
70
+ meta: {
71
+ name: "install",
72
+ description: "Install/refresh the Canopy skill (.agents + .claude)"
73
+ },
74
+ run: reportErrors("skill install", async () => {
75
+ const { claudeMode } = installSkill(process.cwd());
76
+ console.log(`Installed the Canopy skill (v${version}):`);
77
+ console.log(` .agents/skills/${SKILL_NAME}/ (source of truth)`);
78
+ console.log(claudeMode === "symlink" ? ` .claude/skills/${SKILL_NAME} → symlink to .agents` : ` .claude/skills/${SKILL_NAME} (copy — symlinks unavailable here)`);
79
+ console.log("\nYour agent can now use Canopy. Try: canopy docs");
80
+ })
81
+ }) }
82
+ });
83
+ //#endregion
84
+ export { skillCommand, skillInstalled };
@@ -48,14 +48,14 @@ function installArgs(pm, spec) {
48
48
  ];
49
49
  }
50
50
  }
51
- function run(pm, args) {
51
+ function run(command, args) {
52
52
  return new Promise((resolve, reject) => {
53
- const child = spawn(pm, args, {
53
+ const child = spawn(command, args, {
54
54
  stdio: "inherit",
55
55
  shell: process.platform === "win32"
56
56
  });
57
- child.on("error", (err) => reject(err.code === "ENOENT" ? /* @__PURE__ */ new Error(`\`${pm}\` was not found on your PATH — install it or pass --pm.`) : err));
58
- child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`${pm} exited with code ${code ?? "unknown"}.`)));
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
59
  });
60
60
  }
61
61
  const upgradeCommand = defineCommand({
@@ -86,6 +86,13 @@ const upgradeCommand = defineCommand({
86
86
  console.log(`Upgrading via ${pm}: ${pm} ${cmd.join(" ")}`);
87
87
  await run(pm, cmd);
88
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
+ }
89
96
  })
90
97
  });
91
98
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.1.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"
@@ -19,8 +19,8 @@
19
19
  "hono": "^4.8.0",
20
20
  "miniflare": "^4.20260714.0",
21
21
  "vite": "npm:@voidzero-dev/vite-plus-core@0.2.5",
22
- "@mediamonks/canopy": "0.0.1",
23
- "@mediamonks/canopy-data-plane": "0.0.1"
22
+ "@mediamonks/canopy-data-plane": "0.0.1",
23
+ "@mediamonks/canopy": "0.0.1"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.1.1",