agent-stage 0.2.15 → 0.2.18

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 (53) hide show
  1. package/dist/commands/guide.js +5 -5
  2. package/dist/commands/init.d.ts +2 -0
  3. package/dist/commands/init.js +164 -0
  4. package/dist/commands/page/add.js +5 -40
  5. package/dist/commands/run/exec.js +1 -1
  6. package/dist/commands/run/inspect.js +1 -1
  7. package/dist/commands/run/watch.js +1 -1
  8. package/dist/commands/serve.d.ts +2 -0
  9. package/dist/commands/serve.js +238 -0
  10. package/dist/commands/status.d.ts +2 -0
  11. package/dist/commands/{dev/status.js → status.js} +17 -19
  12. package/dist/commands/stop.d.ts +2 -0
  13. package/dist/commands/stop.js +40 -0
  14. package/dist/index.js +8 -2
  15. package/dist/utils/agent-helper.js +5 -5
  16. package/dist/utils/paths.js +5 -5
  17. package/dist/utils/tunnel.d.ts +1 -1
  18. package/dist/utils/tunnel.js +1 -1
  19. package/package.json +8 -5
  20. package/dist/commands/dev/index.d.ts +0 -2
  21. package/dist/commands/dev/index.js +0 -11
  22. package/dist/commands/dev/init.d.ts +0 -2
  23. package/dist/commands/dev/init.js +0 -215
  24. package/dist/commands/dev/start.d.ts +0 -2
  25. package/dist/commands/dev/start.js +0 -145
  26. package/dist/commands/dev/status.d.ts +0 -2
  27. package/dist/commands/dev/stop.d.ts +0 -2
  28. package/dist/commands/dev/stop.js +0 -45
  29. package/template/components.json +0 -17
  30. package/template/index.html +0 -13
  31. package/template/package.json +0 -41
  32. package/template/postcss.config.js +0 -6
  33. package/template/src/components/PageRenderer.tsx +0 -108
  34. package/template/src/components/bridge-state-provider.tsx +0 -87
  35. package/template/src/components/ui/button.tsx +0 -55
  36. package/template/src/components/ui/card.tsx +0 -78
  37. package/template/src/components/ui/input.tsx +0 -24
  38. package/template/src/index.css +0 -59
  39. package/template/src/lib/bridge.ts +0 -53
  40. package/template/src/lib/utils.ts +0 -6
  41. package/template/src/main.tsx +0 -23
  42. package/template/src/pages/counter/store.json +0 -8
  43. package/template/src/pages/counter/ui.json +0 -108
  44. package/template/src/pages/test-page/store.json +0 -8
  45. package/template/src/routeTree.gen.ts +0 -77
  46. package/template/src/routes/__root.tsx +0 -11
  47. package/template/src/routes/counter.tsx +0 -19
  48. package/template/src/routes/index.tsx +0 -46
  49. package/template/src/vite-env.d.ts +0 -1
  50. package/template/tailwind.config.js +0 -53
  51. package/template/tsconfig.json +0 -25
  52. package/template/tsconfig.node.json +0 -11
  53. package/template/vite.config.ts +0 -22
@@ -17,9 +17,9 @@ const guides = {
17
17
  explanation: 'Creates a page with UI and initial state'
18
18
  },
19
19
  {
20
- scenario: '3. Start dev server',
21
- command: 'agentstage dev start',
22
- explanation: 'Starts the development server'
20
+ scenario: '3. Start page runtime',
21
+ command: 'agentstage serve mypage',
22
+ explanation: 'Starts the page runtime process'
23
23
  }
24
24
  ],
