@mediamonks/canopy-cli 0.7.45 → 0.7.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{ai-FBHTSCLZ.mjs → ai-CWPQiUft.mjs} +2 -2
  2. package/dist/{api-Bl6SG8Oq.mjs → api-DmJrIsE7.mjs} +134 -19
  3. package/dist/audience-B68YZBGy.mjs +121 -0
  4. package/dist/{browse-B8-0dweq.mjs → browse-BPLWhoFI.mjs} +2 -2
  5. package/dist/{db-BJJGydgL.mjs → db-DpCMc0uz.mjs} +4 -4
  6. package/dist/{deploy-ExrIeX_m.mjs → deploy-BnRQusjJ.mjs} +51 -5
  7. package/dist/{dev-D-vwE8G-.mjs → dev-BmKeIdMi.mjs} +7 -7
  8. package/dist/dev-BvVlq9np.mjs +2 -0
  9. package/dist/docs/ci.md +112 -50
  10. package/dist/docs/cli-audience.md +47 -0
  11. package/dist/docs/cli-create.md +2 -0
  12. package/dist/docs/cli-deploy.md +1 -1
  13. package/dist/docs/cli-repo.md +34 -2
  14. package/dist/docs/cli-token.md +8 -6
  15. package/dist/docs/cli.md +6 -2
  16. package/dist/docs/deploy.md +3 -2
  17. package/dist/docs/index.json +8 -3
  18. package/dist/docs/platform-deploys-and-rollbacks.md +4 -4
  19. package/dist/docs/platform-members-and-access.md +20 -0
  20. package/dist/{docs-Cpky2XNP.mjs → docs-D5gmZRJk.mjs} +4 -4
  21. package/dist/{egress-C2amvqmE.mjs → egress-BM4EpLBh.mjs} +3 -3
  22. package/dist/{feedback-CTV6inRg.mjs → feedback-C6kiYRro.mjs} +5 -5
  23. package/dist/index.mjs +36 -35
  24. package/dist/{local-BThs382Y.mjs → local-C9Bi9f3y.mjs} +2 -2
  25. package/dist/{login-CREcZfLH.mjs → login-2MW9sGJO.mjs} +3 -3
  26. package/dist/{logs-BnrNZvLc.mjs → logs-dZMPTJLu.mjs} +3 -3
  27. package/dist/{mcp-Cy8EwhhD.mjs → mcp-DuCxfb85.mjs} +3 -3
  28. package/dist/{migrations-D8NqjC5I.mjs → migrations-BReMlGWP.mjs} +2 -2
  29. package/dist/{open-Bpz4C2Sr.mjs → open-DcQSdfx5.mjs} +2 -2
  30. package/dist/{package-Dph4v1Cq.mjs → package-BksVeJZB.mjs} +1 -1
  31. package/dist/{project-template-Cdr1uwwE.mjs → project-template-BPW_Dgxj.mjs} +3 -3
  32. package/dist/project-template-Dw11151p.mjs +2 -0
  33. package/dist/{projects-BRG2HnBs.mjs → projects-CnU3194_.mjs} +26 -8
  34. package/dist/{rename-BGKa25Rq.mjs → rename-BWvERJgA.mjs} +3 -3
  35. package/dist/{repo-CwT2RDoW.mjs → repo-6a7gRQaf.mjs} +171 -8
  36. package/dist/repo-C7AXDtFZ.mjs +2 -0
  37. package/dist/{roles-C7pq9bIx.mjs → roles-CrYf_8wk.mjs} +1 -1
  38. package/dist/{roles-DAPPXf4U.mjs → roles-S2D9Id0W.mjs} +3 -3
  39. package/dist/{schedule-BcqBspn3.mjs → schedule-DvHSSzfR.mjs} +3 -3
  40. package/dist/{secrets-BjbIi7Gp.mjs → secrets-BIn2RPoe.mjs} +3 -3
  41. package/dist/{site-0i7wAJqg.mjs → site-BL5Ik-yj.mjs} +50 -1
  42. package/dist/{skill-Zj19RNo2.mjs → skill-Bz39NhTN.mjs} +2 -2
  43. package/dist/{staging-_2Js0Fkc.mjs → staging-Dn8kH9cK.mjs} +3 -3
  44. package/dist/templates/spa/.github/workflows/canopy-deploy.yml +18 -12
  45. package/dist/templates/spa/README.md +1 -1
  46. package/dist/templates/worker/.github/workflows/canopy-deploy.yml +18 -12
  47. package/dist/templates/worker/README.md +1 -1
  48. package/dist/{token-7vSNuz-X.mjs → token-DMCj88A-.mjs} +6 -6
  49. package/dist/{ui-CLnICOne.mjs → ui-26ETk1n8.mjs} +1 -1
  50. package/dist/{upgrade-B9tCAbre.mjs → upgrade-BjDaghNf.mjs} +2 -2
  51. package/dist/{usage-DGGmTwjJ.mjs → usage-Bpz5B1N1.mjs} +3 -3
  52. package/package.json +1 -1
  53. package/dist/dev-CSimnUP3.mjs +0 -2
  54. package/dist/project-template-715H_lnu.mjs +0 -2
  55. package/dist/repo-DmFs6H9m.mjs +0 -2
