magic-spells 0.2.1 → 0.3.1

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/README.md CHANGED
@@ -48,6 +48,7 @@ wins over a stored one; pasted keys are never re-minted.
48
48
  | `magic logout [--product <p>]` | revoke and forget credentials |
49
49
  | `magic whoami [--json]` | your account (name, email, sign-in expiry) and per product: key source (env, account, manual, none), workspaces, default, masked keys |
50
50
  | `magic doctor [--json]` | Node, credential store, Account and your sign-in, and per product: API reachable, key present, product checks |
51
+ | `magic update [--check] [--json]` | update magic to the latest version: runs `npm install -g magic-spells@<latest>` for a global npm install; `--check` and `--json` only report |
51
52
  | `magic mcp [--products a,b]` | the MCP server over stdio: every product's tools, prefixed (`pyramid_*`) |
52
53
  | `magic pyramid <noun> <verb>` | Pyramid: `task next`, `task move WEB-42 --status "In Review"`, `task comment`, `project list`, `whoami`, … (`magic pyramid --help`) |
53
54
 
@@ -55,6 +56,11 @@ Global flags: `--json`, `--yes/-y`, `--no-color`, `--quiet/-q`, `--workspace <ha
55
56
  Output is JSON whenever stdout is not a terminal; errors go to stderr as
56
57
  `{ "error": { "code", "message", "hint", "product" } }`.
57
58
 
59
+ Once a day `magic` asks the npm registry for the latest version in the background
60
+ (cached under `updateCheck` in `config.json`) and, when a newer one exists, prints one
61
+ line to stderr after the command. Never under `--json`, `--quiet`, `magic mcp` or when
62
+ stdout or stderr is not a terminal.
63
+
58
64
  ## MCP
59
65
 
60
66
  ```sh
@@ -77,6 +83,7 @@ stored, without restarting the server. With several workspaces in a product, set
77
83
  | `MAGIC_ALLOW_DESTRUCTIVE=1` | allow destructive operations (with `--yes`, or `yes: true` in MCP) |
78
84
  | `MAGIC_CREDENTIALS_FILE=1` | store keys in the 0600 file instead of the OS keychain |
79
85
  | `XDG_CONFIG_HOME` | where `magic/` (config and credentials file) lives |
86
+ | `MAGIC_NO_UPDATE_CHECK=1` | no update notice (also off under `CI`) |
80
87
  | `NO_COLOR` | no colour |
81
88
 
82
89
  ## Exit codes
@@ -307,6 +307,16 @@ function createFileStore(path, warn) {
307
307
  }
308
308
  };
309
309
  }
