@mediamonks/canopy-cli 0.7.37 → 0.7.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/cli-create.md +2 -0
- package/dist/docs/cli-projects.md +2 -0
- package/dist/docs/cli-secrets.md +2 -1
- package/dist/docs/cli-template.md +47 -0
- package/dist/docs/index.json +10 -0
- package/dist/docs/templates.md +84 -0
- package/dist/{docs-DvCUaI1O.mjs → docs-T1nuu00A.mjs} +1 -1
- package/dist/{feedback-CIjjiEaj.mjs → feedback-C5NKCOXQ.mjs} +1 -1
- package/dist/index.mjs +13 -12
- package/dist/{mcp-DTUJV4FT.mjs → mcp-BwLRcfqz.mjs} +1 -1
- package/dist/{package-BY69Tjq_.mjs → package-CdDH35SE.mjs} +1 -1
- package/dist/project-template-BEr_tgIY.mjs +125 -0
- package/dist/project-template-DdT0dqpn.mjs +2 -0
- package/dist/{projects-DPGRI-SI.mjs → projects-Bm4oTodB.mjs} +73 -7
- package/dist/{repo-CKIQrrHK.mjs → repo-CE22HpqZ.mjs} +16 -13
- package/dist/repo-DlYX_1tN.mjs +2 -0
- package/dist/{secrets-BQYaPSOT.mjs → secrets-BOKuWBYV.mjs} +4 -3
- package/dist/{skill-B96cKy8K.mjs → skill-3wOwhzDx.mjs} +1 -1
- package/dist/{upgrade-SfxmPh3F.mjs → upgrade-BJ2dWjCG.mjs} +1 -1
- package/package.json +1 -1
- package/dist/repo-DvrIsubH.mjs +0 -2
package/dist/docs/cli-create.md
CHANGED
|
@@ -11,6 +11,7 @@ description: Register a new project on Canopy and scaffold it locally from a sta
|
|
|
11
11
|
canopy create # fully interactive
|
|
12
12
|
canopy create "My Portfolio" --slug my-portfolio --template spa
|
|
13
13
|
canopy create existing-app --no-scaffold # register only, no files written
|
|
14
|
+
canopy create "My Retro" --from retro-board # start from a template
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## Arguments & flags
|
|
@@ -20,6 +21,7 @@ canopy create existing-app --no-scaffold # register only, no files written
|
|
|
20
21
|
- `--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
22
|
- `--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
23
|
- `--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.
|
|
24
|
+
- `--from <template>` — start from a template project instead of a starter: the new project gets a copy of the template's code in its own git repository, cloned into `./<slug>/`, plus the setup the code needs. The CLI shows what comes along and asks before creating. `--template`, `--scaffold` and `--repo` don't apply. See [Start from a template](https://docs.canopy.website/guides/templates).
|
|
23
25
|
- `--profile <site>` — global flag (or `CANOPY_PROFILE`): which site to create the project on. The chosen profile is pinned into the scaffolded `canopy.json`.
|
|
24
26
|
|
|
25
27
|
## How it works
|
|
@@ -10,12 +10,14 @@ description: List your Canopy projects with their status, role, and live URLs.
|
|
|
10
10
|
```bash
|
|
11
11
|
canopy projects
|
|
12
12
|
canopy projects --all
|
|
13
|
+
canopy projects --templates
|
|
13
14
|
canopy --profile staging projects
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## Arguments & flags
|
|
17
18
|
|
|
18
19
|
- `--all` — include projects you can't edit. By default the list shows only projects where you're an owner or editor (admins always see everything, since they can edit everything).
|
|
20
|
+
- `--templates` — list the templates anyone can start a project from, with their owners. See [Start from a template](https://docs.canopy.website/guides/templates).
|
|
19
21
|
- `--profile <site>` — global flag (or `CANOPY_PROFILE`): each site has its own project registry, so listing on `staging` shows a completely different set than `main`.
|
|
20
22
|
|
|
21
23
|
## Details
|
package/dist/docs/cli-secrets.md
CHANGED
|
@@ -25,7 +25,8 @@ target directory, or `--project <slug>` to override — and honor the global
|
|
|
25
25
|
`--profile <site>` / `CANOPY_PROFILE`.
|
|
26
26
|
|
|
27
27
|
- `list` — print the secret names and when each was last set. Values are
|
|
28
|
-
never shown.
|
|
28
|
+
never shown. For a project started from a template, names it still needs a
|
|
29
|
+
value for are listed as missing.
|
|
29
30
|
- `set <name>` — store a value. Names are `UPPER_SNAKE_CASE`, up to 64
|
|
30
31
|
characters. The value comes from **stdin** when piped (one trailing newline
|
|
31
32
|
is trimmed, so `echo` works too), from `--from-env <VAR>` to read an
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy template
|
|
3
|
+
description: Offer a project as a template, or see what a template gives a new project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy template` controls whether others can start a project from yours, and
|
|
7
|
+
shows what a template gives a new project. To start from one, use
|
|
8
|
+
[`canopy create --from`](https://docs.canopy.website/cli/create).
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
canopy template mark [--yes]
|
|
14
|
+
canopy template unmark
|
|
15
|
+
canopy template show <slug>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Arguments & flags
|
|
19
|
+
|
|
20
|
+
`mark` and `unmark` resolve the project the usual way: `canopy.json` in the
|
|
21
|
+
target folder, or `--project <slug>`. All of them honour the global
|
|
22
|
+
`--profile <site>` / `CANOPY_PROFILE`.
|
|
23
|
+
|
|
24
|
+
- `mark` — let anyone on the site start a project from this one. Owner only.
|
|
25
|
+
The project needs a Canopy git repository with code pushed to it. Asks for
|
|
26
|
+
confirmation; `--yes` skips it in scripts. Warns when `canopy.json` names no
|
|
27
|
+
migrations folder, because new projects then start without any tables.
|
|
28
|
+
- `unmark` — stop new projects starting from this one. Existing copies are not
|
|
29
|
+
affected.
|
|
30
|
+
- `show <slug>` — what a new project gets from a template: the code, outbound
|
|
31
|
+
hosts, page settings, Google access, schedules (turned off) and secret names.
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
canopy template mark
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Offers the project in this folder as a template.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
canopy template show retro-board
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Shows what `canopy create --from retro-board` would give you.
|
|
46
|
+
|
|
47
|
+
Read [Start from a template](https://docs.canopy.website/guides/templates) for the bigger picture.
|
package/dist/docs/index.json
CHANGED
|
@@ -72,6 +72,11 @@
|
|
|
72
72
|
"title": "Keep your code in git",
|
|
73
73
|
"summary": "Give your project a git repository on Canopy — history, backup and sharing, no GitHub account needed."
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
"slug": "templates",
|
|
77
|
+
"title": "Start from a template",
|
|
78
|
+
"summary": "Begin a project from someone else's working one — a copy of their code and setup, never their data."
|
|
79
|
+
},
|
|
75
80
|
{
|
|
76
81
|
"slug": "agent-tools",
|
|
77
82
|
"title": "Agent tools (MCP)",
|
|
@@ -266,6 +271,11 @@
|
|
|
266
271
|
"title": "canopy repo",
|
|
267
272
|
"summary": "Create, clone and set up the project's git repository on Canopy."
|
|
268
273
|
},
|
|
274
|
+
{
|
|
275
|
+
"slug": "cli-template",
|
|
276
|
+
"title": "canopy template",
|
|
277
|
+
"summary": "Offer a project as a template, or see what a template gives a new project."
|
|
278
|
+
},
|
|
269
279
|
{
|
|
270
280
|
"slug": "cli-egress",
|
|
271
281
|
"title": "canopy egress",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Start from a template
|
|
3
|
+
description: Begin a project from someone else's working one — a copy of their code and setup, never their data.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Some projects are worth starting from: a retro board, a feedback form, a small
|
|
7
|
+
dashboard on an internal API. When the owner offers one as a **template**,
|
|
8
|
+
anyone on Canopy can start a project of their own from it. You get a copy of
|
|
9
|
+
the code and the setup the code needs, then change it however you like.
|
|
10
|
+
|
|
11
|
+
Your copy is yours. Later changes to the template don't reach it, and nothing
|
|
12
|
+
you do reaches the template.
|
|
13
|
+
|
|
14
|
+
## Find one
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
canopy projects --templates
|
|
18
|
+
canopy template show retro-board
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`show` lists exactly what a new project gets. In the dashboard, switch on the
|
|
22
|
+
**Templates** filter on the project list; each template has a **Use as
|
|
23
|
+
template** button that shows the same list.
|
|
24
|
+
|
|
25
|
+
## Start your project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
canopy create "My Retro" --slug my-retro --from retro-board
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Canopy creates the project, gives it its own git repository with the
|
|
32
|
+
template's code in one fresh commit, and clones it into `./my-retro/`. It then
|
|
33
|
+
tells you what is left, usually:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd my-retro
|
|
37
|
+
canopy secrets set OPENAI_KEY
|
|
38
|
+
canopy deploy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The first deploy runs the template's migrations, so your database starts with
|
|
42
|
+
its tables and nothing else.
|
|
43
|
+
|
|
44
|
+
## What you get, and what you don't
|
|
45
|
+
|
|
46
|
+
You get:
|
|
47
|
+
|
|
48
|
+
- the code in the template's repository, as it is right now, without its
|
|
49
|
+
history;
|
|
50
|
+
- its allowed outbound hosts and page settings;
|
|
51
|
+
- the Google access it asks visitors for (restricted Google access stays off
|
|
52
|
+
until a Canopy admin unlocks it for your project);
|
|
53
|
+
- its schedules, **turned off** — turn each one on when you're ready;
|
|
54
|
+
- the **names** of its secrets, so you know what to fill in. `canopy secrets
|
|
55
|
+
list` shows the ones still missing.
|
|
56
|
+
|
|
57
|
+
You never get its data, stored files, secret values, members, client password,
|
|
58
|
+
budgets, feedback or deploy history. Your project starts with the usual
|
|
59
|
+
defaults for those.
|
|
60
|
+
|
|
61
|
+
## Offer your project as a template
|
|
62
|
+
|
|
63
|
+
Your project needs a Canopy git repository with your code pushed to it (see
|
|
64
|
+
[Keep your code in git](https://docs.canopy.website/guides/git)). Then:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
canopy template mark
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
or **Settings → Template** in the dashboard. Only the owner can do this.
|
|
71
|
+
|
|
72
|
+
Before you do:
|
|
73
|
+
|
|
74
|
+
- **Everyone on the site can copy the code.** Make sure the repository holds no
|
|
75
|
+
keys, passwords or client data.
|
|
76
|
+
- **Put sample data in migrations.** The database never comes along, so any
|
|
77
|
+
starting rows belong in your migrations. Without a migrations folder, new
|
|
78
|
+
projects start with an empty database.
|
|
79
|
+
- **Explain setup in the README.** It's the first thing people read in their
|
|
80
|
+
copy.
|
|
81
|
+
|
|
82
|
+
`canopy template unmark` stops new copies. Projects already started from it
|
|
83
|
+
are not affected. The template's Settings page shows how many projects started
|
|
84
|
+
from it.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CdDH35SE.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-CHfKV6lG.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CdDH35SE.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-BPEYm9ts.mjs";
|
|
4
4
|
import { r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.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-CdDH35SE.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";
|
|
@@ -66,10 +66,10 @@ const main = defineCommand({
|
|
|
66
66
|
login: () => import("./login-CtDWXICZ.mjs").then((m) => m.loginCommand),
|
|
67
67
|
whoami: () => import("./login-CtDWXICZ.mjs").then((m) => m.whoamiCommand),
|
|
68
68
|
logout: () => import("./login-CtDWXICZ.mjs").then((m) => m.logoutCommand),
|
|
69
|
-
create: () => import("./projects-
|
|
70
|
-
init: () => import("./projects-
|
|
71
|
-
projects: () => import("./projects-
|
|
72
|
-
delete: () => import("./projects-
|
|
69
|
+
create: () => import("./projects-Bm4oTodB.mjs").then((m) => m.createCommand),
|
|
70
|
+
init: () => import("./projects-Bm4oTodB.mjs").then((m) => m.initCommand),
|
|
71
|
+
projects: () => import("./projects-Bm4oTodB.mjs").then((m) => m.projectsCommand),
|
|
72
|
+
delete: () => import("./projects-Bm4oTodB.mjs").then((m) => m.deleteCommand),
|
|
73
73
|
dev: () => import("./dev-D_qzeLng.mjs").then((m) => m.devCommand),
|
|
74
74
|
deploy: () => import("./deploy-CqCyry4o.mjs").then((m) => m.deployCommand),
|
|
75
75
|
promote: () => import("./deploy-CqCyry4o.mjs").then((m) => m.promoteCommand),
|
|
@@ -78,20 +78,21 @@ const main = defineCommand({
|
|
|
78
78
|
download: () => import("./deploy-CqCyry4o.mjs").then((m) => m.downloadCommand),
|
|
79
79
|
token: () => import("./token-BWnLsWLg.mjs").then((m) => m.tokenCommand),
|
|
80
80
|
egress: () => import("./egress-ButFsQ2N.mjs").then((m) => m.egressCommand),
|
|
81
|
-
secrets: () => import("./secrets-
|
|
81
|
+
secrets: () => import("./secrets-BOKuWBYV.mjs").then((m) => m.secretsCommand),
|
|
82
82
|
schedule: () => import("./schedule-DdMq5eXC.mjs").then((m) => m.scheduleCommand),
|
|
83
|
-
feedback: () => import("./feedback-
|
|
83
|
+
feedback: () => import("./feedback-C5NKCOXQ.mjs").then((m) => m.feedbackCommand),
|
|
84
84
|
db: () => import("./db-CbqgjfYz.mjs").then((m) => m.dbCommand),
|
|
85
85
|
usage: () => import("./usage-Cqqvnzrf.mjs").then((m) => m.usageCommand),
|
|
86
86
|
ai: () => import("./ai-CjvrcBIz.mjs").then((m) => m.aiCommand),
|
|
87
87
|
open: () => import("./open-DXo60Ikf.mjs").then((m) => m.openCommand),
|
|
88
88
|
browse: () => import("./browse-CBelCgnW.mjs").then((m) => m.browseCommand),
|
|
89
|
-
docs: () => import("./docs-
|
|
90
|
-
skill: () => import("./skill-
|
|
91
|
-
upgrade: () => import("./upgrade-
|
|
89
|
+
docs: () => import("./docs-T1nuu00A.mjs").then((m) => m.docsCommand),
|
|
90
|
+
skill: () => import("./skill-3wOwhzDx.mjs").then((m) => m.skillCommand),
|
|
91
|
+
upgrade: () => import("./upgrade-BJ2dWjCG.mjs").then((m) => m.upgradeCommand),
|
|
92
92
|
logs: () => import("./logs-BltUi4Gp.mjs").then((m) => m.logsCommand),
|
|
93
|
-
mcp: () => import("./mcp-
|
|
94
|
-
repo: () => import("./repo-
|
|
93
|
+
mcp: () => import("./mcp-BwLRcfqz.mjs").then((m) => m.mcpCommand),
|
|
94
|
+
repo: () => import("./repo-DlYX_1tN.mjs").then((m) => m.repoCommand),
|
|
95
|
+
template: () => import("./project-template-DdT0dqpn.mjs").then((m) => m.templateCommand)
|
|
95
96
|
}
|
|
96
97
|
});
|
|
97
98
|
const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve" || process.argv[2] === "repo" && process.argv[3] === "credential";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CdDH35SE.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-BPEYm9ts.mjs";
|
|
4
4
|
import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
+
import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CHfKV6lG.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
/** Plain words for the API's template errors. */
|
|
7
|
+
const TEMPLATE_FIXES = {
|
|
8
|
+
template_not_found: "No template by that name on this site. `canopy projects --templates` lists the templates you can start from.",
|
|
9
|
+
template_empty: "The template's repository has no code yet. Ask its owner to push it first.",
|
|
10
|
+
template_too_large: `The template's files are larger than the 32 MiB a copy can hold. Ask its owner to remove large files from the repository.`,
|
|
11
|
+
repo_required: "A template needs a Canopy git repository. Create one with `canopy repo create`, push the code, then try again.",
|
|
12
|
+
repo_empty: "Push the project's code to its repository first, then try again.",
|
|
13
|
+
owner_only: "Only the project owner can make it a template, or stop it being one.",
|
|
14
|
+
repos_not_configured: "Templates need Canopy git repositories, which are not available on this site yet."
|
|
15
|
+
};
|
|
16
|
+
/** What the new project gets, one line each — shown before creating. */
|
|
17
|
+
function describeCopy(copy) {
|
|
18
|
+
const lines = ["The code in its repository, as one fresh commit (no history)."];
|
|
19
|
+
if (copy.egress_mode === "open") lines.push("Outbound access: open (the backend may call any host).");
|
|
20
|
+
else if (copy.egress_allowlist.length) lines.push(`Outbound hosts allowed: ${copy.egress_allowlist.join(", ")}.`);
|
|
21
|
+
const loosened = [
|
|
22
|
+
!copy.csp_enabled && "Content-Security-Policy off",
|
|
23
|
+
copy.allow_framing && "other sites may frame the app",
|
|
24
|
+
copy.browser_db_access !== "full" && `browser database access: ${copy.browser_db_access}`,
|
|
25
|
+
!copy.feedback_widget && "feedback widget hidden"
|
|
26
|
+
].filter(Boolean);
|
|
27
|
+
if (loosened.length) lines.push(`Page settings: ${loosened.join("; ")}.`);
|
|
28
|
+
if (copy.google_bundles.length) lines.push(`Google access it asks visitors for: ${copy.google_bundles.join(", ")}.`);
|
|
29
|
+
if (copy.schedules.length) lines.push(`Schedules, turned off: ${copy.schedules.map((s) => `${s.name} (${s.cron})`).join(", ")}.`);
|
|
30
|
+
if (copy.secrets.length) lines.push(`Secret names to fill in (no values): ${copy.secrets.join(", ")}.`);
|
|
31
|
+
lines.push("Not copied: its data, files, members, password, budgets or history.");
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
/** What is left to do once the project exists. */
|
|
35
|
+
function nextSteps(slug, copy) {
|
|
36
|
+
const steps = [`cd ${slug}`];
|
|
37
|
+
for (const name of copy.secrets) steps.push(`canopy secrets set ${name}`);
|
|
38
|
+
steps.push("canopy deploy");
|
|
39
|
+
for (const s of copy.schedules) steps.push(`canopy schedule add ${s.name} "${s.cron}" (to turn it on)`);
|
|
40
|
+
if (copy.google_restricted.length) steps.push(`Ask a Canopy admin to unlock restricted Google access (${copy.google_restricted.join(", ")}), then turn it on in the dashboard.`);
|
|
41
|
+
return steps;
|
|
42
|
+
}
|
|
43
|
+
function fetchTemplate(slug) {
|
|
44
|
+
return withContext(apiRequest("GET", `/projects/${slug}/template`), {
|
|
45
|
+
action: `read template ${slug}`,
|
|
46
|
+
fixes: TEMPLATE_FIXES
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const templateCommand = defineCommand({
|
|
50
|
+
meta: {
|
|
51
|
+
name: "template",
|
|
52
|
+
description: "Offer a project as a template (mark, unmark, show)"
|
|
53
|
+
},
|
|
54
|
+
subCommands: {
|
|
55
|
+
mark: defineCommand({
|
|
56
|
+
meta: {
|
|
57
|
+
name: "mark",
|
|
58
|
+
description: "Let anyone on this site start a project from this one"
|
|
59
|
+
},
|
|
60
|
+
args: {
|
|
61
|
+
...projectDirArgs,
|
|
62
|
+
yes: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Skip the confirmation",
|
|
65
|
+
default: false
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
run: reportErrors("template mark", async ({ args }) => {
|
|
69
|
+
const { slug } = resolveProject(args);
|
|
70
|
+
if (!args.yes) {
|
|
71
|
+
requireTty("pass --yes to confirm.");
|
|
72
|
+
p.log.info(`Everyone on this site will be able to start a project of their own with a copy of ${slug}'s current code in its repository, plus its setup (outbound hosts, page settings, schedule and secret names). Its data, secret values and members are never copied.`);
|
|
73
|
+
if (!unwrap(await p.confirm({
|
|
74
|
+
message: `Make ${slug} a template?`,
|
|
75
|
+
initialValue: false
|
|
76
|
+
}))) {
|
|
77
|
+
p.cancel("Not marked.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if ((await withContext(apiRequest("PUT", `/projects/${slug}/template`, { enabled: true }), {
|
|
82
|
+
action: `make ${slug} a template`,
|
|
83
|
+
fixes: TEMPLATE_FIXES
|
|
84
|
+
})).warnings.includes("no_migrations")) p.log.warn("canopy.json sets no \"migrations\", so new projects start with an empty database: none of its tables come along. Add migrations to the repository to fix that.");
|
|
85
|
+
p.log.success(`${slug} is a template. Others start from it with \`canopy create <name> --from ${slug}\`.`);
|
|
86
|
+
})
|
|
87
|
+
}),
|
|
88
|
+
unmark: defineCommand({
|
|
89
|
+
meta: {
|
|
90
|
+
name: "unmark",
|
|
91
|
+
description: "Stop new projects starting from this one"
|
|
92
|
+
},
|
|
93
|
+
args: { ...projectDirArgs },
|
|
94
|
+
run: reportErrors("template unmark", async ({ args }) => {
|
|
95
|
+
const { slug } = resolveProject(args);
|
|
96
|
+
await withContext(apiRequest("PUT", `/projects/${slug}/template`, { enabled: false }), {
|
|
97
|
+
action: `stop ${slug} being a template`,
|
|
98
|
+
fixes: TEMPLATE_FIXES
|
|
99
|
+
});
|
|
100
|
+
p.log.success(`${slug} is no longer a template. Projects already started from it are not affected.`);
|
|
101
|
+
})
|
|
102
|
+
}),
|
|
103
|
+
show: defineCommand({
|
|
104
|
+
meta: {
|
|
105
|
+
name: "show",
|
|
106
|
+
description: "Show what a new project gets from a template"
|
|
107
|
+
},
|
|
108
|
+
args: { slug: {
|
|
109
|
+
type: "positional",
|
|
110
|
+
description: "Template slug",
|
|
111
|
+
required: true
|
|
112
|
+
} },
|
|
113
|
+
run: reportErrors("template show", async ({ args }) => {
|
|
114
|
+
const preview = await fetchTemplate(args.slug);
|
|
115
|
+
const t = preview.template;
|
|
116
|
+
console.log(`${t.name} (${t.slug}) — by ${t.owner.name} <${t.owner.email}>`);
|
|
117
|
+
if (t.description) console.log(t.description);
|
|
118
|
+
console.log("");
|
|
119
|
+
for (const line of describeCopy(preview.copies)) console.log(`- ${line}`);
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
//#endregion
|
|
125
|
+
export { templateCommand as a, nextSteps as i, describeCopy as n, fetchTemplate as r, TEMPLATE_FIXES as t };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { i as apiRequest } from "./api-BPEYm9ts.mjs";
|
|
2
|
+
import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
|
|
3
3
|
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CHfKV6lG.mjs";
|
|
4
|
-
import { r as
|
|
4
|
+
import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-BEr_tgIY.mjs";
|
|
5
|
+
import { i as offerRepoOnCreate, n as cloneProjectRepo } from "./repo-CE22HpqZ.mjs";
|
|
5
6
|
import { defineCommand } from "citty";
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { join, relative, resolve, sep } from "node:path";
|
|
@@ -111,6 +112,10 @@ const createCommand = defineCommand({
|
|
|
111
112
|
type: "boolean",
|
|
112
113
|
description: "Also create a git repository for the project on Canopy",
|
|
113
114
|
negativeDescription: "Don't create a git repository (no question asked)"
|
|
115
|
+
},
|
|
116
|
+
from: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "Start from a template project: a copy of its code and setup (`canopy projects --templates`)"
|
|
114
119
|
}
|
|
115
120
|
},
|
|
116
121
|
run: reportErrors("create", async ({ args }) => {
|
|
@@ -134,6 +139,10 @@ const createCommand = defineCommand({
|
|
|
134
139
|
}
|
|
135
140
|
})).trim();
|
|
136
141
|
slug ||= slugify(name);
|
|
142
|
+
if (args.from) {
|
|
143
|
+
await createFromTemplate(name, slug, args.from);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
137
146
|
const scaffold = shouldScaffoldCreate(args.scaffold);
|
|
138
147
|
let template = TEMPLATES.find((t) => t.id === (args.template ?? "static"));
|
|
139
148
|
if (!template) throw new Error(`Unknown template "${args.template}" — available: ${TEMPLATES.map((t) => t.id).join(", ")}.`);
|
|
@@ -188,6 +197,45 @@ const createCommand = defineCommand({
|
|
|
188
197
|
}
|
|
189
198
|
})
|
|
190
199
|
});
|
|
200
|
+
/**
|
|
201
|
+
* `canopy create --from` (project-templates): show what the new project
|
|
202
|
+
* gets, create it with a copy of the template, clone it, list what's left.
|
|
203
|
+
*/
|
|
204
|
+
async function createFromTemplate(name, slug, from) {
|
|
205
|
+
const targetDir = resolve(slug);
|
|
206
|
+
if (existsSync(targetDir)) throw new Error(`Directory ./${slug} already exists — remove it, or pick another slug.`);
|
|
207
|
+
const preview = await fetchTemplate(from);
|
|
208
|
+
p.note(describeCopy(preview.copies).join("\n"), `From ${preview.template.name} (by ${preview.template.owner.name}), ${slug} gets`);
|
|
209
|
+
if (process.stdin.isTTY) {
|
|
210
|
+
if (!unwrap(await p.confirm({
|
|
211
|
+
message: `Create ${slug} from ${from}?`,
|
|
212
|
+
initialValue: true
|
|
213
|
+
}))) {
|
|
214
|
+
p.cancel("Nothing created.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const spin = p.spinner();
|
|
219
|
+
spin.start(`Creating ${slug} from ${from}…`);
|
|
220
|
+
let project;
|
|
221
|
+
try {
|
|
222
|
+
({project} = await withContext(apiRequest("POST", "/projects", {
|
|
223
|
+
slug,
|
|
224
|
+
name,
|
|
225
|
+
from
|
|
226
|
+
}), {
|
|
227
|
+
action: `create ${slug} from ${from}`,
|
|
228
|
+
fixes: TEMPLATE_FIXES
|
|
229
|
+
}));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
spin.error("Create failed.");
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
spin.stop(`Created ${project.name} — ${projectUrl(project.slug)}`);
|
|
235
|
+
await cloneProjectRepo(slug, targetDir);
|
|
236
|
+
p.note(nextSteps(slug, preview.copies).join("\n"), `Cloned into ./${slug}/ — next`);
|
|
237
|
+
p.outro(`Live URL (after first deploy): ${projectUrl(project.slug)}`);
|
|
238
|
+
}
|
|
191
239
|
/** Empty enough to scaffold into: missing, or holding only dotfiles (.git, .agents…). */
|
|
192
240
|
function isEmptyish(dir) {
|
|
193
241
|
if (!existsSync(dir)) return true;
|
|
@@ -294,13 +342,31 @@ const projectsCommand = defineCommand({
|
|
|
294
342
|
name: "projects",
|
|
295
343
|
description: "List your projects"
|
|
296
344
|
},
|
|
297
|
-
args: {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
args: {
|
|
346
|
+
all: {
|
|
347
|
+
type: "boolean",
|
|
348
|
+
default: false,
|
|
349
|
+
description: "Include projects you can't edit"
|
|
350
|
+
},
|
|
351
|
+
templates: {
|
|
352
|
+
type: "boolean",
|
|
353
|
+
default: false,
|
|
354
|
+
description: "List the templates anyone can start a project from"
|
|
355
|
+
}
|
|
356
|
+
},
|
|
302
357
|
run: reportErrors("projects", async ({ args }) => {
|
|
303
358
|
const { projects, is_admin: isAdmin } = await apiRequest("GET", "/projects");
|
|
359
|
+
if (args.templates) {
|
|
360
|
+
const templates = projects.filter((project) => project.is_template);
|
|
361
|
+
if (templates.length === 0) {
|
|
362
|
+
console.log("No templates on this site yet.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const width = Math.max(...templates.map((project) => project.slug.length));
|
|
366
|
+
for (const project of templates) console.log(`${project.slug.padEnd(width)} ${project.name} (${project.owner_email})`);
|
|
367
|
+
console.log(`\nStart from one with \`canopy create <name> --from <template>\`.`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
304
370
|
const mine = args.all || isAdmin ? projects : projects.filter((project) => project.role);
|
|
305
371
|
if (mine.length === 0) {
|
|
306
372
|
console.log(projects.length === 0 ? "No projects yet — run `canopy create`." : `No projects of your own yet — run \`canopy create\`, or --all to list all ${projects.length}.`);
|
|
@@ -268,20 +268,23 @@ const cloneCommand = defineCommand({
|
|
|
268
268
|
}
|
|
269
269
|
},
|
|
270
270
|
run: reportErrors("repo clone", async ({ args }) => {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
await requireGit();
|
|
274
|
-
const target = resolve(args.target ?? slug);
|
|
275
|
-
const code = await gitInteractive([
|
|
276
|
-
"clone",
|
|
277
|
-
...credentialConfig(repo.remote, currentProfile(), slug).flatMap(([key, values]) => values.flatMap((value) => ["-c", `${key}=${value}`])),
|
|
278
|
-
repo.remote,
|
|
279
|
-
target
|
|
280
|
-
]);
|
|
281
|
-
if (code !== 0) throw new Error(`git clone failed (exit ${code}).`);
|
|
282
|
-
p.log.success(`Cloned ${slug} into ${target}.`);
|
|
271
|
+
const target = await cloneProjectRepo(args.slug, resolve(args.target ?? args.slug));
|
|
272
|
+
p.log.success(`Cloned ${args.slug} into ${target}.`);
|
|
283
273
|
})
|
|
284
274
|
});
|
|
275
|
+
/** Clone a project's Canopy repository into `target`, credential helper included. */
|
|
276
|
+
async function cloneProjectRepo(slug, target) {
|
|
277
|
+
const repo = requireCanopyRepo(slug, await fetchRepo(slug));
|
|
278
|
+
await requireGit();
|
|
279
|
+
const code = await gitInteractive([
|
|
280
|
+
"clone",
|
|
281
|
+
...credentialConfig(repo.remote, currentProfile(), slug).flatMap(([key, values]) => values.flatMap((value) => ["-c", `${key}=${value}`])),
|
|
282
|
+
repo.remote,
|
|
283
|
+
target
|
|
284
|
+
]);
|
|
285
|
+
if (code !== 0) throw new Error(`git clone failed (exit ${code}).`);
|
|
286
|
+
return target;
|
|
287
|
+
}
|
|
285
288
|
const deleteCommand = defineCommand({
|
|
286
289
|
meta: {
|
|
287
290
|
name: "delete",
|
|
@@ -420,4 +423,4 @@ async function offerRepoOnCreate(slug, dir, flag) {
|
|
|
420
423
|
}
|
|
421
424
|
}
|
|
422
425
|
//#endregion
|
|
423
|
-
export { repoCommand as i,
|
|
426
|
+
export { repoCommand as a, offerRepoOnCreate as i, cloneProjectRepo as n, connectFolder as r, cliInvocation as t };
|
|
@@ -52,13 +52,14 @@ const secretsCommand = defineCommand({
|
|
|
52
52
|
args: { ...projectDirArgs },
|
|
53
53
|
run: reportErrors("secrets list", async ({ args }) => {
|
|
54
54
|
const { slug } = resolveProject(args);
|
|
55
|
-
const { secrets } = await apiRequest("GET", `/projects/${slug}/secrets`);
|
|
56
|
-
if (secrets.length === 0) {
|
|
55
|
+
const { secrets, missing = [] } = await apiRequest("GET", `/projects/${slug}/secrets`);
|
|
56
|
+
if (secrets.length === 0 && missing.length === 0) {
|
|
57
57
|
console.log(`No secrets set for ${slug} — add one with \`canopy secrets set NAME\`.`);
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
|
-
const width = Math.max(...secrets.map((s) => s.key.length));
|
|
60
|
+
const width = Math.max(...secrets.map((s) => s.key.length), ...missing.map((m) => m.length));
|
|
61
61
|
for (const s of secrets) console.log(`${s.key.padEnd(width)} updated ${formatDateTime(s.updated_at)}`);
|
|
62
|
+
for (const name of missing) console.log(`${name.padEnd(width)} missing — set it with \`canopy secrets set ${name}\``);
|
|
62
63
|
})
|
|
63
64
|
}),
|
|
64
65
|
set: defineCommand({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CdDH35SE.mjs";
|
|
2
2
|
import { r as reportErrors } from "./ui-CHfKV6lG.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
|
|
|
112
112
|
const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
|
|
113
113
|
const command = pmCommand(pm);
|
|
114
114
|
const cmd = installArgs(pm, spec);
|
|
115
|
-
const { skillInstalled } = await import("./skill-
|
|
115
|
+
const { skillInstalled } = await import("./skill-3wOwhzDx.mjs");
|
|
116
116
|
const { homedir } = await import("node:os");
|
|
117
117
|
const refreshGlobal = skillInstalled(homedir());
|
|
118
118
|
const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
|
package/package.json
CHANGED
package/dist/repo-DvrIsubH.mjs
DELETED