@junejs/cli 0.0.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/LICENSE +21 -0
- package/README.md +21 -0
- package/package.json +29 -0
- package/src/cli.ts +135 -0
- package/src/june.ts +13 -0
- package/src/watch.ts +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 June.build
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @junejs/cli
|
|
2
|
+
|
|
3
|
+
The `june` command — dev, build, deploy, gen, info. **Preview (0.0.x): APIs
|
|
4
|
+
will change.**
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm create june my-app # scaffolds and wires this CLI locally
|
|
8
|
+
cd my-app && npm run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
june dev # dev server with watch + browser live-reload (--no-watch, --port)
|
|
13
|
+
june build # Cloudflare Workers bundle: dist/worker.js + prerendered assets
|
|
14
|
+
june deploy # build + wrangler upload (--dry-run validates only)
|
|
15
|
+
june gen # freeze content/**/*.md → app/_content.ts
|
|
16
|
+
june info # routes + the agent surface (MCP tools, discovery endpoints)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Runs on [Bun](https://bun.sh) (≥ 1.3) — the scaffolder itself (`npm create
|
|
20
|
+
june`) runs on Node. Site & docs: [june.build](https://june.build) · every
|
|
21
|
+
docs page is also markdown (append `.md`).
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@junejs/cli",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "The `june` CLI — dev / build / deploy / gen / info. A thin bin over @junejs/server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://june.build",
|
|
8
|
+
"bin": {
|
|
9
|
+
"june": "./src/june.ts"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/cli.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@junejs/core": "0.0.0",
|
|
23
|
+
"@junejs/server": "0.0.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/junebuild/june"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// The `june` CLI logic — host code (node:* allowed), a thin layer over
|
|
2
|
+
// @junejs/server. Exposed as run(argv) so it is testable without spawning a
|
|
3
|
+
// process; the bin (june.ts) just forwards process.argv. See docs/cli.md.
|
|
4
|
+
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
export type Parsed = {
|
|
8
|
+
verb: string;
|
|
9
|
+
positional: string[];
|
|
10
|
+
flags: Record<string, string | boolean>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Minimal, dependency-free parser: `june <verb> [dir] [--flag] [--flag value]`.
|
|
14
|
+
export function parse(argv: string[]): Parsed {
|
|
15
|
+
const [verb = "", ...rest] = argv;
|
|
16
|
+
const positional: string[] = [];
|
|
17
|
+
const flags: Record<string, string | boolean> = {};
|
|
18
|
+
for (let i = 0; i < rest.length; i++) {
|
|
19
|
+
const a = rest[i]!;
|
|
20
|
+
if (a.startsWith("--")) {
|
|
21
|
+
const key = a.slice(2);
|
|
22
|
+
const next = rest[i + 1];
|
|
23
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
24
|
+
flags[key] = next;
|
|
25
|
+
i++;
|
|
26
|
+
} else {
|
|
27
|
+
flags[key] = true;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
positional.push(a);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { verb, positional, flags };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const HELP = `june — the agent-native React framework
|
|
37
|
+
|
|
38
|
+
Usage: june <command> [dir] [options]
|
|
39
|
+
|
|
40
|
+
Commands:
|
|
41
|
+
dev Start the dev server --port <n> --no-watch
|
|
42
|
+
build Build a workerd-ready bundle --out <dir>
|
|
43
|
+
deploy Build + deploy (Workers) --dry-run
|
|
44
|
+
gen Freeze content/schema --check
|
|
45
|
+
info Show routes + the agent surface
|
|
46
|
+
help Show this help
|
|
47
|
+
|
|
48
|
+
The app directory defaults to the current directory.
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
function appRoot(positional: string[]): string {
|
|
52
|
+
return positional[0] ? resolve(positional[0]) : process.cwd();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function info(root: string): Promise<number> {
|
|
56
|
+
const { createApp, loadJuneConfig } = await import("@junejs/server");
|
|
57
|
+
const { resolveAgent } = await import("@junejs/core/config");
|
|
58
|
+
const { ACTION_REGISTRY } = await import("@junejs/core/agent");
|
|
59
|
+
|
|
60
|
+
const config = await loadJuneConfig(root);
|
|
61
|
+
const app = createApp({ appDir: join(root, "app"), config });
|
|
62
|
+
await app.warmup(); // register defineAction side effects so tools show up
|
|
63
|
+
const routes = await app.routePaths();
|
|
64
|
+
const agent = resolveAgent(config.agent);
|
|
65
|
+
const tools = [...ACTION_REGISTRY.values()].filter((a) => a.description).map((a) => a.id);
|
|
66
|
+
|
|
67
|
+
console.log(`June app: ${config.site?.name ?? "(unnamed)"}`);
|
|
68
|
+
console.log(`\nRoutes (${routes.length}):`);
|
|
69
|
+
for (const r of routes) console.log(` ${r}`);
|
|
70
|
+
if (agent.enabled) {
|
|
71
|
+
console.log(`\nAgent surface:`);
|
|
72
|
+
if (agent.discovery) console.log(` discovery /llms.txt · /sitemap.xml · /.well-known/api-catalog`);
|
|
73
|
+
if (agent.mcp) console.log(` mcp /mcp (tools: ${tools.length ? tools.join(", ") : "none"})`);
|
|
74
|
+
} else {
|
|
75
|
+
console.log(`\nAgent surface: disabled`);
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Returns an exit code for one-shot commands, or undefined for long-running
|
|
81
|
+
// ones (dev) so the bin does NOT call process.exit and the server stays alive.
|
|
82
|
+
export async function run(argv: string[]): Promise<number | undefined> {
|
|
83
|
+
const { verb, positional, flags } = parse(argv);
|
|
84
|
+
const root = appRoot(positional);
|
|
85
|
+
|
|
86
|
+
switch (verb) {
|
|
87
|
+
case "dev": {
|
|
88
|
+
// A restart is the reload: the watch supervisor respawns the serving
|
|
89
|
+
// child on file change (see watch.ts). Children carry JUNE_DEV_CHILD.
|
|
90
|
+
if (!process.env.JUNE_DEV_CHILD && !flags["no-watch"]) {
|
|
91
|
+
const { superviseDev } = await import("./watch");
|
|
92
|
+
return superviseDev(root);
|
|
93
|
+
}
|
|
94
|
+
const { startDevServer } = await import("@junejs/server");
|
|
95
|
+
await startDevServer({
|
|
96
|
+
appDir: join(root, "app"),
|
|
97
|
+
port: flags.port ? Number(flags.port) : 3000,
|
|
98
|
+
});
|
|
99
|
+
return undefined; // server keeps the process alive
|
|
100
|
+
}
|
|
101
|
+
case "build": {
|
|
102
|
+
const { juneBuild } = await import("@junejs/server");
|
|
103
|
+
const out = typeof flags.out === "string" ? { outDir: resolve(flags.out) } : {};
|
|
104
|
+
const r = await juneBuild(root, out);
|
|
105
|
+
console.log(
|
|
106
|
+
`built ${r.outFile} (${r.routes.length} routes, ${r.dynamicRoutes.length} dynamic, ${r.prerendered.length} prerendered)`,
|
|
107
|
+
);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
case "deploy": {
|
|
111
|
+
const { juneDeploy } = await import("@junejs/server");
|
|
112
|
+
const r = await juneDeploy(root, { dryRun: !!flags["dry-run"] });
|
|
113
|
+
console.log(r.url ? `deployed → ${r.url}` : r.dryRun ? "dry-run ok" : "deployed");
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
case "gen": {
|
|
117
|
+
const { generateContent } = await import("@junejs/server");
|
|
118
|
+
const cols = await generateContent(root);
|
|
119
|
+
console.log(cols.length ? `generated content: ${cols.join(", ")}` : "no content/ collections");
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
case "info":
|
|
123
|
+
return info(root);
|
|
124
|
+
case "":
|
|
125
|
+
case "help":
|
|
126
|
+
case "--help":
|
|
127
|
+
case "-h":
|
|
128
|
+
console.log(HELP);
|
|
129
|
+
return 0;
|
|
130
|
+
default:
|
|
131
|
+
console.error(`june: unknown command "${verb}"\n`);
|
|
132
|
+
console.log(HELP);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/june.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The `june` bin. Thin: parse argv → run(). Long-running commands (dev) resolve
|
|
3
|
+
// to undefined so we do NOT exit and the server keeps the process alive.
|
|
4
|
+
import { run } from "./cli";
|
|
5
|
+
|
|
6
|
+
run(process.argv.slice(2))
|
|
7
|
+
.then((code) => {
|
|
8
|
+
if (code !== undefined) process.exit(code);
|
|
9
|
+
})
|
|
10
|
+
.catch((err) => {
|
|
11
|
+
console.error(err instanceof Error ? err.message : err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// `june dev` watch supervisor — a restart IS the reload mechanism. The host's
|
|
2
|
+
// ESM module cache cannot be invalidated (re-importing an edited file is a
|
|
3
|
+
// cached no-op), so the only honest reload is a fresh process: the supervisor
|
|
4
|
+
// watches the app root and respawns the server child on change. Push-based
|
|
5
|
+
// HMR belongs to the native runtime track; this is the Bun/Node host's story.
|
|
6
|
+
|
|
7
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
8
|
+
import { watch } from "node:fs";
|
|
9
|
+
import { sep } from "node:path";
|
|
10
|
+
|
|
11
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", ".june"]);
|
|
12
|
+
|
|
13
|
+
// Exported for tests. `app/_content.*` is generated by `june gen` — watching
|
|
14
|
+
// it would loop (content change → regen → _content event → regen …).
|
|
15
|
+
export function ignoredPath(file: string): boolean {
|
|
16
|
+
const parts = file.split(sep);
|
|
17
|
+
if (parts.some((p) => IGNORED_DIRS.has(p))) return true;
|
|
18
|
+
return parts.length === 2 && parts[0] === "app" && /^_content\.[^/]+$/.test(parts[1]!);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function superviseDev(root: string): undefined {
|
|
22
|
+
let child: ChildProcess | undefined;
|
|
23
|
+
let restarting = false;
|
|
24
|
+
|
|
25
|
+
const start = () => {
|
|
26
|
+
// Respawn this exact invocation (bin + argv) with the child marker set.
|
|
27
|
+
child = spawn(process.execPath, process.argv.slice(1), {
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
env: { ...process.env, JUNE_DEV_CHILD: "1" },
|
|
30
|
+
});
|
|
31
|
+
child.on("exit", (code) => {
|
|
32
|
+
if (restarting) return;
|
|
33
|
+
// Died on its own (e.g. a broken june.config.ts). Stay alive: the next
|
|
34
|
+
// save restarts it — exiting here would make the user re-run dev by hand.
|
|
35
|
+
console.error(`[june] dev server exited (${code ?? "?"}) — waiting for changes`);
|
|
36
|
+
child = undefined;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const restart = async (file: string) => {
|
|
41
|
+
if (restarting) return; // an edit mid-restart is picked up by the next save
|
|
42
|
+
restarting = true;
|
|
43
|
+
if (file.startsWith(`content${sep}`)) {
|
|
44
|
+
// The frozen manifest must be fresh BEFORE the new server boots.
|
|
45
|
+
try {
|
|
46
|
+
const { generateContent } = await import("@junejs/server");
|
|
47
|
+
await generateContent(root);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error("[june] content regen failed:", err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log(`[june] ${file} changed — restarting`);
|
|
53
|
+
const c = child;
|
|
54
|
+
if (!c || c.exitCode !== null || c.signalCode !== null) {
|
|
55
|
+
// The child is already dead (crash state) — once("exit") on an exited
|
|
56
|
+
// process never fires, which would park `restarting` forever. Just
|
|
57
|
+
// bring a fresh one up.
|
|
58
|
+
restarting = false;
|
|
59
|
+
start();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
c.once("exit", () => {
|
|
63
|
+
restarting = false;
|
|
64
|
+
start();
|
|
65
|
+
});
|
|
66
|
+
c.kill();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let pending: string | undefined;
|
|
70
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
71
|
+
watch(root, { recursive: true }, (_event, file) => {
|
|
72
|
+
if (!file || ignoredPath(file)) return;
|
|
73
|
+
pending = file;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
timer = setTimeout(() => void restart(pending!), 80);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
79
|
+
process.on(sig, () => {
|
|
80
|
+
child?.kill(sig);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
start();
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|