@mediamonks/canopy-cli 0.7.15 → 0.7.16

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.
@@ -10,12 +10,14 @@ description: Read the Canopy docs from the terminal — live from the platform,
10
10
  ```bash
11
11
  canopy docs # list all topics
12
12
  canopy docs <topic> # print one topic
13
+ canopy docs search <terms…> # keyword search across all docs
13
14
  canopy docs <topic> --refresh
14
15
  ```
15
16
 
16
17
  ## Arguments & flags
17
18
 
18
19
  - `[topic]` — positional: the topic slug to print. Omit it to list every topic with a one-line summary, grouped by section. Guides keep bare slugs (`deploy`, `staging`); other sections prefix theirs (`sdk-database`, `cli-deploy`, `platform-schedules`).
20
+ - `search <terms…>` — search every doc for the given terms instead of printing one. Case-insensitive; a doc matches only when every term appears somewhere in its title, summary, or body. Results come back best-first (title and summary hits outrank body mentions), each with the matching lines, capped at ten.
19
21
  - `--refresh` — re-fetch from the platform, bypassing the local cache. Default `false`.
20
22
 
21
23
  ## How it works
@@ -30,6 +32,8 @@ That last step is why `canopy docs` works with no network and no login: a fresh
30
32
 
31
33
  Asking for an unknown topic errors with the list of available slugs. Everything prints as plain Markdown to stdout, so it pipes cleanly into a pager or an agent's context.
32
34
 
35
+ Search runs entirely on the local corpus — the cache plus the bundled snapshot — so it's instant and works offline. It's a locator, not a reader: find the topic, then print it with `canopy docs <topic>` (which fetches fresh content when it can).
36
+
33
37
  ## Examples
34
38
 
35
39
  ```bash
@@ -38,6 +42,12 @@ canopy docs
38
42
 
39
43
  Lists every topic with its summary — the starting point when you're not sure which guide covers what you're doing.
40
44
 
45
+ ```bash
46
+ canopy docs search egress allowlist
47
+ ```
48
+
49
+ Finds the docs that mention both terms — useful when the summaries don't tell you which doc covers a detail, like a specific error code or environment variable.
50
+
41
51
  ```bash
42
52
  canopy docs deploy --refresh
43
53
  ```
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CF0dX7UA.mjs";
1
+ import { t as version } from "./package-BGDIsRxe.mjs";
2
2
  import { n as siteConfig } from "./site-BUlQR117.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
4
  import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
@@ -14,6 +14,10 @@ import { fileURLToPath } from "node:url";
14
14
  * never goes stale, with a local cache and a bundled snapshot as offline
15
15
  * fallbacks. The listing is grouped per docs-site section.
16
16
  *
17
+ * `canopy docs search <terms…>` greps the locally available corpus (cache +
18
+ * bundled snapshot) — the corpus is small enough (~60 docs) that keyword
19
+ * matching beats any index, and it works fully offline.
20
+ *
17
21
  * The platform requires a login for docs (internal platform), so fetches carry
18
22
  * the CLI token when there is one; without it we simply fall back to the
19
23
  * bundled snapshot, so `canopy docs` still works before sign-in.
@@ -140,6 +144,75 @@ function bodyFromFallback(slug) {
140
144
  }
141
145
  }
142
146
  }
147
+ const SEARCH_LIMIT = 10;
148
+ const LINE_LIMIT = 2;
149
+ const LINE_WIDTH = 120;
150
+ /** A matching body line, trimmed and windowed around the first term hit. */
151
+ function excerpt(line, matchIndex) {
152
+ const clean = line.replace(/\s+/g, " ").trim();
153
+ if (clean.length <= LINE_WIDTH) return clean;
154
+ const start = Math.max(0, Math.min(matchIndex - Math.floor(LINE_WIDTH / 3), clean.length - LINE_WIDTH));
155
+ const slice = clean.slice(start, start + LINE_WIDTH);
156
+ return `${start > 0 ? "…" : ""}${slice}${start + LINE_WIDTH < clean.length ? "…" : ""}`;
157
+ }
158
+ /**
159
+ * Keyword search over the docs corpus: case-insensitive, every term must match
160
+ * somewhere in a doc (title, summary, or body). Title and summary hits weigh
161
+ * more than body occurrences; results come back best-first.
162
+ */
163
+ function searchDocs(entries, query) {
164
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
165
+ if (terms.length === 0) return [];
166
+ const hits = [];
167
+ for (const entry of entries) {
168
+ const title = entry.title.toLowerCase();
169
+ const summary = entry.summary.toLowerCase();
170
+ const body = entry.body.toLowerCase();
171
+ let score = 0;
172
+ let matchesAll = true;
173
+ for (const term of terms) {
174
+ let termScore = 0;
175
+ if (title.includes(term)) termScore += 10;
176
+ if (summary.includes(term)) termScore += 5;
177
+ let count = 0;
178
+ for (let i = body.indexOf(term); i !== -1 && count < 20; i = body.indexOf(term, i + term.length)) count++;
179
+ termScore += count;
180
+ if (termScore === 0) {
181
+ matchesAll = false;
182
+ break;
183
+ }
184
+ score += termScore;
185
+ }
186
+ if (!matchesAll) continue;
187
+ const lines = [];
188
+ for (const line of entry.body.split("\n")) {
189
+ const lower = line.toLowerCase();
190
+ const at = terms.map((t) => lower.indexOf(t)).find((i) => i !== -1);
191
+ if (at !== void 0 && line.trim()) {
192
+ lines.push(excerpt(line, at));
193
+ if (lines.length >= LINE_LIMIT) break;
194
+ }
195
+ }
196
+ hits.push({
197
+ slug: entry.slug,
198
+ title: entry.title,
199
+ score,
200
+ lines
201
+ });
202
+ }
203
+ return hits.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
204
+ }
205
+ /**
206
+ * Every topic paired with its locally available body (cache first, bundled
207
+ * snapshot second). A topic with no local body still gets searched by title
208
+ * and summary — search never hits the network per doc.
209
+ */
210
+ function searchEntries(index) {
211
+ return index.topics.map((t) => ({
212
+ ...t,
213
+ body: bodyFromFallback(t.slug) ?? ""
214
+ }));
215
+ }
143
216
  async function listIndex(refresh) {
144
217
  const { apiUrl } = siteConfig();
145
218
  if (refresh || !cacheFresh()) {
@@ -174,7 +247,7 @@ const docsCommand = defineCommand({
174
247
  topic: {
175
248
  type: "positional",
176
249
  required: false,
177
- description: "Topic to print (omit to list)"
250
+ description: "Topic to print (omit to list), or `search <terms…>`"
178
251
  },
179
252
  refresh: {
180
253
  type: "boolean",
@@ -187,6 +260,23 @@ const docsCommand = defineCommand({
187
260
  const topic = args.topic;
188
261
  const latest = latestKnownVersion();
189
262
  if (latest && compareSemver(version, latest) < 0) console.error(`Note: these docs describe CLI ${latest}; you have ${version} — commands or flags they mention may not exist here. Run \`canopy upgrade\`.\n`);
