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 +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +28 -0
- package/dist/git.d.ts +15 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +39 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +3 -0
- package/dist/publish-command.d.ts +54 -0
- package/dist/publish-command.d.ts.map +1 -0
- package/dist/publish-command.js +360 -0
- package/dist/target-registry.d.ts +87 -0
- package/dist/target-registry.d.ts.map +1 -0
- package/dist/target-registry.js +110 -0
- package/dist/unpack-command.d.ts +15 -0
- package/dist/unpack-command.d.ts.map +1 -0
- package/dist/unpack-command.js +178 -0
- package/dist/version-command.d.ts +15 -0
- package/dist/version-command.d.ts.map +1 -0
- package/dist/version-command.js +88 -0
- package/package.json +46 -0
- package/targets.json +14 -0
package/dist/cli.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
|
package/dist/main.js
ADDED
|
@@ -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
|
+
}
|