@mediamonks/canopy-cli 0.3.0 → 0.4.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/docs/cli.md CHANGED
@@ -17,10 +17,16 @@ is the overview.
17
17
 
18
18
  ## Projects
19
19
 
20
- - `canopy create [name]` — create and register a project.
20
+ - `canopy create [name]` — register a **new** project (and scaffold it).
21
21
  - `--slug <slug>` — URL slug (defaults to a slugified name).
22
22
  - `--template <id>` — `static` | `spa` | `worker` (picker when omitted).
23
- - `--scaffold` — write `./<slug>/` from the starter template.
23
+ - `--scaffold` — write `./<slug>/` from the starter template (on by default;
24
+ `--no-scaffold` to only register).
25
+ - `canopy init [slug] [dir]` — set up a local folder for an **existing** project
26
+ (e.g. one created in the dashboard) — does **not** register a new project.
27
+ - `dir` — `.` for the current directory, a path, or `./<slug>/` when omitted.
28
+ - `--template <id>` — template to scaffold (picker when omitted).
29
+ - `--no-scaffold` — link an app you already have: just write a `canopy.json`.
24
30
  - `canopy projects` — list your projects. `--all` includes ones you can't edit.
25
31
  - `canopy delete [slug]` — full teardown of a project. `--yes` skips the
26
32
  type-the-slug confirmation.
@@ -60,9 +60,14 @@ This prompts for a name and a starter template, scaffolds a runnable project
60
60
  directory, and registers the project on the platform. To skip the prompts:
61
61
 
62
62
  ```bash
63
- canopy create "My App" --scaffold --template static
63
+ canopy create "My App" --scaffold --template spa
64
64
  ```
65
65
 
66
+ Templates: **`static`** (no-build html/css/js; SDK served untyped from the
67
+ platform) is the default and fine for a trivial page or two. Reach for
68
+ **`spa`** (Vite + React + TypeScript; typed SDK from npm) once you lean on the
69
+ SDK or expect the app to grow — most projects want this.
70
+
66
71
  ## 4. Run it locally
67
72
 
