@junejs/cli 0.0.52-dev.3 → 0.1.0-dev.5
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 +1 -1
- package/bin.mjs +26 -3
- package/dist/cli.js +13 -5
- package/dist/external-BOQj7SFk.js +94 -0
- package/package.json +3 -3
- package/src/cli.ts +15 -4
- package/src/external.ts +115 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ cd my-app && npm run dev
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
june dev # dev server with watch + browser live-reload (--no-watch, --port)
|
|
12
|
+
june dev # dev server with watch + browser live-reload (--no-watch, --port, --host)
|
|
13
13
|
june build # Cloudflare Workers bundle: dist/worker.js + prerendered assets
|
|
14
14
|
june deploy # build + wrangler upload (--dry-run validates only)
|
|
15
15
|
june gen # freeze content/**/*.md → app/_content.ts
|
package/bin.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// (src/june.ts) runs on Bun, and without this hop an npm user without Bun
|
|
4
4
|
// gets a bare shebang error ("env: bun: No such file or directory") instead
|
|
5
5
|
// of a sentence telling them what to install.
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { constants } from "node:os";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
@@ -20,5 +21,27 @@ if (probe.error || probe.status !== 0) {
|
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
// An async child, so this process can relay what is sent to ITS pid: a supervisor's SIGTERM,
|
|
25
|
+
// a closed terminal's SIGHUP, a SIGINT sent to the pid alone. Waiting in spawnSync, Node died
|
|
26
|
+
// on the signal and left Bun — and whatever Bun runs (`june dev`, an external subcommand) —
|
|
27
|
+
// orphaned. Relayed once, while the child runs; it exits on its own terms and we mirror it.
|
|
28
|
+
// SIGINT is always caught (so we wait for the child) but relayed only without a terminal:
|
|
29
|
+
// Ctrl-C in a terminal already reaches the whole foreground group, and a second SIGINT would
|
|
30
|
+
// turn a graceful stop into a forced one.
|
|
31
|
+
const child = spawn("bun", [entry, ...args], { stdio: "inherit", shell: false });
|
|
32
|
+
const relay = (signal) => {
|
|
33
|
+
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
34
|
+
};
|
|
35
|
+
process.on("SIGTERM", () => relay("SIGTERM"));
|
|
36
|
+
process.on("SIGHUP", () => relay("SIGHUP"));
|
|
37
|
+
process.on("SIGINT", () => {
|
|
38
|
+
if (!process.stdin.isTTY) relay("SIGINT");
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (err) => {
|
|
41
|
+
console.error(`june: could not start bun: ${err.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
// A child killed by a signal exits the shell way, 128 + the signal number.
|
|
45
|
+
child.on("exit", (code, signal) => {
|
|
46
|
+
process.exit(code ?? (signal ? 128 + (constants.signals[signal] ?? 0) : 1));
|
|
47
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ const HELP = `june — the agent-native React framework
|
|
|
28
28
|
Usage: june <command> [dir] [options]
|
|
29
29
|
|
|
30
30
|
Commands:
|
|
31
|
-
dev Start the dev server --port <n> --no-watch
|
|
31
|
+
dev Start the dev server --port <n> --host [addr] --no-watch
|
|
32
32
|
build Build a workerd-ready bundle --out <dir>
|
|
33
33
|
deploy Build + deploy (workers/vercel) --dry-run --prod --skip-migrate --allow-destructive
|
|
34
34
|
gen Freeze content + agent module --check
|
|
@@ -37,6 +37,9 @@ Commands:
|
|
|
37
37
|
help Show this help
|
|
38
38
|
|
|
39
39
|
The app directory defaults to the current directory.
|
|
40
|
+
|
|
41
|
+
Any other command runs \`june-<command>\` from node_modules/.bin (this directory or
|
|
42
|
+
a parent) or PATH, with the arguments as given — e.g. \`june inbox\` runs june-inbox.
|
|
40
43
|
`;
|
|
41
44
|
function appRoot(positional) {
|
|
42
45
|
return positional[0] ? resolve(positional[0]) : process.cwd();
|
|
@@ -85,7 +88,8 @@ async function run(argv) {
|
|
|
85
88
|
const { startDevServer } = await import("@junejs/server");
|
|
86
89
|
await startDevServer({
|
|
87
90
|
appDir: join(root, "app"),
|
|
88
|
-
port: flags.port ? Number(flags.port) : 3e3
|
|
91
|
+
port: flags.port ? Number(flags.port) : 3e3,
|
|
92
|
+
...flags.host ? { hostname: flags.host === true ? "0.0.0.0" : flags.host } : {}
|
|
89
93
|
});
|
|
90
94
|
return;
|
|
91
95
|
}
|
|
@@ -176,12 +180,16 @@ async function run(argv) {
|
|
|
176
180
|
case "help":
|
|
177
181
|
case "--help":
|
|
178
182
|
case "-h":
|
|
179
|
-
console.log(HELP);
|
|
183
|
+
console.log(HELP + (await import("./external-BOQj7SFk.js")).firstPartyHelp());
|
|
180
184
|
return 0;
|
|
181
|
-
default:
|
|
182
|
-
|
|
185
|
+
default: {
|
|
186
|
+
const { findExternal, runExternal, unknownVerbMessage } = await import("./external-BOQj7SFk.js");
|
|
187
|
+
const bin = findExternal(verb, process.cwd());
|
|
188
|
+
if (bin) return runExternal(bin, argv.slice(1));
|
|
189
|
+
console.error(`${unknownVerbMessage(verb)}\n`);
|
|
183
190
|
console.log(HELP);
|
|
184
191
|
return 1;
|
|
192
|
+
}
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
//#endregion
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
2
|
+
//#region src/external.ts
|
|
3
|
+
const FIRST_PARTY = {};
|
|
4
|
+
const VERB = /^[a-z][a-z0-9-]*$/;
|
|
5
|
+
function findExternal(verb, cwd, envPath = process.env.PATH ?? "") {
|
|
6
|
+
if (!VERB.test(verb)) return null;
|
|
7
|
+
const dirs = [];
|
|
8
|
+
for (let dir = resolve(cwd);; dir = dirname(dir)) {
|
|
9
|
+
dirs.push(join(dir, "node_modules", ".bin"));
|
|
10
|
+
if (dirname(dir) === dir) break;
|
|
11
|
+
}
|
|
12
|
+
return Bun.which(`june-${verb}`, { PATH: [...dirs, envPath].join(delimiter) });
|
|
13
|
+
}
|
|
14
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
15
|
+
function escapeCmdCommand(command) {
|
|
16
|
+
return command.replace(CMD_META, "^$1");
|
|
17
|
+
}
|
|
18
|
+
function escapeCmdArgument(arg, doubleEscape) {
|
|
19
|
+
let out = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"").replace(/(?=(\\+?)?)\1$/, "$1$1");
|
|
20
|
+
out = `"${out}"`.replace(CMD_META, "^$1");
|
|
21
|
+
return doubleEscape ? out.replace(CMD_META, "^$1") : out;
|
|
22
|
+
}
|
|
23
|
+
function externalCommand(bin, args, platform = process.platform) {
|
|
24
|
+
if (platform !== "win32" || !/\.(cmd|bat)$/i.test(bin)) return {
|
|
25
|
+
argv: [bin, ...args],
|
|
26
|
+
verbatim: false
|
|
27
|
+
};
|
|
28
|
+
const shim = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(bin);
|
|
29
|
+
const line = [escapeCmdCommand(bin), ...args.map((a) => escapeCmdArgument(a, shim))].join(" ");
|
|
30
|
+
return {
|
|
31
|
+
argv: [
|
|
32
|
+
process.env.ComSpec ?? "cmd.exe",
|
|
33
|
+
"/d",
|
|
34
|
+
"/s",
|
|
35
|
+
"/c",
|
|
36
|
+
`"${line}"`
|
|
37
|
+
],
|
|
38
|
+
verbatim: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function runExternal(bin, args) {
|
|
42
|
+
const { argv, verbatim } = externalCommand(bin, args);
|
|
43
|
+
const proc = Bun.spawn(argv, {
|
|
44
|
+
stdio: [
|
|
45
|
+
"inherit",
|
|
46
|
+
"inherit",
|
|
47
|
+
"inherit"
|
|
48
|
+
],
|
|
49
|
+
env: { ...process.env },
|
|
50
|
+
windowsVerbatimArguments: verbatim
|
|
51
|
+
});
|
|
52
|
+
const forward = (signal) => () => proc.kill(signal);
|
|
53
|
+
const onTerm = forward("SIGTERM");
|
|
54
|
+
const onHup = forward("SIGHUP");
|
|
55
|
+
const onInt = () => {
|
|
56
|
+
if (!process.stdin.isTTY) proc.kill("SIGINT");
|
|
57
|
+
};
|
|
58
|
+
process.on("SIGTERM", onTerm);
|
|
59
|
+
process.on("SIGHUP", onHup);
|
|
60
|
+
process.on("SIGINT", onInt);
|
|
61
|
+
try {
|
|
62
|
+
const code = await proc.exited;
|
|
63
|
+
if (proc.signalCode) {
|
|
64
|
+
const n = signalNumber(proc.signalCode);
|
|
65
|
+
return n ? 128 + n : 1;
|
|
66
|
+
}
|
|
67
|
+
return code;
|
|
68
|
+
} finally {
|
|
69
|
+
process.off("SIGTERM", onTerm);
|
|
70
|
+
process.off("SIGHUP", onHup);
|
|
71
|
+
process.off("SIGINT", onInt);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function signalNumber(signal) {
|
|
75
|
+
return {
|
|
76
|
+
SIGHUP: 1,
|
|
77
|
+
SIGINT: 2,
|
|
78
|
+
SIGKILL: 9,
|
|
79
|
+
SIGTERM: 15
|
|
80
|
+
}[signal] ?? 0;
|
|
81
|
+
}
|
|
82
|
+
function firstPartyHelp(firstParty = FIRST_PARTY) {
|
|
83
|
+
const entries = Object.entries(firstParty);
|
|
84
|
+
if (!entries.length) return "";
|
|
85
|
+
const width = Math.max(...entries.map(([v]) => v.length));
|
|
86
|
+
return `\nFirst-party external commands (install the package to use them):\n` + entries.map(([verb, e]) => ` ${verb.padEnd(width)} ${e.summary} (${e.package})`).join("\n") + "\n";
|
|
87
|
+
}
|
|
88
|
+
function unknownVerbMessage(verb, firstParty = FIRST_PARTY) {
|
|
89
|
+
const known = Object.hasOwn(firstParty, verb) ? firstParty[verb] : void 0;
|
|
90
|
+
if (known) return `june ${verb}: ${known.summary} — provided by ${known.package}, which isn't installed.\n install it in this app: bun add -d ${known.package}\n or run it without installing: bunx ${known.package}`;
|
|
91
|
+
return `june: unknown command "${verb}" (no built-in command, and no june-${verb} in node_modules/.bin or on PATH)`;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { findExternal, firstPartyHelp, runExternal, unknownVerbMessage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-dev.5",
|
|
4
4
|
"description": "The `june` CLI — dev / build / deploy / gen / info. A thin bin over @junejs/server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"build": "tsdown"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
31
|
-
"@junejs/server": ">=1.0.0-dev.
|
|
30
|
+
"@junejs/core": ">=0.2.0-dev.65",
|
|
31
|
+
"@junejs/server": ">=1.0.0-dev.45"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/cli.ts
CHANGED
|
@@ -40,7 +40,7 @@ const HELP = `june — the agent-native React framework
|
|
|
40
40
|
Usage: june <command> [dir] [options]
|
|
41
41
|
|
|
42
42
|
Commands:
|
|
43
|
-
dev Start the dev server --port <n> --no-watch
|
|
43
|
+
dev Start the dev server --port <n> --host [addr] --no-watch
|
|
44
44
|
build Build a workerd-ready bundle --out <dir>
|
|
45
45
|
deploy Build + deploy (workers/vercel) --dry-run --prod --skip-migrate --allow-destructive
|
|
46
46
|
gen Freeze content + agent module --check
|
|
@@ -49,6 +49,9 @@ Commands:
|
|
|
49
49
|
help Show this help
|
|
50
50
|
|
|
51
51
|
The app directory defaults to the current directory.
|
|
52
|
+
|
|
53
|
+
Any other command runs \`june-<command>\` from node_modules/.bin (this directory or
|
|
54
|
+
a parent) or PATH, with the arguments as given — e.g. \`june inbox\` runs june-inbox.
|
|
52
55
|
`;
|
|
53
56
|
|
|
54
57
|
function appRoot(positional: string[]): string {
|
|
@@ -115,6 +118,8 @@ export async function run(argv: string[]): Promise<number | undefined> {
|
|
|
115
118
|
await startDevServer({
|
|
116
119
|
appDir: join(root, "app"),
|
|
117
120
|
port: flags.port ? Number(flags.port) : 3000,
|
|
121
|
+
// Bound to 127.0.0.1 unless asked: a bare --host opens every interface.
|
|
122
|
+
...(flags.host ? { hostname: flags.host === true ? "0.0.0.0" : flags.host } : {}),
|
|
118
123
|
});
|
|
119
124
|
return undefined; // server keeps the process alive
|
|
120
125
|
}
|
|
@@ -225,11 +230,17 @@ export async function run(argv: string[]): Promise<number | undefined> {
|
|
|
225
230
|
case "help":
|
|
226
231
|
case "--help":
|
|
227
232
|
case "-h":
|
|
228
|
-
console.log(HELP);
|
|
233
|
+
console.log(HELP + (await import("./external")).firstPartyHelp());
|
|
229
234
|
return 0;
|
|
230
|
-
default:
|
|
231
|
-
|
|
235
|
+
default: {
|
|
236
|
+
// Not built in: an external subcommand (#295) — `june-<verb>` from node_modules/.bin or
|
|
237
|
+
// PATH, handed the arguments exactly as typed (june's own parser never sees them).
|
|
238
|
+
const { findExternal, runExternal, unknownVerbMessage } = await import("./external");
|
|
239
|
+
const bin = findExternal(verb, process.cwd());
|
|
240
|
+
if (bin) return runExternal(bin, argv.slice(1));
|
|
241
|
+
console.error(`${unknownVerbMessage(verb)}\n`);
|
|
232
242
|
console.log(HELP);
|
|
233
243
|
return 1;
|
|
244
|
+
}
|
|
234
245
|
}
|
|
235
246
|
}
|
package/src/external.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// External subcommands (#295, docs/rfc-email.md §9.3): `june <verb>` that is not built in
|
|
2
|
+
// runs `june-<verb>` — the git / cargo model. Looked up in the app's node_modules/.bin (from
|
|
3
|
+
// the working directory up, where a package manager may hoist it), then on PATH. Only the
|
|
4
|
+
// binary the user named runs: nothing is scanned or imported in-process, a client versions
|
|
5
|
+
// and ships on its own, it works for an operator without the app's repository, and the
|
|
6
|
+
// exec boundary survives `june` becoming a native binary.
|
|
7
|
+
|
|
8
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
// First-party external verbs: listed in `june help`, and named in the error when one is
|
|
11
|
+
// invoked but not installed. A verb joins when its package is published.
|
|
12
|
+
export type ExternalVerb = { package: string; summary: string };
|
|
13
|
+
export const FIRST_PARTY: Record<string, ExternalVerb> = {};
|
|
14
|
+
|
|
15
|
+
// A verb names a binary: lowercase, no separators or dots, so `june ../x` never resolves.
|
|
16
|
+
const VERB = /^[a-z][a-z0-9-]*$/;
|
|
17
|
+
|
|
18
|
+
// The `june-<verb>` binary to run, or null. node_modules/.bin directories nearest first,
|
|
19
|
+
// then PATH; Bun.which applies PATHEXT on Windows (npm's .cmd shims, bun's .exe ones).
|
|
20
|
+
export function findExternal(verb: string, cwd: string, envPath = process.env.PATH ?? ""): string | null {
|
|
21
|
+
if (!VERB.test(verb)) return null;
|
|
22
|
+
const dirs: string[] = [];
|
|
23
|
+
for (let dir = resolve(cwd); ; dir = dirname(dir)) {
|
|
24
|
+
dirs.push(join(dir, "node_modules", ".bin"));
|
|
25
|
+
if (dirname(dir) === dir) break;
|
|
26
|
+
}
|
|
27
|
+
return Bun.which(`june-${verb}`, { PATH: [...dirs, envPath].join(delimiter) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Windows: running a .cmd/.bat through cmd.exe ──────────────────────────────
|
|
31
|
+
// cmd.exe re-parses the command line: `&`, `|`, `%`, `^`, `<`, `>` in an argument would split
|
|
32
|
+
// it or run another command (the "BatBadBut" class, CVE-2024-27980) — an argument that
|
|
33
|
+
// carries outside text, say an email a coding agent passes along, must reach the command
|
|
34
|
+
// literally. This is cross-spawn's escaping (MIT, github.com/moxystudio/node-cross-spawn):
|
|
35
|
+
// each argument is quoted with its backslashes and quotes escaped, then every cmd.exe
|
|
36
|
+
// metacharacter is ^-escaped — twice for a node_modules/.bin shim, because the shim's `%*`
|
|
37
|
+
// hands the arguments to one more parse. The line goes to cmd.exe verbatim (no quoting of
|
|
38
|
+
// our own on top).
|
|
39
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
40
|
+
|
|
41
|
+
function escapeCmdCommand(command: string): string {
|
|
42
|
+
return command.replace(CMD_META, "^$1");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function escapeCmdArgument(arg: string, doubleEscape: boolean): string {
|
|
46
|
+
// Backslashes before a quote, or at the end, are doubled so they stay literal once quoted.
|
|
47
|
+
let out = arg.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"').replace(/(?=(\\+?)?)\1$/, "$1$1");
|
|
48
|
+
out = `"${out}"`.replace(CMD_META, "^$1");
|
|
49
|
+
return doubleEscape ? out.replace(CMD_META, "^$1") : out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The argv (and whether it is verbatim) to spawn `bin` with `args`.
|
|
53
|
+
export function externalCommand(bin: string, args: string[], platform = process.platform): { argv: string[]; verbatim: boolean } {
|
|
54
|
+
if (platform !== "win32" || !/\.(cmd|bat)$/i.test(bin)) return { argv: [bin, ...args], verbatim: false };
|
|
55
|
+
const shim = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(bin);
|
|
56
|
+
const line = [escapeCmdCommand(bin), ...args.map((a) => escapeCmdArgument(a, shim))].join(" ");
|
|
57
|
+
return { argv: [process.env.ComSpec ?? "cmd.exe", "/d", "/s", "/c", `"${line}"`], verbatim: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Run it with the arguments exactly as typed and the terminal inherited; resolves to its exit
|
|
61
|
+
// code. A signal that stops `june` (a supervisor's SIGTERM, a closed terminal's SIGHUP) is
|
|
62
|
+
// passed on, so the external command is not left running. SIGINT is always caught — `june`
|
|
63
|
+
// waits for the command to finish its own shutdown — but passed on only without a terminal:
|
|
64
|
+
// Ctrl-C in a terminal already reaches the command (same foreground group), and a second
|
|
65
|
+
// SIGINT would turn its graceful stop into a forced one.
|
|
66
|
+
export async function runExternal(bin: string, args: string[]): Promise<number> {
|
|
67
|
+
const { argv, verbatim } = externalCommand(bin, args);
|
|
68
|
+
// env passed explicitly: Bun.spawn otherwise misses variables set on process.env after
|
|
69
|
+
// startup (a caller that sets JUNE_TOKEN in-process before delegating, say).
|
|
70
|
+
const proc = Bun.spawn(argv, { stdio: ["inherit", "inherit", "inherit"], env: { ...process.env }, windowsVerbatimArguments: verbatim });
|
|
71
|
+
const forward = (signal: NodeJS.Signals) => () => proc.kill(signal);
|
|
72
|
+
const onTerm = forward("SIGTERM");
|
|
73
|
+
const onHup = forward("SIGHUP");
|
|
74
|
+
const onInt = () => { if (!process.stdin.isTTY) proc.kill("SIGINT"); };
|
|
75
|
+
process.on("SIGTERM", onTerm);
|
|
76
|
+
process.on("SIGHUP", onHup);
|
|
77
|
+
process.on("SIGINT", onInt);
|
|
78
|
+
try {
|
|
79
|
+
const code = await proc.exited;
|
|
80
|
+
// Killed by a signal: the shell convention, 128 + the signal number (1 if unrecognized).
|
|
81
|
+
if (proc.signalCode) {
|
|
82
|
+
const n = signalNumber(proc.signalCode);
|
|
83
|
+
return n ? 128 + n : 1;
|
|
84
|
+
}
|
|
85
|
+
return code;
|
|
86
|
+
} finally {
|
|
87
|
+
process.off("SIGTERM", onTerm);
|
|
88
|
+
process.off("SIGHUP", onHup);
|
|
89
|
+
process.off("SIGINT", onInt);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function signalNumber(signal: string): number {
|
|
94
|
+
return ({ SIGHUP: 1, SIGINT: 2, SIGKILL: 9, SIGTERM: 15 } as Record<string, number>)[signal] ?? 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The first-party external verbs, as `june help` lists them; empty when there are none.
|
|
98
|
+
export function firstPartyHelp(firstParty: Record<string, ExternalVerb> = FIRST_PARTY): string {
|
|
99
|
+
const entries = Object.entries(firstParty);
|
|
100
|
+
if (!entries.length) return "";
|
|
101
|
+
const width = Math.max(...entries.map(([v]) => v.length));
|
|
102
|
+
return `\nFirst-party external commands (install the package to use them):\n` +
|
|
103
|
+
entries.map(([verb, e]) => ` ${verb.padEnd(width)} ${e.summary} (${e.package})`).join("\n") + "\n";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The error for a verb that is neither built in nor installed.
|
|
107
|
+
export function unknownVerbMessage(verb: string, firstParty: Record<string, ExternalVerb> = FIRST_PARTY): string {
|
|
108
|
+
const known = Object.hasOwn(firstParty, verb) ? firstParty[verb] : undefined;
|
|
109
|
+
if (known) {
|
|
110
|
+
return `june ${verb}: ${known.summary} — provided by ${known.package}, which isn't installed.\n` +
|
|
111
|
+
` install it in this app: bun add -d ${known.package}\n` +
|
|
112
|
+
` or run it without installing: bunx ${known.package}`;
|
|
113
|
+
}
|
|
114
|
+
return `june: unknown command "${verb}" (no built-in command, and no june-${verb} in node_modules/.bin or on PATH)`;
|
|
115
|
+
}
|