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.
- package/.next/standalone/node_modules/.pnpm/buffer-from@1.1.2/node_modules/buffer-from/index.js +72 -0
- package/.next/standalone/node_modules/.pnpm/buffer-from@1.1.2/node_modules/buffer-from/package.json +19 -0
- package/.next/standalone/node_modules/.pnpm/source-map-support@0.5.21/node_modules/source-map-support/package.json +31 -0
- package/.next/standalone/node_modules/.pnpm/source-map-support@0.5.21/node_modules/source-map-support/source-map-support.js +625 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/array-set.js +121 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/base64-vlq.js +140 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/base64.js +67 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/binary-search.js +111 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/mapping-list.js +79 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/quick-sort.js +114 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/source-map-consumer.js +1145 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/source-map-generator.js +425 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/source-node.js +413 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/util.js +488 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/package.json +73 -0
- package/.next/standalone/node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/source-map.js +8 -0
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/packages/station-kit/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +13 -13
- package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +3 -3
- package/.next/standalone/packages/station-kit/.next/build-manifest.json +2 -2
- package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +12 -12
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +3 -3
- package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
- package/.next/standalone/packages/station-kit/package.json +16 -11
- package/dist/cli/deploy.d.ts +2 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +287 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/parse-args.d.ts +13 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +77 -0
- package/dist/cli/parse-args.js.map +1 -0
- package/dist/cli-main.js +28 -1
- package/dist/cli-main.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +7 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +5 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +22 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -2
- package/dist/server/index.js.map +1 -1
- package/dist/station-dir.d.ts +7 -0
- package/dist/station-dir.d.ts.map +1 -0
- package/dist/station-dir.js +15 -0
- package/dist/station-dir.js.map +1 -0
- package/package.json +15 -10
- package/src/cli/deploy.ts +334 -0
- package/src/cli/parse-args.ts +99 -0
- package/src/cli-main.ts +28 -1
- package/src/cli.ts +1 -1
- package/src/config/loader.ts +7 -3
- package/src/config/schema.ts +29 -3
- package/src/index.ts +3 -1
- package/src/server/index.ts +14 -2
- package/src/station-dir.ts +24 -0
- /package/.next/standalone/packages/station-kit/.next/static/{kqMlNlRLox_FLRK-2GwrJ → xYd6dn0Ox68DaamIrH_pB}/_buildManifest.js +0 -0
- /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
|
});
|
package/src/config/loader.ts
CHANGED
|
@@ -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
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -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
|
|
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";
|
package/src/server/index.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
+
}
|
|
File without changes
|