stone-lang 0.1.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 +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- package/packages/semver.js +635 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ps command - List running Stone processes
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* stone ps
|
|
6
|
+
* stone ps --mine
|
|
7
|
+
* stone ps --status running
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { DaemonClient } from '../DaemonClient.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* List running processes
|
|
14
|
+
* @param {Object} options - Command options
|
|
15
|
+
*/
|
|
16
|
+
export async function psCommand(options = {}) {
|
|
17
|
+
const { mine = false, status = null, json = false } = options;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const client = new DaemonClient({ autoStartDaemon: false });
|
|
21
|
+
|
|
22
|
+
// Check if daemon is running
|
|
23
|
+
const isRunning = await DaemonClient.isDaemonRunning();
|
|
24
|
+
if (!isRunning) {
|
|
25
|
+
if (json) {
|
|
26
|
+
console.log(JSON.stringify({ processes: [], error: 'Daemon not running' }));
|
|
27
|
+
} else {
|
|
28
|
+
console.log('No Stone daemon running.');
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await client.connect();
|
|
34
|
+
|
|
35
|
+
const processes = await client.list({ mine, status });
|
|
36
|
+
|
|
37
|
+
if (json) {
|
|
38
|
+
console.log(JSON.stringify({ processes }));
|
|
39
|
+
} else {
|
|
40
|
+
if (processes.length === 0) {
|
|
41
|
+
console.log('No processes.');
|
|
42
|
+
} else {
|
|
43
|
+
// Print table header
|
|
44
|
+
console.log('ID\t\tSTATUS\t\tSCRIPT\t\tDURATION');
|
|
45
|
+
console.log('-'.repeat(60));
|
|
46
|
+
|
|
47
|
+
for (const proc of processes) {
|
|
48
|
+
const duration = proc.duration ? `${proc.duration}ms` : '-';
|
|
49
|
+
console.log(`${proc.id}\t\t${proc.status}\t\t${proc.script}\t\t${duration}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
client.disconnect();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`Error: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default psCommand;
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run command - Execute a Stone script
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* stone run <file.stn>
|
|
6
|
+
* stone run <file.stn> --json
|
|
7
|
+
* stone run <file.stn> --headless
|
|
8
|
+
*
|
|
9
|
+
* Modes:
|
|
10
|
+
* - Default: Human-readable output, opens browser for graphs
|
|
11
|
+
* - --json: JSON Lines output for IDE/desktop integration
|
|
12
|
+
* - --headless: No browser, exports graphs to files
|
|
13
|
+
*
|
|
14
|
+
* Execution:
|
|
15
|
+
* - Tries to use daemon first (shared with desktop app)
|
|
16
|
+
* - Falls back to direct StoneEngine if daemon unavailable
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { StoneEngine } from '../../StoneEngine.js';
|
|
22
|
+
import { CLIOutputAdapter } from '../CLIOutputAdapter.js';
|
|
23
|
+
import { JSONOutputAdapter } from '../JSONOutputAdapter.js';
|
|
24
|
+
import { ViewerServer } from '../ViewerServer.js';
|
|
25
|
+
import { IPCClientConnection } from '../../daemon/IPCServer.js';
|
|
26
|
+
import { createNodePackageContext } from '../../packages/packageResolver.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Execute a Stone script
|
|
30
|
+
* @param {string} scriptPath - Path to script file
|
|
31
|
+
* @param {Object} options - Command options
|
|
32
|
+
*/
|
|
33
|
+
export async function runCommand(scriptPath, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
json = false,
|
|
36
|
+
headless = false,
|
|
37
|
+
verbose = false,
|
|
38
|
+
quiet = false,
|
|
39
|
+
ast = false,
|
|
40
|
+
bytecode = false,
|
|
41
|
+
noServe = false,
|
|
42
|
+
daemon = true, // --no-daemon sets this to false
|
|
43
|
+
} = options;
|
|
44
|
+
const noDaemon = !daemon;
|
|
45
|
+
|
|
46
|
+
// Resolve script path
|
|
47
|
+
const fullPath = path.isAbsolute(scriptPath)
|
|
48
|
+
? scriptPath
|
|
49
|
+
: path.resolve(process.cwd(), scriptPath);
|
|
50
|
+
|
|
51
|
+
// Check file exists
|
|
52
|
+
if (!fs.existsSync(fullPath)) {
|
|
53
|
+
console.error(`Error: Script not found: ${scriptPath}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read source code
|
|
58
|
+
const source = fs.readFileSync(fullPath, 'utf8');
|
|
59
|
+
|
|
60
|
+
// Handle special output modes (these don't need daemon)
|
|
61
|
+
if (ast) {
|
|
62
|
+
const engine = new StoneEngine({ debug: verbose });
|
|
63
|
+
try {
|
|
64
|
+
const parsed = engine.parse(source, { filename: scriptPath });
|
|
65
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
66
|
+
process.exit(0);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`Parse error: ${err.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Try daemon execution first (unless --no-daemon)
|
|
74
|
+
if (!noDaemon) {
|
|
75
|
+
try {
|
|
76
|
+
const result = await runViaDaemon(fullPath, source, options);
|
|
77
|
+
if (result !== null) {
|
|
78
|
+
// Daemon handled it
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// result === null means daemon unavailable, fall through to direct
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(`[Daemon unavailable: ${err.message}, using direct execution]`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fall back to direct execution
|
|
90
|
+
await runDirect(fullPath, source, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run script via daemon
|
|
95
|
+
* @returns {Promise<boolean|null>} true if success, false if failed, null if daemon unavailable
|
|
96
|
+
*/
|
|
97
|
+
async function runViaDaemon(fullPath, source, options) {
|
|
98
|
+
const { json, verbose, quiet } = options;
|
|
99
|
+
|
|
100
|
+
const client = new IPCClientConnection();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await client.connect();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// Daemon not running - try to start it
|
|
106
|
+
if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
|
|
107
|
+
const started = await startDaemon(verbose);
|
|
108
|
+
if (!started) {
|
|
109
|
+
return null; // Can't start daemon, fall back to direct
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Wait a bit and retry
|
|
113
|
+
await new Promise(r => setTimeout(r, 500));
|
|
114
|
+
try {
|
|
115
|
+
await client.connect();
|
|
116
|
+
} catch (retryErr) {
|
|
117
|
+
return null; // Still can't connect
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// When running via daemon, the desktop app handles graph visualization
|
|
125
|
+
// No ViewerServer needed - just print output and terminal creation messages
|
|
126
|
+
|
|
127
|
+
// Handle events from daemon - set up BEFORE run command to not miss early events
|
|
128
|
+
let exitCode = 0;
|
|
129
|
+
let finished = false;
|
|
130
|
+
let processId = null;
|
|
131
|
+
let terminalsCreated = [];
|
|
132
|
+
|
|
133
|
+
client.on('event', (event) => {
|
|
134
|
+
// Ignore events for other processes
|
|
135
|
+
if (processId && event.processId && event.processId !== processId) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Track terminal creation
|
|
140
|
+
if (event.event === 'terminal:create') {
|
|
141
|
+
const termType = event.type || event.terminalType;
|
|
142
|
+
terminalsCreated.push({ id: event.id, type: termType });
|
|
143
|
+
|
|
144
|
+
// For graph terminals, show creation message (console terminals just print their content)
|
|
145
|
+
if (!json && !quiet && (termType === 'graph2d' || termType === 'graph3d')) {
|
|
146
|
+
console.log(`\x1b[36m[Terminal created: ${termType}]\x1b[0m`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle console terminal output - print the actual lines
|
|
151
|
+
if (event.event === 'terminal:add') {
|
|
152
|
+
const terminal = terminalsCreated.find(t => t.id === event.id);
|
|
153
|
+
if (terminal && terminal.type === 'console') {
|
|
154
|
+
const lines = Array.isArray(event.data) ? event.data : [event.data];
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(JSON.stringify({ type: 'output', value: line }));
|
|
158
|
+
} else if (!quiet) {
|
|
159
|
+
console.log(line);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle print events (for direct print() calls)
|
|
166
|
+
if (event.event === 'print') {
|
|
167
|
+
if (json) {
|
|
168
|
+
console.log(JSON.stringify({ type: 'output', value: event.value }));
|
|
169
|
+
} else if (!quiet) {
|
|
170
|
+
console.log(event.value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle error events
|
|
175
|
+
if (event.event === 'error') {
|
|
176
|
+
if (json) {
|
|
177
|
+
console.log(JSON.stringify({ type: 'error', message: event.message, location: event.location }));
|
|
178
|
+
} else if (!quiet) {
|
|
179
|
+
console.error(`Error: ${event.message}`);
|
|
180
|
+
if (event.location) {
|
|
181
|
+
console.error(` at ${event.location.file}:${event.location.line}:${event.location.column}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for completion
|
|
187
|
+
if (event.event === 'process:end') {
|
|
188
|
+
finished = true;
|
|
189
|
+
exitCode = event.success ? 0 : 1;
|
|
190
|
+
|
|
191
|
+
if (verbose && !json) {
|
|
192
|
+
console.log(`\nCompleted in ${event.duration}ms`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!event.success && !quiet) {
|
|
196
|
+
if (json) {
|
|
197
|
+
console.log(JSON.stringify({ type: 'failed', error: event.error }));
|
|
198
|
+
} else {
|
|
199
|
+
console.error(`\nExecution failed: ${event.error}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Show message about viewing graph terminals in desktop app
|
|
204
|
+
if (!json && !quiet && terminalsCreated.length > 0) {
|
|
205
|
+
const graphTerminals = terminalsCreated.filter(t => t.type === 'graph2d' || t.type === 'graph3d');
|
|
206
|
+
if (graphTerminals.length > 0) {
|
|
207
|
+
console.log(`\n\x1b[33m📊 ${graphTerminals.length} graph terminal(s) created. View in Riva Viewport.\x1b[0m`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Send run command to daemon
|
|
214
|
+
const response = await client.request('run', {
|
|
215
|
+
script: fullPath,
|
|
216
|
+
source: source,
|
|
217
|
+
cwd: path.dirname(fullPath),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!response.processId) {
|
|
221
|
+
console.error('Daemon failed to start process');
|
|
222
|
+
client.disconnect();
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
processId = response.processId;
|
|
227
|
+
if (verbose) {
|
|
228
|
+
console.log(`[Running via daemon, processId: ${processId}]`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Wait for process to complete
|
|
232
|
+
await new Promise((resolve) => {
|
|
233
|
+
const checkInterval = setInterval(() => {
|
|
234
|
+
if (finished) {
|
|
235
|
+
clearInterval(checkInterval);
|
|
236
|
+
resolve();
|
|
237
|
+
}
|
|
238
|
+
}, 50);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
client.disconnect();
|
|
242
|
+
process.exit(exitCode);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Handle a daemon event
|
|
247
|
+
*/
|
|
248
|
+
function handleDaemonEvent(event, processId, ctx) {
|
|
249
|
+
const { json, quiet, verbose, viewerServer, sessionId } = ctx;
|
|
250
|
+
|
|
251
|
+
// Ignore events for other processes (only filter if we have a processId to compare)
|
|
252
|
+
if (processId && event.processId && event.processId !== processId) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
switch (event.event) {
|
|
257
|
+
case 'print':
|
|
258
|
+
if (json) {
|
|
259
|
+
console.log(JSON.stringify({ type: 'output', value: event.value }));
|
|
260
|
+
} else if (!quiet) {
|
|
261
|
+
console.log(event.value);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'terminal:create':
|
|
266
|
+
if (viewerServer && sessionId) {
|
|
267
|
+
const session = viewerServer.getSession(sessionId);
|
|
268
|
+
if (session) {
|
|
269
|
+
session.addTerminal(event.id, event.type, event.config);
|
|
270
|
+
session.broadcast({ type: 'terminal:create', id: event.id, terminalType: event.type, config: event.config });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Open browser on first graph
|
|
274
|
+
if (!ctx.browserOpened && (event.type === 'graph2d' || event.type === 'graph3d')) {
|
|
275
|
+
ctx.onBrowserOpened();
|
|
276
|
+
const viewUrl = viewerServer.getViewUrl(sessionId);
|
|
277
|
+
console.log(`\n View graphs at: \x1b[36m${viewUrl}\x1b[0m\n`);
|
|
278
|
+
|
|
279
|
+
import('child_process').then(({ exec }) => {
|
|
280
|
+
const cmd = process.platform === 'win32' ? 'start' :
|
|
281
|
+
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
282
|
+
exec(`${cmd} ${viewUrl}`);
|
|
283
|
+
}).catch(() => {});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case 'terminal:add':
|
|
289
|
+
if (viewerServer && sessionId) {
|
|
290
|
+
const session = viewerServer.getSession(sessionId);
|
|
291
|
+
if (session) {
|
|
292
|
+
session.addToTerminal(event.id, event.data);
|
|
293
|
+
session.broadcast({ type: 'terminal:add', id: event.id, data: event.data });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
|
|
298
|
+
case 'terminal:set':
|
|
299
|
+
if (viewerServer && sessionId) {
|
|
300
|
+
const session = viewerServer.getSession(sessionId);
|
|
301
|
+
if (session) {
|
|
302
|
+
session.setTerminal(event.id, event.data);
|
|
303
|
+
session.broadcast({ type: 'terminal:set', id: event.id, data: event.data });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
case 'terminal:clear':
|
|
309
|
+
if (viewerServer && sessionId) {
|
|
310
|
+
const session = viewerServer.getSession(sessionId);
|
|
311
|
+
if (session) {
|
|
312
|
+
session.clearTerminal(event.id);
|
|
313
|
+
session.broadcast({ type: 'terminal:clear', id: event.id });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
|
|
318
|
+
case 'error':
|
|
319
|
+
if (json) {
|
|
320
|
+
console.log(JSON.stringify({ type: 'error', message: event.message, location: event.location }));
|
|
321
|
+
} else if (!quiet) {
|
|
322
|
+
console.error(`Error: ${event.message}`);
|
|
323
|
+
if (event.location) {
|
|
324
|
+
console.error(` at ${event.location.file}:${event.location.line}:${event.location.column}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'process:end':
|
|
330
|
+
if (verbose && !json) {
|
|
331
|
+
console.log(`\nCompleted in ${event.duration}ms`);
|
|
332
|
+
}
|
|
333
|
+
if (!event.success && !quiet) {
|
|
334
|
+
if (json) {
|
|
335
|
+
console.log(JSON.stringify({ type: 'failed', error: event.error }));
|
|
336
|
+
} else {
|
|
337
|
+
console.error(`\nExecution failed: ${event.error}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Start the daemon process
|
|
346
|
+
*/
|
|
347
|
+
async function startDaemon(verbose) {
|
|
348
|
+
const { spawn } = await import('child_process');
|
|
349
|
+
|
|
350
|
+
// Find daemon path
|
|
351
|
+
const daemonPath = path.join(
|
|
352
|
+
path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')),
|
|
353
|
+
'..', '..', 'daemon', 'daemon.js'
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (!fs.existsSync(daemonPath)) {
|
|
357
|
+
if (verbose) {
|
|
358
|
+
console.log(`[Daemon not found at ${daemonPath}]`);
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (verbose) {
|
|
364
|
+
console.log(`[Starting daemon at ${daemonPath}]`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const daemon = spawn('node', [daemonPath], {
|
|
368
|
+
detached: true,
|
|
369
|
+
stdio: 'ignore',
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
daemon.unref();
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Direct execution (fallback when daemon unavailable)
|
|
378
|
+
*/
|
|
379
|
+
async function runDirect(fullPath, source, options) {
|
|
380
|
+
const { json, headless, verbose, quiet, noServe } = options;
|
|
381
|
+
const scriptPath = path.basename(fullPath);
|
|
382
|
+
|
|
383
|
+
// ViewerServer for graphs
|
|
384
|
+
let viewerServer = null;
|
|
385
|
+
let sessionId = null;
|
|
386
|
+
let browserOpened = false;
|
|
387
|
+
|
|
388
|
+
const usesGraphs = source.includes('graph2d') || source.includes('graph3d');
|
|
389
|
+
if (usesGraphs && !headless && !json) {
|
|
390
|
+
viewerServer = new ViewerServer();
|
|
391
|
+
try {
|
|
392
|
+
await viewerServer.start();
|
|
393
|
+
sessionId = viewerServer.createSession();
|
|
394
|
+
if (verbose) {
|
|
395
|
+
console.log(`[Viewer server started on port ${viewerServer.port}]`);
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.error(`Failed to start viewer server: ${err.message}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Create appropriate adapter
|
|
403
|
+
let adapter;
|
|
404
|
+
if (json) {
|
|
405
|
+
adapter = new JSONOutputAdapter();
|
|
406
|
+
} else {
|
|
407
|
+
adapter = new CLIOutputAdapter({
|
|
408
|
+
verbose,
|
|
409
|
+
quiet,
|
|
410
|
+
onGraphCreated: (id, type, config) => {
|
|
411
|
+
if (viewerServer && sessionId) {
|
|
412
|
+
const session = viewerServer.getSession(sessionId);
|
|
413
|
+
if (session) {
|
|
414
|
+
session.addTerminal(id, type, config);
|
|
415
|
+
session.broadcast({ type: 'terminal:create', id, terminalType: type, config });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!browserOpened) {
|
|
419
|
+
browserOpened = true;
|
|
420
|
+
const viewUrl = viewerServer.getViewUrl(sessionId);
|
|
421
|
+
console.log(`\n View graphs at: \x1b[36m${viewUrl}\x1b[0m\n`);
|
|
422
|
+
|
|
423
|
+
import('child_process').then(({ exec }) => {
|
|
424
|
+
const cmd = process.platform === 'win32' ? 'start' :
|
|
425
|
+
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
426
|
+
exec(`${cmd} ${viewUrl}`);
|
|
427
|
+
}).catch(() => {});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Wrap adapter to forward graph updates to viewer
|
|
435
|
+
const wrappedAdapter = {
|
|
436
|
+
print: (text) => adapter.print(text),
|
|
437
|
+
createTerminal: (id, type, config) => {
|
|
438
|
+
adapter.createTerminal(id, type, config);
|
|
439
|
+
},
|
|
440
|
+
addToTerminal: (id, data) => {
|
|
441
|
+
adapter.addToTerminal(id, data);
|
|
442
|
+
if (viewerServer && sessionId) {
|
|
443
|
+
const session = viewerServer.getSession(sessionId);
|
|
444
|
+
if (session) {
|
|
445
|
+
session.addToTerminal(id, data);
|
|
446
|
+
session.broadcast({ type: 'terminal:add', id, data });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
setTerminal: (id, data) => {
|
|
451
|
+
adapter.setTerminal(id, data);
|
|
452
|
+
if (viewerServer && sessionId) {
|
|
453
|
+
const session = viewerServer.getSession(sessionId);
|
|
454
|
+
if (session) {
|
|
455
|
+
session.setTerminal(id, data);
|
|
456
|
+
session.broadcast({ type: 'terminal:set', id, data });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
clearTerminal: (id) => {
|
|
461
|
+
adapter.clearTerminal(id);
|
|
462
|
+
if (viewerServer && sessionId) {
|
|
463
|
+
const session = viewerServer.getSession(sessionId);
|
|
464
|
+
if (session) {
|
|
465
|
+
session.clearTerminal(id);
|
|
466
|
+
session.broadcast({ type: 'terminal:clear', id });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
error: (message, location) => adapter.error(message, location),
|
|
471
|
+
finish: (success, result) => adapter.finish(success, result),
|
|
472
|
+
progress: (info) => adapter.progress?.(info),
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Create engine
|
|
476
|
+
const engine = new StoneEngine({
|
|
477
|
+
outputAdapter: wrappedAdapter,
|
|
478
|
+
debug: verbose,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Set up package context for stone_modules resolution
|
|
482
|
+
try {
|
|
483
|
+
const packageContext = await createNodePackageContext();
|
|
484
|
+
engine.setPackageContext(packageContext);
|
|
485
|
+
} catch (e) {
|
|
486
|
+
// Package context not available, continue without it
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Execute
|
|
490
|
+
try {
|
|
491
|
+
const startTime = Date.now();
|
|
492
|
+
|
|
493
|
+
const result = await engine.execute(source, {
|
|
494
|
+
filename: scriptPath,
|
|
495
|
+
currentPath: path.dirname(fullPath),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const duration = Date.now() - startTime;
|
|
499
|
+
|
|
500
|
+
if (result.success) {
|
|
501
|
+
if (verbose && !json) {
|
|
502
|
+
console.log(`\nCompleted in ${duration}ms`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (viewerServer && !noServe) {
|
|
506
|
+
console.log('Press Ctrl+C to exit...');
|
|
507
|
+
await new Promise(() => {});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
process.exit(0);
|
|
511
|
+
} else {
|
|
512
|
+
if (!json && !quiet) {
|
|
513
|
+
console.error(`\nExecution failed: ${result.error}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (viewerServer) {
|
|
517
|
+
await viewerServer.stop();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (!json) {
|
|
524
|
+
console.error(`\nFatal error: ${err.message}`);
|
|
525
|
+
} else {
|
|
526
|
+
console.log(JSON.stringify({ type: 'fatal', error: err.message }));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (viewerServer) {
|
|
530
|
+
await viewerServer.stop();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export default runCommand;
|