@apex-stack/core 0.1.19 → 0.2.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.
Files changed (40) hide show
  1. package/dist/{build-J47A3B4Y.js → build-VHS6KZBK.js} +91 -27
  2. package/dist/chunk-2C2HRLIY.js +18 -0
  3. package/dist/chunk-CHBSGOB3.js +42 -0
  4. package/dist/{chunk-JWYNLP4L.js → chunk-HCNNKT4A.js} +51 -9
  5. package/dist/chunk-JLIAISWM.js +48 -0
  6. package/dist/{chunk-XSN6NDWP.js → chunk-XDKJO6ZC.js} +159 -25
  7. package/dist/cli.js +65 -24
  8. package/dist/client.d.ts +1 -1
  9. package/dist/client.js +2 -1
  10. package/dist/{dev-OCVQRCCE.js → dev-G7HPP6KW.js} +6 -2
  11. package/dist/index.d.ts +88 -5
  12. package/dist/index.js +12 -4
  13. package/dist/{make-JAW22LQZ.js → make-VAYO5GWA.js} +71 -5
  14. package/dist/{mcp-DL4J6JFJ.js → mcp-CH7L4GF3.js} +1 -1
  15. package/dist/{migrate-NOGFOFV2.js → migrate-X6LIHMIE.js} +3 -1
  16. package/dist/{server-L3V34B5X.js → server-PTHGOE42.js} +63 -19
  17. package/dist/{start-AUJJ7HAY.js → start-3O3E43PT.js} +47 -10
  18. package/dist/upgrade-WC5F5FKY.js +168 -0
  19. package/package.json +5 -4
  20. package/templates/default/.env.example +9 -0
  21. package/templates/default/README.md +63 -17
  22. package/templates/default/_gitignore +5 -0
  23. package/templates/default/apex.config.ts +22 -0
  24. package/templates/default/components/Counter.alpine +15 -0
  25. package/templates/default/composables/useToggle.ts +14 -0
  26. package/templates/default/db/README.md +18 -0
  27. package/templates/default/layouts/default.alpine +16 -0
  28. package/templates/default/package.json +11 -2
  29. package/templates/default/pages/index.alpine +23 -92
  30. package/templates/default/public/.gitkeep +0 -0
  31. package/templates/default/server/api/hello.ts +10 -2
  32. package/templates/default/services/GreetingService.ts +12 -0
  33. package/templates/default/shared/types.ts +11 -0
  34. package/templates/default/stores/ui.ts +9 -0
  35. package/templates/default/tests/greeting.test.ts +12 -0
  36. package/templates/default/tsconfig.json +15 -0
  37. package/templates/default/vitest.config.ts +7 -0
  38. package/vscode/apex-alpine.vsix +0 -0
  39. package/dist/chunk-HRJTOSYH.js +0 -8
  40. package/dist/chunk-MZVLRU3R.js +0 -15
@@ -79,32 +79,98 @@ export default defineApexRoute({
79
79
  })
