station-kit 1.0.5 → 1.0.7

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 (122) 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 +17 -17
  20. package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +5 -5
  21. package/.next/standalone/packages/station-kit/.next/build-manifest.json +2 -2
  22. package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +6 -6
  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 +7 -7
  26. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page.js +1 -1
  27. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
  28. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
  30. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +8 -8
  31. package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
  32. package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +8 -8
  33. package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page.js +2 -2
  35. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/packages/station-kit/.next/server/app/settings/page.js +2 -2
  37. package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
  39. package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +8 -8
  40. package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
  43. package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +8 -8
  44. package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +5 -5
  45. package/.next/standalone/packages/station-kit/.next/server/chunks/102.js +1 -1
  46. package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
  47. package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
  48. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +1 -0
  49. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +1 -0
  50. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +1 -0
  51. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +1 -0
  52. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +1 -0
  53. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +1 -0
  54. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +1 -0
  55. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +1 -0
  56. package/.next/standalone/packages/station-kit/package.json +12 -11
  57. package/dist/cli/deploy.d.ts +2 -0
  58. package/dist/cli/deploy.d.ts.map +1 -0
  59. package/dist/cli/deploy.js +287 -0
  60. package/dist/cli/deploy.js.map +1 -0
  61. package/dist/cli/parse-args.d.ts +13 -0
  62. package/dist/cli/parse-args.d.ts.map +1 -0
  63. package/dist/cli/parse-args.js +77 -0
  64. package/dist/cli/parse-args.js.map +1 -0
  65. package/dist/cli-main.js +28 -1
  66. package/dist/cli-main.js.map +1 -1
  67. package/dist/cli.js +1 -1
  68. package/dist/cli.js.map +1 -1
  69. package/dist/config/loader.d.ts +1 -1
  70. package/dist/config/loader.d.ts.map +1 -1
  71. package/dist/config/loader.js +7 -3
  72. package/dist/config/loader.js.map +1 -1
  73. package/dist/config/schema.d.ts +5 -0
  74. package/dist/config/schema.d.ts.map +1 -1
  75. package/dist/config/schema.js +22 -3
  76. package/dist/config/schema.js.map +1 -1
  77. package/dist/index.d.ts +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/server/index.d.ts.map +1 -1
  80. package/dist/server/index.js +6 -4
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/routes/broadcasts.d.ts.map +1 -1
  83. package/dist/server/routes/broadcasts.js +23 -0
  84. package/dist/server/routes/broadcasts.js.map +1 -1
  85. package/dist/server/routes/runs.d.ts +2 -0
  86. package/dist/server/routes/runs.d.ts.map +1 -1
  87. package/dist/server/routes/runs.js +66 -0
  88. package/dist/server/routes/runs.js.map +1 -1
  89. package/dist/server/routes/v1/trigger.d.ts +2 -1
  90. package/dist/server/routes/v1/trigger.d.ts.map +1 -1
  91. package/dist/server/routes/v1/trigger.js +88 -0
  92. package/dist/server/routes/v1/trigger.js.map +1 -1
  93. package/dist/station-dir.d.ts +7 -0
  94. package/dist/station-dir.d.ts.map +1 -0
  95. package/dist/station-dir.js +15 -0
  96. package/dist/station-dir.js.map +1 -0
  97. package/package.json +9 -8
  98. package/src/app/broadcasts/[id]/broadcast-detail.tsx +22 -1
  99. package/src/app/hooks/use-api.ts +3 -0
  100. package/src/app/runs/[id]/run-detail.tsx +45 -1
  101. package/src/cli/deploy.ts +334 -0
  102. package/src/cli/parse-args.ts +99 -0
  103. package/src/cli-main.ts +28 -1
  104. package/src/cli.ts +1 -1
  105. package/src/config/loader.ts +7 -3
  106. package/src/config/schema.ts +29 -3
  107. package/src/index.ts +1 -1
  108. package/src/server/index.ts +7 -4
  109. package/src/server/routes/broadcasts.ts +24 -0
  110. package/src/server/routes/runs.ts +76 -0
  111. package/src/server/routes/v1/trigger.ts +105 -1
  112. package/src/station-dir.ts +24 -0
  113. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a41371d946262e43.js +0 -1
  114. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-1b9fa68dcc219e1d.js +0 -1
  115. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-9d37f15ea4fdcabf.js +0 -1
  116. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-0c92bbc0acac1025.js +0 -1
  117. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-87e302dcd2f7516b.js +0 -1
  118. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-39f130df9a05bf2b.js +0 -1
  119. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-215ec395e232fb36.js +0 -1
  120. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-35b38b909a017693.js +0 -1
  121. /package/.next/standalone/packages/station-kit/.next/static/{_qcazCz3yLLsuU53Qtacw → pHHaxeGaet0VW1dhcIcuY}/_buildManifest.js +0 -0
  122. /package/.next/standalone/packages/station-kit/.next/static/{_qcazCz3yLLsuU53Qtacw → pHHaxeGaet0VW1dhcIcuY}/_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,4 @@ 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";