@@ -1,6 +1,6 @@
1
- import { _ as setActiveProfile, b as getBakedProfile, g as readConfigFile, h as listBakedProfiles, m as clearAllProfileTokens, r as siteConfig, v as upsertProfileAuth, x as isProfileId, y as writeConfigFile } from "./site-0i7wAJqg.mjs";
2
- import { n as agentHeaders } from "./api-Bl6SG8Oq.mjs";
3
- import { n as openBrowser, r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { C as isProfileId, S as getBakedProfile, _ as listBakedProfiles, b as upsertProfileAuth, g as clearAllProfileTokens, r as siteConfig, v as readConfigFile, x as writeConfigFile, y as setActiveProfile } from "./site-BL5Ik-yj.mjs";
2
+ import { n as agentHeaders } from "./api-DmJrIsE7.mjs";
3
+ import { n as openBrowser, r as reportErrors } from "./ui-26ETk1n8.mjs";
4
4
  import { n as clearRepoTokenCache } from "./repo-credential-CNDRM-tG.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { hostname } from "node:os";
@@ -1,6 +1,6 @@
1
- import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { d as resolveProject, l as projectFlagArgs } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { r as reportErrors } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/logs.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { t as version } from "./package-Dph4v1Cq.mjs";
2
- import { c as projectUrl, d as stagingUrl, p as tryResolveProject, r as siteConfig } from "./site-0i7wAJqg.mjs";
3
- import { a as apiRequest, d as noticeRenamesOnly, l as currentSlug, t as ApiError } from "./api-Bl6SG8Oq.mjs";
1
+ import { t as version } from "./package-BksVeJZB.mjs";
2
+ import { h as tryResolveProject, p as stagingUrl, r as siteConfig, u as projectUrl } from "./site-BL5Ik-yj.mjs";
3
+ import { a as apiRequest, f as noticeRenamesOnly, l as currentSlug, t as ApiError } from "./api-DmJrIsE7.mjs";
4
4
  import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -1,5 +1,5 @@
1
- import { f as tryReadCanopyJson } from "./site-0i7wAJqg.mjs";
2
- import { r as apiDataRequest } from "./api-Bl6SG8Oq.mjs";
1
+ import { m as tryReadCanopyJson } from "./site-BL5Ik-yj.mjs";
2
+ import { r as apiDataRequest } from "./api-DmJrIsE7.mjs";
3
3
  import { readFileSync, readdirSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  //#region src/migrations.ts
@@ -1,5 +1,5 @@
1
- import { c as projectUrl, d as stagingUrl, l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
2
- import { n as openBrowser, r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { c as projectDirArgs, d as resolveProject, p as stagingUrl, u as projectUrl } from "./site-BL5Ik-yj.mjs";
2
+ import { n as openBrowser, r as reportErrors } from "./ui-26ETk1n8.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/open.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.45";
2
+ var version = "0.7.47";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,6 +1,6 @@
1
- import { l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { c as projectDirArgs, d as resolveProject } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  /** Plain words for the API's template errors. */
@@ -0,0 +1,2 @@
1
+ import { a as templateCommand } from "./project-template-BPW_Dgxj.mjs";
2
+ export { templateCommand };
@@ -1,8 +1,8 @@
1
- import { c as projectUrl, f as tryReadCanopyJson, r as siteConfig } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CLnICOne.mjs";
4
- import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-Cdr1uwwE.mjs";
5
- import { i as offerRepoOnCreate, n as cloneProjectRepo } from "./repo-CwT2RDoW.mjs";
1
+ import { m as tryReadCanopyJson, r as siteConfig, u as projectUrl } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-26ETk1n8.mjs";
4
+ import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-BPW_Dgxj.mjs";
5
+ import { a as offerRepoOnCreate, i as githubOrigin, n as cloneProjectRepo } from "./repo-6a7gRQaf.mjs";
6
6
  import { defineCommand } from "citty";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
8
8
  import { join, relative, resolve, sep } from "node:path";
@@ -84,6 +84,17 @@ function slugify(input) {
84
84
  function shouldScaffoldCreate(requested, cwd = process.cwd()) {
85
85
  return requested ?? !existsSync(join(cwd, "canopy.json"));
86
86
  }
87
+ /**
88
+ * The "deploy from GitHub" next step (OpenSpec platform-oauth-oidc). Never a
89
+ * link: linking turns CI deploys on, which is the owner's call. With a GitHub
90
+ * `origin` the exact command; without one the shape of it. A folder that
91
+ * pushes to a Canopy repository needs no link, so no step.
92
+ */
93
+ async function ciLinkStep(dir, canopyRemote) {
94
+ if (canopyRemote) return [];
95
+ const origin = await githubOrigin(dir);
96
+ return origin ? [`canopy repo link ${origin.host}/${origin.repository} (deploy on every push to main)`] : ["canopy repo link <owner/name> (once it's on GitHub: deploy on every push to main)"];
97
+ }
87
98
  const createCommand = defineCommand({
88
99
  meta: {
89
100
  name: "create",
@@ -186,13 +197,15 @@ const createCommand = defineCommand({
186
197
  `cd ${slug}`,
187
198
  ...template.needsInstall ? ["npm install"] : [],
188
199
  ...remote ? [`git add -A && git commit -m "Initial commit" && git push -u ${remote} main`] : [],
189
- "canopy dev (or deploy right away: canopy deploy)"
200
+ "canopy dev (or deploy right away: canopy deploy)",
201
+ ...await ciLinkStep(targetDir, remote)
190
202
  ];
191
203
  p.note(steps.join("\n"), `Scaffolded ./${slug}/ (${template.id}) — next`);
192
204
  p.outro(`Live URL (after first deploy): ${projectUrl(project.slug)}`);
193
205
  } else {
194
206
  const remote = await offerRepoOnCreate(slug, process.cwd(), args.repo);
195
207
  if (remote) p.log.info(`This folder pushes to the project's git repository as "${remote}".`);
208
+ for (const step of await ciLinkStep(process.cwd(), remote)) p.log.info(`Next: ${step}`);
196
209
  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.");
197
210
  }
198
211
  })
@@ -319,7 +332,8 @@ const initCommand = defineCommand({
319
332
  const steps = [
320
333
  ...rel === "." ? [] : [`cd ${rel}`],
321
334
  ...template.needsInstall ? ["npm install"] : [],
322
- "canopy dev (or deploy right away: canopy deploy)"
335
+ "canopy dev (or deploy right away: canopy deploy)",
336
+ ...await ciLinkStep(targetDir, null)
323
337
  ];
324
338
  p.note(steps.join("\n"), `Scaffolded ${rel}/ (${template.id}) for ${slug} — next`);
325
339
  p.outro(`Live URL (after first deploy): ${projectUrl(slug)}`);
@@ -333,7 +347,11 @@ const initCommand = defineCommand({
333
347
  return;
334
348
  }
335
349
  writeCanopyJson(targetDir, { slug });
336
- p.note("Add \"dist\"/\"build\" to canopy.json if this app has a build step, then:\ncanopy deploy", `Linked ${rel} to ${slug}`);
350
+ p.note([
351
+ "Add \"dist\"/\"build\" to canopy.json if this app has a build step, then:",
352
+ "canopy deploy",
353
+ ...await ciLinkStep(targetDir, null)
354
+ ].join("\n"), `Linked ${rel} to ${slug}`);
337
355
  p.outro(`Live URL (after first deploy): ${projectUrl(slug)}`);
338
356
  })
339
357
  });
@@ -1,6 +1,6 @@
1
- import { c as projectUrl, l as resolveProject, o as projectDirArgs, u as rewriteCanopyJsonSlug } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { c as projectDirArgs, d as resolveProject, f as rewriteCanopyJsonSlug, u as projectUrl } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/rename.ts
@@ -1,6 +1,6 @@
1
- import { f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, r as siteConfig } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, d as noticeRenamesOnly, f as withContext, n as agentHeaders, u as followRename } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
1
+ import { a as parseGithubRemote, c as projectDirArgs, d as resolveProject, l as projectFlagArgs, m as tryReadCanopyJson, o as parseRepositoryArg, r as siteConfig } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, f as noticeRenamesOnly, m as withContext, n as agentHeaders, u as followRename } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-26ETk1n8.mjs";
4
4
  import { a as parseCredentialInput, i as helperCommand, o as runCredentialHelper, r as fileTokenCache, t as RepoCredentialError } from "./repo-credential-CNDRM-tG.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { existsSync, realpathSync } from "node:fs";
@@ -104,6 +104,13 @@ async function setLocalConfig(dir, key, values) {
104
104
  * - `show` says which repository the project has (`--json` for agents);
105
105
  * - `delete` removes it and its history (owner only).
106
106
  *
107
+ * CI deploys from GitHub Actions (OpenSpec platform-oauth-oidc), owner only:
108
+ *
109
+ * - `link` links a GitHub repository outside Canopy (a Canopy repository needs none);
110
+ * - `unlink` removes that link;
111
+ * - `branches` sets the live and staging branches CI deploys from;
112
+ * - `deploys` turns CI deploys on or off without unlinking.
113
+ *
107
114
  * Setting up a folder points a remote at the repository and registers
108
115
  * `canopy repo credential` as git's credential helper for that one
109
116
  * repository. No token is ever written
@@ -208,16 +215,26 @@ const showCommand = defineCommand({
208
215
  return;
209
216
  }
210
217
  if (repo.kind === "none") console.log(`${slug} has no git repository. Create one with \`canopy repo create\`.`);
211
- else if (repo.kind === "linked") console.log(`${slug} is linked to ${repo.linked_repository}.`);
212
- else {
218
+ else if (repo.kind === "linked") {
219
+ console.log(`${slug} is linked to ${repo.host ? `${repo.host}/` : ""}${repo.linked_repository}.`);
220
+ printCiDeploys(repo);
221
+ } else {
213
222
  console.log(`${slug} has a Canopy git repository.`);
214
223
  console.log(` remote ${repo.remote}`);
215
224
  console.log(` created ${repo.created_at ? formatDateTime(repo.created_at) : "—"}`);
216
225
  console.log(` last push ${repo.last_push_at ? formatDateTime(repo.last_push_at) : "never"}`);
226
+ printCiDeploys(repo);
217
227
  console.log(`\nClone it with \`canopy repo clone ${slug}\`.`);
218
228
  }
219
229
  })
220
230
  });
231
+ /** The CI lines of `repo show`: both branches and whether CI deploys are on. */
232
+ function printCiDeploys(repo) {
233
+ if (repo.deploys_enabled === void 0 || repo.deploys_enabled === null) return;
234
+ console.log(` live ${repo.ref ?? "refs/heads/main"}`);
235
+ console.log(` staging ${repo.staging_ref ?? "—"}`);
236
+ console.log(` CI deploys ${repo.deploys_enabled ? "on" : "off"}`);
237
+ }
221
238
  const createCommand = defineCommand({
222
239
  meta: {
223
240
  name: "create",
@@ -380,13 +397,159 @@ async function readStdin() {
380
397
  for await (const chunk of process.stdin) chunks.push(chunk);
381
398
  return Buffer.concat(chunks).toString("utf8");
382
399
  }
400
+ /** Plain words for the link routes' errors; `invalid_host` here is a GitHub host. */
401
+ const LINK_FIXES = {
402
+ owner_only: "Only the project owner (or a platform admin) can change where CI deploys come from.",
403
+ repo_exists: "This project has a Canopy repository, and CI deploys from it without a link. Set its branches with `canopy repo branches`.",
404
+ invalid_host: "Link a repository on github.com or this site's GitHub Enterprise host (e.g. monks.ghe.com/monks/my-tool).",
405
+ repository_not_linked: "Link a repository first with `canopy repo link <owner/name>`.",
406
+ not_linked: "This project has no linked GitHub repository — `canopy repo show` says what it has."
407
+ };
408
+ /** `main` → `refs/heads/main`; a full ref passes through. */
409
+ function toRef(value) {
410
+ const trimmed = value.trim();
411
+ return trimmed.startsWith("refs/") ? trimmed : `refs/heads/${trimmed}`;
412
+ }
413
+ /** `refs/heads/develop` → `develop`, for the workflow's `on.push.branches`. */
414
+ const branchName = (ref) => ref.replace(/^refs\/heads\//, "");
415
+ /** The GitHub repository the folder's `origin` remote points at, if any. */
416
+ async function githubOrigin(dir) {
417
+ if (!await isWorkingCopy(dir).catch(() => false)) return null;
418
+ const origin = (await remotes(dir).catch(() => /* @__PURE__ */ new Map())).get("origin");
419
+ return origin ? parseGithubRemote(origin) : null;
420
+ }
421
+ function printLink(slug, link) {
422
+ console.log(` repository ${link.host}/${link.repository}`);
423
+ console.log(` live ${link.ref}`);
424
+ console.log(` staging ${link.staging_ref ?? "—"}`);
425
+ console.log(` CI deploys ${link.deploys_enabled ? "on" : "off"}`);
426
+ if (link.staging_ref) p.log.info(`Pushes to ${branchName(link.staging_ref)} deploy ${slug}'s staging channel once the workflow runs on them: add "${branchName(link.staging_ref)}" to on.push.branches in .github/workflows/canopy-deploy.yml.`);
427
+ }
383
428
  const repoCommand = defineCommand({
384
429
  meta: {
385
430
  name: "repo",
386
- description: "The project's git repository (clone, connect, create)"
431
+ description: "The project's git repository (clone, connect, create, link CI deploys)"
387
432
  },
388
433
  subCommands: {
389
434
  show: showCommand,
435
+ link: defineCommand({
436
+ meta: {
437
+ name: "link",
438
+ description: "Link a GitHub repository so its pushes deploy the project (owner only)"
439
+ },
440
+ args: {
441
+ repository: {
442
+ type: "positional",
443
+ description: "[<host>/]<owner/name> — defaults to this folder's origin remote",
444
+ required: false
445
+ },
446
+ ...projectFlagArgs,
447
+ ref: {
448
+ type: "string",
449
+ description: "Live branch (default main)"
450
+ },
451
+ "staging-ref": {
452
+ type: "string",
453
+ description: "Staging branch: its pushes deploy to the staging channel only"
454
+ }
455
+ },
456
+ run: reportErrors("repo link", async ({ args }) => {
457
+ const { dir, slug } = resolveProject({ project: args.project });
458
+ let target;
459
+ if (args.repository) {
460
+ target = parseRepositoryArg(args.repository);
461
+ if (!target) throw new Error(`"${args.repository}" is not a repository — pass <owner/name> or <host>/<owner/name>, e.g. monks/my-tool.`);
462
+ } else {
463
+ target = await githubOrigin(dir);
464
+ if (!target) throw new Error("This folder has no GitHub origin remote to link — pass the repository: `canopy repo link <owner/name>`.");
465
+ }
466
+ const link = await withContext(apiRequest("PUT", `/projects/${slug}/github`, {
467
+ repository: target.repository,
468
+ ...target.host && { host: target.host },
469
+ ...args.ref && { ref: toRef(args.ref) },
470
+ ...args["staging-ref"] && { staging_ref: toRef(args["staging-ref"]) }
471
+ }), {
472
+ action: `link ${slug} to ${target.repository}`,
473
+ fixes: LINK_FIXES
474
+ });
475
+ p.log.success(`Linked ${slug} to ${link.host}/${link.repository} @ ${link.ref}. A push there now deploys ${slug}.`);
476
+ printLink(slug, link);
477
+ })
478
+ }),
479
+ unlink: defineCommand({
480
+ meta: {
481
+ name: "unlink",
482
+ description: "Remove the project's GitHub repository link (owner only)"
483
+ },
484
+ args: { ...projectDirArgs },
485
+ run: reportErrors("repo unlink", async ({ args }) => {
486
+ const { slug } = resolveProject(args);
487
+ await withContext(apiRequest("DELETE", `/projects/${slug}/github`), {
488
+ action: `unlink ${slug}'s repository`,
489
+ fixes: LINK_FIXES
490
+ });
491
+ p.log.success(`Unlinked ${slug}. GitHub Actions runs now skip the deploy.`);
492
+ })
493
+ }),
494
+ branches: defineCommand({
495
+ meta: {
496
+ name: "branches",
497
+ description: "Set the live and staging branches CI deploys from (owner only)"
498
+ },
499
+ args: {
500
+ ...projectDirArgs,
501
+ ref: {
502
+ type: "string",
503
+ description: "Live branch, e.g. main"
504
+ },
505
+ "staging-ref": {
506
+ type: "string",
507
+ description: "Staging branch, e.g. develop (`none` removes it)"
508
+ }
509
+ },
510
+ run: reportErrors("repo branches", async ({ args }) => {
511
+ const { slug } = resolveProject(args);
512
+ const stagingArg = args["staging-ref"];
513
+ if (!args.ref && stagingArg === void 0) throw new Error("Pass --ref and/or --staging-ref (see `canopy repo show` for the current ones).");
514
+ const link = await withContext(apiRequest("PATCH", `/projects/${slug}/github`, {
515
+ ...args.ref && { ref: toRef(args.ref) },
516
+ ...stagingArg !== void 0 && { staging_ref: stagingArg === "none" || stagingArg === "" ? null : toRef(stagingArg) }
517
+ }), {
518
+ action: `set ${slug}'s CI branches`,
519
+ fixes: LINK_FIXES
520
+ });
521
+ p.log.success(`CI deploys for ${slug} come from:`);
522
+ printLink(slug, link);
523
+ })
524
+ }),
525
+ deploys: defineCommand({
526
+ meta: {
527
+ name: "deploys",
528
+ description: "Turn CI deploys from GitHub Actions on or off (owner only)"
529
+ },
530
+ args: {
531
+ state: {
532
+ type: "positional",
533
+ description: "on | off",
534
+ required: true
535
+ },
536
+ ...projectFlagArgs
537
+ },
538
+ run: reportErrors("repo deploys", async ({ args }) => {
539
+ if (args.state !== "on" && args.state !== "off") throw new Error(`Pass on or off (got "${args.state}").`);
540
+ const { slug } = resolveProject({ project: args.project });
541
+ const enabled = args.state === "on";
542
+ await withContext(apiRequest("PATCH", `/projects/${slug}/github`, { deploys_enabled: enabled }), {
543
+ action: `turn CI deploys ${args.state} for ${slug}`,
544
+ fixes: LINK_FIXES
545
+ });
546
+ if (enabled) p.log.success(`CI deploys are on for ${slug}: pushes to its branches deploy again.`);
547
+ else {
548
+ p.log.success(`CI deploys are off for ${slug}: GitHub Actions runs skip the deploy with a warning. The repository link stays.`);
549
+ p.log.info("Deploy tokens are not affected — a workflow with CANOPY_DEPLOY_TOKEN still deploys. Revoke them with `canopy token revoke` to stop those too.");
550
+ }
551
+ })
552
+ }),
390
553
  clone: cloneCommand,
391
554
  connect: connectCommand,
392
555
  create: createCommand,
@@ -411,7 +574,7 @@ async function offerRepoOnCreate(slug, dir, flag) {
411
574
  if (flag === void 0) {
412
575
  if (!process.stdin.isTTY || !await reposAvailable()) return null;
413
576
  const origin = await isWorkingCopy(dir).catch(() => false) ? (await remotes(dir)).get("origin") : void 0;
414
- if (origin && /github\.com[:/]/.test(origin)) return null;
577
+ if (origin && parseGithubRemote(origin)) return null;
415
578
  if (!unwrap(await p.confirm({
416
579
  message: "Create a git repository for this project on Canopy?",
417
580
  initialValue: false
@@ -428,4 +591,4 @@ async function offerRepoOnCreate(slug, dir, flag) {
428
591
  }
429
592
  }
430
593
  //#endregion
431
- export { repoCommand as a, offerRepoOnCreate as i, cloneProjectRepo as n, connectFolder as r, cliInvocation as t };
594
+ export { offerRepoOnCreate as a, githubOrigin as i, cloneProjectRepo as n, repoCommand as o, connectFolder as r, toRef as s, cliInvocation as t };
@@ -0,0 +1,2 @@
1
+ import { o as repoCommand } from "./repo-6a7gRQaf.mjs";
2
+ export { repoCommand };
@@ -1,2 +1,2 @@
1
- import { i as membersCommand, o as rolesCommand } from "./roles-DAPPXf4U.mjs";
1
+ import { i as membersCommand, o as rolesCommand } from "./roles-S2D9Id0W.mjs";
2
2
  export { membersCommand, rolesCommand };
@@ -1,6 +1,6 @@
1
- import { l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { c as projectDirArgs, d as resolveProject } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/roles.ts
@@ -1,6 +1,6 @@
1
- import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
1
+ import { d as resolveProject, l as projectFlagArgs } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { r as reportErrors, t as formatDateTime } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/schedule.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
1
+ import { c as projectDirArgs, d as resolveProject } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/secrets.ts
@@ -259,6 +259,55 @@ function tryResolveProject(args) {
259
259
  slug: args.project ?? tryReadCanopyJson(dir)?.slug
260
260
  };
261
261
  }
262
+ const GITHUB_HOST = /^(?:github\.com|[a-z0-9][a-z0-9-]*\.ghe\.com)$/;
263
+ /**
264
+ * The GitHub repository a git remote URL points at, or null when it is not on
265
+ * github.com or a `*.ghe.com` host (OpenSpec platform-oauth-oidc). Whether the
266
+ * host is one this site trusts is the API's call, not ours.
267
+ *
268
+ * git@monks.ghe.com:monks/my-tool.git
269
+ * ssh://git@github.com/monks/my-tool.git
270
+ * https://github.com/monks/my-tool(.git)
271
+ */
272
+ function parseGithubRemote(url) {
273
+ const trimmed = url.trim();
274
+ const scp = /^[\w.-]+@([^:/]+):([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(trimmed);
275
+ let host;
276
+ let parts;
277
+ if (scp) {
278
+ host = scp[1];
279
+ parts = [scp[2], scp[3]];
280
+ } else try {
281
+ const parsed = new URL(trimmed);
282
+ if (![
283
+ "https:",
284
+ "http:",
285
+ "ssh:",
286
+ "git:"
287
+ ].includes(parsed.protocol)) return null;
288
+ host = parsed.hostname;
289
+ parts = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "").split("/");
290
+ } catch {
291
+ return null;
292
+ }
293
+ host = host?.toLowerCase();
294
+ if (!host || !GITHUB_HOST.test(host) || parts?.length !== 2 || !parts[0] || !parts[1]) return null;
295
+ return {
296
+ host,
297
+ repository: `${parts[0]}/${parts[1]}`
298
+ };
299
+ }
300
+ /** `[<host>/]owner/name` as typed on the command line; null when it is neither. */
301
+ function parseRepositoryArg(value) {
302
+ const parts = value.trim().replace(/\.git$/, "").split("/");
303
+ if (parts.some((part) => part === "")) return null;
304
+ if (parts.length === 2) return { repository: `${parts[0]}/${parts[1]}` };
305
+ if (parts.length === 3 && parts[0].includes(".")) return {
306
+ host: parts[0].toLowerCase(),
307
+ repository: `${parts[1]}/${parts[2]}`
308
+ };
309
+ return null;
310
+ }
262
311
  //#endregion
263
312
  //#region src/site.ts
264
313
  /**
@@ -300,4 +349,4 @@ function siteHeaderLine(cliVersion) {
300
349
  return `canopy ${cliVersion} · ${c.envOverride || !c.profileId ? c.apiUrl : `${c.profileId} (${c.projectDomain ?? new URL(c.apiUrl).hostname})`}`;
301
350
  }
302
351
  //#endregion
303
- export { setActiveProfile as _, projectDialect as a, getBakedProfile as b, projectUrl as c, stagingUrl as d, tryReadCanopyJson as f, readConfigFile as g, listBakedProfiles as h, siteHeaderLine as i, resolveProject as l, clearAllProfileTokens as m, setInvocationProfile as n, projectDirArgs as o, tryResolveProject as p, siteConfig as r, projectFlagArgs as s, projectDir as t, rewriteCanopyJsonSlug as u, upsertProfileAuth as v, isProfileId as x, writeConfigFile as y };
352
+ export { isProfileId as C, getBakedProfile as S, listBakedProfiles as _, parseGithubRemote as a, upsertProfileAuth as b, projectDirArgs as c, resolveProject as d, rewriteCanopyJsonSlug as f, clearAllProfileTokens as g, tryResolveProject as h, siteHeaderLine as i, projectFlagArgs as l, tryReadCanopyJson as m, setInvocationProfile as n, parseRepositoryArg as o, stagingUrl as p, siteConfig as r, projectDialect as s, projectDir as t, projectUrl as u, readConfigFile as v, writeConfigFile as x, setActiveProfile as y };
@@ -1,5 +1,5 @@
1
- import { t as version } from "./package-Dph4v1Cq.mjs";
2
- import { r as reportErrors } from "./ui-CLnICOne.mjs";
1
+ import { t as version } from "./package-BksVeJZB.mjs";
2
+ import { r as reportErrors } from "./ui-26ETk1n8.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
@@ -1,6 +1,6 @@
1
- import { c as projectUrl, d as stagingUrl, l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
1
+ import { d as resolveProject, l as projectFlagArgs, p as stagingUrl, u as projectUrl } from "./site-BL5Ik-yj.mjs";
2
+ import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-26ETk1n8.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/staging.ts
@@ -1,34 +1,40 @@
1
1
  # Deploy to Canopy on every push to main. GitHub's runners are the build
2
2
  # farm — the platform owns no build infrastructure.
3
3
  #
4
- # Needs a CANOPY_DEPLOY_TOKEN repo secret: create a deploy token in the
5
- # dashboard (project → Settings → Deploy tokens) or with `canopy token
6
- # create`, then add it via `gh secret set CANOPY_DEPLOY_TOKEN` or the repo's
7
- # Settings → Secrets. Until the secret is set, this workflow skips itself
8
- # instead of failing the build.
4
+ # No secret needed: the run proves which repository and branch it comes from
5
+ # with GitHub's own identity token (the id-token permission below), and
6
+ # Canopy deploys it once the project owner has linked this repository:
7
+ #
8
+ # canopy repo link (in this folder: links the origin remote)
9
+ #
10
+ # Until then, every run skips the deploy with a warning instead of failing.
11
+ # The owner can pause CI deploys without editing this file:
12
+ #
13
+ # canopy repo deploys off (and `on` to resume)
14
+ #
15
+ # A staging branch (`canopy repo branches --staging-ref develop`) deploys to
16
+ # the project's staging channel only. Add it to `branches` below, e.g.
17
+ # [main, develop], so pushes to it run this workflow.
9
18
  name: Deploy to Canopy
10
19
 
11
20
  on:
12
21
  push:
13
22
  branches: [main]
14
23
 
24
+ permissions:
25
+ id-token: write
26
+ contents: read
27
+
15
28
  jobs:
16
29
  deploy:
17
30
  runs-on: ubuntu-latest
18
- env:
19
- CANOPY_DEPLOY_TOKEN: ${{ secrets.CANOPY_DEPLOY_TOKEN }}
20
31
  steps:
21
32
  - uses: actions/checkout@v7
22
33
  - uses: actions/setup-node@v7
23
34
  with:
24
35
  node-version: 24
25
36
  - name: Install dependencies
26
- if: env.CANOPY_DEPLOY_TOKEN != ''
27
37
  run: |
28
38
  if [ -f package-lock.json ]; then npm ci; else npm install; fi
29
39
  - name: Deploy
30
- if: env.CANOPY_DEPLOY_TOKEN != ''
31
40
  run: npm exec --yes @mediamonks/canopy-cli deploy
32
- - name: Skipped (no deploy token)
33
- if: env.CANOPY_DEPLOY_TOKEN == ''
34
- run: echo "CANOPY_DEPLOY_TOKEN secret not set — skipping deploy."
@@ -11,4 +11,4 @@ A [Canopy](https://dashboard.thanarie.dev) project — Vite + React. Live
11
11
  - `AGENTS.md` — project guardrails and skill setup for coding agents
12
12
 
13
13
  Push-to-main deploys are prepared in `.github/workflows/canopy-deploy.yml`
14
- (needs a `CANOPY_DEPLOY_TOKEN` repo secret — see the workflow file).
14
+ (no secret: link the repository once with `canopy repo link` — see the workflow file).
@@ -1,34 +1,40 @@
1
1
  # Deploy to Canopy on every push to main. GitHub's runners are the build
2
2
  # farm — the platform owns no build infrastructure.
3
3
  #
4
- # Needs a CANOPY_DEPLOY_TOKEN repo secret: create a deploy token in the
5
- # dashboard (project → Settings → Deploy tokens) or with `canopy token
6
- # create`, then add it via `gh secret set CANOPY_DEPLOY_TOKEN` or the repo's
7
- # Settings → Secrets. Until the secret is set, this workflow skips itself
8
- # instead of failing the build.
4
+ # No secret needed: the run proves which repository and branch it comes from
5
+ # with GitHub's own identity token (the id-token permission below), and
6
+ # Canopy deploys it once the project owner has linked this repository:
7
+ #
8
+ # canopy repo link (in this folder: links the origin remote)
9
+ #
10
+ # Until then, every run skips the deploy with a warning instead of failing.
11
+ # The owner can pause CI deploys without editing this file:
12
+ #
13
+ # canopy repo deploys off (and `on` to resume)
14
+ #
15
+ # A staging branch (`canopy repo branches --staging-ref develop`) deploys to
16
+ # the project's staging channel only. Add it to `branches` below, e.g.
17
+ # [main, develop], so pushes to it run this workflow.
9
18
  name: Deploy to Canopy
10
19
 
11
20
  on:
12
21
  push:
13
22
  branches: [main]
14
23
 
24
+ permissions:
25
+ id-token: write
26
+ contents: read
27
+
15
28
  jobs:
16
29
  deploy:
17
30
  runs-on: ubuntu-latest
18
- env:
19
- CANOPY_DEPLOY_TOKEN: ${{ secrets.CANOPY_DEPLOY_TOKEN }}
20
31
  steps:
21
32
  - uses: actions/checkout@v7
22
33
  - uses: actions/setup-node@v7
23
34
  with:
24
35
  node-version: 24
25
36
  - name: Install dependencies
26
- if: env.CANOPY_DEPLOY_TOKEN != ''
27
37
  run: |
28
38
  if [ -f package-lock.json ]; then npm ci; else npm install; fi
29
39
  - name: Deploy
30
- if: env.CANOPY_DEPLOY_TOKEN != ''
31
40
  run: npm exec --yes @mediamonks/canopy-cli deploy
32
- - name: Skipped (no deploy token)
33
- if: env.CANOPY_DEPLOY_TOKEN == ''
34
- run: echo "CANOPY_DEPLOY_TOKEN secret not set — skipping deploy."
@@ -14,4 +14,4 @@ https://{{slug}}.thanarie.dev after the first `canopy deploy`.
14
14
  - `AGENTS.md` — project guardrails and skill setup for coding agents
15
15
 
16
16
  Push-to-main deploys are prepared in `.github/workflows/canopy-deploy.yml`
17
- (needs a `CANOPY_DEPLOY_TOKEN` repo secret — see the workflow file).
17
+ (no secret: link the repository once with `canopy repo link` — see the workflow file).