25
25
  commonErrors: [
@@ -160,8 +160,8 @@ push/pop: Navigate forward/back`
160
160
  },
161
161
  {
162
162
  error: 'Runtime is not running',
163
- cause: 'Dev server is not started',
164
- fix: 'Run: agentstage dev start'
163
+ cause: 'Page runtime is not started',
164
+ fix: 'Run: agentstage serve <pageId>'
165
165
  }
166
166
  ]
167
167
  },
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const initCommand: Command;
@@ -0,0 +1,164 @@
1
+ import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
3
+ import consola from "consola";
4
+ import c from "picocolors";
5
+ import { execa } from "execa";
6
+ import { mkdir, readdir, writeFile } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ import { resolve, join } from "pathe";
9
+ import { homedir } from "os";
10
+ import { setWorkspaceDir } from "../utils/paths.js";
11
+ import { checkCloudflared, printInstallInstructions, } from "../utils/cloudflared.js";
12
+ const PROJECT_NAME = "webapp";
13
+ const workspaceDependencies = {
14
+ "@agentstage/render": "^0.2.2",
15
+ "@agentstage/bridge": "^0.1.2",
16
+ react: "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ zod: "^3.23.0",
19
+ };
20
+ export const initCommand = new Command("init")
21
+ .description("Initialize a new Agentstage runtime workspace")
22
+ .option("-y, --yes", "Use default settings (non-interactive)", false)
23
+ .option("--skip-cloudflared-check", "Skip cloudflared installation check", false)
24
+ .action(async (options) => {
25
+ const useDefault = options.yes;
26
+ try {
27
+ await execa("bun", ["--version"], { stdio: "pipe" });
28
+ }
29
+ catch {
30
+ consola.error("Bun is required to initialize workspace.");
31
+ consola.info("Install Bun: https://bun.sh/docs/installation");
32
+ process.exit(1);
33
+ }
34
+ if (!options.skipCloudflaredCheck) {
35
+ const cloudflaredInfo = await checkCloudflared();
36
+ if (!cloudflaredInfo.installed) {
37
+ printInstallInstructions(cloudflaredInfo);
38
+ const shouldContinue = await p.confirm({
39
+ message: "Continue with initialization? (You can install cloudflared later)",
40
+ initialValue: true,
41
+ });
42
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
43
+ consola.info("Cancelled");
44
+ process.exit(0);
45
+ }
46
+ }
47
+ else {
48
+ consola.success(`Cloudflare Tunnel available: ${c.dim(cloudflaredInfo.version)}`);
49
+ }
50
+ }
51
+ let locationMode;
52
+ if (useDefault) {
53
+ locationMode = "default";
54
+ }
55
+ else {
56
+ const result = await p.select({
57
+ message: "Where to store the workspace?",
58
+ options: [
59
+ {
60
+ value: "default",
61
+ label: `Default (~/.agentstage/${PROJECT_NAME})`,
62
+ hint: "Recommended",
63
+ },
64
+ {
65
+ value: "current",
66
+ label: "Current directory (./.agentstage)",
67
+ },
68
+ {
69
+ value: "custom",
70
+ label: "Custom path",
71
+ },
72
+ ],
73
+ });
74
+ if (p.isCancel(result)) {
75
+ consola.info("Cancelled");
76
+ return;
77
+ }
78
+ locationMode = result;
79
+ }
80
+ let targetDir;
81
+ switch (locationMode) {
82
+ case "default":
83
+ targetDir = join(homedir(), ".agentstage", PROJECT_NAME);
84
+ break;
85
+ case "current":
86
+ targetDir = join(process.cwd(), ".agentstage");
87
+ break;
88
+ case "custom": {
89
+ const customPath = await p.text({
90
+ message: "Enter custom path:",
91
+ placeholder: "/path/to/workspace",
92
+ validate: (value) => {
93
+ if (!value || value.trim() === "") {
94
+ return "Path is required";
95
+ }
96
+ },
97
+ });
98
+ if (p.isCancel(customPath)) {
99
+ consola.info("Cancelled");
100
+ return;
101
+ }
102
+ targetDir = resolve(customPath);
103
+ break;
104
+ }
105
+ default:
106
+ targetDir = join(homedir(), ".agentstage", PROJECT_NAME);
107
+ }
108
+ if (existsSync(targetDir)) {
109
+ const files = await readdirSafe(targetDir);
110
+ if (files.length > 0) {
111
+ console.log();
112
+ consola.info("Workspace already initialized!");
113
+ console.log(` Location: ${c.cyan(targetDir)}`);
114
+ console.log();
115
+ console.log(` cd ${c.cyan(targetDir)}`);
116
+ console.log(` ${c.cyan("agentstage serve <pageId>")}`);
117
+ console.log();
118
+ await setWorkspaceDir(targetDir);
119
+ return;
120
+ }
121
+ }
122
+ await setWorkspaceDir(targetDir);
123
+ const s = p.spinner();
124
+ try {
125
+ s.start("Creating workspace structure...");
126
+ await mkdir(join(targetDir, "pages"), { recursive: true });
127
+ await mkdir(join(targetDir, ".agentstage"), { recursive: true });
128
+ const packageJson = {
129
+ name: "agentstage-workspace",
130
+ private: true,
131
+ version: "0.0.0",
132
+ type: "module",
133
+ dependencies: workspaceDependencies,
134
+ };
135
+ await writeFile(join(targetDir, "package.json"), JSON.stringify(packageJson, null, 2));
136
+ s.stop("Workspace created");
137
+ s.start("Installing dependencies with Bun...");
138
+ await execa("bun", ["install"], { cwd: targetDir, stdio: "pipe" });
139
+ s.stop("Dependencies installed");
140
+ console.log();
141
+ consola.success("Workspace initialized successfully");
142
+ console.log(` Location: ${c.cyan(targetDir)}`);
143
+ console.log(` Pages: ${c.cyan("pages/<pageId>/ui.json")}`);
144
+ console.log();
145
+ console.log(` cd ${c.cyan(targetDir)}`);
146
+ console.log(` ${c.cyan("agentstage page add counter")}`);
147
+ console.log(` ${c.cyan("agentstage serve counter")}`);
148
+ console.log();
149
+ }
150
+ catch (error) {
151
+ s.stop("Failed to initialize workspace");
152
+ const message = error instanceof Error ? error.message : "Unknown initialization error";
153
+ consola.error(message);
154
+ process.exit(1);
155
+ }
156
+ });
157
+ async function readdirSafe(dir) {
158
+ try {
159
+ return await readdir(dir);
160
+ }
161
+ catch {
162
+ return [];
163
+ }
164
+ }
@@ -2,7 +2,6 @@ import { Command } from 'commander';
2
2
  import consola from 'consola';
3
3
  import c from 'picocolors';
4
4
  import { writeFile, mkdir } from 'fs/promises';
5
- import { existsSync } from 'fs';
6
5
  import { join } from 'pathe';
7
6
  import { getWorkspaceDir, isInitialized, readRuntimeConfig, getPagesDir } from '../../utils/paths.js';
8
7
  import { FileStore } from '@agentstage/bridge';
@@ -35,19 +34,9 @@ export const pageAddCommand = new Command('add')
35
34
  try {
36
35
  const workspaceDir = await getWorkspaceDir();
37
36
  const config = await readRuntimeConfig();
38
- const routesDir = join(workspaceDir, 'src', 'routes');
39
- const pagesDir = join(workspaceDir, 'src', 'pages', name);
40
- const pageFile = join(routesDir, `${name}.tsx`);
37
+ const pagesDir = join(workspaceDir, 'pages', name);
41
38
  // 确保目录存在
42
- await mkdir(routesDir, { recursive: true });
43
39
  await mkdir(pagesDir, { recursive: true });
44
- if (existsSync(pageFile)) {
45
- printAgentErrorHelp(`Page "${name}" already exists`);
46
- process.exit(1);
47
- }
48
- // 生成 .tsx 路由文件
49
- const pageContent = generateTsxContent(name);
50
- await writeFile(pageFile, pageContent);
51
40
  // 处理 UI
52
41
  let uiContent;
53
42
  if (options.uiStdin) {
@@ -122,17 +111,16 @@ export const pageAddCommand = new Command('add')
122
111
  if (options.ui) {
123
112
  // 提供了完整 UI
124
113
  printAgentSuccess(`Page "${name}" created with custom UI and state`, [
125
- `Start dev server: agentstage dev start`,
126
- `Open http://localhost:${port}/${name} to see your page`,
114
+ `Start runtime: agentstage serve ${name}`,
115
+ `Open http://localhost:${port} to see your page`,
127
116
  `Update state: agentstage run set-state ${name} '{"key": "value"}'`
128
117
  ]);
