@apex-stack/core 0.1.8 → 0.1.9

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.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  } from "./chunk-IEXQ7E5C.js";
14
14
 
15
15
  // src/cli.ts
16
- import { resolve as resolve5 } from "path";
17
- import { defineCommand as defineCommand6, runMain } from "citty";
16
+ import { resolve as resolve6 } from "path";
17
+ import { defineCommand as defineCommand7, runMain } from "citty";
18
18
 
19
19
  // src/commands/build.ts
20
20
  import { cpSync, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, rmSync, writeFileSync } from "fs";
@@ -417,15 +417,106 @@ var migrateCommand = defineCommand4({
417
417
  }
418
418
  });
419
419
 
420
- // src/commands/start.ts
421
- import { existsSync as existsSync5 } from "fs";
422
- import { join as join7, resolve as resolve4 } from "path";
420
+ // src/commands/new.ts
421
+ import { spawnSync } from "child_process";
422
+ import { cpSync as cpSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "fs";
423
+ import { basename, join as join6, resolve as resolve4 } from "path";
424
+ import { fileURLToPath } from "url";
423
425
  import { defineCommand as defineCommand5 } from "citty";
426
+ var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
427
+ function detectPackageManager() {
428
+ const ua = process.env.npm_config_user_agent || "";
429
+ if (ua.startsWith("pnpm")) return "pnpm";
430
+ if (ua.startsWith("yarn")) return "yarn";
431
+ if (ua.startsWith("bun")) return "bun";
432
+ return "npm";
433
+ }
434
+ function run(cmd, cmdArgs, cwd, quiet = false) {
435
+ const res = spawnSync(cmd, cmdArgs, {
436
+ cwd,
437
+ stdio: quiet ? "ignore" : "inherit",
438
+ shell: process.platform === "win32"
439
+ // npm/pnpm/yarn are .cmd shims on Windows
440
+ });
441
+ return res.status === 0;
442
+ }
443
+ var c = {
444
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
445
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
446
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
447
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`
448
+ };
449
+ var newCommand = defineCommand5({
450
+ meta: { name: "new", description: "Scaffold a new Apex JS app" },
451
+ args: {
452
+ dir: { type: "positional", required: false, description: "Target directory", default: "apex-app" },
453
+ install: { type: "boolean", default: true, description: "Install dependencies (use --no-install to skip)" },
454
+ git: { type: "boolean", default: true, description: "Initialize a git repository (use --no-git to skip)" }
455
+ },
456
+ run({ args }) {
457
+ const target = resolve4(process.cwd(), String(args.dir));
458
+ const name = basename(target);
459
+ if (existsSync4(target) && readdirSync3(target).length > 0) {
460
+ console.error(`
461
+ \u2717 Target directory is not empty: ${target}
462
+ `);
463
+ process.exit(1);
464
+ }
465
+ cpSync2(TEMPLATE_DIR, target, { recursive: true });
466
+ const gitignore = join6(target, "_gitignore");
467
+ if (existsSync4(gitignore)) renameSync(gitignore, join6(target, ".gitignore"));
468
+ for (const rel of ["package.json", "README.md"]) {
469
+ const file = join6(target, rel);
470
+ if (existsSync4(file)) writeFileSync3(file, readFileSync2(file, "utf8").replaceAll("{{name}}", name));
471
+ }
472
+ const log = console.log;
473
+ log(`
474
+ ${c.cyan("Apex JS")} app created in ${args.dir}`);
475
+ const pm = detectPackageManager();
476
+ let gitOk = false;
477
+ if (args.git) {
478
+ const hasGit = spawnSync("git", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
479
+ if (hasGit && run("git", ["init", "-q"], target, true)) {
480
+ run("git", ["add", "-A"], target, true);
481
+ run("git", ["commit", "-m", "Initial commit from Apex JS", "--no-gpg-sign"], target, true);
482
+ gitOk = true;
483
+ }
484
+ }
485
+ let installed = false;
486
+ if (args.install) {
487
+ log(`
488
+ Installing dependencies with ${c.cyan(pm)}\u2026 ${c.dim("(first install can take a minute)")}
489
+ `);
490
+ const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
491
+ installed = run(pm, installArgs, target);
492
+ if (!installed) log(`
493
+ ${c.yellow("\u26A0")} Dependency install failed \u2014 run it yourself after cd'ing in.
494
+ `);
495
+ }
496
+ const runPrefix = pm === "npm" ? "npm run" : pm;
497
+ const steps = [`cd ${args.dir}`];
498
+ if (!installed) steps.push(pm === "yarn" ? "yarn" : `${pm} install`);
499
+ steps.push(`apex dev ${c.dim("# start the dev server \u2192 http://localhost:3000")}`);
500
+ log(`
501
+ ${installed ? c.green("Ready.") : "Next steps:"}
502
+ ${steps.map((s) => ` ${s}`).join("\n")}
503
+
504
+ ${c.dim("Not global? Run")} ${c.cyan(runPrefix + " dev")}${c.dim(" instead of ")}${c.cyan("apex dev")}${c.dim(".")}
505
+ ${c.dim("Islands mode:")} apex dev --islands
506
+ ${gitOk ? c.dim("Git repository initialized. ") : ""}Your server/api/*.ts routes are also MCP tools at /mcp.
507
+ `);
508
+ }
509
+ });
510
+
511
+ // src/commands/start.ts
512
+ import { existsSync as existsSync6 } from "fs";
513
+ import { join as join8, resolve as resolve5 } from "path";
514
+ import { defineCommand as defineCommand6 } from "citty";
424
515
 
425
516
  // src/prod/server.ts
426
517
  import { createServer as createHttpServer } from "http";
427
- import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "fs";
428
- import { join as join6 } from "path";
518
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
519
+ import { join as join7 } from "path";
429
520
  import { pathToFileURL as pathToFileURL2 } from "url";
430
521
  import {
431
522
  createApp,
@@ -450,8 +541,8 @@ var MIME = {
450
541
  async function startProdServer(options) {
451
542
  const dir = options.dir;
452
543
  const port = options.port ?? 3e3;
453
- const manifest = JSON.parse(readFileSync2(join6(dir, "apex-manifest.json"), "utf8"));
454
- const importServer = (relFile) => import(pathToFileURL2(join6(dir, "server", relFile)).href);
544
+ const manifest = JSON.parse(readFileSync3(join7(dir, "apex-manifest.json"), "utf8"));
545
+ const importServer = (relFile) => import(pathToFileURL2(join7(dir, "server", relFile)).href);
455
546
  const registry = {};
456
547
  let componentCss = "";
457
548
  for (const [name, file] of Object.entries(manifest.components)) {
@@ -473,12 +564,12 @@ async function startProdServer(options) {
473
564
  if (event.method !== "GET") return;
474
565
  const path = decodeURIComponent(getRequestURL(event).pathname);
475
566
  if (path === "/" || path.startsWith("/api") || path === "/mcp") return;
476
- const file = join6(dir, path);
477
- if (!file.startsWith(dir) || !existsSync4(file) || !statSync(file).isFile()) return;
567
+ const file = join7(dir, path);
568
+ if (!file.startsWith(dir) || !existsSync5(file) || !statSync(file).isFile()) return;
478
569
  const ext = path.slice(path.lastIndexOf("."));
479
570
  setResponseHeader(event, "Content-Type", MIME[ext] ?? "application/octet-stream");
480
571
  if (path.startsWith("/assets/")) setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
481
- return readFileSync2(file);
572
+ return readFileSync3(file);
482
573
  })
483
574
  );
484
575
  if (apiEntries.length) app.use("/api", createApiHandler(apiEntries));
@@ -508,20 +599,20 @@ async function startProdServer(options) {
508
599
  })
509
600
  );
510
601
  const server = createHttpServer(toNodeListener(app));
511
- await new Promise((resolve6) => server.listen(port, resolve6));
602
+ await new Promise((resolve7) => server.listen(port, resolve7));
512
603
  return { server, port };
513
604
  }
514
605
 
515
606
  // src/commands/start.ts
516
- var startCommand = defineCommand5({
607
+ var startCommand = defineCommand6({
517
608
  meta: { name: "start", description: "Run a production build (from `apex build --server`)" },
518
609
  args: {
519
610
  dir: { type: "positional", required: false, description: "Build directory", default: "dist" },
520
611
  port: { type: "string", description: "Port to listen on", default: "3000" }
521
612
  },
522
613
  async run({ args }) {
523
- const dir = resolve4(process.cwd(), args.dir);
524
- if (!existsSync5(join7(dir, "apex-manifest.json"))) {
614
+ const dir = resolve5(process.cwd(), args.dir);
615
+ if (!existsSync6(join8(dir, "apex-manifest.json"))) {
525
616
  console.error(`
526
617
  No build found in ${args.dir}/. Run \`apex build --server\` first.
527
618
  `);
@@ -536,7 +627,7 @@ var startCommand = defineCommand5({
536
627
  });
537
628
 
538
629
  // src/cli.ts
539
- var dev = defineCommand6({
630
+ var dev = defineCommand7({
540
631
  meta: { name: "dev", description: "Start the Apex JS development server" },
541
632
  args: {
542
633
  root: { type: "positional", required: false, description: "Project root", default: "." },
@@ -544,7 +635,7 @@ var dev = defineCommand6({
544
635
  islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
545
636
  },
546
637
  async run({ args }) {
547
- const root = resolve5(process.cwd(), args.root);
638
+ const root = resolve6(process.cwd(), args.root);
548
639
  const port = Number(args.port);
549
640
  const { port: actual } = await startDevServer({ root, port, islands: args.islands });
550
641
  console.log(`
@@ -553,12 +644,13 @@ var dev = defineCommand6({
553
644
  `);
554
645
  }
555
646
  });
556
- var main = defineCommand6({
647
+ var main = defineCommand7({
557
648
  meta: {
558
649
  name: "apex",
559
650
  description: "The full-stack meta-framework for Alpine.js"
560
651
  },
561
652
  subCommands: {
653
+ new: newCommand,
562
654
  dev,
563
655
  build: buildCommand,
564
656
  start: startCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-stack/core",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,8 @@
36
36
  }
37
37
  },
38
38
  "files": [
39
- "dist"
39
+ "dist",
40
+ "templates"
40
41
  ],
41
42
  "dependencies": {
42
43
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -54,7 +55,7 @@
54
55
  "node": ">=20.19"
55
56
  },
56
57
  "scripts": {
57
- "build": "tsup",
58
+ "build": "node scripts/copy-templates.mjs && tsup",
58
59
  "dev": "tsup --watch",
59
60
  "typecheck": "tsc --noEmit"
60
61
  }
@@ -0,0 +1,33 @@
1
+ # {{name}}
2
+
3
+ An [Apex JS](https://github.com/andrecorugda/apexjs) app — a meta-framework for
4
+ Alpine.js that renders on the server and hydrates in the browser.
5
+
6
+ ## Getting started
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ Then open [http://localhost:3000](http://localhost:3000).
14
+
15
+ ## Islands mode
16
+
17
+ Ship interactive JavaScript only where you need it:
18
+
19
+ ```bash
20
+ apex dev --islands
21
+ ```
22
+
23
+ ## Project structure
24
+
25
+ - `pages/*.alpine` — single-file components. The `<script server>` block runs on
26
+ the server; its `loader()` return value becomes the Alpine `x-data` scope.
27
+ - `server/api/*.ts` — API routes defined with `defineApexRoute`.
28
+
29
+ ## AI-native API
30
+
31
+ Every route in `server/api/*.ts` is a REST endpoint **and** an MCP tool at the
32
+ same time. Set `mcp: true` on a route (see `server/api/hello.ts`) and it is
33
+ automatically exposed to AI agents at the `/mcp` endpoint — no extra wiring.
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ *.log
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "apex dev",
7
+ "dev:islands": "apex dev --islands"
8
+ },
9
+ "dependencies": {
10
+ "alpinejs": "^3.14.8",
11
+ "@apex-stack/core": "latest",
12
+ "zod": "^4.0.0"
13
+ }
14
+ }
@@ -0,0 +1,107 @@
1
+ <script server lang="ts">
2
+ export function loader() {
3
+ return {
4
+ title: 'Welcome to Apex JS',
5
+ tagline: 'The meta-framework for Alpine.js — server-rendered, then hydrated.',
6
+ }
7
+ }
8
+ </script>
9
+
10
+ <template x-data="{ open: false }">
11
+ <main>
12
+ <h1 x-text="title"></h1>
13
+ <p class="tagline" x-text="tagline"></p>
14
+
15
+ <section class="card">
16
+ <p>
17
+ This page was rendered on the server from
18
+ <code>pages/index.alpine</code>, then hydrated by Alpine in the browser.
19
+ Edit it and save to see your changes live.
20
+ </p>
21
+
22
+ <button type="button" @click="open = !open" x-text="open ? 'Hide the details' : 'Show me how'"></button>
23
+
24
+ <div x-show="open" x-transition class="details">
25
+ <p>
26
+ The <code>loader()</code> in the server block runs on the server and
27
+ hands its data straight to Alpine's <code>x-data</code> scope — no
28
+ fetch, no boilerplate.
29
+ </p>
30
+ <p>
31
+ Next, open <code>server/api/hello.ts</code>: it's a REST endpoint
32
+ <em>and</em> an MCP tool at <code>/mcp</code> at the same time.
33
+ </p>
34
+ </div>
35
+ </section>
36
+
37
+ <p class="hint">
38
+ Run <code>apex dev --islands</code> to ship interactive islands only where
39
+ you need them.
40
+ </p>
41
+ </main>
42
+ </template>
43
+
44
+ <style scoped>
45
+ main {
46
+ max-width: 40rem;
47
+ margin: 4rem auto;
48
+ padding: 0 1.5rem;
49
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
50
+ line-height: 1.6;
51
+ color: #1e293b;
52
+ }
53
+
54
+ h1 {
55
+ color: #2563eb;
56
+ font-size: 2.5rem;
57
+ margin-bottom: 0.5rem;
58
+ }
59
+
60
+ .tagline {
61
+ font-size: 1.15rem;
62
+ color: #475569;
63
+ margin-top: 0;
64
+ }
65
+
66
+ .card {
67
+ margin-top: 2rem;
68
+ padding: 1.5rem;
69
+ border: 1px solid #e2e8f0;
70
+ border-radius: 0.75rem;
71
+ background: #f8fafc;
72
+ }
73
+
74
+ button {
75
+ margin-top: 0.5rem;
76
+ padding: 0.6rem 1.1rem;
77
+ font-size: 1rem;
78
+ font-weight: 600;
79
+ color: #fff;
80
+ background: #2563eb;
81
+ border: none;
82
+ border-radius: 0.5rem;
83
+ cursor: pointer;
84
+ transition: background 0.15s ease;
85
+ }
86
+
87
+ button:hover {
88
+ background: #1d4ed8;
89
+ }
90
+
91
+ .details {
92
+ margin-top: 1rem;
93
+ }
94
+
95
+ code {
96
+ padding: 0.1rem 0.35rem;
97
+ font-size: 0.9em;
98
+ background: #e2e8f0;
99
+ border-radius: 0.35rem;
100
+ }
101
+
102
+ .hint {
103
+ margin-top: 2rem;
104
+ font-size: 0.95rem;
105
+ color: #64748b;
106
+ }
107
+ </style>
@@ -0,0 +1,10 @@
1
+ import { defineApexRoute } from '@apex-stack/core'
2
+ import { z } from 'zod'
3
+
4
+ export default defineApexRoute({
5
+ method: 'GET',
6
+ description: 'Say hello to someone by name',
7
+ input: { name: z.string() },
8
+ mcp: true,
9
+ handler: ({ input }) => ({ message: `Hello, ${input.name}!` }),
10
+ })