@junejs/cli 0.0.52-dev.2 → 0.1.0-dev.4

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/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
- const child = spawnSync("bun", [entry, ...args], { stdio: "inherit", shell: false });
24
- process.exit(child.status ?? 1);
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.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  //#region src/cli.d.ts
2
- type Parsed = {
2
+ export type Parsed = {
3
3
  verb: string;
4
4
  positional: string[];
5
5
  flags: Record<string, string | boolean>;
6
6
  };
7
- declare function parse(argv: string[]): Parsed;
8
- declare function run(argv: string[]): Promise<number | undefined>;
9
- //#endregion
10
- export { Parsed, parse, run };
7
+ export declare function parse(argv: string[]): Parsed;
8
+ export declare function run(argv: string[]): Promise<number | undefined>;
9
+ //#endregion
package/dist/cli.js CHANGED
@@ -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();
@@ -59,8 +62,12 @@ async function info(root) {
59
62
  for (const r of routes) console.log(` ${r}`);
60
63
  if (agent.enabled) {
61
64
  console.log(`\nAgent surface:`);
62
- if (agent.discovery) console.log(` discovery /llms.txt · /sitemap.xml · /.well-known/api-catalog`);
65
+ if (agent.discovery) {
66
+ console.log(` discovery /llms.txt · /sitemap.xml · /.well-known/api-catalog`);
67
+ console.log(` /.well-known/ai-catalog.json · /.well-known/agent-skills/index.json`);
68
+ }
63
69
  if (agent.mcp) console.log(` mcp /mcp (tools: ${tools.length ? tools.join(", ") : "none"})`);
70
+ if (agent.api) console.log(` api POST /api/<tool> · /openapi.json`);
64
71
  } else console.log(`\nAgent surface: disabled`);
65
72
  return 0;
66
73
  }
@@ -75,7 +82,7 @@ async function run(argv) {
75
82
  return 1;
76
83
  }
77
84
  if (!process.env.JUNE_DEV_CHILD && !flags["no-watch"]) {
78
- const { superviseDev } = await import("./watch-CeyzNVKv.js");
85
+ const { superviseDev } = await import("./watch-BcQz4ONx.js");
79
86
  return superviseDev(root);
80
87
  }
81
88
  const { startDevServer } = await import("@junejs/server");
@@ -107,7 +114,7 @@ async function run(argv) {
107
114
  if (!flags.check && existsSync(join(root, "app"))) {
108
115
  const cols = await generateContent(root);
109
116
  console.log(cols.length ? `generated content: ${cols.join(", ")}` : "no content/ collections");
110
- const { generateMessages } = await import("./messages-DfAlYpfN.js");
117
+ const { generateMessages } = await import("./messages-CofbPnix.js");
111
118
  const locales = await generateMessages(root);
112
119
  if (locales) console.log(`generated messages: ${locales.join(", ")}`);
113
120
  }
@@ -159,7 +166,8 @@ async function run(argv) {
159
166
  console.log("nothing to generate");
160
167
  return 0;
161
168
  }
162
- await writeFile(join(dbRoot, "db", "schema.d.ts"), text);
169
+ const out = join(dbRoot, "db", "schema.d.ts");
170
+ await writeFile(out, text);
163
171
  console.log("wrote db/schema.d.ts");
164
172
  return 0;
165
173
  }
@@ -171,12 +179,16 @@ async function run(argv) {
171
179
  case "help":
172
180
  case "--help":
173
181
  case "-h":
174
- console.log(HELP);
182
+ console.log(HELP + (await import("./external-BOQj7SFk.js")).firstPartyHelp());
175
183
  return 0;
176
- default:
177
- console.error(`june: unknown command "${verb}"\n`);
184
+ default: {
185
+ const { findExternal, runExternal, unknownVerbMessage } = await import("./external-BOQj7SFk.js");
186
+ const bin = findExternal(verb, process.cwd());
187
+ if (bin) return runExternal(bin, argv.slice(1));
188
+ console.error(`${unknownVerbMessage(verb)}\n`);
178
189
  console.log(HELP);
179
190
  return 1;
191
+ }
180
192
  }
181
193
  }
182
194
  //#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 };
@@ -11,7 +11,8 @@ async function generateMessages(root) {
11
11
  if (!existsSync(messagesDir)) return null;
12
12
  let codegen;
13
13
  try {
14
- codegen = await import(pathToFileURL(createRequire(resolve(root, "package.json")).resolve("@junejs/i18n/codegen")).href);
14
+ const resolved = createRequire(resolve(root, "package.json")).resolve("@junejs/i18n/codegen");
15
+ codegen = await import(pathToFileURL(resolved).href);
15
16
  } catch {
16
17
  console.warn("[june] messages/ found but @junejs/i18n is not installed — skipping (run: bun add @junejs/i18n)");
17
18
  return null;
@@ -41,7 +41,7 @@ function superviseDev(root) {
41
41
  console.error("[june] content regen failed:", err);
42
42
  }
43
43
  if (file.startsWith(`messages${sep}`)) try {
44
- const { generateMessages } = await import("./messages-DfAlYpfN.js");
44
+ const { generateMessages } = await import("./messages-CofbPnix.js");
45
45
  await generateMessages(root);
46
46
  } catch (err) {
47
47
  console.error("[june] messages regen failed:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/cli",
3
- "version": "0.0.52-dev.2",
3
+ "version": "0.1.0-dev.4",
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.33",
31
- "@junejs/server": ">=1.0.0-dev.15"
30
+ "@junejs/core": ">=0.2.0-dev.62",
31
+ "@junejs/server": ">=1.0.0-dev.41"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/cli.ts CHANGED
@@ -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 {
@@ -72,8 +75,12 @@ async function info(root: string): Promise<number> {
72
75
  for (const r of routes) console.log(` ${r}`);
73
76
  if (agent.enabled) {
74
77
  console.log(`\nAgent surface:`);
75
- if (agent.discovery) console.log(` discovery /llms.txt · /sitemap.xml · /.well-known/api-catalog`);
78
+ if (agent.discovery) {
79
+ console.log(` discovery /llms.txt · /sitemap.xml · /.well-known/api-catalog`);
80
+ console.log(` /.well-known/ai-catalog.json · /.well-known/agent-skills/index.json`);
81
+ }
76
82
  if (agent.mcp) console.log(` mcp /mcp (tools: ${tools.length ? tools.join(", ") : "none"})`);
83
+ if (agent.api) console.log(` api POST /api/<tool> · /openapi.json`);
77
84
  } else {
78
85
  console.log(`\nAgent surface: disabled`);
79
86
  }
@@ -221,11 +228,17 @@ export async function run(argv: string[]): Promise<number | undefined> {
221
228
  case "help":
222
229
  case "--help":
223
230
  case "-h":
224
- console.log(HELP);
231
+ console.log(HELP + (await import("./external")).firstPartyHelp());
225
232
  return 0;
226
- default:
227
- console.error(`june: unknown command "${verb}"\n`);
233
+ default: {
234
+ // Not built in: an external subcommand (#295) — `june-<verb>` from node_modules/.bin or
235
+ // PATH, handed the arguments exactly as typed (june's own parser never sees them).
236
+ const { findExternal, runExternal, unknownVerbMessage } = await import("./external");
237
+ const bin = findExternal(verb, process.cwd());
238
+ if (bin) return runExternal(bin, argv.slice(1));
239
+ console.error(`${unknownVerbMessage(verb)}\n`);
228
240
  console.log(HELP);
229
241
  return 1;
242
+ }
230
243
  }
231
244
  }
@@ -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
+ }