@mediamonks/canopy-cli 0.7.33 → 0.7.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
2
+ import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
3
+ import { defineCommand } from "citty";
4
+ //#region src/browse.ts
5
+ /**
6
+ * canopy browse — a one-time sign-in link to a project for an agent's
7
+ * browser (agent-browse-sessions). Opening it within a minute gives that
8
+ * browser an hour on this one project and channel, as the signed-in Monk;
9
+ * nothing else on Canopy. The link is the only thing printed: no cookie, no
10
+ * token, nothing written to disk, and no browser opened (that is `canopy open`).
11
+ */
12
+ var BrowseError = class extends Error {
13
+ name = "BrowseError";
14
+ };
15
+ /** The control plane's refusal, in words that say what to do about it. */
16
+ function explainBrowseRefusal(code, slug, detail) {
17
+ switch (code) {
18
+ case "members_only": return `${slug} is limited to its collaborators — ask the owner to add you.`;
19
+ case "project_not_found": return `There is no project ${slug} on this site — check the slug and --profile.`;
20
+ case "project_paused": return `${slug} is paused.`;
21
+ case "project_moved": return `${slug} has moved${detail ? ` (${detail})` : ""}.`;
22
+ case "staging_not_enabled": return `${slug} has no staging channel yet — drop --staging, or create it with \`canopy deploy --staging\`.`;
23
+ case "invalid_path": return "--path must be a page on the project, starting with / (for example /settings).";
24
+ case "cli_session_required": return "Browse links need your own CLI sign-in, not a deploy token — run `canopy login`.";
25
+ case "not_configured": return "This site has no public project hostnames, so it can't hand out browse links.";
26
+ default: return detail ? `${code}: ${detail}` : code;
27
+ }
28
+ }
29
+ const browseCommand = defineCommand({
30
+ meta: {
31
+ name: "browse",
32
+ description: "Print a one-time sign-in link to a project for an agent's browser"
33
+ },
34
+ args: {
35
+ slug: {
36
+ type: "positional",
37
+ description: "Project slug (defaults to canopy.json in the current directory)",
38
+ required: false
39
+ },
40
+ staging: {
41
+ type: "boolean",
42
+ default: false,
43
+ description: "Link to the staging channel instead of live"
44
+ },
45
+ path: {
46
+ type: "string",
47
+ description: "Page to land on after signing in (default /)"
48
+ },
49
+ json: {
50
+ type: "boolean",
51
+ default: false,
52
+ description: "Print only { url, expires_at } as JSON"
53
+ }
54
+ },
55
+ async run({ args }) {
56
+ try {
57
+ const slug = tryResolveProject({ project: args.slug }).slug;
58
+ if (!slug) throw new BrowseError("No project named — pass a slug (`canopy browse <slug>`) or run from a project directory.");
59
+ if (process.env["CANOPY_DEPLOY_TOKEN"]) throw new BrowseError("Browse links use your own sign-in, not a deploy token — unset CANOPY_DEPLOY_TOKEN and run `canopy login`.");
60
+ if (!siteConfig().token) throw new BrowseError("Not signed in to this site — run `canopy login` first.");
61
+ let link;
62
+ try {
63
+ link = await apiRequest("POST", `/projects/${slug}/browse-ticket`, {
64
+ channel: args.staging ? "staging" : "live",
65
+ ...args.path !== void 0 && { path: args.path }
66
+ });
67
+ } catch (err) {
68
+ if (err instanceof ApiError && err.status !== 401) throw new BrowseError(explainBrowseRefusal(err.code, slug, err.detail));
69
+ throw err;
70
+ }
71
+ if (args.json) {
72
+ process.stdout.write(`${JSON.stringify({
73
+ url: link.url,
74
+ expires_at: link.expires_at
75
+ })}\n`);
76
+ return;
77
+ }
78
+ console.log(`\n ${link.url}\n`);
79
+ console.log(" Open it within a minute; it works once. The browser that opens it is signed in to");
80
+ console.log(` ${slug}${args.staging ? " (staging)" : ""} as you for an hour, and to nothing else on Canopy.\n`);
81
+ } catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ process.stderr.write(`canopy browse: ${message}\n`);
84
+ process.exitCode = 1;
85
+ }
86
+ }
87
+ });
88
+ //#endregion
89
+ export { browseCommand };
@@ -2,7 +2,7 @@ import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as sit
2
2
  import { i as apiRequest, o as withContext, t as ApiError } from "./api-C4rMhE7i.mjs";
