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.
- package/dist/commands/guide.js +5 -5
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +164 -0
- package/dist/commands/page/add.js +5 -40
- package/dist/commands/run/exec.js +1 -1
- package/dist/commands/run/inspect.js +1 -1
- package/dist/commands/run/watch.js +1 -1
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.js +238 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/{dev/status.js → status.js} +17 -19
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.js +40 -0
- package/dist/index.js +8 -2
- package/dist/utils/agent-helper.js +5 -5
- package/dist/utils/paths.js +5 -5
- package/dist/utils/tunnel.d.ts +1 -1
- package/dist/utils/tunnel.js +1 -1
- package/package.json +8 -5
- package/dist/commands/dev/index.d.ts +0 -2
- package/dist/commands/dev/index.js +0 -11
- package/dist/commands/dev/init.d.ts +0 -2
- package/dist/commands/dev/init.js +0 -215
- package/dist/commands/dev/start.d.ts +0 -2
- package/dist/commands/dev/start.js +0 -145
- package/dist/commands/dev/status.d.ts +0 -2
- package/dist/commands/dev/stop.d.ts +0 -2
- package/dist/commands/dev/stop.js +0 -45
- package/template/components.json +0 -17
- package/template/index.html +0 -13
- package/template/package.json +0 -41
- package/template/postcss.config.js +0 -6
- package/template/src/components/PageRenderer.tsx +0 -108
- package/template/src/components/bridge-state-provider.tsx +0 -87
- package/template/src/components/ui/button.tsx +0 -55
- package/template/src/components/ui/card.tsx +0 -78
- package/template/src/components/ui/input.tsx +0 -24
- package/template/src/index.css +0 -59
- package/template/src/lib/bridge.ts +0 -53
- package/template/src/lib/utils.ts +0 -6
- package/template/src/main.tsx +0 -23
- package/template/src/pages/counter/store.json +0 -8
- package/template/src/pages/counter/ui.json +0 -108
- package/template/src/pages/test-page/store.json +0 -8
- package/template/src/routeTree.gen.ts +0 -77
- package/template/src/routes/__root.tsx +0 -11
- package/template/src/routes/counter.tsx +0 -19
- package/template/src/routes/index.tsx +0 -46
- package/template/src/vite-env.d.ts +0 -1
- package/template/tailwind.config.js +0 -53
- package/template/tsconfig.json +0 -25
- package/template/tsconfig.node.json +0 -11
- package/template/vite.config.ts +0 -22
package/dist/commands/guide.js
CHANGED
|
@@ -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
|
|
21
|
-
command: 'agentstage
|
|
22
|
-
explanation: 'Starts the
|
|
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: '
|
|
164
|
-
fix: 'Run: agentstage
|
|
163
|
+
cause: 'Page runtime is not started',
|
|
164
|
+
fix: 'Run: agentstage serve <pageId>'
|
|
165
165
|
}
|
|
166
166
|
]
|
|
167
167
|
},
|
|
@@ -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
|
|
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
|
|
126
|
-
`Open http://localhost:${port}
|
|
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(`
|
|
134
|
-
console.log(`
|
|
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
|
|
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
|
|
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
|
|
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,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
|
+
});
|
|
@@ -1,41 +1,39 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import consola from
|
|
3
|
-
import c from
|
|
4
|
-
import { readRuntimeConfig, isInitialized, getWorkspaceDir } from
|
|
5
|
-
import { checkCloudflared } from
|
|
6
|
-
export const
|
|
7
|
-
.description(
|
|
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(
|
|
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(
|
|
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(
|
|
19
|
+
console.log(c.bold("Cloudflare Tunnel:"));
|
|
21
20
|
if (cloudflared.installed) {
|
|
22
|
-
console.log(` Status: ${c.green(
|
|
23
|
-
console.log(` Version: ${c.gray(cloudflared.version ||
|
|
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(
|
|
25
|
+
console.log(` Status: ${c.yellow("✗ not installed")}`);
|
|
27
26
|
console.log(` Install: ${c.gray(cloudflared.installCommand)}`);
|
|
28
27
|
}
|
|
29
28
|
console.log();
|
|
30
|
-
|
|
31
|
-
console.log(c.bold('Runtime:'));
|
|
29
|
+
console.log(c.bold("Runtime:"));
|
|
32
30
|
if (!config) {
|
|
33
|
-
console.log(` Status: ${c.gray(
|
|
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(
|
|
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(
|
|
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,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
|
+
});
|