@mediamonks/canopy-cli 0.3.1 → 0.4.1
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 +8 -2
- package/dist/docs/data.md +17 -0
- package/dist/docs/getting-started.md +6 -1
- package/dist/index.mjs +7 -6
- package/dist/{package-B34Xg7L3.mjs → package-CA5g4zPx.mjs} +1 -1
- package/dist/{projects-DOYKtMZC.mjs → projects-DbKcG5kx.mjs} +100 -2
- package/dist/skill/canopy/SKILL.md +28 -3
- package/dist/{skill-BlRch5eA.mjs → skill-DMs7GNl7.mjs} +1 -1
- package/dist/{upgrade-DblPIwpX.mjs → upgrade-BMTdpOxO.mjs} +1 -1
- package/package.json +1 -1
package/dist/docs/cli.md
CHANGED
|
@@ -17,10 +17,16 @@ is the overview.
|
|
|
17
17
|
|
|
18
18
|
## Projects
|
|
19
19
|
|
|
20
|
-
- `canopy create [name]` —
|
|
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.
|
package/dist/docs/data.md
CHANGED
|
@@ -9,6 +9,23 @@ Each Canopy project gets its own database (D1) and blob storage (R2). Your app
|
|
|
9
9
|
reads and writes them through the Canopy SDK at runtime; the CLI gives you tools
|
|
10
10
|
to manage the data around it.
|
|
11
11
|
|
|
12
|
+
## Collections in your app (SDK)
|
|
13
|
+
|
|
14
|
+
The paved road is document-style collections:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
await canopy.db.collection("blog_posts").insert({ title: "Hello" });
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Collection names must be lowercase** — start with a letter, then letters,
|
|
21
|
+
digits, and underscores only (`^[a-z][a-z0-9_]*$`). `blog_posts` is fine;
|
|
22
|
+
`blogPosts` or `BlogPosts` are **not** and throw at runtime:
|
|
23
|
+
|
|
24
|
+
> `Invalid collection name "BlogPosts" — use lowercase letters, digits and underscores.`
|
|
25
|
+
|
|
26
|
+
Use `snake_case` for multi-word names. This mirrors the collection name straight
|
|
27
|
+
into SQLite storage, so keeping it lowercase avoids case-sensitivity surprises.
|
|
28
|
+
|
|
12
29
|
## Download a dump
|
|
13
30
|
|
|
14
31
|
```bash
|
|
@@ -50,7 +50,7 @@ Check who you are at any time:
|
|
|
50
50
|
canopy whoami
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
## 3. Create
|
|
53
|
+
## 3. Create your first project
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
canopy create
|
|
@@ -63,6 +63,11 @@ directory, and registers the project on the platform. To skip the prompts:
|
|
|
63
63
|
canopy create "My App" --scaffold --template spa
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
> If you are an agent: don't stop at printing this command. Ask the person what
|
|
67
|
+
> they'd like to build and what it's about, then pick a name and template for
|
|
68
|
+
> them and run `canopy create` non-interactively — then keep going and build
|
|
69
|
+
> it. Aim for a working first project, not a list of commands.
|
|
70
|
+
|
|
66
71
|
Templates: **`static`** (no-build html/css/js; SDK served untyped from the
|
|
67
72
|
platform) is the default and fine for a trivial page or two. Reach for
|
|
68
73
|
**`spa`** (Vite + React + TypeScript; typed SDK from npm) once you lean on the
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as version } from "./package-
|
|
2
|
+
import { t as version } from "./package-CA5g4zPx.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-
|
|
48
|
-
|
|
49
|
-
|
|
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-
|
|
58
|
-
upgrade: () => import("./upgrade-
|
|
58
|
+
skill: () => import("./skill-DMs7GNl7.mjs").then((m) => m.skillCommand),
|
|
59
|
+
upgrade: () => import("./upgrade-BMTdpOxO.mjs").then((m) => m.upgradeCommand),
|
|
59
60
|
logs: planned("logs", "Tail worker logs", "2")
|
|
60
61
|
}
|
|
61
62
|
});
|
|
@@ -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";
|
|
@@ -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 };
|
|
@@ -45,6 +45,26 @@ canopy whoami # is there a valid login?
|
|
|
45
45
|
|
|
46
46
|
Skip login if `canopy whoami` already shows a valid, unexpired token.
|
|
47
47
|
|
|
48
|
+
## Then: offer to build their first project
|
|
49
|
+
|
|
50
|
+
Once the CLI is installed and signed in, **do not just print a list of
|
|
51
|
+
commands and stop.** The person following the getting-started prompt is often
|
|
52
|
+
non-technical and expects you to drive. Take the lead:
|
|
53
|
+
|
|
54
|
+
1. Ask them, in plain language, whether they'd like to create their first
|
|
55
|
+
Canopy project now — and **what it's about** (what should it do, who's it
|
|
56
|
+
for). Ask one short question at a time; wait for the answer.
|
|
57
|
+
2. From their answer, pick a sensible name and template (see the template
|
|
58
|
+
guidance below — default to `spa` for anything beyond a trivial page), then
|
|
59
|
+
scaffold it for them non-interactively, e.g.
|
|
60
|
+
`canopy create "Their App Name" --template spa`.
|
|
61
|
+
3. `cd` into the new folder, then keep going — build the thing they described,
|
|
62
|
+
running `canopy dev` to preview and `canopy docs`/`canopy <cmd> --help`
|
|
63
|
+
whenever you need platform details.
|
|
64
|
+
|
|
65
|
+
Only fall back to handing them raw commands if they say they'd rather do it
|
|
66
|
+
themselves. The goal is a working first project, not a command cheat-sheet.
|
|
67
|
+
|
|
48
68
|
## Then: read the docs for whatever you're doing
|
|
49
69
|
|
|
50
70
|
The CLI ships the full documentation and keeps it fresh from the platform. List
|
|
@@ -62,7 +82,11 @@ truth and never go stale.
|
|
|
62
82
|
|
|
63
83
|
## Command map (quick orientation — see `canopy docs cli` for detail)
|
|
64
84
|
|
|
65
|
-
- Create a project: `canopy create`
|
|
85
|
+
- Create a **new** project: `canopy create`
|
|
86
|
+
- Set up a folder for an **existing** project (e.g. made in the dashboard):
|
|
87
|
+
`canopy init <slug>` — never use `canopy create` for a project that already
|
|
88
|
+
exists; that would register a duplicate. `canopy init <slug> .` targets the
|
|
89
|
+
current directory; add `--no-scaffold` to link an app you already have.
|
|
66
90
|
- Run locally: `canopy dev` (live data) or `canopy dev --local` (offline sandbox)
|
|
67
91
|
- Deploy: `canopy deploy` (add `--build` if the project has a build step)
|
|
68
92
|
- Roll back: `canopy rollback`
|
|
@@ -70,9 +94,10 @@ truth and never go stale.
|
|
|
70
94
|
- Manage data: `canopy db pull | snapshot | snapshots | restore`
|
|
71
95
|
- CI credentials: `canopy token create | list | revoke`
|
|
72
96
|
|
|
73
|
-
## Choosing a starter template (`canopy create`)
|
|
97
|
+
## Choosing a starter template (`canopy create` / `canopy init`)
|
|
74
98
|
|
|
75
|
-
`canopy create`
|
|
99
|
+
Both `canopy create` and `canopy init` scaffold from the same three templates.
|
|
100
|
+
**`static` is the default, but reach
|
|
76
101
|
for it only when the app is genuinely trivial** — a page or two with light SDK
|
|
77
102
|
use. Its SDK is served untyped from the platform (`/_api/v1/sdk.js`): no
|
|
78
103
|
autocomplete, no type-checking, mistakes only show up in the browser.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CA5g4zPx.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-
|
|
118
|
+
const { skillInstalled } = await import("./skill-DMs7GNl7.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) => {
|