@myna-sh/cli 0.8.0 → 0.10.0

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/dist/main.js CHANGED
@@ -130,10 +130,10 @@ var HttpClient = class {
130
130
  * while in flight — this is request coalescing, not a cache.
131
131
  */
132
132
  inFlight = /* @__PURE__ */ new Map();
133
- dedupe(key, run) {
133
+ dedupe(key, run2) {
134
134
  const existing = this.inFlight.get(key);
135
135
  if (existing) return existing.then((response) => response.clone());
136
- const started = run();
136
+ const started = run2();
137
137
  this.inFlight.set(key, started);
138
138
  const cleanup = () => {
139
139
  if (this.inFlight.get(key) === started) this.inFlight.delete(key);
@@ -468,6 +468,16 @@ var ManagementClient = class {
468
468
  collection ? { origin, collection } : { origin },
469
469
  signal
470
470
  ),
471
+ /**
472
+ * Why a call into this project would be denied: whether the credential
473
+ * holds a capability, whether it is confined to other collections, and
474
+ * whether the collection exists at all.
475
+ */
476
+ access: (project, opts = {}, signal) => this.get(
477
+ `/projects/${enc(project)}/access`,
478
+ { capability: opts.capability, collection: opts.collection },
479
+ signal
480
+ ),
471
481
  views: (project, signal) => this.get(`/projects/${enc(project)}/views`, void 0, signal),
472
482
  createView: (project, body) => this.mutate("POST", `/projects/${enc(project)}/views`, body),
473
483
  deleteView: (project, view) => this.mutate("DELETE", `/projects/${enc(project)}/views/${enc(view)}`)
@@ -649,6 +659,19 @@ var ManagementClient = class {
649
659
  }),
650
660
  get: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, void 0, signal),
651
661
  usage: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, { usage: "true" }, signal),
662
+ /**
663
+ * Assets that look like the same picture, by perceptual hash — a resized or
664
+ * re-encoded copy, which `checksum` cannot see. `maxDistance` is in bits out
665
+ * of 64 (default 5, maximum 16).
666
+ */
667
+ similar: (project, asset, opts = {}, signal) => this.get(
668
+ `/projects/${enc(project)}/assets/${enc(asset)}/similar`,
669
+ {
670
+ maxDistance: opts.maxDistance === void 0 ? void 0 : String(opts.maxDistance),
671
+ limit: opts.limit === void 0 ? void 0 : String(opts.limit)
672
+ },
673
+ signal
674
+ ).then((r) => r.similar),
652
675
  update: (project, asset, body) => this.mutate("PATCH", `/projects/${enc(project)}/assets/${enc(asset)}`, body),
653
676
  replace: (project, asset, body) => this.mutate("POST", `/projects/${enc(project)}/assets/${enc(asset)}/replace`, body),
654
677
  delete: (project, asset) => this.mutate("DELETE", `/projects/${enc(project)}/assets/${enc(asset)}`),
@@ -1194,7 +1217,33 @@ function startDevice(apiUrl) {
1194
1217
  function pollDevice(apiUrl, deviceCode) {
1195
1218
  return apiPost(apiUrl, `/auth/device/${deviceCode}/token`, { deviceCode });
1196
1219
  }
1220
+ function registerAccess(program) {
1221
+ program.command("access").description("Explain what this credential may do in a project, and why a call was denied").option("--capability <cap>", "capability to check, e.g. content:publish").option("--collection <key>", "collection the denied call touched").action(
1222
+ handle(async (ctx, _args, opts) => {
1223
+ const project = ctx.requireProject();
1224
+ const access = await ctx.management().projects.access(project, {
1225
+ capability: opts.capability,
1226
+ collection: opts.collection
1227
+ });
1228
+ emit(access, () => {
1229
+ keyValues([
1230
+ ["Project", access.projectSlug],
1231
+ ["Credential", access.credential + (access.role ? ` (${access.role})` : "")],
1232
+ ["Capabilities", access.capabilities.join(", ") || "(none)"],
1233
+ ["Collections", access.allowedCollections?.join(", ") ?? "all"]
1234
+ ]);
1235
+ for (const check2 of access.checks) {
1236
+ diag(` ${check2.ok ? "ok " : "fail"} ${check2.id} \u2014 ${check2.detail}`);
1237
+ }
1238
+ if (access.remedy) diag(`
1239
+ remedy: ${access.remedy}`);
1240
+ });
1241
+ if (!access.allowed) process.exitCode = 1;
1242
+ })
1243
+ );
1244
+ }
1197
1245
  function registerAuth(program) {
1246
+ registerAccess(program);
1198
1247
  const login = program.command("login").description("Authenticate via the browser device-authorization flow").action(
1199
1248
  handle(async (ctx) => {
1200
1249
  const start = await startDevice(ctx.apiUrl);
@@ -1483,7 +1532,7 @@ function registerWorkspace(program) {
1483
1532
  }
1484
1533
 
1485
1534
  // src/commands/schema.ts
1486
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1535
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1487
1536
  import { join as join4 } from "path";
1488
1537
 
1489
1538
  // src/schema-loader.ts
@@ -1717,6 +1766,89 @@ function varName(name) {
1717
1766
  return /^[A-Za-z_$]/.test(camel) ? camel : `_${camel}`;
1718
1767
  }
1719
1768
 
1769
+ // src/git.ts
1770
+ import { execFileSync as execFileSync2 } from "child_process";
1771
+ import { createHash as createHash2 } from "crypto";
1772
+ function run(command, args, cwd) {
1773
+ try {
1774
+ return execFileSync2(command, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
1775
+ } catch (error) {
1776
+ const stderr = error.stderr;
1777
+ const detail = typeof stderr === "string" ? stderr : stderr?.toString("utf8");
1778
+ throw new CliError(`${command} ${args[0]} failed: ${(detail || error.message).trim()}`);
1779
+ }
1780
+ }
1781
+ function git(args, cwd) {
1782
+ return run("git", args, cwd).trim();
1783
+ }
1784
+ function gitQuiet(args, cwd) {
1785
+ try {
1786
+ execFileSync2("git", args, { cwd, stdio: "ignore" });
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ function available(command) {
1791
+ try {
1792
+ execFileSync2(command, ["--version"], { stdio: "ignore" });
1793
+ return true;
1794
+ } catch {
1795
+ return false;
1796
+ }
1797
+ }
1798
+ function branchForContent(contents) {
1799
+ const digest = createHash2("sha256").update(contents.join("\0")).digest("hex").slice(0, 8);
1800
+ return `myna/schema-${digest}`;
1801
+ }
1802
+ function openPullRequest(input) {
1803
+ if (!available("git")) throw new CliError("git is required to open a pull request.");
1804
+ if (!available("gh")) {
1805
+ throw new CliError("The GitHub CLI (gh) is required to open a pull request. See https://cli.github.com.");
1806
+ }
1807
+ if (git(["rev-parse", "--is-inside-work-tree"], input.cwd) !== "true") {
1808
+ throw new CliError(`${input.cwd} is not inside a git repository.`);
1809
+ }
1810
+ const base = input.base ?? git(["rev-parse", "--abbrev-ref", "HEAD"], input.cwd);
1811
+ if (base === "HEAD") {
1812
+ throw new CliError("HEAD is detached; pass --base to say which branch to open the pull request against.");
1813
+ }
1814
+ const exists = (() => {
1815
+ try {
1816
+ execFileSync2("git", ["rev-parse", "--verify", `refs/heads/${input.branch}`], {
1817
+ cwd: input.cwd,
1818
+ stdio: "ignore"
1819
+ });
1820
+ return true;
1821
+ } catch {
1822
+ return false;
1823
+ }
1824
+ })();
1825
+ if (exists) return { opened: false, branch: input.branch, base, url: null, reason: "branch-exists" };
1826
+ git(["checkout", "-b", input.branch], input.cwd);
1827
+ let pushed = false;
1828
+ try {
1829
+ git(["add", "--", ...input.files], input.cwd);
1830
+ git(["commit", "-m", input.title, "-m", input.body], input.cwd);
1831
+ git(["push", "--set-upstream", "origin", input.branch], input.cwd);
1832
+ pushed = true;
1833
+ const url = run(
1834
+ "gh",
1835
+ ["pr", "create", "--base", base, "--head", input.branch, "--title", input.title, "--body", input.body],
1836
+ input.cwd
1837
+ ).trim().split("\n").filter((line) => line.startsWith("http")).pop();
1838
+ git(["checkout", base], input.cwd);
1839
+ return { opened: true, branch: input.branch, base, url: url ?? null };
1840
+ } catch (error) {
1841
+ gitQuiet(["checkout", base], input.cwd);
1842
+ if (pushed) gitQuiet(["push", "origin", "--delete", input.branch], input.cwd);
1843
+ gitQuiet(["branch", "-D", input.branch], input.cwd);
1844
+ throw error;
1845
+ }
1846
+ }
1847
+ function dirtyFiles(cwd, paths) {
1848
+ if (paths.length === 0) return [];
1849
+ return run("git", ["status", "--porcelain", "--", ...paths], cwd).split("\n").filter(Boolean).map((line) => line.slice(3).trim());
1850
+ }
1851
+
1720
1852
  // src/commands/schema.ts
1721
1853
  async function deployedSchemas(ctx, project) {
1722
1854
  const mgmt = ctx.management();
@@ -1762,21 +1894,59 @@ function registerSchema(program) {
1762
1894
  });
1763
1895
  })
1764
1896
  );
1765
- schema.command("pull").description("Write deployed schemas to local DSL files").option("--schema-dir <dir>", "target schema directory").action(
1897
+ schema.command("pull").description("Write deployed schemas to local DSL files").option("--schema-dir <dir>", "target schema directory").option("--open-pr", "commit the drift onto a branch and open a pull request instead of writing in place", false).option("--branch <name>", "branch to use with --open-pr (default: derived from the content)").option("--base <branch>", "branch to open the pull request against (default: the current branch)").action(
1766
1898
  handle(async (ctx, _args, opts) => {
1767
1899
  const project = ctx.requireProject();
1768
1900
  const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
1769
1901
  const schemas = await deployedSchemas(ctx, project);
1770
- mkdirSync3(dir, { recursive: true });
1771
- const written = [];
1772
- for (const s of schemas) {
1773
- const file = join4(dir, `${s.name}.ts`);
1774
- writeFileSync3(file, schemaToDsl(s));
1775
- written.push(file);
1776
- }
1777
- emit({ pulled: schemas.length, files: written }, () => {
1778
- for (const f of written) process.stdout.write(` wrote ${f}
1902
+ const rendered = schemas.map((s) => ({ file: join4(dir, `${s.name}.ts`), code: schemaToDsl(s) }));
1903
+ const changed = rendered.filter(
1904
+ (r) => !existsSync4(r.file) || readFileSync3(r.file, "utf8") !== r.code
1905
+ );
1906
+ const write = () => {
1907
+ mkdirSync3(dir, { recursive: true });
1908
+ for (const r of rendered) writeFileSync3(r.file, r.code);
1909
+ return rendered.map((r) => r.file);
1910
+ };
1911
+ if (!opts.openPr) {
1912
+ const written = write();
1913
+ emit({ pulled: schemas.length, files: written, changed: changed.map((c) => c.file) }, () => {
1914
+ for (const f of written) process.stdout.write(` wrote ${f}
1779
1915
  `);
1916
+ });
1917
+ return;
1918
+ }
1919
+ if (changed.length === 0) {
1920
+ emit(
1921
+ { pulled: schemas.length, changed: [], pullRequest: null },
1922
+ () => diag("No drift: the local schema files already match the deployed schema.")
1923
+ );
1924
+ return;
1925
+ }
1926
+ const root = ctx.linkedRoot ?? process.cwd();
1927
+ const dirty = dirtyFiles(root, [dir]);
1928
+ if (dirty.length > 0) {
1929
+ throw new UsageError(
1930
+ `Commit or stash changes under ${dir} first: ${dirty.join(", ")}.`
1931
+ );
1932
+ }
1933
+ const body = await driftBody(ctx, project, dir, changed.map((c) => c.file));
1934
+ write();
1935
+ const branch = opts.branch ?? branchForContent(changed.map((c) => c.code));
1936
+ const result = openPullRequest({
1937
+ cwd: root,
1938
+ files: changed.map((c) => c.file),
1939
+ branch,
1940
+ base: opts.base,
1941
+ title: `chore(schema): sync ${changed.length} collection(s) from ${project}`,
1942
+ body
1943
+ });
1944
+ emit({ pulled: schemas.length, changed: changed.map((c) => c.file), pullRequest: result }, () => {
1945
+ if (!result.opened) {
1946
+ diag(`Branch ${result.branch} already exists \u2014 this drift is already proposed.`);
1947
+ return;
1948
+ }
1949
+ diag(`Opened ${result.url ?? `pull request from ${result.branch}`} against ${result.base}.`);
1780
1950
  });
1781
1951
  })
1782
1952
  );
@@ -1894,6 +2064,29 @@ function registerSchema(program) {
1894
2064
  })
1895
2065
  );
1896
2066
  }
2067
+ async function driftBody(ctx, project, dir, files) {
2068
+ const lines = [
2069
+ `Deployed schema for \`${project}\`, pulled into this repository by \`myna schema pull --open-pr\`.`,
2070
+ "",
2071
+ "Files:",
2072
+ ...files.map((f) => `- \`${f}\``)
2073
+ ];
2074
+ try {
2075
+ const local = await loadLocalSchemas(dir);
2076
+ const diff = await ctx.management().schema.diff(project, canonicalJson(local));
2077
+ if (diff.ops.length > 0) {
2078
+ lines.push(
2079
+ "",
2080
+ `Before this change, pushing this repository would have applied ${diff.ops.length} operation(s) (${diff.classification}):`,
2081
+ "",
2082
+ ...diff.ops.map((op) => `- \`${op.kind}\` ${op.collection}${op.field ? `.${op.field}` : ""} \u2014 ${op.detail}`)
2083
+ );
2084
+ }
2085
+ } catch {
2086
+ lines.push("", "_The local schema could not be diffed; review the file changes directly._");
2087
+ }
2088
+ return lines.join("\n");
2089
+ }
1897
2090
  function renderDiff(diff) {
1898
2091
  if (diff.ops.length === 0) {
1899
2092
  diag("No changes.");
@@ -2482,6 +2675,9 @@ function registerChanges(program) {
2482
2675
  { header: "BEFORE", value: (c) => preview(c.before) },
2483
2676
  { header: "AFTER", value: (c) => preview(c.after) }
2484
2677
  ]);
2678
+ for (const change of item.changes) {
2679
+ if (change.segments) diag(` ${change.path}: ${inlineWords(change.segments)}`);
2680
+ }
2485
2681
  }
2486
2682
  if (diff.items.length === 0) diag("No items.");
2487
2683
  });
@@ -2563,6 +2759,15 @@ function registerChanges(program) {
2563
2759
  })
2564
2760
  );
2565
2761
  }
2762
+ function inlineWords(segments) {
2763
+ const KEEP = 32;
2764
+ return segments.map((s) => {
2765
+ const flat = s.value.replaceAll(/\s+/g, " ");
2766
+ if (s.op === "insert") return `{+${flat}+}`;
2767
+ if (s.op === "delete") return `[-${flat}-]`;
2768
+ return flat.length > KEEP * 2 ? `${flat.slice(0, KEEP)} \u2026 ${flat.slice(-KEEP)}` : flat;
2769
+ }).join("");
2770
+ }
2566
2771
  function preview(value) {
2567
2772
  if (value === void 0 || value === null) return "\u2014";
2568
2773
  const text = typeof value === "string" ? value : JSON.stringify(value);
@@ -2703,6 +2908,10 @@ function registerAssets(program) {
2703
2908
  let asset = await ctx.management().assets.upload(project, path);
2704
2909
  if (opts.alt) asset = await ctx.management().assets.update(project, asset.id, { defaultAlt: opts.alt });
2705
2910
  uploaded.push(asset);
2911
+ const similar = await ctx.management().assets.similar(project, asset.id).catch(() => []);
2912
+ for (const match of similar) {
2913
+ diag(` looks like ${match.asset.id} (${match.asset.displayFilename}), distance ${match.distance}`);
2914
+ }
2706
2915
  }
2707
2916
  emit(
2708
2917
  uploaded.length === 1 ? uploaded[0] : uploaded,
@@ -2734,6 +2943,26 @@ function registerAssets(program) {
2734
2943
  );
2735
2944
  })
2736
2945
  );
2946
+ assets.command("similar").description("Find images that look like the same picture as this one").argument("<id>", "asset id (ast_)").option("--max-distance <n>", "how many of the fingerprint's 64 bits may differ", "5").action(
2947
+ handle(async (ctx, args, opts) => {
2948
+ const project = ctx.requireProject();
2949
+ const similar = await ctx.management().assets.similar(project, args[0], {
2950
+ maxDistance: Number(opts.maxDistance)
2951
+ });
2952
+ emit(similar, () => {
2953
+ if (similar.length === 0) {
2954
+ diag("No near-duplicates. (Images uploaded before fingerprinting shipped have none.)");
2955
+ return;
2956
+ }
2957
+ table(similar, [
2958
+ { header: "ID", value: (s) => s.asset.id },
2959
+ { header: "FILENAME", value: (s) => s.asset.displayFilename },
2960
+ { header: "DISTANCE", value: (s) => String(s.distance) },
2961
+ { header: "STATE", value: (s) => s.asset.state }
2962
+ ]);
2963
+ });
2964
+ })
2965
+ );
2737
2966
  assets.command("inspect").description("Show an asset and its usage").argument("<id>", "asset id (ast_)").action(
2738
2967
  handle(async (ctx, args) => {
2739
2968
  const project = ctx.requireProject();
@@ -3459,25 +3688,11 @@ function registerBilling(program) {
3459
3688
  }
3460
3689
 
3461
3690
  // src/commands/doctor.ts
3462
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3691
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3463
3692
  import { join as join6 } from "path";
3464
3693
 
3465
- // src/version.ts
3466
- var VERSION = true ? "0.8.0" : "0.0.0-dev";
3467
-
3468
- // src/commands/doctor.ts
3694
+ // src/registry.ts
3469
3695
  var NPM_REGISTRY = "https://registry.npmjs.org";
3470
- var SCAN_IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".react-router", ".turbo", "coverage"]);
3471
- var GENERATED_MARKERS = [
3472
- "// Generated by `myna types generate`. Do not edit by hand.",
3473
- "export interface MynaCollections {"
3474
- ];
3475
- function looksGenerated(contents) {
3476
- return GENERATED_MARKERS.every((marker) => contents.includes(marker));
3477
- }
3478
- function check(id, title, status, detail, remedy) {
3479
- return remedy ? { id, title, status, detail, remedy } : { id, title, status, detail };
3480
- }
3481
3696
  async function latestPublished(pkg) {
3482
3697
  try {
3483
3698
  const res = await fetch(`${NPM_REGISTRY}/${pkg}/latest`, {
@@ -3491,8 +3706,45 @@ async function latestPublished(pkg) {
3491
3706
  return void 0;
3492
3707
  }
3493
3708
  }
3709
+ function detectPackageManager(binPath) {
3710
+ const path = binPath.replaceAll("\\", "/");
3711
+ if (path.includes("/.bun/")) return "bun";
3712
+ if (path.includes("/pnpm/") || path.includes("/.pnpm/")) return "pnpm";
3713
+ if (path.includes("/yarn/") || path.includes("/.yarn/")) return "yarn";
3714
+ return "npm";
3715
+ }
3716
+ function installCommand(manager, version) {
3717
+ const spec = `@myna-sh/cli@${version}`;
3718
+ switch (manager) {
3719
+ case "pnpm":
3720
+ return ["pnpm", "add", "-g", spec];
3721
+ case "yarn":
3722
+ return ["yarn", "global", "add", spec];
3723
+ case "bun":
3724
+ return ["bun", "add", "-g", spec];
3725
+ case "npm":
3726
+ return ["npm", "install", "-g", spec];
3727
+ }
3728
+ }
3729
+
3730
+ // src/version.ts
3731
+ var VERSION = true ? "0.10.0" : "0.0.0-dev";
3732
+ var IS_RELEASE_BUILD = true;
3733
+
3734
+ // src/commands/doctor.ts
3735
+ var SCAN_IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".react-router", ".turbo", "coverage"]);
3736
+ var GENERATED_MARKERS = [
3737
+ "// Generated by `myna types generate`. Do not edit by hand.",
3738
+ "export interface MynaCollections {"
3739
+ ];
3740
+ function looksGenerated(contents) {
3741
+ return GENERATED_MARKERS.every((marker) => contents.includes(marker));
3742
+ }
3743
+ function check(id, title, status, detail, remedy) {
3744
+ return remedy ? { id, title, status, detail, remedy } : { id, title, status, detail };
3745
+ }
3494
3746
  async function checkCliVersion() {
3495
- if (!parseSemVer(VERSION)) {
3747
+ if (!IS_RELEASE_BUILD) {
3496
3748
  return check(
3497
3749
  "cli.version",
3498
3750
  "CLI version",
@@ -3529,7 +3781,7 @@ function checkApi(meta, apiUrl) {
3529
3781
  const checks = [
3530
3782
  check("api.reachable", "API reachable", "pass", `${apiUrl} speaks API ${meta.apiVersion}.`)
3531
3783
  ];
3532
- if (!parseSemVer(VERSION)) {
3784
+ if (!IS_RELEASE_BUILD) {
3533
3785
  checks.push(
3534
3786
  check("api.compatibility", "Client compatibility", "skip", "Unreleased CLI build; nothing to compare.")
3535
3787
  );
@@ -3650,7 +3902,7 @@ async function checkOrigin(ctx, origin) {
3650
3902
  }
3651
3903
  async function checkSchema(ctx, schemaDir) {
3652
3904
  const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
3653
- if (!existsSync4(dir)) {
3905
+ if (!existsSync5(dir)) {
3654
3906
  return check("schema.drift", "Local schema", "skip", `No schema directory at ${dir}.`);
3655
3907
  }
3656
3908
  if (!ctx.project) {
@@ -3677,7 +3929,9 @@ async function checkSchema(ctx, schemaDir) {
3677
3929
  "Local schema",
3678
3930
  "warn",
3679
3931
  `${diff.ops.length} undeployed change(s) (${diff.classification}).`,
3680
- "myna schema diff, then myna schema push"
3932
+ // Drift has two directions and two fixes: push local ahead, or pull
3933
+ // the deployed schema back into the repository as a reviewable change.
3934
+ "myna schema diff, then myna schema push \u2014 or myna schema pull --open-pr if the deployed schema is the one to keep"
3681
3935
  );
3682
3936
  } catch (error) {
3683
3937
  return check(
@@ -3711,7 +3965,7 @@ function findGeneratedTypes(root, depth = 4) {
3711
3965
  }
3712
3966
  if (!/\.(ts|d\.ts)$/.test(entry)) continue;
3713
3967
  try {
3714
- if (looksGenerated(readFileSync3(full, "utf8"))) return full;
3968
+ if (looksGenerated(readFileSync4(full, "utf8"))) return full;
3715
3969
  } catch {
3716
3970
  }
3717
3971
  }
@@ -3728,16 +3982,16 @@ async function checkTypes(ctx, explicit, schemaDir) {
3728
3982
  if (!file) {
3729
3983
  return check("types.freshness", "Generated types", "skip", "No generated types file found.");
3730
3984
  }
3731
- if (!existsSync4(file)) {
3985
+ if (!existsSync5(file)) {
3732
3986
  return check("types.freshness", "Generated types", "fail", `${file} does not exist.`);
3733
3987
  }
3734
3988
  const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
3735
- if (!existsSync4(dir)) {
3989
+ if (!existsSync5(dir)) {
3736
3990
  return check("types.freshness", "Generated types", "skip", `Found ${file} but no schema directory to compare against.`);
3737
3991
  }
3738
3992
  try {
3739
3993
  const expected = generateTypesModule(await loadLocalSchemas(dir));
3740
- return readFileSync3(file, "utf8") === expected ? check("types.freshness", "Generated types", "pass", `${file} matches the local schema.`) : check(
3994
+ return readFileSync4(file, "utf8") === expected ? check("types.freshness", "Generated types", "pass", `${file} matches the local schema.`) : check(
3741
3995
  "types.freshness",
3742
3996
  "Generated types",
3743
3997
  "warn",
@@ -3799,6 +4053,58 @@ ${failed} failure(s), ${warned} warning(s).`
3799
4053
  );
3800
4054
  }
3801
4055
 
4056
+ // src/commands/update.ts
4057
+ import { execFileSync as execFileSync3 } from "child_process";
4058
+ import { fileURLToPath as fileURLToPath2 } from "url";
4059
+ function registerUpdate(program) {
4060
+ program.command("update").description("Update the CLI to the latest published release").option("--check", "report whether an update exists without installing it", false).action(
4061
+ handle(async (_ctx, _args, opts) => {
4062
+ const current = VERSION;
4063
+ const checkOnly = Boolean(opts.check);
4064
+ if (!IS_RELEASE_BUILD) {
4065
+ emit(
4066
+ { current, latest: null, upToDate: false, updated: false, reason: "unreleased" },
4067
+ () => diag(`Running an unreleased build (${current}); nothing to update. Install a release with: npm install -g @myna-sh/cli`)
4068
+ );
4069
+ return;
4070
+ }
4071
+ const latest = await latestPublished("@myna-sh/cli");
4072
+ if (!latest) {
4073
+ throw new CliError("Could not reach the npm registry to check for updates.");
4074
+ }
4075
+ if (compareSemVer(current, latest) >= 0) {
4076
+ emit(
4077
+ { current, latest, upToDate: true, updated: false },
4078
+ () => diag(`${current} is the latest release.`)
4079
+ );
4080
+ return;
4081
+ }
4082
+ const manager = detectPackageManager(fileURLToPath2(import.meta.url));
4083
+ const command = installCommand(manager, latest);
4084
+ if (checkOnly) {
4085
+ emit(
4086
+ { current, latest, upToDate: false, updated: false, command: command.join(" ") },
4087
+ () => diag(`${current} is behind ${latest}. Update with: ${command.join(" ")}`)
4088
+ );
4089
+ process.exitCode = 1;
4090
+ return;
4091
+ }
4092
+ diag(`Updating ${current} \u2192 ${latest} with: ${command.join(" ")}`);
4093
+ try {
4094
+ execFileSync3(command[0], command.slice(1), { stdio: "inherit" });
4095
+ } catch (error) {
4096
+ throw new CliError(
4097
+ `Update failed: ${error instanceof Error ? error.message : String(error)}. Run it yourself with: ${command.join(" ")}`
4098
+ );
4099
+ }
4100
+ emit(
4101
+ { current, latest, upToDate: false, updated: true, command: command.join(" ") },
4102
+ () => diag(`Updated to ${latest}. Run \`myna doctor\` to confirm the API agrees.`)
4103
+ );
4104
+ })
4105
+ );
4106
+ }
4107
+
3802
4108
  // src/commands/sync.ts
3803
4109
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
3804
4110
  import { join as join7 } from "path";
@@ -3980,6 +4286,7 @@ function buildProgram() {
3980
4286
  program.name("myna").description("Myna \u2014 content infrastructure for developers and agents").version(VERSION, "-v, --version").option("--json", "emit a single machine-readable JSON value on stdout").option("--project <ref>", "project id or slug").option("--organization <ref>", "organization id or slug").option("--token <token>", "API token (overrides stored credentials)").option("--api-url <url>", "API base URL").option("--no-interactive", "disable prompts and browser opening").showHelpAfterError();
3981
4287
  registerAuth(program);
3982
4288
  registerDoctor(program);
4289
+ registerUpdate(program);
3983
4290
  registerWorkspace(program);
3984
4291
  registerSchema(program);
3985
4292
  registerEntries(program);