@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.
- package/dist/docs/ci.md +48 -0
- package/dist/docs/cli.md +67 -0
- package/dist/docs/data.md +49 -0
- package/dist/docs/deploy.md +50 -0
- package/dist/docs/dev.md +44 -0
- package/dist/docs/getting-started.md +100 -0
- package/dist/docs/troubleshooting.md +61 -0
- package/dist/docs-C3aOAx_A.mjs +179 -0
- package/dist/index.mjs +7 -7
- package/dist/{login-DsWvYc8C.mjs → login-Dif9Y32q.mjs} +36 -9
- package/dist/package-OcWG7JAW.mjs +4 -0
- package/dist/skill/canopy/SKILL.md +80 -0
- package/dist/skill-jbmERP1X.mjs +84 -0
- package/dist/{upgrade-TbMOjkLY.mjs → upgrade-Y8ejvoeg.mjs} +11 -4
- package/package.json +3 -3
package/dist/docs/ci.md
ADDED
|
@@ -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.
|
package/dist/docs/cli.md
ADDED
|
@@ -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`.
|
package/dist/docs/dev.md
ADDED
|
@@ -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-
|
|
47
|
-
whoami: () => import("./login-
|
|
48
|
-
logout: () => import("./login-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
135
|
+
spin?.error("Sign-in failed.");
|
|
114
136
|
throw err;
|
|
115
137
|
}
|
|
116
|
-
spin
|
|
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
|
-
|
|
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,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(
|
|
51
|
+
function run(command, args) {
|
|
52
52
|
return new Promise((resolve, reject) => {
|
|
53
|
-
const child = spawn(
|
|
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(`\`${
|
|
58
|
-
child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`${
|
|
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.
|
|
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
|
|
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",
|