station-kit 1.0.2 → 1.0.3

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 (58) hide show
  1. package/dist/cli-main.js +3 -29
  2. package/dist/cli-main.js.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server/index.d.ts.map +1 -1
  6. package/dist/server/index.js +89 -9
  7. package/dist/server/index.js.map +1 -1
  8. package/next.config.ts +1 -1
  9. package/out/404.html +1 -0
  10. package/out/_next/static/7q5_eGqbkbdP_jAqzl6RE/_buildManifest.js +1 -0
  11. package/out/_next/static/7q5_eGqbkbdP_jAqzl6RE/_ssgManifest.js +1 -0
  12. package/out/_next/static/chunks/580-f007f4d4c050db4e.js +1 -0
  13. package/out/_next/static/chunks/743-5bb03adbb0e4ddec.js +1 -0
  14. package/out/_next/static/chunks/8e6518bb-c26e82767f1faf66.js +1 -0
  15. package/out/_next/static/chunks/app/_not-found/page-ce21b4ba9038a5a7.js +1 -0
  16. package/out/_next/static/chunks/app/broadcasts/[id]/page-057eeaa51d28cbfd.js +1 -0
  17. package/out/_next/static/chunks/app/broadcasts/page-ac768ee4bcf3086f.js +1 -0
  18. package/out/_next/static/chunks/app/layout-a5f4d2f2e87939b2.js +1 -0
  19. package/out/_next/static/chunks/app/page-62d1dbcfdc93b566.js +1 -0
  20. package/out/_next/static/chunks/app/runs/[id]/page-7f726f2f4ea8f616.js +1 -0
  21. package/out/_next/static/chunks/app/signals/[name]/page-8f9f032eb0171ded.js +1 -0
  22. package/out/_next/static/chunks/app/signals/page-fb42d9c0368bbc58.js +1 -0
  23. package/out/_next/static/chunks/framework-077b27ad7787463c.js +1 -0
  24. package/out/_next/static/chunks/main-app-a9f19d5831b41b19.js +1 -0
  25. package/out/_next/static/chunks/main-f1c74cefd4965abf.js +1 -0
  26. package/out/_next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +1 -0
  27. package/out/_next/static/chunks/pages/_error-273a093c18b5ed0f.js +1 -0
  28. package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  29. package/out/_next/static/chunks/webpack-e40b05d6bdcb3589.js +1 -0
  30. package/out/_next/static/css/0be0e65a5f561f37.css +1 -0
  31. package/out/broadcasts/_.html +1 -0
  32. package/out/broadcasts/_.txt +22 -0
  33. package/out/broadcasts.html +1 -0
  34. package/out/broadcasts.txt +25 -0
  35. package/out/index.html +1 -0
  36. package/out/index.txt +25 -0
  37. package/out/runs/_.html +1 -0
  38. package/out/runs/_.txt +22 -0
  39. package/out/signals/_.html +1 -0
  40. package/out/signals/_.txt +22 -0
  41. package/out/signals.html +1 -0
  42. package/out/signals.txt +25 -0
  43. package/package.json +16 -9
  44. package/src/app/broadcasts/[id]/broadcast-detail.tsx +511 -0
  45. package/src/app/broadcasts/[id]/page.tsx +4 -506
  46. package/src/app/hooks/use-api.ts +1 -1
  47. package/src/app/hooks/use-realtime.ts +2 -3
  48. package/src/app/runs/[id]/page.tsx +4 -272
  49. package/src/app/runs/[id]/run-detail.tsx +277 -0
  50. package/src/app/signals/[name]/page.tsx +4 -245
  51. package/src/app/signals/[name]/signal-detail.tsx +250 -0
  52. package/src/cli-main.ts +3 -36
  53. package/src/cli.ts +1 -1
  54. package/src/server/index.ts +94 -10
  55. package/next-env.d.ts +0 -6
  56. package/station.config.example.ts +0 -16
  57. package/tsconfig.json +0 -12
  58. package/tsconfig.tsbuildinfo +0 -1
@@ -1,250 +1,9 @@
1
- "use client";
1
+ import { SignalDetail } from "./signal-detail";
2
2
 
