nstantpage-agent 0.2.0
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/README.md +207 -0
- package/dist/checker.d.ts +35 -0
- package/dist/checker.js +151 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +67 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +62 -0
- package/dist/commands/start.d.ts +23 -0
- package/dist/commands/start.js +140 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.js +53 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +23 -0
- package/dist/devServer.d.ts +65 -0
- package/dist/devServer.js +308 -0
- package/dist/errorStore.d.ts +51 -0
- package/dist/errorStore.js +167 -0
- package/dist/fileManager.d.ts +44 -0
- package/dist/fileManager.js +129 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/localServer.d.ts +70 -0
- package/dist/localServer.js +555 -0
- package/dist/packageInstaller.d.ts +29 -0
- package/dist/packageInstaller.js +111 -0
- package/dist/tunnel.d.ts +71 -0
- package/dist/tunnel.js +247 -0
- package/package.json +62 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start command — launch the nstantpage agent
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* 1. Start local API server (handles /live/* requests for file sync, checks, etc.)
|
|
6
|
+
* 2. Start local dev server (Vite/Next.js — runs project on user's machine)
|
|
7
|
+
* 3. Connect tunnel to gateway (relays requests from cloud to local machine)
|
|
8
|
+
*
|
|
9
|
+
* The agent fully replaces Docker containers:
|
|
10
|
+
* - Files are on the user's disk (no docker cp needed)
|
|
11
|
+
* - Dev server runs natively (no container overhead)
|
|
12
|
+
* - Type checking runs locally (full IDE speed)
|
|
13
|
+
* - Package installation uses local npm/pnpm
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import { getConfig } from '../config.js';
|
|
19
|
+
import { TunnelClient } from '../tunnel.js';
|
|
20
|
+
import { LocalServer } from '../localServer.js';
|
|
21
|
+
export async function startCommand(directory, options) {
|
|
22
|
+
const conf = getConfig();
|
|
23
|
+
// Check authentication
|
|
24
|
+
const token = conf.get('token');
|
|
25
|
+
if (!token) {
|
|
26
|
+
console.log(chalk.red('✗ Not authenticated. Run "nstantpage login" first.'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
// Resolve project directory
|
|
30
|
+
const projectDir = path.resolve(directory);
|
|
31
|
+
if (!fs.existsSync(projectDir)) {
|
|
32
|
+
console.log(chalk.red(`✗ Directory not found: ${projectDir}`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Verify it's a project directory
|
|
36
|
+
if (!fs.existsSync(path.join(projectDir, 'package.json'))) {
|
|
37
|
+
console.log(chalk.red(`✗ No package.json found in ${projectDir}`));
|
|
38
|
+
console.log(chalk.gray(' Make sure you\'re in a project directory'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const devPort = parseInt(options.port, 10);
|
|
42
|
+
const apiPort = parseInt(options.apiPort, 10);
|
|
43
|
+
// Determine project ID
|
|
44
|
+
let projectId = options.projectId || conf.get('projectId');
|
|
45
|
+
// Try .nstantpage.json in project dir
|
|
46
|
+
const localConfig = path.join(projectDir, '.nstantpage.json');
|
|
47
|
+
if (!projectId && fs.existsSync(localConfig)) {
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(fs.readFileSync(localConfig, 'utf-8'));
|
|
50
|
+
projectId = data.projectId;
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
if (!projectId) {
|
|
55
|
+
console.log(chalk.yellow('⚠ No project ID specified.'));
|
|
56
|
+
console.log(chalk.gray(' Use --project-id <id> or create a .nstantpage.json file'));
|
|
57
|
+
console.log(chalk.gray(' with { "projectId": "<your-project-id>" }'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
// Save project ID
|
|
61
|
+
conf.set('projectId', projectId);
|
|
62
|
+
console.log(chalk.blue(`\n🚀 nstantpage agent v0.2.0\n`));
|
|
63
|
+
console.log(chalk.gray(` Directory: ${projectDir}`));
|
|
64
|
+
console.log(chalk.gray(` Project ID: ${projectId}`));
|
|
65
|
+
console.log(chalk.gray(` Dev server: port ${devPort}`));
|
|
66
|
+
console.log(chalk.gray(` API server: port ${apiPort}`));
|
|
67
|
+
console.log(chalk.gray(` Gateway: ${options.gateway}\n`));
|
|
68
|
+
// 1. Start local server (API + dev server)
|
|
69
|
+
const localServer = new LocalServer({
|
|
70
|
+
projectDir,
|
|
71
|
+
projectId,
|
|
72
|
+
apiPort,
|
|
73
|
+
devPort,
|
|
74
|
+
});
|
|
75
|
+
// 2. Create tunnel client
|
|
76
|
+
const tunnel = new TunnelClient({
|
|
77
|
+
gatewayUrl: options.gateway,
|
|
78
|
+
token,
|
|
79
|
+
projectId,
|
|
80
|
+
apiPort,
|
|
81
|
+
devPort,
|
|
82
|
+
});
|
|
83
|
+
// Handle graceful shutdown
|
|
84
|
+
const shutdown = async () => {
|
|
85
|
+
console.log(chalk.yellow('\n\n Shutting down...\n'));
|
|
86
|
+
tunnel.disconnect();
|
|
87
|
+
await localServer.stop();
|
|
88
|
+
conf.delete('agentPid');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
};
|
|
91
|
+
process.on('SIGTERM', shutdown);
|
|
92
|
+
process.on('SIGINT', shutdown);
|
|
93
|
+
// Store PID for "nstantpage stop"
|
|
94
|
+
conf.set('agentPid', process.pid);
|
|
95
|
+
try {
|
|
96
|
+
// Start local API server
|
|
97
|
+
console.log(chalk.gray(' Starting local API server...'));
|
|
98
|
+
await localServer.start();
|
|
99
|
+
console.log(chalk.green(` ✓ API server on port ${apiPort}`));
|
|
100
|
+
// Start dev server unless --no-dev flag
|
|
101
|
+
if (!options.noDev) {
|
|
102
|
+
console.log(chalk.gray(' Starting dev server...'));
|
|
103
|
+
try {
|
|
104
|
+
const devServer = localServer.getDevServer();
|
|
105
|
+
await devServer.start();
|
|
106
|
+
console.log(chalk.green(` ✓ Dev server on port ${devPort}`));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.log(chalk.yellow(` ⚠ Dev server failed to start: ${err.message}`));
|
|
110
|
+
console.log(chalk.gray(' You can start it later from the editor'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(chalk.gray(' Dev server skipped (--no-dev)'));
|
|
115
|
+
}
|
|
116
|
+
// Connect tunnel to gateway
|
|
117
|
+
console.log(chalk.gray(' Connecting to gateway...'));
|
|
118
|
+
await tunnel.connect();
|
|
119
|
+
console.log(chalk.green(` ✓ Tunnel connected\n`));
|
|
120
|
+
// Display status
|
|
121
|
+
console.log(chalk.blue.bold(` ┌──────────────────────────────────────────────┐`));
|
|
122
|
+
console.log(chalk.blue.bold(` │ Your project is live! │`));
|
|
123
|
+
console.log(chalk.blue.bold(` ├──────────────────────────────────────────────┤`));
|
|
124
|
+
console.log(chalk.white(` │ Cloud: https://${projectId}.webprev.live`));
|
|
125
|
+
console.log(chalk.white(` │ Local: http://localhost:${devPort}`));
|
|
126
|
+
console.log(chalk.blue.bold(` └──────────────────────────────────────────────┘\n`));
|
|
127
|
+
console.log(chalk.gray(` Mode: ${chalk.green('Agent')} (no containers needed)`));
|
|
128
|
+
console.log(chalk.gray(` All builds, checks, and previews run on this machine.`));
|
|
129
|
+
console.log(chalk.gray(` Press Ctrl+C to stop\n`));
|
|
130
|
+
// Keep alive
|
|
131
|
+
await new Promise(() => { });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(chalk.red(`\n✗ Failed to start: ${err.message}`));
|
|
135
|
+
await localServer.stop();
|
|
136
|
+
conf.delete('agentPid');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status command — show agent connection status
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getConfig } from '../config.js';
|
|
6
|
+
export async function statusCommand() {
|
|
7
|
+
const conf = getConfig();
|
|
8
|
+
const token = conf.get('token');
|
|
9
|
+
const projectId = conf.get('projectId');
|
|
10
|
+
const pid = conf.get('agentPid');
|
|
11
|
+
const lastConnected = conf.get('lastConnected');
|
|
12
|
+
const devPort = conf.get('devPort') || 3000;
|
|
13
|
+
const apiPort = conf.get('apiPort') || 18924;
|
|
14
|
+
console.log(chalk.blue('\n nstantpage agent v0.2.0\n'));
|
|
15
|
+
// Auth
|
|
16
|
+
if (token) {
|
|
17
|
+
console.log(chalk.green(' ✓ Authenticated'));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(chalk.red(' ✗ Not authenticated (run "nstantpage login")'));
|
|
21
|
+
}
|
|
22
|
+
// Project
|
|
23
|
+
if (projectId) {
|
|
24
|
+
console.log(chalk.gray(` Project: ${projectId}`));
|
|
25
|
+
console.log(chalk.gray(` Preview: https://${projectId}.webprev.live`));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(chalk.gray(' No project linked'));
|
|
29
|
+
}
|
|
30
|
+
// Running
|
|
31
|
+
if (pid) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0); // Check if process exists
|
|
34
|
+
console.log(chalk.green(` ✓ Agent running (PID ${pid})`));
|
|
35
|
+
console.log(chalk.gray(` Dev server: http://localhost:${devPort}`));
|
|
36
|
+
console.log(chalk.gray(` API server: http://localhost:${apiPort}`));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
console.log(chalk.gray(' Agent not running'));
|
|
40
|
+
conf.delete('agentPid');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(chalk.gray(' Agent not running'));
|
|
45
|
+
}
|
|
46
|
+
// Last connected
|
|
47
|
+
if (lastConnected) {
|
|
48
|
+
console.log(chalk.gray(` Last connected: ${lastConnected}`));
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.gray(`\n Mode: Agent (replaces cloud containers)`));
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=status.js.map
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent configuration store (persisted to disk)
|
|
3
|
+
*/
|
|
4
|
+
import Conf from 'conf';
|
|
5
|
+
let _conf = null;
|
|
6
|
+
export function getConfig() {
|
|
7
|
+
if (!_conf) {
|
|
8
|
+
_conf = new Conf({
|
|
9
|
+
projectName: 'nstantpage-agent',
|
|
10
|
+
schema: {
|
|
11
|
+
token: { type: 'string', default: '' },
|
|
12
|
+
gatewayUrl: { type: 'string', default: 'wss://webprev.live' },
|
|
13
|
+
projectId: { type: 'string', default: '' },
|
|
14
|
+
agentPid: { type: 'number' },
|
|
15
|
+
lastConnected: { type: 'string' },
|
|
16
|
+
devPort: { type: 'number', default: 3000 },
|
|
17
|
+
apiPort: { type: 'number', default: 18924 },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return _conf;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Server — manages the local development server process.
|
|
3
|
+
* Enhanced version of runner.ts with log capturing and lifecycle management.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the container-based dev server (docker exec npx vite).
|
|
6
|
+
*/
|
|
7
|
+
export interface DevServerOptions {
|
|
8
|
+
projectDir: string;
|
|
9
|
+
port: number;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export interface LogEntry {
|
|
13
|
+
timestamp: number;
|
|
14
|
+
type: 'stdout' | 'stderr';
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class DevServer {
|
|
18
|
+
private options;
|
|
19
|
+
private process;
|
|
20
|
+
private backendProcess;
|
|
21
|
+
private logs;
|
|
22
|
+
private maxLogs;
|
|
23
|
+
private _isRunning;
|
|
24
|
+
private startedAt;
|
|
25
|
+
constructor(options: DevServerOptions);
|
|
26
|
+
get isRunning(): boolean;
|
|
27
|
+
get port(): number;
|
|
28
|
+
get uptime(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Start the dev server based on project type detection.
|
|
31
|
+
*/
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Stop all dev server processes.
|
|
35
|
+
*/
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Restart the dev server.
|
|
39
|
+
*/
|
|
40
|
+
restart(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Get recent logs.
|
|
43
|
+
*/
|
|
44
|
+
getLogs(limit?: number): LogEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* Get resource usage stats.
|
|
47
|
+
*/
|
|
48
|
+
getStats(): {
|
|
49
|
+
cpuPercent: number;
|
|
50
|
+
memoryMb: number;
|
|
51
|
+
pid: number | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Execute a command in the project directory.
|
|
55
|
+
*/
|
|
56
|
+
exec(command: string, timeoutMs?: number): Promise<{
|
|
57
|
+
exitCode: number;
|
|
58
|
+
stdout: string;
|
|
59
|
+
stderr: string;
|
|
60
|
+
}>;
|
|
61
|
+
private captureOutput;
|
|
62
|
+
private addLog;
|
|
63
|
+
private killProcess;
|
|
64
|
+
private waitForReady;
|
|
65
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Server — manages the local development server process.
|
|
3
|
+
* Enhanced version of runner.ts with log capturing and lifecycle management.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the container-based dev server (docker exec npx vite).
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, execSync } from 'child_process';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import http from 'http';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
export class DevServer {
|
|
13
|
+
options;
|
|
14
|
+
process = null;
|
|
15
|
+
backendProcess = null;
|
|
16
|
+
logs = [];
|
|
17
|
+
maxLogs = 500;
|
|
18
|
+
_isRunning = false;
|
|
19
|
+
startedAt = 0;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
get isRunning() {
|
|
24
|
+
return this._isRunning;
|
|
25
|
+
}
|
|
26
|
+
get port() {
|
|
27
|
+
return this.options.port;
|
|
28
|
+
}
|
|
29
|
+
get uptime() {
|
|
30
|
+
return this._isRunning ? Date.now() - this.startedAt : 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Start the dev server based on project type detection.
|
|
34
|
+
*/
|
|
35
|
+
async start() {
|
|
36
|
+
if (this._isRunning) {
|
|
37
|
+
console.log(' [DevServer] Already running, restarting...');
|
|
38
|
+
await this.stop();
|
|
39
|
+
}
|
|
40
|
+
const { projectDir, port } = this.options;
|
|
41
|
+
const extraEnv = this.options.env || {};
|
|
42
|
+
// Detect project type
|
|
43
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
44
|
+
if (!fs.existsSync(pkgPath)) {
|
|
45
|
+
throw new Error(`No package.json found in ${projectDir}`);
|
|
46
|
+
}
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
48
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
49
|
+
const hasVite = 'vite' in deps;
|
|
50
|
+
const hasNext = 'next' in deps;
|
|
51
|
+
const hasBackend = fs.existsSync(path.join(projectDir, 'server', 'index.ts')) ||
|
|
52
|
+
fs.existsSync(path.join(projectDir, 'server', 'index.js'));
|
|
53
|
+
this.logs = [];
|
|
54
|
+
this.startedAt = Date.now();
|
|
55
|
+
// Start backend if present
|
|
56
|
+
if (hasBackend) {
|
|
57
|
+
const backendPort = port + 1000;
|
|
58
|
+
const backendEntry = fs.existsSync(path.join(projectDir, 'server', 'index.ts'))
|
|
59
|
+
? 'server/index.ts'
|
|
60
|
+
: 'server/index.js';
|
|
61
|
+
console.log(` [DevServer] Starting backend (port ${backendPort})...`);
|
|
62
|
+
this.backendProcess = spawn('npx', ['tsx', backendEntry], {
|
|
63
|
+
cwd: projectDir,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
...extraEnv,
|
|
67
|
+
PORT: String(backendPort),
|
|
68
|
+
NODE_ENV: 'development',
|
|
69
|
+
},
|
|
70
|
+
shell: true,
|
|
71
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
|
+
});
|
|
73
|
+
this.captureOutput(this.backendProcess, 'backend');
|
|
74
|
+
}
|
|
75
|
+
// Start frontend dev server
|
|
76
|
+
let cmd;
|
|
77
|
+
let args;
|
|
78
|
+
if (hasNext) {
|
|
79
|
+
cmd = 'npx';
|
|
80
|
+
args = ['next', 'dev', '-p', String(port)];
|
|
81
|
+
}
|
|
82
|
+
else if (hasVite) {
|
|
83
|
+
cmd = 'npx';
|
|
84
|
+
args = ['vite', '--port', String(port), '--host', '0.0.0.0', '--strictPort'];
|
|
85
|
+
}
|
|
86
|
+
else if (pkg.scripts?.dev) {
|
|
87
|
+
cmd = 'npm';
|
|
88
|
+
args = ['run', 'dev'];
|
|
89
|
+
}
|
|
90
|
+
else if (pkg.scripts?.start) {
|
|
91
|
+
cmd = 'npm';
|
|
92
|
+
args = ['start'];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
throw new Error('Could not detect dev server command');
|
|
96
|
+
}
|
|
97
|
+
const frontendEnv = {
|
|
98
|
+
...process.env,
|
|
99
|
+
...extraEnv,
|
|
100
|
+
PORT: String(port),
|
|
101
|
+
};
|
|
102
|
+
if (hasBackend) {
|
|
103
|
+
frontendEnv['VITE_BACKEND_PORT'] = String(port + 1000);
|
|
104
|
+
}
|
|
105
|
+
console.log(` [DevServer] Starting: ${cmd} ${args.join(' ')} (port ${port})`);
|
|
106
|
+
this.process = spawn(cmd, args, {
|
|
107
|
+
cwd: projectDir,
|
|
108
|
+
env: frontendEnv,
|
|
109
|
+
shell: true,
|
|
110
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
|
+
});
|
|
112
|
+
this.captureOutput(this.process, 'frontend');
|
|
113
|
+
this.process.on('close', (code) => {
|
|
114
|
+
this._isRunning = false;
|
|
115
|
+
this.addLog('stderr', `Dev server exited with code ${code}`);
|
|
116
|
+
console.log(` [DevServer] Process exited (code ${code})`);
|
|
117
|
+
});
|
|
118
|
+
this.process.on('error', (err) => {
|
|
119
|
+
this._isRunning = false;
|
|
120
|
+
this.addLog('stderr', `Dev server error: ${err.message}`);
|
|
121
|
+
console.error(` [DevServer] Error: ${err.message}`);
|
|
122
|
+
});
|
|
123
|
+
this._isRunning = true;
|
|
124
|
+
// Wait for server to be ready
|
|
125
|
+
try {
|
|
126
|
+
await this.waitForReady(port, 30_000);
|
|
127
|
+
console.log(` [DevServer] Ready on port ${port}`);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
console.warn(` [DevServer] Health check timed out, but server may still be starting`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Stop all dev server processes.
|
|
135
|
+
*/
|
|
136
|
+
async stop() {
|
|
137
|
+
const kills = [];
|
|
138
|
+
if (this.process) {
|
|
139
|
+
kills.push(this.killProcess(this.process));
|
|
140
|
+
this.process = null;
|
|
141
|
+
}
|
|
142
|
+
if (this.backendProcess) {
|
|
143
|
+
kills.push(this.killProcess(this.backendProcess));
|
|
144
|
+
this.backendProcess = null;
|
|
145
|
+
}
|
|
146
|
+
await Promise.all(kills);
|
|
147
|
+
this._isRunning = false;
|
|
148
|
+
console.log(' [DevServer] Stopped');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Restart the dev server.
|
|
152
|
+
*/
|
|
153
|
+
async restart() {
|
|
154
|
+
await this.stop();
|
|
155
|
+
await new Promise(r => setTimeout(r, 500));
|
|
156
|
+
await this.start();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get recent logs.
|
|
160
|
+
*/
|
|
161
|
+
getLogs(limit = 100) {
|
|
162
|
+
return this.logs.slice(-limit);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get resource usage stats.
|
|
166
|
+
*/
|
|
167
|
+
getStats() {
|
|
168
|
+
if (!this.process || !this.process.pid) {
|
|
169
|
+
return { cpuPercent: 0, memoryMb: 0, pid: null };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const pid = this.process.pid;
|
|
173
|
+
const platform = os.platform();
|
|
174
|
+
let output;
|
|
175
|
+
if (platform === 'win32') {
|
|
176
|
+
// Windows: use wmic or tasklist
|
|
177
|
+
output = execSync(`wmic process where ProcessId=${pid} get WorkingSetSize /format:value`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
178
|
+
const match = output.match(/WorkingSetSize=(\d+)/);
|
|
179
|
+
const bytes = match ? parseInt(match[1], 10) : 0;
|
|
180
|
+
return {
|
|
181
|
+
cpuPercent: 0, // CPU% hard to get on Windows without sampling
|
|
182
|
+
memoryMb: bytes / (1024 * 1024),
|
|
183
|
+
pid,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// macOS / Linux: use ps
|
|
188
|
+
output = execSync(`ps -p ${pid} -o %cpu=,rss= 2>/dev/null`, { encoding: 'utf-8', timeout: 2000 }).trim();
|
|
189
|
+
const [cpu, rss] = output.split(/\s+/).map(Number);
|
|
190
|
+
return {
|
|
191
|
+
cpuPercent: cpu || 0,
|
|
192
|
+
memoryMb: (rss || 0) / 1024, // rss is in KB
|
|
193
|
+
pid,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { cpuPercent: 0, memoryMb: 0, pid: this.process.pid };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Execute a command in the project directory.
|
|
203
|
+
*/
|
|
204
|
+
async exec(command, timeoutMs = 30_000) {
|
|
205
|
+
return new Promise((resolve) => {
|
|
206
|
+
const isWin = os.platform() === 'win32';
|
|
207
|
+
const shell = isWin ? 'cmd' : 'sh';
|
|
208
|
+
const shellArgs = isWin ? ['/c', command] : ['-c', command];
|
|
209
|
+
const proc = spawn(shell, shellArgs, {
|
|
210
|
+
cwd: this.options.projectDir,
|
|
211
|
+
env: process.env,
|
|
212
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
213
|
+
});
|
|
214
|
+
let stdout = '';
|
|
215
|
+
let stderr = '';
|
|
216
|
+
proc.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
217
|
+
proc.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
218
|
+
proc.on('close', (code) => {
|
|
219
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
220
|
+
});
|
|
221
|
+
proc.on('error', (err) => {
|
|
222
|
+
resolve({ exitCode: 1, stdout, stderr: err.message });
|
|
223
|
+
});
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
try {
|
|
226
|
+
proc.kill();
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
resolve({ exitCode: 1, stdout, stderr: stderr + '\nCommand timed out' });
|
|
230
|
+
}, timeoutMs);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
captureOutput(proc, source) {
|
|
234
|
+
proc.stdout?.on('data', (data) => {
|
|
235
|
+
const msg = data.toString().trim();
|
|
236
|
+
if (msg) {
|
|
237
|
+
this.addLog('stdout', msg);
|
|
238
|
+
process.stdout.write(` [${source}] ${msg}\n`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
proc.stderr?.on('data', (data) => {
|
|
242
|
+
const msg = data.toString().trim();
|
|
243
|
+
if (msg) {
|
|
244
|
+
this.addLog('stderr', msg);
|
|
245
|
+
// Only print actual errors, not noisy warnings
|
|
246
|
+
if (!msg.includes('[BABEL] Note:') && !msg.includes('at-rule-no-unknown')) {
|
|
247
|
+
process.stderr.write(` [${source}] ${msg}\n`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
addLog(type, message) {
|
|
253
|
+
this.logs.push({ timestamp: Date.now(), type, message });
|
|
254
|
+
if (this.logs.length > this.maxLogs) {
|
|
255
|
+
this.logs = this.logs.slice(-this.maxLogs);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
killProcess(proc) {
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
if (!proc.pid) {
|
|
261
|
+
resolve();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
// Try graceful shutdown first
|
|
266
|
+
proc.kill('SIGTERM');
|
|
267
|
+
const forceKill = setTimeout(() => {
|
|
268
|
+
try {
|
|
269
|
+
proc.kill();
|
|
270
|
+
}
|
|
271
|
+
catch { }
|
|
272
|
+
resolve();
|
|
273
|
+
}, 5000);
|
|
274
|
+
proc.on('close', () => {
|
|
275
|
+
clearTimeout(forceKill);
|
|
276
|
+
resolve();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
resolve();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
waitForReady(port, timeoutMs) {
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const start = Date.now();
|
|
287
|
+
const check = () => {
|
|
288
|
+
if (Date.now() - start > timeoutMs) {
|
|
289
|
+
reject(new Error('Timeout'));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const req = http.get(`http://localhost:${port}/`, (res) => {
|
|
293
|
+
res.resume();
|
|
294
|
+
resolve();
|
|
295
|
+
});
|
|
296
|
+
req.on('error', () => {
|
|
297
|
+
setTimeout(check, 500);
|
|
298
|
+
});
|
|
299
|
+
req.setTimeout(2000, () => {
|
|
300
|
+
req.destroy();
|
|
301
|
+
setTimeout(check, 500);
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
setTimeout(check, 1000);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=devServer.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Store — tracks build, type, lint, and runtime errors for a project.
|
|
3
|
+
* This replaces the container-based error tracking from the gateway.
|
|
4
|
+
*/
|
|
5
|
+
export interface StructuredError {
|
|
6
|
+
file?: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
col?: number;
|
|
9
|
+
message: string;
|
|
10
|
+
type: 'build' | 'type' | 'lint' | 'hard' | 'runtime';
|
|
11
|
+
code?: string;
|
|
12
|
+
signature: string;
|
|
13
|
+
raw?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class ErrorStore {
|
|
16
|
+
private buildErrors;
|
|
17
|
+
private typeErrors;
|
|
18
|
+
private lintErrors;
|
|
19
|
+
private runtimeErrors;
|
|
20
|
+
setBuildErrors(errors: StructuredError[]): void;
|
|
21
|
+
setTypeErrors(errors: StructuredError[]): void;
|
|
22
|
+
setLintErrors(errors: StructuredError[]): void;
|
|
23
|
+
addRuntimeError(error: StructuredError): void;
|
|
24
|
+
clearRuntimeErrors(): void;
|
|
25
|
+
clearAll(): void;
|
|
26
|
+
getAllErrors(): StructuredError[];
|
|
27
|
+
getErrorsByType(): {
|
|
28
|
+
buildErrors: string[];
|
|
29
|
+
typeErrors: string[];
|
|
30
|
+
lintErrors: string[];
|
|
31
|
+
runtimeErrors: string[];
|
|
32
|
+
};
|
|
33
|
+
getStructuredByType(): {
|
|
34
|
+
buildErrors: StructuredError[];
|
|
35
|
+
typeErrors: StructuredError[];
|
|
36
|
+
lintErrors: StructuredError[];
|
|
37
|
+
runtimeErrors: StructuredError[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert structured error to display string.
|
|
42
|
+
*/
|
|
43
|
+
export declare function structuredErrorToString(error: StructuredError): string;
|
|
44
|
+
/**
|
|
45
|
+
* Parse a raw error string into a structured error.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseErrorString(errorStr: string, defaultType?: StructuredError['type']): StructuredError;
|
|
48
|
+
/**
|
|
49
|
+
* Filter non-error messages (Babel notes, CSS at-rule warnings, etc.)
|
|
50
|
+
*/
|
|
51
|
+
export declare function filterNonErrors(errors: string[]): string[];
|