station-kit 1.0.6 → 1.0.8

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 (88) hide show
  1. package/.next/standalone/node_modules/.pnpm/buffer-from@1.1.2/node_modules/buffer-from/index.js +72 -0
  2. package/.next/standalone/node_modules/.pnpm/buffer-from@1.1.2/node_modules/buffer-from/package.json +19 -0
  3. package/.next/standalone/node_modules/.pnpm/source-map-support@0.5.21/node_modules/source-map-support/package.json +31 -0
  4. package/.next/standalone/node_modules/.pnpm/source-map-support@0.5.21/node_modules/source-map-support/source-map-support.js +625 -0
  5. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/array-set.js +121 -0
  6. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/base64-vlq.js +140 -0
  7. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/base64.js +67 -0
  8. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/binary-search.js +111 -0
  9. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/mapping-list.js +79 -0
  10. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/quick-sort.js +114 -0
  11. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/source-map-consumer.js +1145 -0
  12. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/source-map-generator.js +425 -0
  13. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/source-node.js +413 -0
  14. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/util.js +488 -0
  15. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/package.json +73 -0
  16. package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/source-map.js +8 -0
  17. package/.next/standalone/package.json +2 -1
  18. package/.next/standalone/packages/station-kit/.next/BUILD_ID +1 -1
  19. package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +13 -13
  20. package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +3 -3
  21. package/.next/standalone/packages/station-kit/.next/build-manifest.json +2 -2
  22. package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +12 -12
  23. package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/packages/station-kit/.next/server/app/_not-found.html +1 -1
  25. package/.next/standalone/packages/station-kit/.next/server/app/_not-found.rsc +1 -1
  26. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
  27. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
  28. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
  29. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +1 -1
  30. package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
  31. package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +1 -1
  32. package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
  36. package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +1 -1
  37. package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
  40. package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +1 -1
  41. package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +3 -3
  42. package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
  43. package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
  44. package/.next/standalone/packages/station-kit/package.json +16 -11
  45. package/dist/cli/deploy.d.ts +2 -0
  46. package/dist/cli/deploy.d.ts.map +1 -0
  47. package/dist/cli/deploy.js +287 -0
  48. package/dist/cli/deploy.js.map +1 -0
  49. package/dist/cli/parse-args.d.ts +13 -0
  50. package/dist/cli/parse-args.d.ts.map +1 -0
  51. package/dist/cli/parse-args.js +77 -0
  52. package/dist/cli/parse-args.js.map +1 -0
  53. package/dist/cli-main.js +28 -1
  54. package/dist/cli-main.js.map +1 -1
  55. package/dist/cli.js +1 -1
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config/loader.d.ts +1 -1
  58. package/dist/config/loader.d.ts.map +1 -1
  59. package/dist/config/loader.js +7 -3
  60. package/dist/config/loader.js.map +1 -1
  61. package/dist/config/schema.d.ts +5 -0
  62. package/dist/config/schema.d.ts.map +1 -1
  63. package/dist/config/schema.js +22 -3
  64. package/dist/config/schema.js.map +1 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/server/index.d.ts +7 -0
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +7 -2
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/station-dir.d.ts +7 -0
  74. package/dist/station-dir.d.ts.map +1 -0
  75. package/dist/station-dir.js +15 -0
  76. package/dist/station-dir.js.map +1 -0
  77. package/package.json +15 -10
  78. package/src/cli/deploy.ts +334 -0
  79. package/src/cli/parse-args.ts +99 -0
  80. package/src/cli-main.ts +28 -1
  81. package/src/cli.ts +1 -1
  82. package/src/config/loader.ts +7 -3
  83. package/src/config/schema.ts +29 -3
  84. package/src/index.ts +3 -1
  85. package/src/server/index.ts +14 -2
  86. package/src/station-dir.ts +24 -0
  87. /package/.next/standalone/packages/station-kit/.next/static/{kqMlNlRLox_FLRK-2GwrJ → xYd6dn0Ox68DaamIrH_pB}/_buildManifest.js +0 -0
  88. /package/.next/standalone/packages/station-kit/.next/static/{kqMlNlRLox_FLRK-2GwrJ → xYd6dn0Ox68DaamIrH_pB}/_ssgManifest.js +0 -0
