mindcraft-cli 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # mindcraft-cli
2
+
3
+ Command-line tools for publishing and versioning Mindcraft projects.
4
+
5
+ A Mindcraft project is a directory with a `mindcraft.json` manifest (plus the
6
+ files it lists). This CLI publishes such a project to GitHub as a versioned,
7
+ installable release, bumps its version, and turns a `.mindcraft` export from
8
+ the web editor into a project directory you can publish.
9
+
10
+ ## Install and run
11
+
12
+ Requires Node.js 18 or newer.
13
+
14
+ Run without installing:
15
+
16
+ ```
17
+ npx mindcraft-cli <command> [arguments]
18
+ ```
19
+
20
+ Or install once and call it directly:
21
+
22
+ ```
23
+ npm install -g mindcraft-cli
24
+ mindcraft <command> [arguments]
25
+ ```
26
+
27
+ Both give you the same `mindcraft` command. Examples below use `mindcraft`;
28
+ substitute `npx mindcraft-cli` if you did not install it.
29
+
30
+ ## Commands at a glance
31
+
32
+ | Command | What it does |
33
+ |---|---|
34
+ | [`mindcraft publish`](#mindcraft-publish) | Publish a version of a project to its GitHub repository. |
35
+ | [`mindcraft version`](#mindcraft-version) | Increment a project's version in its `mindcraft.json`. |
36
+ | [`mindcraft unpack`](#mindcraft-unpack) | Turn a `.mindcraft` export into a publishable project directory. |
37
+
38
+ Running `mindcraft` with no command, or an unknown command, prints usage and
39
+ exits non-zero.
40
+
41
+ `mindcraft --version` (or `-v`) prints this CLI's own version and exits. It is
42
+ separate from the [`version`](#mindcraft-version) command below, which changes
43
+ a project's version.
44
+
45
+ ## Concepts (read once)
46
+
47
+ - `mindcraft.json` is the project manifest. It carries the project's `version`,
48
+ its `files` list, its `identity`, and everything else the project comprises.
49
+ - A project's identity is its `<owner>/<repo>` GitHub coordinate, recorded in
50
+ the manifest's `identity` field. You do not hand-write it: `publish` stamps it
51
+ from the repository you publish to, and `unpack --coordinate` sets it once.
52
+ - A published version is the project's content at the git tag `v<version>` on
53
+ its GitHub repository. The manifest version and the tag always match.
54
+
55
+ ---
56
+
57
+ ## `mindcraft publish`
58
+
59
+ Publish a version of the project to its GitHub repository: commit the project's
60
+ tree, tag it `v<version>`, and push.
61
+
62
+ ```
63
+ mindcraft publish [patch|minor|major] [--dir <path>] [--remote <url>] [--allow-unstable-refs]
64
+ ```
65
+
66
+ ### Arguments
67
+
68
+ | Argument | Meaning |
69
+ |---|---|
70
+ | `patch` \| `minor` \| `major` | Optional. Increment that part of the version before publishing. Omit it only for the very first publish to a repository that has no tags yet, which publishes the manifest's current version as-is. |
71
+ | `--dir <path>` | Project directory. Default: the current directory. |
72
+ | `--remote <url>` | The git remote to publish to. Usually unnecessary (see below). |
73
+ | `--allow-unstable-refs` | Permit dependencies that are unstable for people who install this project: a branch reference, or a pinned version the source does not yet serve. Without this flag, publishing stops and asks you to confirm. |
74
+
75
+ ### How the publish target is chosen
76
+
77
+ If you do not pass `--remote`, where the project publishes depends on your
78
+ checkout:
79
+
80
+ - The project directory is the root of its own git checkout: it publishes to
81
+ that checkout's `origin`.
82
+ - The project lives in a subdirectory of a larger checkout (for example a
83
+ monorepo), or its checkout has no `origin`: it publishes to the GitHub remote
84
+ derived from the recorded identity, `https://github.com/<owner>/<repo>.git`.
85
+ - A first publish, before any identity is recorded, targets `origin`.
86
+
87
+ Pass `--remote <url>` to override this.
88
+
89
+ ### What it does
90
+
91
+ - Bumps the manifest version (if you named a component) and stamps the
92
+ manifest's `identity` with the coordinate of the publish remote. It prints a
93
+ warning if that changes a previously recorded identity (for example when you
94
+ publish a clone to your own repository -- a fork).
95
+ - Commits the project's tree (its `mindcraft.json` plus the files the manifest
96
+ lists), tags it `v<version>`, and pushes the branch and tag.
97
+ - Writes the published version and identity back into the project directory's
98
+ `mindcraft.json` when the tree is published to a derived or explicit remote.
99
+
100
+ Requires the git working tree to be clean, and Git credentials that can push to
101
+ the target repository. Published repositories are public.
102
+
103
+ ### Examples
104
+
105
+ First publish, run from inside the project after creating its empty GitHub repo:
106
+
107
+ ```
108
+ mindcraft publish
109
+ ```
110
+
111
+ Cut a routine release (bump the patch version and publish):
112
+
113
+ ```
114
+ mindcraft publish patch
115
+ ```
116
+
117
+ Publish a library kept in a monorepo subdirectory (the remote is derived from
118
+ its recorded identity):
119
+
120
+ ```
121
+ mindcraft publish minor --dir libs/my-widget
122
+ ```
123
+
124
+ Publish to an explicit repository:
125
+
126
+ ```
127
+ mindcraft publish patch --remote https://github.com/acme/my-widget.git
128
+ ```
129
+
130
+ Publish even though a dependency points at a moving branch:
131
+
132
+ ```
133
+ mindcraft publish patch --allow-unstable-refs
134
+ ```
135
+
136
+ ---
137
+
138
+ ## `mindcraft version`
139
+
140
+ Increment the project's version in its `mindcraft.json`, without publishing.
141
+
142
+ ```
143
+ mindcraft version <patch|minor|major> [--dir <path>]
144
+ ```
145
+
146
+ ### Arguments
147
+
148
+ | Argument | Meaning |
149
+ |---|---|
150
+ | `patch` \| `minor` \| `major` | Required. Which part of the version to increment. |
151
+ | `--dir <path>` | Project directory. Default: the current directory. |
152
+
153
+ ### What it does
154
+
155
+ Reads the project's `mindcraft.json`, increments the version by the named
156
+ component, writes it back, and prints the new version (for example
157
+ `version 1.4.0`).
158
+
159
+ Use this when you package a target for distribution: bump the version first so
160
+ the built bundle bakes in the new version, then publish the packaged target
161
+ verbatim (no bump at publish time).
162
+
163
+ ### Examples
164
+
165
+ ```
166
+ mindcraft version patch
167
+ mindcraft version minor --dir path/to/project
168
+ ```
169
+
170
+ Fails with `VERSION_MANIFEST_MISSING` if the directory has no `mindcraft.json`,
171
+ or `VERSION_MANIFEST_INVALID` if the manifest cannot be parsed.
172
+
173
+ ---
174
+
175
+ ## `mindcraft unpack`
176
+
177
+ Turn a `.mindcraft` export (downloaded from the web editor) into a project
178
+ directory on disk that you can then publish.
179
+
180
+ ```
181
+ mindcraft unpack <file.mindcraft> [dir] [--coordinate <owner/repo>] [--force]
182
+ ```
183
+
184
+ ### Arguments
185
+
186
+ | Argument | Meaning |
187
+ |---|---|
188
+ | `<file.mindcraft>` | Required. The exported document to unpack. |
189
+ | `dir` | Target directory. Default: the document's base name, in the current directory. |
190
+ | `--coordinate <owner/repo>` | Record this as the project's published identity in the manifest's `identity` field. |
191
+ | `--force` | Allow unpacking into a directory that is not empty. |
192
+
193
+ ### What it does
194
+
195
+ Writes the export's embedded manifest as `mindcraft.json` and every file in the
196
+ export to disk. If the manifest declares no `files` list, one is generated
197
+ naming every unpacked file -- including any scratch files that were in the
198
+ exported workspace, so review and prune `mindcraft.json` before publishing.
199
+ Prints how many files were written.
200
+
201
+ After unpacking, that directory is the canonical project: publish from it, and
202
+ treat re-exporting from the app as an occasional release step, not a live sync.
203
+
204
+ ### Examples
205
+
206
+ Unpack into a directory named after the file (`./my-project`):
207
+
208
+ ```
209
+ mindcraft unpack my-project.mindcraft
210
+ ```
211
+
212
+ Unpack into a chosen directory and record the project's identity:
213
+
214
+ ```
215
+ mindcraft unpack my-project.mindcraft my-widget --coordinate acme/my-widget
216
+ ```
217
+
218
+ Unpack into the current, non-empty directory:
219
+
220
+ ```
221
+ mindcraft unpack export.mindcraft . --force
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Exit codes and output (for scripting and automation)
227
+
228
+ - Every command returns exit code `0` on success and `1` on failure.
229
+ - Human-readable results go to stdout; errors go to stderr.
230
+ - Failures carry a stable, uppercase error code in the stderr message (for
231
+ example `VERSION_MANIFEST_MISSING`, `PUBLISH_UNCOMMITTED_CHANGES`,
232
+ `PUBLISH_UNSTABLE_DEPENDENCIES_UNCONFIRMED`). Match on the code, not the
233
+ surrounding prose.
234
+
235
+ ## Using this CLI as a tool (for LLMs and agents)
236
+
237
+ - Invoke exactly one command per call: `mindcraft <command> [arguments]`.
238
+ Available commands are `publish`, `version`, and `unpack`. `mindcraft
239
+ --version` reports the tool's own version.
240
+ - All input is positional arguments and flags; the CLI does not read piped
241
+ stdin. It is safe to run non-interactively.
242
+ - Decide success from the exit code (`0` ok, `1` failed), not from output text.
243
+ On failure, read stderr and match the uppercase error code.
244
+ - `publish` performs network and git operations (commit, tag, push) against a
245
+ real GitHub repository and requires push credentials. Do not run it to
246
+ "test"; it has durable, external side effects. `version` and `unpack` only
247
+ touch local files.
248
+ - `--dir` defaults to the current working directory for `publish` and
249
+ `version`. Pass `--dir <path>` explicitly when the working directory is not
250
+ the project.
251
+ - `publish` will stop and require confirmation if a dependency is unstable for
252
+ consumers; pass `--allow-unstable-refs` only when that is intended.
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Read the `version` field from the package.json at `packageJsonPath`.
3
+ *
4
+ * @param packageJsonPath - Filesystem path to a package.json file.
5
+ * @returns The declared version string, or `0.0.0` when it is absent or not a string.
6
+ */
7
+ export declare function readCliVersion(packageJsonPath: string): string;
8
+ /**
9
+ * Format the `--version` line for a build. Appends ` (local)` when the build at
10
+ * `runningMainPath` is a working copy.
11
+ *
12
+ * @param version - The package version string to display.
13
+ * @param runningMainPath - Path to the running build's entry point.
14
+ * @returns The version, with a ` (local)` suffix for a working-copy build.
15
+ */
16
+ export declare function formatCliVersion(version: string, runningMainPath: string): string;
1
17
  /**
2
18
  * Run the `mindcraft` command line with `argv` (the arguments after the
3
19
  * program name). Returns the process exit code.
package/dist/cli.d.ts.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAuBA;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBrE"}
package/dist/cli.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { isLocalBuild } from "./local-redirect.js";
1
4
  import { runPublishCommand } from "./publish-command.js";
2
5
  import { runUnpackCommand } from "./unpack-command.js";
3
6
  import { runVersionCommand } from "./version-command.js";
@@ -7,12 +10,45 @@ commands:
7
10
  publish publish a version of a Mindcraft project to GitHub
8
11
  version increment a Mindcraft project's version in its mindcraft.json
9
12
  unpack convert a .mindcraft export into a publishable project directory
13
+
14
+ options:
15
+ -v, --version print the mindcraft-cli version
10
16
  `;
17
+ /** Location of the CLI's own package.json, resolved relative to this module. */
18
+ const OWN_PACKAGE_JSON_URL = new URL("../package.json", import.meta.url);
19
+ /** Location of the running build's entry point, resolved relative to this module. */
20
+ const OWN_MAIN_URL = new URL("main.js", import.meta.url);
21
+ /**
22
+ * Read the `version` field from the package.json at `packageJsonPath`.
23
+ *
24
+ * @param packageJsonPath - Filesystem path to a package.json file.
25
+ * @returns The declared version string, or `0.0.0` when it is absent or not a string.
26
+ */
27
+ export function readCliVersion(packageJsonPath) {
28
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
29
+ return typeof parsed.version === "string" ? parsed.version : "0.0.0";
30
+ }
31
+ /**
32
+ * Format the `--version` line for a build. Appends ` (local)` when the build at
33
+ * `runningMainPath` is a working copy.
34
+ *
35
+ * @param version - The package version string to display.
36
+ * @param runningMainPath - Path to the running build's entry point.
37
+ * @returns The version, with a ` (local)` suffix for a working-copy build.
38
+ */
39
+ export function formatCliVersion(version, runningMainPath) {
40
+ return isLocalBuild(runningMainPath) ? `${version} (local)` : version;
41
+ }
11
42
  /**
12
43
  * Run the `mindcraft` command line with `argv` (the arguments after the
13
44
  * program name). Returns the process exit code.
14
45
  */
15
46
  export async function runCli(argv) {
47
+ if (argv.includes("--version") || argv.includes("-v")) {
48
+ const version = readCliVersion(fileURLToPath(OWN_PACKAGE_JSON_URL));
49
+ process.stdout.write(`${formatCliVersion(version, fileURLToPath(OWN_MAIN_URL))}\n`);
50
+ return 0;
51
+ }
16
52
  const [command, ...rest] = argv;
17
53
  if (command === "publish") {
18
54
  return runPublishCommand(rest);
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Locate the built entry point of a local `mindcraft-cli` working copy by
3
+ * walking up from `startDir`. At each ancestor directory it tests both known
4
+ * layouts; a candidate qualifies when its `package.json` declares
5
+ * `name === "mindcraft-cli"` and a `dist/main.js` exists beside it.
6
+ *
7
+ * @param startDir - Directory to begin the upward walk from.
8
+ * @returns Absolute path to the first qualifying candidate's `dist/main.js`, or
9
+ * undefined when no qualifying candidate exists up to the filesystem root.
10
+ */
11
+ export declare function findLocalCliMain(startDir: string): string | undefined;
12
+ /** Inputs to {@link shouldRedirect}. */
13
+ export interface ShouldRedirectInput {
14
+ /** Path to a discovered local build's `dist/main.js`, or undefined when none was found. */
15
+ localMain: string | undefined;
16
+ /** Path to the entry point of the build currently executing. */
17
+ runningMain: string;
18
+ /** Environment to read the redirect escape hatch and loop-guard from. */
19
+ env: NodeJS.ProcessEnv;
20
+ }
21
+ /**
22
+ * Decide whether the running CLI should re-execute a discovered local build.
23
+ * Returns false when there is no local build, when the redirect is disabled via
24
+ * {@link NO_REDIRECT_ENV}, when this process is itself a re-exec'd local build
25
+ * (loop guard via {@link LOCAL_MARKER_ENV}), or when the local build and the
26
+ * running build are the same file. Both paths are resolved through
27
+ * `realpathSync` before comparison, so a symlinked global bin still matches the
28
+ * local build it points at.
29
+ */
30
+ export declare function shouldRedirect({ localMain, runningMain, env }: ShouldRedirectInput): boolean;
31
+ /**
32
+ * Report whether the build at `runningMainPath` is a working-copy build. A
33
+ * working copy keeps `src/` beside `dist/`; a published install ships only
34
+ * `dist` and `targets.json`, so it has no `src/`. The package root is
35
+ * `dirname(dirname(runningMainPath))`, i.e. the `.../packages/cli` directory
36
+ * holding both `dist/` and, in a working copy, `src/`.
37
+ */
38
+ export declare function isLocalBuild(runningMainPath: string): boolean;
39
+ /**
40
+ * Re-execute `localMain` as a child of the current Node binary, forwarding
41
+ * `args` verbatim and inheriting standard streams so pipes and interactive
42
+ * prompts pass through. The child is marked with {@link LOCAL_MARKER_ENV} to
43
+ * prevent it from redirecting again.
44
+ *
45
+ * @param localMain - Absolute path to the local build's `dist/main.js`.
46
+ * @param args - Arguments to forward after the program name.
47
+ * @param baseEnv - Environment to layer the local marker over.
48
+ * @returns The child's exit code, or `128 + signal number` when it was killed by
49
+ * a signal.
50
+ */
51
+ export declare function runLocalBuild(localMain: string, args: readonly string[], baseEnv: NodeJS.ProcessEnv): Promise<number>;
52
+ /**
53
+ * When the CLI is invoked from within a `mindcraft-lang` checkout (or a repo
54
+ * embedding it at `external/mindcraft-lang`), transparently re-execute the local
55
+ * working-copy build in place of the installed one, so a developer exercises
56
+ * local code by calling `mindcraft` exactly as a user would. Discovery is
57
+ * anchored at `process.cwd()`.
58
+ *
59
+ * This runs local repository code, like `npx` or a project-local `.bin`; the
60
+ * `name === "mindcraft-cli"` check guards against accidental collision with an
61
+ * unrelated monorepo that also has a `packages/cli`, and is not a security
62
+ * boundary against a hostile repository.
63
+ *
64
+ * @returns True when a local build was re-executed (in which case
65
+ * `process.exitCode` has been set from the child); false when the caller should
66
+ * proceed to run the installed build normally.
67
+ */
68
+ export declare function maybeRedirectToLocalBuild(): Promise<boolean>;
69
+ //# sourceMappingURL=local-redirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-redirect.d.ts","sourceRoot":"","sources":["../src/local-redirect.ts"],"names":[],"mappings":"AAwEA;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQrE;AAgBD,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,mBAAmB,GAAG,OAAO,CAQ5F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAG7D;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAWrH;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CAiBlE"}
@@ -0,0 +1,181 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
+ import { constants } from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ /** The `package.json` name a directory must declare to count as a local CLI checkout. */
7
+ const LOCAL_CLI_PACKAGE_NAME = "mindcraft-cli";
8
+ /**
9
+ * Subdirectories, relative to an ancestor directory, where a local
10
+ * `mindcraft-cli` package may live: a direct mindcraft-lang checkout keeps it at
11
+ * `packages/cli`, and a repository embedding the reference checkout keeps it at
12
+ * `external/mindcraft-lang/packages/cli`.
13
+ */
14
+ const CLI_SUBPATHS = [
15
+ path.join("packages", "cli"),
16
+ path.join("external", "mindcraft-lang", "packages", "cli"),
17
+ ];
18
+ /** Environment variable set on a re-exec'd child to break the redirect loop. */
19
+ const LOCAL_MARKER_ENV = "MINDCRAFT_CLI_LOCAL";
20
+ /** Environment variable a developer sets to disable the local-build redirect entirely. */
21
+ const NO_REDIRECT_ENV = "MINDCRAFT_CLI_NO_REDIRECT";
22
+ /** Environment variable that, when set, prints a one-line note naming the local build path. */
23
+ const DEBUG_ENV = "MINDCRAFT_CLI_DEBUG";
24
+ /**
25
+ * Read a candidate CLI directory's declared name and build presence. Never
26
+ * throws: a missing or malformed `package.json` yields an undefined name.
27
+ */
28
+ function inspectCliCandidate(cliDir) {
29
+ const hasBuild = existsSync(path.join(cliDir, "dist", "main.js"));
30
+ const packageJsonPath = path.join(cliDir, "package.json");
31
+ if (!existsSync(packageJsonPath)) {
32
+ return { name: undefined, hasBuild };
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
36
+ return { name: typeof parsed.name === "string" ? parsed.name : undefined, hasBuild };
37
+ }
38
+ catch {
39
+ return { name: undefined, hasBuild };
40
+ }
41
+ }
42
+ /**
43
+ * Yield every candidate `.../packages/cli` directory found by walking from
44
+ * `startDir` up to the filesystem root, testing both known layouts at each
45
+ * ancestor.
46
+ */
47
+ function* ancestorCliDirs(startDir) {
48
+ let current = path.resolve(startDir);
49
+ while (true) {
50
+ for (const subpath of CLI_SUBPATHS) {
51
+ yield path.join(current, subpath);
52
+ }
53
+ const parent = path.dirname(current);
54
+ if (parent === current) {
55
+ return;
56
+ }
57
+ current = parent;
58
+ }
59
+ }
60
+ /**
61
+ * Locate the built entry point of a local `mindcraft-cli` working copy by
62
+ * walking up from `startDir`. At each ancestor directory it tests both known
63
+ * layouts; a candidate qualifies when its `package.json` declares
64
+ * `name === "mindcraft-cli"` and a `dist/main.js` exists beside it.
65
+ *
66
+ * @param startDir - Directory to begin the upward walk from.
67
+ * @returns Absolute path to the first qualifying candidate's `dist/main.js`, or
68
+ * undefined when no qualifying candidate exists up to the filesystem root.
69
+ */
70
+ export function findLocalCliMain(startDir) {
71
+ for (const cliDir of ancestorCliDirs(startDir)) {
72
+ const candidate = inspectCliCandidate(cliDir);
73
+ if (candidate.name === LOCAL_CLI_PACKAGE_NAME && candidate.hasBuild) {
74
+ return path.join(cliDir, "dist", "main.js");
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+ /**
80
+ * Report whether a local `mindcraft-cli` checkout exists in the ancestry of
81
+ * `startDir` but has not been built (no `dist/main.js`).
82
+ */
83
+ function hasUnbuiltLocalCli(startDir) {
84
+ for (const cliDir of ancestorCliDirs(startDir)) {
85
+ const candidate = inspectCliCandidate(cliDir);
86
+ if (candidate.name === LOCAL_CLI_PACKAGE_NAME && !candidate.hasBuild) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+ /**
93
+ * Decide whether the running CLI should re-execute a discovered local build.
94
+ * Returns false when there is no local build, when the redirect is disabled via
95
+ * {@link NO_REDIRECT_ENV}, when this process is itself a re-exec'd local build
96
+ * (loop guard via {@link LOCAL_MARKER_ENV}), or when the local build and the
97
+ * running build are the same file. Both paths are resolved through
98
+ * `realpathSync` before comparison, so a symlinked global bin still matches the
99
+ * local build it points at.
100
+ */
101
+ export function shouldRedirect({ localMain, runningMain, env }) {
102
+ if (localMain === undefined) {
103
+ return false;
104
+ }
105
+ if (env[NO_REDIRECT_ENV] !== undefined || env[LOCAL_MARKER_ENV] !== undefined) {
106
+ return false;
107
+ }
108
+ return realpathSync(localMain) !== realpathSync(runningMain);
109
+ }
110
+ /**
111
+ * Report whether the build at `runningMainPath` is a working-copy build. A
112
+ * working copy keeps `src/` beside `dist/`; a published install ships only
113
+ * `dist` and `targets.json`, so it has no `src/`. The package root is
114
+ * `dirname(dirname(runningMainPath))`, i.e. the `.../packages/cli` directory
115
+ * holding both `dist/` and, in a working copy, `src/`.
116
+ */
117
+ export function isLocalBuild(runningMainPath) {
118
+ const packageRoot = path.dirname(path.dirname(runningMainPath));
119
+ return existsSync(path.join(packageRoot, "src"));
120
+ }
121
+ /** Map a terminating signal name to the conventional `128 + signal number` exit code. */
122
+ function signalExitCode(signal) {
123
+ const signalNumber = constants.signals[signal] ?? 0;
124
+ return 128 + signalNumber;
125
+ }
126
+ /**
127
+ * Re-execute `localMain` as a child of the current Node binary, forwarding
128
+ * `args` verbatim and inheriting standard streams so pipes and interactive
129
+ * prompts pass through. The child is marked with {@link LOCAL_MARKER_ENV} to
130
+ * prevent it from redirecting again.
131
+ *
132
+ * @param localMain - Absolute path to the local build's `dist/main.js`.
133
+ * @param args - Arguments to forward after the program name.
134
+ * @param baseEnv - Environment to layer the local marker over.
135
+ * @returns The child's exit code, or `128 + signal number` when it was killed by
136
+ * a signal.
137
+ */
138
+ export function runLocalBuild(localMain, args, baseEnv) {
139
+ return new Promise((resolve, reject) => {
140
+ const child = spawn(process.execPath, [localMain, ...args], {
141
+ stdio: "inherit",
142
+ env: { ...baseEnv, [LOCAL_MARKER_ENV]: "1" },
143
+ });
144
+ child.on("error", reject);
145
+ child.on("exit", (code, signal) => {
146
+ resolve(signal !== null ? signalExitCode(signal) : (code ?? 0));
147
+ });
148
+ });
149
+ }
150
+ /**
151
+ * When the CLI is invoked from within a `mindcraft-lang` checkout (or a repo
152
+ * embedding it at `external/mindcraft-lang`), transparently re-execute the local
153
+ * working-copy build in place of the installed one, so a developer exercises
154
+ * local code by calling `mindcraft` exactly as a user would. Discovery is
155
+ * anchored at `process.cwd()`.
156
+ *
157
+ * This runs local repository code, like `npx` or a project-local `.bin`; the
158
+ * `name === "mindcraft-cli"` check guards against accidental collision with an
159
+ * unrelated monorepo that also has a `packages/cli`, and is not a security
160
+ * boundary against a hostile repository.
161
+ *
162
+ * @returns True when a local build was re-executed (in which case
163
+ * `process.exitCode` has been set from the child); false when the caller should
164
+ * proceed to run the installed build normally.
165
+ */
166
+ export async function maybeRedirectToLocalBuild() {
167
+ const runningMain = realpathSync(fileURLToPath(new URL("main.js", import.meta.url)));
168
+ const startDir = process.cwd();
169
+ const localMain = findLocalCliMain(startDir);
170
+ if (localMain === undefined || !shouldRedirect({ localMain, runningMain, env: process.env })) {
171
+ if (localMain === undefined && hasUnbuiltLocalCli(startDir)) {
172
+ process.stderr.write("mindcraft: local mindcraft-cli found but not built; run npm run build to use it\n");
173
+ }
174
+ return false;
175
+ }
176
+ if (process.env[DEBUG_ENV] !== undefined) {
177
+ process.stderr.write(`mindcraft: redirecting to local build at ${localMain}\n`);
178
+ }
179
+ process.exitCode = await runLocalBuild(localMain, process.argv.slice(2), process.env);
180
+ return true;
181
+ }
package/dist/main.js CHANGED
@@ -1,3 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from "./cli.js";
3
- process.exitCode = await runCli(process.argv.slice(2));
2
+ import { maybeRedirectToLocalBuild } from "./local-redirect.js";
3
+ if (!(await maybeRedirectToLocalBuild())) {
4
+ const { runCli } = await import("./cli.js");
5
+ process.exitCode = await runCli(process.argv.slice(2));
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindcraft-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Command-line tools for Mindcraft projects",
5
5
  "license": "MIT",
6
6
  "author": "humanapp",