@myna-sh/cli 0.11.0 → 0.12.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/dist/main.js CHANGED
@@ -90,9 +90,9 @@ function buildQuery(query) {
90
90
  return qs ? `?${qs}` : "";
91
91
  }
92
92
  function sleep(ms, signal) {
93
- return new Promise((resolve3, reject) => {
93
+ return new Promise((resolve4, reject) => {
94
94
  if (signal?.aborted) return reject(signal.reason ?? new Error("Aborted"));
95
- const timer = setTimeout(resolve3, ms);
95
+ const timer = setTimeout(resolve4, ms);
96
96
  signal?.addEventListener(
97
97
  "abort",
98
98
  () => {
@@ -1533,7 +1533,7 @@ function registerWorkspace(program) {
1533
1533
 
1534
1534
  // src/commands/schema.ts
1535
1535
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1536
- import { join as join4 } from "path";
1536
+ import { join as join5 } from "path";
1537
1537
 
1538
1538
  // src/schema-loader.ts
1539
1539
  import { existsSync as existsSync3, readdirSync, statSync } from "fs";
@@ -1769,6 +1769,8 @@ function varName(name) {
1769
1769
  // src/git.ts
1770
1770
  import { execFileSync as execFileSync2 } from "child_process";
1771
1771
  import { createHash as createHash2 } from "crypto";
1772
+ import { rmSync as rmSync2 } from "fs";
1773
+ import { join as join4 } from "path";
1772
1774
  function run(command, args, cwd) {
1773
1775
  try {
1774
1776
  return execFileSync2(command, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -1799,6 +1801,23 @@ function branchForContent(contents) {
1799
1801
  const digest = createHash2("sha256").update(contents.join("\0")).digest("hex").slice(0, 8);
1800
1802
  return `myna/schema-${digest}`;
1801
1803
  }
1804
+ function branchExists(cwd, branch) {
1805
+ try {
1806
+ execFileSync2("git", ["rev-parse", "--verify", `refs/heads/${branch}`], { cwd, stdio: "ignore" });
1807
+ return true;
1808
+ } catch {
1809
+ }
1810
+ try {
1811
+ const out = execFileSync2("git", ["ls-remote", "--heads", "origin", branch], {
1812
+ cwd,
1813
+ encoding: "utf8",
1814
+ stdio: ["ignore", "pipe", "ignore"]
1815
+ });
1816
+ return out.trim().length > 0;
1817
+ } catch {
1818
+ return false;
1819
+ }
1820
+ }
1802
1821
  function openPullRequest(input) {
1803
1822
  if (!available("git")) throw new CliError("git is required to open a pull request.");
1804
1823
  if (!available("gh")) {
@@ -1811,18 +1830,9 @@ function openPullRequest(input) {
1811
1830
  if (base === "HEAD") {
1812
1831
  throw new CliError("HEAD is detached; pass --base to say which branch to open the pull request against.");
1813
1832
  }
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" };
1833
+ if (branchExists(input.cwd, input.branch)) {
1834
+ return { opened: false, branch: input.branch, base, url: null, reason: "branch-exists" };
1835
+ }
1826
1836
  git(["checkout", "-b", input.branch], input.cwd);
1827
1837
  let pushed = false;
1828
1838
  try {
@@ -1848,6 +1858,44 @@ function dirtyFiles(cwd, paths) {
1848
1858
  if (paths.length === 0) return [];
1849
1859
  return run("git", ["status", "--porcelain", "--", ...paths], cwd).split("\n").filter(Boolean).map((line) => line.slice(3).trim());
1850
1860
  }
1861
+ function repositoryRoot(cwd) {
1862
+ try {
1863
+ return execFileSync2("git", ["rev-parse", "--show-toplevel"], {
1864
+ cwd,
1865
+ encoding: "utf8",
1866
+ stdio: ["ignore", "pipe", "ignore"]
1867
+ }).trim();
1868
+ } catch {
1869
+ return void 0;
1870
+ }
1871
+ }
1872
+ function trackedLockfiles(cwd) {
1873
+ const root = repositoryRoot(cwd);
1874
+ if (!root) return [];
1875
+ try {
1876
+ return execFileSync2("git", ["ls-files", "--full-name", "*myna.lock", "myna.lock"], {
1877
+ cwd: root,
1878
+ encoding: "utf8",
1879
+ stdio: ["ignore", "pipe", "ignore"]
1880
+ }).split("\n").filter(Boolean).map((f) => join4(root, f));
1881
+ } catch {
1882
+ return [];
1883
+ }
1884
+ }
1885
+ function worktreeChanges(cwd) {
1886
+ const root = repositoryRoot(cwd);
1887
+ if (!root) throw new CliError(`${cwd} is not inside a git repository.`);
1888
+ return run("git", ["status", "--porcelain"], cwd).split("\n").filter(Boolean).map((line) => ({
1889
+ file: join4(root, line.slice(3).trim()),
1890
+ untracked: line.startsWith("??")
1891
+ }));
1892
+ }
1893
+ function discardChanges(cwd, changes) {
1894
+ for (const change of changes) {
1895
+ if (change.untracked) rmSync2(change.file, { force: true, recursive: true });
1896
+ else gitQuiet(["checkout", "--", change.file], cwd);
1897
+ }
1898
+ }
1851
1899
 
1852
1900
  // src/commands/schema.ts
1853
1901
  async function deployedSchemas(ctx, project) {
@@ -1899,7 +1947,7 @@ function registerSchema(program) {
1899
1947
  const project = ctx.requireProject();
1900
1948
  const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
1901
1949
  const schemas = await deployedSchemas(ctx, project);
1902
- const rendered = schemas.map((s) => ({ file: join4(dir, `${s.name}.ts`), code: schemaToDsl(s) }));
1950
+ const rendered = schemas.map((s) => ({ file: join5(dir, `${s.name}.ts`), code: schemaToDsl(s) }));
1903
1951
  const changed = rendered.filter(
1904
1952
  (r) => !existsSync4(r.file) || readFileSync3(r.file, "utf8") !== r.code
1905
1953
  );
@@ -2108,7 +2156,7 @@ function renderDiff(diff) {
2108
2156
 
2109
2157
  // src/content-files.ts
2110
2158
  import { readFile as readFile2, readdir, stat } from "fs/promises";
2111
- import { basename as basename2, extname, join as join5 } from "path";
2159
+ import { basename as basename2, extname, join as join6 } from "path";
2112
2160
  async function detectFormat(path) {
2113
2161
  const info = await stat(path).catch(() => void 0);
2114
2162
  if (!info) throw new UsageError(`No such file or directory: ${path}`);
@@ -2186,7 +2234,7 @@ async function readJsonRows(file) {
2186
2234
  }
2187
2235
  async function readMarkdownRows(dir, bodyField) {
2188
2236
  const info = await stat(dir).catch(() => void 0);
2189
- const files = info?.isDirectory() ? (await readdir(dir)).filter((name) => [".md", ".markdown"].includes(extname(name).toLowerCase())).sort().map((name) => join5(dir, name)) : [dir];
2237
+ const files = info?.isDirectory() ? (await readdir(dir)).filter((name) => [".md", ".markdown"].includes(extname(name).toLowerCase())).sort().map((name) => join6(dir, name)) : [dir];
2190
2238
  if (files.length === 0) throw new UsageError(`No .md files found in ${dir}`);
2191
2239
  const rows = [];
2192
2240
  for (const file of files) {
@@ -2823,6 +2871,25 @@ function registerReleases(program) {
2823
2871
  });
2824
2872
  })
2825
2873
  );
2874
+ releases.command("diff").description("Show what a release changed, field by field").argument("<release>", "release number or published change set id").action(
2875
+ handle(async (ctx, args) => {
2876
+ const project = ctx.requireProject();
2877
+ const release = await ctx.management().releases.get(project, args[0]);
2878
+ const diff = await ctx.management().changeSets.diff(project, release.id);
2879
+ emit({ release, ...diff }, () => {
2880
+ diag(`#${release.number} \u2014 ${release.title} (${diff.items.length} item(s))`);
2881
+ for (const item of diff.items) {
2882
+ const name = item.collection && item.slug ? `${item.collection}/${item.slug}` : item.resourceId;
2883
+ process.stdout.write(` ${item.operation} ${name}
2884
+ `);
2885
+ for (const change of item.changes) {
2886
+ process.stdout.write(` ${change.kind} ${change.path}
2887
+ `);
2888
+ }
2889
+ }
2890
+ });
2891
+ })
2892
+ );
2826
2893
  releases.command("revert").description("Generate a change set restoring the state before a release").argument("<release>", "release number or published change set id").option("--title <title>", "title for the generated change set").option("--force", "revert resources a later release already changed", false).action(
2827
2894
  handle(async (ctx, args, opts) => {
2828
2895
  const project = ctx.requireProject();
@@ -2854,6 +2921,439 @@ function registerReleases(program) {
2854
2921
  );
2855
2922
  }
2856
2923
 
2924
+ // src/commands/pull.ts
2925
+ import { execSync } from "child_process";
2926
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2927
+ import { join as join8 } from "path";
2928
+
2929
+ // ../sdk/dist/lock.js
2930
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
2931
+ import { dirname as dirname2, join as join7, resolve as resolve3 } from "path";
2932
+ var LOCKFILE_NAME = "myna.lock";
2933
+ function formatLock(lock) {
2934
+ const ordered = {
2935
+ project: lock.project,
2936
+ release: lock.release,
2937
+ publishedAt: lock.publishedAt
2938
+ };
2939
+ if (lock.title)
2940
+ ordered.title = lock.title;
2941
+ if (lock.apiUrl)
2942
+ ordered.apiUrl = lock.apiUrl;
2943
+ return `${JSON.stringify(ordered, null, 2)}
2944
+ `;
2945
+ }
2946
+ function parseLock(source, origin = LOCKFILE_NAME) {
2947
+ let parsed;
2948
+ try {
2949
+ parsed = JSON.parse(source);
2950
+ } catch (error) {
2951
+ throw new Error(`${origin} is not valid JSON: ${error.message}`);
2952
+ }
2953
+ const lock = parsed;
2954
+ if (typeof lock?.project !== "string" || !lock.project) {
2955
+ throw new Error(`${origin} is missing "project".`);
2956
+ }
2957
+ if (typeof lock.release !== "number" || !Number.isInteger(lock.release) || lock.release < 0) {
2958
+ throw new Error(`${origin} is missing a non-negative integer "release".`);
2959
+ }
2960
+ return {
2961
+ project: lock.project,
2962
+ release: lock.release,
2963
+ publishedAt: typeof lock.publishedAt === "string" ? lock.publishedAt : "",
2964
+ ...lock.title ? { title: lock.title } : {},
2965
+ ...lock.apiUrl ? { apiUrl: lock.apiUrl } : {}
2966
+ };
2967
+ }
2968
+ var LOCKFILE_NAME2 = "myna.lock";
2969
+ function formatLock2(lock) {
2970
+ return formatLock(lock);
2971
+ }
2972
+ function parseLock2(source, origin = LOCKFILE_NAME2) {
2973
+ return parseLock(source, origin);
2974
+ }
2975
+ function findLockfile(startDir = process.cwd()) {
2976
+ let dir = resolve3(startDir);
2977
+ for (; ; ) {
2978
+ const file = join7(dir, LOCKFILE_NAME2);
2979
+ if (existsSync5(file)) return file;
2980
+ const parent = dirname2(dir);
2981
+ if (parent === dir) return void 0;
2982
+ dir = parent;
2983
+ }
2984
+ }
2985
+ function writeLock(file, lock) {
2986
+ writeFileSync4(file, formatLock2(lock));
2987
+ }
2988
+
2989
+ // ../shared/dist/ids.js
2990
+ var ID_PREFIXES = {
2991
+ user: "usr",
2992
+ oauthAccount: "oau",
2993
+ session: "ses",
2994
+ organization: "org",
2995
+ organizationMember: "mem",
2996
+ organizationInvitation: "inv",
2997
+ project: "prj",
2998
+ projectOrigin: "por",
2999
+ apiKey: "key",
3000
+ collection: "col",
3001
+ collectionVersion: "clv",
3002
+ entry: "ent",
3003
+ entrySlugAlias: "als",
3004
+ entryRevision: "rev",
3005
+ changeSet: "chs",
3006
+ changeSetItem: "csi",
3007
+ changeSetReview: "csr",
3008
+ changeSetComment: "csc",
3009
+ changeSetCheck: "chk",
3010
+ projectCheck: "pck",
3011
+ asset: "ast",
3012
+ assetUpload: "upl",
3013
+ savedView: "viw",
3014
+ previewToken: "prv",
3015
+ webhookEndpoint: "whk",
3016
+ webhookEvent: "whe",
3017
+ webhookDelivery: "whd",
3018
+ auditEvent: "aud",
3019
+ job: "job",
3020
+ idempotencyKey: "idm",
3021
+ billingCustomer: "bcu",
3022
+ billingSubscription: "bsu",
3023
+ billingProviderEvent: "evt",
3024
+ usagePeriod: "usp",
3025
+ mcpClient: "mcl",
3026
+ mcpGrant: "mgr",
3027
+ mcpAuthorizationCode: "mac",
3028
+ mcpToken: "mtk",
3029
+ deviceAuthorization: "dev",
3030
+ githubInstallation: "ghi",
3031
+ projectRepository: "prp"
3032
+ };
3033
+ var PREFIX_SET = new Set(Object.values(ID_PREFIXES));
3034
+
3035
+ // ../shared/dist/dates.js
3036
+ var HOURS = 60 * 60;
3037
+ var DAYS = 24 * 60 * 60;
3038
+
3039
+ // ../shared/dist/origins.js
3040
+ function normalizeOrigin(value) {
3041
+ try {
3042
+ const url = new URL(value.trim());
3043
+ if (url.protocol !== "http:" && url.protocol !== "https:")
3044
+ return void 0;
3045
+ return url.origin;
3046
+ } catch {
3047
+ return void 0;
3048
+ }
3049
+ }
3050
+
3051
+ // ../shared/dist/plans.js
3052
+ var GB = 1024 ** 3;
3053
+ var MB = 1024 ** 2;
3054
+ var UNLIMITED = Number.MAX_SAFE_INTEGER;
3055
+ var PLANS = {
3056
+ free: {
3057
+ key: "free",
3058
+ priceEurMonthly: 0,
3059
+ priceEurYearly: 0,
3060
+ maxProjects: 2,
3061
+ maxMembers: 2,
3062
+ storageBytes: 1 * GB,
3063
+ monthlyBandwidthBytes: 25 * GB,
3064
+ monthlyPublicApiRequests: 1e4,
3065
+ maxWebhookEndpoints: 1,
3066
+ revisionRetentionDays: 30,
3067
+ maxUploadBytes: 50 * MB
3068
+ },
3069
+ pro: {
3070
+ key: "pro",
3071
+ priceEurMonthly: 9,
3072
+ priceEurYearly: 90,
3073
+ maxProjects: 10,
3074
+ maxMembers: 5,
3075
+ storageBytes: 10 * GB,
3076
+ monthlyBandwidthBytes: 250 * GB,
3077
+ monthlyPublicApiRequests: 5e5,
3078
+ maxWebhookEndpoints: 10,
3079
+ revisionRetentionDays: null,
3080
+ maxUploadBytes: 250 * MB
3081
+ },
3082
+ /**
3083
+ * Myna's own organizations — the changelog, and anything else we run on the
3084
+ * product we sell. It is never sold, never offered at checkout, and cannot be
3085
+ * reached through the API: `scripts/bootstrap-internal-org.mjs` writes it
3086
+ * directly, and billing refuses to move an organization on or off it.
3087
+ */
3088
+ internal: {
3089
+ key: "internal",
3090
+ priceEurMonthly: 0,
3091
+ priceEurYearly: null,
3092
+ maxProjects: UNLIMITED,
3093
+ maxMembers: UNLIMITED,
3094
+ storageBytes: UNLIMITED,
3095
+ monthlyBandwidthBytes: UNLIMITED,
3096
+ monthlyPublicApiRequests: UNLIMITED,
3097
+ maxWebhookEndpoints: UNLIMITED,
3098
+ revisionRetentionDays: null,
3099
+ maxUploadBytes: UNLIMITED
3100
+ }
3101
+ };
3102
+
3103
+ // ../shared/dist/rank.js
3104
+ var DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3105
+ var MIN_DIGIT = DIGITS[0];
3106
+ var LAST_INDEX = DIGITS.length - 1;
3107
+ var MID_DIGIT = DIGITS[Math.floor(DIGITS.length / 2)];
3108
+
3109
+ // ../shared/dist/release.js
3110
+ function parseSemVer(version) {
3111
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
3112
+ if (!match)
3113
+ return void 0;
3114
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
3115
+ }
3116
+ function compareSemVer(a, b) {
3117
+ const left = parseSemVer(a);
3118
+ const right = parseSemVer(b);
3119
+ if (!left || !right)
3120
+ throw new Error(`Cannot compare versions "${a}" and "${b}".`);
3121
+ return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
3122
+ }
3123
+
3124
+ // ../shared/dist/lock.js
3125
+ var LOCKFILE_NAME3 = "myna.lock";
3126
+ function branchForRelease(project, release) {
3127
+ const slug = project.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "project";
3128
+ return `myna/content-${slug}-r${release}`;
3129
+ }
3130
+ var MAX_DETAILED_RELEASES = 20;
3131
+ function releasePullRequestTitle(input) {
3132
+ return input.rollback ? `chore(content): roll ${input.project} back to release #${input.to.number}` : `chore(content): ${input.project} release #${input.to.number} \u2014 ${input.to.title}`;
3133
+ }
3134
+ function releasePullRequestBody(input) {
3135
+ const { project, from, to, releases, rollback } = input;
3136
+ const lines = [
3137
+ from === null ? `Pins \`${LOCKFILE_NAME3}\` for \`${project}\` to release #${to.number}.` : rollback ? `Rolls \`${LOCKFILE_NAME3}\` for \`${project}\` back from release #${from} to #${to.number}.` : `Moves \`${LOCKFILE_NAME3}\` for \`${project}\` from release #${from} to #${to.number}.`,
3138
+ "",
3139
+ rollback ? `${releases.length} release(s) are being undone for this repository. The content itself is untouched \u2014 nothing is unpublished \u2014 this only changes which release the build reads.` : `${releases.length} release(s). The build reads content at this release, so CI on this pull request is running against exactly the content that will ship.`
3140
+ ];
3141
+ const detailed = releases.slice(-MAX_DETAILED_RELEASES);
3142
+ if (detailed.length < releases.length) {
3143
+ const omitted = releases.length - detailed.length;
3144
+ lines.push("", `_Showing the newest ${detailed.length} of ${releases.length} releases. The remaining ${omitted} are listed without field detail._`, "", ...releases.slice(0, omitted).map((r) => `- #${r.number} \u2014 ${r.title} (${r.itemCount ?? 0} item(s))`));
3145
+ }
3146
+ for (const release of detailed) {
3147
+ lines.push("", `### #${release.number} \u2014 ${release.title}`);
3148
+ if (release.description)
3149
+ lines.push("", release.description);
3150
+ if (!release.diff) {
3151
+ lines.push("", `_Diff unavailable; ${release.itemCount ?? 0} item(s) changed._`);
3152
+ continue;
3153
+ }
3154
+ for (const item of release.diff.items) {
3155
+ const name = item.collection && item.slug ? `${item.collection}/${item.slug}` : item.resourceId;
3156
+ lines.push("", `- **${item.operation} ${name}**`);
3157
+ if (item.changeSummary)
3158
+ lines.push(` - _${item.changeSummary}_`);
3159
+ for (const change of item.changes)
3160
+ lines.push(` - \`${change.path}\` ${change.kind}`);
3161
+ }
3162
+ }
3163
+ return lines.join("\n");
3164
+ }
3165
+
3166
+ // src/commands/pull.ts
3167
+ function lockPath(ctx) {
3168
+ const cwd = ctx.linkedRoot ?? process.cwd();
3169
+ const found = findLockfile(cwd);
3170
+ if (found) {
3171
+ const boundary = repositoryRoot(cwd);
3172
+ if (!boundary || found.startsWith(boundary)) return found;
3173
+ }
3174
+ return join8(cwd, LOCKFILE_NAME2);
3175
+ }
3176
+ function assertOneLockfile(cwd, active) {
3177
+ const all = trackedLockfiles(cwd);
3178
+ if (all.length <= 1) return;
3179
+ throw new UsageError(
3180
+ `This repository has ${all.length} lockfiles:
3181
+ ` + all.map((f) => ` ${f}${f === active ? " (the one this would move)" : ""}`).join("\n") + `
3182
+ Reads take the one nearest the build, which may not be that one. Delete the extras and keep a single ${LOCKFILE_NAME2}.`
3183
+ );
3184
+ }
3185
+ function readLockAt(file) {
3186
+ if (!existsSync6(file)) return void 0;
3187
+ return parseLock2(readFileSync5(file, "utf8"), file);
3188
+ }
3189
+ async function latestRelease(ctx, project) {
3190
+ const page = await ctx.management().releases.list(project, { limit: 1 });
3191
+ const latest = page.data[0];
3192
+ if (!latest) {
3193
+ throw new CliError(
3194
+ `Project ${project} has not published a release yet. There is nothing to pin \u2014 publish a change set first.`
3195
+ );
3196
+ }
3197
+ return latest;
3198
+ }
3199
+ async function releasesBetween(ctx, project, from, to) {
3200
+ const found = [];
3201
+ let cursor;
3202
+ for (; ; ) {
3203
+ const page = await ctx.management().releases.list(project, { limit: 50, ...cursor ? { cursor } : {} });
3204
+ for (const release of page.data) {
3205
+ if (release.number > to) continue;
3206
+ if (release.number <= from) return found.reverse();
3207
+ found.push(release);
3208
+ }
3209
+ if (!page.nextCursor) return found.reverse();
3210
+ cursor = page.nextCursor;
3211
+ }
3212
+ }
3213
+ async function pullRequestText(ctx, project, from, to, releases, rollback) {
3214
+ const summaries = await Promise.all(
3215
+ releases.map(async (release) => ({
3216
+ number: release.number,
3217
+ title: release.title,
3218
+ description: release.description,
3219
+ itemCount: release.itemCount,
3220
+ // A release whose diff cannot be read is still a release worth naming.
3221
+ diff: await ctx.management().changeSets.diff(project, release.id).catch(() => null)
3222
+ }))
3223
+ );
3224
+ const input = {
3225
+ project,
3226
+ from: from?.release ?? null,
3227
+ to: { number: to.number, title: to.title, description: to.description, itemCount: to.itemCount },
3228
+ releases: summaries,
3229
+ rollback
3230
+ };
3231
+ return { title: releasePullRequestTitle(input), body: releasePullRequestBody(input) };
3232
+ }
3233
+ function registerPull(program) {
3234
+ program.command("pull").description(`Pin this repository's content to a release in ${LOCKFILE_NAME2}`).option("--release <n>", "release number to pin to (default: the newest)").option("--check", "report whether the pin is behind and exit non-zero if it is", false).option("--open-pr", "commit the bump onto a branch and open a pull request", false).option(
3235
+ "--run <command>",
3236
+ "shell command to run after moving the pin; anything it changes is committed with the lockfile"
3237
+ ).option("--branch <name>", "branch to use with --open-pr (default: derived from the release)").option("--base <branch>", "branch to open the pull request against (default: the current branch)").action(
3238
+ handle(async (ctx, _args, opts) => {
3239
+ const project = ctx.requireProject();
3240
+ const file = lockPath(ctx);
3241
+ assertOneLockfile(ctx.linkedRoot ?? process.cwd(), file);
3242
+ const current = readLockAt(file);
3243
+ if (current && current.project !== project) {
3244
+ throw new UsageError(
3245
+ `${LOCKFILE_NAME2} pins project "${current.project}" but the selected project is "${project}". Pass --project ${current.project}, or delete the lockfile.`
3246
+ );
3247
+ }
3248
+ const target = opts.release ? await ctx.management().releases.get(project, String(opts.release)) : await latestRelease(ctx, project);
3249
+ if (opts.check) {
3250
+ if (!current) {
3251
+ throw new UsageError(`No ${LOCKFILE_NAME2} here. Run \`myna pull\` to create one.`);
3252
+ }
3253
+ const behind = await releasesBetween(ctx, project, current.release, target.number);
3254
+ emit(
3255
+ { lockfile: file, release: current.release, latest: target.number, behind: behind.map((r) => r.number) },
3256
+ () => {
3257
+ if (behind.length === 0) {
3258
+ diag(`${LOCKFILE_NAME2} is at release #${current.release} \u2014 current.`);
3259
+ return;
3260
+ }
3261
+ diag(`${LOCKFILE_NAME2} is at release #${current.release}; #${target.number} is available.`);
3262
+ table(behind, [
3263
+ { header: "#", value: (r) => String(r.number) },
3264
+ { header: "TITLE", value: (r) => r.title },
3265
+ { header: "PUBLISHED", value: (r) => r.publishedAt },
3266
+ { header: "ITEMS", value: (r) => String(r.itemCount) }
3267
+ ]);
3268
+ }
3269
+ );
3270
+ if (behind.length > 0) process.exitCode = 1;
3271
+ return;
3272
+ }
3273
+ const next = {
3274
+ project,
3275
+ release: target.number,
3276
+ publishedAt: target.publishedAt,
3277
+ title: target.title,
3278
+ ...ctx.apiUrl && !ctx.apiUrl.startsWith("https://api.myna.sh") ? { apiUrl: ctx.apiUrl } : {}
3279
+ };
3280
+ if (current && formatLock2(current) === formatLock2(next)) {
3281
+ emit(
3282
+ { lockfile: file, release: target.number, changed: false, pullRequest: null },
3283
+ () => diag(`${LOCKFILE_NAME2} is already at release #${target.number}.`)
3284
+ );
3285
+ return;
3286
+ }
3287
+ const rollback = current !== void 0 && target.number < current.release;
3288
+ const releases = rollback ? await releasesBetween(ctx, project, target.number, current.release) : await releasesBetween(ctx, project, current?.release ?? 0, target.number);
3289
+ if (!opts.openPr) {
3290
+ writeLock(file, next);
3291
+ if (opts.run) execSync(opts.run, { cwd: ctx.linkedRoot ?? process.cwd(), stdio: "inherit" });
3292
+ emit(
3293
+ {
3294
+ lockfile: file,
3295
+ release: target.number,
3296
+ from: current?.release ?? null,
3297
+ changed: true,
3298
+ rollback,
3299
+ releases: releases.map((r) => r.number)
3300
+ },
3301
+ () => {
3302
+ if (!current) {
3303
+ diag(`${LOCKFILE_NAME2}: pinned to #${target.number}.`);
3304
+ return;
3305
+ }
3306
+ diag(
3307
+ rollback ? `${LOCKFILE_NAME2}: #${current.release} \u2192 #${target.number}, rolling back ${releases.length} release(s).` : `${LOCKFILE_NAME2}: #${current.release} \u2192 #${target.number} (${releases.length} release(s)).`
3308
+ );
3309
+ }
3310
+ );
3311
+ return;
3312
+ }
3313
+ const root = ctx.linkedRoot ?? process.cwd();
3314
+ const command = opts.run;
3315
+ const dirty = command ? worktreeChanges(root).map((c) => c.file) : dirtyFiles(root, [file]);
3316
+ if (dirty.length > 0) {
3317
+ throw new UsageError(`Commit or stash ${dirty.join(", ")} first.`);
3318
+ }
3319
+ const text = await pullRequestText(ctx, project, current, target, releases, rollback);
3320
+ writeLock(file, next);
3321
+ if (command) {
3322
+ try {
3323
+ execSync(command, { cwd: root, stdio: "inherit" });
3324
+ } catch (error) {
3325
+ discardChanges(root, worktreeChanges(root));
3326
+ throw new CliError(`--run command failed: ${error.message}`);
3327
+ }
3328
+ }
3329
+ const changes = worktreeChanges(root);
3330
+ const branch = opts.branch ?? branchForRelease(project, target.number);
3331
+ let result;
3332
+ try {
3333
+ result = openPullRequest({
3334
+ cwd: root,
3335
+ files: changes.map((c) => c.file),
3336
+ branch,
3337
+ base: opts.base,
3338
+ title: text.title,
3339
+ body: text.body
3340
+ });
3341
+ } catch (error) {
3342
+ discardChanges(root, changes);
3343
+ throw error;
3344
+ }
3345
+ if (!result.opened) discardChanges(root, changes);
3346
+ emit({ lockfile: file, release: target.number, from: current?.release ?? null, releases: releases.map((r) => r.number), pullRequest: result }, () => {
3347
+ if (!result.opened) {
3348
+ diag(`Branch ${result.branch} already exists \u2014 release #${target.number} is already proposed.`);
3349
+ return;
3350
+ }
3351
+ diag(`Opened ${result.url ?? `pull request from ${result.branch}`} against ${result.base}.`);
3352
+ });
3353
+ })
3354
+ );
3355
+ }
3356
+
2857
3357
  // src/commands/previews.ts
2858
3358
  function previewTarget(ref) {
2859
3359
  if (ref.startsWith("chs_")) return { changeSetId: ref };
@@ -3024,139 +3524,6 @@ function registerAssets(program) {
3024
3524
  );
3025
3525
  }
3026
3526
 
3027
- // ../shared/dist/ids.js
3028
- var ID_PREFIXES = {
3029
- user: "usr",
3030
- oauthAccount: "oau",
3031
- session: "ses",
3032
- organization: "org",
3033
- organizationMember: "mem",
3034
- organizationInvitation: "inv",
3035
- project: "prj",
3036
- projectOrigin: "por",
3037
- apiKey: "key",
3038
- collection: "col",
3039
- collectionVersion: "clv",
3040
- entry: "ent",
3041
- entrySlugAlias: "als",
3042
- entryRevision: "rev",
3043
- changeSet: "chs",
3044
- changeSetItem: "csi",
3045
- changeSetReview: "csr",
3046
- changeSetComment: "csc",
3047
- changeSetCheck: "chk",
3048
- projectCheck: "pck",
3049
- asset: "ast",
3050
- assetUpload: "upl",
3051
- savedView: "viw",
3052
- previewToken: "prv",
3053
- webhookEndpoint: "whk",
3054
- webhookEvent: "whe",
3055
- webhookDelivery: "whd",
3056
- auditEvent: "aud",
3057
- job: "job",
3058
- idempotencyKey: "idm",
3059
- billingCustomer: "bcu",
3060
- billingSubscription: "bsu",
3061
- billingProviderEvent: "evt",
3062
- usagePeriod: "usp",
3063
- mcpClient: "mcl",
3064
- mcpGrant: "mgr",
3065
- mcpAuthorizationCode: "mac",
3066
- mcpToken: "mtk",
3067
- deviceAuthorization: "dev"
3068
- };
3069
- var PREFIX_SET = new Set(Object.values(ID_PREFIXES));
3070
-
3071
- // ../shared/dist/dates.js
3072
- var HOURS = 60 * 60;
3073
- var DAYS = 24 * 60 * 60;
3074
-
3075
- // ../shared/dist/origins.js
3076
- function normalizeOrigin(value) {
3077
- try {
3078
- const url = new URL(value.trim());
3079
- if (url.protocol !== "http:" && url.protocol !== "https:")
3080
- return void 0;
3081
- return url.origin;
3082
- } catch {
3083
- return void 0;
3084
- }
3085
- }
3086
-
3087
- // ../shared/dist/plans.js
3088
- var GB = 1024 ** 3;
3089
- var MB = 1024 ** 2;
3090
- var UNLIMITED = Number.MAX_SAFE_INTEGER;
3091
- var PLANS = {
3092
- free: {
3093
- key: "free",
3094
- priceEurMonthly: 0,
3095
- priceEurYearly: 0,
3096
- maxProjects: 2,
3097
- maxMembers: 2,
3098
- storageBytes: 1 * GB,
3099
- monthlyBandwidthBytes: 25 * GB,
3100
- monthlyPublicApiRequests: 1e4,
3101
- maxWebhookEndpoints: 1,
3102
- revisionRetentionDays: 30,
3103
- maxUploadBytes: 50 * MB
3104
- },
3105
- pro: {
3106
- key: "pro",
3107
- priceEurMonthly: 9,
3108
- priceEurYearly: 90,
3109
- maxProjects: 10,
3110
- maxMembers: 5,
3111
- storageBytes: 10 * GB,
3112
- monthlyBandwidthBytes: 250 * GB,
3113
- monthlyPublicApiRequests: 5e5,
3114
- maxWebhookEndpoints: 10,
3115
- revisionRetentionDays: null,
3116
- maxUploadBytes: 250 * MB
3117
- },
3118
- /**
3119
- * Myna's own organizations — the changelog, and anything else we run on the
3120
- * product we sell. It is never sold, never offered at checkout, and cannot be
3121
- * reached through the API: `scripts/bootstrap-internal-org.mjs` writes it
3122
- * directly, and billing refuses to move an organization on or off it.
3123
- */
3124
- internal: {
3125
- key: "internal",
3126
- priceEurMonthly: 0,
3127
- priceEurYearly: null,
3128
- maxProjects: UNLIMITED,
3129
- maxMembers: UNLIMITED,
3130
- storageBytes: UNLIMITED,
3131
- monthlyBandwidthBytes: UNLIMITED,
3132
- monthlyPublicApiRequests: UNLIMITED,
3133
- maxWebhookEndpoints: UNLIMITED,
3134
- revisionRetentionDays: null,
3135
- maxUploadBytes: UNLIMITED
3136
- }
3137
- };
3138
-
3139
- // ../shared/dist/rank.js
3140
- var DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3141
- var MIN_DIGIT = DIGITS[0];
3142
- var LAST_INDEX = DIGITS.length - 1;
3143
- var MID_DIGIT = DIGITS[Math.floor(DIGITS.length / 2)];
3144
-
3145
- // ../shared/dist/release.js
3146
- function parseSemVer(version) {
3147
- const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
3148
- if (!match)
3149
- return void 0;
3150
- return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
3151
- }
3152
- function compareSemVer(a, b) {
3153
- const left = parseSemVer(a);
3154
- const right = parseSemVer(b);
3155
- if (!left || !right)
3156
- throw new Error(`Cannot compare versions "${a}" and "${b}".`);
3157
- return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
3158
- }
3159
-
3160
3527
  // src/commands/admin.ts
3161
3528
  function csv(value) {
3162
3529
  return value.split(",").map((s) => s.trim()).filter(Boolean);
@@ -3698,8 +4065,8 @@ function registerBilling(program) {
3698
4065
  }
3699
4066
 
3700
4067
  // src/commands/doctor.ts
3701
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3702
- import { join as join6 } from "path";
4068
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
4069
+ import { join as join9 } from "path";
3703
4070
 
3704
4071
  // src/registry.ts
3705
4072
  var NPM_REGISTRY = "https://registry.npmjs.org";
@@ -3738,7 +4105,7 @@ function installCommand(manager, version) {
3738
4105
  }
3739
4106
 
3740
4107
  // src/version.ts
3741
- var VERSION = true ? "0.11.0" : "0.0.0-dev";
4108
+ var VERSION = true ? "0.12.1" : "0.0.0-dev";
3742
4109
  var IS_RELEASE_BUILD = true;
3743
4110
 
3744
4111
  // src/commands/doctor.ts
@@ -3912,7 +4279,7 @@ async function checkOrigin(ctx, origin) {
3912
4279
  }
3913
4280
  async function checkSchema(ctx, schemaDir) {
3914
4281
  const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
3915
- if (!existsSync5(dir)) {
4282
+ if (!existsSync7(dir)) {
3916
4283
  return check("schema.drift", "Local schema", "skip", `No schema directory at ${dir}.`);
3917
4284
  }
3918
4285
  if (!ctx.project) {
@@ -3962,7 +4329,7 @@ function findGeneratedTypes(root, depth = 4) {
3962
4329
  const dirs = [];
3963
4330
  for (const entry of entries) {
3964
4331
  if (SCAN_IGNORE.has(entry) || entry.startsWith(".")) continue;
3965
- const full = join6(root, entry);
4332
+ const full = join9(root, entry);
3966
4333
  let stats;
3967
4334
  try {
3968
4335
  stats = statSync2(full);
@@ -3975,7 +4342,7 @@ function findGeneratedTypes(root, depth = 4) {
3975
4342
  }
3976
4343
  if (!/\.(ts|d\.ts)$/.test(entry)) continue;
3977
4344
  try {
3978
- if (looksGenerated(readFileSync4(full, "utf8"))) return full;
4345
+ if (looksGenerated(readFileSync6(full, "utf8"))) return full;
3979
4346
  } catch {
3980
4347
  }
3981
4348
  }
@@ -3992,16 +4359,16 @@ async function checkTypes(ctx, explicit, schemaDir) {
3992
4359
  if (!file) {
3993
4360
  return check("types.freshness", "Generated types", "skip", "No generated types file found.");
3994
4361
  }
3995
- if (!existsSync5(file)) {
4362
+ if (!existsSync7(file)) {
3996
4363
  return check("types.freshness", "Generated types", "fail", `${file} does not exist.`);
3997
4364
  }
3998
4365
  const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
3999
- if (!existsSync5(dir)) {
4366
+ if (!existsSync7(dir)) {
4000
4367
  return check("types.freshness", "Generated types", "skip", `Found ${file} but no schema directory to compare against.`);
4001
4368
  }
4002
4369
  try {
4003
4370
  const expected = generateTypesModule(await loadLocalSchemas(dir));
4004
- return readFileSync4(file, "utf8") === expected ? check("types.freshness", "Generated types", "pass", `${file} matches the local schema.`) : check(
4371
+ return readFileSync6(file, "utf8") === expected ? check("types.freshness", "Generated types", "pass", `${file} matches the local schema.`) : check(
4005
4372
  "types.freshness",
4006
4373
  "Generated types",
4007
4374
  "warn",
@@ -4117,7 +4484,7 @@ function registerUpdate(program) {
4117
4484
 
4118
4485
  // src/commands/sync.ts
4119
4486
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
4120
- import { join as join7 } from "path";
4487
+ import { join as join10 } from "path";
4121
4488
  async function isDirectory(path) {
4122
4489
  const info = await stat2(path).catch(() => void 0);
4123
4490
  return Boolean(info?.isDirectory());
@@ -4132,7 +4499,7 @@ async function planDirectories(dir, collection) {
4132
4499
  const plan = [];
4133
4500
  for (const name of children.sort()) {
4134
4501
  if (name.startsWith(".")) continue;
4135
- const full = join7(dir, name);
4502
+ const full = join10(dir, name);
4136
4503
  if (await isDirectory(full)) plan.push({ collection: name, path: full });
4137
4504
  }
4138
4505
  if (plan.length === 0) {
@@ -4303,6 +4670,7 @@ function buildProgram() {
4303
4670
  registerSync(program);
4304
4671
  registerChanges(program);
4305
4672
  registerReleases(program);
4673
+ registerPull(program);
4306
4674
  registerPreviews(program);
4307
4675
  registerAssets(program);
4308
4676
  registerAdmin(program);