statikapi 0.1.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/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # statikapi — Static API generator (CLI)
2
+
3
+ Build a folder of JSON endpoints from simple files, then preview them in a lightweight UI.
4
+
5
+ > Requires **Node 18+**.
6
+
7
+ ## Install
8
+
9
+ Use without installing (recommended):
10
+
11
+ ```
12
+ npx statikapi --help
13
+
14
+ Or add to a project
15
+ pnpm add -D statikapi
16
+ # npm i -D statikapi
17
+ # yarn add -D statikapi
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ ```
23
+ statikapi <command> [options]
24
+
25
+ Commands:
26
+ init Scaffold a new StatikAPI project
27
+ build Build static JSON endpoints
28
+ dev Watch & rebuild on changes
29
+ preview Serve the built JSON files + UI
30
+
31
+ Global:
32
+ -h, --help Show help
33
+ -v, --version Show version
34
+ ```
35
+
36
+ ##Quick start
37
+
38
+ ```
39
+ # 1) Create a folder with API sources
40
+
41
+ mkdir src-api
42
+ echo "export default { hello: 'world' }" > src-api/index.js
43
+
44
+ # 2) Build
45
+
46
+ npx statikapi build --pretty
47
+
48
+ # 3) Preview (opens http://127.0.0.1:8788/_ui)
49
+
50
+ npx statikapi preview
51
+ ```
52
+
53
+ ## Project layout
54
+
55
+ - src-api/: your source files (default; configurable)
56
+ - api-out/: generated JSON, one folder per route (default)
57
+
58
+ Examples of file → route mapping:
59
+
60
+ File Route Output file
61
+ src-api/index.js / api-out/index.json
62
+ src-api/blog/archive.js /blog/archive api-out/blog/archive/index.json
63
+ src-api/users/[id].js /users/:id dynamic (see below)
64
+ src-api/docs/[...slug].js /docs/\*slug catch-all (see below)
65
+
66
+ ## Dynamic routes
67
+
68
+ For src-api/users/[id].js, export a paths() function that returns the concrete IDs to prebuild:
69
+
70
+ ```
71
+ // src-api/users/[id].js
72
+ export async function paths() {
73
+ return ['1', '2', '3']; // builds /users/1, /users/2, /users/3
74
+ }
75
+
76
+ export async function data({ params }) {
77
+ return { id: params.id };
78
+ }
79
+ ```
80
+
81
+ Catch-all works similarly:
82
+
83
+ ```
84
+ // src-api/docs/[...slug].js
85
+ export async function paths() {
86
+ return [['a', 'b'], ['guide']]; // → /docs/a/b and /docs/guide
87
+ }
88
+ export async function data({ params }) {
89
+ return { slug: params.slug, path: params.slug.join('/') };
90
+ }
91
+ ```
92
+
93
+ ## Producing data
94
+
95
+ Each module can export either:
96
+
97
+ - `export async function data(ctx) { ... }` → its return value is serialized, or
98
+ - `export default <value|function>` → if a function, it’s called and awaited.
99
+
100
+ Returned data must be JSON-serializable (plain objects/arrays, finite numbers, no functions, etc.).
101
+
102
+ ## Config
103
+
104
+ You can optionally add statikapi.config.js in your project root:
105
+
106
+ ```
107
+ export default {
108
+ srcDir: 'src-api',
109
+ outDir: 'api-out',
110
+ };
111
+ ```
112
+
113
+ You can override via flags: `--srcDir <dir>`, `--outDir <dir>`.
114
+
115
+ ## Flags (per command)
116
+
117
+ `build`
118
+
119
+ - `--pretty` (or `--minify=false`) — pretty-print JSON.
120
+ - `--srcDir`, `--outDir` — override config paths.
121
+
122
+ `dev`
123
+
124
+ - Rebuilds on changes, updates the preview UI via SSE.
125
+ - `--previewHost`, `--previewPort` — where to notify the preview server.
126
+ - `--srcDir`, `--outDir` — override config paths.
127
+
128
+ `preview`
129
+
130
+ - Serves `api-out/` and the UI at `/\_ui`.
131
+ - `--host` (default 127.0.0.1)
132
+ - `--port` (default 8788)
133
+ - `--open` — try to open the browser
134
+ - UI source:
135
+ - `--uiDir <path>` — serve a built UI from this directory
136
+ - Otherwise, uses the embedded UI bundled with the CLI
137
+ - If missing, proxies to a dev UI at `http://127.0.0.1:5173` (override with `--uiDevHost`, `--uiDevPort`)
138
+
139
+ ## Examples
140
+
141
+ There are two example projects in this repo under `example/`:
142
+
143
+ ```
144
+ # from repo root
145
+
146
+ pnpm -C example/basic dev
147
+ pnpm -C example/basic preview
148
+
149
+ pnpm -C example/dynamic dev
150
+ pnpm -C example/dynamic preview
151
+ ```
152
+
153
+ ## Troubleshooting
154
+
155
+ - UI doesn’t load: ensure `preview` is running; if you’re developing the UI separately, start Vite on port 5173 or pass `--uiDir` to serve a built UI.
156
+ - Dynamic routes not emitted: make sure the file exports a valid `paths()` function returning strings (or arrays of strings for catch-all).
157
+
158
+ License
159
+
160
+ ISC – see LICENSE
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/index.js';
3
+
4
+ // pass argv explicitly
5
+ const code = await run(process.argv.slice(2));
6
+
7
+ // let stdout/stderr flush; still returns non-zero on failure
8
+ process.exitCode = Number.isInteger(code) ? code : 0;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "statikapi",
3
+ "version": "0.1.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/zonayedpca/statikapi",
7
+ "directory": "packages/cli"
8
+ },
9
+ "bugs": { "url": "https://github.com/zonayedpca/statikapi/issues" },
10
+ "homepage": "https://github.com/zonayedpca/statikapi#readme",
11
+ "type": "module",
12
+ "bin": { "statikapi": "bin/statikapi.js" },
13
+ "files": ["bin", "dist", "ui", "src", "README.md", "LICENSE"],
14
+ "exports": { ".": { "import": "./src/index.js" } },
15
+ "engines": { "node": ">=18" },
16
+ "license": "MIT",
17
+ "keywords": ["static", "json", "api", "cli", "ssg"],
18
+ "publishConfig": { "access": "public", "provenance": true },
19
+ "dependencies": {
20
+ "chokidar": "^3.6.0"
21
+ },
22
+ "scripts": {
23
+ "build": "node ./scripts/build.js || true",
24
+ "sync-ui": "rm -rf ui && mkdir -p ui && cp -R ../ui/dist/* ui/",
25
+ "prepack": "pnpm -w --filter @statikapi/ui build && pnpm run sync-ui"
26
+ }
27
+ }
@@ -0,0 +1,8 @@
1
+ import path from 'node:path';
2
+
3
+ /** Map a route to an output JSON file (index.json style). Static routes only. */
4
+ export function routeToOutPath({ outAbs, route }) {
5
+ // '/' -> index.json, '/users' -> users/index.json, '/blog/archive' -> blog/archive/index.json
6
+ const rel = route === '/' ? 'index.json' : route.slice(1) + '/index.json';
7
+ return path.join(outAbs, rel);
8
+ }
@@ -0,0 +1,191 @@
1
+ import { readFlags } from '../util/readFlags.js'; // from Task 3
2
+ import { loadConfig } from '../config/loadConfig.js'; // from Task 3
3
+ import { ConfigError } from '../config/validate.js'; // from Task 3
4
+ import { mapRoutes } from '../router/mapRoutes.js'; // from Task 4
5
+ import { loadModuleValue } from '../loader/loadModuleValue.js'; // from Task 5
6
+ import { loadPaths } from '../loader/loadPaths.js';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs/promises';
9
+ import crypto from 'node:crypto';
10
+
11
+ import { emptyDir, writeFileEnsured } from '../util/fsx.js';
12
+ import { routeToOutPath } from '../build/routeOutPath.js';
13
+ import { formatBytes } from '../util/bytes.js';
14
+
15
+ function toConcrete(routePattern, segTokens, segs) {
16
+ // segTokens: ['users', ':id'] or ['docs','*slug']
17
+ // segs: ['1'] or ['a','b']
18
+ let idx = 0;
19
+ const parts = routePattern.split('/').map((p) => {
20
+ if (p.startsWith(':')) return segs[idx++] ?? '';
21
+ if (p.startsWith('*')) return segs.slice(idx).join('/');
22
+ return p;
23
+ });
24
+ const concrete = parts.join('/').replace(/\/+/g, '/');
25
+ return concrete;
26
+ }
27
+
28
+ function toParams(segTokens, concreteRoute) {
29
+ const concreteSegs = concreteRoute.split('/').filter(Boolean);
30
+ const params = {};
31
+
32
+ for (let i = 0; i < segTokens.length; i++) {
33
+ const tok = segTokens[i];
34
+ if (tok.startsWith(':')) {
35
+ params[tok.slice(1)] = concreteSegs[i] ?? '';
36
+ } else if (tok.startsWith('*')) {
37
+ params[tok.slice(1)] = concreteSegs.slice(i);
38
+ break;
39
+ }
40
+ }
41
+ return params;
42
+ }
43
+
44
+ export default async function buildCmd(argv) {
45
+ const t0 = Date.now();
46
+ try {
47
+ const flags = readFlags(argv);
48
+ const { config } = await loadConfig({ flags });
49
+
50
+ const pretty = flags.pretty === true || flags.minify === false;
51
+ const space = pretty ? 2 : 0;
52
+
53
+ // keep legacy-friendly stub line so the old test passes
54
+ console.log('statikapi build → building JSON endpoints (MVP)');
55
+
56
+ // discover routes
57
+ const routes = await mapRoutes({ srcAbs: config.paths.srcAbs });
58
+
59
+ // MVP: only handle static routes (dynamic/catch-all in next task)
60
+ const staticRoutes = routes.filter((r) => r.type === 'static');
61
+ const dynRoutes = routes.filter((r) => r.type === 'dynamic');
62
+ const catRoutes = routes.filter((r) => r.type === 'catchall');
63
+
64
+ // prepare outDir (clean, then write)
65
+ await emptyDir(config.paths.outAbs);
66
+
67
+ let fileCount = 0;
68
+ let byteCount = 0;
69
+ let skippedDynamic = 0;
70
+ const manifest = []; // array of unified entries
71
+
72
+ const digest = (s) => crypto.createHash('sha1').update(s).digest('hex');
73
+ const relSrc = (abs) => {
74
+ try {
75
+ return path.relative(process.cwd(), abs) || abs;
76
+ } catch {
77
+ return abs;
78
+ }
79
+ };
80
+
81
+ const relOut = (abs) => {
82
+ try {
83
+ return path.relative(process.cwd(), abs).replaceAll(path.sep, '/') || abs;
84
+ } catch {
85
+ return abs;
86
+ }
87
+ };
88
+
89
+ async function pushManifest({ route, srcFile, outFile, json }) {
90
+ const st = await fs.stat(outFile).catch(() => null);
91
+ const entry = {
92
+ // stable field order
93
+ route,
94
+ outFile: relOut(outFile),
95
+ srcFile: relSrc(srcFile),
96
+ // backward-compat (old tests read filePath → output path)
97
+ filePath: relOut(outFile),
98
+ bytes: Buffer.byteLength(json),
99
+ mtime: st ? st.mtimeMs : Date.now(),
100
+ hash: digest(json),
101
+ revalidate: null,
102
+ };
103
+ manifest.push(entry);
104
+ }
105
+
106
+ for (const r of staticRoutes) {
107
+ const val = await loadModuleValue(r.file);
108
+ const json = JSON.stringify(val, null, space) + (pretty ? '\n' : '');
109
+ const outFile = routeToOutPath({ outAbs: config.paths.outAbs, route: r.route });
110
+ await writeFileEnsured(outFile, json);
111
+ fileCount++;
112
+ byteCount += Buffer.byteLength(json);
113
+
114
+ await pushManifest({ route: r.route, srcFile: r.file, outFile, json });
115
+ }
116
+
117
+ // helper: materialize a concrete route from tokens + param segments
118
+ async function emitConcreteRoute(r, segs) {
119
+ const concreteRoute = toConcrete(r.route, r.segments, segs);
120
+ const params = toParams(r.segments, concreteRoute);
121
+ const val = await loadModuleValue(r.file, { params });
122
+ const json = JSON.stringify(val, null, space) + (pretty ? '\n' : '');
123
+ const outFile = routeToOutPath({ outAbs: config.paths.outAbs, route: concreteRoute });
124
+ await writeFileEnsured(outFile, json);
125
+ fileCount++;
126
+ byteCount += Buffer.byteLength(json);
127
+
128
+ await pushManifest({ route: concreteRoute, srcFile: r.file, outFile, json });
129
+ }
130
+
131
+ // dynamic: expect [['val'], ...] from loadPaths()
132
+ for (const r of dynRoutes) {
133
+ const list = await loadPaths(r.file, r);
134
+ if (!list) {
135
+ skippedDynamic++;
136
+ continue;
137
+ }
138
+ const seen = new Set();
139
+ for (const segs of list) {
140
+ const concrete = toConcrete(r.route, r.segments, segs);
141
+ if (seen.has(concrete)) continue;
142
+ seen.add(concrete);
143
+ await emitConcreteRoute(r, segs);
144
+ }
145
+ }
146
+
147
+ // catch-all: expect [['a','b'], ['guide'], ...]
148
+ for (const r of catRoutes) {
149
+ const list = await loadPaths(r.file, r);
150
+ if (!list) {
151
+ skippedDynamic++;
152
+ continue;
153
+ }
154
+ const seen = new Set();
155
+ for (const segs of list) {
156
+ const concrete = toConcrete(r.route, r.segments, segs);
157
+ if (seen.has(concrete)) continue;
158
+ seen.add(concrete);
159
+ await emitConcreteRoute(r, segs);
160
+ }
161
+ }
162
+
163
+ // Write manifest once (sorted for determinism)
164
+ const manifestPath = path.join(config.paths.outAbs, '.statikapi', 'manifest.json');
165
+ const sorted = manifest.sort((a, b) => a.route.localeCompare(b.route));
166
+ const manifestJson = JSON.stringify(sorted, null, pretty ? 2 : 0) + (pretty ? '\n' : '');
167
+ await writeFileEnsured(manifestPath, manifestJson);
168
+ byteCount += Buffer.byteLength(manifestJson);
169
+ fileCount++;
170
+
171
+ const elapsed = Date.now() - t0;
172
+
173
+ const extra = skippedDynamic ? `, skipped ${skippedDynamic} dynamic route(s)` : '';
174
+ console.log(
175
+ `[statikapi] wrote ${fileCount} file(s), ${formatBytes(byteCount)} in ${elapsed} ms${extra}`
176
+ );
177
+ return 0;
178
+ } catch (err) {
179
+ if (err instanceof ConfigError) {
180
+ console.error(`[statikapi] Config error: ${err.message}`);
181
+ return 1;
182
+ }
183
+ // LoaderError already includes file path; show as-is
184
+ if (err && err.name === 'LoaderError') {
185
+ console.error(`[statikapi] ${err.message}`);
186
+ return 1;
187
+ }
188
+ console.error('[statikapi] Build failed:', err?.stack || err?.message || err);
189
+ return 1;
190
+ }
191
+ }
@@ -0,0 +1,249 @@
1
+ import chokidar from 'chokidar';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import crypto from 'node:crypto';
5
+ import { readFlags } from '../util/readFlags.js';
6
+ import { loadConfig } from '../config/loadConfig.js';
7
+ import { mapRoutes, fileToRoute } from '../router/mapRoutes.js';
8
+ import { routeToOutPath } from '../build/routeOutPath.js';
9
+ import { writeFileEnsured } from '../util/fsx.js';
10
+ import { loadModuleValue } from '../loader/loadModuleValue.js';
11
+ import { loadPaths } from '../loader/loadPaths.js';
12
+
13
+ function clearScreen() {
14
+ process.stdout.write('\x1Bc'); // ANSI "clear screen"
15
+ }
16
+
17
+ function toConcrete(routePattern, segTokens, segs) {
18
+ let idx = 0;
19
+ const parts = routePattern.split('/').map((p) => {
20
+ if (p.startsWith(':')) return segs[idx++] ?? '';
21
+ if (p.startsWith('*')) return segs.slice(idx).join('/');
22
+ return p;
23
+ });
24
+ return parts.join('/').replace(/\/+/g, '/');
25
+ }
26
+
27
+ function toParams(segTokens, concreteRoute) {
28
+ const concreteSegs = concreteRoute.split('/').filter(Boolean);
29
+ const params = {};
30
+ for (let i = 0; i < segTokens.length; i++) {
31
+ const tok = segTokens[i];
32
+ if (tok.startsWith(':')) params[tok.slice(1)] = concreteSegs[i] ?? '';
33
+ else if (tok.startsWith('*')) {
34
+ params[tok.slice(1)] = concreteSegs.slice(i);
35
+ break;
36
+ }
37
+ }
38
+ return params;
39
+ }
40
+
41
+ export default async function devCmd(argv) {
42
+ // In non-TTY (like node --test), behave like the old stub so tests don't hang.
43
+ if (!process.stdout.isTTY) {
44
+ console.log('statikapi dev → starting dev server (stub)');
45
+ return 0;
46
+ }
47
+
48
+ const flags = readFlags(argv);
49
+ const { config } = await loadConfig({ flags });
50
+
51
+ // Where to notify preview
52
+ const previewHost = String(flags.previewHost ?? '127.0.0.1');
53
+ const previewPort = Number.isFinite(flags.previewPort) ? Number(flags.previewPort) : 8788;
54
+ const notifyOrigin = `http://${previewHost}:${previewPort}`;
55
+
56
+ async function notifyChanged(route) {
57
+ try {
58
+ // Node 18+ has global fetch
59
+ const u = `${notifyOrigin}/_ui/changed?route=${encodeURIComponent(route)}`;
60
+ await fetch(u, { method: 'POST' }).catch(() => {});
61
+ } catch {
62
+ // Ignore if preview isn't running
63
+ }
64
+ }
65
+
66
+ // Cache of outputs per source file (for deletions on subsequent rebuilds)
67
+ const lastEmitted = new Map(); // fileAbs -> Set<concreteRoute>
68
+
69
+ // Manifest state
70
+ const manifestByRoute = new Map(); // route -> entry
71
+ const digest = (s) => crypto.createHash('sha1').update(s).digest('hex');
72
+ const relSrc = (abs) => {
73
+ try {
74
+ return path.relative(process.cwd(), abs) || abs;
75
+ } catch {
76
+ return abs;
77
+ }
78
+ };
79
+ const relOut = (abs) => {
80
+ try {
81
+ return path.relative(process.cwd(), abs).replaceAll(path.sep, '/') || abs;
82
+ } catch {
83
+ return abs;
84
+ }
85
+ };
86
+ async function writeManifest() {
87
+ const list = Array.from(manifestByRoute.values()).sort((a, b) =>
88
+ a.route.localeCompare(b.route)
89
+ );
90
+ const json = JSON.stringify(list, null, 2) + '\n';
91
+ await writeFileEnsured(path.join(config.paths.outAbs, '.statikapi', 'manifest.json'), json);
92
+ }
93
+ async function upsertManifest({ route, srcFile, outFile, json }) {
94
+ const st = await fs.stat(outFile).catch(() => null);
95
+
96
+ const entry = {
97
+ route,
98
+ outFile: relOut(outFile),
99
+ srcFile: relSrc(srcFile),
100
+ filePath: relOut(outFile), // backward-compat alias
101
+ bytes: Buffer.byteLength(json),
102
+ mtime: st ? st.mtimeMs : Date.now(),
103
+ hash: digest(json),
104
+ revalidate: null,
105
+ };
106
+ manifestByRoute.set(route, entry);
107
+ }
108
+ function deleteFromManifest(route) {
109
+ manifestByRoute.delete(route);
110
+ }
111
+
112
+ async function emitStatic(r, { fresh = false } = {}) {
113
+ const val = await loadModuleValue(r.file, { __fresh: fresh });
114
+ const json = JSON.stringify(val, null, 2) + '\n';
115
+ const outFile = routeToOutPath({ outAbs: config.paths.outAbs, route: r.route });
116
+ await writeFileEnsured(outFile, json);
117
+ lastEmitted.set(r.file, new Set([r.route]));
118
+ await upsertManifest({ route: r.route, srcFile: r.file, outFile, json });
119
+ // notify UI
120
+ await notifyChanged(r.route);
121
+ return 1;
122
+ }
123
+
124
+ async function emitDynamic(r, { fresh = false } = {}) {
125
+ const list = await loadPaths(r.file, r, { fresh });
126
+ if (!list) {
127
+ lastEmitted.set(r.file, new Set());
128
+ return { written: 0, skipped: 1 };
129
+ }
130
+ const seen = new Set();
131
+ const emittedRoutes = new Set();
132
+ let written = 0;
133
+ for (const segs of list) {
134
+ const concrete = toConcrete(r.route, r.segments, segs);
135
+ if (seen.has(concrete)) continue;
136
+ seen.add(concrete);
137
+ const params = toParams(r.segments, concrete);
138
+ const val = await loadModuleValue(r.file, { params, __fresh: fresh });
139
+ const json = JSON.stringify(val, null, 2) + '\n';
140
+ const outFile = routeToOutPath({ outAbs: config.paths.outAbs, route: concrete });
141
+ await writeFileEnsured(outFile, json);
142
+ emittedRoutes.add(concrete);
143
+ await upsertManifest({ route: concrete, srcFile: r.file, outFile, json });
144
+ written++;
145
+ }
146
+ // Delete stale outputs from this file (not present anymore)
147
+ const prev = lastEmitted.get(r.file) || new Set();
148
+ for (const oldRoute of prev) {
149
+ if (!emittedRoutes.has(oldRoute)) {
150
+ const p = routeToOutPath({ outAbs: config.paths.outAbs, route: oldRoute });
151
+ try {
152
+ await fs.rm(p, { force: true });
153
+ } catch {
154
+ // ignore
155
+ }
156
+ deleteFromManifest(oldRoute);
157
+ await notifyChanged(oldRoute);
158
+ }
159
+ }
160
+ lastEmitted.set(r.file, emittedRoutes);
161
+
162
+ // notify UI for all emitted routes
163
+ for (const route of emittedRoutes) {
164
+ await notifyChanged(route);
165
+ }
166
+
167
+ return { written, skipped: 0 };
168
+ }
169
+
170
+ function shouldHandle(fileAbs) {
171
+ const rel = path.posix.normalize(
172
+ fileAbs.replaceAll(path.sep, '/').slice(config.paths.srcAbs.length + 1)
173
+ );
174
+ if (!rel) return false;
175
+ if (rel.startsWith('_')) return false;
176
+ const ext = path.extname(rel);
177
+ return ext === '.js' || ext === '.mjs' || ext === '.cjs';
178
+ }
179
+
180
+ async function buildOne(fileAbs, kind) {
181
+ if (!shouldHandle(fileAbs)) return;
182
+ const info = fileToRoute({ srcAbs: config.paths.srcAbs, fileAbs });
183
+ clearScreen();
184
+ console.log(`statikapi dev → ${kind}: ${path.relative(process.cwd(), fileAbs)}`);
185
+
186
+ if (!info) {
187
+ // File is ignored or no longer maps; delete prior outputs if any
188
+ const prev = lastEmitted.get(fileAbs);
189
+ if (prev) {
190
+ for (const route of prev) {
191
+ const p = routeToOutPath({ outAbs: config.paths.outAbs, route });
192
+ try {
193
+ await fs.rm(p, { force: true });
194
+ } catch {
195
+ // ignore
196
+ }
197
+ deleteFromManifest(route);
198
+ await notifyChanged(route);
199
+ }
200
+ lastEmitted.delete(fileAbs);
201
+ }
202
+ console.log(`[statikapi] (ignored or unmapped)`);
203
+ await writeManifest();
204
+ return;
205
+ }
206
+
207
+ const r = { file: fileAbs, route: info.route, type: info.type, segments: info.normSegments };
208
+ try {
209
+ if (r.type === 'static') {
210
+ const files = await emitStatic(r, { fresh: true });
211
+ console.log(`[statikapi] wrote ${files} file(s) for ${r.route}`);
212
+ } else {
213
+ const { written, skipped } = await emitDynamic(r, { fresh: true });
214
+ const extra = skipped ? `, skipped ${skipped}` : '';
215
+ console.log(`[statikapi] wrote ${written} file(s) for ${r.route}${extra}`);
216
+ }
217
+ await writeManifest();
218
+ } catch (err) {
219
+ console.error(`[statikapi] ${err?.message || err}`);
220
+ }
221
+ }
222
+
223
+ // Initial full build
224
+ clearScreen();
225
+ console.log('statikapi dev → initial build…');
226
+ const routes = await mapRoutes({ srcAbs: config.paths.srcAbs });
227
+ for (const r of routes) {
228
+ if (r.type === 'static') await emitStatic(r);
229
+ else await emitDynamic(r);
230
+ }
231
+ await writeManifest();
232
+ console.log(`[statikapi] ready. Watching ${path.relative(process.cwd(), config.paths.srcAbs)}/`);
233
+
234
+ const watcher = chokidar.watch(config.paths.srcAbs, {
235
+ ignoreInitial: true,
236
+ ignored: (p) => path.basename(p).startsWith('_'),
237
+ });
238
+ watcher.on('add', (p) => buildOne(p, 'add'));
239
+ watcher.on('change', (p) => buildOne(p, 'change'));
240
+ watcher.on('unlink', (p) => buildOne(p, 'unlink'));
241
+
242
+ // Keep process alive until SIGINT
243
+ await new Promise((resolve) => {
244
+ const stop = () => watcher.close().then(resolve).catch(resolve);
245
+ process.on('SIGINT', stop);
246
+ process.on('SIGTERM', stop);
247
+ });
248
+ return 0;
249
+ }
@@ -0,0 +1,10 @@
1
+ export default async function initCmd() {
2
+ console.log('statikapi init → use `npx create-statikapi <name>` to scaffold a new project.');
3
+ }
4
+
5
+ // after publishing, replace the above with the below:
6
+ // export default async function initCmd(argv) {
7
+ // // Delegate to create-statikapi programmatically
8
+ // const { main } = await import('create-statikapi/src/index.js');
9
+ // return (await main(argv), 0);
10
+ // }