@mediamonks/canopy-cli 0.5.1 → 0.7.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.
Files changed (43) hide show
  1. package/dist/{db-BW1ZolWn.mjs → db-BkUmbGgx.mjs} +71 -15
  2. package/dist/{deploy-alC2--pZ.mjs → deploy-BcczXlSQ.mjs} +106 -8
  3. package/dist/dev-CvtoGFW0.mjs +2 -0
  4. package/dist/{dev-CQQhMOoD.mjs → dev-DvyKHe2d.mjs} +44 -14
  5. package/dist/docs/ci.md +1 -3
  6. package/dist/docs/cli.md +3 -4
  7. package/dist/docs/data.md +5 -4
  8. package/dist/docs/deploy.md +16 -4
  9. package/dist/docs/dev.md +13 -11
  10. package/dist/docs/getting-started.md +1 -3
  11. package/dist/docs/infrastructure.md +27 -15
  12. package/dist/docs/migrations.md +183 -0
  13. package/dist/docs/porting-nextjs.md +232 -0
  14. package/dist/docs/sdk-ai.md +103 -0
  15. package/dist/docs/sdk-auth.md +57 -0
  16. package/dist/docs/sdk-database.md +126 -0
  17. package/dist/docs/sdk-files.md +82 -0
  18. package/dist/docs/sdk-services.md +58 -0
  19. package/dist/docs/sdk.md +113 -0
  20. package/dist/docs/staging.md +98 -0
  21. package/dist/docs/static-serving.md +4 -1
  22. package/dist/docs/troubleshooting.md +1 -3
  23. package/dist/{docs-DvSPpSIR.mjs → docs-2cUEug69.mjs} +23 -10
  24. package/dist/index.mjs +22 -20
  25. package/dist/{local-C5-GWJcx.mjs → local-C47tmhl7.mjs} +365 -41
  26. package/dist/{login-BO6abf40.mjs → login-ByD0hcxu.mjs} +5 -3
  27. package/dist/migrations-I1adj0z2.mjs +50 -0
  28. package/dist/{open-DnlYDqrU.mjs → open-DD9oa-Ho.mjs} +11 -4
  29. package/dist/{package-CHoZKzaw.mjs → package-CDB-uxy3.mjs} +1 -1
  30. package/dist/{projects-ijfe7Ffx.mjs → projects-CSH7KsPi.mjs} +17 -9
  31. package/dist/{site-BJjoePQS.mjs → site-BUlQR117.mjs} +14 -1
  32. package/dist/skill/canopy/SKILL.md +24 -1
  33. package/dist/{skill-CW0QSbAJ.mjs → skill-DrWzmUMv.mjs} +2 -2
  34. package/dist/staging-CfF2-pHs.mjs +113 -0
  35. package/dist/templates/spa/AGENTS.md +4 -0
  36. package/dist/templates/static/AGENTS.md +4 -0
  37. package/dist/templates/worker/AGENTS.md +7 -0
  38. package/dist/{token-BeF9zczg.mjs → token-nieJV1w7.mjs} +2 -2
  39. package/dist/{ui-DThlyHgo.mjs → ui-D8HxX0Ng.mjs} +3 -2
  40. package/dist/{upgrade-CGz1yzsW.mjs → upgrade-CH_jHkc7.mjs} +2 -2
  41. package/dist/{usage-DQpH7A3X.mjs → usage-qqPtkDtT.mjs} +2 -2
  42. package/package.json +2 -2
  43. package/dist/dev-BJ5xEwqW.mjs +0 -2
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: Database
3
+ description: Document-style collections on your project's private database, plus a raw SQL escape hatch.
4
+ ---
5
+
6
+ Every project has its own private database. The paved road is `canopy.db` —
7
+ document-style collections with no tables to create and no migrations to run.
8
+ Insert a JSON document, get it back with an id and timestamps. For the rare
9
+ case where documents aren't enough, `db.sql()` runs raw SQL against the same
10
+ database.
11
+
12
+ ## Collections
13
+
14
+ ```ts
15
+ const todos = canopy.db.collection<Todo>("todos");
16
+ ```
17
+
18
+ `db.collection<T>(name)` returns a typed `Collection<T>`. The name must be
19
+ lowercase: start with a letter, then letters, digits, and underscores only
20
+ (`^[a-z][a-z0-9_]*$`). Use `snake_case` for multi-word names — `blog_posts`,
21
+ not `blogPosts`. An invalid name throws immediately.
22
+
23
+ Pass an interface as `T` to type every call end to end:
24
+
25
+ ```ts
26
+ interface Todo extends Record<string, unknown> {
27
+ title: string;
28
+ done: boolean;
29
+ }
30
+
31
+ const todos = canopy.db.collection<Todo>("todos");
32
+ ```
33
+
34
+ Every document you read back is a `Doc<T>` — your own fields plus three
35
+ platform-managed ones:
36
+
37
+ | Field | Type | Meaning |
38
+ | ------------ | -------- | ------------------------------- |
39
+ | `_id` | `string` | Stable document id |
40
+ | `_createdAt` | `string` | ISO timestamp, set on insert |
41
+ | `_updatedAt` | `string` | ISO timestamp, bumped on update |
42
+
43
+ ## Writing documents
44
+
45
+ ```ts
46
+ // insert(data: T) → Promise<Doc<T>>
47
+ const created = await todos.insert({ title: "try canopy", done: false });
48
+ created._id; // "…" — use this for get/update/delete
49
+
50
+ // update(id, patch: Partial<T>) → Promise<Doc<T>>
51
+ await todos.update(created._id, { done: true });
52
+
53
+ // delete(id) → Promise<void>
54
+ await todos.delete(created._id);
55
+ ```
56
+
57
+ `update` is a merge-patch (RFC 7386): top-level and nested objects merge into
58
+ the existing document, and setting a key to `null` removes it. Fields you
59
+ leave out of the patch are untouched.
60
+
61
+ ## Reading documents
62
+
63
+ ```ts
64
+ // get(id) → Promise<Doc<T> | null> — null when the id doesn't exist
65
+ const one = await todos.get(created._id);
66
+
67
+ // find(options?) → Promise<Doc<T>[]>
68
+ const open = await todos.find({
69
+ where: { done: false },
70
+ orderBy: "_createdAt",
71
+ order: "desc",
72
+ limit: 5,
73
+ offset: 0,
74
+ });
75
+ ```
76
+
77
+ `find` takes a `FindOptions` object; every field is optional (call
78
+ `find()` with no arguments to list everything):
79
+
80
+ | Option | Type | Meaning |
81
+ | --------- | ------------------------- | ---------------------------------------------------- |
82
+ | `where` | `Record<string, unknown>` | Filter on document fields, e.g. `{ status: "open" }` |
83
+ | `orderBy` | `string` | Field to sort on (your fields or `_createdAt` etc.) |
84
+ | `order` | `"asc" \| "desc"` | Sort direction |
85
+ | `limit` | `number` | Maximum documents to return |
86
+ | `offset` | `number` | Documents to skip (pagination) |
87
+
88
+ ## Searching text
89
+
90
+ ```ts
91
+ // search(text, options?) → Promise<Doc<T>[]>
92
+ const hits = await todos.search("canopy", { limit: 10 });
93
+ ```
94
+
95
+ `search` matches `text` against the content of every document in the
96
+ collection. On SQLite sites it is real full-text search; on Postgres sites it
97
+ falls back to substring matching — same call, same results shape, slightly
98
+ different ranking. `options` accepts everything `FindOptions` does except
99
+ `where` (`orderBy`, `order`, `limit`, `offset`).
100
+
101
+ ## Raw SQL
102
+
103
+ ```ts
104
+ // dialect() → Promise<"sqlite" | "postgres">
105
+ const dialect = await canopy.db.dialect();
106
+
107
+ // sql<Row>(query, params?) → Promise<Row[]>
108
+ const rows =
109
+ dialect === "postgres"
110
+ ? await canopy.db.sql("SELECT * FROM my_table WHERE id = $1", [42])
111
+ : await canopy.db.sql("SELECT * FROM my_table WHERE id = ?", [42]);
112
+ ```
113
+
114
+ `db.sql()` runs against the same project database, but it is **not
115
+ portable**: the query passes through untranslated in the deployment's own
116
+ dialect. That means SQLite syntax on some sites and Postgres syntax on
117
+ others — including placeholders, `?` on SQLite versus `$1` on Postgres.
118
+ `db.dialect()` tells you which one you're talking to; the result is cached
119
+ per SDK instance, so calling it often is free.
120
+
121
+ > **Note:** Reach for collections first — they work identically everywhere your project can deploy. Use
122
+ > `db.sql()` only when you genuinely need SQL (aggregates, joins, bulk updates), and branch on
123
+ > `db.dialect()` whenever the syntax differs.
124
+
125
+ Where the data lives, snapshots, and how to inspect it from the dashboard are
126
+ covered in [Project data](https://docs.canopy.website/platform/project-data).
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Files
3
+ description: Upload, download, and list files on your project's private blob storage.
4
+ ---
5
+
6
+ `canopy.blob` stores files under your project's own isolated prefix. Nothing
7
+ is publicly accessible — every download goes through the platform with the
8
+ visitor's session, or through a short-lived signed URL you request explicitly.
9
+
10
+ ## Uploading
11
+
12
+ ```ts
13
+ // put(key, data, options?) → Promise<BlobObject>
14
+ await canopy.blob.put("notes.txt", "hello canopy", { contentType: "text/plain" });
15
+ await canopy.blob.put("avatars/arjan.png", file); // a File/Blob carries its own type
16
+ ```
17
+
18
+ `data` can be a `Blob` (including a `File`), an `ArrayBuffer`, or a `string`.
19
+ The content type is resolved in order: the explicit `options.contentType`, then
20
+ the `Blob`'s own `type`, then `application/octet-stream`. Keys are plain
21
+ strings — use `/`-separated segments (`avatars/arjan.png`) so related files
22
+ share a prefix you can `list()` later. Putting to an existing key overwrites it.
23
+
24
+ Every upload (and every `list` entry) is described by a `BlobObject`:
25
+
26
+ | Field | Type | Meaning |
27
+ | ------------- | -------- | ---------------------------- |
28
+ | `key` | `string` | The key you stored it under |
29
+ | `size` | `number` | Size in bytes |
30
+ | `contentType` | `string` | MIME type it was stored with |
31
+ | `uploadedAt` | `string` | ISO timestamp of the upload |
32
+
33
+ ## Downloading
34
+
35
+ ```ts
36
+ // get(key) → Promise<Blob | null> — null when the key doesn't exist
37
+ const blob = await canopy.blob.get("notes.txt");
38
+ const text = await blob?.text();
39
+
40
+ // getUrl(key) → Promise<{ url: string; expiresAt: string }>
41
+ const { url, expiresAt } = await canopy.blob.getUrl("avatars/arjan.png");
42
+ ```
43
+
44
+ `get` returns the content as a standard `Blob`, typed with the MIME type it
45
+ was uploaded with — use `.text()` for text, `.arrayBuffer()` or `.stream()`
46
+ for processing, or `URL.createObjectURL(blob)` for an `<img src>`.
47
+
48
+ `getUrl` returns a short-lived download URL served through the platform (the
49
+ signature alone grants access, so no cookies are needed). Prefer it when the
50
+ browser should fetch the bytes itself — image `src` attributes, download
51
+ links, media elements — and prefer `get` when your code needs the content.
52
+ Request a fresh URL when you render rather than storing one: it expires at
53
+ `expiresAt`.
54
+
55
+ ## Listing and deleting
56
+
57
+ ```ts
58
+ // list(prefix?) → Promise<BlobObject[]>
59
+ const everything = await canopy.blob.list();
60
+ const avatars = await canopy.blob.list("avatars/");
61
+
62
+ // delete(key) → Promise<void>
63
+ await canopy.blob.delete("notes.txt");
64
+ ```
65
+
66
+ ## Example: upload from a file input
67
+
68
+ ```tsx
69
+ async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {
70
+ const file = event.target.files?.[0];
71
+ if (!file) return;
72
+
73
+ const stored = await canopy.blob.put(`uploads/${file.name}`, file);
74
+ const { url } = await canopy.blob.getUrl(stored.key);
75
+ setImageUrl(url); // <img src={imageUrl} />
76
+ }
77
+
78
+ <input type="file" accept="image/*" onChange={onFileChange} />;
79
+ ```
80
+
81
+ Storage location and limits are covered in
82
+ [Project data](https://docs.canopy.website/platform/project-data).
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Services
3
+ description: Curated internal data services, invoked through the platform's connector — auth handled for you.
4
+ ---
5
+
6
+ `canopy.services` is the catalog of curated internal data services enabled
7
+ for your project. Calls go through the platform's connector, which handles
8
+ authentication — a service token or on-behalf-of-user credentials, depending
9
+ on the service — so there is nothing to configure in your code.
10
+
11
+ > **Warning:** Services aren't live yet: the SDK surface below is stable, but the platform currently answers
12
+ > every service call with "not implemented" and no services can be enabled. This page describes what
13
+ > to build against once the first services land.
14
+
15
+ ## services.list()
16
+
17
+ The services enabled for this project:
18
+
19
+ ```ts
20
+ const available = await canopy.services.list();
21
+ // [{ name: string, description: string }]
22
+ ```
23
+
24
+ The catalog is curated per project — an empty array means no services are
25
+ enabled yet.
26
+
27
+ ## services.get(name)
28
+
29
+ Returns a `ServiceRef` for a named service. This is a local handle — no
30
+ network call happens until you invoke it:
31
+
32
+ ```ts
33
+ const service = canopy.services.get("example");
34
+ ```
35
+
36
+ ## ref.fetch(path, init?)
37
+
38
+ Invoke a path on the service and get its JSON response back, typed with the
39
+ generic you provide:
40
+
41
+ ```ts
42
+ const pong = await canopy.services.get("example").fetch("/ping");
43
+
44
+ const report = await canopy.services.get("example").fetch<Report>("/reports/weekly", {
45
+ method: "POST",
46
+ body: { week: 33 },
47
+ });
48
+ ```
49
+
50
+ - `path` — the path to invoke on the service, e.g. `"/ping"`.
51
+ - `init.method` — optional HTTP method, defaults to `"GET"`.
52
+ - `init.body` — optional request body, sent as JSON.
53
+ - Returns `Promise<T>` (defaults to `unknown`) — the service's JSON response.
54
+
55
+ Failed invocations throw `CanopyError` with the HTTP `status`.
56
+
57
+ > **Note:** Services are for reading and invoking internal Monks data sources the platform has connected — not
58
+ > arbitrary outbound HTTP. For external APIs, use `fetch` directly.
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: Using the SDK
3
+ description: One typed client for auth, data, files, AI, and services — no keys, no connection strings, the platform authenticates for you.
4
+ ---
5
+
6
+ `@mediamonks/canopy` is a thin, typed client over the platform's project API.
7
+ One `createCanopy()` call gives you five modules — `auth`, `db`, `blob`, `ai`,
8
+ and `services` — and that is the entire surface. There are no API keys,
9
+ connection strings, or environment variables to manage: every call rides the
10
+ session the platform already established before your code ran.
11
+
12
+ The same code works everywhere your project runs — in the browser during
13
+ `canopy dev`, in the deployed site, and in a project's backend worker.
14
+
15
+ ## Getting the SDK
16
+
17
+ You usually don't install anything. The `spa` and `worker` templates ship with
18
+ `@mediamonks/canopy` in `package.json`; the `static` template imports the same
19
+ bundle straight from the platform, untyped but always version-matched to the
20
+ API:
21
+
22
+ ```ts
23
+ // spa / worker templates (typed, from npm)
24
+ import { createCanopy } from "@mediamonks/canopy";
25
+
26
+ // static template (untyped, served by the platform)
27
+ import { createCanopy } from "/_api/data/v1/sdk.js";
28
+ ```
29
+
30
+ ## createCanopy(config)
31
+
32
+ `createCanopy` takes an optional config object and returns the client:
33
+
34
+ ```ts
35
+ import { createCanopy } from "@mediamonks/canopy";
36
+
37
+ const canopy = createCanopy();
38
+ // canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.services
39
+ ```
40
+
41
+ | Option | Type | Default | What it does |
42
+ | --------- | -------------- | -------------- | ---------------------------------------------------------------------------------------------------- |
43
+ | `baseUrl` | `string` | `"/_api"` | Base URL of the platform data API. The default is same-origin, so it works untouched in the browser. |
44
+ | `fetch` | `typeof fetch` | global `fetch` | Override how requests are sent — a backend worker passes its platform service binding here. |
45
+
46
+ In the browser no config is needed. In a backend worker (the `worker`
47
+ template), the platform is reachable through the `CANOPY` service binding, so
48
+ create the client per request with that binding's fetch:
49
+
50
+ ```ts
51
+ app.get("/api/hello", async (c) => {
52
+ const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
53
+ const user = await canopy.auth.user();
54
+ return c.json({ message: `Hello from the backend, ${user.name}!` });
55
+ });
56
+ ```
57
+
58
+ ## One instance, shared
59
+
60
+ In the browser, create the client once and import it everywhere. The scaffolds
61
+ do this in `src/canopy.ts`:
62
+
63
+ ```ts
64
+ // src/canopy.ts
65
+ import { createCanopy } from "@mediamonks/canopy";
66
+
67
+ export const canopy = createCanopy();
68
+ ```
69
+
70
+ ## Error handling
71
+
72
+ Any call that fails throws a `CanopyError` (exported from the package). It
73
+ extends `Error` with the HTTP `status` of the failed request; `message` carries
74
+ the response body when the platform sent one:
75
+
76
+ ```ts
77
+ import { CanopyError } from "@mediamonks/canopy";
78
+
79
+ try {
80
+ await canopy.db.collection("todos").insert({ title: "hi", done: false });
81
+ } catch (err) {
82
+ if (err instanceof CanopyError && err.status === 403) {
83
+ // e.g. a viewer hitting a write — show a friendly message
84
+ }
85
+ throw err;
86
+ }
87
+ ```
88
+
89
+ Two read calls are deliberately soft instead: `collection.get(id)` and
90
+ `blob.get(key)` resolve to `null` for a missing document or object rather than
91
+ throwing.
92
+
93
+ ## Where it runs
94
+
95
+ The client is plain `fetch` underneath, so behavior is identical across
96
+ environments:
97
+
98
+ - **Browser, deployed** — calls go to same-origin `/_api`; the platform
99
+ authenticated the visitor before your page loaded, and the session cookie
100
+ rides along automatically.
101
+ - **Browser, `canopy dev`** — the dev server proxies `/_api` to the platform
102
+ with your CLI login, so local code talks to your project's real data.
103
+ - **Backend worker** — pass the `CANOPY` service binding's fetch (shown
104
+ above); the incoming request's identity carries through, so `auth.user()`
105
+ and per-project budgets behave exactly as in the browser.
106
+
107
+ ## Modules
108
+
109
+ - [Auth](https://docs.canopy.website/sdk/auth) — who is signed in, and ending sessions.
110
+ - [Database](https://docs.canopy.website/sdk/database) — document collections, plus a raw SQL escape hatch.
111
+ - [Files](https://docs.canopy.website/sdk/files) — private blob storage under the project's prefix.
112
+ - [AI](https://docs.canopy.website/sdk/ai) — metered, budget-capped LLM access.
113
+ - [Services](https://docs.canopy.website/sdk/services) — curated internal data services.
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: Staging & promoting
3
+ description: Deploy to a safe copy of your project first — own database cloned from live — then promote the exact same version.
4
+ ---
5
+
6
+ Every project can have a **staging channel**: a second, private copy of the
7
+ app with its own database (cloned from live), its own files, and its own
8
+ backend, served at
9
+
10
+ ```
11
+ https://staging-dot-<slug>.canopy.website
12
+ ```
13
+
14
+ behind the same sign-in as the live site. Deploy there first, click around
15
+ against real-shaped data, then **promote** — the exact version you tested
16
+ becomes live, nothing is rebuilt.
17
+
18
+ You don't set anything up. The first staged deploy creates the channel and
19
+ copies the live database into it.
20
+
21
+ ## The workflow
22
+
23
+ ```bash
24
+ canopy deploy --staging # build + deploy to staging (created on first use)
25
+ canopy open --staging # check it in the browser
26
+ canopy promote # happy? make that version live
27
+ ```
28
+
29
+ `canopy deploy --staging` is the normal deploy in every way — same build, same
30
+ versioning — except the result serves on the staging URL and the live site is
31
+ untouched. Versions share one sequence: if live is at v7 and you stage v8,
32
+ promoting makes that same v8 live.
33
+
34
+ `canopy promote` is not a redeploy: the platform re-uses the exact files and
35
+ backend bundle you staged. If migrations were rehearsed on staging, they replay
36
+ against the live database first (with an automatic snapshot), then the pointer
37
+ flips.
38
+
39
+ ## Testing schema migrations
40
+
41
+ This is the staging channel's superpower. Because the staging database starts
42
+ as a copy of live, a migration that works there will behave the same way on
43
+ the real thing:
44
+
45
+ ```bash
46
+ canopy deploy --staging # pending migrations apply to the STAGING database
47
+ # … verify the app against the migrated copy …
48
+ canopy promote # the same migrations replay against live, then v goes live
49
+ ```
50
+
51
+ A migration that fails on staging costs you nothing — live never saw it. See
52
+ [Schema migrations](https://docs.canopy.website/guides/migrations) for the migration system itself.
53
+
54
+ ## Keeping staging fresh
55
+
56
+ Staging data drifts as you test. Re-clone it from live anytime:
57
+
58
+ ```bash
59
+ canopy staging sync # replace staging data with a fresh copy of live
60
+ canopy staging status # what's staged vs live
61
+ ```
62
+
63
+ Writes on staging never flow back to live — promote moves code and schema, not
64
+ data.
65
+
66
+ ## What's shared, what's separate
67
+
68
+ Separate per channel: the database, uploaded files, and the backend worker.
69
+ Shared with live: members, secrets, the client password, schedules (they run
70
+ against live only), and the AI budget — staging traffic counts against the
71
+ same cap.
72
+
73
+ ## Developing against staging data
74
+
75
+ Remote dev can point at the staging database instead of live — useful while a
76
+ schema is in flux:
77
+
78
+ ```bash
79
+ canopy dev --staging
80
+ ```
81
+
82
+ ## Cleaning up
83
+
84
+ ```bash
85
+ canopy staging destroy # removes the staging database, files and backend
86
+ ```
87
+
88
+ Live is untouched, and the next `canopy deploy --staging` recreates the channel
89
+ from a fresh copy. The dashboard's Settings tab has the same controls, the
90
+ Overview tab shows what's staged with a Promote button, and a Live/Staging
91
+ toggle above the Data and Files tabs browses either channel's data.
92
+
93
+ ## Staging vs version previews
94
+
95
+ Version preview URLs (`v12-dot-<slug>`) show an old **frontend** against the
96
+ **live** data — good for comparing builds. The staging channel is the full
97
+ package: candidate frontend, candidate backend, and its own database. Use
98
+ previews to look backwards, staging to look forwards.
@@ -1,4 +1,7 @@
1
- # How static files are served
1
+ ---
2
+ title: How static files are served
3
+ description: The full path a browser request takes to a deployed file.
4
+ ---
2
5
 
3
6
  This explains the full path a browser request takes to receive a deployed
4
7
  project's `index.html` and its assets (JS/CSS/images/fonts), and reviews that
@@ -1,10 +1,8 @@
1
1
  ---
2
2
  title: Troubleshooting
3
- summary: Common errors and how to resolve them.
3
+ description: Common errors and how to resolve them.
4
4
  ---
5
5
 
6
- # Troubleshooting
7
-
8
6
  ## `command not found: canopy`
9
7
 
10
8
  The CLI isn't installed or isn't on your PATH. Install it:
@@ -1,5 +1,5 @@
1
- import { n as siteConfig } from "./site-BJjoePQS.mjs";
2
- import { n as reportErrors } from "./ui-DThlyHgo.mjs";
1
+ import { n as siteConfig } from "./site-BUlQR117.mjs";
2
+ import { n as reportErrors } from "./ui-D8HxX0Ng.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
@@ -24,12 +24,20 @@ const cacheDir = join(homedir(), ".canopy", "docs-cache");
24
24
  const indexPath = join(cacheDir, "index.json");
25
25
  const CACHE_TTL_MS = 3600 * 1e3;
26
26
  const FETCH_TIMEOUT_MS = 4e3;
27
- /** The bundled snapshot dir: dist/docs when published, repo docs/guides in dev. */
27
+ /**
28
+ * The bundled snapshot dir: dist/docs when published (generated by
29
+ * sync-templates from the terminal-docs corpus), with raw guide sources as a
30
+ * dev last resort — those are MDX, which agents read fine untransformed.
31
+ */
28
32
  function bundledDocsDir() {
29
33
  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;
34
+ for (const candidate of [
35
+ join(base, "docs"),
36
+ join(base, "..", "dist", "docs"),
37
+ join(base, "..", "..", "..", "apps", "docs", "content", "docs", "guides")
38
+ ]) if (existsSync(candidate)) return candidate;
31
39
  }
32
- /** Parse `---\ntitle: …\nsummary: …\n---\n<body>` frontmatter. */
40
+ /** Parse `---\ntitle: …\ndescription: …\n---\n<body>` frontmatter. */
33
41
  function parseFrontmatter(raw) {
34
42
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
35
43
  if (!match?.[1] || match[2] === void 0) return { body: raw.trim() };
@@ -40,7 +48,7 @@ function parseFrontmatter(raw) {
40
48
  }
41
49
  return {
42
50
  title: meta["title"],
43
- summary: meta["summary"],
51
+ summary: meta["description"] ?? meta["summary"],
44
52
  body: match[2].trim()
45
53
  };
46
54
  }
@@ -96,8 +104,8 @@ function topicsFromFallback() {
96
104
  } catch {}
97
105
  const dir = bundledDocsDir();
98
106
  if (!dir) return [];
99
- return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => {
100
- const slug = f.replace(/\.md$/, "");
107
+ return readdirSync(dir).filter((f) => /\.mdx?$/.test(f)).map((f) => {
108
+ const slug = f.replace(/\.mdx?$/, "");
101
109
  const { title, summary } = parseFrontmatter(readFileSync(join(dir, f), "utf8"));
102
110
  return {
103
111
  slug,
@@ -111,8 +119,13 @@ function bodyFromFallback(slug) {
111
119
  const cached = join(cacheDir, `${slug}.md`);
112
120
  if (existsSync(cached)) return readFileSync(cached, "utf8");
113
121
  const dir = bundledDocsDir();
114
- const file = dir && join(dir, `${slug}.md`);
115
- if (file && existsSync(file)) return parseFrontmatter(readFileSync(file, "utf8")).body;
122
+ for (const ext of [".md", ".mdx"]) {
123
+ const file = dir && join(dir, `${slug}${ext}`);
124
+ if (file && existsSync(file)) {
125
+ const { title, body } = parseFrontmatter(readFileSync(file, "utf8"));
126
+ return title ? `# ${title}\n\n${body}` : body;
127
+ }
128
+ }
116
129
  }
117
130
  async function listTopics(refresh) {
118
131
  const { apiUrl } = siteConfig();
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CHoZKzaw.mjs";
3
- import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BJjoePQS.mjs";
2
+ import { t as version } from "./package-CDB-uxy3.mjs";
3
+ import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
4
4
  import { i as runRefresh, r as notifyOfUpdate } from "./version-check-Cuh6HaLX.mjs";
5
5
  import { defineCommand, runMain } from "citty";
6
6
  //#region src/index.ts
@@ -73,24 +73,26 @@ const main = defineCommand({
73
73
  description: "canopy — create and deploy vibecoded projects"
74
74
  },
75
75
  subCommands: {
76
- login: () => import("./login-BO6abf40.mjs").then((m) => m.loginCommand),
77
- whoami: () => import("./login-BO6abf40.mjs").then((m) => m.whoamiCommand),
78
- logout: () => import("./login-BO6abf40.mjs").then((m) => m.logoutCommand),
79
- create: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.createCommand),
80
- init: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.initCommand),
81
- projects: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.projectsCommand),
82
- delete: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.deleteCommand),
83
- dev: () => import("./dev-BJ5xEwqW.mjs").then((m) => m.devCommand),
84
- deploy: () => import("./deploy-alC2--pZ.mjs").then((m) => m.deployCommand),
85
- rollback: () => import("./deploy-alC2--pZ.mjs").then((m) => m.rollbackCommand),
86
- download: () => import("./deploy-alC2--pZ.mjs").then((m) => m.downloadCommand),
87
- token: () => import("./token-BeF9zczg.mjs").then((m) => m.tokenCommand),
88
- db: () => import("./db-BW1ZolWn.mjs").then((m) => m.dbCommand),
89
- usage: () => import("./usage-DQpH7A3X.mjs").then((m) => m.usageCommand),
90
- open: () => import("./open-DnlYDqrU.mjs").then((m) => m.openCommand),
91
- docs: () => import("./docs-DvSPpSIR.mjs").then((m) => m.docsCommand),
92
- skill: () => import("./skill-CW0QSbAJ.mjs").then((m) => m.skillCommand),
93
- upgrade: () => import("./upgrade-CGz1yzsW.mjs").then((m) => m.upgradeCommand),
76
+ login: () => import("./login-ByD0hcxu.mjs").then((m) => m.loginCommand),
77
+ whoami: () => import("./login-ByD0hcxu.mjs").then((m) => m.whoamiCommand),
78
+ logout: () => import("./login-ByD0hcxu.mjs").then((m) => m.logoutCommand),
79
+ create: () => import("./projects-CSH7KsPi.mjs").then((m) => m.createCommand),
80
+ init: () => import("./projects-CSH7KsPi.mjs").then((m) => m.initCommand),
81
+ projects: () => import("./projects-CSH7KsPi.mjs").then((m) => m.projectsCommand),
82
+ delete: () => import("./projects-CSH7KsPi.mjs").then((m) => m.deleteCommand),
83
+ dev: () => import("./dev-CvtoGFW0.mjs").then((m) => m.devCommand),
84
+ deploy: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.deployCommand),
85
+ promote: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.promoteCommand),
86
+ staging: () => import("./staging-CfF2-pHs.mjs").then((m) => m.stagingCommand),
87
+ rollback: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.rollbackCommand),
88
+ download: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.downloadCommand),
89
+ token: () => import("./token-nieJV1w7.mjs").then((m) => m.tokenCommand),
90
+ db: () => import("./db-BkUmbGgx.mjs").then((m) => m.dbCommand),
91
+ usage: () => import("./usage-qqPtkDtT.mjs").then((m) => m.usageCommand),
92
+ open: () => import("./open-DD9oa-Ho.mjs").then((m) => m.openCommand),
93
+ docs: () => import("./docs-2cUEug69.mjs").then((m) => m.docsCommand),
94
+ skill: () => import("./skill-DrWzmUMv.mjs").then((m) => m.skillCommand),
95
+ upgrade: () => import("./upgrade-CH_jHkc7.mjs").then((m) => m.upgradeCommand),
94
96
  logs: planned("logs", "Tail worker logs", "2")
95
97
  }
96
98
  });