@mediamonks/canopy-cli 0.7.32 → 0.7.34
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/browse-Dycwj0V_.mjs +89 -0
- package/dist/{dev-CkhBnSlv.mjs → dev-CGSIY1-y.mjs} +4 -4
- package/dist/dev-DO6Eso4L.mjs +2 -0
- package/dist/{dev-worker-CaCsGBkn.mjs → dev-worker-JbLEXmVg.mjs} +1 -1
- package/dist/docs/agent-browsing.md +58 -0
- package/dist/docs/cli-browse.md +48 -0
- package/dist/docs/cli-create.md +1 -0
- package/dist/docs/cli-repo.md +87 -0
- package/dist/docs/getting-started.md +3 -0
- package/dist/docs/git.md +86 -0
- package/dist/docs/index.json +20 -0
- package/dist/{docs-CtNH5kbN.mjs → docs-Cdoh5sMW.mjs} +1 -1
- package/dist/{feedback-_L3vqktY.mjs → feedback-vvysW6iw.mjs} +1 -1
- package/dist/index.mjs +18 -15
- package/dist/{local-DAYCNUyz.mjs → local-qR8CBseB.mjs} +2 -2
- package/dist/{login-B-m-i8uu.mjs → login-D0J5s_Ex.mjs} +3 -1
- package/dist/{mcp-RcN17Z2v.mjs → mcp-OR_ashGC.mjs} +1 -1
- package/dist/{package-YGjjin4p.mjs → package-Doigafvd.mjs} +1 -1
- package/dist/{projects-CN2YhCZn.mjs → projects-DQAhCZm8.mjs} +13 -1
- package/dist/repo-1RQ1vBMv.mjs +2 -0
- package/dist/repo-Bb9ntNa4.mjs +423 -0
- package/dist/repo-credential-CNDRM-tG.mjs +118 -0
- package/dist/skill/canopy/SKILL.md +13 -1
- package/dist/{skill-CM7R-4Bq.mjs → skill-DfSAZ2cW.mjs} +1 -1
- package/dist/{src-BkACMkol.mjs → src-DE37PKho.mjs} +6 -2
- package/dist/templates/spa/AGENTS.md +13 -0
- package/dist/templates/spa/_gitignore +4 -0
- package/dist/templates/static/AGENTS.md +11 -0
- package/dist/templates/static/_gitignore +4 -0
- package/dist/templates/worker/AGENTS.md +13 -0
- package/dist/templates/worker/_gitignore +3 -0
- package/dist/{upgrade-ChcSZW8i.mjs → upgrade-DCQ3NzoL.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-MoHHjQh1.mjs +0 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
2
|
+
import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
//#region src/browse.ts
|
|
5
|
+
/**
|
|
6
|
+
* canopy browse — a one-time sign-in link to a project for an agent's
|
|
7
|
+
* browser (agent-browse-sessions). Opening it within a minute gives that
|
|
8
|
+
* browser an hour on this one project and channel, as the signed-in Monk;
|
|
9
|
+
* nothing else on Canopy. The link is the only thing printed: no cookie, no
|
|
10
|
+
* token, nothing written to disk, and no browser opened (that is `canopy open`).
|
|
11
|
+
*/
|
|
12
|
+
var BrowseError = class extends Error {
|
|
13
|
+
name = "BrowseError";
|
|
14
|
+
};
|
|
15
|
+
/** The control plane's refusal, in words that say what to do about it. */
|
|
16
|
+
function explainBrowseRefusal(code, slug, detail) {
|
|
17
|
+
switch (code) {
|
|
18
|
+
case "members_only": return `${slug} is limited to its collaborators — ask the owner to add you.`;
|
|
19
|
+
case "project_not_found": return `There is no project ${slug} on this site — check the slug and --profile.`;
|
|
20
|
+
case "project_paused": return `${slug} is paused.`;
|
|
21
|
+
case "project_moved": return `${slug} has moved${detail ? ` (${detail})` : ""}.`;
|
|
22
|
+
case "staging_not_enabled": return `${slug} has no staging channel yet — drop --staging, or create it with \`canopy deploy --staging\`.`;
|
|
23
|
+
case "invalid_path": return "--path must be a page on the project, starting with / (for example /settings).";
|
|
24
|
+
case "cli_session_required": return "Browse links need your own CLI sign-in, not a deploy token — run `canopy login`.";
|
|
25
|
+
case "not_configured": return "This site has no public project hostnames, so it can't hand out browse links.";
|
|
26
|
+
default: return detail ? `${code}: ${detail}` : code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const browseCommand = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "browse",
|
|
32
|
+
description: "Print a one-time sign-in link to a project for an agent's browser"
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
slug: {
|
|
36
|
+
type: "positional",
|
|
37
|
+
description: "Project slug (defaults to canopy.json in the current directory)",
|
|
38
|
+
required: false
|
|
39
|
+
},
|
|
40
|
+
staging: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
default: false,
|
|
43
|
+
description: "Link to the staging channel instead of live"
|
|
44
|
+
},
|
|
45
|
+
path: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Page to land on after signing in (default /)"
|
|
48
|
+
},
|
|
49
|
+
json: {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
default: false,
|
|
52
|
+
description: "Print only { url, expires_at } as JSON"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async run({ args }) {
|
|
56
|
+
try {
|
|
57
|
+
const slug = tryResolveProject({ project: args.slug }).slug;
|
|
58
|
+
if (!slug) throw new BrowseError("No project named — pass a slug (`canopy browse <slug>`) or run from a project directory.");
|
|
59
|
+
if (process.env["CANOPY_DEPLOY_TOKEN"]) throw new BrowseError("Browse links use your own sign-in, not a deploy token — unset CANOPY_DEPLOY_TOKEN and run `canopy login`.");
|
|
60
|
+
if (!siteConfig().token) throw new BrowseError("Not signed in to this site — run `canopy login` first.");
|
|
61
|
+
let link;
|
|
62
|
+
try {
|
|
63
|
+
link = await apiRequest("POST", `/projects/${slug}/browse-ticket`, {
|
|
64
|
+
channel: args.staging ? "staging" : "live",
|
|
65
|
+
...args.path !== void 0 && { path: args.path }
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof ApiError && err.status !== 401) throw new BrowseError(explainBrowseRefusal(err.code, slug, err.detail));
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
if (args.json) {
|
|
72
|
+
process.stdout.write(`${JSON.stringify({
|
|
73
|
+
url: link.url,
|
|
74
|
+
expires_at: link.expires_at
|
|
75
|
+
})}\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(`\n ${link.url}\n`);
|
|
79
|
+
console.log(" Open it within a minute; it works once. The browser that opens it is signed in to");
|
|
80
|
+
console.log(` ${slug}${args.staging ? " (staging)" : ""} as you for an hour, and to nothing else on Canopy.\n`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
process.stderr.write(`canopy browse: ${message}\n`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
//#endregion
|
|
89
|
+
export { browseCommand };
|
|
@@ -2,7 +2,7 @@ import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as sit
|
|
|
2
2
|
import { i as apiRequest, o as withContext, t as ApiError } from "./api-C4rMhE7i.mjs";
|
|
3
3
|
import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
|
|
4
4
|
import { r as readMigrationFiles } from "./migrations-Czlcddyz.mjs";
|
|
5
|
-
import { r as stampWorkerHeaders } from "./src-
|
|
5
|
+
import { r as stampWorkerHeaders } from "./src-DE37PKho.mjs";
|
|
6
6
|
import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { defineCommand } from "citty";
|
|
@@ -40,7 +40,7 @@ async function workerPlugin(flags, slug) {
|
|
|
40
40
|
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
-
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-
|
|
43
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-JbLEXmVg.mjs");
|
|
44
44
|
const secrets = readDevVars(flags.dir);
|
|
45
45
|
const email = siteConfig().email;
|
|
46
46
|
const plugin = workerDevPlugin({
|
|
@@ -225,7 +225,7 @@ async function devRemote(flags, slug) {
|
|
|
225
225
|
async function devLocal(flags, slug) {
|
|
226
226
|
let dump;
|
|
227
227
|
if (flags.pull) {
|
|
228
|
-
const { fetchProjectDump } = await import("./local-
|
|
228
|
+
const { fetchProjectDump } = await import("./local-qR8CBseB.mjs");
|
|
229
229
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
230
230
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
231
231
|
}
|
|
@@ -240,7 +240,7 @@ async function devLocal(flags, slug) {
|
|
|
240
240
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
241
241
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
242
242
|
}
|
|
243
|
-
const { createSandbox } = await import("./local-
|
|
243
|
+
const { createSandbox } = await import("./local-qR8CBseB.mjs");
|
|
244
244
|
const sandbox = await createSandbox({
|
|
245
245
|
slug,
|
|
246
246
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as redactCanopyCredentials, r as stampWorkerHeaders, t as CALLER_HEADER } from "./src-
|
|
1
|
+
import { n as redactCanopyCredentials, r as stampWorkerHeaders, t as CALLER_HEADER } from "./src-DE37PKho.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
//#region src/dev-worker.ts
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Let your agent browse a project
|
|
3
|
+
description: Give an AI agent's browser a short sign-in to one project, so it can look at and click through your app as you.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Coding agents often need to see the app they're working on: check that a page renders, click through a flow, or read what a screen shows. Every Canopy project sits behind the Monks sign-in, which an agent can't get through on its own.
|
|
7
|
+
|
|
8
|
+
`canopy browse` gives the agent a way in that doesn't involve handing over your own browser session:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
canopy browse my-app --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It prints a one-time link. Whatever browser opens it is signed in to **my-app**, as you, for **an hour**, and to nothing else on Canopy.
|
|
15
|
+
|
|
16
|
+
## What the agent gets
|
|
17
|
+
|
|
18
|
+
- **One project, one channel.** The live site, or staging with `--staging`. The browser is not signed in to your other projects, or to the dashboard.
|
|
19
|
+
- **You.** The app sees you as the visitor, exactly as when you open it yourself. Your backend's `env.USER` is you, and data you can see, the agent can see. On a collaborators-only project you have to be a collaborator.
|
|
20
|
+
- **An hour.** After that the agent meets the sign-in page and can run `canopy browse` again.
|
|
21
|
+
- **Marked as an agent.** Your backend can tell the request came from an agent: `mcpCaller(request, env).agent` from `@mediamonks/canopy/mcp` is `true`.
|
|
22
|
+
|
|
23
|
+
The link itself works **once**, within **a minute**. Opening it a second time shows a page saying to run `canopy browse` again.
|
|
24
|
+
|
|
25
|
+
## Using it from different tools
|
|
26
|
+
|
|
27
|
+
**Playwright, or a browser tool in your agent:** open the URL. The browser is redirected to the page and stays signed in.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
await page.goto(url); // lands on the project, signed in
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**curl:** keep the cookie in a jar and follow the redirect.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -c jar.txt -L "$(canopy browse my-app --json | jq -r .url)"
|
|
37
|
+
curl -b jar.txt https://my-app.canopy.website/api/notes
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**A specific page:** `canopy browse my-app --path /settings` lands there directly.
|
|
41
|
+
|
|
42
|
+
## Stopping it
|
|
43
|
+
|
|
44
|
+
The agent's session ends when the hour is up. It also ends early if you:
|
|
45
|
+
|
|
46
|
+
- run `canopy logout`,
|
|
47
|
+
- revoke that CLI sign-in on your Account page in the dashboard, or
|
|
48
|
+
- sign out everywhere.
|
|
49
|
+
|
|
50
|
+
If you lose access to a collaborators-only project, the agent loses it too.
|
|
51
|
+
|
|
52
|
+
## Good to know
|
|
53
|
+
|
|
54
|
+
- Your project's backend needs a redeploy after this feature shipped before an agent can call its `/api/*` routes. Until then those calls answer "redeploy required", while pages still load.
|
|
55
|
+
- The feedback widget doesn't appear for agents.
|
|
56
|
+
- Never give an agent your own browser session cookie. It opens every project and the dashboard, as you.
|
|
57
|
+
|
|
58
|
+
See also [canopy browse](https://docs.canopy.website/cli/browse), and [Agent tools](https://docs.canopy.website/guides/agent-tools) for letting agents call your backend directly.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy browse
|
|
3
|
+
description: Print a one-time sign-in link that lets an agent's browser open a project as you.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy browse` prints a link for an AI agent's browser: Playwright, a browser tool in Claude or Cursor, or plain `curl`. Opening the link signs that browser in to **one project**, as you, for **one hour**. It reaches nothing else on Canopy: no other projects, and not the dashboard.
|
|
7
|
+
|
|
8
|
+
Read [Let your agent browse a project](https://docs.canopy.website/guides/agent-browsing) for the full picture.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
canopy browse [slug] [--staging] [--path <page>] [--json]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Arguments & flags
|
|
17
|
+
|
|
18
|
+
- `[slug]` — the project. Defaults to the `canopy.json` in the current directory.
|
|
19
|
+
- `--staging` — link to the project's [staging channel](https://docs.canopy.website/guides/staging) instead of the live site.
|
|
20
|
+
- `--path <page>` — the page to land on after signing in, such as `/settings`. Defaults to `/`.
|
|
21
|
+
- `--json` — print only `{ "url", "expires_at" }`, for an agent to read.
|
|
22
|
+
|
|
23
|
+
The site comes from the active profile, as for every command (`--profile staging` links to the project on the staging site).
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
The command uses your `canopy login` sign-in to ask Canopy for a link to the project, under the same rules as opening it yourself. Collaborators-only projects only give links to collaborators.
|
|
28
|
+
|
|
29
|
+
- The link **works once** and **expires after a minute**. Run the command again for a new one.
|
|
30
|
+
- The browser that opens it is signed in to that project and channel for an hour. After that it sees the sign-in page again.
|
|
31
|
+
- Running `canopy logout`, revoking the CLI sign-in on your Account page, or signing out everywhere also signs the agent out.
|
|
32
|
+
- The command never opens a browser itself (that's [canopy open](https://docs.canopy.website/cli/open)), never prints a cookie, and saves nothing.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
canopy browse my-app --json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prints `{"url":"https://my-app.…/__canopy/agent-login?ticket=…","expires_at":"…"}`. Hand the URL to your agent's browser.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
canopy browse --staging --path /admin
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A link to the staging channel of the project in this folder, landing on `/admin`.
|
|
47
|
+
|
|
48
|
+
See also [canopy mcp](https://docs.canopy.website/cli/mcp), which lets agents call your project's backend as tools instead of using its pages.
|
package/dist/docs/cli-create.md
CHANGED
|
@@ -19,6 +19,7 @@ canopy create existing-app --no-scaffold # register only, no files written
|
|
|
19
19
|
- `--slug <slug>` — the URL slug; the project will live at `https://<slug>.<site-domain>`. Lowercase letters, digits and dashes only. Defaults to a slugified version of the name (prompted on a TTY).
|
|
20
20
|
- `--template <id>` — starter to scaffold: `static` (plain HTML/CSS/JS, no build step, untyped SDK from the platform — simplest apps), `spa` (Vite + React + TypeScript with the typed SDK from npm — best for real SDK use or growth), or `worker` (`spa` plus a backend worker in `worker/` — for server-side code: secrets, schedules, calling external APIs). A picker appears when omitted on a TTY; `static` is the default otherwise.
|
|
21
21
|
- `--scaffold` / `--no-scaffold` — whether to write `./<slug>/` from the template. When neither is given, the CLI scaffolds unless the current directory already has a `canopy.json` (in which case it assumes you're registering an existing app and only registers).
|
|
22
|
+
- `--repo` / `--no-repo` — also create a git repository for the project on Canopy and set up the new folder to push to it (see [`canopy repo`](https://docs.canopy.website/cli/repo)). When neither is given, an interactive run asks (default no) on sites that offer repositories; non-interactive runs create none.
|
|
22
23
|
- `--profile <site>` — global flag (or `CANOPY_PROFILE`): which site to create the project on. The chosen profile is pinned into the scaffolded `canopy.json`.
|
|
23
24
|
|
|
24
25
|
## How it works
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy repo
|
|
3
|
+
description: Create, clone and set up the project's git repository on Canopy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy repo` manages the project's git repository. A project has at most one: a
|
|
7
|
+
Canopy repository, which Canopy keeps for it, or its own GitHub repository.
|
|
8
|
+
Once a folder is set up, you use plain git. The CLI signs git in on every push
|
|
9
|
+
and pull, so you never handle a password or token.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
canopy repo show [--json]
|
|
15
|
+
canopy repo create
|
|
16
|
+
canopy repo clone <slug> [folder]
|
|
17
|
+
canopy repo connect
|
|
18
|
+
canopy repo delete [--yes]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Arguments & flags
|
|
22
|
+
|
|
23
|
+
`show`, `create`, `connect` and `delete` resolve the project the usual way:
|
|
24
|
+
`canopy.json` in the target folder, or `--project <slug>`. All of them honour
|
|
25
|
+
the global `--profile <site>` / `CANOPY_PROFILE`.
|
|
26
|
+
|
|
27
|
+
- `show` — whether the project has a repository, and if so its remote, when it
|
|
28
|
+
was created and when it was last pushed to.
|
|
29
|
+
- `--json` — the same as JSON, for scripts and agents.
|
|
30
|
+
- `create` — create the project's Canopy repository. Owners, editors and
|
|
31
|
+
platform admins can. Run it in the project's folder and the folder is set up
|
|
32
|
+
too.
|
|
33
|
+
- `clone <slug> [folder]` — clone the project's repository into a new folder
|
|
34
|
+
(the slug by default), ready to push.
|
|
35
|
+
- `connect` — set up an existing folder.
|
|
36
|
+
- If the folder isn't a git repository yet, it is initialised on branch `main`.
|
|
37
|
+
- If it has no `origin`, the Canopy repository becomes `origin`.
|
|
38
|
+
- If `origin` points somewhere else, the Canopy repository is added as
|
|
39
|
+
`canopy`.
|
|
40
|
+
- `delete` — delete the repository and all its history. Owner only. Asks you to
|
|
41
|
+
type the slug.
|
|
42
|
+
- `--yes` — skip that question.
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
Setting up a folder (`create`, `clone`, `connect`) does two things:
|
|
47
|
+
|
|
48
|
+
1. It points a remote at the project's repository.
|
|
49
|
+
2. It registers the CLI as git's credential helper for that one repository.
|
|
50
|
+
|
|
51
|
+
Nothing secret is written into the folder or its git settings. On each push or
|
|
52
|
+
pull, git asks the CLI for credentials. The CLI exchanges your `canopy login`
|
|
53
|
+
for a short-lived token that only works for this repository, keeps it in
|
|
54
|
+
`~/.canopy` for up to an hour, and gets a new one when needed. `canopy logout`
|
|
55
|
+
throws them away.
|
|
56
|
+
|
|
57
|
+
Pushing doesn't deploy. Run `canopy deploy` for that.
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
canopy repo create
|
|
63
|
+
git add -A && git commit -m "First version" && git push -u origin main
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Creates the repository for the project in this folder and pushes the code.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
canopy repo clone my-tool
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Clones a teammate's project into `./my-tool`, ready to push.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
canopy repo show --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"kind": "canopy",
|
|
81
|
+
"remote": "https://monks.ghe.com/canopy/my-tool.git",
|
|
82
|
+
"created_at": "2026-10-21T09:12:00Z",
|
|
83
|
+
"last_push_at": "2026-10-21T09:15:42Z"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Read the [git guide](https://docs.canopy.website/guides/git) for the bigger picture.
|
|
@@ -93,6 +93,9 @@ Then open the live site:
|
|
|
93
93
|
canopy open
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
Want your code in git too, without a GitHub account? `canopy repo create` gives
|
|
97
|
+
the project a repository on Canopy — see [Keep your code in git](https://docs.canopy.website/guides/git).
|
|
98
|
+
|
|
96
99
|
## Where to go next
|
|
97
100
|
|
|
98
101
|
Everything the CLI can do is documented and available offline. List the topics:
|
package/dist/docs/git.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Keep your code in git
|
|
3
|
+
description: Give your project a git repository on Canopy — history, backup and sharing, no GitHub account needed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Every project can have one git repository. If you already use GitHub, keep using
|
|
7
|
+
it. If you don't have a GitHub account, Canopy can keep a private repository for
|
|
8
|
+
your project instead. Your code then has history and a backup, and teammates and
|
|
9
|
+
their agents can pick it up.
|
|
10
|
+
|
|
11
|
+
A Canopy repository is for storing code. Pushing to it doesn't deploy anything —
|
|
12
|
+
`canopy deploy` still does that.
|
|
13
|
+
|
|
14
|
+
## Create one
|
|
15
|
+
|
|
16
|
+
From your project folder:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
canopy repo create
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That creates the repository and sets up the folder, so from then on it is plain
|
|
23
|
+
git:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git add -A
|
|
27
|
+
git commit -m "First version"
|
|
28
|
+
git push -u origin main
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
New projects can get one straight away with `canopy create my-tool --repo`. You
|
|
32
|
+
can also create one from the dashboard, on the project's **Deploys** tab.
|
|
33
|
+
|
|
34
|
+
## Work on it from another machine (or hand it to a teammate)
|
|
35
|
+
|
|
36
|
+
Anyone on the project can clone it:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
canopy repo clone my-tool
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Already have the code in a folder? Set that folder up instead:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
canopy repo connect
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If the folder already pushes somewhere else (say your own GitHub), that stays
|
|
49
|
+
`origin` and the Canopy repository is added as a second remote called `canopy`.
|
|
50
|
+
|
|
51
|
+
## Signing in
|
|
52
|
+
|
|
53
|
+
You never type a password or paste a token. The Canopy CLI signs git in for you
|
|
54
|
+
on every push and pull, using your `canopy login`. When that sign-in expires, git
|
|
55
|
+
stops and tells you to run `canopy login` again.
|
|
56
|
+
|
|
57
|
+
Who can do what follows the project's members:
|
|
58
|
+
|
|
59
|
+
- Owners and editors can pull and push.
|
|
60
|
+
- Platform admins can pull.
|
|
61
|
+
- Nobody else gets in.
|
|
62
|
+
|
|
63
|
+
Remove someone from the project and they can no longer push or pull, even from
|
|
64
|
+
a copy they already cloned.
|
|
65
|
+
|
|
66
|
+
## Git apps (GitHub Desktop, VS Code, Tower, …)
|
|
67
|
+
|
|
68
|
+
Set the folder up once with `canopy repo clone` or `canopy repo connect` in a
|
|
69
|
+
terminal. After that, git apps that use the git installed on your computer push
|
|
70
|
+
and pull as usual. Apps that bring their own git or ask for a password can't
|
|
71
|
+
sign in to a Canopy repository; use the terminal for those.
|
|
72
|
+
|
|
73
|
+
## Good to know
|
|
74
|
+
|
|
75
|
+
- **Keep secrets out.** Files like `.env` and `.dev.vars` are ignored by the
|
|
76
|
+
project templates; secrets belong in `canopy secrets`.
|
|
77
|
+
- **There is no branch protection.** Anyone who can push can rewrite history, so
|
|
78
|
+
avoid force-pushes on `main`.
|
|
79
|
+
- **Deleting is for good.** `canopy repo delete` (owner only) removes the
|
|
80
|
+
repository and all its history. Deleting the project deletes its repository
|
|
81
|
+
too.
|
|
82
|
+
- **Moving to your own GitHub later** is ordinary git: create an empty
|
|
83
|
+
repository there, then `git remote add github <its url>` and
|
|
84
|
+
`git push github --all`.
|
|
85
|
+
|
|
86
|
+
See the [`canopy repo` reference](https://docs.canopy.website/cli/repo) for every command and flag.
|
package/dist/docs/index.json
CHANGED
|
@@ -67,11 +67,21 @@
|
|
|
67
67
|
"title": "CI and deploy tokens",
|
|
68
68
|
"summary": "Push-to-main deploys using a project-scoped deploy token."
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"slug": "git",
|
|
72
|
+
"title": "Keep your code in git",
|
|
73
|
+
"summary": "Give your project a git repository on Canopy — history, backup and sharing, no GitHub account needed."
|
|
74
|
+
},
|
|
70
75
|
{
|
|
71
76
|
"slug": "agent-tools",
|
|
72
77
|
"title": "Agent tools (MCP)",
|
|
73
78
|
"summary": "Let AI agents like Claude Code and Cursor use your project's backend as tools — as the person running them."
|
|
74
79
|
},
|
|
80
|
+
{
|
|
81
|
+
"slug": "agent-browsing",
|
|
82
|
+
"title": "Let your agent browse a project",
|
|
83
|
+
"summary": "Give an AI agent's browser a short sign-in to one project, so it can look at and click through your app as you."
|
|
84
|
+
},
|
|
75
85
|
{
|
|
76
86
|
"slug": "porting-nextjs",
|
|
77
87
|
"title": "Porting a Next.js app",
|
|
@@ -251,6 +261,11 @@
|
|
|
251
261
|
"title": "canopy token",
|
|
252
262
|
"summary": "Create, list, and revoke project-scoped deploy tokens for CI and push-to-main deploys."
|
|
253
263
|
},
|
|
264
|
+
{
|
|
265
|
+
"slug": "cli-repo",
|
|
266
|
+
"title": "canopy repo",
|
|
267
|
+
"summary": "Create, clone and set up the project's git repository on Canopy."
|
|
268
|
+
},
|
|
254
269
|
{
|
|
255
270
|
"slug": "cli-egress",
|
|
256
271
|
"title": "canopy egress",
|
|
@@ -301,6 +316,11 @@
|
|
|
301
316
|
"title": "canopy open",
|
|
302
317
|
"summary": "Open the deployed project in your browser."
|
|
303
318
|
},
|
|
319
|
+
{
|
|
320
|
+
"slug": "cli-browse",
|
|
321
|
+
"title": "canopy browse",
|
|
322
|
+
"summary": "Print a one-time sign-in link that lets an agent's browser open a project as you."
|
|
323
|
+
},
|
|
304
324
|
{
|
|
305
325
|
"slug": "cli-docs",
|
|
306
326
|
"title": "canopy docs",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-Doigafvd.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
4
|
import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-Doigafvd.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-C4rMhE7i.mjs";
|
|
4
4
|
import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
|
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-Doigafvd.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -20,6 +20,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
20
20
|
* ai models the site's AI models and auto/* aliases
|
|
21
21
|
* mcp add|serve a project's tools for AI agents (docs/49)
|
|
22
22
|
* open open the deployed project / dashboard [phase 1]
|
|
23
|
+
* browse one-time sign-in link for an agent's browser
|
|
23
24
|
*/
|
|
24
25
|
if (process.env["CANOPY_UPDATE_REFRESH"] === "1") {
|
|
25
26
|
await runRefresh();
|
|
@@ -62,14 +63,14 @@ const main = defineCommand({
|
|
|
62
63
|
description: "canopy — create and deploy vibecoded projects"
|
|
63
64
|
},
|
|
64
65
|
subCommands: {
|
|
65
|
-
login: () => import("./login-
|
|
66
|
-
whoami: () => import("./login-
|
|
67
|
-
logout: () => import("./login-
|
|
68
|
-
create: () => import("./projects-
|
|
69
|
-
init: () => import("./projects-
|
|
70
|
-
projects: () => import("./projects-
|
|
71
|
-
delete: () => import("./projects-
|
|
72
|
-
dev: () => import("./dev-
|
|
66
|
+
login: () => import("./login-D0J5s_Ex.mjs").then((m) => m.loginCommand),
|
|
67
|
+
whoami: () => import("./login-D0J5s_Ex.mjs").then((m) => m.whoamiCommand),
|
|
68
|
+
logout: () => import("./login-D0J5s_Ex.mjs").then((m) => m.logoutCommand),
|
|
69
|
+
create: () => import("./projects-DQAhCZm8.mjs").then((m) => m.createCommand),
|
|
70
|
+
init: () => import("./projects-DQAhCZm8.mjs").then((m) => m.initCommand),
|
|
71
|
+
projects: () => import("./projects-DQAhCZm8.mjs").then((m) => m.projectsCommand),
|
|
72
|
+
delete: () => import("./projects-DQAhCZm8.mjs").then((m) => m.deleteCommand),
|
|
73
|
+
dev: () => import("./dev-DO6Eso4L.mjs").then((m) => m.devCommand),
|
|
73
74
|
deploy: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.deployCommand),
|
|
74
75
|
promote: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.promoteCommand),
|
|
75
76
|
staging: () => import("./staging-BZVty57b.mjs").then((m) => m.stagingCommand),
|
|
@@ -79,19 +80,21 @@ const main = defineCommand({
|
|
|
79
80
|
egress: () => import("./egress-If_Gsx_M.mjs").then((m) => m.egressCommand),
|
|
80
81
|
secrets: () => import("./secrets-BSEjZnxg.mjs").then((m) => m.secretsCommand),
|
|
81
82
|
schedule: () => import("./schedule-C5lMf3ny.mjs").then((m) => m.scheduleCommand),
|
|
82
|
-
feedback: () => import("./feedback-
|
|
83
|
+
feedback: () => import("./feedback-vvysW6iw.mjs").then((m) => m.feedbackCommand),
|
|
83
84
|
db: () => import("./db-BB6O6085.mjs").then((m) => m.dbCommand),
|
|
84
85
|
usage: () => import("./usage-CUYrWxv0.mjs").then((m) => m.usageCommand),
|
|
85
86
|
ai: () => import("./ai-DHLnyqfq.mjs").then((m) => m.aiCommand),
|
|
86
87
|
open: () => import("./open-DcaQKi1v.mjs").then((m) => m.openCommand),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
browse: () => import("./browse-Dycwj0V_.mjs").then((m) => m.browseCommand),
|
|
89
|
+
docs: () => import("./docs-Cdoh5sMW.mjs").then((m) => m.docsCommand),
|
|
90
|
+
skill: () => import("./skill-DfSAZ2cW.mjs").then((m) => m.skillCommand),
|
|
91
|
+
upgrade: () => import("./upgrade-DCQ3NzoL.mjs").then((m) => m.upgradeCommand),
|
|
90
92
|
logs: () => import("./logs-DQQOawOG.mjs").then((m) => m.logsCommand),
|
|
91
|
-
mcp: () => import("./mcp-
|
|
93
|
+
mcp: () => import("./mcp-OR_ashGC.mjs").then((m) => m.mcpCommand),
|
|
94
|
+
repo: () => import("./repo-1RQ1vBMv.mjs").then((m) => m.repoCommand)
|
|
92
95
|
}
|
|
93
96
|
});
|
|
94
|
-
const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve";
|
|
97
|
+
const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve" || process.argv[2] === "repo" && process.argv[3] === "credential";
|
|
95
98
|
if (process.argv[2] !== "upgrade" && !quiet) notifyOfUpdate(version);
|
|
96
99
|
if (!quiet && process.argv[2] && process.argv[2] !== "--help" && process.argv[2] !== "-h") console.error(siteHeaderLine(version));
|
|
97
100
|
await runMain(main);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as apiDataRequest } from "./api-C4rMhE7i.mjs";
|
|
2
|
-
import { a as dbRouter, i as realtimeRouter, o as META_COLUMNS, s as blobRouter } from "./src-
|
|
3
|
-
import { i as rewriteApiPath } from "./dev-
|
|
2
|
+
import { a as dbRouter, i as realtimeRouter, o as META_COLUMNS, s as blobRouter } from "./src-DE37PKho.mjs";
|
|
3
|
+
import { i as rewriteApiPath } from "./dev-CGSIY1-y.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-DM2J10CB.mjs";
|
|
2
2
|
import { n as openBrowser, r as reportErrors } from "./ui-C_jSc1Kz.mjs";
|
|
3
|
+
import { n as clearRepoTokenCache } from "./repo-credential-CNDRM-tG.mjs";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
4
5
|
import { hostname } from "node:os";
|
|
5
6
|
import * as p from "@clack/prompts";
|
|
@@ -98,8 +99,9 @@ const logoutCommand = defineCommand({
|
|
|
98
99
|
if (!result.ok) failed.push(`${id} (${result.error})`);
|
|
99
100
|
}
|
|
100
101
|
clearAllProfileTokens();
|
|
102
|
+
clearRepoTokenCache();
|
|
101
103
|
if (args.local) {
|
|
102
|
-
console.log("Logged out locally — tokens removed from ~/.canopy
|
|
104
|
+
console.log("Logged out locally — tokens removed from ~/.canopy but still valid on the platform until they expire (git repository tokens within the hour). Revoke them from the dashboard's Account page.");
|
|
103
105
|
return;
|
|
104
106
|
}
|
|
105
107
|
if (failed.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-Doigafvd.mjs";
|
|
2
2
|
import { d as tryResolveProject, l as stagingUrl, n as siteConfig, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
|
|
4
4
|
import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
2
|
import { i as apiRequest } from "./api-C4rMhE7i.mjs";
|
|
3
3
|
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-C_jSc1Kz.mjs";
|
|
4
|
+
import { r as offerRepoOnCreate } from "./repo-Bb9ntNa4.mjs";
|
|
4
5
|
import { defineCommand } from "citty";
|
|
5
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { join, relative, resolve, sep } from "node:path";
|
|
@@ -105,6 +106,11 @@ const createCommand = defineCommand({
|
|
|
105
106
|
type: "boolean",
|
|
106
107
|
description: "Scaffold ./<slug>/ from the starter template",
|
|
107
108
|
negativeDescription: "Only register — no files written"
|
|
109
|
+
},
|
|
110
|
+
repo: {
|
|
111
|
+
type: "boolean",
|
|
112
|
+
description: "Also create a git repository for the project on Canopy",
|
|
113
|
+
negativeDescription: "Don't create a git repository (no question asked)"
|
|
108
114
|
}
|
|
109
115
|
},
|
|
110
116
|
run: reportErrors("create", async ({ args }) => {
|
|
@@ -166,14 +172,20 @@ const createCommand = defineCommand({
|
|
|
166
172
|
...tryReadCanopyJson(targetDir),
|
|
167
173
|
slug
|
|
168
174
|
});
|
|
175
|
+
const remote = await offerRepoOnCreate(slug, targetDir, args.repo);
|
|
169
176
|
const steps = [
|
|
170
177
|
`cd ${slug}`,
|
|
171
178
|
...template.needsInstall ? ["npm install"] : [],
|
|
179
|
+
...remote ? [`git add -A && git commit -m "Initial commit" && git push -u ${remote} main`] : [],
|
|
172
180
|
"canopy dev (or deploy right away: canopy deploy)"
|
|
173
181
|
];
|
|
174
182
|
p.note(steps.join("\n"), `Scaffolded ./${slug}/ (${template.id}) — next`);
|
|
175
183
|
p.outro(`Live URL (after first deploy): ${projectUrl(project.slug)}`);
|
|
176
|
-
} else
|
|
184
|
+
} else {
|
|
185
|
+
const remote = await offerRepoOnCreate(slug, process.cwd(), args.repo);
|
|
186
|
+
if (remote) p.log.info(`This folder pushes to the project's git repository as "${remote}".`);
|
|
187
|
+
p.outro(existsSync(join(process.cwd(), "canopy.json")) ? "Registered without scaffolding — the existing canopy.json was left unchanged." : "Registered without scaffolding — add a canopy.json to deploy an existing app.");
|
|
188
|
+
}
|
|
177
189
|
})
|
|
178
190
|
});
|
|
179
191
|
/** Empty enough to scaffold into: missing, or holding only dotfiles (.git, .agents…). */
|