129
118
  }
130
119
  else {
131
120
  // 默认 UI,输出 prompts
132
121
  consola.success(`Page "${name}" created`);
133
- console.log(` Route: ${c.cyan(`src/routes/${name}.tsx`)}`);
134
- console.log(` UI: ${c.cyan(`src/pages/${name}/ui.json`)}`);
135
- console.log(` Store: ${c.cyan(`src/pages/${name}/store.json`)}`);
122
+ console.log(` UI: ${c.cyan(`pages/${name}/ui.json`)}`);
123
+ console.log(` Store: ${c.cyan(`pages/${name}/store.json`)}`);
136
124
  console.log(` URL: ${c.cyan(`http://localhost:${port}/${name}`)}`);
137
125
  console.log();
138
126
  console.log(c.bold('─'.repeat(60)));
@@ -152,26 +140,6 @@ export const pageAddCommand = new Command('add')
152
140
  process.exit(1);
153
141
  }
154
142
  });
155
- function generateTsxContent(name) {
156
- const pascalName = toPascalCase(name);
157
- return `import { createFileRoute } from '@tanstack/react-router'
158
- import { useMemo } from 'react'
159
- import { PageRenderer } from '../components/PageRenderer'
160
- import { createPageBridge } from '../lib/bridge'
161
-
162
- export const Route = createFileRoute('/${name}')({
163
- component: ${pascalName}Page,
164
- })
165
-
166
- function ${pascalName}Page() {
167
- const bridge = useMemo(() => createPageBridge({
168
- pageId: '${name}',
169
- }), [])
170
-
171
- return <PageRenderer pageId="${name}" bridge={bridge} />
172
- }
173
- `;
174
- }
175
143
  function generateDefaultUi(name) {
176
144
  const titleName = toTitleCase(name);
177
145
  return {
@@ -211,9 +179,6 @@ function generateDefaultState(name) {
211
179
  pageId: name,
212
180
  };
213
181
  }
214
- function toPascalCase(str) {
215
- return str.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
216
- }
217
182
  function toTitleCase(str) {
218
183
  return str.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
219
184
  }
@@ -16,7 +16,7 @@ export const runExecCommand = new Command('exec')
16
16
  }
