create-apexjs 0.5.2 → 0.6.0

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/index.js CHANGED
@@ -4,9 +4,15 @@
4
4
  import { spawnSync } from "child_process";
5
5
  import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
6
  import { basename, join, resolve } from "path";
7
+ import { createInterface } from "readline";
7
8
  import { fileURLToPath } from "url";
8
9
  import { defineCommand, runMain } from "citty";
9
10
  var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
11
+ var FEATURES = [
12
+ { key: "data", title: "Data & models" },
13
+ { key: "auth", title: "Auth" },
14
+ { key: "i18n", title: "i18n" }
15
+ ];
10
16
  function substituteName(dir, name) {
11
17
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
12
18
  const p = join(dir, entry.name);
@@ -28,11 +34,21 @@ function run(cmd, cmdArgs, cwd, quiet = false) {
28
34
  const res = spawnSync(cmd, cmdArgs, {
29
35
  cwd,
30
36
  stdio: quiet ? "ignore" : "inherit",
31
- // On Windows, npm/pnpm/yarn are .cmd shims that need a shell.
37
+ // On Windows, npm/pnpm/yarn/npx are .cmd shims that need a shell.
32
38
  shell: process.platform === "win32"
33
39
  });
34
40
  return res.status === 0;
35
41
  }
42
+ function promptYesNo(question, def = false) {
43
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
44
+ return new Promise((res) => {
45
+ rl.question(`${question} ${c.dim(def ? "(Y/n)" : "(y/N)")} `, (ans) => {
46
+ rl.close();
47
+ const a = ans.trim().toLowerCase();
48
+ res(a === "" ? def : a === "y" || a === "yes");
49
+ });
50
+ });
51
+ }
36
52
  var c = {
37
53
  cyan: (s) => `\x1B[36m${s}\x1B[0m`,
38
54
  dim: (s) => `\x1B[2m${s}\x1B[0m`,
@@ -60,9 +76,21 @@ var main = defineCommand({
60
76
  type: "boolean",
61
77
  default: true,
62
78
  description: "Initialize a git repository (use --no-git to skip)"
79
+ },
80
+ data: {
81
+ type: "boolean",
82
+ description: "Include the data/models feature (skips the prompt)"
83
+ },
84
+ auth: {
85
+ type: "boolean",
86
+ description: "Include the auth feature (skips the prompt)"
87
+ },
88
+ i18n: {
89
+ type: "boolean",
90
+ description: "Include the i18n feature (skips the prompt)"
63
91
  }
64
92
  },
65
- run({ args }) {
93
+ async run({ args }) {
66
94
  const target = resolve(process.cwd(), args.dir);
67
95
  const name = basename(target);
68
96
  if (existsSync(target) && readdirSync(target).length > 0) {
@@ -78,14 +106,24 @@ var main = defineCommand({
78
106
  console.log(`
79
107
  ${c.cyan("Apex JS")} app created in ${args.dir}`);
80
108
  const pm = detectPackageManager();
81
- let gitOk = false;
82
- if (args.git) {
83
- const hasGit = spawnSync("git", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
84
- if (hasGit && run("git", ["init", "-q"], target, true)) {
85
- run("git", ["add", "-A"], target, true);
86
- run("git", ["commit", "-m", "Initial commit from Apex JS", "--no-gpg-sign"], target, true);
87
- gitOk = true;
109
+ const selected = [];
110
+ let headerShown = false;
111
+ for (const f of FEATURES) {
112
+ const flag = args[f.key];
113
+ let want = false;
114
+ if (flag !== void 0) {
115
+ want = flag;
116
+ } else if (process.stdin.isTTY) {
117
+ if (!headerShown) {
118
+ console.log(
119
+ `
120
+ ${c.cyan("Optional features")} ${c.dim("(add later anytime with 'apex extend <name>')")}`
121
+ );
122
+ headerShown = true;
123
+ }
124
+ want = await promptYesNo(` Add ${f.title}?`, false);
88
125
  }
126
+ if (want) selected.push(f.key);
89
127
  }
90
128
  let installed = false;
91
129
  if (args.install) {
@@ -104,16 +142,52 @@ var main = defineCommand({
104
142
  );
105
143
  }
106
144
  }
145
+ const applied = [];
146
+ if (selected.length) {
147
+ if (installed) {
148
+ console.log(`
149
+ Adding features: ${c.cyan(selected.join(", "))}\u2026
150
+ `);
151
+ for (const key of selected) {
152
+ if (run("npx", ["apex", "extend", key], target)) applied.push(key);
153
+ else
154
+ console.log(
155
+ `
156
+ ${c.yellow("\u26A0")} Could not add ${key} \u2014 run ${c.cyan(`npx apex extend ${key}`)} yourself.
157
+ `
158
+ );
159
+ }
160
+ } else {
161
+ console.log(
162
+ `
163
+ ${c.yellow("\u26A0")} Features ${selected.join(", ")} need dependencies \u2014 after installing, run: ${c.cyan(
164
+ selected.map((k) => `apex extend ${k}`).join(" && ")
165
+ )}
166
+ `
167
+ );
168
+ }
169
+ }
170
+ let gitOk = false;
171
+ if (args.git) {
172
+ const hasGit = spawnSync("git", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
173
+ if (hasGit && run("git", ["init", "-q"], target, true)) {
174
+ run("git", ["add", "-A"], target, true);
175
+ run("git", ["commit", "-m", "Initial commit from Apex JS", "--no-gpg-sign"], target, true);
176
+ gitOk = true;
177
+ }
178
+ }
107
179
  const runPrefix = pm === "npm" ? "npm run" : pm;
108
180
  const steps = [`cd ${args.dir}`];
109
181
  if (!installed) steps.push(pm === "yarn" ? "yarn" : `${pm} install`);
110
182
  steps.push(
111
183
  `${runPrefix} dev ${c.dim("# start the dev server \u2192 http://localhost:3000")}`
112
184
  );
185
+ const featuresNote = applied.length ? `
186
+ ${c.green("Features added:")} ${c.cyan(applied.join(", "))} ${c.dim("(add more with 'apex extend <name>')")}` : "";
113
187
  console.log(`
114
188
  ${installed ? c.green("Ready.") : "Next steps:"}
115
189
  ${steps.map((s) => ` ${s}`).join("\n")}
116
-
190
+ ${featuresNote}
117
191
  ${c.yellow("Run the CLI with:")} ${c.cyan(`${runPrefix} dev`)} ${c.dim("(or npx apex dev)")}
118
192
  A bare ${c.cyan("apex")} won't resolve \u2014 it's a local dependency, like ${c.cyan("next")} or ${c.cyan("vite")}.
119
193
  ${c.dim("Prefer a global command? ")}${c.cyan("npm i -g @apex-stack/core")}${c.dim(" \u2192 then `apex dev` works anywhere.")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-apexjs",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a new Apex JS app",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,88 @@
1
+ # AGENTS.md — building this app with an AI agent
2
+
3
+ This is an **Apex JS** app. Apex is a full-stack, AI-native meta-framework for
4
+ Alpine.js: file-based routing, SSR, islands, and **every typed API route is also
5
+ an MCP tool**. This file tells you (the agent) how to work here effectively.
6
+
7
+ ## Golden rules
8
+ 1. **Prefer generators over hand-writing.** Run `apex make …` / `apex extend …` /
9
+ `apex add …` to create files — they produce correct, conventional, typed code.
10
+ Hand-writing boilerplate is where you make mistakes.
11
+ 2. **Everything has one place.** Follow the structure below; don't invent folders.
12
+ 3. **Security is the framework's job, not yours.** Use `auth`/`can`/`scope` — never
13
+ hand-roll auth. Defaults are fail-closed.
14
+ 4. **Verify your work.** After changing code, run `apex check` (type gate) then
15
+ `apex test`. Add tests with `createTestApp`. Fix type errors before moving on.
16
+ 5. **TypeScript is strict.** If it compiles, it's probably right; lean on the types.
17
+
18
+ ## Do things with the CLI (this is the fast path)
19
+ ```bash
20
+ apex make page dashboard # pages/dashboard.alpine (a route → /dashboard)
21
+ apex make component Card # components/Card.alpine (<Card /> in templates)
22
+ apex make model Post title:string body:string # models/Post.ts → migration + REST + MCP CRUD
23
+ apex make api webhooks # server/api/webhooks.ts (defineApexRoute; also an MCP tool)
24
+ apex make service Billing # services/BillingService.ts
25
+ apex extend auth # add sealed-cookie sessions + login/logout + /account
26
+ apex extend data # add a DB-backed model + /guestbook demo (Supabase-ready)
27
+ apex extend i18n # add locales/*.json + /fr routing
28
+ apex add button card modal # copy themeable UI components in
29
+ apex migrate # apply DB migrations
30
+ apex check # type-check (tsc --noEmit; fast with the native compiler)
31
+ apex build --preset vercel # build + Vercel config (also: netlify, docker); then deploy
32
+ apex test # run Vitest
33
+ ```
34
+ `apex make <kind> …` kinds: `page component api service store layout middleware test model migration auth`.
35
+
36
+ ## Project structure
37
+ ```
38
+ pages/**/*.alpine Routes. File path → URL. [param] = dynamic. index.alpine → parent path.
39
+ layouts/*.alpine Shared shells; default.alpine wraps every page (<slot/>).
40
+ components/*.alpine Reusable <PascalCase/> components (props via attributes).
41
+ server/api/*.ts Typed routes (defineApexRoute) / model resources → REST + MCP.
42
+ server/auth.ts Identity (sessionAuth) → ctx.user everywhere. (from `apex extend auth`)
43
+ models/*.ts defineModel → schema + migration + table + REST/MCP CRUD.
44
+ db/ createDb + migrations. Uses DATABASE_URL (Postgres) or in-memory libSQL.
45
+ locales/*.json i18n catalogs. (from `apex extend i18n`)
46
+ services/*.ts Business logic (classes). Keep routes thin; delegate here.
47
+ stores/*.ts Global SSR-safe state ($store.x).
48
+ composables/*.ts Reusable client logic (useX) for <script client>.
49
+ shared/*.ts Types shared by server + client.
50
+ tests/*.test.ts Vitest. createTestApp boots the whole app in-process.
51
+ ```
52
+
53
+ ## The `.alpine` single-file component
54
+ ```alpine
55
+ <script server lang="ts">
56
+ // Runs on the server. loader() data becomes the page's x-data (real HTML first).
57
+ export function loader() { return { title: 'Hello' } }
58
+ export function head() { return { title: 'Hello' } } // SEO
59
+ </script>
60
+ <script client lang="ts">
61
+ // Optional client-only logic; import composables here.
62
+ </script>
63
+ <template x-data>
64
+ <h1 x-text="title"></h1>
65
+ <Counter start="0" client:load /> <!-- island: client:load | client:visible | client:idle -->
66
+ </template>
67
+ <style scoped> h1 { color: var(--color-primary); } </style>
68
+ ```
69
+
70
+ ## APIs you'll use (import from `@apex-stack/core`, `/server`, `/testing`; data from `@apex-stack/data`)
71
+ - **Route** → `defineApexRoute({ method, input: { …zod }, mcp: true, auth: true, can, handler })`. `mcp: true` makes it an AI-callable tool at `/mcp`.
72
+ - **Model** → `defineModel('posts', { fields: {…}, use: [timestamps(), owned()] })` → `.resource(handle)` mounts REST + MCP CRUD.
73
+ - **Auth** → `server/auth.ts` default-exports `sessionAuth({ password })`; gate routes with `auth: true` / `can`. In loaders, the user is `locals.user`.
74
+ - **Config** → `apex.config.ts`: `defineConfig({ runtimeConfig: { …, public: {…} }, i18n: {…} })`. Read with `useRuntimeConfig()`.
75
+ - **Test** → `createTestApp({ root })` → `app.get/post(path, body?, { user })`, `app.mcp.listTools()`.
76
+
77
+ The full stability contract of public APIs is in the framework's `API.md` — 🟢 Stable ones won't break; 🟡 Experimental ones might.
78
+
79
+ ## Deploy
80
+ `apex build --preset vercel|netlify|docker`. Set `DATABASE_URL` (Supabase/Neon/Postgres or Turso) + `APEX_SESSION_PASSWORD` (≥32 chars) in the host env. See the deploy docs.
81
+
82
+ ## Drive Apex as MCP tools (optional, powerful)
83
+ Apex ships an MCP server for its own CLI — run `apex mcp-server` (stdio). Point your
84
+ agent host at it to get `apex_make`, `apex_extend`, `apex_add`, `apex_build`, `apex_list`,
85
+ `apex_project_info`, `apex_check`, and `apex_test` as structured tools, so you scaffold,
86
+ type-check, and test by calling tools instead of shelling out.
87
+
88
+ Docs: https://apexjs.site/docs/