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 +252 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +36 -0
- package/dist/local-redirect.d.ts +69 -0
- package/dist/local-redirect.d.ts.map +1 -0
- package/dist/local-redirect.js +181 -0
- package/dist/main.js +5 -2
- package/package.json +1 -1
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":"
|
|
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 {
|
|
3
|
-
|
|
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
|
+
}
|