@mediamonks/canopy-cli 0.7.26 → 0.7.28

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.
@@ -24,12 +24,15 @@ canopy feedback attach <id> ./after-fix.png --comment "fixed — see the shot"
24
24
 
25
25
  ## Subcommands
26
26
 
27
- Inbox commands resolve the project the usual way — `canopy.json` in the target
28
- directory, or `--project <slug>` — and require project membership. The
29
- item-scoped ones (`show`, `comment`, `status`, `attach`) also work with just
30
- the id from anywhere: report ids are unique across projects, so with no
31
- project context the item is looked up by id alone. `list` and `show` take
32
- `--json` for machine-readable output.
27
+ Every report has a key made of its project and a number, like
28
+ `my-project-17`. Inbox commands resolve the project the usual way —
29
+ `canopy.json` in the target directory, or `--project <slug>` — and require
30
+ project membership; inside the project you can refer to a report by its
31
+ number alone (`canopy feedback show 17`). The item-scoped commands (`show`,
32
+ `comment`, `status`, `attach`) also take the full key from anywhere, with no
33
+ project context: the key names its project. Older ids from before keys
34
+ existed still work wherever a key does. `list` and `show` take `--json` for
35
+ machine-readable output.
33
36
 
34
37
  - `list [--status <s>] [--kind <k>]` — the project's inbox. Statuses:
35
38
  `investigate`, `investigated`, `accepted`, `in_progress`, `resolved`,
@@ -7,7 +7,7 @@ Everything on Canopy is driven by one command-line tool: `canopy`. It scaffolds
7
7
 
8
8
  ## Install
9
9
 
10
- The CLI is published on npm as `@mediamonks/canopy-cli`. You need Node.js 20 or newer; then install it globally:
10
+ The CLI is published on npm as `@mediamonks/canopy-cli`. You need Node.js 24 or newer; then install it globally:
11
11
 
12
12
  ```bash
13
13
  npm install -g @mediamonks/canopy-cli
@@ -24,7 +24,7 @@ Confirm it's on your PATH:
24
24
  canopy --version
25
25
  ```
26
26
 
27
- If `npm` isn't available, install Node.js 20+ first (https://nodejs.org), then
27
+ If `npm` isn't available, install Node.js 24+ first (https://nodejs.org), then
28
28
  re-run the install.
29
29
 
30
30
  ## 2. Sign in
@@ -112,6 +112,31 @@ Switching back is one command (`canopy egress mode allowlist`), and the
112
112
  dashboard offers to **seed the allowlist from the observed hosts** so
113
113
  tightening up later doesn't break the app.
114
114
 