@@ -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";
@@ -42,17 +43,19 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
42
43
  const broadcastAdapter: BroadcastQueueAdapter | undefined =
43
44
  config.broadcastAdapter ?? (config.broadcastsDir ? new BroadcastMemoryAdapter() : undefined);
44
45
 
46
+ const { dataDir } = ensureStationDir(cwd, config.stationDir);
47
+
45
48
  const wsHub = new WebSocketHub();
46
49
  const sseHub = new SSEHub();
47
50
  const logBuffer = new LogBuffer();
48
- const logStore = new LogStore(resolve(cwd, "station-logs.db"));
51
+ const logStore = new LogStore(resolve(dataDir, "station-logs.db"));
49
52
 
50
53
  // Auth: create KeyStore and SessionConfig if auth is configured
51
54
  let keyStore: KeyStore | undefined;
52
55
  let sessionConfig: SessionConfig | undefined;
53
56
 
54
57
  if (config.auth) {
55
- keyStore = new KeyStore(resolve(cwd, "station-keys.db"));
58
+ keyStore = new KeyStore(resolve(dataDir, "station-keys.db"));
56
59
  sessionConfig = {
57
60
  username: config.auth.username,
58
61
  password: config.auth.password,
@@ -169,7 +172,7 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
169
172
 
170
173
  app.route("/api", healthRoutes({ signalAdapter, broadcastAdapter }));
171
174
  app.route("/api", signalRoutes({ signalRunner, signalAdapter, signalSubscriber: stationSignalSub }));
172
- app.route("/api", runRoutes({ signalRunner, signalAdapter, logBuffer, logStore }));
175
+ app.route("/api", runRoutes({ signalRunner, signalAdapter, logBuffer, logStore, signalSubscriber: stationSignalSub }));
173
176
  app.route("/api", broadcastRoutes({ broadcastRunner, broadcastAdapter, broadcastSubscriber: stationBroadcastSub, logBuffer, logStore }));
174
177
 
175
178
  // ── v1 API routes (authenticated) ──────────────────────────────────
@@ -199,7 +202,7 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
199
202
  // Trigger-scope routes
200
203
  const triggerRoutes = new Hono();
201
204
  triggerRoutes.use("/*", requireScope("trigger"));
202
- triggerRoutes.route("/", v1TriggerRoutes({ signalRunner, signalAdapter, broadcastRunner, signalSubscriber: stationSignalSub }));
205
+ triggerRoutes.route("/", v1TriggerRoutes({ signalRunner, signalAdapter, broadcastRunner, broadcastAdapter, signalSubscriber: stationSignalSub }));
203
206
  v1.route("/", triggerRoutes);
204
207
 
205
208
  // Cancel-scope routes — only the cancel endpoints
@@ -146,6 +146,30 @@ export function broadcastRoutes(deps: BroadcastDeps) {
146
146
  return c.json({ data: { cancelled: true } });
147
147
  });
148
148
 
149
+ // POST /broadcast-runs/:id/rerun
150
+ app.post("/broadcast-runs/:id/rerun", async (c) => {
151
+ const id = c.req.param("id");
152
+ if (!deps.broadcastRunner || !deps.broadcastAdapter) {
153
+ return c.json({ error: "read_only", message: "Station is in read-only mode." }, 403);
154
+ }
155
+ const run = await deps.broadcastAdapter.getBroadcastRun(id);
156
+ if (!run) {
157
+ return c.json({ error: "not_found", message: "Broadcast run not found." }, 404);
158
+ }
159
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
160
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled broadcast runs can be rerun." }, 400);
161
+ }
162
+
163
+ try {
164
+ const input = typeof run.input === "string" ? JSON.parse(run.input) : run.input;
165
+ const newId = await deps.broadcastRunner.trigger(run.broadcastName, input);
166
+ return c.json({ data: { id: newId, broadcastName: run.broadcastName, status: "pending" } });
167
+ } catch (err: unknown) {
168
+ const message = err instanceof Error ? err.message : String(err);
169
+ return c.json({ error: "rerun_failed", message }, 400);
170
+ }
171
+ });
172
+
149
173
  return app;
150
174
  }
151
175