@@ -0,0 +1,334 @@
1
+ import {
2
+ writeFileSync,
3
+ readFileSync,
4
+ cpSync,
5
+ existsSync,
6
+ readdirSync,
7
+ rmSync,
8
+ mkdirSync,
9
+ statSync,
10
+ } from "node:fs";
11
+ import { join, resolve, dirname, relative } from "node:path";
12
+ import { builtinModules } from "node:module";
13
+ import { build } from "esbuild";
14
+ import { loadConfig } from "../config/loader.js";
15
+ import { ensureStationDir } from "../station-dir.js";
16
+
17
+ const cwd = process.cwd();
18
+
19
+ // Ensure .station/data exists before loading config — adapter constructors may open DBs
20
+ const defaultStationDir = ".station";
21
+ mkdirSync(join(cwd, defaultStationDir, "data"), { recursive: true });
22
+
23
+ const config = await loadConfig(cwd);
24
+ const { outDir } = ensureStationDir(cwd, config.stationDir);
25
+
26
+ // ── Workspace resolution ────────────────────────────────────
27
+
28
+ function findWorkspaceRoot(startDir: string): string | null {
29
+ let dir = startDir;
30
+ while (true) {
31
+ if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
32
+ const pkgPath = join(dir, "package.json");
33
+ if (existsSync(pkgPath)) {
34
+ try {
35
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
36
+ if (pkg.workspaces) return dir;
37
+ } catch {}
38
+ }
39
+ const parent = dirname(dir);
40
+ if (parent === dir) return null;
41
+ dir = parent;
42
+ }
43
+ }
44
+
45
+ function resolveWorkspaceVersion(depName: string, startDir: string): string | null {
46
+ const root = findWorkspaceRoot(startDir);
47
+ if (!root) return null;
48
+ const candidate = join(root, "packages", depName, "package.json");
49
+ if (!existsSync(candidate)) return null;
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
52
+ return pkg.version ?? null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // ── Production package.json ─────────────────────────────────
59
+
60
+ interface PkgJson {
61
+ name?: string;
62
+ version?: string;
63
+ type?: string;
64
+ private?: boolean;
65
+ description?: string;
66
+ license?: string;
67
+ author?: unknown;
68
+ dependencies?: Record<string, string>;
69
+ devDependencies?: Record<string, string>;
70
+ [key: string]: unknown;
71
+ }
72
+
73
+ const resolvedDeps: Array<{ dep: string; from: string; to: string }> = [];
74
+
75
+ function buildProductionPackageJson(): PkgJson {
76
+ const pkgPath = join(cwd, "package.json");
77
+ if (!existsSync(pkgPath)) {
78
+ console.error("[station] No package.json found.");
79
+ process.exit(1);
80
+ }
81
+
82
+ const raw: PkgJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
83
+
84
+ // Merge deps: all dependencies + station-* from devDependencies
85
+ const deps: Record<string, string> = { ...(raw.dependencies ?? {}) };
86
+ if (raw.devDependencies) {
87
+ for (const [name, version] of Object.entries(raw.devDependencies)) {
88
+ if (name.startsWith("station-") && !deps[name]) {
89
+ deps[name] = version;
90
+ }
91
+ }
92
+ }
93
+
94
+ // Resolve workspace:* → ^{version}
95
+ for (const [name, version] of Object.entries(deps)) {
96
+ if (version.startsWith("workspace:")) {
97
+ const resolved_version = resolveWorkspaceVersion(name, cwd);
98
+ if (resolved_version) {
99
+ const newVersion = `^${resolved_version}`;
100
+ resolvedDeps.push({ dep: name, from: version, to: newVersion });
101
+ deps[name] = newVersion;
102
+ } else {
103
+ console.warn(`[station] Could not resolve ${version} for "${name}" — ensure you are in a workspace or use real version numbers.`);
104
+ }
105
+ }
106
+ }
107
+
108
+ const out: PkgJson = {};
109
+ if (raw.name) out.name = raw.name;
110
+ if (raw.version) out.version = raw.version;
111
+ if (raw.type) out.type = raw.type;
112
+ if (raw.private !== undefined) out.private = raw.private;
113
+ if (raw.description) out.description = raw.description;
114
+ if (raw.license) out.license = raw.license;
115
+ if (raw.author) out.author = raw.author;
116
+ out.scripts = { start: "npx station --no-open --host 0.0.0.0" };
117
+ out.dependencies = deps;
118
+
119
+ return out;
120
+ }
121
+
122
+ // ── Discover entry points ───────────────────────────────────
123
+
124
+ function discoverFiles(dir: string): string[] {
125
+ if (!existsSync(dir)) return [];
126
+ const files: string[] = [];
127
+ for (const entry of readdirSync(dir, { recursive: true })) {
128
+ const full = join(dir, entry.toString());
129
+ if (statSync(full).isFile() && /\.(ts|js|mjs)$/.test(full)) {
130
+ files.push(full);
131
+ }
132
+ }
133
+ return files;
134
+ }
135
+
136
+ function findConfigFile(): string | null {
137
+ for (const name of ["station.config.ts", "station.config.js", "station.config.mjs"]) {
138
+ const candidate = join(cwd, name);
139
+ if (existsSync(candidate)) return candidate;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ // ── Build ───────────────────────────────────────────────────
145
+
146
+ console.log("\n[station] Building deploy bundle...\n");
147
+
148
+ // Clean out directory
149
+ if (existsSync(outDir)) {
150
+ rmSync(outDir, { recursive: true });
151
+ }
152
+ mkdirSync(outDir, { recursive: true });
153
+
154
+ // Write production package.json first (need deps list for externals)
155
+ const prodPkg = buildProductionPackageJson();
156
+ writeFileSync(join(outDir, "package.json"), JSON.stringify(prodPkg, null, 2) + "\n");
157
+
158
+ // Collect entry points
159
+ const entryPoints: string[] = [];
160
+ let signalCount = 0;
161
+ let broadcastCount = 0;
162
+
163
+ if (config.signalsDir) {
164
+ const signalsSrc = resolve(cwd, config.signalsDir);
165
+ const files = discoverFiles(signalsSrc);
166
+ signalCount = files.length;
167
+ entryPoints.push(...files);
168
+ }
169
+
170
+ if (config.broadcastsDir) {
171
+ const broadcastsSrc = resolve(cwd, config.broadcastsDir);
172
+ const files = discoverFiles(broadcastsSrc);
173
+ broadcastCount = files.length;
174
+ entryPoints.push(...files);
175
+ }
176
+
177
+ const configFile = findConfigFile();
178
+ if (configFile) {
179
+ entryPoints.push(configFile);
180
+ }
181
+
182
+ if (entryPoints.length === 0) {
183
+ console.error("[station] No signals, broadcasts, or config found to bundle.");
184
+ process.exit(1);
185
+ }
186
+
187
+ // Collect externals: all npm deps + node builtins
188
+ const external = [
189
+ ...Object.keys(prodPkg.dependencies ?? {}),
190
+ ...builtinModules,
191
+ ...builtinModules.map((m) => `node:${m}`),
192
+ ];
193
+
194
+ // Run esbuild
195
+ try {
196
+ const result = await build({
197
+ entryPoints,
198
+ outdir: outDir,
199
+ outbase: cwd,
200
+ bundle: true,
201
+ splitting: true,
202
+ format: "esm",
203
+ platform: "node",
204
+ target: "node20",
205
+ external,
206
+ logLevel: "warning",
207
+ });
208
+
209
+ if (result.errors.length > 0) {
210
+ console.error("[station] Build failed.");
211
+ process.exit(1);
212
+ }
213
+ } catch (err: unknown) {
214
+ console.error("[station] Build failed:", (err as Error).message);
215
+ process.exit(1);
216
+ }
217
+
218
+ // Count shared chunks
219
+ const outputFiles = readdirSync(outDir);
220
+ const chunks = outputFiles.filter((f) => f.startsWith("chunk-") && f.endsWith(".js"));
221
+
222
+ console.log(` Bundled ${signalCount} signal${signalCount !== 1 ? "s" : ""}, ${broadcastCount} broadcast${broadcastCount !== 1 ? "s" : ""}`);
223
+ if (chunks.length > 0) {
224
+ console.log(` ${chunks.length} shared chunk${chunks.length !== 1 ? "s" : ""} extracted`);
225
+ }
226
+ if (configFile) {
227
+ console.log(` Config compiled`);
228
+ }
229
+
230
+ // ── Copy deploy.include entries ─────────────────────────────
231
+
232
+ if (config.deploy?.include) {
233
+ for (const entry of config.deploy.include) {
234
+ const src = resolve(cwd, entry);
235
+ if (!existsSync(src)) {
236
+ console.warn(` [station] Warning: deploy.include path not found: ${entry}`);
237
+ continue;
238
+ }
239
+ const dest = join(outDir, entry);
240
+ if (statSync(src).isDirectory()) {
241
+ cpSync(src, dest, { recursive: true });
242
+ } else {
243
+ mkdirSync(dirname(dest), { recursive: true });
244
+ cpSync(src, dest);
245
+ }
246
+ }
247
+ }
248
+
249
+ // ── Dockerfile ──────────────────────────────────────────────
250
+
251
+ function generateDockerfile(): string {
252
+ const port = config.port;
253
+ return [
254
+ `FROM node:20-alpine`,
255
+ `WORKDIR /app`,
256
+ ``,
257
+ `COPY package.json ./`,
258
+ `RUN npm install --omit=dev`,
259
+ ``,
260
+ `COPY . .`,
261
+ ``,
262
+ `EXPOSE ${port}`,
263
+ `ENV NODE_ENV=production`,
264
+ `ENV HOST=0.0.0.0`,
265
+ `ENV PORT=${port}`,
266
+ ``,
267
+ `# Set these in your deployment platform:`,
268
+ `# ENV STATION_AUTH_USERNAME=admin`,
269
+ `# ENV STATION_AUTH_PASSWORD=changeme`,
270
+ ``,
271
+ `CMD ["npx", "station", "--no-open", "--host", "0.0.0.0"]`,
272
+ ``,
273
+ ].join("\n");
274
+ }
275
+
276
+ // ── nixpacks.toml ───────────────────────────────────────────
277
+
278
+ function generateNixpacks(): string {
279
+ return [
280
+ `[phases.setup]`,
281
+ `nixPkgs = ["nodejs_20"]`,
282
+ ``,
283
+ `[phases.install]`,
284
+ `cmds = ["npm install --omit=dev"]`,
285
+ ``,
286
+ `[start]`,
287
+ `cmd = "npx station --no-open --host 0.0.0.0"`,
288
+ ``,
289
+ ].join("\n");
290
+ }
291
+
292
+ // ── Write deployment files ──────────────────────────────────
293
+
294
+ writeFileSync(join(outDir, "Dockerfile"), generateDockerfile());
295
+ writeFileSync(join(outDir, "nixpacks.toml"), generateNixpacks());
296
+ writeFileSync(join(outDir, ".dockerignore"), ["node_modules", ".station/data", "*.db", ".git", ""].join("\n"));
297
+ writeFileSync(join(outDir, ".gitignore"), ["node_modules/", "data/", "*.db", ""].join("\n"));
298
+
299
+ // ── Summary ─────────────────────────────────────────────────
300
+
301
+ function listDir(dir: string, prefix = ""): string[] {
302
+ const entries: string[] = [];
303
+ for (const entry of readdirSync(dir).sort()) {
304
+ const full = join(dir, entry);
305
+ if (statSync(full).isDirectory()) {
306
+ entries.push(`${prefix}${entry}/`);
307
+ entries.push(...listDir(full, `${prefix} `));
308
+ } else {
309
+ entries.push(`${prefix}${entry}`);
310
+ }
311
+ }
312
+ return entries;
313
+ }
314
+
315
+ console.log(`\n ${outDir}\n`);
316
+ for (const line of listDir(outDir)) {
317
+ console.log(` ${line}`);
318
+ }
319
+
320
+ if (resolvedDeps.length > 0) {
321
+ console.log(`\n Resolved dependencies:`);
322
+ for (const r of resolvedDeps) {
323
+ console.log(` ${r.dep}: ${r.from} → ${r.to}`);
324
+ }
325
+ }
326
+
327
+ console.log(`\n Environment variables:`);
328
+ console.log(` STATION_AUTH_USERNAME — dashboard login`);
329
+ console.log(` STATION_AUTH_PASSWORD — dashboard password`);
330
+ console.log(` PORT — server port (default: ${config.port})`);
331
+
332
+ console.log(`\n Deploy:`);
333
+ console.log(` docker build -t station ${outDir}`);
334
+ console.log(` docker run -p ${config.port}:${config.port} station\n`);
@@ -0,0 +1,99 @@
1
+ export interface CliArgs {
2
+ port?: number;
3
+ host?: string;
4
+ dir?: string;
5
+ noOpen?: boolean;
6
+ noRunners?: boolean;
7
+ config?: string;
8
+ subcommand?: string;
9
+ help?: boolean;
10
+ }
11
+
12
+ const FLAGS_WITH_VALUE = new Set(["--port", "--host", "--dir", "--config"]);
13
+
14
+ export function parseArgs(argv: string[]): CliArgs {
15
+ const args: CliArgs = {};
16
+ let i = 0;
17
+
18
+ while (i < argv.length) {
19
+ const arg = argv[i];
20
+
21
+ if (arg === "--help" || arg === "-h") {
22
+ args.help = true;
23
+ i++;
24
+ continue;
25
+ }
26
+
27
+ if (arg === "--no-open") {
28
+ args.noOpen = true;
29
+ i++;
30
+ continue;
31
+ }
32
+
33
+ if (arg === "--no-runners" || arg === "--read-only") {
34
+ args.noRunners = true;
35
+ i++;
36
+ continue;
37
+ }
38
+
39
+ if (FLAGS_WITH_VALUE.has(arg)) {
40
+ const value = argv[i + 1];
41
+ if (value === undefined || value.startsWith("--")) {
42
+ throw new Error(`Missing value for ${arg}`);
43
+ }
44
+
45
+ switch (arg) {
46
+ case "--port": {
47
+ const n = parseInt(value, 10);
48
+ if (isNaN(n) || n < 1 || n > 65535) {
49
+ throw new Error(`Invalid port: ${value}`);
50
+ }
51
+ args.port = n;
52
+ break;
53
+ }
54
+ case "--host":
55
+ args.host = value;
56
+ break;
57
+ case "--dir":
58
+ args.dir = value;
59
+ break;
60
+ case "--config":
61
+ args.config = value;
62
+ break;
63
+ }
64
+
65
+ i += 2;
66
+ continue;
67
+ }
68
+
69
+ // First positional arg that doesn't start with -- is a subcommand
70
+ if (!arg.startsWith("-")) {
71
+ args.subcommand = arg;
72
+ i++;
73
+ continue;
74
+ }
75
+
76
+ throw new Error(`Unknown flag: ${arg}`);
77
+ }
78
+
79
+ return args;
80
+ }
81
+
82
+ export function printUsage(): void {
83
+ console.log(`
84
+ Usage: station [command] [options]
85
+
86
+ Commands:
87
+ deploy Generate deployment files (Dockerfile, nixpacks.toml)
88
+
89
+ Options:
90
+ --port <n> Override server port (default: 4400)
91
+ --host <s> Override server host (default: localhost)
92
+ --dir <path> Set station directory for generated files (default: .station)
93
+ --config <path> Path to config file (default: station.config.ts)
94
+ --no-open Don't open browser on start
95
+ --no-runners Don't execute signal/broadcast runners (read-only mode)
96
+ --read-only Alias for --no-runners
97
+ -h, --help Show this help message
98
+ `.trim());
99
+ }
package/src/cli-main.ts CHANGED
@@ -1,11 +1,38 @@
1
1
  import { loadConfig } from "./config/loader.js";
2
2
  import { createStation } from "./server/index.js";
3
+ import { parseArgs, printUsage } from "./cli/parse-args.js";
3
4
  import { spawn, type ChildProcess } from "node:child_process";
4
5
  import { resolve } from "node:path";
5
6
  import { existsSync } from "node:fs";
6
7
 
8
+ // Parse CLI arguments
9
+ const cliArgs = parseArgs(process.argv.slice(2));
10
+
11
+ if (cliArgs.help) {
12
+ printUsage();
13
+ process.exit(0);
14
+ }
15
+
16
+ if (cliArgs.subcommand === "deploy") {
17
+ await import("./cli/deploy.js");
18
+ process.exit(0);
19
+ }
20
+
21
+ if (cliArgs.subcommand) {
22
+ console.error(`[station] Unknown command: ${cliArgs.subcommand}`);
23
+ printUsage();
24
+ process.exit(1);
25
+ }
26
+
7
27
  const cwd = process.cwd();
8
- const config = await loadConfig(cwd);
28
+ const config = await loadConfig(cwd, cliArgs.config);
29
+
30
+ // Apply CLI overrides
31
+ if (cliArgs.port !== undefined) config.port = cliArgs.port;
32
+ if (cliArgs.host !== undefined) config.host = cliArgs.host;
33
+ if (cliArgs.dir !== undefined) config.stationDir = cliArgs.dir;
34
+ if (cliArgs.noOpen) config.open = false;
35
+ if (cliArgs.noRunners) config.runRunners = false;
9
36
 
10
37
  // Spawn Next.js standalone server for the dashboard
11
38
  const pkgRoot = resolve(import.meta.dirname, "..");
package/src/cli.ts CHANGED
@@ -23,7 +23,7 @@ if (!process.env[MARKER]) {
23
23
  }
24
24
 
25
25
  const main = fileURLToPath(new URL("./cli-main.js", import.meta.url));
26
- const child = spawn(execPath, ["--import", tsxSpecifier, main], {
26
+ const child = spawn(execPath, ["--import", tsxSpecifier, main, ...process.argv.slice(2)], {
27
27
  stdio: "inherit",
28
28
  env: { ...process.env, [MARKER]: "1", __STATION_TSX: tsxSpecifier },
29
29
  });
@@ -9,10 +9,14 @@ const CONFIG_NAMES = [
9
9
  "station.config.mjs",
10
10
  ];
11
11
 
12
- export async function loadConfig(cwd: string): Promise<StationConfig> {
13
- const configPath = findConfigFile(cwd);
12
+ export async function loadConfig(cwd: string, configFile?: string): Promise<StationConfig> {
13
+ const configPath = configFile ? resolve(cwd, configFile) : findConfigFile(cwd);
14
14
 
15
- if (!configPath) {
15
+ if (!configPath || !existsSync(configPath)) {
16
+ if (configFile) {
17
+ console.error(`[station] Config file not found: ${configFile}`);
18
+ process.exit(1);
19
+ }
16
20
  console.log("[station] No config file found. Using defaults.");
17
21
  return resolveConfig({});
18
22
  }
@@ -18,6 +18,10 @@ export interface BroadcastRunnerConfig {
18
18
  pollIntervalMs: number;
19
19
  }
20
20
 
21
+ export interface DeployConfig {
22
+ include?: string[];
23
+ }
24
+
21
25
  export interface StationConfig {
22
26
  port: number;
23
27
  host: string;
@@ -25,12 +29,14 @@ export interface StationConfig {
25
29
  broadcastAdapter?: BroadcastQueueAdapter;
26
30
  signalsDir?: string;
27
31
  broadcastsDir?: string;
32
+ stationDir: string;
28
33
  runner: RunnerConfig;
29
34
  broadcastRunner: BroadcastRunnerConfig;
30
35
  runRunners: boolean;
31
36
  open: boolean;
32
37
  logLevel: "debug" | "info" | "warn" | "error";
33
38
  auth?: AuthConfig;
39
+ deploy?: DeployConfig;
34
40
  }
35
41
 
36
42
  export type StationUserConfig = Partial<Omit<StationConfig, "runner" | "broadcastRunner">> & {
@@ -41,6 +47,7 @@ export type StationUserConfig = Partial<Omit<StationConfig, "runner" | "broadcas
41
47
  const DEFAULTS: StationConfig = {
42
48
  port: 4400,
43
49
  host: "localhost",
50
+ stationDir: ".station",
44
51
  runner: {
45
52
  pollIntervalMs: 1000,
46
53
  maxConcurrent: 5,
@@ -56,13 +63,31 @@ const DEFAULTS: StationConfig = {
56
63
  };
57
64
 
58
65
  export function resolveConfig(input: StationUserConfig): StationConfig {
66
+ const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
67
+ const envHost = process.env.HOST;
68
+ const envAuthUser = process.env.STATION_AUTH_USERNAME;
69
+ const envAuthPass = process.env.STATION_AUTH_PASSWORD;
70
+
71
+ // Env vars for auth: override config values, or create auth if both are set
72
+ let auth = input.auth;
73
+ if (auth) {
74
+ auth = {
75
+ ...auth,
76
+ username: envAuthUser ?? auth.username,
77
+ password: envAuthPass ?? auth.password,
78
+ };
79
+ } else if (envAuthUser && envAuthPass) {
80
+ auth = { username: envAuthUser, password: envAuthPass };
81
+ }
82
+
59
83
  return {
60
- port: input.port ?? DEFAULTS.port,
61
- host: input.host ?? DEFAULTS.host,
84
+ port: input.port ?? envPort ?? DEFAULTS.port,
85
+ host: input.host ?? envHost ?? DEFAULTS.host,
62
86
  adapter: input.adapter,
63
87
  broadcastAdapter: input.broadcastAdapter,
64
88
  signalsDir: input.signalsDir,
65
89
  broadcastsDir: input.broadcastsDir,
90
+ stationDir: input.stationDir ?? DEFAULTS.stationDir,
66
91
  runner: {
67
92
  pollIntervalMs: input.runner?.pollIntervalMs ?? DEFAULTS.runner.pollIntervalMs,
68
93
  maxConcurrent: input.runner?.maxConcurrent ?? DEFAULTS.runner.maxConcurrent,
@@ -75,6 +100,7 @@ export function resolveConfig(input: StationUserConfig): StationConfig {
75
100
  runRunners: input.runRunners ?? DEFAULTS.runRunners,
76
101
  open: input.open ?? DEFAULTS.open,
77
102
  logLevel: input.logLevel ?? DEFAULTS.logLevel,
78
- auth: input.auth,
103
+ auth,
104
+ deploy: input.deploy,
79
105
  };
80
106
  }
package/src/index.ts CHANGED
@@ -4,4 +4,6 @@ export function defineConfig(config: StationUserConfig): StationUserConfig {
4
4
  return config;
5
5
  }
6
6
 
7
- export type { StationConfig, StationUserConfig, AuthConfig } from "./config/schema.js";
7
+ export type { StationConfig, StationUserConfig, AuthConfig, DeployConfig } from "./config/schema.js";
8
+ export { resolveConfig } from "./config/schema.js";
9
+ export { loadConfig } from "./config/loader.js";
@@ -9,6 +9,7 @@ import { BroadcastRunner, BroadcastMemoryAdapter } from "station-broadcast";
9
9
  import type { SignalQueueAdapter } from "station-signal";
10
10
  import type { BroadcastQueueAdapter } from "station-broadcast";
11
11
  import type { StationConfig } from "../config/schema.js";
12
+ import { ensureStationDir } from "../station-dir.js";
12
13
  import { WebSocketHub } from "./ws.js";
13
14
  import { SSEHub } from "./sse.js";
14
15
  import { LogBuffer } from "./log-buffer.js";
@@ -32,9 +33,16 @@ import { v1KeyRoutes } from "./routes/v1/keys.js";
32
33
  import { v1AuthRoutes } from "./routes/v1/auth.js";
33
34
  import { v1EventRoutes } from "./routes/v1/events.js";
34
35
 
36
+ export { KeyStore } from "./auth/keys.js";
37
+ export type { ApiKey } from "./auth/keys.js";
38
+
35
39
  export interface StationInstance {
36
40
  start(): Promise<void>;
37
41
  stop(): Promise<void>;
42
+ /** The KeyStore instance (available when auth is configured). */
43
+ keyStore?: KeyStore;
44
+ /** The resolved data directory path. */
45
+ dataDir: string;
38
46
  }
39
47
 
40
48
  export async function createStation(config: StationConfig, cwd: string, nextPort?: number): Promise<StationInstance> {
@@ -42,17 +50,19 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
42
50
  const broadcastAdapter: BroadcastQueueAdapter | undefined =
43
51
  config.broadcastAdapter ?? (config.broadcastsDir ? new BroadcastMemoryAdapter() : undefined);
44
52
 
53
+ const { dataDir } = ensureStationDir(cwd, config.stationDir);
54
+
45
55
  const wsHub = new WebSocketHub();
46
56
  const sseHub = new SSEHub();
47
57
  const logBuffer = new LogBuffer();
48
- const logStore = new LogStore(resolve(cwd, "station-logs.db"));
58
+ const logStore = new LogStore(resolve(dataDir, "station-logs.db"));
49
59
 
50
60
  // Auth: create KeyStore and SessionConfig if auth is configured
51
61
  let keyStore: KeyStore | undefined;
52
62
  let sessionConfig: SessionConfig | undefined;
53
63
 
54
64
  if (config.auth) {
55
- keyStore = new KeyStore(resolve(cwd, "station-keys.db"));
65
+ keyStore = new KeyStore(resolve(dataDir, "station-keys.db"));
56
66
  sessionConfig = {
57
67
  username: config.auth.username,
58
68
  password: config.auth.password,
@@ -288,6 +298,8 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
288
298
  let httpServer: Server | null = null;
289
299
 
290
300
  return {
301
+ keyStore,
302
+ dataDir,
291
303
  async start() {
292
304
  // Start runners (non-blocking — they have internal poll loops)
293
305
  if (config.runRunners) {
@@ -0,0 +1,24 @@
1
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+
4
+ export interface StationDirs {
5
+ baseDir: string;
6
+ dataDir: string;
7
+ outDir: string;
8
+ }
9
+
10
+ export function ensureStationDir(cwd: string, stationDir: string): StationDirs {
11
+ const baseDir = resolve(cwd, stationDir);
12
+ const dataDir = join(baseDir, "data");
13
+ const outDir = join(baseDir, "out");
14
+
15
+ mkdirSync(dataDir, { recursive: true });
16
+ mkdirSync(outDir, { recursive: true });
17
+
18
+ const gitignorePath = join(baseDir, ".gitignore");
19
+ if (!existsSync(gitignorePath)) {
20
+ writeFileSync(gitignorePath, "data/\n");
21
+ }
22
+
23
+ return { baseDir, dataDir, outDir };
24
+ }