@positronic/cli 0.0.3 → 0.0.5
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/src/commands/helpers.js +57 -27
- package/dist/types/commands/helpers.d.ts.map +1 -1
- package/package.json +5 -1
- package/dist/src/commands/brain.test.js +0 -2936
- package/dist/src/commands/helpers.test.js +0 -832
- package/dist/src/commands/project.test.js +0 -1201
- package/dist/src/commands/resources.test.js +0 -2511
- package/dist/src/commands/schedule.test.js +0 -1235
- package/dist/src/commands/secret.test.d.js +0 -1
- package/dist/src/commands/secret.test.js +0 -761
- package/dist/src/commands/server.test.js +0 -1237
- package/dist/src/commands/test-utils.js +0 -737
- package/dist/src/components/secret-sync.js +0 -303
- package/dist/src/test/mock-api-client.js +0 -371
- package/dist/src/test/test-dev-server.js +0 -1376
- package/dist/types/commands/test-utils.d.ts +0 -45
- package/dist/types/commands/test-utils.d.ts.map +0 -1
- package/dist/types/components/secret-sync.d.ts +0 -9
- package/dist/types/components/secret-sync.d.ts.map +0 -1
- package/dist/types/test/mock-api-client.d.ts +0 -25
- package/dist/types/test/mock-api-client.d.ts.map +0 -1
- package/dist/types/test/test-dev-server.d.ts +0 -129
- package/dist/types/test/test-dev-server.d.ts.map +0 -1
- package/src/cli.ts +0 -997
- package/src/commands/backend.ts +0 -63
- package/src/commands/brain.test.ts +0 -1004
- package/src/commands/brain.ts +0 -215
- package/src/commands/helpers.test.ts +0 -487
- package/src/commands/helpers.ts +0 -870
- package/src/commands/project-config-manager.ts +0 -152
- package/src/commands/project.test.ts +0 -502
- package/src/commands/project.ts +0 -109
- package/src/commands/resources.test.ts +0 -1052
- package/src/commands/resources.ts +0 -97
- package/src/commands/schedule.test.ts +0 -481
- package/src/commands/schedule.ts +0 -65
- package/src/commands/secret.test.ts +0 -210
- package/src/commands/secret.ts +0 -50
- package/src/commands/server.test.ts +0 -493
- package/src/commands/server.ts +0 -353
- package/src/commands/test-utils.ts +0 -324
- package/src/components/brain-history.tsx +0 -198
- package/src/components/brain-list.tsx +0 -105
- package/src/components/brain-rerun.tsx +0 -111
- package/src/components/brain-show.tsx +0 -92
- package/src/components/error.tsx +0 -24
- package/src/components/project-add.tsx +0 -59
- package/src/components/project-create.tsx +0 -83
- package/src/components/project-list.tsx +0 -83
- package/src/components/project-remove.tsx +0 -55
- package/src/components/project-select.tsx +0 -200
- package/src/components/project-show.tsx +0 -58
- package/src/components/resource-clear.tsx +0 -127
- package/src/components/resource-delete.tsx +0 -160
- package/src/components/resource-list.tsx +0 -177
- package/src/components/resource-sync.tsx +0 -170
- package/src/components/resource-types.tsx +0 -55
- package/src/components/resource-upload.tsx +0 -182
- package/src/components/schedule-create.tsx +0 -90
- package/src/components/schedule-delete.tsx +0 -116
- package/src/components/schedule-list.tsx +0 -186
- package/src/components/schedule-runs.tsx +0 -151
- package/src/components/secret-bulk.tsx +0 -79
- package/src/components/secret-create.tsx +0 -49
- package/src/components/secret-delete.tsx +0 -41
- package/src/components/secret-list.tsx +0 -41
- package/src/components/watch.tsx +0 -155
- package/src/hooks/useApi.ts +0 -183
- package/src/positronic.ts +0 -40
- package/src/test/data/resources/config.json +0 -1
- package/src/test/data/resources/data/config.json +0 -1
- package/src/test/data/resources/data/logo.png +0 -2
- package/src/test/data/resources/docs/api.md +0 -3
- package/src/test/data/resources/docs/readme.md +0 -3
- package/src/test/data/resources/example.md +0 -3
- package/src/test/data/resources/file with spaces.txt +0 -1
- package/src/test/data/resources/readme.md +0 -3
- package/src/test/data/resources/test.txt +0 -1
- package/src/test/mock-api-client.ts +0 -145
- package/src/test/test-dev-server.ts +0 -1003
- package/tsconfig.json +0 -11
package/src/commands/server.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import chokidar, { type FSWatcher } from 'chokidar';
|
|
5
|
-
import type { ArgumentsCamelCase } from 'yargs';
|
|
6
|
-
import { syncResources, generateTypes } from './helpers.js';
|
|
7
|
-
import type { PositronicDevServer, ServerHandle } from '@positronic/spec';
|
|
8
|
-
|
|
9
|
-
export class ServerCommand {
|
|
10
|
-
constructor(private server: PositronicDevServer) {}
|
|
11
|
-
|
|
12
|
-
async handle(argv: ArgumentsCamelCase<any>) {
|
|
13
|
-
// Handle kill option
|
|
14
|
-
if (argv.k) {
|
|
15
|
-
return this.handleKill(argv);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Validate arguments
|
|
19
|
-
if (argv.port && argv.d && !argv.logFile) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
'When using --port with -d, you must also specify --log-file'
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Check for existing PID file (skip if we're a detached child process)
|
|
26
|
-
const pidFile = this.getPidFilePath(argv.port);
|
|
27
|
-
if (!process.env.POSITRONIC_DETACHED_CHILD && fs.existsSync(pidFile)) {
|
|
28
|
-
const existingPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
|
|
29
|
-
if (this.isProcessRunning(existingPid)) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Server already running (PID: ${existingPid}). Stop it with: px server -k`
|
|
32
|
-
);
|
|
33
|
-
} else {
|
|
34
|
-
console.log('WARNING: Removing stale PID file');
|
|
35
|
-
fs.unlinkSync(pidFile);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// If -d flag is set, spawn a detached process
|
|
40
|
-
if (argv.d) {
|
|
41
|
-
return this.handleDetached(argv);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Write PID file for foreground process too
|
|
45
|
-
fs.writeFileSync(pidFile, String(process.pid));
|
|
46
|
-
|
|
47
|
-
const brainsDir = path.join(this.server.projectRootDir, 'brains');
|
|
48
|
-
const resourcesDir = path.join(this.server.projectRootDir, 'resources');
|
|
49
|
-
|
|
50
|
-
let serverHandle: ServerHandle | null = null;
|
|
51
|
-
let watcher: FSWatcher | null = null;
|
|
52
|
-
let logStream: fs.WriteStream | null = null;
|
|
53
|
-
|
|
54
|
-
// Always create a log file (use default if not specified)
|
|
55
|
-
const logFilePath = argv.logFile
|
|
56
|
-
? path.resolve(argv.logFile)
|
|
57
|
-
: path.join(this.server.projectRootDir, '.positronic-server.log');
|
|
58
|
-
|
|
59
|
-
// Ensure directory exists
|
|
60
|
-
const logDir = path.dirname(logFilePath);
|
|
61
|
-
if (!fs.existsSync(logDir)) {
|
|
62
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Create log stream (append mode)
|
|
66
|
-
logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
|
|
67
|
-
|
|
68
|
-
// Helper function to log to both console and file
|
|
69
|
-
const logBoth = (level: string, message: string) => {
|
|
70
|
-
const timestamp = new Date().toISOString();
|
|
71
|
-
const logLine = `[${timestamp}] [${level}] ${message}\n`;
|
|
72
|
-
if (logStream && !logStream.destroyed) {
|
|
73
|
-
logStream.write(logLine);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Console output is handled by the dev server's direct forwarding
|
|
77
|
-
// to avoid duplication
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Always register log callbacks to capture server logs
|
|
81
|
-
// The server is responsible for generating logs through these callbacks
|
|
82
|
-
this.server.onLog((message) => logBoth('INFO', message));
|
|
83
|
-
this.server.onError((message) => logBoth('ERROR', message));
|
|
84
|
-
this.server.onWarning((message) => logBoth('WARN', message));
|
|
85
|
-
|
|
86
|
-
const cleanup = async () => {
|
|
87
|
-
if (watcher) {
|
|
88
|
-
await watcher.close();
|
|
89
|
-
watcher = null;
|
|
90
|
-
}
|
|
91
|
-
if (serverHandle && !serverHandle.killed) {
|
|
92
|
-
serverHandle.kill();
|
|
93
|
-
serverHandle = null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Close log stream
|
|
97
|
-
if (logStream) {
|
|
98
|
-
logStream.end();
|
|
99
|
-
logStream = null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Remove PID file
|
|
103
|
-
if (fs.existsSync(pidFile)) {
|
|
104
|
-
fs.unlinkSync(pidFile);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
process.exit(0);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
process.on('SIGINT', cleanup); // Catches Ctrl+C
|
|
111
|
-
process.on('SIGTERM', cleanup); // Catches kill commands
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
// Use the dev server's setup method
|
|
115
|
-
await this.server.setup(argv.force);
|
|
116
|
-
|
|
117
|
-
// Use the dev server's start method
|
|
118
|
-
serverHandle = await this.server.start(argv.port);
|
|
119
|
-
|
|
120
|
-
serverHandle.onClose((code?: number | null) => {
|
|
121
|
-
if (watcher) {
|
|
122
|
-
watcher.close();
|
|
123
|
-
watcher = null;
|
|
124
|
-
}
|
|
125
|
-
process.exit(code ?? 1); // Exit with server's code or 1 if null
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
serverHandle.onError((err: Error) => {
|
|
129
|
-
console.error('Failed to start dev server:', err);
|
|
130
|
-
if (watcher) {
|
|
131
|
-
watcher.close();
|
|
132
|
-
watcher = null;
|
|
133
|
-
}
|
|
134
|
-
process.exit(1);
|
|
135
|
-
});
|
|
136
|
-
// Wait for the server to be ready before syncing resources
|
|
137
|
-
const isReady = await serverHandle.waitUntilReady(15000);
|
|
138
|
-
|
|
139
|
-
if (!isReady) {
|
|
140
|
-
console.error(
|
|
141
|
-
'⚠️ Server startup timeout: The server is taking longer than expected to initialize.'
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
// Clean up and exit
|
|
145
|
-
if (serverHandle && !serverHandle.killed) {
|
|
146
|
-
serverHandle.kill();
|
|
147
|
-
}
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Initial resource sync and type generation
|
|
152
|
-
try {
|
|
153
|
-
const syncResult = await syncResources(this.server.projectRootDir);
|
|
154
|
-
if (syncResult.errorCount > 0) {
|
|
155
|
-
console.log(
|
|
156
|
-
`⚠️ Resource sync completed with ${syncResult.errorCount} errors:`
|
|
157
|
-
);
|
|
158
|
-
syncResult.errors.forEach((error) => {
|
|
159
|
-
console.log(` • ${error.file}: ${error.message}`);
|
|
160
|
-
});
|
|
161
|
-
} else {
|
|
162
|
-
console.log(
|
|
163
|
-
`✅ Synced ${syncResult.uploadCount} resources (${syncResult.skipCount} up to date, ${syncResult.deleteCount} deleted)`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
await generateTypes(this.server.projectRootDir);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error(
|
|
169
|
-
'❌ Error during resource synchronization:',
|
|
170
|
-
error instanceof Error ? error.message : String(error)
|
|
171
|
-
);
|
|
172
|
-
console.error(
|
|
173
|
-
'\nThe server is running, but resources may not be available to your brains.'
|
|
174
|
-
);
|
|
175
|
-
console.error(
|
|
176
|
-
'\nYou can manually sync resources by running: px resources sync'
|
|
177
|
-
);
|
|
178
|
-
// Don't exit here - let the server continue running
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Watcher setup - target the user's brains and resources directories
|
|
182
|
-
const watchPaths = [
|
|
183
|
-
path.join(brainsDir, '*.ts'),
|
|
184
|
-
path.join(resourcesDir, '**/*'),
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
watcher = chokidar.watch(watchPaths, {
|
|
188
|
-
ignored: [/(^|[\/\\])\../, '**/node_modules/**'],
|
|
189
|
-
persistent: true,
|
|
190
|
-
ignoreInitial: true,
|
|
191
|
-
awaitWriteFinish: {
|
|
192
|
-
stabilityThreshold: 200,
|
|
193
|
-
pollInterval: 100,
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const handleResourceChange = async () => {
|
|
198
|
-
await syncResources(this.server.projectRootDir);
|
|
199
|
-
await generateTypes(this.server.projectRootDir);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
watcher
|
|
203
|
-
.on('add', async (filePath) => {
|
|
204
|
-
if (filePath.startsWith(resourcesDir)) {
|
|
205
|
-
await handleResourceChange();
|
|
206
|
-
} else if (filePath.startsWith(brainsDir)) {
|
|
207
|
-
// Call the dev server's watch method if it exists
|
|
208
|
-
if (this.server.watch) {
|
|
209
|
-
await this.server.watch(filePath, 'add');
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
.on('change', async (filePath) => {
|
|
214
|
-
if (filePath.startsWith(resourcesDir)) {
|
|
215
|
-
await handleResourceChange();
|
|
216
|
-
} else if (filePath.startsWith(brainsDir)) {
|
|
217
|
-
// Call the dev server's watch method if it exists
|
|
218
|
-
if (this.server.watch) {
|
|
219
|
-
await this.server.watch(filePath, 'change');
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
.on('unlink', async (filePath) => {
|
|
224
|
-
if (filePath.startsWith(resourcesDir)) {
|
|
225
|
-
await handleResourceChange();
|
|
226
|
-
} else if (filePath.startsWith(brainsDir)) {
|
|
227
|
-
// Call the dev server's watch method if it exists
|
|
228
|
-
if (this.server.watch) {
|
|
229
|
-
await this.server.watch(filePath, 'unlink');
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
.on('error', (error) => console.error(`Watcher error: ${error}`));
|
|
234
|
-
} catch (error) {
|
|
235
|
-
console.error('An error occurred during server startup:', error);
|
|
236
|
-
await cleanup(); // Attempt cleanup on error
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private async handleDetached(argv: ArgumentsCamelCase<any>) {
|
|
241
|
-
// Get the path to the current CLI executable
|
|
242
|
-
const cliPath = process.argv[1];
|
|
243
|
-
|
|
244
|
-
// Build the command arguments
|
|
245
|
-
const args = ['server'];
|
|
246
|
-
|
|
247
|
-
// Add optional arguments if they were provided
|
|
248
|
-
if (argv.force) args.push('--force');
|
|
249
|
-
if (argv.port) args.push('--port', String(argv.port));
|
|
250
|
-
if (argv.logFile) args.push('--log-file', argv.logFile);
|
|
251
|
-
|
|
252
|
-
// Determine output file for logs
|
|
253
|
-
const logFile =
|
|
254
|
-
argv.logFile ||
|
|
255
|
-
path.join(this.server.projectRootDir, '.positronic-server.log');
|
|
256
|
-
|
|
257
|
-
// Open log file in append mode
|
|
258
|
-
const out = fs.openSync(logFile, 'a');
|
|
259
|
-
const err = fs.openSync(logFile, 'a');
|
|
260
|
-
|
|
261
|
-
// Spawn the detached process with a special environment variable to skip PID check
|
|
262
|
-
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
263
|
-
detached: true,
|
|
264
|
-
stdio: ['ignore', out, err],
|
|
265
|
-
cwd: this.server.projectRootDir,
|
|
266
|
-
env: { ...process.env, POSITRONIC_DETACHED_CHILD: 'true' },
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Write the PID to a file for later reference
|
|
270
|
-
const pidFile = this.getPidFilePath(argv.port);
|
|
271
|
-
fs.writeFileSync(pidFile, String(child.pid));
|
|
272
|
-
|
|
273
|
-
// Detach from the child process
|
|
274
|
-
child.unref();
|
|
275
|
-
|
|
276
|
-
console.log(`✅ Server started in background (PID: ${child.pid})`);
|
|
277
|
-
console.log(` Logs: ${logFile}`);
|
|
278
|
-
console.log(` To stop: px server -k`);
|
|
279
|
-
|
|
280
|
-
// Exit the parent process
|
|
281
|
-
process.exit(0);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private getPidFilePath(port?: number): string {
|
|
285
|
-
if (port) {
|
|
286
|
-
return path.join(
|
|
287
|
-
this.server.projectRootDir,
|
|
288
|
-
`.positronic-server-${port}.pid`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
return path.join(this.server.projectRootDir, '.positronic-server.pid');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private isProcessRunning(pid: number): boolean {
|
|
295
|
-
try {
|
|
296
|
-
// This sends signal 0 which doesn't kill the process, just checks if it exists
|
|
297
|
-
process.kill(pid, 0);
|
|
298
|
-
return true;
|
|
299
|
-
} catch (e) {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private async handleKill(argv: ArgumentsCamelCase<any>) {
|
|
305
|
-
const pidFile = path.join(this.server.projectRootDir, '.positronic-server.pid');
|
|
306
|
-
|
|
307
|
-
if (!fs.existsSync(pidFile)) {
|
|
308
|
-
console.error(`❌ No default server is running`);
|
|
309
|
-
console.error(` PID file not found: ${pidFile}`);
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
|
|
315
|
-
|
|
316
|
-
if (!this.isProcessRunning(pid)) {
|
|
317
|
-
console.log('⚠️ Server process not found, removing stale PID file');
|
|
318
|
-
fs.unlinkSync(pidFile);
|
|
319
|
-
process.exit(0);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Kill the process
|
|
323
|
-
process.kill(pid, 'SIGTERM');
|
|
324
|
-
|
|
325
|
-
// Wait a moment to see if the process stops
|
|
326
|
-
let killed = false;
|
|
327
|
-
for (let i = 0; i < 10; i++) {
|
|
328
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
329
|
-
if (!this.isProcessRunning(pid)) {
|
|
330
|
-
killed = true;
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (!killed) {
|
|
336
|
-
// Force kill if SIGTERM didn't work
|
|
337
|
-
console.log('⚠️ Server did not stop gracefully, forcing shutdown');
|
|
338
|
-
process.kill(pid, 'SIGKILL');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Clean up PID file
|
|
342
|
-
if (fs.existsSync(pidFile)) {
|
|
343
|
-
fs.unlinkSync(pidFile);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
console.log(`✅ Server stopped (PID: ${pid})`);
|
|
347
|
-
process.exit(0);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
console.error('❌ Failed to kill server:', error instanceof Error ? error.message : String(error));
|
|
350
|
-
process.exit(1);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from 'ink-testing-library';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import process from 'process';
|
|
8
|
-
import type { TestServerHandle } from '../test/test-dev-server.js';
|
|
9
|
-
import { TestDevServer } from '../test/test-dev-server.js';
|
|
10
|
-
import { buildCli } from '../cli.js';
|
|
11
|
-
import type { PositronicDevServer } from '@positronic/spec';
|
|
12
|
-
import caz from 'caz';
|
|
13
|
-
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
|
-
|
|
17
|
-
// Singleton cache for the template to avoid repeated npm installs
|
|
18
|
-
// This is shared across all tests in a test run and should NOT be deleted
|
|
19
|
-
// by individual test cleanup. It's cleaned up automatically when the process exits.
|
|
20
|
-
let cachedTemplatePath: string | null = null;
|
|
21
|
-
|
|
22
|
-
async function getCachedTemplate(): Promise<string> {
|
|
23
|
-
// Check if cached template exists and is valid
|
|
24
|
-
if (cachedTemplatePath && fs.existsSync(cachedTemplatePath)) {
|
|
25
|
-
return cachedTemplatePath;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Create cache only once per test run
|
|
29
|
-
const devPath = path.resolve(__dirname, '../../../');
|
|
30
|
-
const originalTemplate = path.resolve(devPath, 'template-new-project');
|
|
31
|
-
|
|
32
|
-
// First, copy template to temp location so caz can mess with that copy
|
|
33
|
-
const tempCopyDir = fs.mkdtempSync(
|
|
34
|
-
path.join(os.tmpdir(), 'positronic-template-copy-')
|
|
35
|
-
);
|
|
36
|
-
fs.cpSync(originalTemplate, tempCopyDir, { recursive: true });
|
|
37
|
-
|
|
38
|
-
// Now generate the actual cached template in another temp directory
|
|
39
|
-
cachedTemplatePath = fs.mkdtempSync(
|
|
40
|
-
path.join(os.tmpdir(), 'positronic-cached-template-')
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// Run caz once to generate a clean template
|
|
44
|
-
const cazOptions = {
|
|
45
|
-
name: 'test-project',
|
|
46
|
-
backend: 'none',
|
|
47
|
-
install: false,
|
|
48
|
-
claudemd: false, // Add the new claudemd option
|
|
49
|
-
force: true,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Temporarily hijack all output streams to suppress caz output
|
|
53
|
-
const originalStdoutWrite = process.stdout.write;
|
|
54
|
-
const originalStderrWrite = process.stderr.write;
|
|
55
|
-
const originalConsoleLog = console.log;
|
|
56
|
-
const originalConsoleError = console.error;
|
|
57
|
-
|
|
58
|
-
process.stdout.write = () => true;
|
|
59
|
-
process.stderr.write = () => true;
|
|
60
|
-
console.log = () => {};
|
|
61
|
-
console.error = () => {};
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
await caz.default(tempCopyDir, cachedTemplatePath, cazOptions);
|
|
65
|
-
} finally {
|
|
66
|
-
// Restore original output streams
|
|
67
|
-
process.stdout.write = originalStdoutWrite;
|
|
68
|
-
process.stderr.write = originalStderrWrite;
|
|
69
|
-
console.log = originalConsoleLog;
|
|
70
|
-
console.error = originalConsoleError;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Clean up the temp copy directory
|
|
74
|
-
fs.rmSync(tempCopyDir, { recursive: true, force: true });
|
|
75
|
-
|
|
76
|
-
return cachedTemplatePath;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Helper function to copy test resources from test data directory
|
|
80
|
-
function copyTestResources(targetDir: string) {
|
|
81
|
-
const testDataPath = path.join(__dirname, '../test/data/resources');
|
|
82
|
-
const targetResourcesPath = path.join(targetDir, 'resources');
|
|
83
|
-
|
|
84
|
-
// Remove existing resources directory if it exists
|
|
85
|
-
if (fs.existsSync(targetResourcesPath)) {
|
|
86
|
-
fs.rmSync(targetResourcesPath, { recursive: true, force: true });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Copy the test data resources
|
|
90
|
-
fs.cpSync(testDataPath, targetResourcesPath, { recursive: true });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Helper function to create a minimal Positronic project structure
|
|
94
|
-
export async function createMinimalProject(dir: string) {
|
|
95
|
-
// Get or create the cached template
|
|
96
|
-
const cachedTemplate = await getCachedTemplate();
|
|
97
|
-
|
|
98
|
-
// Copy the cached template to the target directory
|
|
99
|
-
fs.cpSync(cachedTemplate, dir, { recursive: true });
|
|
100
|
-
copyTestResources(dir);
|
|
101
|
-
// Update positronic.config.json with the correct project name if it exists
|
|
102
|
-
const configPath = path.join(dir, 'positronic.config.json');
|
|
103
|
-
if (fs.existsSync(configPath)) {
|
|
104
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
105
|
-
config.name = 'test-project';
|
|
106
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export class TestEnv {
|
|
111
|
-
private serverHandle: TestServerHandle | null = null;
|
|
112
|
-
constructor(public server: TestDevServer) {}
|
|
113
|
-
get projectRootDir() {
|
|
114
|
-
return this.server.projectRootDir;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
setup(setup: (tempDir: string) => void | Promise<void>) {
|
|
118
|
-
setup(this.projectRootDir);
|
|
119
|
-
return this;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async start() {
|
|
123
|
-
if (this.serverHandle) {
|
|
124
|
-
throw new Error('Server already started');
|
|
125
|
-
}
|
|
126
|
-
this.serverHandle = await this.server.start();
|
|
127
|
-
return async (argv: string[]) => {
|
|
128
|
-
if (!this.serverHandle) {
|
|
129
|
-
throw new Error('Server not started');
|
|
130
|
-
}
|
|
131
|
-
return px(argv, {
|
|
132
|
-
server: this.server,
|
|
133
|
-
projectRootDir: this.projectRootDir,
|
|
134
|
-
});
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
cleanup() {
|
|
139
|
-
fs.rmSync(this.projectRootDir, { recursive: true, force: true });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async stop() {
|
|
143
|
-
if (!this.serverHandle) {
|
|
144
|
-
throw new Error('Server not started');
|
|
145
|
-
}
|
|
146
|
-
this.serverHandle.kill();
|
|
147
|
-
this.serverHandle = null;
|
|
148
|
-
await this.server.stop();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async stopAndCleanup() {
|
|
152
|
-
await this.stop();
|
|
153
|
-
this.cleanup();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function createTestEnv(): Promise<TestEnv> {
|
|
158
|
-
const tempDir = fs.mkdtempSync(
|
|
159
|
-
path.join(os.tmpdir(), 'positronic-server-test-')
|
|
160
|
-
);
|
|
161
|
-
await createMinimalProject(tempDir);
|
|
162
|
-
|
|
163
|
-
// Create test dev server instance
|
|
164
|
-
const devServer = new TestDevServer(tempDir);
|
|
165
|
-
|
|
166
|
-
return new TestEnv(devServer);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Helper function to wait for types file to contain specific content
|
|
170
|
-
export async function waitForTypesFile(
|
|
171
|
-
typesPath: string,
|
|
172
|
-
expectedContent: string | string[],
|
|
173
|
-
maxWaitMs = 5000
|
|
174
|
-
): Promise<string> {
|
|
175
|
-
const startTime = Date.now();
|
|
176
|
-
const contentToCheck = Array.isArray(expectedContent)
|
|
177
|
-
? expectedContent
|
|
178
|
-
: [expectedContent];
|
|
179
|
-
|
|
180
|
-
while (Date.now() - startTime < maxWaitMs) {
|
|
181
|
-
if (fs.existsSync(typesPath)) {
|
|
182
|
-
const content = fs.readFileSync(typesPath, 'utf-8');
|
|
183
|
-
// Check if all expected content is present
|
|
184
|
-
if (contentToCheck.every((expected) => content.includes(expected))) {
|
|
185
|
-
return content;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return '';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
interface PxResult {
|
|
195
|
-
waitForOutput: (regex?: RegExp, maxTries?: number) => Promise<boolean>;
|
|
196
|
-
waitForTypesFile: (types: string | string[]) => Promise<string>;
|
|
197
|
-
instance: {
|
|
198
|
-
lastFrame: () => string | undefined;
|
|
199
|
-
rerender: (element: React.ReactElement) => void;
|
|
200
|
-
unmount: () => void;
|
|
201
|
-
frames: string[];
|
|
202
|
-
stdin: {
|
|
203
|
-
write: (data: string) => void;
|
|
204
|
-
};
|
|
205
|
-
stdout: {
|
|
206
|
-
lastFrame: () => string | undefined;
|
|
207
|
-
frames: string[];
|
|
208
|
-
};
|
|
209
|
-
stderr: {
|
|
210
|
-
lastFrame: () => string | undefined;
|
|
211
|
-
frames: string[];
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export async function px(
|
|
217
|
-
argv: string[],
|
|
218
|
-
options: {
|
|
219
|
-
server?: PositronicDevServer;
|
|
220
|
-
projectRootDir?: string;
|
|
221
|
-
configDir?: string;
|
|
222
|
-
} = {}
|
|
223
|
-
): Promise<PxResult> {
|
|
224
|
-
const { server, projectRootDir, configDir } = options;
|
|
225
|
-
let instance: ReturnType<typeof render> | null = null;
|
|
226
|
-
instance = await runCli(argv, {
|
|
227
|
-
server,
|
|
228
|
-
configDir,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// const { lastFrame, rerender, unmount, frames, stdin, stdout, stderr } = instance!;
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
waitForOutput: async (regex?: RegExp, maxTries = 10) => {
|
|
235
|
-
if (!instance && !regex) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
if (!instance && regex) {
|
|
239
|
-
console.error('waitForOutput failed, instance is null');
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
let tries = 0;
|
|
244
|
-
while (tries < maxTries) {
|
|
245
|
-
const lastFrame = instance!.lastFrame() ?? '';
|
|
246
|
-
if (regex!.test(lastFrame)) {
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
250
|
-
tries++;
|
|
251
|
-
}
|
|
252
|
-
console.error('waitForOutput failed, last frame:', instance!.lastFrame());
|
|
253
|
-
return false;
|
|
254
|
-
},
|
|
255
|
-
waitForTypesFile: async (types: string | string[]) => {
|
|
256
|
-
if (projectRootDir) {
|
|
257
|
-
const typesPath = path.join(projectRootDir, 'resources.d.ts');
|
|
258
|
-
return waitForTypesFile(typesPath, types, 1000);
|
|
259
|
-
}
|
|
260
|
-
console.warn(
|
|
261
|
-
"waitForTypesFile didn't wait for anything, projectRootDir is not set"
|
|
262
|
-
);
|
|
263
|
-
return '';
|
|
264
|
-
},
|
|
265
|
-
instance: instance
|
|
266
|
-
? {
|
|
267
|
-
lastFrame: instance.lastFrame,
|
|
268
|
-
rerender: instance.rerender,
|
|
269
|
-
unmount: instance.unmount,
|
|
270
|
-
frames: instance.frames,
|
|
271
|
-
stdin: instance.stdin,
|
|
272
|
-
stdout: instance.stdout,
|
|
273
|
-
stderr: instance.stderr,
|
|
274
|
-
}
|
|
275
|
-
: {
|
|
276
|
-
lastFrame: () => undefined,
|
|
277
|
-
rerender: () => {},
|
|
278
|
-
unmount: () => {},
|
|
279
|
-
frames: [],
|
|
280
|
-
stdin: { write: () => {} },
|
|
281
|
-
stdout: { lastFrame: () => undefined, frames: [] },
|
|
282
|
-
stderr: { lastFrame: () => undefined, frames: [] },
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Helper function to test CLI commands with ink-testing-library
|
|
288
|
-
async function runCli(
|
|
289
|
-
argv: string[],
|
|
290
|
-
options: {
|
|
291
|
-
server?: PositronicDevServer;
|
|
292
|
-
configDir?: string;
|
|
293
|
-
} = {}
|
|
294
|
-
): Promise<ReturnType<typeof render> | null> {
|
|
295
|
-
let capturedElement: ReturnType<typeof render> | null = null;
|
|
296
|
-
const { configDir, server } = options;
|
|
297
|
-
const mockRenderFn = (element: React.ReactElement) => {
|
|
298
|
-
capturedElement = render(element);
|
|
299
|
-
return capturedElement;
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
// Setup project-specific environment if configDir is provided
|
|
303
|
-
if (configDir) {
|
|
304
|
-
process.env.POSITRONIC_CONFIG_DIR = configDir;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
const testCli = buildCli({
|
|
309
|
-
argv,
|
|
310
|
-
server,
|
|
311
|
-
exitProcess: false,
|
|
312
|
-
render: mockRenderFn,
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
await testCli.parse();
|
|
316
|
-
|
|
317
|
-
return capturedElement;
|
|
318
|
-
} finally {
|
|
319
|
-
// Cleanup project-specific environment if configDir was provided
|
|
320
|
-
if (configDir) {
|
|
321
|
-
delete process.env.POSITRONIC_CONFIG_DIR;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|