80
80
  `;
81
81
  }
82
+ function serviceTemplate(name) {
83
+ const cls = `${pascalCase(name)}Service`;
84
+ return `/**
85
+ * ${cls} \u2014 business logic as a plain, testable class. Keep routes and loaders
86
+ * thin and delegate to services like this one (the clean-code backbone).
87
+ */
88
+ export class ${cls} {
89
+ // Replace with your methods.
90
+ run(input: string): string {
91
+ return input
92
+ }
93
+ }
94
+ `;
95
+ }
96
+ function middlewareTemplate() {
97
+ return `import { defineMiddleware } from '@apex-stack/core'
98
+
99
+ // Runs on every request before the page/API handler. Attach request-scoped
100
+ // state to ctx.locals (read in a page loader via \`loader({ locals })\` and in
101
+ // route handlers via \`{ locals }\`), or return ctx.redirect('/path') to
102
+ // short-circuit. Files run in filename order \u2014 prefix with 01. / 02. to order.
103
+ export default defineMiddleware((ctx) => {
104
+ // ctx.locals.user = await getUser(ctx.headers)
105
+ // if (ctx.url.startsWith('/admin') && !ctx.locals.user) return ctx.redirect('/login')
106
+ })
107
+ `;
108
+ }
109
+ function testTemplate(name) {
110
+ return `import { describe, expect, it } from 'vitest'
111
+
112
+ describe('${name}', () => {
113
+ it('works', () => {
114
+ expect(true).toBe(true)
115
+ })
116
+ })
117
+ `;
118
+ }
82
119
  function plan(kind, name, root) {
83
120
  switch (kind) {
84
121
  case "page":
85
122
  return { path: join(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
86
123
  case "component":
87
- return { path: join(root, "components", `${pascalCase(name)}.alpine`), contents: componentTemplate() };
124
+ return {
125
+ path: join(root, "components", `${pascalCase(name)}.alpine`),
126
+ contents: componentTemplate()
127
+ };
88
128
  case "api":
89
129
  return { path: join(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
90
130
  case "store":
91
131
  return { path: join(root, "stores", `${name}.ts`), contents: storeTemplate(name) };
92
132
  case "layout":
93
133
  return { path: join(root, "layouts", `${name}.alpine`), contents: layoutTemplate() };
134
+ case "service":
135
+ return {
136
+ path: join(root, "services", `${pascalCase(name)}Service.ts`),
137
+ contents: serviceTemplate(name)
138
+ };
139
+ case "test":
140
+ return { path: join(root, "tests", `${name}.test.ts`), contents: testTemplate(name) };
141
+ case "middleware":
142
+ return { path: join(root, "middleware", `${name}.ts`), contents: middlewareTemplate() };
94
143
  }
95
144
  }
96
145
  var makeCommand = defineCommand({
97
- meta: { name: "make", description: "Generate a page, component, API route, store, or layout" },
146
+ meta: {
147
+ name: "make",
148
+ description: "Generate a page, component, API route, store, layout, service, test, or middleware"
149
+ },
98
150
  args: {
99
- kind: { type: "positional", required: true, description: "page | component | api | store | layout" },
151
+ kind: {
152
+ type: "positional",
153
+ required: true,
154
+ description: "page | component | api | store | layout | service | test | middleware"
155
+ },
100
156
  name: { type: "positional", required: true, description: "Name (about, Counter, todos, \u2026)" },
101
157
  root: { type: "string", description: "Project root", default: "." }
102
158
  },
103
159
  run({ args }) {
104
160
  const kind = args.kind;
105
- if (kind !== "page" && kind !== "component" && kind !== "api" && kind !== "store" && kind !== "layout") {
161
+ const kinds = [
162
+ "page",
163
+ "component",
164
+ "api",
165
+ "store",
166
+ "layout",
167
+ "service",
168
+ "test",
169
+ "middleware"
170
+ ];
171
+ if (!kinds.includes(kind)) {
106
172
  console.error(`
107
- Unknown type "${args.kind}". Use: page | component | api | store | layout
173
+ Unknown type "${args.kind}". Use: ${kinds.join(" | ")}
108
174
  `);
109
175
  process.exit(1);
110
176
  }
@@ -30,7 +30,7 @@ var mcpCommand = defineCommand({
30
30
  console.log(`
