@myna-sh/cli 0.7.0 → 0.9.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 +382 -21
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
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,
|
|
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 =
|
|
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)}`)
|
|
@@ -590,12 +600,40 @@ var ManagementClient = class {
|
|
|
590
600
|
body
|
|
591
601
|
),
|
|
592
602
|
/** Field-level before/after for everything this change set would change. */
|
|
603
|
+
/**
|
|
604
|
+
* Replay the change set's edits on top of what is published now.
|
|
605
|
+
*
|
|
606
|
+
* Refuses by default when a field changed on both sides; pass
|
|
607
|
+
* `onConflict` to take one side, or `dryRun` to see the plan first.
|
|
608
|
+
*/
|
|
609
|
+
rebase: (project, changeSet, body = {}) => this.mutate(
|
|
610
|
+
"POST",
|
|
611
|
+
`/projects/${enc(project)}/change-sets/${enc(changeSet)}/rebase`,
|
|
612
|
+
body
|
|
613
|
+
),
|
|
593
614
|
diff: (project, changeSet, signal) => this.get(
|
|
594
615
|
`/projects/${enc(project)}/change-sets/${enc(changeSet)}/diff`,
|
|
595
616
|
void 0,
|
|
596
617
|
signal
|
|
597
618
|
)
|
|
598
619
|
};
|
|
620
|
+
// --- Releases -------------------------------------------------------------
|
|
621
|
+
/**
|
|
622
|
+
* What shipped, and how to take it back.
|
|
623
|
+
*
|
|
624
|
+
* `revert` returns an open change set rather than publishing: it goes through
|
|
625
|
+
* the same review, preview, and gate path as anything written by hand.
|
|
626
|
+
*/
|
|
627
|
+
releases = {
|
|
628
|
+
list: (project, opts = {}) => this.page(`/projects/${enc(project)}/releases`, opts),
|
|
629
|
+
/** `release` is a release number or the published change set's id. */
|
|
630
|
+
get: (project, release, signal) => this.get(`/projects/${enc(project)}/releases/${enc(String(release))}`, void 0, signal),
|
|
631
|
+
revert: (project, release, body = {}) => this.mutate(
|
|
632
|
+
"POST",
|
|
633
|
+
`/projects/${enc(project)}/releases/${enc(String(release))}/revert`,
|
|
634
|
+
body
|
|
635
|
+
)
|
|
636
|
+
};
|
|
599
637
|
// --- Declared external checks ---------------------------------------------
|
|
600
638
|
checks = {
|
|
601
639
|
list: (project, signal) => this.get(`/projects/${enc(project)}/checks`, void 0, signal),
|
|
@@ -621,6 +659,19 @@ var ManagementClient = class {
|
|
|
621
659
|
}),
|
|
622
660
|
get: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, void 0, signal),
|
|
623
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),
|
|
624
675
|
update: (project, asset, body) => this.mutate("PATCH", `/projects/${enc(project)}/assets/${enc(asset)}`, body),
|
|
625
676
|
replace: (project, asset, body) => this.mutate("POST", `/projects/${enc(project)}/assets/${enc(asset)}/replace`, body),
|
|
626
677
|
delete: (project, asset) => this.mutate("DELETE", `/projects/${enc(project)}/assets/${enc(asset)}`),
|
|
@@ -1166,7 +1217,33 @@ function startDevice(apiUrl) {
|
|
|
1166
1217
|
function pollDevice(apiUrl, deviceCode) {
|
|
1167
1218
|
return apiPost(apiUrl, `/auth/device/${deviceCode}/token`, { deviceCode });
|
|
1168
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
|
+
}
|
|
1169
1245
|
function registerAuth(program) {
|
|
1246
|
+
registerAccess(program);
|
|
1170
1247
|
const login = program.command("login").description("Authenticate via the browser device-authorization flow").action(
|
|
1171
1248
|
handle(async (ctx) => {
|
|
1172
1249
|
const start = await startDevice(ctx.apiUrl);
|
|
@@ -1455,7 +1532,7 @@ function registerWorkspace(program) {
|
|
|
1455
1532
|
}
|
|
1456
1533
|
|
|
1457
1534
|
// src/commands/schema.ts
|
|
1458
|
-
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";
|
|
1459
1536
|
import { join as join4 } from "path";
|
|
1460
1537
|
|
|
1461
1538
|
// src/schema-loader.ts
|
|
@@ -1689,6 +1766,89 @@ function varName(name) {
|
|
|
1689
1766
|
return /^[A-Za-z_$]/.test(camel) ? camel : `_${camel}`;
|
|
1690
1767
|
}
|
|
1691
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
|
+
|
|
1692
1852
|
// src/commands/schema.ts
|
|
1693
1853
|
async function deployedSchemas(ctx, project) {
|
|
1694
1854
|
const mgmt = ctx.management();
|
|
@@ -1734,21 +1894,59 @@ function registerSchema(program) {
|
|
|
1734
1894
|
});
|
|
1735
1895
|
})
|
|
1736
1896
|
);
|
|
1737
|
-
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(
|
|
1738
1898
|
handle(async (ctx, _args, opts) => {
|
|
1739
1899
|
const project = ctx.requireProject();
|
|
1740
1900
|
const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
|
|
1741
1901
|
const schemas = await deployedSchemas(ctx, project);
|
|
1742
|
-
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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}
|
|
1751
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}.`);
|
|
1752
1950
|
});
|
|
1753
1951
|
})
|
|
1754
1952
|
);
|
|
@@ -1866,6 +2064,29 @@ function registerSchema(program) {
|
|
|
1866
2064
|
})
|
|
1867
2065
|
);
|
|
1868
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
|
+
}
|
|
1869
2090
|
function renderDiff(diff) {
|
|
1870
2091
|
if (diff.ops.length === 0) {
|
|
1871
2092
|
diag("No changes.");
|
|
@@ -2454,11 +2675,46 @@ function registerChanges(program) {
|
|
|
2454
2675
|
{ header: "BEFORE", value: (c) => preview(c.before) },
|
|
2455
2676
|
{ header: "AFTER", value: (c) => preview(c.after) }
|
|
2456
2677
|
]);
|
|
2678
|
+
for (const change of item.changes) {
|
|
2679
|
+
if (change.segments) diag(` ${change.path}: ${inlineWords(change.segments)}`);
|
|
2680
|
+
}
|
|
2457
2681
|
}
|
|
2458
2682
|
if (diff.items.length === 0) diag("No items.");
|
|
2459
2683
|
});
|
|
2460
2684
|
})
|
|
2461
2685
|
);
|
|
2686
|
+
changes.command("rebase").description("Replay a change set's edits on top of what is published now").argument("<id>", "change set id").option("--dry-run", "show what a rebase would do without writing", false).option("--take-mine", "on conflict, keep this change set's value", false).option("--take-theirs", "on conflict, accept what was published", false).action(
|
|
2687
|
+
handle(async (ctx, args, opts) => {
|
|
2688
|
+
const project = ctx.requireProject();
|
|
2689
|
+
if (opts.takeMine && opts.takeTheirs) {
|
|
2690
|
+
throw new UsageError("Provide at most one of --take-mine or --take-theirs.");
|
|
2691
|
+
}
|
|
2692
|
+
const result = await ctx.management().changeSets.rebase(project, args[0], {
|
|
2693
|
+
...opts.dryRun ? { dryRun: true } : {},
|
|
2694
|
+
...opts.takeMine ? { onConflict: "take-mine" } : {},
|
|
2695
|
+
...opts.takeTheirs ? { onConflict: "take-theirs" } : {}
|
|
2696
|
+
});
|
|
2697
|
+
emit(result, () => {
|
|
2698
|
+
if (result.items.length === 0) {
|
|
2699
|
+
diag(`Nothing to rebase; ${result.upToDate.length} item(s) already on the current head.`);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
for (const item of result.items) {
|
|
2703
|
+
const label = item.collection && item.slug ? `${item.collection}/${item.slug}` : item.entryId;
|
|
2704
|
+
diag(`${label} \u2014 ${item.appliedPaths.length} field(s) replayed, ${item.conflicts.length} conflict(s)`);
|
|
2705
|
+
if (item.conflicts.length > 0) {
|
|
2706
|
+
table(item.conflicts, [
|
|
2707
|
+
{ header: "FIELD", value: (c) => c.path },
|
|
2708
|
+
{ header: "MINE", value: (c) => preview(c.mine) },
|
|
2709
|
+
{ header: "THEIRS", value: (c) => preview(c.theirs) }
|
|
2710
|
+
]);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
diag(result.dryRun ? "Dry run \u2014 nothing written." : "Rebased. Approvals are now stale; re-review.");
|
|
2714
|
+
});
|
|
2715
|
+
if (result.items.some((i) => i.conflicts.length > 0) && result.dryRun) process.exitCode = 1;
|
|
2716
|
+
})
|
|
2717
|
+
);
|
|
2462
2718
|
changes.command("update").description("Retitle a change set or mark it ready for review").argument("<id>", "change set id").option("--title <title>", "new title").option("--description <text>", "new description").option("--ready", "mark ready for review", false).action(
|
|
2463
2719
|
handle(async (ctx, args, opts) => {
|
|
2464
2720
|
const project = ctx.requireProject();
|
|
@@ -2503,6 +2759,15 @@ function registerChanges(program) {
|
|
|
2503
2759
|
})
|
|
2504
2760
|
);
|
|
2505
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
|
+
}
|
|
2506
2771
|
function preview(value) {
|
|
2507
2772
|
if (value === void 0 || value === null) return "\u2014";
|
|
2508
2773
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
@@ -2510,6 +2775,75 @@ function preview(value) {
|
|
|
2510
2775
|
return flat.length > 48 ? `${flat.slice(0, 47)}\u2026` : flat;
|
|
2511
2776
|
}
|
|
2512
2777
|
|
|
2778
|
+
// src/commands/releases.ts
|
|
2779
|
+
function registerReleases(program) {
|
|
2780
|
+
const releases = program.command("releases").description("Inspect what shipped and generate a revert");
|
|
2781
|
+
releases.command("list").description("List releases, newest first").option("--limit <n>", "page size", "25").action(
|
|
2782
|
+
handle(async (ctx, _args, opts) => {
|
|
2783
|
+
const project = ctx.requireProject();
|
|
2784
|
+
const page = await ctx.management().releases.list(project, { limit: Number(opts.limit) });
|
|
2785
|
+
emit(
|
|
2786
|
+
{ data: page.data, nextCursor: page.nextCursor },
|
|
2787
|
+
() => table(page.data, [
|
|
2788
|
+
{ header: "#", value: (r) => String(r.number) },
|
|
2789
|
+
{ header: "TITLE", value: (r) => r.title },
|
|
2790
|
+
{ header: "PUBLISHED", value: (r) => r.publishedAt },
|
|
2791
|
+
{ header: "BY", value: (r) => r.publishedBy?.displayName ?? "\u2014" },
|
|
2792
|
+
{ header: "ITEMS", value: (r) => String(r.itemCount) },
|
|
2793
|
+
{
|
|
2794
|
+
header: "STATE",
|
|
2795
|
+
value: (r) => r.revertedBy ? r.revertedBy.status === "published" ? `reverted by #${r.revertedBy.releaseNumber ?? "?"}` : `revert ${r.revertedBy.status}` : r.revertsReleaseNumber ? `reverts #${r.revertsReleaseNumber}` : "live"
|
|
2796
|
+
}
|
|
2797
|
+
])
|
|
2798
|
+
);
|
|
2799
|
+
})
|
|
2800
|
+
);
|
|
2801
|
+
releases.command("inspect").description("Show one release").argument("<release>", "release number or published change set id").action(
|
|
2802
|
+
handle(async (ctx, args) => {
|
|
2803
|
+
const project = ctx.requireProject();
|
|
2804
|
+
const release = await ctx.management().releases.get(project, args[0]);
|
|
2805
|
+
emit(release, () => {
|
|
2806
|
+
diag(`#${release.number} \u2014 ${release.title}`);
|
|
2807
|
+
diag(`published ${release.publishedAt} by ${release.publishedBy?.displayName ?? "unknown"}`);
|
|
2808
|
+
diag(`${release.itemCount} item(s), change set ${release.id}`);
|
|
2809
|
+
if (release.revertsReleaseNumber) diag(`reverts release #${release.revertsReleaseNumber}`);
|
|
2810
|
+
if (release.revertedBy) {
|
|
2811
|
+
diag(`revert ${release.revertedBy.status}: change set ${release.revertedBy.changeSetId}`);
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
})
|
|
2815
|
+
);
|
|
2816
|
+
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(
|
|
2817
|
+
handle(async (ctx, args, opts) => {
|
|
2818
|
+
const project = ctx.requireProject();
|
|
2819
|
+
const result = await ctx.management().releases.revert(project, args[0], {
|
|
2820
|
+
...opts.title ? { title: opts.title } : {},
|
|
2821
|
+
...opts.force ? { force: true } : {}
|
|
2822
|
+
});
|
|
2823
|
+
emit(result, () => {
|
|
2824
|
+
diag(`Created change set ${result.changeSet.id} \u2014 ${result.changeSet.title}`);
|
|
2825
|
+
table(result.reverted, [
|
|
2826
|
+
{ header: "OP", value: (r) => r.operation },
|
|
2827
|
+
{
|
|
2828
|
+
header: "RESOURCE",
|
|
2829
|
+
value: (r) => r.collection && r.slug ? `${r.collection}/${r.slug}` : r.resourceId
|
|
2830
|
+
},
|
|
2831
|
+
{ header: "RESTORES", value: (r) => r.restoredFromRevisionId ?? "\u2014" }
|
|
2832
|
+
]);
|
|
2833
|
+
if (result.skipped.length > 0) {
|
|
2834
|
+
diag(`${result.skipped.length} resource(s) skipped:`);
|
|
2835
|
+
for (const s of result.skipped) {
|
|
2836
|
+
const label = s.collection && s.slug ? `${s.collection}/${s.slug}` : s.resourceId;
|
|
2837
|
+
process.stderr.write(` ${label} \u2014 ${s.reason}: ${s.message}
|
|
2838
|
+
`);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
diag(`Not published. Review, then: myna changes publish ${result.changeSet.id} --confirm-publish`);
|
|
2842
|
+
});
|
|
2843
|
+
})
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2513
2847
|
// src/commands/previews.ts
|
|
2514
2848
|
function previewTarget(ref) {
|
|
2515
2849
|
if (ref.startsWith("chs_")) return { changeSetId: ref };
|
|
@@ -2574,6 +2908,10 @@ function registerAssets(program) {
|
|
|
2574
2908
|
let asset = await ctx.management().assets.upload(project, path);
|
|
2575
2909
|
if (opts.alt) asset = await ctx.management().assets.update(project, asset.id, { defaultAlt: opts.alt });
|
|
2576
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
|
+
}
|
|
2577
2915
|
}
|
|
2578
2916
|
emit(
|
|
2579
2917
|
uploaded.length === 1 ? uploaded[0] : uploaded,
|
|
@@ -2605,6 +2943,26 @@ function registerAssets(program) {
|
|
|
2605
2943
|
);
|
|
2606
2944
|
})
|
|
2607
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
|
+
);
|
|
2608
2966
|
assets.command("inspect").description("Show an asset and its usage").argument("<id>", "asset id (ast_)").action(
|
|
2609
2967
|
handle(async (ctx, args) => {
|
|
2610
2968
|
const project = ctx.requireProject();
|
|
@@ -3330,11 +3688,11 @@ function registerBilling(program) {
|
|
|
3330
3688
|
}
|
|
3331
3689
|
|
|
3332
3690
|
// src/commands/doctor.ts
|
|
3333
|
-
import { existsSync as
|
|
3691
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3334
3692
|
import { join as join6 } from "path";
|
|
3335
3693
|
|
|
3336
3694
|
// src/version.ts
|
|
3337
|
-
var VERSION = true ? "0.
|
|
3695
|
+
var VERSION = true ? "0.9.0" : "0.0.0-dev";
|
|
3338
3696
|
|
|
3339
3697
|
// src/commands/doctor.ts
|
|
3340
3698
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
@@ -3521,7 +3879,7 @@ async function checkOrigin(ctx, origin) {
|
|
|
3521
3879
|
}
|
|
3522
3880
|
async function checkSchema(ctx, schemaDir) {
|
|
3523
3881
|
const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
|
|
3524
|
-
if (!
|
|
3882
|
+
if (!existsSync5(dir)) {
|
|
3525
3883
|
return check("schema.drift", "Local schema", "skip", `No schema directory at ${dir}.`);
|
|
3526
3884
|
}
|
|
3527
3885
|
if (!ctx.project) {
|
|
@@ -3548,7 +3906,9 @@ async function checkSchema(ctx, schemaDir) {
|
|
|
3548
3906
|
"Local schema",
|
|
3549
3907
|
"warn",
|
|
3550
3908
|
`${diff.ops.length} undeployed change(s) (${diff.classification}).`,
|
|
3551
|
-
|
|
3909
|
+
// Drift has two directions and two fixes: push local ahead, or pull
|
|
3910
|
+
// the deployed schema back into the repository as a reviewable change.
|
|
3911
|
+
"myna schema diff, then myna schema push \u2014 or myna schema pull --open-pr if the deployed schema is the one to keep"
|
|
3552
3912
|
);
|
|
3553
3913
|
} catch (error) {
|
|
3554
3914
|
return check(
|
|
@@ -3582,7 +3942,7 @@ function findGeneratedTypes(root, depth = 4) {
|
|
|
3582
3942
|
}
|
|
3583
3943
|
if (!/\.(ts|d\.ts)$/.test(entry)) continue;
|
|
3584
3944
|
try {
|
|
3585
|
-
if (looksGenerated(
|
|
3945
|
+
if (looksGenerated(readFileSync4(full, "utf8"))) return full;
|
|
3586
3946
|
} catch {
|
|
3587
3947
|
}
|
|
3588
3948
|
}
|
|
@@ -3599,16 +3959,16 @@ async function checkTypes(ctx, explicit, schemaDir) {
|
|
|
3599
3959
|
if (!file) {
|
|
3600
3960
|
return check("types.freshness", "Generated types", "skip", "No generated types file found.");
|
|
3601
3961
|
}
|
|
3602
|
-
if (!
|
|
3962
|
+
if (!existsSync5(file)) {
|
|
3603
3963
|
return check("types.freshness", "Generated types", "fail", `${file} does not exist.`);
|
|
3604
3964
|
}
|
|
3605
3965
|
const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
|
|
3606
|
-
if (!
|
|
3966
|
+
if (!existsSync5(dir)) {
|
|
3607
3967
|
return check("types.freshness", "Generated types", "skip", `Found ${file} but no schema directory to compare against.`);
|
|
3608
3968
|
}
|
|
3609
3969
|
try {
|
|
3610
3970
|
const expected = generateTypesModule(await loadLocalSchemas(dir));
|
|
3611
|
-
return
|
|
3971
|
+
return readFileSync4(file, "utf8") === expected ? check("types.freshness", "Generated types", "pass", `${file} matches the local schema.`) : check(
|
|
3612
3972
|
"types.freshness",
|
|
3613
3973
|
"Generated types",
|
|
3614
3974
|
"warn",
|
|
@@ -3856,6 +4216,7 @@ function buildProgram() {
|
|
|
3856
4216
|
registerEntries(program);
|
|
3857
4217
|
registerSync(program);
|
|
3858
4218
|
registerChanges(program);
|
|
4219
|
+
registerReleases(program);
|
|
3859
4220
|
registerPreviews(program);
|
|
3860
4221
|
registerAssets(program);
|
|
3861
4222
|
registerAdmin(program);
|