mindcraft-cli 0.1.2

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/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Run the `mindcraft` command line with `argv` (the arguments after the
3
+ * program name). Returns the process exit code.
4
+ */
5
+ export declare function runCli(argv: readonly string[]): Promise<number>;
6
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAarE"}
package/dist/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ import { runPublishCommand } from "./publish-command.js";
2
+ import { runUnpackCommand } from "./unpack-command.js";
3
+ import { runVersionCommand } from "./version-command.js";
4
+ const CLI_USAGE = `usage: mindcraft <command> [arguments]
5
+
6
+ commands:
7
+ publish publish a version of a Mindcraft project to GitHub
8
+ version increment a Mindcraft project's version in its mindcraft.json
9
+ unpack convert a .mindcraft export into a publishable project directory
10
+ `;
11
+ /**
12
+ * Run the `mindcraft` command line with `argv` (the arguments after the
13
+ * program name). Returns the process exit code.
14
+ */
15
+ export async function runCli(argv) {
16
+ const [command, ...rest] = argv;
17
+ if (command === "publish") {
18
+ return runPublishCommand(rest);
19
+ }
20
+ if (command === "version") {
21
+ return runVersionCommand(rest);
22
+ }
23
+ if (command === "unpack") {
24
+ return runUnpackCommand(rest);
25
+ }
26
+ process.stderr.write(command === undefined ? CLI_USAGE : `mindcraft: unknown command "${command}"\n${CLI_USAGE}`);
27
+ return 1;
28
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** Error thrown when a git command exits with a nonzero status. */
2
+ export declare class GitCommandError extends Error {
3
+ constructor(args: readonly string[], stderr: string);
4
+ }
5
+ /**
6
+ * Run a git command in `cwd` and return its stdout. Throws
7
+ * {@link GitCommandError} when git exits with a nonzero status.
8
+ */
9
+ export declare function git(cwd: string, ...args: string[]): Promise<string>;
10
+ /**
11
+ * Run a git command in `cwd` and return its stdout, or `undefined` when git
12
+ * exits with a nonzero status.
13
+ */
14
+ export declare function tryGit(cwd: string, ...args: string[]): Promise<string | undefined>;
15
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM;CAIpD;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAUnE;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CASxF"}
package/dist/git.js ADDED
@@ -0,0 +1,39 @@
1
+ import { execFile } from "node:child_process";
2
+ /** Error thrown when a git command exits with a nonzero status. */
3
+ export class GitCommandError extends Error {
4
+ constructor(args, stderr) {
5
+ super(`git ${args.join(" ")} failed: ${stderr.trim()}`);
6
+ this.name = "GitCommandError";
7
+ }
8
+ }
9
+ /**
10
+ * Run a git command in `cwd` and return its stdout. Throws
11
+ * {@link GitCommandError} when git exits with a nonzero status.
12
+ */
13
+ export function git(cwd, ...args) {
14
+ return new Promise((resolve, reject) => {
15
+ execFile("git", args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
16
+ if (error) {
17
+ reject(new GitCommandError(args, stderr || error.message));
18
+ }
19
+ else {
20
+ resolve(stdout);
21
+ }
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * Run a git command in `cwd` and return its stdout, or `undefined` when git
27
+ * exits with a nonzero status.
28
+ */
29
+ export async function tryGit(cwd, ...args) {
30
+ try {
31
+ return await git(cwd, ...args);
32
+ }
33
+ catch (error) {
34
+ if (error instanceof GitCommandError) {
35
+ return undefined;
36
+ }
37
+ throw error;
38
+ }
39
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
package/dist/main.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cli.js";
3
+ process.exitCode = await runCli(process.argv.slice(2));
@@ -0,0 +1,54 @@
1
+ /** Stable identifiers for publish command failures beyond the engine's refusals. */
2
+ export declare const PublishCommandErrorCode: {
3
+ readonly WRITE_BACK_FAILED: "PUBLISH_WRITE_BACK_FAILED";
4
+ };
5
+ /** Union of all {@link PublishCommandErrorCode} values. */
6
+ export type PublishCommandErrorCode = (typeof PublishCommandErrorCode)[keyof typeof PublishCommandErrorCode];
7
+ /** Inputs {@link resolvePublishTarget} decides the publish target from. */
8
+ export interface PublishTargetInput {
9
+ /** Value of `--remote`, or `undefined` when the flag is absent. */
10
+ readonly explicitRemote: string | undefined;
11
+ /**
12
+ * The `<owner>/<repo>` identity recorded in the project's manifest, or
13
+ * `undefined` when the manifest records none, is missing, or is invalid.
14
+ */
15
+ readonly identity: string | undefined;
16
+ /**
17
+ * `true` when the project directory is the repository root of its git
18
+ * checkout; `false` for a subdirectory of a checkout, or a directory that is
19
+ * not in a git checkout at all.
20
+ */
21
+ readonly isCheckoutRoot: boolean;
22
+ /** `true` when the project's git checkout has an `origin` remote. */
23
+ readonly hasOrigin: boolean;
24
+ }
25
+ /** Where a publish records its version, and how the target repository is reached. */
26
+ export type PublishTarget = {
27
+ readonly mode: "in-place";
28
+ } | {
29
+ readonly mode: "constructed";
30
+ readonly remote: string;
31
+ };
32
+ /**
33
+ * Decide where a publish records its version:
34
+ * - `--remote` given: constructed mode to that URL.
35
+ * - no `--remote`, a standalone project (the project directory is the
36
+ * repository root of a checkout that has an origin): in-place mode on
37
+ * origin, whatever identity the manifest records.
38
+ * - no `--remote` and not a standalone project (a subdirectory of a checkout,
39
+ * or no origin), with a recorded identity: constructed mode to the GitHub
40
+ * remote derived from the identity, `https://github.com/<owner>/<repo>.git`.
41
+ * - no `--remote`, not a standalone project, and no recorded identity:
42
+ * in-place mode on origin.
43
+ *
44
+ * Always returns a target and does not validate that it can publish; a missing
45
+ * manifest, an unusable origin, or an identity that cannot be stamped is
46
+ * refused by the publish the chosen target dispatches to.
47
+ */
48
+ export declare function resolvePublishTarget(input: PublishTargetInput): PublishTarget;
49
+ /**
50
+ * Run `mindcraft publish` with the arguments following the subcommand name.
51
+ * Returns the process exit code.
52
+ */
53
+ export declare function runPublishCommand(args: readonly string[]): Promise<number>;
54
+ //# sourceMappingURL=publish-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-command.d.ts","sourceRoot":"","sources":["../src/publish-command.ts"],"names":[],"mappings":"AA4DA,oFAAoF;AACpF,eAAO,MAAM,uBAAuB;;CAE1B,CAAC;AAEX,2DAA2D;AAC3D,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAC;AAkD7G,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,qFAAqF;AACrF,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAW7E;AAoMD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAoDhF"}
@@ -0,0 +1,360 @@
1
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { createJsDelivrExtensionTransport, deriveCoordinateFromRemoteUrl, ExtensionPublishErrorCode, githubRemoteUrlForCoordinate, MINDCRAFT_JSON_PATH, parseProjectContentManifest, publishExtensionVersion, serializeProjectContentManifest, } from "@mindcraft-lang/app-host";
5
+ import { GitCommandError, git, tryGit } from "./git.js";
6
+ const PUBLISH_USAGE = `usage: mindcraft publish [patch|minor|major] [--dir <path>] [--remote <url>] [--allow-unstable-refs]
7
+
8
+ Publishes a version of the Mindcraft project in --dir (default: the current
9
+ directory). Run from inside an already-published project's folder, no flags are
10
+ needed: the current directory supplies --dir, and the project's git checkout
11
+ supplies the remote. With a version bump, the manifest version is incremented;
12
+ without one, the manifest's current version is published as-is, which is valid
13
+ only as a first publish to a repository that has no tags. Every publish stamps
14
+ the published manifest's identity field with the <owner>/<repo> coordinate of
15
+ the publish remote; a warning is printed when the stamp changes a previously
16
+ recorded identity.
17
+
18
+ Without --remote, the publish target depends on where the project sits in its
19
+ git checkout. A standalone project, whose directory is the repository root of
20
+ its checkout, publishes to the checkout's origin: it is committed, tagged
21
+ v<version>, and the branch and tag are pushed to origin. A project kept in a
22
+ subdirectory of its checkout (for example inside a monorepo), or one whose
23
+ checkout has no origin, publishes to the GitHub remote derived from the
24
+ manifest's recorded identity, https://github.com/<owner>/<repo>.git. A first
25
+ publish, whose manifest records no identity yet, targets origin.
26
+
27
+ With --remote, or when the remote is derived from the identity, the project's
28
+ published tree (mindcraft.json plus its manifest-listed files) is committed to
29
+ that remote's default branch and tagged v<version>, and the published version
30
+ and identity are written back to the project directory's mindcraft.json.
31
+
32
+ --dir <path> project directory (default: current directory)
33
+ --remote <url> git remote to publish the project tree to; without it a
34
+ standalone checkout publishes to its origin, and a
35
+ subdirectory project publishes to the GitHub remote derived
36
+ from the recorded identity
37
+ --allow-unstable-refs
38
+ allow dependencies that are unstable for consumers: a
39
+ branch reference, or a pinned version the fetch source
40
+ does not yet serve
41
+ `;
42
+ /** Stable identifiers for publish command failures beyond the engine's refusals. */
43
+ export const PublishCommandErrorCode = {
44
+ WRITE_BACK_FAILED: "PUBLISH_WRITE_BACK_FAILED",
45
+ };
46
+ const VERSION_BUMPS = ["patch", "minor", "major"];
47
+ function isVersionBump(value) {
48
+ return VERSION_BUMPS.includes(value);
49
+ }
50
+ function parsePublishArguments(args) {
51
+ let bump;
52
+ let dir = process.cwd();
53
+ let remote;
54
+ let allowUnstableRefs = false;
55
+ for (let i = 0; i < args.length; i++) {
56
+ const arg = args[i];
57
+ if (arg === "--allow-unstable-refs") {
58
+ allowUnstableRefs = true;
59
+ }
60
+ else if (arg === "--dir" || arg === "--remote") {
61
+ const value = args[i + 1];
62
+ if (value === undefined) {
63
+ return `${arg} requires a value`;
64
+ }
65
+ i++;
66
+ if (arg === "--dir") {
67
+ dir = path.resolve(value);
68
+ }
69
+ else {
70
+ remote = value;
71
+ }
72
+ }
73
+ else if (isVersionBump(arg)) {
74
+ if (bump !== undefined) {
75
+ return `unexpected argument "${arg}"`;
76
+ }
77
+ bump = arg;
78
+ }
79
+ else {
80
+ return `unexpected argument "${arg}"`;
81
+ }
82
+ }
83
+ return { bump, dir, remote, allowUnstableRefs };
84
+ }
85
+ /**
86
+ * Decide where a publish records its version:
87
+ * - `--remote` given: constructed mode to that URL.
88
+ * - no `--remote`, a standalone project (the project directory is the
89
+ * repository root of a checkout that has an origin): in-place mode on
90
+ * origin, whatever identity the manifest records.
91
+ * - no `--remote` and not a standalone project (a subdirectory of a checkout,
92
+ * or no origin), with a recorded identity: constructed mode to the GitHub
93
+ * remote derived from the identity, `https://github.com/<owner>/<repo>.git`.
94
+ * - no `--remote`, not a standalone project, and no recorded identity:
95
+ * in-place mode on origin.
96
+ *
97
+ * Always returns a target and does not validate that it can publish; a missing
98
+ * manifest, an unusable origin, or an identity that cannot be stamped is
99
+ * refused by the publish the chosen target dispatches to.
100
+ */
101
+ export function resolvePublishTarget(input) {
102
+ if (input.explicitRemote !== undefined) {
103
+ return { mode: "constructed", remote: input.explicitRemote };
104
+ }
105
+ if (input.isCheckoutRoot && input.hasOrigin) {
106
+ return { mode: "in-place" };
107
+ }
108
+ if (input.identity !== undefined) {
109
+ return { mode: "constructed", remote: githubRemoteUrlForCoordinate(input.identity) };
110
+ }
111
+ return { mode: "in-place" };
112
+ }
113
+ function isFileNotFound(error) {
114
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
115
+ }
116
+ /**
117
+ * Read the `<owner>/<repo>` identity recorded in the project's `mindcraft.json`.
118
+ * Returns `undefined` when the directory has no manifest, its manifest does not
119
+ * parse, or it records no identity; a missing or invalid manifest is reported
120
+ * by the publish itself.
121
+ */
122
+ async function readRecordedIdentity(dir) {
123
+ let manifestText;
124
+ try {
125
+ manifestText = await readFile(path.join(dir, MINDCRAFT_JSON_PATH), "utf8");
126
+ }
127
+ catch (error) {
128
+ if (isFileNotFound(error))
129
+ return undefined;
130
+ throw error;
131
+ }
132
+ const parsed = parseProjectContentManifest(manifestText);
133
+ return parsed.ok ? parsed.manifest.identity : undefined;
134
+ }
135
+ /**
136
+ * A publish content source over a project directory: the manifest is
137
+ * `<dir>/mindcraft.json`, and listed files resolve relative to `dir`. Paths
138
+ * that resolve outside `dir` read as absent.
139
+ */
140
+ function directoryContentSource(dir) {
141
+ const root = path.resolve(dir);
142
+ return {
143
+ readManifest: async () => {
144
+ try {
145
+ return await readFile(path.join(root, MINDCRAFT_JSON_PATH), "utf8");
146
+ }
147
+ catch (error) {
148
+ if (isFileNotFound(error))
149
+ return undefined;
150
+ throw error;
151
+ }
152
+ },
153
+ readFile: async (filePath) => {
154
+ const resolved = path.resolve(root, filePath);
155
+ const relative = path.relative(root, resolved);
156
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
157
+ return undefined;
158
+ }
159
+ try {
160
+ return new Uint8Array(await readFile(resolved));
161
+ }
162
+ catch (error) {
163
+ if (isFileNotFound(error))
164
+ return undefined;
165
+ throw error;
166
+ }
167
+ },
168
+ };
169
+ }
170
+ async function writePublishFiles(root, files) {
171
+ for (const file of files) {
172
+ const target = path.join(root, file.path);
173
+ await mkdir(path.dirname(target), { recursive: true });
174
+ await writeFile(target, file.content);
175
+ }
176
+ }
177
+ /**
178
+ * A publish backend over a git checkout whose repository root is `dir`: the
179
+ * repository's own history defines the published content. Applying a publish
180
+ * writes the publish files into the checkout, commits them, tags, and pushes
181
+ * the current branch and the tag to `origin`.
182
+ */
183
+ function checkoutPublishBackend(dir) {
184
+ return {
185
+ isClean: async () => (await git(dir, "status", "--porcelain")).trim() === "",
186
+ tagExists: async (tag) => {
187
+ if ((await tryGit(dir, "rev-parse", "--verify", `refs/tags/${tag}`)) !== undefined) {
188
+ return true;
189
+ }
190
+ const remoteTag = await git(dir, "ls-remote", "--tags", "origin", `refs/tags/${tag}`);
191
+ return remoteTag.trim() !== "";
192
+ },
193
+ hasAnyTags: async () => {
194
+ if ((await git(dir, "tag", "--list")).trim() !== "") {
195
+ return true;
196
+ }
197
+ return (await git(dir, "ls-remote", "--tags", "origin")).trim() !== "";
198
+ },
199
+ readHeadManifest: () => tryGit(dir, "show", `HEAD:${MINDCRAFT_JSON_PATH}`),
200
+ apply: async ({ tag, files }) => {
201
+ await writePublishFiles(dir, files);
202
+ await git(dir, "add", "--", ...files.map((file) => file.path));
203
+ // An as-is publish can leave the tree identical to head; the tag alone
204
+ // records the publish then.
205
+ if ((await tryGit(dir, "diff", "--cached", "--quiet")) === undefined) {
206
+ await git(dir, "commit", "-m", tag);
207
+ }
208
+ await git(dir, "tag", tag);
209
+ await git(dir, "push", "origin", "HEAD", `refs/tags/${tag}`);
210
+ },
211
+ };
212
+ }
213
+ /**
214
+ * A dependency pin probe over the public content CDN: a pin is published
215
+ * exactly when the CDN serves the repository's `mindcraft.json` at it.
216
+ */
217
+ function cdnPinProbe() {
218
+ const transport = createJsDelivrExtensionTransport();
219
+ return async (owner, repo, pin) => {
220
+ const result = await transport.fetchFile(owner, repo, pin, MINDCRAFT_JSON_PATH);
221
+ return result.ok;
222
+ };
223
+ }
224
+ /**
225
+ * Publish the checkout at `dir` in place: bump, commit, tag, and push on the
226
+ * repository the directory itself is a checkout of. The stamped identity
227
+ * coordinate derives from the checkout's `origin` remote URL.
228
+ */
229
+ async function publishInCheckout(options) {
230
+ const originUrl = (await tryGit(options.dir, "remote", "get-url", "origin"))?.trim();
231
+ return publishExtensionVersion({
232
+ bump: options.bump,
233
+ coordinate: originUrl === undefined ? undefined : deriveCoordinateFromRemoteUrl(originUrl),
234
+ confirmUnstableDependencies: options.allowUnstableRefs,
235
+ isPinPublished: cdnPinProbe(),
236
+ source: directoryContentSource(options.dir),
237
+ backend: checkoutPublishBackend(options.dir),
238
+ });
239
+ }
240
+ /**
241
+ * Publish the project directory's manifest-described tree to `remote` through
242
+ * a temporary clone: the clone's working tree is replaced with the publish
243
+ * files, committed to the remote's default branch, tagged, and pushed. The
244
+ * stamped identity coordinate derives from the `remote` URL.
245
+ */
246
+ async function publishToRemote(options, remote) {
247
+ const scratch = await mkdtemp(path.join(tmpdir(), "mindcraft-publish-"));
248
+ try {
249
+ const clone = path.join(scratch, "repo");
250
+ await git(scratch, "clone", "--quiet", remote, clone);
251
+ const branch = (await git(clone, "symbolic-ref", "--short", "HEAD")).trim();
252
+ const backend = {
253
+ isClean: async () => true,
254
+ tagExists: async (tag) => (await tryGit(clone, "rev-parse", "--verify", `refs/tags/${tag}`)) !== undefined,
255
+ hasAnyTags: async () => (await git(clone, "tag", "--list")).trim() !== "",
256
+ readHeadManifest: async () => {
257
+ try {
258
+ return await readFile(path.join(clone, MINDCRAFT_JSON_PATH), "utf8");
259
+ }
260
+ catch (error) {
261
+ if (isFileNotFound(error))
262
+ return undefined;
263
+ throw error;
264
+ }
265
+ },
266
+ apply: async ({ tag, files }) => {
267
+ for (const entry of await readdir(clone)) {
268
+ if (entry === ".git")
269
+ continue;
270
+ await rm(path.join(clone, entry), { recursive: true, force: true });
271
+ }
272
+ await writePublishFiles(clone, files);
273
+ await git(clone, "add", "--all");
274
+ await git(clone, "commit", "-m", tag);
275
+ await git(clone, "tag", tag);
276
+ await git(clone, "push", "--quiet", "origin", branch, `refs/tags/${tag}`);
277
+ },
278
+ };
279
+ return await publishExtensionVersion({
280
+ bump: options.bump,
281
+ coordinate: deriveCoordinateFromRemoteUrl(remote),
282
+ confirmUnstableDependencies: options.allowUnstableRefs,
283
+ isPinPublished: cdnPinProbe(),
284
+ source: directoryContentSource(options.dir),
285
+ backend,
286
+ });
287
+ }
288
+ finally {
289
+ await rm(scratch, { recursive: true, force: true });
290
+ }
291
+ }
292
+ /**
293
+ * Write a successful publish's version and stamped identity into the source
294
+ * directory's `mindcraft.json` through the manifest serializer, leaving every
295
+ * other field as parsed. Throws when the manifest cannot be read, parsed, or
296
+ * written.
297
+ */
298
+ async function writeBackPublishedManifest(dir, version, identity) {
299
+ const manifestPath = path.join(dir, MINDCRAFT_JSON_PATH);
300
+ const parsed = parseProjectContentManifest(await readFile(manifestPath, "utf8"));
301
+ if (!parsed.ok) {
302
+ throw new Error(`${manifestPath} is no longer a valid content manifest`);
303
+ }
304
+ await writeFile(manifestPath, serializeProjectContentManifest({ ...parsed.manifest, version, identity }));
305
+ }
306
+ /**
307
+ * Run `mindcraft publish` with the arguments following the subcommand name.
308
+ * Returns the process exit code.
309
+ */
310
+ export async function runPublishCommand(args) {
311
+ const parsed = parsePublishArguments(args);
312
+ if (typeof parsed === "string") {
313
+ process.stderr.write(`mindcraft publish: ${parsed}\n${PUBLISH_USAGE}`);
314
+ return 1;
315
+ }
316
+ try {
317
+ const originUrl = (await tryGit(parsed.dir, "remote", "get-url", "origin"))?.trim();
318
+ // --show-prefix is the project directory's path relative to its checkout's
319
+ // repository root: empty at the root, and absent outside any checkout.
320
+ const checkoutPrefix = (await tryGit(parsed.dir, "rev-parse", "--show-prefix"))?.trim();
321
+ const target = resolvePublishTarget({
322
+ explicitRemote: parsed.remote,
323
+ identity: await readRecordedIdentity(parsed.dir),
324
+ isCheckoutRoot: checkoutPrefix === "",
325
+ hasOrigin: originUrl !== undefined,
326
+ });
327
+ const result = target.mode === "constructed" ? await publishToRemote(parsed, target.remote) : await publishInCheckout(parsed);
328
+ if (!result.ok) {
329
+ process.stderr.write(`mindcraft publish: ${result.error.code}: ${result.error.message}\n`);
330
+ if (result.error.code === ExtensionPublishErrorCode.UNSTABLE_DEPENDENCIES_UNCONFIRMED) {
331
+ process.stderr.write("Pass --allow-unstable-refs to publish anyway.\n");
332
+ }
333
+ return 1;
334
+ }
335
+ if (result.previousIdentity !== undefined) {
336
+ process.stderr.write(`warning: identity changed: ${result.previousIdentity} -> ${result.identity}\n`);
337
+ }
338
+ if (target.mode === "constructed") {
339
+ try {
340
+ await writeBackPublishedManifest(parsed.dir, result.version, result.identity);
341
+ }
342
+ catch (error) {
343
+ const message = error instanceof Error ? error.message : String(error);
344
+ process.stderr.write(`mindcraft publish: ${PublishCommandErrorCode.WRITE_BACK_FAILED}: version ${result.version} was ` +
345
+ `published (tag ${result.tag}), but writing it back to ${path.join(parsed.dir, MINDCRAFT_JSON_PATH)} ` +
346
+ `failed: ${message}\n`);
347
+ return 1;
348
+ }
349
+ }
350
+ process.stdout.write(`published ${result.version} (tag ${result.tag})\n`);
351
+ return 0;
352
+ }
353
+ catch (error) {
354
+ if (error instanceof GitCommandError) {
355
+ process.stderr.write(`mindcraft publish: ${error.message}\n`);
356
+ return 1;
357
+ }
358
+ throw error;
359
+ }
360
+ }
@@ -0,0 +1,87 @@
1
+ import type { ExtensionCatalogDocumentError, ExtensionCatalogDocumentWarning } from "@mindcraft-lang/app-host";
2
+ /** Stable identifiers for target registry failures. */
3
+ export declare const TargetRegistryErrorCode: {
4
+ /** The bundled registry file could not be read from disk. */
5
+ readonly REGISTRY_UNREADABLE: "TARGET_REGISTRY_UNREADABLE";
6
+ /** The registry content did not validate as a clean catalog document. */
7
+ readonly REGISTRY_PARSE_FAILED: "TARGET_REGISTRY_PARSE_FAILED";
8
+ /** A requested coordinate is not present in the registry. */
9
+ readonly UNKNOWN_TARGET_COORDINATE: "TARGET_REGISTRY_UNKNOWN_COORDINATE";
10
+ };
11
+ /** Union of all {@link TargetRegistryErrorCode} values. */
12
+ export type TargetRegistryErrorCode = (typeof TargetRegistryErrorCode)[keyof typeof TargetRegistryErrorCode];
13
+ /**
14
+ * Failure raised by {@link loadTargetRegistry}, {@link parseTargetRegistry},
15
+ * and {@link TargetRegistry.resolve}. Match on {@link code}; the message is
16
+ * secondary human context.
17
+ */
18
+ export declare class TargetRegistryError extends Error {
19
+ /** Stable machine-readable failure code. */
20
+ readonly code: TargetRegistryErrorCode;
21
+ /** Parser errors that rejected the document, when {@link code} is `REGISTRY_PARSE_FAILED`. */
22
+ readonly parseErrors: readonly ExtensionCatalogDocumentError[];
23
+ /** Parser warnings treated as failures, when {@link code} is `REGISTRY_PARSE_FAILED`. */
24
+ readonly parseWarnings: readonly ExtensionCatalogDocumentWarning[];
25
+ /**
26
+ * @param code - Stable failure code.
27
+ * @param message - Human-readable failure context.
28
+ * @param details - Parser diagnostics carried for a parse failure.
29
+ */
30
+ constructor(code: TargetRegistryErrorCode, message: string, details?: {
31
+ readonly parseErrors?: readonly ExtensionCatalogDocumentError[];
32
+ readonly parseWarnings?: readonly ExtensionCatalogDocumentWarning[];
33
+ });
34
+ }
35
+ /** One published target the CLI can resolve to its pinned content. */
36
+ export interface TargetRegistryEntry {
37
+ /** The target's `<owner>/<repo>` coordinate. */
38
+ readonly coordinate: string;
39
+ /** Display name shown for the target. */
40
+ readonly name: string;
41
+ /** Published version the pin corresponds to. */
42
+ readonly version: string;
43
+ /** Description shown for the target. */
44
+ readonly description: string;
45
+ /** Pinned reference `gh:<coordinate>@<full 40-character commit SHA>`. */
46
+ readonly ref: string;
47
+ }
48
+ /**
49
+ * The set of targets the CLI ships, each pinned to exact content. Construct one
50
+ * with {@link loadTargetRegistry} or {@link parseTargetRegistry}.
51
+ */
52
+ export declare class TargetRegistry {
53
+ /** The registry's entries, in document order. */
54
+ readonly entries: readonly TargetRegistryEntry[];
55
+ /** @param entries - Validated registry entries in document order. */
56
+ constructor(entries: readonly TargetRegistryEntry[]);
57
+ /**
58
+ * Resolve a target coordinate to its pinned reference string.
59
+ *
60
+ * @param coordinate - An `<owner>/<repo>` target coordinate.
61
+ * @returns The entry's `gh:<coordinate>@<sha>` pin.
62
+ * @throws {TargetRegistryError} With code `UNKNOWN_TARGET_COORDINATE` when the
63
+ * coordinate is not in the registry.
64
+ */
65
+ resolve(coordinate: string): string;
66
+ }
67
+ /**
68
+ * Build a {@link TargetRegistry} from catalog document text. Any parser error
69
+ * or warning is a failure: an unknown-kind entry the catalog parser skips with
70
+ * a warning would silently vanish, so a warning rejects the whole document.
71
+ *
72
+ * @param content - JSON text of a `mindcraft.catalog/1` document.
73
+ * @throws {TargetRegistryError} With code `REGISTRY_PARSE_FAILED` when the
74
+ * content does not validate cleanly.
75
+ */
76
+ export declare function parseTargetRegistry(content: string): TargetRegistry;
77
+ /**
78
+ * Load the registry bundled with the CLI. The file is read relative to this
79
+ * module, so resolution is identical whether the CLI runs from npm, from the
80
+ * built `dist/`, or from `src/` in development, and does not depend on the
81
+ * caller's working directory.
82
+ *
83
+ * @throws {TargetRegistryError} With code `REGISTRY_UNREADABLE` when the file
84
+ * cannot be read, or `REGISTRY_PARSE_FAILED` when it does not validate cleanly.
85
+ */
86
+ export declare function loadTargetRegistry(): TargetRegistry;
87
+ //# sourceMappingURL=target-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target-registry.d.ts","sourceRoot":"","sources":["../src/target-registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,6BAA6B,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAC;AAG/G,uDAAuD;AACvD,eAAO,MAAM,uBAAuB;IAClC,6DAA6D;;IAE7D,yEAAyE;;IAEzE,6DAA6D;;CAErD,CAAC;AAEX,2DAA2D;AAC3D,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAC;AAE7G;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,8FAA8F;IAC9F,QAAQ,CAAC,WAAW,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC/D,yFAAyF;IACzF,QAAQ,CAAC,aAAa,EAAE,SAAS,+BAA+B,EAAE,CAAC;IAEnE;;;;OAIG;gBAED,IAAI,EAAE,uBAAuB,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;QAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,+BAA+B,EAAE,CAAC;KACrE;CAQJ;AAED,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC,gDAAgD;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAEjD,qEAAqE;gBACzD,OAAO,EAAE,SAAS,mBAAmB,EAAE;IAInD;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAUpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAwBnE;AAKD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CAWnD"}
@@ -0,0 +1,110 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseExtensionCatalogDocument } from "@mindcraft-lang/app-host";
3
+ /** Stable identifiers for target registry failures. */
4
+ export const TargetRegistryErrorCode = {
5
+ /** The bundled registry file could not be read from disk. */
6
+ REGISTRY_UNREADABLE: "TARGET_REGISTRY_UNREADABLE",
7
+ /** The registry content did not validate as a clean catalog document. */
8
+ REGISTRY_PARSE_FAILED: "TARGET_REGISTRY_PARSE_FAILED",
9
+ /** A requested coordinate is not present in the registry. */
10
+ UNKNOWN_TARGET_COORDINATE: "TARGET_REGISTRY_UNKNOWN_COORDINATE",
11
+ };
12
+ /**
13
+ * Failure raised by {@link loadTargetRegistry}, {@link parseTargetRegistry},
14
+ * and {@link TargetRegistry.resolve}. Match on {@link code}; the message is
15
+ * secondary human context.
16
+ */
17
+ export class TargetRegistryError extends Error {
18
+ /** Stable machine-readable failure code. */
19
+ code;
20
+ /** Parser errors that rejected the document, when {@link code} is `REGISTRY_PARSE_FAILED`. */
21
+ parseErrors;
22
+ /** Parser warnings treated as failures, when {@link code} is `REGISTRY_PARSE_FAILED`. */
23
+ parseWarnings;
24
+ /**
25
+ * @param code - Stable failure code.
26
+ * @param message - Human-readable failure context.
27
+ * @param details - Parser diagnostics carried for a parse failure.
28
+ */
29
+ constructor(code, message, details) {
30
+ super(message);
31
+ this.name = "TargetRegistryError";
32
+ this.code = code;
33
+ this.parseErrors = details?.parseErrors ?? [];
34
+ this.parseWarnings = details?.parseWarnings ?? [];
35
+ }
36
+ }
37
+ /**
38
+ * The set of targets the CLI ships, each pinned to exact content. Construct one
39
+ * with {@link loadTargetRegistry} or {@link parseTargetRegistry}.
40
+ */
41
+ export class TargetRegistry {
42
+ /** The registry's entries, in document order. */
43
+ entries;
44
+ /** @param entries - Validated registry entries in document order. */
45
+ constructor(entries) {
46
+ this.entries = entries;
47
+ }
48
+ /**
49
+ * Resolve a target coordinate to its pinned reference string.
50
+ *
51
+ * @param coordinate - An `<owner>/<repo>` target coordinate.
52
+ * @returns The entry's `gh:<coordinate>@<sha>` pin.
53
+ * @throws {TargetRegistryError} With code `UNKNOWN_TARGET_COORDINATE` when the
54
+ * coordinate is not in the registry.
55
+ */
56
+ resolve(coordinate) {
57
+ const entry = this.entries.find((candidate) => candidate.coordinate === coordinate);
58
+ if (entry === undefined) {
59
+ throw new TargetRegistryError(TargetRegistryErrorCode.UNKNOWN_TARGET_COORDINATE, `No target "${coordinate}" is in the registry.`);
60
+ }
61
+ return entry.ref;
62
+ }
63
+ }
64
+ /**
65
+ * Build a {@link TargetRegistry} from catalog document text. Any parser error
66
+ * or warning is a failure: an unknown-kind entry the catalog parser skips with
67
+ * a warning would silently vanish, so a warning rejects the whole document.
68
+ *
69
+ * @param content - JSON text of a `mindcraft.catalog/1` document.
70
+ * @throws {TargetRegistryError} With code `REGISTRY_PARSE_FAILED` when the
71
+ * content does not validate cleanly.
72
+ */
73
+ export function parseTargetRegistry(content) {
74
+ const result = parseExtensionCatalogDocument(content);
75
+ if (!result.ok) {
76
+ throw new TargetRegistryError(TargetRegistryErrorCode.REGISTRY_PARSE_FAILED, "Target registry document did not validate.", { parseErrors: result.errors });
77
+ }
78
+ if (result.warnings.length > 0) {
79
+ throw new TargetRegistryError(TargetRegistryErrorCode.REGISTRY_PARSE_FAILED, "Target registry document produced warnings, which are not accepted.", { parseWarnings: result.warnings });
80
+ }
81
+ const entries = result.document.entries.map((entry) => ({
82
+ coordinate: entry.coordinate,
83
+ name: entry.name,
84
+ version: entry.version,
85
+ description: entry.description,
86
+ ref: entry.ref,
87
+ }));
88
+ return new TargetRegistry(entries);
89
+ }
90
+ /** Location of the bundled registry file, resolved relative to this module. */
91
+ const REGISTRY_FILE_URL = new URL("../targets.json", import.meta.url);
92
+ /**
93
+ * Load the registry bundled with the CLI. The file is read relative to this
94
+ * module, so resolution is identical whether the CLI runs from npm, from the
95
+ * built `dist/`, or from `src/` in development, and does not depend on the
96
+ * caller's working directory.
97
+ *
98
+ * @throws {TargetRegistryError} With code `REGISTRY_UNREADABLE` when the file
99
+ * cannot be read, or `REGISTRY_PARSE_FAILED` when it does not validate cleanly.
100
+ */
101
+ export function loadTargetRegistry() {
102
+ let content;
103
+ try {
104
+ content = readFileSync(REGISTRY_FILE_URL, "utf8");
105
+ }
106
+ catch {
107
+ throw new TargetRegistryError(TargetRegistryErrorCode.REGISTRY_UNREADABLE, `Could not read the target registry at ${REGISTRY_FILE_URL.pathname}.`);
108
+ }
109
+ return parseTargetRegistry(content);
110
+ }
@@ -0,0 +1,15 @@
1
+ /** Stable identifiers for unpack refusals. */
2
+ export declare const UnpackErrorCode: {
3
+ readonly DOCUMENT_MISSING: "UNPACK_DOCUMENT_MISSING";
4
+ readonly DOCUMENT_INVALID: "UNPACK_DOCUMENT_INVALID";
5
+ readonly INVALID_COORDINATE: "UNPACK_INVALID_COORDINATE";
6
+ readonly TARGET_DIR_NOT_EMPTY: "UNPACK_TARGET_DIR_NOT_EMPTY";
7
+ };
8
+ /** Union of all {@link UnpackErrorCode} values. */
9
+ export type UnpackErrorCode = (typeof UnpackErrorCode)[keyof typeof UnpackErrorCode];
10
+ /**
11
+ * Run `mindcraft unpack` with the arguments following the subcommand name.
12
+ * Returns the process exit code.
13
+ */
14
+ export declare function runUnpackCommand(args: readonly string[]): Promise<number>;
15
+ //# sourceMappingURL=unpack-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unpack-command.d.ts","sourceRoot":"","sources":["../src/unpack-command.ts"],"names":[],"mappings":"AA6BA,8CAA8C;AAC9C,eAAO,MAAM,eAAe;;;;;CAKlB,CAAC;AAEX,mDAAmD;AACnD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AA0HrF;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqE/E"}
@@ -0,0 +1,178 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isExtensionCoordinate, MINDCRAFT_JSON_PATH, serializeProjectContentManifest, validateProjectContentManifest, } from "@mindcraft-lang/app-host";
4
+ import { parseMindcraftProjectDocument } from "@mindcraft-lang/service-api";
5
+ const UNPACK_USAGE = `usage: mindcraft unpack <file.mindcraft> [dir] [--coordinate <owner/repo>] [--force]
6
+
7
+ Converts a .mindcraft export document into a publishable project directory:
8
+ the document's embedded manifest is written as mindcraft.json and every file
9
+ in its contents is written to disk. A manifest that declares no files list
10
+ gets one naming every unpacked file, including scratch files that happened to
11
+ be in the exported workspace; prune mindcraft.json before publishing. After
12
+ this graduation the repository is the canonical project -- re-exporting from
13
+ the app is a release ritual, not a sync.
14
+
15
+ dir target directory (default: the document's base
16
+ name, in the current directory)
17
+ --coordinate <owner/repo> record the project's own published identity in
18
+ the manifest's identity field
19
+ --force unpack into a non-empty target directory
20
+ `;
21
+ /** Stable identifiers for unpack refusals. */
22
+ export const UnpackErrorCode = {
23
+ DOCUMENT_MISSING: "UNPACK_DOCUMENT_MISSING",
24
+ DOCUMENT_INVALID: "UNPACK_DOCUMENT_INVALID",
25
+ INVALID_COORDINATE: "UNPACK_INVALID_COORDINATE",
26
+ TARGET_DIR_NOT_EMPTY: "UNPACK_TARGET_DIR_NOT_EMPTY",
27
+ };
28
+ function parseUnpackArguments(args) {
29
+ const positional = [];
30
+ let coordinate;
31
+ let force = false;
32
+ for (let i = 0; i < args.length; i++) {
33
+ const arg = args[i];
34
+ if (arg === "--force") {
35
+ force = true;
36
+ }
37
+ else if (arg === "--coordinate") {
38
+ const value = args[i + 1];
39
+ if (value === undefined) {
40
+ return `${arg} requires a value`;
41
+ }
42
+ i++;
43
+ coordinate = value;
44
+ }
45
+ else if (arg.startsWith("--")) {
46
+ return `unexpected argument "${arg}"`;
47
+ }
48
+ else {
49
+ positional.push(arg);
50
+ }
51
+ }
52
+ if (positional.length === 0) {
53
+ return "expected a .mindcraft document to unpack";
54
+ }
55
+ if (positional.length > 2) {
56
+ return `unexpected argument "${positional[2]}"`;
57
+ }
58
+ const file = path.resolve(positional[0]);
59
+ const dir = positional[1] !== undefined
60
+ ? path.resolve(positional[1])
61
+ : path.resolve(path.basename(positional[0], path.extname(positional[0])));
62
+ return { file, dir, coordinate, force };
63
+ }
64
+ /**
65
+ * Assemble the published-repo tree from a validated document: the document's
66
+ * contents plus its embedded manifest, with the identity recorded when a
67
+ * coordinate is given and a files list synthesized from the contents when the
68
+ * manifest declares none.
69
+ */
70
+ function buildUnpackedTree(document, coordinate) {
71
+ const validated = validateProjectContentManifest(document.manifest);
72
+ if (!validated.ok) {
73
+ const details = validated.errors.map((error) => `${error.code} at ${error.path}: ${error.message}`).join(" ");
74
+ return {
75
+ code: UnpackErrorCode.DOCUMENT_INVALID,
76
+ message: `The document's embedded manifest is not a valid content manifest. ${details}`,
77
+ };
78
+ }
79
+ const files = Object.entries(document.contents)
80
+ .filter(([filePath]) => filePath !== MINDCRAFT_JSON_PATH)
81
+ .map(([filePath, content]) => ({ path: filePath, content }));
82
+ const declaredFilesList = validated.manifest.files !== undefined;
83
+ const manifest = {
84
+ ...validated.manifest,
85
+ ...(coordinate !== undefined ? { identity: coordinate } : {}),
86
+ ...(declaredFilesList ? {} : { files: files.map((file) => file.path) }),
87
+ };
88
+ return { manifestText: serializeProjectContentManifest(manifest), files, declaredFilesList };
89
+ }
90
+ function isRefusal(value) {
91
+ return "code" in value;
92
+ }
93
+ /** Refuse an unusable target: an existing non-directory, or a non-empty directory without `force`. */
94
+ async function checkTargetDir(dir, force) {
95
+ let info;
96
+ try {
97
+ info = await stat(dir);
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ if (!info.isDirectory()) {
103
+ return {
104
+ code: UnpackErrorCode.TARGET_DIR_NOT_EMPTY,
105
+ message: `Target "${dir}" exists and is not a directory.`,
106
+ };
107
+ }
108
+ if (!force && (await readdir(dir)).length > 0) {
109
+ return {
110
+ code: UnpackErrorCode.TARGET_DIR_NOT_EMPTY,
111
+ message: `Target directory "${dir}" is not empty; pass --force to unpack into it anyway.`,
112
+ };
113
+ }
114
+ return undefined;
115
+ }
116
+ /**
117
+ * Run `mindcraft unpack` with the arguments following the subcommand name.
118
+ * Returns the process exit code.
119
+ */
120
+ export async function runUnpackCommand(args) {
121
+ const parsed = parseUnpackArguments(args);
122
+ if (typeof parsed === "string") {
123
+ process.stderr.write(`mindcraft unpack: ${parsed}\n${UNPACK_USAGE}`);
124
+ return 1;
125
+ }
126
+ const refuse = (refusal) => {
127
+ process.stderr.write(`mindcraft unpack: ${refusal.code}: ${refusal.message}\n`);
128
+ return 1;
129
+ };
130
+ if (parsed.coordinate !== undefined && !isExtensionCoordinate(parsed.coordinate)) {
131
+ return refuse({
132
+ code: UnpackErrorCode.INVALID_COORDINATE,
133
+ message: `--coordinate must be "<owner>/<repo>", got "${parsed.coordinate}".`,
134
+ });
135
+ }
136
+ let text;
137
+ try {
138
+ text = await readFile(parsed.file, "utf8");
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ return refuse({
143
+ code: UnpackErrorCode.DOCUMENT_MISSING,
144
+ message: `Cannot read "${parsed.file}": ${message}`,
145
+ });
146
+ }
147
+ const documentResult = parseMindcraftProjectDocument(text);
148
+ if (!documentResult.ok) {
149
+ const details = documentResult.errors.map((error) => `${error.code} at ${error.path}: ${error.message}`).join(" ");
150
+ return refuse({
151
+ code: UnpackErrorCode.DOCUMENT_INVALID,
152
+ message: `"${parsed.file}" is not a valid .mindcraft project document. ${details}`,
153
+ });
154
+ }
155
+ const tree = buildUnpackedTree(documentResult.document, parsed.coordinate);
156
+ if (isRefusal(tree)) {
157
+ return refuse(tree);
158
+ }
159
+ const targetRefusal = await checkTargetDir(parsed.dir, parsed.force);
160
+ if (targetRefusal !== undefined) {
161
+ return refuse(targetRefusal);
162
+ }
163
+ await mkdir(parsed.dir, { recursive: true });
164
+ for (const file of tree.files) {
165
+ const target = path.join(parsed.dir, file.path);
166
+ await mkdir(path.dirname(target), { recursive: true });
167
+ await writeFile(target, file.content, "utf8");
168
+ }
169
+ await writeFile(path.join(parsed.dir, MINDCRAFT_JSON_PATH), tree.manifestText, "utf8");
170
+ process.stdout.write(`unpacked ${tree.files.length} project files and ${MINDCRAFT_JSON_PATH} into ${parsed.dir}\n`);
171
+ if (!tree.declaredFilesList && tree.files.length > 0) {
172
+ process.stdout.write("note: the manifest's files list names everything the export carried, including scratch\n" +
173
+ "files; prune mindcraft.json before publishing.\n");
174
+ }
175
+ process.stdout.write("The unpacked repository is now the canonical project; re-exporting from the app is a\n" +
176
+ "release ritual, not a sync.\n");
177
+ return 0;
178
+ }
@@ -0,0 +1,15 @@
1
+ /** Stable identifiers for version command failures. */
2
+ export declare const VersionCommandErrorCode: {
3
+ readonly MANIFEST_MISSING: "VERSION_MANIFEST_MISSING";
4
+ readonly MANIFEST_INVALID: "VERSION_MANIFEST_INVALID";
5
+ };
6
+ /** Union of all {@link VersionCommandErrorCode} values. */
7
+ export type VersionCommandErrorCode = (typeof VersionCommandErrorCode)[keyof typeof VersionCommandErrorCode];
8
+ /**
9
+ * Run `mindcraft version` with the arguments following the subcommand name:
10
+ * read the project's `mindcraft.json`, increment its version by the named
11
+ * component through the shared bump engine, and write it back. Returns the
12
+ * process exit code.
13
+ */
14
+ export declare function runVersionCommand(args: readonly string[]): Promise<number>;
15
+ //# sourceMappingURL=version-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-command.d.ts","sourceRoot":"","sources":["../src/version-command.ts"],"names":[],"mappings":"AAoBA,uDAAuD;AACvD,eAAO,MAAM,uBAAuB;;;CAG1B,CAAC;AAEX,2DAA2D;AAC3D,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAC;AA+C7G;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmChF"}
@@ -0,0 +1,88 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { bumpVersion, MINDCRAFT_JSON_PATH, parseProjectContentManifest, serializeProjectContentManifest, } from "@mindcraft-lang/app-host";
4
+ const VERSION_USAGE = `usage: mindcraft version <patch|minor|major> [--dir <path>]
5
+
6
+ Increments the version of the Mindcraft project in --dir (default: the current
7
+ directory) by the named component and writes it back to the project's
8
+ mindcraft.json. Run this before packaging a target so the built bundle is baked
9
+ at the new version; the target is then published verbatim, without a bump.
10
+
11
+ --dir <path> project directory (default: current directory)
12
+ `;
13
+ /** Stable identifiers for version command failures. */
14
+ export const VersionCommandErrorCode = {
15
+ MANIFEST_MISSING: "VERSION_MANIFEST_MISSING",
16
+ MANIFEST_INVALID: "VERSION_MANIFEST_INVALID",
17
+ };
18
+ const VERSION_BUMPS = ["patch", "minor", "major"];
19
+ function isVersionBump(value) {
20
+ return VERSION_BUMPS.includes(value);
21
+ }
22
+ function parseVersionArguments(args) {
23
+ let bump;
24
+ let dir = process.cwd();
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (arg === "--dir") {
28
+ const value = args[i + 1];
29
+ if (value === undefined) {
30
+ return `${arg} requires a value`;
31
+ }
32
+ i++;
33
+ dir = path.resolve(value);
34
+ }
35
+ else if (isVersionBump(arg)) {
36
+ if (bump !== undefined) {
37
+ return `unexpected argument "${arg}"`;
38
+ }
39
+ bump = arg;
40
+ }
41
+ else {
42
+ return `unexpected argument "${arg}"`;
43
+ }
44
+ }
45
+ if (bump === undefined) {
46
+ return "a version component (patch, minor, or major) is required";
47
+ }
48
+ return { bump, dir };
49
+ }
50
+ function isFileNotFound(error) {
51
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
52
+ }
53
+ /**
54
+ * Run `mindcraft version` with the arguments following the subcommand name:
55
+ * read the project's `mindcraft.json`, increment its version by the named
56
+ * component through the shared bump engine, and write it back. Returns the
57
+ * process exit code.
58
+ */
59
+ export async function runVersionCommand(args) {
60
+ const parsed = parseVersionArguments(args);
61
+ if (typeof parsed === "string") {
62
+ process.stderr.write(`mindcraft version: ${parsed}\n${VERSION_USAGE}`);
63
+ return 1;
64
+ }
65
+ const manifestPath = path.join(parsed.dir, MINDCRAFT_JSON_PATH);
66
+ let manifestText;
67
+ try {
68
+ manifestText = await readFile(manifestPath, "utf8");
69
+ }
70
+ catch (error) {
71
+ if (isFileNotFound(error)) {
72
+ process.stderr.write(`mindcraft version: ${VersionCommandErrorCode.MANIFEST_MISSING}: ${manifestPath} does not exist.\n`);
73
+ return 1;
74
+ }
75
+ throw error;
76
+ }
77
+ const parseResult = parseProjectContentManifest(manifestText);
78
+ if (!parseResult.ok) {
79
+ const details = parseResult.errors.map((error) => `${error.code} at ${error.path}: ${error.message}`).join(" ");
80
+ process.stderr.write(`mindcraft version: ${VersionCommandErrorCode.MANIFEST_INVALID}: ${manifestPath} is not a valid ` +
81
+ `content manifest. ${details}\n`);
82
+ return 1;
83
+ }
84
+ const next = bumpVersion(parseResult.manifest.version, parsed.bump);
85
+ await writeFile(manifestPath, serializeProjectContentManifest({ ...parseResult.manifest, version: next }));
86
+ process.stdout.write(`version ${next}\n`);
87
+ return 0;
88
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "mindcraft-cli",
3
+ "version": "0.1.2",
4
+ "description": "Command-line tools for Mindcraft projects",
5
+ "license": "MIT",
6
+ "author": "humanapp",
7
+ "type": "module",
8
+ "files": [
9
+ "dist",
10
+ "targets.json"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "bin": {
16
+ "mindcraft": "dist/main.js"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc --build",
20
+ "build:prod": "tsc --build tsconfig.prod.json",
21
+ "clean": "tsc --build --clean",
22
+ "watch": "tsc --build --watch",
23
+ "lint:only": "biome lint ./src",
24
+ "check:only": "biome check ./src",
25
+ "format:only": "biome format ./src",
26
+ "lint": "biome lint ./src --write",
27
+ "check": "biome check ./src --write",
28
+ "format": "biome format ./src --write",
29
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.spec.json",
30
+ "pretest": "npm run build:prod",
31
+ "test": "tsx --tsconfig tsconfig.spec.json --test $(find src -name '*.spec.ts')",
32
+ "release:patch": "node ../../scripts/release.js patch",
33
+ "release:minor": "node ../../scripts/release.js minor",
34
+ "release:major": "node ../../scripts/release.js major"
35
+ },
36
+ "devDependencies": {
37
+ "@biomejs/biome": "2.3.15",
38
+ "@types/node": "^25.3.0",
39
+ "tsx": "^4.21.0",
40
+ "typescript": "~5.9.3"
41
+ },
42
+ "dependencies": {
43
+ "@mindcraft-lang/app-host": "^0.2.2",
44
+ "@mindcraft-lang/service-api": "^0.2.1"
45
+ }
46
+ }
package/targets.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "format": "mindcraft.catalog/1",
3
+ "entries": [
4
+ {
5
+ "kind": "target",
6
+ "coordinate": "mindcraft-lang/trg-microbit-v2",
7
+ "ref": "gh:mindcraft-lang/trg-microbit-v2@c6cfc18d88a6c475be7af63cd6529480d444d8e4",
8
+ "name": "micro:bit v2",
9
+ "version": "0.7.0",
10
+ "description": "Program the BBC micro:bit (v2) with Mindcraft.",
11
+ "alias": "microbit-v2"
12
+ }
13
+ ]
14
+ }