17
17
  const config = await readRuntimeConfig();
18
18
  if (!config) {
19
- consola.error('Runtime is not running. Start it first with `agentstage dev start`.');
19
+ consola.error('Runtime is not running. Start it first with `agentstage serve <pageId>`.');
20
20
  process.exit(1);
21
21
  }
22
22
  // Parse payload
@@ -13,7 +13,7 @@ export const runInspectCommand = new Command('inspect')
13
13
  }
14
14
  const config = await readRuntimeConfig();
15
15
  if (!config) {
16
- consola.error('Runtime is not running. Start it first with `agentstage dev start`.');
16
+ consola.error('Runtime is not running. Start it first with `agentstage serve <pageId>`.');
17
17
  process.exit(1);
18
18
  }
19
19
  const client = new BridgeClient(`ws://localhost:${config.port}/_bridge`);
@@ -14,7 +14,7 @@ export const runWatchCommand = new Command('watch')
14
14
  }
15
15
  const config = await readRuntimeConfig();
16
16
  if (!config) {
17
- consola.error('Runtime is not running. Start it first with `agentstage dev start`.');
17
+ consola.error('Runtime is not running. Start it first with `agentstage serve <pageId>`.');
18
18
  process.exit(1);
19
19
  }
20
20
  const client = new BridgeClient(`ws://localhost:${config.port}/_bridge`);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const serveCommand: Command;