3
3
  import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
4
4
  import { r as readMigrationFiles } from "./migrations-Czlcddyz.mjs";
5
- import { r as stampWorkerHeaders } from "./src-BkACMkol.mjs";
5
+ import { r as stampWorkerHeaders } from "./src-DE37PKho.mjs";
6
6
  import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { defineCommand } from "citty";
@@ -40,7 +40,7 @@ async function workerPlugin(flags, slug) {
40
40
  console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
41
41
  return;
42
42
  }
43
- const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-CaCsGBkn.mjs");
43
+ const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-JbLEXmVg.mjs");
44
44
  const secrets = readDevVars(flags.dir);
45
45
  const email = siteConfig().email;
46
46
  const plugin = workerDevPlugin({
@@ -225,7 +225,7 @@ async function devRemote(flags, slug) {
225
225
  async function devLocal(flags, slug) {
226
226
  let dump;
227
227
  if (flags.pull) {
228
- const { fetchProjectDump } = await import("./local-DAYCNUyz.mjs");
228
+ const { fetchProjectDump } = await import("./local-qR8CBseB.mjs");
229
229
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
230
230
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
231
231
  }
@@ -240,7 +240,7 @@ async function devLocal(flags, slug) {
240
240
  const reason = err instanceof ApiError ? err.explain().why : String(err);
241
241
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
242
242
  }
243
- const { createSandbox } = await import("./local-DAYCNUyz.mjs");
243
+ const { createSandbox } = await import("./local-qR8CBseB.mjs");
244
244
  const sandbox = await createSandbox({
245
245
  slug,
246
246
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -0,0 +1,2 @@
1
+ import { n as devCommand } from "./dev-CGSIY1-y.mjs";
2
+ export { devCommand };
@@ -1,4 +1,4 @@
1
- import { n as redactCanopyCredentials, r as stampWorkerHeaders, t as CALLER_HEADER } from "./src-BkACMkol.mjs";
1
+ import { n as redactCanopyCredentials, r as stampWorkerHeaders, t as CALLER_HEADER } from "./src-DE37PKho.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { relative, resolve, sep } from "node:path";
4
4
  //#region src/dev-worker.ts
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Let your agent browse a project
3
+ description: Give an AI agent's browser a short sign-in to one project, so it can look at and click through your app as you.
4
+ ---
5
+
6
+ Coding agents often need to see the app they're working on: check that a page renders, click through a flow, or read what a screen shows. Every Canopy project sits behind the Monks sign-in, which an agent can't get through on its own.
7
+
8
+ `canopy browse` gives the agent a way in that doesn't involve handing over your own browser session:
9
+
10
+ ```bash
11
+ canopy browse my-app --json
12
+ ```
13
+
14
+ It prints a one-time link. Whatever browser opens it is signed in to **my-app**, as you, for **an hour**, and to nothing else on Canopy.
15
+
16
+ ## What the agent gets
17
+
18
+ - **One project, one channel.** The live site, or staging with `--staging`. The browser is not signed in to your other projects, or to the dashboard.
19
+ - **You.** The app sees you as the visitor, exactly as when you open it yourself. Your backend's `env.USER` is you, and data you can see, the agent can see. On a collaborators-only project you have to be a collaborator.
20
+ - **An hour.** After that the agent meets the sign-in page and can run `canopy browse` again.
21
+ - **Marked as an agent.** Your backend can tell the request came from an agent: `mcpCaller(request, env).agent` from `@mediamonks/canopy/mcp` is `true`.
22
+
23
+ The link itself works **once**, within **a minute**. Opening it a second time shows a page saying to run `canopy browse` again.
24
+
25
+ ## Using it from different tools
26
+
27
+ **Playwright, or a browser tool in your agent:** open the URL. The browser is redirected to the page and stays signed in.
28
+
29
+ ```ts
30
+ await page.goto(url); // lands on the project, signed in
31
+ ```
32
+
33
+ **curl:** keep the cookie in a jar and follow the redirect.
34
+
35
+ ```bash
36
+ curl -c jar.txt -L "$(canopy browse my-app --json | jq -r .url)"
37
+ curl -b jar.txt https://my-app.canopy.website/api/notes
38
+ ```
39
+
40
+ **A specific page:** `canopy browse my-app --path /settings` lands there directly.
41
+
42
+ ## Stopping it
43
+
44
+ The agent's session ends when the hour is up. It also ends early if you:
45
+
46
+ - run `canopy logout`,
47
+ - revoke that CLI sign-in on your Account page in the dashboard, or
48
+ - sign out everywhere.
49
+
50
+ If you lose access to a collaborators-only project, the agent loses it too.
51
+
52
+ ## Good to know
53
+
54
+ - Your project's backend needs a redeploy after this feature shipped before an agent can call its `/api/*` routes. Until then those calls answer "redeploy required", while pages still load.
55
+ - The feedback widget doesn't appear for agents.
56
+ - Never give an agent your own browser session cookie. It opens every project and the dashboard, as you.
57
+
58
+ See also [canopy browse](https://docs.canopy.website/cli/browse), and [Agent tools](https://docs.canopy.website/guides/agent-tools) for letting agents call your backend directly.
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: canopy browse
3
+ description: Print a one-time sign-in link that lets an agent's browser open a project as you.
4
+ ---
5
+
6
+ `canopy browse` prints a link for an AI agent's browser: Playwright, a browser tool in Claude or Cursor, or plain `curl`. Opening the link signs that browser in to **one project**, as you, for **one hour**. It reaches nothing else on Canopy: no other projects, and not the dashboard.
7
+
8
+ Read [Let your agent browse a project](https://docs.canopy.website/guides/agent-browsing) for the full picture.
9
+
10
+ ## Usage
11
+
12
+ ```bash
13
+ canopy browse [slug] [--staging] [--path <page>] [--json]
14
+ ```
15
+
16
+ ## Arguments & flags
17
+
18
+ - `[slug]` — the project. Defaults to the `canopy.json` in the current directory.
19
+ - `--staging` — link to the project's [staging channel](https://docs.canopy.website/guides/staging) instead of the live site.
20
+ - `--path <page>` — the page to land on after signing in, such as `/settings`. Defaults to `/`.
21
+ - `--json` — print only `{ "url", "expires_at" }`, for an agent to read.
22
+
23
+ The site comes from the active profile, as for every command (`--profile staging` links to the project on the staging site).
24
+
25
+ ## How it works
26
+
27
+ The command uses your `canopy login` sign-in to ask Canopy for a link to the project, under the same rules as opening it yourself. Collaborators-only projects only give links to collaborators.
28
+
29
+ - The link **works once** and **expires after a minute**. Run the command again for a new one.
30
+ - The browser that opens it is signed in to that project and channel for an hour. After that it sees the sign-in page again.
31
+ - Running `canopy logout`, revoking the CLI sign-in on your Account page, or signing out everywhere also signs the agent out.
32
+ - The command never opens a browser itself (that's [canopy open](https://docs.canopy.website/cli/open)), never prints a cookie, and saves nothing.
33
+
34
+ ## Examples
35
+
36
+ ```bash
37
+ canopy browse my-app --json
38
+ ```
39
+
40
+ Prints `{"url":"https://my-app.…/__canopy/agent-login?ticket=…","expires_at":"…"}`. Hand the URL to your agent's browser.
41
+
42
+ ```bash
43
+ canopy browse --staging --path /admin
44
+ ```
45
+
46
+ A link to the staging channel of the project in this folder, landing on `/admin`.
47
+
48
+ See also [canopy mcp](https://docs.canopy.website/cli/mcp), which lets agents call your project's backend as tools instead of using its pages.
@@ -77,6 +77,11 @@
77
77
  "title": "Agent tools (MCP)",
78
78
  "summary": "Let AI agents like Claude Code and Cursor use your project's backend as tools — as the person running them."
79
79
  },
80
+ {
81
+ "slug": "agent-browsing",
82
+ "title": "Let your agent browse a project",
83
+ "summary": "Give an AI agent's browser a short sign-in to one project, so it can look at and click through your app as you."
84
+ },
80
85
  {
81
86
  "slug": "porting-nextjs",
82
87
  "title": "Porting a Next.js app",
@@ -311,6 +316,11 @@
311
316
  "title": "canopy open",
312
317
  "summary": "Open the deployed project in your browser."
313
318
  },
319
+ {
320
+ "slug": "cli-browse",
321
+ "title": "canopy browse",
322
+ "summary": "Print a one-time sign-in link that lets an agent's browser open a project as you."
323
+ },
314
324
  {
315
325
  "slug": "cli-docs",
316
326
  "title": "canopy docs",
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CQIPmdcO.mjs";
1
+ import { t as version } from "./package-Doigafvd.mjs";
2
2
  import { n as siteConfig } from "./site-DM2J10CB.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
4
  import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CQIPmdcO.mjs";
1
+ import { t as version } from "./package-Doigafvd.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
3
3
  import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-C4rMhE7i.mjs";
4
4
  import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CQIPmdcO.mjs";
2
+ import { t as version } from "./package-Doigafvd.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -20,6 +20,7 @@ import { defineCommand, runMain } from "citty";
20
20
  * ai models the site's AI models and auto/* aliases
21
21
  * mcp add|serve a project's tools for AI agents (docs/49)
22
22
  * open open the deployed project / dashboard [phase 1]
23
+ * browse one-time sign-in link for an agent's browser
23
24
  */
24
25
  if (process.env["CANOPY_UPDATE_REFRESH"] === "1") {
25
26
  await runRefresh();
@@ -69,7 +70,7 @@ const main = defineCommand({
69
70
  init: () => import("./projects-DQAhCZm8.mjs").then((m) => m.initCommand),
70
71
  projects: () => import("./projects-DQAhCZm8.mjs").then((m) => m.projectsCommand),
71
72
  delete: () => import("./projects-DQAhCZm8.mjs").then((m) => m.deleteCommand),
72
- dev: () => import("./dev-MoHHjQh1.mjs").then((m) => m.devCommand),
73
+ dev: () => import("./dev-DO6Eso4L.mjs").then((m) => m.devCommand),
73
74
  deploy: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.deployCommand),
74
75
  promote: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.promoteCommand),
75
76
  staging: () => import("./staging-BZVty57b.mjs").then((m) => m.stagingCommand),
@@ -79,16 +80,17 @@ const main = defineCommand({
79
80
  egress: () => import("./egress-If_Gsx_M.mjs").then((m) => m.egressCommand),
80
81
  secrets: () => import("./secrets-BSEjZnxg.mjs").then((m) => m.secretsCommand),
81
82
  schedule: () => import("./schedule-C5lMf3ny.mjs").then((m) => m.scheduleCommand),
82
- feedback: () => import("./feedback-5oPSxAz0.mjs").then((m) => m.feedbackCommand),
83
+ feedback: () => import("./feedback-vvysW6iw.mjs").then((m) => m.feedbackCommand),
83
84
  db: () => import("./db-BB6O6085.mjs").then((m) => m.dbCommand),
84
85
  usage: () => import("./usage-CUYrWxv0.mjs").then((m) => m.usageCommand),
85
86
  ai: () => import("./ai-DHLnyqfq.mjs").then((m) => m.aiCommand),
86
87
  open: () => import("./open-DcaQKi1v.mjs").then((m) => m.openCommand),
87
- docs: () => import("./docs-BP5jmD10.mjs").then((m) => m.docsCommand),
88
- skill: () => import("./skill-Dg7dQF0k.mjs").then((m) => m.skillCommand),
89
- upgrade: () => import("./upgrade-huJDjhrV.mjs").then((m) => m.upgradeCommand),
88
+ browse: () => import("./browse-Dycwj0V_.mjs").then((m) => m.browseCommand),
89
+ docs: () => import("./docs-Cdoh5sMW.mjs").then((m) => m.docsCommand),
90
+ skill: () => import("./skill-DfSAZ2cW.mjs").then((m) => m.skillCommand),
91
+ upgrade: () => import("./upgrade-DCQ3NzoL.mjs").then((m) => m.upgradeCommand),
90
92
  logs: () => import("./logs-DQQOawOG.mjs").then((m) => m.logsCommand),
91
- mcp: () => import("./mcp-DXfTYVN_.mjs").then((m) => m.mcpCommand),
93
+ mcp: () => import("./mcp-OR_ashGC.mjs").then((m) => m.mcpCommand),
92
94
  repo: () => import("./repo-1RQ1vBMv.mjs").then((m) => m.repoCommand)
93
95
  }
94
96
  });
@@ -1,6 +1,6 @@
1
1
  import { n as apiDataRequest } from "./api-C4rMhE7i.mjs";
2
- import { a as dbRouter, i as realtimeRouter, o as META_COLUMNS, s as blobRouter } from "./src-BkACMkol.mjs";
3
- import { i as rewriteApiPath } from "./dev-CkhBnSlv.mjs";
2
+ import { a as dbRouter, i as realtimeRouter, o as META_COLUMNS, s as blobRouter } from "./src-DE37PKho.mjs";
3
+ import { i as rewriteApiPath } from "./dev-CGSIY1-y.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, relative, sep } from "node:path";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CQIPmdcO.mjs";
1
+ import { t as version } from "./package-Doigafvd.mjs";
2
2
  import { d as tryResolveProject, l as stagingUrl, n as siteConfig, s as projectUrl } from "./site-DM2J10CB.mjs";
3
3
  import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
4
4
  import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.33";
2
+ var version = "0.7.34";
3
3
  //#endregion
4
4
  export { version as t };
@@ -97,6 +97,12 @@ and never go stale.
97
97
  (own database cloned from live; `canopy staging status | sync | destroy`)
98
98
  - Roll back: `canopy rollback`
99
99
  - Open the live site: `canopy open`
100
+ - **Look at a deployed project in your own browser** (Playwright, a browser
101
+ tool, curl): `canopy browse <slug> --json` prints a one-time sign-in link
102
+ (`--staging`, `--path /page`). Open it within a minute; that browser is
103
+ then signed in to that one project, as the user, for an hour. On a sign-in
104
+ page later, run it again. Never ask the user for their session cookie
105
+ (`canopy docs agent-browsing`).
100
106
  - Manage data: `canopy db pull | snapshot | snapshots | restore | migrate`
101
107
  - Use AI (`canopy.ai`): run `canopy ai models` before naming a model in code.
102
108
  Prefer the aliases `auto/low` (default) / `auto/mid` / `auto/high` over
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CQIPmdcO.mjs";
1
+ import { t as version } from "./package-Doigafvd.mjs";
2
2
  import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -857,7 +857,11 @@ const AGENT_CREDENTIAL_HEADER = "x-canopy-agent-credential";
857
857
  */
858
858
  const CALLER_HEADER = "x-canopy-caller";
859
859
  /** Cookies that authenticate to Canopy itself — the monk session and the client-page session. */
860
- const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set(["canopy_session", "canopy_client"]);
860
+ const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set([
861
+ "canopy_session",
862
+ "canopy_client",
863
+ "canopy_agent"
864
+ ]);
861
865
  /**
862
866
  * Strip Canopy's own credentials from a request before untrusted project code
863
867
  * sees it (R-01).
@@ -870,7 +874,7 @@ const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set(["canopy_session", "canopy_c
870
874
  *
871
875
  * Redaction is surgical on purpose: an app's own cookies and its own
872
876
  * Authorization scheme survive, because those belong to the app. Only
873
- * `canopy_session` / `canopy_client` and a `cnpy_` bearer are removed.
877
+ * `canopy_session` / `canopy_client` / `canopy_agent` and a `cnpy_` bearer are removed.
874
878
  *
875
879
  * `env.CANOPY.fetch` still authenticates as the visitor — the host passes it
876
880
  * the unredacted headers — so nothing about the platform-facing behaviour
@@ -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-Dg7dQF0k.mjs");
115
+ const { skillInstalled } = await import("./skill-DfSAZ2cW.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.33",
3
+ "version": "0.7.34",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -1,2 +0,0 @@
1
- import { n as devCommand } from "./dev-CkhBnSlv.mjs";
2
- export { devCommand };