115
+ ## Allowlisting Canopy on the other side
116
+
117
+ Some external services only accept calls from IP addresses you register with
118
+ them first — a partner API, a corporate database, a payment provider's
119
+ allowlist. Projects hosted on **Google Cloud** (the `gcp-pilot` site,
120
+ `canopy.monks.zone`) make every outbound request from a fixed pair of
121
+ addresses, so you can hand them over:
122
+
123
+ | Address |
124
+ | ---------------- |
125
+ | `34.187.107.103` |
126
+ | `34.158.168.43` |
127
+
128
+ Register **both**. Traffic may leave from either one, and the second gives the
129
+ platform room to grow without asking you to update your allowlist. These
130
+ addresses are shared by every project on that site, so registering them lets
131
+ any Canopy project on Google Cloud reach the service — the other service's own
132
+ authentication (API keys, tokens) still decides who gets in.
133
+
134
+ The addresses are a platform commitment: they will not change without notice
135
+ and a migration period. Projects hosted on Cloudflare have **no** fixed
136
+ outbound address (requests leave from Cloudflare's shared edge), so if a
137
+ service you depend on insists on IP allowlisting, run that project on the
138
+ Google Cloud site.
139
+
115
140
  ## The outbound traffic log
116
141
 
117
142
  In both modes, the platform keeps a per-project log of the hosts the worker
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CBso2611.mjs";
1
+ import { t as version } from "./package-BL6hRqtr.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-DR-siu2S.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CBso2611.mjs";
1
+ import { t as version } from "./package-BL6hRqtr.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 { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui-DR-siu2S.mjs";
4
4
  import { defineCommand } from "citty";
@@ -72,19 +72,38 @@ const jsonArg = { json: {
72
72
  type: "boolean",
73
73
  description: "Machine-readable JSON output"
74
74
  } };
75
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
76
+ const KEY_RE = /^([a-z][a-z0-9-]*)-(\d+)$/;
75
77
  /**
76
- * The project an item-scoped command targets: `--project` or canopy.json when
77
- * present, otherwise the API's ID-only lookup — item ids are globally unique,
78
- * so the item names its own project. That's the usual agent situation: holding
79
- * an id from a notification or a Slack thread, in a directory that isn't the
80
- * project.
78
+ * The project slug a full key names (`kitchen-sink-17` → `kitchen-sink`), or
79
+ * null for a UUID or a bare number. UUIDs are checked first: one whose last
80
+ * group is all digits would otherwise look like a key.
81
81
  */
82
- async function resolveItemSlug(args, itemId) {
82
+ function slugOfKey(ref) {
83
+ if (UUID_RE.test(ref)) return null;
84
+ return KEY_RE.exec(ref.toLowerCase())?.[1] ?? null;
85
+ }
86
+ /**
87
+ * The project an item-scoped command targets. A full key names its own
88
+ * project and wins over the directory — `canopy feedback show other-3` from
89
+ * inside `sink` means other's report, not a miss on sink's. Otherwise
90
+ * `--project` or canopy.json; and failing those, the API's project-less
91
+ * lookup for a UUID from an old link. A bare number is only meaningful with a
92
+ * project in hand.
93
+ */
94
+ async function resolveItemSlug(args, ref) {
95
+ const fromKey = slugOfKey(ref);
96
+ if (fromKey) return fromKey;
83
97
  const { slug } = tryResolveProject(args);
84
98
  if (slug) return slug;
85
- const { project } = await apiRequest("GET", `/feedback/${encodeURIComponent(itemId)}`);
99
+ if (/^\d+$/.test(ref)) throw new Error(`"${ref}" is a bare report number — run this inside the project, pass --project, or use the full key (e.g. my-project-${ref}).`);
100
+ const { project } = await apiRequest("GET", `/feedback/${encodeURIComponent(ref)}`);
86
101
  return project.slug;
87
102
  }
103
+ /** What a report is called in output: its key, or the UUID on pre-key rows. */
104
+ function refOf(item) {
105
+ return item.key ?? item.id;
106
+ }
88
107
  function firstLine(body) {
89
108
  const line = body.split("\n")[0] ?? "";
90
109
  return line.length > 72 ? `${line.slice(0, 71)}…` : line;
@@ -101,7 +120,7 @@ function printItems(items) {
101
120
  for (const item of items) {
102
121
  const comments = item.comment_count ? ` 💬${item.comment_count}` : "";
103
122
  const shots = item.attachment_count ? ` 📎${item.attachment_count}` : "";
104
- console.log(`${item.id} ${item.status} ${item.kind} ${itemLabel(item)}${comments}${shots}`);
123
+ console.log(`${refOf(item)} ${item.status} ${item.kind} ${itemLabel(item)}${comments}${shots}`);
105
124
  }
106
125
  }
107
126
  const listCommand = defineCommand({
@@ -139,7 +158,7 @@ const showCommand = defineCommand({
139
158
  args: {
140
159
  id: {
141
160
  type: "positional",
142
- description: "Item id (from `canopy feedback list`)",
161
+ description: "Report key like my-project-17 (from `canopy feedback list`), or its number inside the project",
143
162
  required: true
144
163
  },
145
164
  ...projectDirArgs,
@@ -150,7 +169,8 @@ const showCommand = defineCommand({
150
169
  }
151
170
  },
152
171
  run: reportErrors("feedback show", async ({ args }) => {
153
- const { slug } = tryResolveProject(args);
172
+ const slug = slugOfKey(args.id) ?? tryResolveProject(args).slug;
173
+ if (!slug && /^\d+$/.test(args.id)) throw new Error(`"${args.id}" is a bare report number — run this inside the project, pass --project, or use the full key (e.g. my-project-${args.id}).`);
154
174
  const detail = await apiRequest("GET", slug ? `/projects/${slug}/feedback/${encodeURIComponent(args.id)}` : `/feedback/${encodeURIComponent(args.id)}`);
155
175
  if (args.json) {
156
176
  console.log(JSON.stringify(detail, null, 2));
@@ -158,7 +178,7 @@ const showCommand = defineCommand({
158
178
  }
159
179
  const { item, comments } = detail;
160
180
  if (item.title) console.log(item.title);
161
- console.log(`${item.kind} · ${item.status} · via ${item.source} · ${formatDateTime(item.created_at)}`);
181
+ console.log(`${refOf(item)} · ${item.kind} · ${item.status} · via ${item.source} · ${formatDateTime(item.created_at)}`);
162
182
  console.log(`reported by ${item.reporter_name || item.reporter_email || "unknown"}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
163
183
  console.log(`\n${item.body}\n`);
164
184
  if (item.context) {
@@ -207,7 +227,7 @@ const commentCommand = defineCommand({
207
227
  args: {
208
228
  id: {
209
229
  type: "positional",
210
- description: "Item id",
230
+ description: "Report key (my-project-17), or its number inside the project",
211
231
  required: true
212
232
  },
213
233
  text: {
@@ -237,7 +257,7 @@ const statusCommand = defineCommand({
237
257
  args: {
238
258
  id: {
239
259
  type: "positional",
240
- description: "Item id",
260
+ description: "Report key (my-project-17), or its number inside the project",
241
261
  required: true
242
262
  },
243
263
  to: {
@@ -281,7 +301,7 @@ const mineCommand = defineCommand({
281
301
  console.log("You haven't filed any reports.");
282
302
  return;
283
303
  }
284
- for (const item of items) console.log(`${item.id} ${item.status} ${item.kind} [${item.project_slug}] ${itemLabel(item)}`);
304
+ for (const item of items) console.log(`${refOf(item)} ${item.status} ${item.kind} [${item.project_slug}] ${itemLabel(item)}`);
285
305
  })
286
306
  });
287
307
  const reportCommand = defineCommand({
@@ -348,8 +368,8 @@ const reportCommand = defineCommand({
348
368
  ...args.status ? { status: args.status } : {},
349
369
  context
350
370
  });
351
- console.log(`Filed ${kind} ${item.id} on ${item.project} as ${item.status}.`);
352
- if (paths.length > 0) await attachFiles(item.project, item.id, paths, args.caption);
371
+ console.log(`Filed ${kind} ${item.key} on ${item.project} as ${item.status}.`);
372
+ if (paths.length > 0) await attachFiles(item.project, item.key, paths, args.caption);
353
373
  console.log(`Follow it with \`canopy feedback mine\` or on the dashboard's Reports page.`);
354
374
  })
355
375
  });
@@ -370,7 +390,7 @@ const feedbackCommand = defineCommand({
370
390
  args: {
371
391
  id: {
372
392
  type: "positional",
373
- description: "Item id",
393
+ description: "Report key (my-project-17), or its number inside the project",
374
394
  required: true
375
395
  },
376
396
  file: {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CBso2611.mjs";
2
+ import { t as version } from "./package-BL6hRqtr.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";
@@ -77,13 +77,13 @@ const main = defineCommand({
77
77
  egress: () => import("./egress-Cn-P46O3.mjs").then((m) => m.egressCommand),
78
78
  secrets: () => import("./secrets-BD3DaTih.mjs").then((m) => m.secretsCommand),
79
79
  schedule: () => import("./schedule-Buu71RWQ.mjs").then((m) => m.scheduleCommand),
80
- feedback: () => import("./feedback-C9m3dNkJ.mjs").then((m) => m.feedbackCommand),
80
+ feedback: () => import("./feedback-OMAkwHpR.mjs").then((m) => m.feedbackCommand),
81
81
  db: () => import("./db-DVMwCLlU.mjs").then((m) => m.dbCommand),
82
82
  usage: () => import("./usage-81JSiWIx.mjs").then((m) => m.usageCommand),
83
83
  open: () => import("./open-rS1ARzOy.mjs").then((m) => m.openCommand),
84
- docs: () => import("./docs-x3hMtUzT.mjs").then((m) => m.docsCommand),
85
- skill: () => import("./skill-DmVlZXgv.mjs").then((m) => m.skillCommand),
86
- upgrade: () => import("./upgrade-C7u8pxeV.mjs").then((m) => m.upgradeCommand),
84
+ docs: () => import("./docs-DMFMPnxG.mjs").then((m) => m.docsCommand),
85
+ skill: () => import("./skill-C83Za5BU.mjs").then((m) => m.skillCommand),
86
+ upgrade: () => import("./upgrade-BjQguwG6.mjs").then((m) => m.upgradeCommand),
87
87
  logs: () => import("./logs-CfDINw7U.mjs").then((m) => m.logsCommand)
88
88
  }
89
89
  });
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.26";
2
+ var version = "0.7.28";
3
3
  //#endregion
4
4
  export { version as t };
@@ -30,7 +30,7 @@ canopy whoami # is there a valid login?
30
30
  npm install -g @mediamonks/canopy-cli
31
31
  ```
32
32
 
33
- (Needs Node.js 20+. If `npm` is missing, tell the user to install Node first.)
33
+ (Needs Node.js 24+. If `npm` is missing, tell the user to install Node first.)
34
34
 
35
35
  - **`canopy whoami` says not logged in / expired** → run:
36
36
  ```bash
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CBso2611.mjs";
1
+ import { t as version } from "./package-BL6hRqtr.mjs";
2
2
  import { r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -18,10 +18,10 @@ jobs:
18
18
  env:
19
19
  CANOPY_DEPLOY_TOKEN: ${{ secrets.CANOPY_DEPLOY_TOKEN }}
20
20
  steps:
21
- - uses: actions/checkout@v4
22
- - uses: actions/setup-node@v4
21
+ - uses: actions/checkout@v7
22
+ - uses: actions/setup-node@v7
23
23
  with:
24
- node-version: 22
24
+ node-version: 24
25
25
  - name: Install dependencies
26
26
  if: env.CANOPY_DEPLOY_TOKEN != ''
27
27
  run: |
@@ -18,10 +18,10 @@ jobs:
18
18
  env:
19
19
  CANOPY_DEPLOY_TOKEN: ${{ secrets.CANOPY_DEPLOY_TOKEN }}
20
20
  steps:
21
- - uses: actions/checkout@v4
22
- - uses: actions/setup-node@v4
21
+ - uses: actions/checkout@v7
22
+ - uses: actions/setup-node@v7
23
23
  with:
24
- node-version: 22
24
+ node-version: 24
25
25
  - name: Install dependencies
26
26
  if: env.CANOPY_DEPLOY_TOKEN != ''
27
27
  run: |
@@ -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-DmVlZXgv.mjs");
115
+ const { skillInstalled } = await import("./skill-C83Za5BU.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.26",
3
+ "version": "0.7.28",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"