@@ -0,0 +1,238 @@
1
+ import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
3
+ import consola from "consola";
4
+ import c from "picocolors";
5
+ import { spawn } from "node:child_process";
6
+ import { createRequire } from "node:module";
7
+ import { createServer } from "node:net";
8
+ import { execa } from "execa";
9
+ import { existsSync } from "fs";
10
+ import { dirname, join } from "pathe";
11
+ import { getWorkspaceDir, isInitialized, readRuntimeConfig, saveRuntimeConfig, } from "../utils/paths.js";
12
+ import { canStartTunnel, startTunnel, printTunnelInfo } from "../utils/tunnel.js";
13
+ import { checkCloudflared, printInstallInstructions } from "../utils/cloudflared.js";
14
+ const require = createRequire(import.meta.url);
15
+ async function ensurePortAvailable(port, host) {
16
+ await new Promise((resolve, reject) => {
17
+ const probe = createServer();
18
+ probe.once("error", (error) => {
19
+ probe.close();
20
+ if (error.code === "EADDRINUSE") {
21
+ reject(new Error(`Port ${port} is already in use`));
22
+ return;
23
+ }
24
+ reject(error);
25
+ });
26
+ probe.listen(port, host, () => {
27
+ probe.close((error) => {
28
+ if (error) {
29
+ reject(error);
30
+ return;
31
+ }
32
+ resolve();
33
+ });
34
+ });
35
+ });
36
+ }
37
+ async function waitForRuntimeReady(port, pageId, timeoutMs = 5000) {
38
+ const start = Date.now();
39
+ let lastError = null;
40
+ while (Date.now() - start < timeoutMs) {
41
+ try {
42
+ const response = await fetch(`http://127.0.0.1:${port}/health`, {
43
+ signal: AbortSignal.timeout(500),
44
+ });
45
+ if (response.ok) {
46
+ const health = (await response.json());
47
+ if (health.ok === true && health.pageId === pageId) {
48
+ return;
49
+ }
50
+ lastError = new Error("runtime health mismatch");
51
+ }
52
+ else {
53
+ lastError = new Error(`health returned ${response.status}`);
54
+ }
55
+ }
56
+ catch (error) {
57
+ lastError = error;
58
+ }
59
+ await new Promise((resolve) => setTimeout(resolve, 200));
60
+ }
61
+ throw lastError instanceof Error
62
+ ? lastError
63
+ : new Error("Runtime health check timed out");
64
+ }
65
+ function resolveRenderServeBin() {
66
+ const override = process.env.AGENTSTAGE_RENDER_SERVE_BIN;
67
+ if (override) {
68
+ return override;
69
+ }
70
+ try {
71
+ const serveModulePath = require.resolve("@agentstage/render/serve");
72
+ const candidate = join(dirname(serveModulePath), "serve-cli.js");
73
+ if (existsSync(candidate)) {
74
+ return candidate;
75
+ }
76
+ }
77
+ catch {
78
+ // ignore resolution failure
79
+ }
80
+ return null;
81
+ }
82
+ async function waitForRuntimeProcessOrReady(subprocess, ready) {
83
+ let onError = null;
84
+ let onExit = null;
85
+ const exited = new Promise((_, reject) => {
86
+ onError = (error) => {
87
+ reject(error);
88
+ };
89
+ onExit = (code, signal) => {
90
+ const detail = code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
91
+ reject(new Error(`Runtime process exited before becoming ready (${detail})`));
92
+ };
93
+ subprocess.once("error", onError);
94
+ subprocess.once("exit", onExit);
95
+ });
96
+ try {
97
+ await Promise.race([ready, exited]);
98
+ }
99
+ finally {
100
+ if (onError) {
101
+ subprocess.off("error", onError);
102
+ }
103
+ if (onExit) {
104
+ subprocess.off("exit", onExit);
105
+ }
106
+ }
107
+ }
108
+ export const serveCommand = new Command("serve")
109
+ .description("Serve a single page runtime (Bun required)")
110
+ .argument("<pageId>", "Page id to serve")
111
+ .option("-p, --port <port>", "Port to run the server on", "3000")
112
+ .option("--host <host>", "Host to bind", "0.0.0.0")
113
+ .option("-t, --tunnel", "Expose server to internet via Cloudflare Tunnel", false)
114
+ .option("--open", "Open browser automatically", false)
115
+ .action(async (pageId, options) => {
116
+ if (!isInitialized()) {
117
+ consola.error("Project not initialized. Please run `agentstage init` first.");
118
+ process.exit(1);
119
+ }
120
+ if (!/^[a-z0-9-]+$/.test(pageId)) {
121
+ consola.error("Invalid pageId. Allowed: lowercase letters, numbers, hyphen");
122
+ process.exit(1);
123
+ }
124
+ const workspaceDir = await getWorkspaceDir();
125
+ const port = Number.parseInt(String(options.port), 10);
126
+ const host = String(options.host || "0.0.0.0");
127
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) {
128
+ consola.error(`Invalid port: ${options.port}`);
129
+ process.exit(1);
130
+ }
131
+ const pageUiPath = join(workspaceDir, "pages", pageId, "ui.json");
132
+ if (!existsSync(pageUiPath)) {
133
+ consola.error(`Page "${pageId}" not found: ${pageUiPath}`);
134
+ process.exit(1);
135
+ }
136
+ try {
137
+ await execa("bun", ["--version"], { stdio: "pipe" });
138
+ }
139
+ catch {
140
+ consola.error("Bun is required but not found.");
141
+ consola.info("Install Bun: https://bun.sh/docs/installation");
142
+ process.exit(1);
143
+ }
144
+ const serveBin = resolveRenderServeBin();
145
+ if (!serveBin) {
146
+ consola.error("Cannot resolve @agentstage/render serve runtime entry.");
147
+ process.exit(1);
148
+ }
149
+ const existingConfig = await readRuntimeConfig();
150
+ if (existingConfig) {
151
+ try {
152
+ process.kill(existingConfig.pid, 0);
153
+ consola.warn(`Runtime is already running (PID: ${existingConfig.pid}, Port: ${existingConfig.port})`);
154
+ console.log(` Web: ${c.cyan(`http://localhost:${existingConfig.port}`)}`);
155
+ if (existingConfig.tunnelUrl) {
156
+ console.log(` Public: ${c.cyan(c.underline(existingConfig.tunnelUrl))}`);
157
+ }
158
+ console.log(` Bridge: ${c.cyan(`ws://localhost:${existingConfig.port}/_bridge`)}`);
159
+ return;
160
+ }
161
+ catch {
162
+ // stale runtime config
163
+ }
164
+ }
165
+ await ensurePortAvailable(port, host);
166
+ let tunnelUrl;
167
+ if (options.tunnel) {
168
+ const canTunnel = await canStartTunnel();
169
+ if (!canTunnel) {
170
+ const info = await checkCloudflared();
171
+ printInstallInstructions(info);
172
+ consola.error("Cannot start with --tunnel: cloudflared not installed");
173
+ process.exit(1);
174
+ }
175
+ }
176
+ const s = p.spinner();
177
+ s.start(`Starting page runtime (${pageId})...`);
178
+ try {
179
+ const subprocess = spawn("bun", [
180
+ serveBin,
181
+ "--workspace",
182
+ workspaceDir,
183
+ "--page",
184
+ pageId,
185
+ "--port",
186
+ String(port),
187
+ "--host",
188
+ host,
189
+ ], {
190
+ cwd: workspaceDir,
191
+ detached: true,
192
+ stdio: "ignore",
193
+ });
194
+ if (!subprocess.pid) {
195
+ throw new Error("Failed to start runtime process");
196
+ }
197
+ subprocess.unref();
198
+ if (options.tunnel) {
199
+ s.message("Starting Cloudflare Tunnel...");
200
+ const tunnel = await startTunnel(port);
201
+ tunnelUrl = tunnel.url;
202
+ }
203
+ await waitForRuntimeProcessOrReady(subprocess, waitForRuntimeReady(port, pageId));
204
+ const config = {
205
+ pid: subprocess.pid,
206
+ port,
207
+ startedAt: new Date().toISOString(),
208
+ tunnelUrl,
209
+ };
210
+ await saveRuntimeConfig(config);
211
+ s.stop(`Runtime started (${pageId})`);
212
+ console.log();
213
+ consola.success("Agentstage runtime is running");
214
+ console.log(` Page: ${c.cyan(pageId)}`);
215
+ console.log(` Web: ${c.cyan(`http://localhost:${port}`)}`);
216
+ if (tunnelUrl) {
217
+ printTunnelInfo(tunnelUrl);
218
+ }
219
+ console.log(` Bridge: ${c.cyan(`ws://localhost:${port}/_bridge`)}`);
220
+ console.log(` Workspace: ${c.gray(workspaceDir)}`);
221
+ console.log();
222
+ if (options.open) {
223
+ const openUrl = tunnelUrl || `http://localhost:${port}`;
224
+ try {
225
+ await execa("open", [openUrl]);
226
+ }
227
+ catch {
228
+ // ignore open errors
229
+ }
230
+ }
231
+ }
232
+ catch (error) {
233
+ s.stop("Failed to start runtime");
234
+ const message = error instanceof Error ? error.message : "Unknown error";
235
+ consola.error(message);
236
+ process.exit(1);
237
+ }
238
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const statusCommand: Command;
@@ -1,41 +1,39 @@
1
- import { Command } from 'commander';
2
- import consola from 'consola';
3
- import c from 'picocolors';
4
- import { readRuntimeConfig, isInitialized, getWorkspaceDir } from '../../utils/paths.js';
5
- import { checkCloudflared } from '../../utils/cloudflared.js';
6
- export const devStatusCommand = new Command('status')
7
- .description('Check the Agentstage Runtime status')
1
+ import { Command } from "commander";
2
+ import consola from "consola";
3
+ import c from "picocolors";
4
+ import { readRuntimeConfig, isInitialized, getWorkspaceDir, } from "../utils/paths.js";
5
+ import { checkCloudflared } from "../utils/cloudflared.js";
6
+ export const statusCommand = new Command("status")
7
+ .description("Check the Agentstage Runtime status")
8
8
  .action(async () => {
9
9
  if (!isInitialized()) {
10
- consola.error('Project not initialized. Please run `agentstage init` first.');
10
+ consola.error("Project not initialized. Please run `agentstage init` first.");
11
11
  process.exit(1);
12
12
  }
13
13
  const workspaceDir = await getWorkspaceDir();
14
14
  const config = await readRuntimeConfig();
15
15
  console.log();
16
- console.log(c.bold('Workspace:'), c.cyan(workspaceDir));
16
+ console.log(c.bold("Workspace:"), c.cyan(workspaceDir));
17
17
  console.log();
18
- // Check cloudflared
19
18
  const cloudflared = await checkCloudflared();
20
- console.log(c.bold('Cloudflare Tunnel:'));
19
+ console.log(c.bold("Cloudflare Tunnel:"));
21
20
  if (cloudflared.installed) {
22
- console.log(` Status: ${c.green('✓ installed')}`);
23
- console.log(` Version: ${c.gray(cloudflared.version || 'unknown')}`);
21
+ console.log(` Status: ${c.green("✓ installed")}`);
22
+ console.log(` Version: ${c.gray(cloudflared.version || "unknown")}`);
24
23
  }
25
24
  else {
26
- console.log(` Status: ${c.yellow('✗ not installed')}`);
25
+ console.log(` Status: ${c.yellow("✗ not installed")}`);
27
26
  console.log(` Install: ${c.gray(cloudflared.installCommand)}`);
28
27
  }
29
28
  console.log();
30
- // Check runtime
31
- console.log(c.bold('Runtime:'));
29
+ console.log(c.bold("Runtime:"));
32
30
  if (!config) {
33
- console.log(` Status: ${c.gray('stopped')}`);
31
+ console.log(` Status: ${c.gray("stopped")}`);
34
32
  }
35
33
  else {
36
34
  try {
37
35
  process.kill(config.pid, 0);
38
- console.log(` Status: ${c.green('running')}`);
36
+ console.log(` Status: ${c.green("running")}`);
39
37
  console.log(` PID: ${c.gray(config.pid)}`);
40
38
  console.log(` Port: ${c.cyan(config.port)}`);
41
39
  console.log(` Local: ${c.cyan(`http://localhost:${config.port}`)}`);
@@ -46,7 +44,7 @@ export const devStatusCommand = new Command('status')
46
44
  console.log(` Started: ${c.gray(new Date(config.startedAt).toLocaleString())}`);
47
45
  }
48
46
  catch {
49
- console.log(` Status: ${c.yellow('stale (process not found)')}`);
47
+ console.log(` Status: ${c.yellow("stale (process not found)")}`);
50
48
  console.log(` Last PID: ${c.gray(config.pid)}`);
51
49
  console.log(` Last Port: ${c.gray(config.port)}`);
52
50
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const stopCommand: Command;
@@ -0,0 +1,40 @@
1
+ import { Command } from "commander";
2
+ import consola from "consola";
3
+ import c from "picocolors";
4
+ import { readRuntimeConfig, removeRuntimeConfig, isInitialized, } from "../utils/paths.js";
5
+ export const stopCommand = new Command("stop")
6
+ .description("Stop the Agentstage Runtime")
7
+ .action(async () => {
8
+ if (!isInitialized()) {
9
+ consola.error("Project not initialized. Please run `agentstage init` first.");
10
+ process.exit(1);
11
+ }
12
+ const config = await readRuntimeConfig();
13
+ if (!config) {
14
+ consola.warn("Runtime is not running");
15
+ return;
16
+ }
17
+ try {
18
+ process.kill(config.pid, 0);
19
+ process.kill(config.pid, "SIGTERM");
20
+ await new Promise((resolve) => setTimeout(resolve, 1000));
21
+ try {
22
+ process.kill(config.pid, 0);
23
+ process.kill(config.pid, "SIGKILL");
24
+ }
25
+ catch {
26
+ // process already stopped
27
+ }
28
+ await removeRuntimeConfig();
29
+ consola.success("Runtime stopped");
30
+ console.log(` PID: ${c.gray(config.pid)}`);
31
+ console.log(` Port: ${c.gray(config.port)}`);
32
+ if (config.tunnelUrl) {
33
+ console.log(` Tunnel: ${c.gray(config.tunnelUrl)}`);
34
+ }
35
+ }
36
+ catch {
37
+ await removeRuntimeConfig();
38
+ consola.info("Runtime was not running (stale config cleaned up)");
39
+ }
40
+ });