68
73
  ```bash
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-DyEUYkEq.mjs";
2
+ import { t as version } from "./package-BNWFYk8y.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
@@ -44,9 +44,10 @@ const main = defineCommand({
44
44
  login: () => import("./login-Dif9Y32q.mjs").then((m) => m.loginCommand),
45
45
  whoami: () => import("./login-Dif9Y32q.mjs").then((m) => m.whoamiCommand),
46
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),
47
+ create: () => import("./projects-DbKcG5kx.mjs").then((m) => m.createCommand),
48
+ init: () => import("./projects-DbKcG5kx.mjs").then((m) => m.initCommand),
49
+ projects: () => import("./projects-DbKcG5kx.mjs").then((m) => m.projectsCommand),
50
+ delete: () => import("./projects-DbKcG5kx.mjs").then((m) => m.deleteCommand),
50
51
  dev: () => import("./dev-D_GlUULk.mjs").then((m) => m.devCommand),
51
52
  deploy: () => import("./deploy-N88l4Kt3.mjs").then((m) => m.deployCommand),
52
53
  rollback: () => import("./deploy-N88l4Kt3.mjs").then((m) => m.rollbackCommand),
@@ -54,8 +55,8 @@ const main = defineCommand({
54
55
  db: () => import("./db-CtUG4vfG.mjs").then((m) => m.dbCommand),
55
56
  open: () => import("./open-xLiGUz_W.mjs").then((m) => m.openCommand),
56
57
  docs: () => import("./docs-C3aOAx_A.mjs").then((m) => m.docsCommand),
57
- skill: () => import("./skill-nN0e59rO.mjs").then((m) => m.skillCommand),
58
- upgrade: () => import("./upgrade-C5IoUEZg.mjs").then((m) => m.upgradeCommand),
58
+ skill: () => import("./skill-esz0u6PO.mjs").then((m) => m.skillCommand),
59
+ upgrade: () => import("./upgrade-D_fgGLxu.mjs").then((m) => m.upgradeCommand),
59
60
  logs: planned("logs", "Tail worker logs", "2")
60
61
  }
61
62
  });
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.3.0";
2
+ var version = "0.4.0";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,5 +1,5 @@
1
1
  import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
2
- import { r as projectUrl } from "./project-CpiPLNby.mjs";
2
+ import { a as tryReadCanopyJson, r as projectUrl } from "./project-CpiPLNby.mjs";
3
3
  import { i as unwrap, n as reportErrors, r as requireTty } from "./ui-C8VuPzGp.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
@@ -21,12 +21,12 @@ const TEMPLATES = [
21
21
  {
22
22
  id: "static",
23
23
  label: "Static site",
24
- hint: "plain html/css/js, no build step (default)"
24
+ hint: "plain html/css/js, no build; untyped SDK from the platform — simplest apps only"
25
25
  },
26
26
  {
27
27
  id: "spa",
28
28
  label: "React SPA",
29
- hint: "Vite + React + TypeScript, SDK from npm",
29
+ hint: "Vite + React + TypeScript, typed SDK from npm — best for real SDK use or growth",
30
30
  needsInstall: true
31
31
  },
32
32
  {
@@ -156,6 +156,104 @@ const createCommand = defineCommand({
156
156
  } else p.outro("Registered without scaffolding — add a canopy.json to deploy an existing app.");
157
157
  })
158
158
  });
159
+ /** Empty enough to scaffold into: missing, or holding only dotfiles (.git, .agents…). */
160
+ function isEmptyish(dir) {
161
+ if (!existsSync(dir)) return true;
162
+ return readdirSync(dir).every((name) => name.startsWith("."));
163
+ }
164
+ const initCommand = defineCommand({
165
+ meta: {
166
+ name: "init",
167
+ description: "Set up a local folder for an existing project (dashboard or CLI created)"
168
+ },
169
+ args: {
170
+ slug: {
171
+ type: "positional",
172
+ description: "Project slug (picked from your projects when omitted)",
173
+ required: false
174
+ },
175
+ dir: {
176
+ type: "positional",
177
+ description: "Target directory: '.' for the current dir, a path, or ./<slug>/ when omitted",
178
+ required: false
179
+ },
180
+ template: {
181
+ type: "string",
182
+ description: `Template to scaffold: ${TEMPLATES.map((t) => t.id).join(" | ")} (picker when omitted)`
183
+ },
184
+ scaffold: {
185
+ type: "boolean",
186
+ default: true,
187
+ description: "Scaffold from a starter template",
188
+ negativeDescription: "Only link an existing app — write a canopy.json, no template"
189
+ }
190
+ },
191
+ run: reportErrors("init", async ({ args }) => {
192
+ p.intro("canopy init");
193
+ const { projects, is_admin: isAdmin } = await apiRequest("GET", "/projects");
194
+ const editable = isAdmin ? projects : projects.filter((project) => project.role);
195
+ let slug = args.slug?.trim();
196
+ if (slug) {
197
+ if (!projects.some((project) => project.slug === slug)) throw new Error(`No project "${slug}" — check the slug, or create it with \`canopy create\`.`);
198
+ if (!editable.some((project) => project.slug === slug)) throw new Error(`You don't have edit access to "${slug}".`);
199
+ } else {
200
+ requireTty("pass the project slug as an argument.");
201
+ if (editable.length === 0) throw new Error("You have no projects to set up — create one with `canopy create`, or in the dashboard.");
202
+ slug = unwrap(await p.select({
203
+ message: "Project",
204
+ options: editable.map((project) => ({
205
+ value: project.slug,
206
+ label: project.slug,
207
+ hint: project.name
208
+ }))
209
+ }));
210
+ }
211
+ const project = projects.find((candidate) => candidate.slug === slug);
212
+ if (!project) throw new Error(`No project "${slug}".`);
213
+ const dirArg = args.dir;
214
+ const targetDir = dirArg === "." ? process.cwd() : resolve(dirArg ?? slug);
215
+ const rel = relative(process.cwd(), targetDir) || ".";
216
+ if (args.scaffold) {
217
+ if (!isEmptyish(targetDir)) throw new Error(`${rel} already has files — link it with \`canopy init ${slug} ${rel} --no-scaffold\`, or pick an empty directory.`);
218
+ let template = TEMPLATES.find((t) => t.id === (args.template ?? "static"));
219
+ if (!template) throw new Error(`Unknown template "${args.template}" — available: ${TEMPLATES.map((t) => t.id).join(", ")}.`);
220
+ if (!args.template && process.stdin.isTTY) {
221
+ const id = unwrap(await p.select({
222
+ message: "Template",
223
+ options: TEMPLATES.filter((t) => !t.hidden).map((t) => ({
224
+ value: t.id,
225
+ label: t.label,
226
+ hint: t.hint
227
+ }))
228
+ }));
229
+ template = TEMPLATES.find((t) => t.id === id) ?? template;
230
+ }
231
+ if (template.hidden) p.log.warn("Per-project Workers are not deployable yet (Phase 2) — `canopy deploy` publishes the frontend only for now.");
232
+ scaffoldTemplate(template.id, targetDir, {
233
+ name: project.name,
234
+ slug
235
+ });
236
+ const steps = [
237
+ ...rel === "." ? [] : [`cd ${rel}`],
238
+ ...template.needsInstall ? ["npm install"] : [],
239
+ "canopy dev (or deploy right away: canopy deploy)"
240
+ ];
241
+ p.note(steps.join("\n"), `Scaffolded ${rel}/ (${template.id}) for ${slug} — next`);
242
+ p.outro(`Live URL (after first deploy): ${projectUrl(slug)}`);
243
+ return;
244
+ }
245
+ mkdirSync(targetDir, { recursive: true });
246
+ const existing = tryReadCanopyJson(targetDir);
247
+ if (existing?.slug && existing.slug !== slug) throw new Error(`${rel}/canopy.json already links to "${existing.slug}" — remove it to relink to "${slug}".`);
248
+ if (existing?.slug === slug) {
249
+ p.outro(`${rel} is already linked to ${slug}.`);
250
+ return;
251
+ }
252
+ writeFileSync(join(targetDir, "canopy.json"), `${JSON.stringify({ slug }, null, 2)}\n`);
253
+ p.note("Add \"dist\"/\"build\" to canopy.json if this app has a build step, then:\ncanopy deploy", `Linked ${rel} to ${slug}`);
254
+ p.outro(`Live URL (after first deploy): ${projectUrl(slug)}`);
255
+ })
256
+ });
159
257
  const projectsCommand = defineCommand({
160
258
  meta: {
161
259
  name: "projects",
@@ -225,4 +323,4 @@ const deleteCommand = defineCommand({
225
323
  })
226
324
  });
227
325
  //#endregion
228
- export { createCommand, deleteCommand, projectsCommand };
326
+ export { createCommand, deleteCommand, initCommand, projectsCommand };
@@ -62,7 +62,11 @@ truth and never go stale.
62
62
 
63
63
  ## Command map (quick orientation — see `canopy docs cli` for detail)
64
64
 
65
- - Create a project: `canopy create`
65
+ - Create a **new** project: `canopy create`
66
+ - Set up a folder for an **existing** project (e.g. made in the dashboard):
67
+ `canopy init <slug>` — never use `canopy create` for a project that already
68
+ exists; that would register a duplicate. `canopy init <slug> .` targets the
69
+ current directory; add `--no-scaffold` to link an app you already have.
66
70
  - Run locally: `canopy dev` (live data) or `canopy dev --local` (offline sandbox)
67
71
  - Deploy: `canopy deploy` (add `--build` if the project has a build step)
68
72
  - Roll back: `canopy rollback`
@@ -70,6 +74,30 @@ truth and never go stale.
70
74
  - Manage data: `canopy db pull | snapshot | snapshots | restore`
71
75
  - CI credentials: `canopy token create | list | revoke`
72
76
 
77
+ ## Choosing a starter template (`canopy create` / `canopy init`)
78
+
79
+ Both `canopy create` and `canopy init` scaffold from the same three templates.
80
+ **`static` is the default, but reach
81
+ for it only when the app is genuinely trivial** — a page or two with light SDK
82
+ use. Its SDK is served untyped from the platform (`/_api/v1/sdk.js`): no
83
+ autocomplete, no type-checking, mistakes only show up in the browser.
84
+
85
+ Prefer **`spa`** the moment either is true — which covers most real projects:
86
+
87
+ - **You lean on the SDK** (`db` queries, `blob`, `ai` — anything past a stray
88
+ `auth.user()`). `spa` installs the SDK from npm (`@mediamonks/canopy`), fully
89
+ typed, so the compiler catches mistakes as you write them.
90
+ - **The app will grow.** `spa` is Vite + React + TypeScript, giving components,
91
+ routing, and state a real home; `static` has no build step to organise into.
92
+
93
+ Use **`worker`** (`--template worker`; hidden from the picker) only when you
94
+ need server-side backend code — e.g. calling an external API with a secret the
95
+ browser can't hold. Per-project Workers are a Phase 2 preview (frontend deploys
96
+ now; `worker/` is picked up once the platform supports it).
97
+
98
+ Non-interactive: `canopy create "My App" --template spa`. **When unsure between
99
+ `static` and `spa`, pick `spa`.**
100
+
73
101
  ## Safety rules
74
102
 
75
103
  - **Destructive commands** — `canopy db restore` and `canopy delete` wipe data.
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-DyEUYkEq.mjs";
1
+ import { t as version } from "./package-BNWFYk8y.mjs";
2
2
  import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -115,7 +115,7 @@ const upgradeCommand = defineCommand({
115
115
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
116
116
  await run(command, cmd);
117
117
  console.log(`\nDone. Run \`canopy --version\` to confirm.`);
118
- const { skillInstalled } = await import("./skill-nN0e59rO.mjs");
118
+ const { skillInstalled } = await import("./skill-esz0u6PO.mjs");
119
119
  if (skillInstalled(process.cwd())) {
120
120
  console.log("Refreshing the Canopy agent skill in this repo…");
121
121
  await run("canopy", ["skill", "install"]).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",