263
+ if (topic === "search") {
264
+ const query = args._.slice(1).join(" ").trim();
265
+ if (!query) throw new Error("Usage: canopy docs search <terms…>");
266
+ const hits = searchDocs(searchEntries(await listIndex(refresh)), query);
267
+ if (hits.length === 0) {
268
+ console.log(`No docs match "${query}". Run \`canopy docs\` to list all topics.`);
269
+ return;
270
+ }
271
+ const shown = hits.slice(0, SEARCH_LIMIT);
272
+ console.log(`${hits.length} doc${hits.length === 1 ? "" : "s"} match "${query}" — run \`canopy docs <topic>\` to read one:\n`);
273
+ for (const hit of shown) {
274
+ console.log(`${hit.slug} — ${hit.title}`);
275
+ for (const line of hit.lines) console.log(` ${line}`);
276
+ }
277
+ if (hits.length > shown.length) console.log(`\n…and ${hits.length - shown.length} more — add terms to narrow it down.`);
278
+ return;
279
+ }
190
280
  if (!topic) {
191
281
  const index = await listIndex(refresh);
192
282
  if (index.topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CF0dX7UA.mjs";
1
+ import { t as version } from "./package-BGDIsRxe.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
3
3
  import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-B1Px5y7q.mjs";
4
4
  import { defineCommand } from "citty";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CF0dX7UA.mjs";
2
+ import { t as version } from "./package-BGDIsRxe.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -76,13 +76,13 @@ const main = defineCommand({
76
76
  token: () => import("./token-BUxgRJVA.mjs").then((m) => m.tokenCommand),
77
77
  egress: () => import("./egress-PJp0SZy2.mjs").then((m) => m.egressCommand),
78
78
  schedule: () => import("./schedule-Cq9K0rnE.mjs").then((m) => m.scheduleCommand),
79
- feedback: () => import("./feedback-h_j6DBBZ.mjs").then((m) => m.feedbackCommand),
79
+ feedback: () => import("./feedback-B0iX3iZ1.mjs").then((m) => m.feedbackCommand),
80
80
  db: () => import("./db-DKnJQPm5.mjs").then((m) => m.dbCommand),
81
81
  usage: () => import("./usage-DjNngyvy.mjs").then((m) => m.usageCommand),
82
82
  open: () => import("./open-C5x3tX6I.mjs").then((m) => m.openCommand),
83
- docs: () => import("./docs-BlzdYmou.mjs").then((m) => m.docsCommand),
84
- skill: () => import("./skill-DaaxgqEk.mjs").then((m) => m.skillCommand),
85
- upgrade: () => import("./upgrade-DG-7xwOs.mjs").then((m) => m.upgradeCommand),
83
+ docs: () => import("./docs-D7Ey1ZMQ.mjs").then((m) => m.docsCommand),
84
+ skill: () => import("./skill-Be_pqqdH.mjs").then((m) => m.skillCommand),
85
+ upgrade: () => import("./upgrade-ChwehW_q.mjs").then((m) => m.upgradeCommand),
86
86
  logs: () => import("./logs-C53y2eAS.mjs").then((m) => m.logsCommand)
87
87
  }
88
88
  });
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.15";
2
+ var version = "0.7.16";
3
3
  //#endregion
4
4
  export { version as t };
@@ -71,8 +71,9 @@ The CLI ships the full documentation and keeps it fresh from the platform. List
71
71
  the topics and read the relevant one _before_ running unfamiliar commands:
72
72
 
73
73
  ```bash
74
- canopy docs # list all topics
75
- canopy docs deploy # print one topic
74
+ canopy docs # list all topics
75
+ canopy docs deploy # print one topic
76
+ canopy docs search egress allowlist # find the topics that mention all terms
76
77
  ```
77
78
 
78
79
  The listing is grouped by section: guides (`getting-started`, `deploy`, `dev`,
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CF0dX7UA.mjs";
1
+ import { t as version } from "./package-BGDIsRxe.mjs";
2
2
  import { n as reportErrors } from "./ui-B1Px5y7q.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-DaaxgqEk.mjs");
115
+ const { skillInstalled } = await import("./skill-Be_pqqdH.mjs");
116
116
  const refreshSkill = skillInstalled(process.cwd());
117
117
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
118
118
  await run(command, cmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.15",
3
+ "version": "0.7.16",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"