310
+ function decodeHexOutput(v) {
311
+ if (v.length % 2 !== 0 || !/^[0-9a-f]+$/.test(v)) return v;
312
+ const buf = Buffer.from(v, "hex");
313
+ if (buf.toString("hex") !== v) return v;
314
+ try {
315
+ return new TextDecoder("utf-8", { fatal: true }).decode(buf);
316
+ } catch {
317
+ return v;
318
+ }
319
+ }
310
320
  function createMacStore(exec, file) {
311
321
  const fromKeychain = (item) => {
312
322
  try {
@@ -318,7 +328,7 @@ function createMacStore(exec, file) {
318
328
  item,
319
329
  "-w"
320
330
  ]).trim();
321
- return v.length > 0 ? v : null;
331
+ return v.length > 0 ? decodeHexOutput(v) : null;
322
332
  } catch {
323
333
  return null;
324
334
  }
@@ -1565,12 +1575,17 @@ async function startLoopback(opts) {
1565
1575
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1566
1576
  const state = url.searchParams.get("state");
1567
1577
  if (done || exchanging || req.method !== "GET" || url.pathname !== "/callback") {
1568
- return reply(res, 400, "Bad request.");
1578
+ return reply(res, 400, "Bad request", "Bad request.");
1569
1579
  }
1570
- if (!sameState(state, opts.state)) return reply(res, 400, "Bad request.");
1580
+ if (!sameState(state, opts.state)) return reply(res, 400, "Bad request", "Bad request.");
1571
1581
  const error = url.searchParams.get("error");
1572
1582
  if (error) {
1573
- reply(res, 200, "Sign-in cancelled. You can close this tab and return to your terminal.");
1583
+ reply(
1584
+ res,
1585
+ 200,
1586
+ "Sign-in cancelled",
1587
+ "Sign-in cancelled. You can close this tab and return to your terminal."
1588
+ );
1574
1589
  return finish({
1575
1590
  error: error === "access_denied" ? cancelledError() : new MagicError("auth_invalid", `Account refused the sign-in (${error}).`, {
1576
1591
  product: ACCOUNT_PRODUCT
@@ -1578,17 +1593,17 @@ async function startLoopback(opts) {
1578
1593
  });
1579
1594
  }
1580
1595
  const code = url.searchParams.get("code");
1581
- if (!code) return reply(res, 400, "Bad request.");
1596
+ if (!code) return reply(res, 400, "Bad request", "Bad request.");
1582
1597
  exchanging = true;
1583
1598
  clearTimeout(timer);
1584
1599
  const signals = [AbortSignal.timeout(CODE_EXCHANGE_TIMEOUT_MS)];
1585
1600
  if (opts.signal) signals.push(opts.signal);
1586
1601
  try {
1587
1602
  const value = await opts.exchange(code, redirectUri, AbortSignal.any(signals));
1588
- reply(res, 200, "Signed in to Magic Spells. You can close this tab.");
1603
+ reply(res, 200, "Signed in", "Signed in to Magic Spells. You can close this tab.");
1589
1604
  finish({ value });
1590
1605
  } catch (err) {
1591
- reply(res, 400, "Sign-in failed. Return to your terminal for details.");
1606
+ reply(res, 400, "Sign-in failed", "Sign-in failed. Return to your terminal for details.");
1592
1607
  finish({ error: toMagicError(err, { product: ACCOUNT_PRODUCT }) });
1593
1608
  }
1594
1609
  };
@@ -1624,14 +1639,145 @@ function sameState(got, want) {
1624
1639
  const b = Buffer.from(want);
1625
1640
  return a.length === b.length && timingSafeEqual(a, b);
1626
1641
  }
1627
- function reply(res, status, text) {
1642
+ function reply(res, status, heading, text) {
1628
1643
  res.writeHead(status, {
1629
- "content-type": "text/plain; charset=utf-8",
1644
+ "content-type": "text/html; charset=utf-8",
1630
1645
  "cache-control": "no-store",
1646
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
1631
1647
  connection: "close"
1632
1648
  });
1633
- res.end(`${text}
1649
+ res.end(page(heading, text));
1650
+ }
1651
+ function page(heading, text) {
1652
+ return `<!doctype html>
1653
+ <html lang="en">
1654
+ <head>
1655
+ <meta charset="utf-8">
1656
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1657
+ <title>Magic Spells</title>
1658
+ <style>
1659
+ :root { color-scheme: light dark; --bg: #ffffff; --fg: #111111; --muted: #555555; }
1660
+ @media (prefers-color-scheme: dark) { :root { --bg: #111111; --fg: #f2f2f2; --muted: #a0a0a0; } }
1661
+ html, body { height: 100%; margin: 0; }
1662
+ body { display: flex; align-items: center; justify-content: center; padding: 0 16px; background: var(--bg); color: var(--fg); font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; text-align: center; }
1663
+ h1 { margin: 0 0 8px; font-size: 1.5rem; font-weight: 600; }
1664
+ p { margin: 0; color: var(--muted); line-height: 1.5; }
1665
+ </style>
1666
+ </head>
1667
+ <body>
1668
+ <main>
1669
+ <h1>${heading}</h1>
1670
+ <p>${text}</p>
1671
+ </main>
1672
+ </body>
1673
+ </html>
1674
+ `;
1675
+ }
1676
+
1677
+ // ../kit/src/update.ts
1678
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
1679
+ import { get as httpGet } from "node:http";
1680
+ import { get as httpsGet } from "node:https";
1681
+ import { dirname as dirname2 } from "node:path";
1682
+ var UPDATE_REGISTRY_URL = "https://registry.npmjs.org/magic-spells/latest";
1683
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
1684
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1685
+ function fetchLatestVersion(url, opts) {
1686
+ return new Promise((resolve, reject) => {
1687
+ const get = url.startsWith("https:") ? httpsGet : httpGet;
1688
+ const req = get(url, { headers: { accept: "application/json" } }, (res) => {
1689
+ if (res.statusCode !== 200) {
1690
+ res.resume();
1691
+ reject(new Error(`The npm registry answered HTTP ${res.statusCode}.`));
1692
+ return;
1693
+ }
1694
+ let body = "";
1695
+ res.setEncoding("utf8");
1696
+ res.on("data", (chunk) => body += chunk);
1697
+ res.on("error", reject);
1698
+ res.on("end", () => {
1699
+ let version;
1700
+ try {
1701
+ version = JSON.parse(body).version;
1702
+ } catch {
1703
+ }
1704
+ if (typeof version === "string" && VERSION_PATTERN.test(version)) resolve(version);
1705
+ else reject(new Error("The npm registry answered without a valid version."));
1706
+ });
1707
+ });
1708
+ const timer = setTimeout(() => {
1709
+ const err = Object.assign(new Error("The npm registry did not answer in time."), {
1710
+ code: "ETIMEDOUT"
1711
+ });
1712
+ req.destroy(err);
1713
+ }, opts.timeoutMs);
1714
+ req.on("close", () => clearTimeout(timer));
1715
+ req.on("error", reject);
1716
+ if (opts.unref) {
1717
+ timer.unref();
1718
+ req.on("socket", (socket) => socket.unref());
1719
+ }
1720
+ });
1721
+ }
1722
+ function isNewer(latest, current) {
1723
+ const parse = (v) => {
1724
+ const [core = "", pre] = v.split("-", 2);
1725
+ return [core.split(".").map(Number), pre !== void 0];
1726
+ };
1727
+ const [a, aPre] = parse(latest);
1728
+ const [b, bPre] = parse(current);
1729
+ for (let i = 0; i < 3; i++) {
1730
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
1731
+ }
1732
+ return bPre && !aPre;
1733
+ }
1734
+ var WINDOWS_GLOBAL = /\\(npm|nodejs|nvm\\v[^\\]+|volta\\tools\\image\\packages\\magic-spells)\\node_modules\\magic-spells\\/i;
1735
+ function installMethod(binPath) {
1736
+ if (/[/\\]_npx[/\\]/.test(binPath)) return "npx";
1737
+ if (binPath.includes("/lib/node_modules/magic-spells/")) return "npm-global";
1738
+ if (WINDOWS_GLOBAL.test(binPath)) return "npm-global";
1739
+ return "other";
1740
+ }
1741
+ function readUpdateCheck(file) {
1742
+ const c = file?.updateCheck;
1743
+ if (!c || typeof c.checkedAt !== "string" || typeof c.latest !== "string") return null;
1744
+ if (!VERSION_PATTERN.test(c.latest) || Number.isNaN(Date.parse(c.checkedAt))) return null;
1745
+ return { checkedAt: c.checkedAt, latest: c.latest };
1746
+ }
1747
+ function updateCheckDue(check, now) {
1748
+ return !check || now - Date.parse(check.checkedAt) >= UPDATE_CHECK_INTERVAL_MS;
1749
+ }
1750
+ function writeUpdateCheck(path, check) {
1751
+ let text = null;
1752
+ try {
1753
+ text = readFileSync4(path, "utf8");
1754
+ } catch {
1755
+ }
1756
+ let file = {};
1757
+ if (text !== null) {
1758
+ let parsed;
1759
+ try {
1760
+ parsed = JSON.parse(text);
1761
+ } catch {
1762
+ return;
1763
+ }
1764
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
1765
+ file = parsed;
1766
+ }
1767
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
1768
+ const tmp = `${path}.${process.pid}.tmp`;
1769
+ writeFileSync2(tmp, `${JSON.stringify({ ...file, updateCheck: check }, null, 2)}
1634
1770
  `);
1771
+ renameSync(tmp, path);
1772
+ }
1773
+ function updateNotice(current, latest) {
1774
+ return isNewer(latest, current) ? `A newer magic is available: ${latest} (you have ${current}). Run \`magic update\`.` : null;
1775
+ }
1776
+ function registryError(err) {
1777
+ const message = err instanceof Error ? err.message : "The npm registry could not be reached.";
1778
+ return new MagicError("network", message, {
1779
+ hint: "Check your connection, or run npm install -g magic-spells@latest."
1780
+ });
1635
1781
  }
1636
1782
 
1637
1783
  // ../kit/src/mcp.ts
@@ -1752,18 +1898,18 @@ var TtlCache = class {
1752
1898
  };
1753
1899
 
1754
1900
  // ../kit/src/version.ts
1755
- import { readFileSync as readFileSync4 } from "node:fs";
1756
- import { dirname as dirname2, join as join4 } from "node:path";
1901
+ import { readFileSync as readFileSync5 } from "node:fs";
1902
+ import { dirname as dirname3, join as join4 } from "node:path";
1757
1903
  import { fileURLToPath } from "node:url";
1758
1904
  function readPackageVersion(moduleUrl) {
1759
- let dir = dirname2(fileURLToPath(moduleUrl));
1905
+ let dir = dirname3(fileURLToPath(moduleUrl));
1760
1906
  for (let i = 0; i < 8; i++) {
1761
1907
  try {
1762
- const pkg = JSON.parse(readFileSync4(join4(dir, "package.json"), "utf8"));
1908
+ const pkg = JSON.parse(readFileSync5(join4(dir, "package.json"), "utf8"));
1763
1909
  if (typeof pkg.version === "string") return pkg.version;
1764
1910
  } catch {
1765
1911
  }
1766
- const parent = dirname2(dir);
1912
+ const parent = dirname3(dir);
1767
1913
  if (parent === dir) break;
1768
1914
  dir = parent;
1769
1915
  }
@@ -1821,6 +1967,16 @@ export {
1821
1967
  runWithRemint,
1822
1968
  runDoctorChecks,
1823
1969
  startLoopback,
1970
+ UPDATE_REGISTRY_URL,
1971
+ VERSION_PATTERN,
1972
+ fetchLatestVersion,
1973
+ isNewer,
1974
+ installMethod,
1975
+ readUpdateCheck,
1976
+ updateCheckDue,
1977
+ writeUpdateCheck,
1978
+ updateNotice,
1979
+ registryError,
1824
1980
  runProductCli,
1825
1981
  createMcpServer,
1826
1982
  TtlCache,
@@ -4,7 +4,7 @@ import {
4
4
  readStr,
5
5
  rec,
6
6
  toMagicError
7
- } from "./chunk-ZC4NCQBG.js";
7
+ } from "./chunk-BJ2UK7WA.js";
8
8
 
9
9
  // ../pyramid/src/operations/index.ts
10
10
  import { z } from "zod";
package/dist/main.js CHANGED
@@ -2,6 +2,8 @@ import {
2
2
  AccountClient,
3
3
  GRANT_ITEM,
4
4
  MagicError,
5
+ UPDATE_REGISTRY_URL,
6
+ VERSION_PATTERN,
5
7
  authorizeUrl,
6
8
  buildContext,
7
9
  cell,
@@ -11,12 +13,15 @@ import {
11
13
  createMcpServer,
12
14
  createPkce,
13
15
  exitCodeFor,
16
+ fetchLatestVersion,
14
17
  forgetProductKey,
15
18
  forgetSessions,
16
19
  grantExpired,
17
20
  grantFromToken,
18
21
  grantIdleExpiry,
22
+ installMethod,
19
23
  isMagicError,
24
+ isNewer,
20
25
  jsonMode,
21
26
  loadCliConfig,
22
27
  manualItem,
@@ -34,7 +39,9 @@ import {
34
39
  readIndex,
35
40
  readPackageVersion,
36
41
  readStr,
42
+ readUpdateCheck,
37
43
  rec,
44
+ registryError,
38
45
  remintFor,
39
46
  render,
40
47
  renderError,
@@ -49,9 +56,12 @@ import {
49
56
  startLoopback,
50
57
  storeSessions,
51
58
  toMagicError,
59
+ updateCheckDue,
60
+ updateNotice,
52
61
  usage,
53
- waitForDevice
54
- } from "./chunk-ZC4NCQBG.js";
62
+ waitForDevice,
63
+ writeUpdateCheck
64
+ } from "./chunk-BJ2UK7WA.js";
55
65
 
56
66
  // src/browser.ts
57
67
  import { spawn } from "node:child_process";
@@ -821,8 +831,8 @@ async function serveMcp(shell, argv) {
821
831
  process.stdin.on("close", () => void shutdown("stdin closed"));
822
832
  process.stdin.on("end", () => void shutdown("stdin end"));
823
833
  await built.server.connect(transport);
824
- const summary = built.products.map((p) => `${p.module.id} (${p.operations.length} tools)`);
825
- err(`ready on stdio \xB7 ${summary.join(", ")}`);
834
+ const summary2 = built.products.map((p) => `${p.module.id} (${p.operations.length} tools)`);
835
+ err(`ready on stdio \xB7 ${summary2.join(", ")}`);
826
836
  for (const { module } of built.products) {
827
837
  try {
828
838
  await resolveCredential(module, { env: shell.env, store: shell.store });
@@ -845,6 +855,133 @@ function guardStdout() {
845
855
  console.debug = toErr;
846
856
  }
847
857
 
858
+ // src/commands/update.ts
859
+ import { spawn as spawn2 } from "node:child_process";
860
+ import { realpathSync } from "node:fs";
861
+ var LOOKUP_TIMEOUT_MS = 5e3;
862
+ var BACKGROUND_TIMEOUT_MS = 2e3;
863
+ var MANUAL = "npm install -g magic-spells@latest";
864
+ async function update(shell, argv) {
865
+ if (wantsHelp(argv)) {
866
+ shell.io.err(
867
+ "Usage: magic update [--check] [--json]\n\nUpdate magic to the latest version; --check only reports.\n"
868
+ );
869
+ return 0;
870
+ }
871
+ const args = [...argv];
872
+ rejectWorkspace("update", shell.opts);
873
+ const check = takeSwitch(args, "check");
874
+ noMoreArgs("update", args);
875
+ const deps = shell.update;
876
+ let latest;
877
+ try {
878
+ latest = await fetchLatestVersion(deps.registryUrl ?? UPDATE_REGISTRY_URL, {
879
+ timeoutMs: LOOKUP_TIMEOUT_MS
880
+ });
881
+ } catch (err) {
882
+ throw registryError(err);
883
+ }
884
+ const binPath = deps.binPath ?? runningBin();
885
+ const current = version();
886
+ const report2 = {
887
+ current,
888
+ latest,
889
+ upToDate: !isNewer(latest, current),
890
+ method: installMethod(binPath)
891
+ };
892
+ if (check || shell.opts.json) {
893
+ if (jsonMode(shell.opts, shell.io)) render(report2, shell.opts, shell.io);
894
+ else shell.io.out(`${summary(report2)}
895
+ `);
896
+ return 0;
897
+ }
898
+ if (report2.upToDate) {
899
+ shell.io.out(`${summary(report2)}
900
+ `);
901
+ return 0;
902
+ }
903
+ if (report2.method === "npx") {
904
+ shell.io.out(
905
+ "You're running magic through npx, which already fetches the latest version; nothing to update.\n"
906
+ );
907
+ return 0;
908
+ }
909
+ if (report2.method === "other") {
910
+ shell.io.out(
911
+ `${summary(report2)}
912
+ This magic runs from ${binPath}, not a global npm install. To install the latest globally:
913
+ ${MANUAL}
914
+ `
915
+ );
916
+ return 0;
917
+ }
918
+ if (!VERSION_PATTERN.test(latest)) throw registryError(new Error("Invalid version."));
919
+ const spec = `magic-spells@${latest}`;
920
+ if (!shell.opts.quiet)
921
+ shell.io.err(`Updating magic ${current} \u2192 ${latest}: npm install -g ${spec}
922
+ `);
923
+ const code = await (deps.npmInstall ?? npmInstall)(spec);
924
+ if (code !== 0) {
925
+ throw new MagicError("unknown", `npm install -g ${spec} failed (exit ${code}).`, {
926
+ hint: `Run ${MANUAL} yourself to see why.`
927
+ });
928
+ }
929
+ shell.io.out(`magic ${latest} installed.
930
+ `);
931
+ return 0;
932
+ }
933
+ function summary(r) {
934
+ return r.upToDate ? `magic ${r.current} is the latest version.` : `magic ${r.latest} is available (you have ${r.current}).`;
935
+ }
936
+ function runningBin() {
937
+ const bin = process.argv[1] ?? "";
938
+ try {
939
+ return realpathSync(bin);
940
+ } catch {
941
+ return bin;
942
+ }
943
+ }
944
+ function npmInstall(spec) {
945
+ const win = process.platform === "win32";
946
+ return new Promise((resolve, reject) => {
947
+ const child = spawn2(win ? "npm.cmd" : "npm", ["install", "-g", spec], {
948
+ stdio: "inherit",
949
+ shell: win
950
+ });
951
+ child.on("error", reject);
952
+ child.on("exit", (code) => resolve(code ?? 1));
953
+ });
954
+ }
955
+ var NO_NOTICE = /* @__PURE__ */ new Set(["update", "mcp"]);
956
+ function startUpdateNotice(ctx) {
957
+ const has = (...flags) => flags.some((f) => ctx.argv.includes(f));
958
+ if (NO_NOTICE.has(ctx.cmd) || has("--json", "--quiet", "-q", "--help", "-h")) return noop;
959
+ if (!ctx.stderrIsTTY || !ctx.io.isTTY()) return noop;
960
+ const file = readConfigFile(ctx.configPath);
961
+ try {
962
+ if (!loadCliConfig({ products: [], env: ctx.env, file }).updateCheck) return noop;
963
+ } catch {
964
+ return noop;
965
+ }
966
+ const cached2 = readUpdateCheck(file);
967
+ const now = ctx.deps.now ?? Date.now;
968
+ if (updateCheckDue(cached2, now())) {
969
+ const done = fetchLatestVersion(ctx.deps.registryUrl ?? UPDATE_REGISTRY_URL, {
970
+ timeoutMs: BACKGROUND_TIMEOUT_MS,
971
+ unref: true
972
+ }).then((latest) => {
973
+ writeUpdateCheck(ctx.configPath, { checkedAt: new Date(now()).toISOString(), latest });
974
+ }).catch(() => {
975
+ });
976
+ ctx.deps.onBackgroundCheck?.(done);
977
+ }
978
+ const line = cached2 ? updateNotice(version(), cached2.latest) : null;
979
+ return line ? () => ctx.io.err(`${line}
980
+ `) : noop;
981
+ }
982
+ function noop() {
983
+ }
984
+
848
985
  // src/commands/whoami.ts
849
986
  async function whoami(shell, argv) {
850
987
  if (wantsHelp(argv)) {
@@ -974,12 +1111,12 @@ var PRODUCTS = [
974
1111
  {
975
1112
  id: "pyramid",
976
1113
  title: "Pyramid \u2014 projects and tasks",
977
- load: async () => (await import("./src-56NZ53TG.js")).default
1114
+ load: async () => (await import("./src-QBB4E5KU.js")).default
978
1115
  },
979
1116
  {
980
1117
  id: "sites",
981
1118
  title: "Sites \u2014 sign-in only until its commands arrive",
982
- load: async () => (await import("./src-DJYPPJFM.js")).default
1119
+ load: async () => (await import("./src-GSMZBKYC.js")).default
983
1120
  },
984
1121
  {
985
1122
  id: "marketing",
@@ -1038,7 +1175,8 @@ var COMMANDS = {
1038
1175
  logout: { summary: "revoke and forget credentials: logout [--product <p>]", run: logout },
1039
1176
  whoami: { summary: "your account and your workspaces in each product", run: whoami },
1040
1177
  doctor: { summary: "check the install, the sign-in and each product", run: doctor },
1041
- mcp: { summary: "serve every product\u2019s tools over stdio: mcp [--products a,b]", run: serveMcp }
1178
+ mcp: { summary: "serve every product\u2019s tools over stdio: mcp [--products a,b]", run: serveMcp },
1179
+ update: { summary: "update magic to the latest version: update [--check]", run: update }
1042
1180
  };
1043
1181
  function help(products) {
1044
1182
  const lines = [
@@ -1075,34 +1213,51 @@ async function run(argv, deps = {}) {
1075
1213
  return 0;
1076
1214
  }
1077
1215
  const store = () => deps.store ?? createCredentialStore({ env });
1078
- const readConfig = () => readConfigFile(configFilePath({ env }));
1079
- const entry = products.find((p) => p.id === cmd);
1080
- if (entry) {
1081
- const product = await entry.load();
1082
- return runProductCli(product, rest, { io, env, store: store(), readConfig });
1083
- }
1084
- let opts = { json: argv.includes("--json"), yes: false, quiet: false };
1085
- try {
1086
- const command = COMMANDS[cmd];
1087
- if (!command) throw usage(`unknown command "${cmd}"`, "Run `magic --help` for usage.");
1088
- const parsed = parseGlobals(rest, [], env);
1089
- opts = parsed.opts;
1090
- const shell = {
1091
- io,
1092
- env,
1093
- store: store(),
1094
- entries: products,
1095
- opts,
1096
- readConfig,
1097
- readSecret: deps.readSecret ?? readSecret,
1098
- openBrowser: deps.openBrowser ?? openBrowser,
1099
- confirm: deps.confirm !== void 0 ? deps.confirm : process.stdin.isTTY ? confirmOnTty : null,
1100
- platform: deps.platform ?? process.platform,
1101
- login: deps.login ?? {}
1102
- };
1103
- return await command.run(shell, parsed.rest);
1104
- } catch (err) {
1105
- return renderError(toMagicError(err), opts, io);
1216
+ const configPath = configFilePath({ env });
1217
+ const readConfig = () => readConfigFile(configPath);
1218
+ const updateDeps = deps.update ?? {};
1219
+ const notice = startUpdateNotice({
1220
+ cmd,
1221
+ argv,
1222
+ io,
1223
+ env,
1224
+ stderrIsTTY: deps.stderrIsTTY ?? process.stderr.isTTY === true,
1225
+ configPath,
1226
+ deps: updateDeps
1227
+ });
1228
+ const code = await dispatch();
1229
+ notice();
1230
+ return code;
1231
+ async function dispatch() {
1232
+ const entry = products.find((p) => p.id === cmd);
1233
+ if (entry) {
1234
+ const product = await entry.load();
1235
+ return runProductCli(product, rest, { io, env, store: store(), readConfig });
1236
+ }
1237
+ let opts = { json: argv.includes("--json"), yes: false, quiet: false };
1238
+ try {
1239
+ const command = COMMANDS[cmd];
1240
+ if (!command) throw usage(`unknown command "${cmd}"`, "Run `magic --help` for usage.");
1241
+ const parsed = parseGlobals(rest, [], env);
1242
+ opts = parsed.opts;
1243
+ const shell = {
1244
+ io,
1245
+ env,
1246
+ store: store(),
1247
+ entries: products,
1248
+ opts,
1249
+ readConfig,
1250
+ readSecret: deps.readSecret ?? readSecret,
1251
+ openBrowser: deps.openBrowser ?? openBrowser,
1252
+ confirm: deps.confirm !== void 0 ? deps.confirm : process.stdin.isTTY ? confirmOnTty : null,
1253
+ platform: deps.platform ?? process.platform,
1254
+ login: deps.login ?? {},
1255
+ update: updateDeps
1256
+ };
1257
+ return await command.run(shell, parsed.rest);
1258
+ } catch (err) {
1259
+ return renderError(toMagicError(err), opts, io);
1260
+ }
1106
1261
  }
1107
1262
  }
1108
1263
  export {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  operations,
3
3
  operationsByName
4
- } from "./chunk-4Y3SKWF7.js";
5
- import "./chunk-ZC4NCQBG.js";
4
+ } from "./chunk-E3VCJGAM.js";
5
+ import "./chunk-BJ2UK7WA.js";
6
6
  export {
7
7
  operations,
8
8
  operationsByName
@@ -3,7 +3,7 @@ var sites = {
3
3
  id: "sites",
4
4
  title: "Sites",
5
5
  credential: "sites_",
6
- apiUrl: "https://api.sites.magicspells.io",
6
+ apiUrl: "https://sites-api.magicspells.io",
7
7
  keyHelp: "Sites \u2192 Settings \u2192 Access keys",
8
8
  sessions: { path: "/v1/cli/sessions" },
9
9
  load: async () => [],
@@ -3,13 +3,13 @@ import {
3
3
  operationsByName,
4
4
  pyramidError,
5
5
  toPyramidError
6
- } from "./chunk-4Y3SKWF7.js";
6
+ } from "./chunk-E3VCJGAM.js";
7
7
  import {
8
8
  MagicError,
9
9
  TtlCache,
10
10
  readField,
11
11
  readStr
12
- } from "./chunk-ZC4NCQBG.js";
12
+ } from "./chunk-BJ2UK7WA.js";
13
13
 
14
14
  // ../pyramid/src/cache/resolver.ts
15
15
  var TTL_MS = 6e4;
@@ -884,10 +884,10 @@ var PYRAMID_CREDENTIAL = {
884
884
  credential: "pyramid_",
885
885
  keyHelp: "Pyramid \u2192 Settings \u2192 API Keys"
886
886
  };
887
- var DEFAULT_API_URL = "https://api.pyramid.magicspells.io";
887
+ var DEFAULT_API_URL = "https://pyramid.magicspells.io";
888
888
 
889
889
  // ../pyramid/src/doctor.ts
890
- var whoamiOp = async () => (await import("./operations-VJZEQHSW.js")).operationsByName.get("pyramid.whoami");
890
+ var whoamiOp = async () => (await import("./operations-H6MRMD2G.js")).operationsByName.get("pyramid.whoami");
891
891
  var doctorChecks = [
892
892
  {
893
893
  name: "pyramid auth",
@@ -1049,7 +1049,7 @@ var pyramid = {
1049
1049
  // `magic login` mints workspace keys here from the Account grant (API-PYRAMID-CLI-SESSIONS).
1050
1050
  sessions: { path: "/v1/cli/sessions" },
1051
1051
  // The operations graph loads only when a Pyramid command runs.
1052
- load: async () => (await import("./operations-VJZEQHSW.js")).operations,
1052
+ load: async () => (await import("./operations-H6MRMD2G.js")).operations,
1053
1053
  client(session, config) {
1054
1054
  const api = new PyramidClient({
1055
1055
  apiKey: session.key,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-spells",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Magic CLI — one command line and MCP server for Magic Spells' hosted services.",
5
5
  "license": "MIT",
6
6
  "type": "module",