3
- import { useEffect, useState, useCallback } from "react";
4
- import { useParams } from "next/navigation";
5
- import { useApi, type SignalMeta, type SchemaField } from "../../hooks/use-api";
6
- import { useStation } from "../../hooks/use-station";
7
- import { useBreadcrumb } from "../../hooks/use-breadcrumb";
8
- import { RunTable } from "../../components/run-table";
9
- import { SchemaForm } from "../../components/schema-form";
10
-
11
- function formatMs(ms: number): string {
12
- if (ms < 1000) return `${ms}ms`;
13
- if (ms < 60_000) return `${(ms / 1000).toFixed(0)}s`;
14
- if (ms < 3_600_000) return `${(ms / 60_000).toFixed(0)}m`;
15
- return `${(ms / 3_600_000).toFixed(0)}h`;
16
- }
17
-
18
- function renderSchemaInline(schema: SchemaField): string {
19
- if (schema.type === "object" && schema.properties) {
20
- const fields = Object.entries(schema.properties)
21
- .map(([key, field]) => `${key}: ${field.type}${field.required ? "" : "?"}`)
22
- .join(", ");
23
- return `{ ${fields} }`;
24
- }
25
- if (schema.type === "array" && schema.items) {
26
- return `${renderSchemaInline(schema.items)}[]`;
27
- }
28
- if (schema.type === "enum" && schema.values) {
29
- return schema.values.map((v) => `"${v}"`).join(" | ");
30
- }
31
- return schema.type;
3
+ export function generateStaticParams() {
4
+ return [{ name: "_" }];
32
5
  }
33
6
 
34
- const STATUS_FILTERS = ["all", "completed", "failed", "running", "pending"] as const;
35
- type StatusFilter = (typeof STATUS_FILTERS)[number];
36
-
37
7
  export default function SignalDetailPage() {
38
- const params = useParams();
39
- const name = params.name as string;
40
- const decodedName = decodeURIComponent(name);
41
- const api = useApi();
42
- const { events } = useStation();
43
-
44
- const [signal, setSignal] = useState<SignalMeta | null>(null);
45
- const [runs, setRuns] = useState<any[]>([]);
46
- const [loading, setLoading] = useState(true);
47
- const [triggering, setTriggering] = useState(false);
48
- const [inputJson, setInputJson] = useState("{}");
49
- const [filter, setFilter] = useState<StatusFilter>("all");
50
-
51
- useBreadcrumb(
52
- [{ label: "Signals", href: "/signals" }, { label: decodedName }],
53
- "signals",
54
- );
55
-
56
- const loadRuns = useCallback(() => {
57
- api.getSignalRuns(name).then((r) => setRuns(r.data)).catch((e) => console.error("Failed to refresh runs:", e));
58
- }, [name]);
59
-
60
- useEffect(() => {
61
- async function load() {
62
- try {
63
- const [signalRes, runsRes] = await Promise.all([
64
- api.getSignal(name),
65
- api.getSignalRuns(name),
66
- ]);
67
- setSignal(signalRes.data);
68
- setRuns(runsRes.data);
69
- } catch (err: unknown) {
70
- if (err instanceof Error) {
71
- console.error("Failed to load signal:", err.message);
72
- }
73
- }
74
- setLoading(false);
75
- }
76
- load();
77
- }, [name]);
78
-
79
- useEffect(() => {
80
- if (events.length === 0) return;
81
- const latest = events[0];
82
- if (latest.type.startsWith("run:")) {
83
- const eventSignal =
84
- (latest.data.run as Record<string, unknown>)?.signalName ??
85
- (latest.data as Record<string, unknown>).signalName;
86
- if (eventSignal === decodedName) {
87
- loadRuns();
88
- }
89
- }
90
- }, [events.length, decodedName, loadRuns]);
91
-
92
- async function handleTrigger() {
93
- setTriggering(true);
94
- try {
95
- const input = JSON.parse(inputJson);
96
- await api.triggerSignal(name, input);
97
- setInputJson("{}");
98
- setTimeout(loadRuns, 300);
99
- } catch (err: unknown) {
100
- if (err instanceof Error) {
101
- console.error("Trigger failed:", err.message);
102
- }
103
- }
104
- setTriggering(false);
105
- }
106
-
107
- if (loading) {
108
- return (
109
- <div>
110
- <h1 className="page-title">{decodedName}</h1>
111
- <div className="loading-bar"><div className="loading-bar-fill" /></div>
112
- </div>
113
- );
114
- }
115
-
116
- if (!signal) {
117
- return (
118
- <div>
119
- <h1 className="page-title">{decodedName}</h1>
120
- <div className="empty-state">
121
- <p className="empty-state-text">Signal not found.</p>
122
- </div>
123
- </div>
124
- );
125
- }
126
-
127
- const hasSchema = signal.inputSchema !== null || signal.outputSchema !== null;
128
- const filteredRuns = filter === "all" ? runs : runs.filter((r) => r.status === filter);
129
-
130
- return (
131
- <div>
132
- <div className="page-header">
133
- <h1 className="page-title" style={{ marginBottom: 0 }}>{decodedName}</h1>
134
- <div className="page-header-actions">
135
- <button className="btn btn--primary" onClick={handleTrigger} disabled={triggering}>
136
- {triggering ? "Dispatching..." : "Trigger"}
137
- </button>
138
- </div>
139
- </div>
140
-
141
- <div className="detail-section">
142
- <div className="detail-section-label">Configuration</div>
143
- <div className="config-grid">
144
- <div className="config-item">
145
- <span className="config-item-label">Schedule</span>
146
- <span className="config-item-value">{signal.interval ?? "Manual trigger"}</span>
147
- </div>
148
- <div className="config-item">
149
- <span className="config-item-label">Timeout</span>
150
- <span className="config-item-value">{formatMs(signal.timeout)}</span>
151
- </div>
152
- <div className="config-item">
153
- <span className="config-item-label">Max Attempts</span>
154
- <span className="config-item-value">{signal.maxAttempts}</span>
155
- </div>
156
- <div className="config-item">
157
- <span className="config-item-label">Max Concurrency</span>
158
- <span className="config-item-value">{signal.maxConcurrency ?? "\u2014"}</span>
159
- </div>
160
- <div className="config-item">
161
- <span className="config-item-label">Steps</span>
162
- <span className="config-item-value">
163
- {signal.hasSteps ? signal.stepNames.join(", ") : "Single handler"}
164
- </span>
165
- </div>
166
- </div>
167
- </div>
168
-
169
- {hasSchema && (
170
- <div className="detail-section">
171
- <div className="detail-section-label">Schema</div>
172
- <div className="schema-pair">
173
- <div>
174
- <div style={{
175
- fontFamily: "var(--font-mono)",
176
- fontSize: "0.6875rem",
177
- textTransform: "uppercase",
178
- letterSpacing: "0.08em",
179
- color: "var(--muted)",
180
- marginBottom: "0.5rem",
181
- }}>
182
- Input
183
- </div>
184
- {signal.inputSchema ? (
185
- <pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
186
- {renderSchemaInline(signal.inputSchema)}
187
- </pre>
188
- ) : (
189
- <span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
190
- )}
191
- </div>
192
- <div>
193
- <div style={{
194
- fontFamily: "var(--font-mono)",
195
- fontSize: "0.6875rem",
196
- textTransform: "uppercase",
197
- letterSpacing: "0.08em",
198
- color: "var(--muted)",
199
- marginBottom: "0.5rem",
200
- }}>
201
- Output
202
- </div>
203
- {signal.outputSchema ? (
204
- <pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
205
- {renderSchemaInline(signal.outputSchema)}
206
- </pre>
207
- ) : (
208
- <span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
209
- )}
210
- </div>
211
- </div>
212
- </div>
213
- )}
214
-
215
- <div className="detail-section">
216
- <div className="detail-section-label">Trigger</div>
217
- <SchemaForm
218
- schema={signal.inputSchema}
219
- value={inputJson}
220
- onChange={setInputJson}
221
- />
222
- <div style={{ marginTop: "0.5rem" }}>
223
- <button
224
- className="btn btn--primary"
225
- onClick={handleTrigger}
226
- disabled={triggering}
227
- >
228
- {triggering ? "Dispatching..." : "Dispatch"}
229
- </button>
230
- </div>
231
- </div>
232
-
233
- <div className="detail-section">
234
- <div className="detail-section-label">Run History</div>
235
- <div className="filter-bar">
236
- {STATUS_FILTERS.map((f) => (
237
- <button
238
- key={f}
239
- className={`filter-btn${filter === f ? " filter-btn--active" : ""}`}
240
- onClick={() => setFilter(f)}
241
- >
242
- {f.charAt(0).toUpperCase() + f.slice(1)}
243
- </button>
244
- ))}
245
- </div>
246
- <RunTable runs={filteredRuns} />
247
- </div>
248
- </div>
249
- );
8
+ return <SignalDetail />;
250
9
  }
@@ -0,0 +1,250 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { useParams } from "next/navigation";
5
+ import { useApi, type SignalMeta, type SchemaField } from "../../hooks/use-api";
6
+ import { useStation } from "../../hooks/use-station";
7
+ import { useBreadcrumb } from "../../hooks/use-breadcrumb";
8
+ import { RunTable } from "../../components/run-table";
9
+ import { SchemaForm } from "../../components/schema-form";
10
+
11
+ function formatMs(ms: number): string {
12
+ if (ms < 1000) return `${ms}ms`;
13
+ if (ms < 60_000) return `${(ms / 1000).toFixed(0)}s`;
14
+ if (ms < 3_600_000) return `${(ms / 60_000).toFixed(0)}m`;
15
+ return `${(ms / 3_600_000).toFixed(0)}h`;
16
+ }
17
+
18
+ function renderSchemaInline(schema: SchemaField): string {
19
+ if (schema.type === "object" && schema.properties) {
20
+ const fields = Object.entries(schema.properties)
21
+ .map(([key, field]) => `${key}: ${field.type}${field.required ? "" : "?"}`)
22
+ .join(", ");
23
+ return `{ ${fields} }`;
24
+ }
25
+ if (schema.type === "array" && schema.items) {
26
+ return `${renderSchemaInline(schema.items)}[]`;
27
+ }
28
+ if (schema.type === "enum" && schema.values) {
29
+ return schema.values.map((v) => `"${v}"`).join(" | ");
30
+ }
31
+ return schema.type;
32
+ }
33
+
34
+ const STATUS_FILTERS = ["all", "completed", "failed", "running", "pending"] as const;
35
+ type StatusFilter = (typeof STATUS_FILTERS)[number];
36
+
37
+ export function SignalDetail() {
38
+ const params = useParams();
39
+ const name = params.name as string;
40
+ const decodedName = decodeURIComponent(name);
41
+ const api = useApi();
42
+ const { events } = useStation();
43
+
44
+ const [signal, setSignal] = useState<SignalMeta | null>(null);
45
+ const [runs, setRuns] = useState<any[]>([]);
46
+ const [loading, setLoading] = useState(true);
47
+ const [triggering, setTriggering] = useState(false);
48
+ const [inputJson, setInputJson] = useState("{}");
49
+ const [filter, setFilter] = useState<StatusFilter>("all");
50
+
51
+ useBreadcrumb(
52
+ [{ label: "Signals", href: "/signals" }, { label: decodedName }],
53
+ "signals",
54
+ );
55
+
56
+ const loadRuns = useCallback(() => {
57
+ api.getSignalRuns(name).then((r) => setRuns(r.data)).catch((e) => console.error("Failed to refresh runs:", e));
58
+ }, [name]);
59
+
60
+ useEffect(() => {
61
+ async function load() {
62
+ try {
63
+ const [signalRes, runsRes] = await Promise.all([
64
+ api.getSignal(name),
65
+ api.getSignalRuns(name),
66
+ ]);
67
+ setSignal(signalRes.data);
68
+ setRuns(runsRes.data);
69
+ } catch (err: unknown) {
70
+ if (err instanceof Error) {
71
+ console.error("Failed to load signal:", err.message);
72
+ }
73
+ }
74
+ setLoading(false);
75
+ }
76
+ load();
77
+ }, [name]);
78
+
79
+ useEffect(() => {
80
+ if (events.length === 0) return;
81
+ const latest = events[0];
82
+ if (latest.type.startsWith("run:")) {
83
+ const eventSignal =
84
+ (latest.data.run as Record<string, unknown>)?.signalName ??
85
+ (latest.data as Record<string, unknown>).signalName;
86
+ if (eventSignal === decodedName) {
87
+ loadRuns();
88
+ }
89
+ }
90
+ }, [events.length, decodedName, loadRuns]);
91
+
92
+ async function handleTrigger() {
93
+ setTriggering(true);
94
+ try {
95
+ const input = JSON.parse(inputJson);
96
+ await api.triggerSignal(name, input);
97
+ setInputJson("{}");
98
+ setTimeout(loadRuns, 300);
99
+ } catch (err: unknown) {
100
+ if (err instanceof Error) {
101
+ console.error("Trigger failed:", err.message);
102
+ }
103
+ }
104
+ setTriggering(false);
105
+ }
106
+
107
+ if (loading) {
108
+ return (
109
+ <div>
110
+ <h1 className="page-title">{decodedName}</h1>
111
+ <div className="loading-bar"><div className="loading-bar-fill" /></div>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ if (!signal) {
117
+ return (
118
+ <div>
119
+ <h1 className="page-title">{decodedName}</h1>
120
+ <div className="empty-state">
121
+ <p className="empty-state-text">Signal not found.</p>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ const hasSchema = signal.inputSchema !== null || signal.outputSchema !== null;
128
+ const filteredRuns = filter === "all" ? runs : runs.filter((r) => r.status === filter);
129
+
130
+ return (
131
+ <div>
132
+ <div className="page-header">
133
+ <h1 className="page-title" style={{ marginBottom: 0 }}>{decodedName}</h1>
134
+ <div className="page-header-actions">
135
+ <button className="btn btn--primary" onClick={handleTrigger} disabled={triggering}>
136
+ {triggering ? "Dispatching..." : "Trigger"}
137
+ </button>
138
+ </div>
139
+ </div>
140
+
141
+ <div className="detail-section">
142
+ <div className="detail-section-label">Configuration</div>
143
+ <div className="config-grid">
144
+ <div className="config-item">
145
+ <span className="config-item-label">Schedule</span>
146
+ <span className="config-item-value">{signal.interval ?? "Manual trigger"}</span>
147
+ </div>
148
+ <div className="config-item">
149
+ <span className="config-item-label">Timeout</span>
150
+ <span className="config-item-value">{formatMs(signal.timeout)}</span>
151
+ </div>
152
+ <div className="config-item">
153
+ <span className="config-item-label">Max Attempts</span>
154
+ <span className="config-item-value">{signal.maxAttempts}</span>
155
+ </div>
156
+ <div className="config-item">
157
+ <span className="config-item-label">Max Concurrency</span>
158
+ <span className="config-item-value">{signal.maxConcurrency ?? "\u2014"}</span>
159
+ </div>
160
+ <div className="config-item">
161
+ <span className="config-item-label">Steps</span>
162
+ <span className="config-item-value">
163
+ {signal.hasSteps ? signal.stepNames.join(", ") : "Single handler"}
164
+ </span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ {hasSchema && (
170
+ <div className="detail-section">
171
+ <div className="detail-section-label">Schema</div>
172
+ <div className="schema-pair">
173
+ <div>
174
+ <div style={{
175
+ fontFamily: "var(--font-mono)",
176
+ fontSize: "0.6875rem",
177
+ textTransform: "uppercase",
178
+ letterSpacing: "0.08em",
179
+ color: "var(--muted)",
180
+ marginBottom: "0.5rem",
181
+ }}>
182
+ Input
183
+ </div>
184
+ {signal.inputSchema ? (
185
+ <pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
186
+ {renderSchemaInline(signal.inputSchema)}
187
+ </pre>
188
+ ) : (
189
+ <span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
190
+ )}
191
+ </div>
192
+ <div>
193
+ <div style={{
194
+ fontFamily: "var(--font-mono)",
195
+ fontSize: "0.6875rem",
196
+ textTransform: "uppercase",
197
+ letterSpacing: "0.08em",
198
+ color: "var(--muted)",
199
+ marginBottom: "0.5rem",
200
+ }}>
201
+ Output
202
+ </div>
203
+ {signal.outputSchema ? (
204
+ <pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
205
+ {renderSchemaInline(signal.outputSchema)}
206
+ </pre>
207
+ ) : (
208
+ <span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
209
+ )}
210
+ </div>
211
+ </div>
212
+ </div>
213
+ )}
214
+
215
+ <div className="detail-section">
216
+ <div className="detail-section-label">Trigger</div>
217
+ <SchemaForm
218
+ schema={signal.inputSchema}
219
+ value={inputJson}
220
+ onChange={setInputJson}
221
+ />
222
+ <div style={{ marginTop: "0.5rem" }}>
223
+ <button
224
+ className="btn btn--primary"
225
+ onClick={handleTrigger}
226
+ disabled={triggering}
227
+ >
228
+ {triggering ? "Dispatching..." : "Dispatch"}
229
+ </button>
230
+ </div>
231
+ </div>
232
+
233
+ <div className="detail-section">
234
+ <div className="detail-section-label">Run History</div>
235
+ <div className="filter-bar">
236
+ {STATUS_FILTERS.map((f) => (
237
+ <button
238
+ key={f}
239
+ className={`filter-btn${filter === f ? " filter-btn--active" : ""}`}
240
+ onClick={() => setFilter(f)}
241
+ >
242
+ {f.charAt(0).toUpperCase() + f.slice(1)}
243
+ </button>
244
+ ))}
245
+ </div>
246
+ <RunTable runs={filteredRuns} />
247
+ </div>
248
+ </div>
249
+ );
250
+ }
package/src/cli-main.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
2
- import { resolve } from "node:path";
3
1
  import { loadConfig } from "./config/loader.js";
4
2
  import { createStation } from "./server/index.js";
5
3
 
@@ -10,45 +8,15 @@ const config = await loadConfig(cwd);
10
8
  const station = await createStation(config, cwd);
11
9
  await station.start();
12
10
 
13
- // Start Next.js dev server as child process
14
- const nextPort = config.port + 1;
15
- const stationRoot = resolve(import.meta.dirname, "..");
16
-
17
- const nextProcess: ChildProcess = spawn(
18
- "npx",
19
- ["next", "dev", "--port", String(nextPort), "--hostname", config.host],
20
- {
21
- cwd: stationRoot,
22
- stdio: ["ignore", "pipe", "pipe"],
23
- env: {
24
- ...process.env,
25
- NEXT_PUBLIC_STATION_API: `http://${config.host}:${config.port}`,
26
- },
27
- },
28
- );
29
-
30
- nextProcess.stdout?.on("data", (chunk: Buffer) => {
31
- const msg = chunk.toString().trim();
32
- if (msg) console.log(`[station:ui] ${msg}`);
33
- });
34
-
35
- nextProcess.stderr?.on("data", (chunk: Buffer) => {
36
- const msg = chunk.toString().trim();
37
- // Filter out noisy Next.js dev warnings
38
- if (msg && !msg.includes("ExperimentalWarning")) {
39
- console.error(`[station:ui] ${msg}`);
40
- }
41
- });
42
-
43
- console.log(`[station] Dashboard on http://${config.host}:${nextPort}`);
11
+ console.log(`[station] Dashboard on http://${config.host}:${config.port}`);
44
12
 
45
13
  // Open browser
46
14
  if (config.open) {
47
- const url = `http://${config.host}:${nextPort}`;
15
+ const url = `http://${config.host}:${config.port}`;
48
16
  const { execFile } = await import("node:child_process");
49
17
  const platform = process.platform;
50
18
 
51
- await new Promise((res) => setTimeout(() => res(true), 5000));
19
+ await new Promise((res) => setTimeout(() => res(true), 2000));
52
20
  if (platform === "darwin") {
53
21
  execFile("open", [url]);
54
22
  } else if (platform === "linux") {
@@ -61,7 +29,6 @@ if (config.open) {
61
29
  // Graceful shutdown
62
30
  const shutdown = async () => {
63
31
  console.log("\n[station] Shutting down...");
64
- nextProcess.kill("SIGTERM");
65
32
  await station.stop();
66
33
  process.exit(0);
67
34
  };
package/src/cli.ts CHANGED
@@ -25,7 +25,7 @@ if (!process.env[MARKER]) {
25
25
  const main = fileURLToPath(new URL("./cli-main.js", import.meta.url));
26
26
  const child = spawn(execPath, ["--import", tsxSpecifier, main], {
27
27
  stdio: "inherit",
28
- env: { ...process.env, [MARKER]: "1" },
28
+ env: { ...process.env, [MARKER]: "1", __STATION_TSX: tsxSpecifier },
29
29
  });
30
30
  child.on("exit", (code) => process.exit(code ?? 0));
31
31
  child.on("error", (err) => {