31
31
  \x1B[36m${args.call}\x1B[0m(${args.args}) \u2192`);
32
32
  for (const part of result.content) {
33
- console.log(" " + (part.text ?? JSON.stringify(part)));
33
+ console.log(` ${part.text ?? JSON.stringify(part)}`);
34
34
  }
35
35
  console.log();
36
36
  } else {
@@ -19,7 +19,9 @@ var migrateCommand = defineCommand({
19
19
  const require2 = createRequire(join(root, "package.json"));
20
20
  data = await import(pathToFileURL(require2.resolve("@apex-stack/data")).href);
21
21
  } catch {
22
- console.error("\n @apex-stack/data is not installed in this project. Run: npm i @apex-stack/data\n");
22
+ console.error(
23
+ "\n @apex-stack/data is not installed in this project. Run: npm i @apex-stack/data\n"
24
+ );
23
25
  process.exit(1);
24
26
  }
25
27
  const config = args.driver === "postgres" ? { driver: "postgres", url: args.url } : args.driver === "pglite" ? { driver: "pglite", dir: args.url } : resolve(root, args.db);
@@ -4,17 +4,20 @@ import {
4
4
  import {
5
5
  createApiHandler,
6
6
  createMcpHandler,
7
- loadApiRoutes
8
- } from "./chunk-JWYNLP4L.js";
9
- import "./chunk-HRJTOSYH.js";
7
+ loadApiRoutes,
8
+ loadMiddleware,
9
+ runMiddleware
10
+ } from "./chunk-HCNNKT4A.js";
11
+ import "./chunk-2C2HRLIY.js";
10
12
  import {
11
13
  loadStores,
12
14
  matchRoute,
13
15
  renderIslandsPage,
14
16
  renderPage,
17
+ resolveApexConfig,
15
18
  scanPages
16
- } from "./chunk-XSN6NDWP.js";
17
- import "./chunk-MZVLRU3R.js";
19
+ } from "./chunk-XDKJO6ZC.js";
20
+ import "./chunk-JLIAISWM.js";
18
21
 
19
22
  // src/dev/server.ts
20
23
  import { existsSync as existsSync2, readdirSync } from "fs";
@@ -27,6 +30,7 @@ import {
27
30
  createApp,
28
31
  defineEventHandler,
29
32
  fromNodeMiddleware,
33
+ getRequestHeaders,
30
34
  setResponseHeader,
31
35
  setResponseStatus,
32
36
  toNodeListener
@@ -36,20 +40,24 @@ import { createServer as createViteServer } from "vite";
36
40
  // src/dev/errorPage.ts
37
41
  import { existsSync, readFileSync } from "fs";
38
42
  function esc(s) {
39
- return s.replace(/[&<>"]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;");
43
+ return s.replace(
44
+ /[&<>"]/g,
45
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;"
46
+ );
40
47
  }
41
48
  function firstFileFrame(stack, root) {
42
49
  const re = /(?:file:\/\/\/?)?((?:[A-Za-z]:[\\/]|\/)[^\s():]+):(\d+):(\d+)/g;
43
- let m;
44
50
  const frames = [];
45
- while (m = re.exec(stack)) {
51
+ let m = re.exec(stack);
52
+ while (m) {
46
53
  const raw = m[1];
47
- if (!raw) continue;
48
- if (/^[A-Za-z]:[\\/]/.test(raw) === false && !raw.startsWith("/")) continue;
49
- const file = raw.replace(/\//g, process.platform === "win32" ? "\\" : "/");
50
- if (existsSync(file) && !file.includes("node_modules")) {
51
- frames.push({ file, line: Number(m[2] ?? 0), col: Number(m[3] ?? 0) });
54
+ if (raw && (/^[A-Za-z]:[\\/]/.test(raw) || raw.startsWith("/"))) {
55
+ const file = raw.replace(/\//g, process.platform === "win32" ? "\\" : "/");
56
+ if (existsSync(file) && !file.includes("node_modules")) {
57
+ frames.push({ file, line: Number(m[2] ?? 0), col: Number(m[3] ?? 0) });
58
+ }
52
59
  }
60
+ m = re.exec(stack);
53
61
  }
54
62
  return frames.find((f) => f.file.startsWith(root)) ?? frames[0];
55
63
  }
@@ -174,7 +182,9 @@ async function startDevServer(options) {
174
182
  plugins.unshift(tw());
175
183
  } catch {
176
184
  }
177
- const appCssRel = ["app.css", "styles/app.css", "src/app.css"].find((p) => existsSync2(join(options.root, p)));
185
+ const appCssRel = ["app.css", "styles/app.css", "src/app.css"].find(
186
+ (p) => existsSync2(join(options.root, p))
187
+ );
178
188
  const appCss = appCssRel ? `/${appCssRel}` : void 0;
179
189
  const vite = await createViteServer({
180
190
  root: options.root,
@@ -192,11 +202,43 @@ async function startDevServer(options) {
192
202
  const resolved = id[0] === "/" && !id.startsWith(options.root) ? join(options.root, id).replace(/\\/g, "/") : id;
193
203
  return vite.ssrLoadModule(resolved);
194
204
  };
205
+ const { runtimeConfig, publicConfig } = await resolveApexConfig(
206
+ options.root,
207
+ (id) => ssrLoad(id)
208
+ );
195
209
  const app = createApp();
196
210
  app.use(fromNodeMiddleware(vite.middlewares));
211
+ app.use(
212
+ defineEventHandler(async (event) => {
213
+ const mws = await loadMiddleware(options.root, (id) => ssrLoad(id));
214
+ if (!mws.length) return;
215
+ const { redirect, locals } = await runMiddleware(mws, {
216
+ url: event.path || "/",
217
+ method: event.method,
218
+ config: runtimeConfig,
219
+ headers: getRequestHeaders(event)
220
+ });
221
+ event.context.apexLocals = locals;
222
+ if (redirect) {
223
+ setResponseStatus(event, redirect.status);
224
+ setResponseHeader(event, "Location", redirect.to);
225
+ return "";
226
+ }
227
+ })
228
+ );
197
229
  const loadEntries = () => loadApiRoutes(options.root, (id) => ssrLoad(id));
198
- app.use("/api", defineEventHandler((event) => loadEntries().then((e) => createApiHandler(e)(event))));
199
- app.use("/mcp", defineEventHandler((event) => loadEntries().then((e) => createMcpHandler(e)(event))));
230
+ app.use(
231
+ "/api",
232
+ defineEventHandler(
233
+ (event) => loadEntries().then((e) => createApiHandler(e, runtimeConfig)(event))
234
+ )
235
+ );
236
+ app.use(
237
+ "/mcp",
238
+ defineEventHandler(
239
+ (event) => loadEntries().then((e) => createMcpHandler(e, runtimeConfig)(event))
240
+ )
241
+ );
200
242
  app.use(
201
243
  defineEventHandler(async (event) => {
202
244
  const url = event.path || "/";
@@ -226,6 +268,10 @@ async function startDevServer(options) {
226
268
  stores,
227
269
  appCss,
228
270
  layouts,
271
+ runtimeConfig,
272
+ publicConfig,
273
+ locals: event.context.apexLocals ?? {},
274
+ errorPageId: existsSync2(join(options.root, "pages", "error.alpine")) ? "/pages/error.alpine" : void 0,
229
275
  transformHtml: (u, doc) => vite.transformIndexHtml(u, doc)
230
276
  });
231
277
  setResponseHeader(event, "Content-Type", "text/html");
@@ -252,9 +298,7 @@ async function startDevServer(options) {
252
298
  port,
253
299
  close: async () => {
254
300
  await vite.close();
255
- await new Promise(
256
- (resolve, reject) => server.close((e) => e ? reject(e) : resolve())
257
- );
301
+ await new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve()));
258
302
  }
259
303
  };
260
304
  }
@@ -2,15 +2,17 @@ import {
2
2
  createApiHandler,
3
3
  createMcpHandler,
4
4
  expandApiModule,
5
- hasMcpRoutes
6
- } from "./chunk-JWYNLP4L.js";
7
- import "./chunk-HRJTOSYH.js";
5
+ hasMcpRoutes,
6
+ runMiddleware
7
+ } from "./chunk-HCNNKT4A.js";
8
+ import "./chunk-2C2HRLIY.js";
8
9
  import {
10
+ applyEnvToRuntimeConfig,
9
11
  matchRoute,
10
12
  renderIslandsPage,
11
13
  renderPage
12
- } from "./chunk-XSN6NDWP.js";
13
- import "./chunk-MZVLRU3R.js";
14
+ } from "./chunk-XDKJO6ZC.js";
15
+ import "./chunk-JLIAISWM.js";
14
16
 
15
17
  // src/commands/start.ts
16
18
  import { existsSync as existsSync2 } from "fs";
@@ -18,13 +20,14 @@ import { join as join2, resolve } from "path";
18
20
  import { defineCommand } from "citty";
19
21
 
20
22
  // src/prod/server.ts
21
- import { createServer as createHttpServer } from "http";
22
23
  import { existsSync, readFileSync, statSync } from "fs";
24
+ import { createServer as createHttpServer } from "http";
23
25
  import { join } from "path";
24
26
  import { pathToFileURL } from "url";
25
27
  import {
26
28
  createApp,
27
29
  defineEventHandler,
30
+ getRequestHeaders,
28
31
  getRequestURL,
29
32
  setResponseHeader,
30
33
  setResponseStatus,
@@ -46,6 +49,11 @@ async function startProdServer(options) {
46
49
  const dir = options.dir;
47
50
  const port = options.port ?? 3e3;
48
51
  const manifest = JSON.parse(readFileSync(join(dir, "apex-manifest.json"), "utf8"));
52
+ const runtimeConfig = applyEnvToRuntimeConfig(
53
+ manifest.runtimeConfig ?? { public: {} },
54
+ process.cwd()
55
+ );
56
+ const publicConfig = runtimeConfig.public ?? {};
49
57
  const importServer = (relFile) => import(pathToFileURL(join(dir, "server", relFile)).href);
50
58
  const registry = {};
51
59
  let componentCss = "";
@@ -60,7 +68,13 @@ async function startProdServer(options) {
60
68
  const mod = await importServer(serverFile);
61
69
  apiEntries.push(...expandApiModule(name, mod.default));
62
70
  }
71
+ const middleware = [];
72
+ for (const { serverFile } of manifest.middleware ?? []) {
73
+ const mod = await importServer(serverFile);
74
+ if (typeof mod.default === "function") middleware.push(mod.default);
75
+ }
63
76
  const serverFileFor = new Map(manifest.routes.map((r) => [r.pageId, r.serverFile]));
77
+ const errorPageId = serverFileFor.has("/pages/error.alpine") ? "/pages/error.alpine" : void 0;
64
78
  const loadModule = (id) => importServer(serverFileFor.get(id));
65
79
  const app = createApp();
66
80
  app.use(
@@ -72,12 +86,31 @@ async function startProdServer(options) {
72
86
  if (!file.startsWith(dir) || !existsSync(file) || !statSync(file).isFile()) return;
73
87
  const ext = path.slice(path.lastIndexOf("."));
74
88
  setResponseHeader(event, "Content-Type", MIME[ext] ?? "application/octet-stream");
75
- if (path.startsWith("/assets/")) setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
89
+ if (path.startsWith("/assets/"))
90
+ setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
76
91
  return readFileSync(file);
77
92
  })
78
93
  );
79
- if (apiEntries.length) app.use("/api", createApiHandler(apiEntries));
80
- if (hasMcpRoutes(apiEntries)) app.use("/mcp", createMcpHandler(apiEntries));
94
+ if (middleware.length) {
95
+ app.use(
96
+ defineEventHandler(async (event) => {
97
+ const { redirect, locals } = await runMiddleware(middleware, {
98
+ url: getRequestURL(event).pathname,
99
+ method: event.method,
100
+ config: runtimeConfig,
101
+ headers: getRequestHeaders(event)
102
+ });
103
+ event.context.apexLocals = locals;
104
+ if (redirect) {
105
+ setResponseStatus(event, redirect.status);
106
+ setResponseHeader(event, "Location", redirect.to);
107
+ return "";
108
+ }
109
+ })
110
+ );
111
+ }
112
+ if (apiEntries.length) app.use("/api", createApiHandler(apiEntries, runtimeConfig));
113
+ if (hasMcpRoutes(apiEntries)) app.use("/mcp", createMcpHandler(apiEntries, runtimeConfig));
81
114
  app.use(
82
115
  defineEventHandler(async (event) => {
83
116
  const url = getRequestURL(event).pathname;
@@ -96,7 +129,11 @@ async function startProdServer(options) {
96
129
  url,
97
130
  registry,
98
131
  componentCss,
99
- clientHref: route?.clientHref
132
+ clientHref: route?.clientHref,
133
+ runtimeConfig,
134
+ publicConfig,
135
+ locals: event.context.apexLocals ?? {},
136
+ errorPageId
100
137
  });
101
138
  setResponseHeader(event, "Content-Type", "text/html");
102
139
  return html;
@@ -0,0 +1,168 @@
1
+ import {
2
+ offerExtension
3
+ } from "./chunk-CHBSGOB3.js";
4
+ import {
5
+ VERSION,
6
+ banner,
7
+ color
8
+ } from "./chunk-QIXJSQLW.js";
9
+
10
+ // src/commands/upgrade.ts
11
+ import { spawnSync } from "child_process";
12
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
13
+ import { basename, dirname, join, relative, resolve } from "path";
14
+ import { fileURLToPath } from "url";
15
+ import { defineCommand } from "citty";
16
+ var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
17
+ var PROTECTED = /* @__PURE__ */ new Set(["package.json"]);
18
+ function projectName(root) {
19
+ try {
20
+ return JSON.parse(readFileSync(join(root, "package.json"), "utf8")).name || basename(root);
21
+ } catch {
22
+ return basename(root);
23
+ }
24
+ }
25
+ function bumpApexDeps(root) {
26
+ const pkgPath = join(root, "package.json");
27
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
28
+ let bumped = 0;
29
+ for (const field of ["dependencies", "devDependencies"]) {
30
+ const deps = pkg[field];
31
+ if (!deps) continue;
32
+ for (const dep of Object.keys(deps)) {
33
+ if (dep.startsWith("@apex-stack/") && /^[\d^~]/.test(deps[dep] ?? "")) {
34
+ if (deps[dep] !== `^${VERSION}`) {
35
+ deps[dep] = `^${VERSION}`;
36
+ bumped++;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ if (bumped) writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
42
+ `);
43
+ return bumped;
44
+ }
45
+ function detectPm() {
46
+ const ua = process.env.npm_config_user_agent || "";
47
+ if (ua.startsWith("pnpm")) return "pnpm";
48
+ if (ua.startsWith("yarn")) return "yarn";
49
+ if (ua.startsWith("bun")) return "bun";
50
+ return "npm";
51
+ }
52
+ function walk(dir, onFile) {
53
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
54
+ const p = join(dir, entry.name);
55
+ if (entry.isDirectory()) walk(p, onFile);
56
+ else onFile(p);
57
+ }
58
+ }
59
+ function targetRelPath(rel) {
60
+ return basename(rel) === "_gitignore" ? join(dirname(rel), ".gitignore") : rel;
61
+ }
62
+ var upgradeCommand = defineCommand({
63
+ meta: {
64
+ name: "upgrade",
65
+ description: "Adopt new scaffold defaults in an existing app (non-destructive)"
66
+ },
67
+ args: {
68
+ root: { type: "positional", required: false, description: "Project root", default: "." },
69
+ force: {
70
+ type: "boolean",
71
+ default: false,
72
+ description: "Re-sync files that differ from the template (package.json is always preserved)"
73
+ },
74
+ install: {
75
+ type: "boolean",
76
+ default: true,
77
+ description: "Run the package manager after bumping @apex-stack/* (use --no-install to skip)"
78
+ },
79
+ vscode: {
80
+ type: "boolean",
81
+ description: "Install the Apex VS Code extension (skip the prompt)"
82
+ }
83
+ },
84
+ async run({ args }) {
85
+ const root = resolve(process.cwd(), String(args.root));
86
+ const log = console.log;
87
+ if (!existsSync(join(root, "package.json"))) {
88
+ console.error(`
89
+ ${color.red("\u2717")} No package.json in ${root} \u2014 is this an Apex project?
90
+ `);
91
+ process.exit(1);
92
+ }
93
+ const name = projectName(root);
94
+ process.stdout.write(banner());
95
+ const added = [];
96
+ const updated = [];
97
+ let unchanged = 0;
98
+ walk(TEMPLATE_DIR, (absFile) => {
99
+ const rel = targetRelPath(relative(TEMPLATE_DIR, absFile));
100
+ const target = join(root, rel);
101
+ let content = readFileSync(absFile, "utf8");
102
+ if (content.includes("{{name}}")) content = content.replaceAll("{{name}}", name);
103
+ if (!existsSync(target)) {
104
+ mkdirSync(dirname(target), { recursive: true });
105
+ writeFileSync(target, content);
106
+ added.push(rel);
107
+ return;
108
+ }
109
+ if (args.force && !PROTECTED.has(rel)) {
110
+ if (readFileSync(target, "utf8") !== content) {
111
+ writeFileSync(target, content);
112
+ updated.push(rel);
113
+ return;
114
+ }
115
+ }
116
+ unchanged++;
117
+ });
118
+ if (added.length) {
119
+ log(`
120
+ ${color.green("+")} Added ${added.length} new file(s):`);
121
+ for (const f of added.sort()) log(` ${color.green(f)}`);
122
+ }
123
+ if (updated.length) {
124
+ log(`
125
+ ${color.cyan("~")} Re-synced ${updated.length} file(s):`);
126
+ for (const f of updated.sort()) log(` ${color.cyan(f)}`);
127
+ }
128
+ if (added.length || updated.length) {
129
+ log(`
130
+ ${color.gray(`${unchanged} existing file(s) left untouched.`)}`);
131
+ }
132
+ const bumped = bumpApexDeps(root);
133
+ if (bumped)
134
+ log(`
135
+ ${color.cyan("\u2191")} Bumped ${bumped} @apex-stack/* dependency \u2192 ^${VERSION}`);
136
+ if (!added.length && !updated.length && !bumped) {
137
+ log(`
138
+ ${color.green("\u2713")} Already up to date.`);
139
+ }
140
+ if (bumped && args.install) {
141
+ const pm = detectPm();
142
+ log(`
143
+ ${color.gray(`Installing with ${pm}\u2026`)}`);
144
+ const ok = spawnSync(pm, ["install"], {
145
+ cwd: root,
146
+ stdio: "inherit",
147
+ shell: process.platform === "win32"
148
+ }).status === 0;
149
+ log(
150
+ ok ? ` ${color.green("\u2713")} Dependencies updated` : ` ${color.red("\u2717")} Install failed \u2014 run ${color.cyan(`${pm} install`)} yourself`
151
+ );
152
+ } else if (bumped) {
153
+ log(
154
+ ` ${color.gray("Run")} ${color.cyan(`${detectPm()} install`)} ${color.gray("to apply.")}`
155
+ );
156
+ }
157
+ const ext = await offerExtension(args.vscode);
158
+ if (ext) log(` ${color.green("\u2713")} ${ext}`);
159
+ log(
160
+ `
161
+ ${color.gray("Non-destructive: your files are never overwritten")}${args.force ? color.gray(" except with --force") : color.gray(" (use --force to re-sync)")}${color.gray("; package.json entries are only version-bumped.")}
162
+ `
163
+ );
164
+ }
165
+ });
166
+ export {
167
+ upgradeCommand
168
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-stack/core",
3
- "version": "0.1.19",
3
+ "version": "0.2.0",
4
4
  "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,7 +37,8 @@
37
37
  },
38
38
  "files": [
39
39
  "dist",
40
- "templates"
40
+ "templates",
41
+ "vscode"
41
42
  ],
42
43
  "dependencies": {
43
44
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -45,8 +46,8 @@
45
46
  "h3": "^1.13.0",
46
47
  "vite": "^6.0.7",
47
48
  "zod": "^4.4.3",
48
- "@apex-stack/kit": "0.1.5",
49
- "@apex-stack/vite": "0.1.5"
49
+ "@apex-stack/kit": "0.2.0",
50
+ "@apex-stack/vite": "0.1.6"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "alpinejs": "^3.14.0"
@@ -0,0 +1,9 @@
1
+ # Copy to `.env` and fill in. These override the defaults in apex.config.ts.
2
+ # Private keys: APEX_<KEY>. Public keys (sent to the browser): APEX_PUBLIC_<KEY>.
3
+
4
+ # Private — server-only
5
+ APEX_API_SECRET=
6
+
7
+ # Public — readable in the browser
8
+ APEX_PUBLIC_APP_NAME={{name}}
9
+ APEX_PUBLIC_SITE_URL=http://localhost:3000