@positronic/cloudflare 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.
Files changed (52) hide show
  1. package/dist/src/api.js +1270 -0
  2. package/dist/src/brain-runner-do.js +654 -0
  3. package/dist/src/dev-server.js +1357 -0
  4. package/{src/index.ts → dist/src/index.js} +1 -6
  5. package/dist/src/manifest.js +278 -0
  6. package/dist/src/monitor-do.js +408 -0
  7. package/{src/node-index.ts → dist/src/node-index.js} +3 -7
  8. package/dist/src/r2-loader.js +207 -0
  9. package/dist/src/schedule-do.js +705 -0
  10. package/dist/src/sqlite-adapter.js +69 -0
  11. package/dist/types/api.d.ts +21 -0
  12. package/dist/types/api.d.ts.map +1 -0
  13. package/dist/types/brain-runner-do.d.ts +25 -0
  14. package/dist/types/brain-runner-do.d.ts.map +1 -0
  15. package/dist/types/dev-server.d.ts +45 -0
  16. package/dist/types/dev-server.d.ts.map +1 -0
  17. package/dist/types/index.d.ts +7 -0
  18. package/dist/types/index.d.ts.map +1 -0
  19. package/dist/types/manifest.d.ts +11 -0
  20. package/dist/types/manifest.d.ts.map +1 -0
  21. package/dist/types/monitor-do.d.ts +16 -0
  22. package/dist/types/monitor-do.d.ts.map +1 -0
  23. package/dist/types/node-index.d.ts +10 -0
  24. package/dist/types/node-index.d.ts.map +1 -0
  25. package/dist/types/r2-loader.d.ts +10 -0
  26. package/dist/types/r2-loader.d.ts.map +1 -0
  27. package/dist/types/schedule-do.d.ts +47 -0
  28. package/dist/types/schedule-do.d.ts.map +1 -0
  29. package/dist/types/sqlite-adapter.d.ts +10 -0
  30. package/dist/types/sqlite-adapter.d.ts.map +1 -0
  31. package/package.json +5 -1
  32. package/src/api.ts +0 -579
  33. package/src/brain-runner-do.ts +0 -309
  34. package/src/dev-server.ts +0 -776
  35. package/src/manifest.ts +0 -69
  36. package/src/monitor-do.ts +0 -268
  37. package/src/r2-loader.ts +0 -27
  38. package/src/schedule-do.ts +0 -377
  39. package/src/sqlite-adapter.ts +0 -50
  40. package/test-project/package-lock.json +0 -3010
  41. package/test-project/package.json +0 -21
  42. package/test-project/src/index.ts +0 -70
  43. package/test-project/src/runner.ts +0 -24
  44. package/test-project/tests/api.test.ts +0 -1005
  45. package/test-project/tests/r2loader.test.ts +0 -73
  46. package/test-project/tests/resources-api.test.ts +0 -671
  47. package/test-project/tests/spec.test.ts +0 -135
  48. package/test-project/tests/tsconfig.json +0 -7
  49. package/test-project/tsconfig.json +0 -20
  50. package/test-project/vitest.config.ts +0 -12
  51. package/test-project/wrangler.jsonc +0 -53
  52. package/tsconfig.json +0 -11
package/src/dev-server.ts DELETED
@@ -1,776 +0,0 @@
1
- import * as path from 'path';
2
- import * as fsPromises from 'fs/promises';
3
- import * as fs from 'fs';
4
- import * as os from 'os';
5
- import { spawn, exec, type ChildProcess } from 'child_process';
6
- import * as dotenv from 'dotenv';
7
- import caz from 'caz';
8
- import type { PositronicDevServer, ServerHandle } from '@positronic/spec';
9
-
10
- /**
11
- * Implementation of ServerHandle that wraps a ChildProcess
12
- */
13
- class ProcessServerHandle implements ServerHandle {
14
- private closeCallbacks: Array<(code?: number | null) => void> = [];
15
- private errorCallbacks: Array<(error: Error) => void> = [];
16
- private _killed = false;
17
-
18
- constructor(private process: ChildProcess, private port?: number) {
19
- // Forward process events to registered callbacks
20
- process.on('close', (code) => {
21
- this.closeCallbacks.forEach((cb) => cb(code));
22
- });
23
-
24
- process.on('error', (error) => {
25
- this.errorCallbacks.forEach((cb) => cb(error));
26
- });
27
-
28
- process.on('exit', () => {
29
- this._killed = true;
30
- });
31
- }
32
-
33
- onClose(callback: (code?: number | null) => void): void {
34
- this.closeCallbacks.push(callback);
35
- }
36
-
37
- onError(callback: (error: Error) => void): void {
38
- this.errorCallbacks.push(callback);
39
- }
40
-
41
- kill(signal?: string): boolean {
42
- if (!this._killed && this.process && !this.process.killed) {
43
- const result = this.process.kill(signal as any);
44
- if (result) {
45
- this._killed = true;
46
- }
47
- return result;
48
- }
49
- return false;
50
- }
51
-
52
- get killed(): boolean {
53
- return this._killed || (this.process?.killed ?? true);
54
- }
55
-
56
- async waitUntilReady(maxWaitMs: number = 30000): Promise<boolean> {
57
- const startTime = Date.now();
58
- const port = this.port || 8787;
59
-
60
- while (Date.now() - startTime < maxWaitMs) {
61
- try {
62
- const response = await fetch(`http://localhost:${port}/status`);
63
- if (response.ok) {
64
- const data = (await response.json()) as { ready?: boolean };
65
- if (data.ready === true) {
66
- return true;
67
- }
68
- }
69
- } catch (error) {
70
- // Server not ready yet, continue polling
71
- }
72
-
73
- // Wait a bit before trying again
74
- await new Promise((resolve) => setTimeout(resolve, 500));
75
- }
76
-
77
- return false;
78
- }
79
- }
80
-
81
- async function generateProject(
82
- projectName: string,
83
- projectDir: string,
84
- onSuccess?: () => Promise<void> | void
85
- ) {
86
- const devPath = process.env.POSITRONIC_LOCAL_PATH;
87
- let newProjectTemplatePath = '@positronic/template-new-project';
88
- let cazOptions: {
89
- name: string;
90
- backend?: string;
91
- install?: boolean;
92
- pm?: string;
93
- } = { name: projectName };
94
-
95
- try {
96
- if (devPath) {
97
- // Copying templates, why you ask?
98
- // Well because when caz runs if you pass it a path to the template module
99
- // (e.g. for development environment setting POSITRONIC_LOCAL_PATH)
100
- // it runs npm install --production in the template directory. This is a problem
101
- // in our monorepo because this messes up the node_modules at the root of the
102
- // monorepo which then causes the tests to fail. Also any time I was generating a new
103
- // project it was a pain to have to run npm install over and over again just
104
- // to get back to a good state.
105
- const originalNewProjectPkg = path.resolve(
106
- devPath,
107
- 'packages',
108
- 'template-new-project'
109
- );
110
- const copiedNewProjectPkg = fs.mkdtempSync(
111
- path.join(os.tmpdir(), 'positronic-newproj-')
112
- );
113
- fs.cpSync(originalNewProjectPkg, copiedNewProjectPkg, {
114
- recursive: true,
115
- });
116
- newProjectTemplatePath = copiedNewProjectPkg;
117
- cazOptions = {
118
- name: projectName,
119
- backend: 'cloudflare',
120
- install: true,
121
- pm: 'npm',
122
- };
123
- }
124
-
125
- await caz.default(newProjectTemplatePath, projectDir, {
126
- ...cazOptions,
127
- force: false,
128
- });
129
-
130
- await onSuccess?.();
131
- } finally {
132
- // Clean up the temporary copied new project package
133
- if (devPath) {
134
- fs.rmSync(newProjectTemplatePath, {
135
- recursive: true,
136
- force: true,
137
- maxRetries: 3,
138
- });
139
- }
140
- }
141
- }
142
-
143
- async function regenerateManifestFile(
144
- projectRootPath: string,
145
- targetSrcDir: string
146
- ) {
147
- const runnerPath = path.join(projectRootPath, 'runner.ts');
148
- const brainsDir = path.join(projectRootPath, 'brains');
149
- const manifestPath = path.join(targetSrcDir, '_manifest.ts');
150
-
151
- let importStatements = `import type { Brain } from '@positronic/core';\n`;
152
- let manifestEntries = '';
153
-
154
- const brainsDirExists = await fsPromises
155
- .access(brainsDir)
156
- .then(() => true)
157
- .catch(() => false);
158
- if (brainsDirExists) {
159
- const files = await fsPromises.readdir(brainsDir);
160
- const brainFiles = files.filter(
161
- (file) => file.endsWith('.ts') && !file.startsWith('_')
162
- );
163
-
164
- for (const file of brainFiles) {
165
- const brainName = path.basename(file, '.ts');
166
- const importPath = `../../brains/${brainName}.js`;
167
- const importAlias = `brain_${brainName.replace(/[^a-zA-Z0-9_]/g, '_')}`;
168
-
169
- importStatements += `import * as ${importAlias} from '${importPath}';\n`;
170
- manifestEntries += ` ${JSON.stringify(
171
- brainName
172
- )}: ${importAlias}.default as Brain,\n`;
173
- }
174
- }
175
-
176
- const manifestContent = `// This file is generated automatically. Do not edit directly.\n${importStatements}\nexport const staticManifest: Record<string, Brain> = {\n${manifestEntries}};
177
- `;
178
-
179
- const runnerContent = await fsPromises.readFile(runnerPath, 'utf-8');
180
- await fsPromises.mkdir(targetSrcDir, { recursive: true });
181
- await fsPromises.writeFile(manifestPath, manifestContent, 'utf-8');
182
- await fsPromises.writeFile(
183
- path.join(targetSrcDir, 'runner.ts'),
184
- runnerContent,
185
- 'utf-8'
186
- );
187
- }
188
-
189
- export class CloudflareDevServer implements PositronicDevServer {
190
- // TODO: Future architectural improvements:
191
- // 1. Extract .positronic directory into its own template package to eliminate temp directory hack
192
- // 2. Create a declarative configuration model for wrangler updates
193
- // 3. Move more logic into the template itself using template interpolation
194
- // 4. Consider a pipeline-based setup process for better composability
195
- // 5. Separate concerns better between template generation, env syncing, and dynamic configuration
196
-
197
- /**
198
- * Sets up the .positronic server environment directory.
199
- * If the directory is missing or forceSetup is true, it generates the
200
- * full project in a temporary directory and copies the .positronic
201
- * part into the actual project.
202
- *
203
- * Doing it this way because it's tricky to split the template-new-project
204
- * into a template-cloudflare without lots of extra code, was better to combine
205
- * backend templates into a single template-new-project. But then we still need
206
- * a way to generate the .positronic directory if it's not there, so this is the
207
- * simplest solution.
208
- */
209
-
210
- private logCallbacks: Array<(message: string) => void> = [];
211
- private errorCallbacks: Array<(message: string) => void> = [];
212
- private warningCallbacks: Array<(message: string) => void> = [];
213
-
214
- constructor(public projectRootDir: string) {}
215
-
216
- async setup(force?: boolean): Promise<void> {
217
- const projectRoot = this.projectRootDir;
218
- const serverDir = path.join(projectRoot, '.positronic');
219
-
220
- // Ensure .positronic directory exists
221
- await this.ensureServerDirectory(projectRoot, serverDir, force);
222
-
223
- // Sync environment variables to .dev.vars
224
- await this.syncEnvironmentVariables(projectRoot, serverDir);
225
-
226
- // Regenerate manifest based on actual project state
227
- await this.regenerateProjectManifest(projectRoot, serverDir);
228
-
229
- // Update wrangler config based on environment
230
- await this.updateWranglerConfiguration(projectRoot, serverDir);
231
- }
232
-
233
- private async ensureServerDirectory(
234
- projectRoot: string,
235
- serverDir: string,
236
- force?: boolean
237
- ): Promise<void> {
238
- const serverDirExists = await fsPromises
239
- .access(serverDir)
240
- .then(() => true)
241
- .catch(() => false);
242
-
243
- if (!serverDirExists || force) {
244
- console.log(
245
- force
246
- ? 'Forcing regeneration of .positronic environment...'
247
- : 'Missing .positronic environment, generating...'
248
- );
249
- let tempDir: string | undefined;
250
- try {
251
- // Create a temp directory to generate the project in
252
- // so we can copy the .positronic directory to the user's project
253
- tempDir = fs.mkdtempSync(
254
- path.join(os.tmpdir(), 'positronic-server-setup-')
255
- );
256
-
257
- // Read the actual project name from the config file
258
- const configPath = path.join(projectRoot, 'positronic.config.json');
259
- const configContent = await fsPromises.readFile(configPath, 'utf-8');
260
- const config = JSON.parse(configContent);
261
- const projectName = config.projectName;
262
-
263
- if (!projectName) {
264
- throw new Error('Project name not found in positronic.config.json');
265
- }
266
-
267
- await generateProject(projectName, tempDir, async () => {
268
- const sourcePositronicDir = path.join(tempDir!, '.positronic');
269
- const targetPositronicDir = serverDir;
270
-
271
- // If forcing setup, remove existing target first
272
- if (serverDirExists && force) {
273
- await fsPromises.rm(targetPositronicDir, {
274
- recursive: true,
275
- force: true,
276
- });
277
- }
278
-
279
- // Copy the generated .positronic directory
280
- await fsPromises.cp(sourcePositronicDir, targetPositronicDir, {
281
- recursive: true,
282
- });
283
- });
284
- } finally {
285
- // Clean up the temporary generation directory
286
- if (tempDir) {
287
- fs.rmSync(tempDir, { recursive: true, force: true });
288
- }
289
- }
290
- }
291
- }
292
-
293
- private async syncEnvironmentVariables(
294
- projectRoot: string,
295
- serverDir: string
296
- ): Promise<void> {
297
- const rootEnvFilePath = path.join(projectRoot, '.env');
298
- const devVarsPath = path.join(serverDir, '.dev.vars');
299
- let devVarsContent = '';
300
-
301
- if (fs.existsSync(rootEnvFilePath)) {
302
- const rootEnvFileContent = fs.readFileSync(rootEnvFilePath);
303
- const parsedRootEnv = dotenv.parse(rootEnvFileContent);
304
- if (Object.keys(parsedRootEnv).length > 0) {
305
- devVarsContent =
306
- Object.entries(parsedRootEnv)
307
- .map(([key, value]) => `${key}="${value.replace(/"/g, '\\\\"')}"`)
308
- .join('\n') + '\n';
309
- }
310
- }
311
- fs.writeFileSync(devVarsPath, devVarsContent);
312
- }
313
-
314
- private async regenerateProjectManifest(
315
- projectRoot: string,
316
- serverDir: string
317
- ): Promise<void> {
318
- const srcDir = path.join(serverDir, 'src');
319
- await regenerateManifestFile(projectRoot, srcDir);
320
- }
321
-
322
- private async updateWranglerConfiguration(
323
- projectRoot: string,
324
- serverDir: string
325
- ): Promise<void> {
326
- const wranglerConfigPath = path.join(serverDir, 'wrangler.jsonc');
327
- if (!fs.existsSync(wranglerConfigPath)) {
328
- return;
329
- }
330
-
331
- // Parse environment variables
332
- const parsedEnv = this.parseEnvironmentFile(projectRoot);
333
-
334
- // Check R2 configuration mode
335
- const hasR2Credentials = this.hasCompleteR2Credentials(parsedEnv);
336
-
337
- // Update wrangler config if needed
338
- await this.applyWranglerConfigUpdates(
339
- wranglerConfigPath,
340
- parsedEnv,
341
- hasR2Credentials
342
- );
343
- }
344
-
345
- private parseEnvironmentFile(projectRoot: string): Record<string, string> {
346
- const rootEnvFilePath = path.join(projectRoot, '.env');
347
- if (!fs.existsSync(rootEnvFilePath)) {
348
- return {};
349
- }
350
-
351
- const rootEnvFileContent = fs.readFileSync(rootEnvFilePath);
352
- return dotenv.parse(rootEnvFileContent);
353
- }
354
-
355
- private hasCompleteR2Credentials(env: Record<string, string>): boolean {
356
- return Boolean(
357
- env.R2_ACCESS_KEY_ID?.trim() &&
358
- env.R2_SECRET_ACCESS_KEY?.trim() &&
359
- env.R2_ACCOUNT_ID?.trim() &&
360
- env.R2_BUCKET_NAME?.trim()
361
- );
362
- }
363
-
364
- private async applyWranglerConfigUpdates(
365
- configPath: string,
366
- env: Record<string, string>,
367
- hasR2Credentials: boolean
368
- ): Promise<void> {
369
- // Read and parse the wrangler config
370
- const wranglerContent = fs.readFileSync(configPath, 'utf-8');
371
- const wranglerConfig = JSON.parse(wranglerContent);
372
- let configChanged = false;
373
-
374
- if (hasR2Credentials) {
375
- configChanged = this.configureRemoteR2(wranglerConfig, env);
376
- } else {
377
- configChanged = this.configureLocalR2(wranglerConfig);
378
- }
379
-
380
- // Write back the updated configuration only if it changed
381
- if (configChanged) {
382
- const updatedContent = JSON.stringify(wranglerConfig, null, 2);
383
- fs.writeFileSync(configPath, updatedContent);
384
- console.log(
385
- hasR2Credentials
386
- ? '🔗 Configured for remote R2 bindings'
387
- : '🏠 Configured for local R2 bindings'
388
- );
389
- }
390
- }
391
-
392
- private configureRemoteR2(config: any, env: Record<string, string>): boolean {
393
- let changed = false;
394
-
395
- if (config.r2_buckets && config.r2_buckets[0]) {
396
- if (config.r2_buckets[0].bucket_name !== env.R2_BUCKET_NAME) {
397
- config.r2_buckets[0].bucket_name = env.R2_BUCKET_NAME;
398
- changed = true;
399
- }
400
- if (!config.r2_buckets[0].experimental_remote) {
401
- config.r2_buckets[0].experimental_remote = true;
402
- changed = true;
403
- }
404
- }
405
-
406
- // Also update the vars section
407
- if (config.vars && config.vars.R2_BUCKET_NAME !== env.R2_BUCKET_NAME) {
408
- config.vars.R2_BUCKET_NAME = env.R2_BUCKET_NAME;
409
- changed = true;
410
- }
411
-
412
- return changed;
413
- }
414
-
415
- private configureLocalR2(config: any): boolean {
416
- let changed = false;
417
-
418
- if (config.r2_buckets && config.r2_buckets[0]) {
419
- // Get project name from the wrangler config name (remove "positronic-dev-" prefix)
420
- const configName = config.name || 'local-project';
421
- const projectName =
422
- configName.replace(/^positronic-dev-/, '') || 'local-project';
423
-
424
- if (config.r2_buckets[0].experimental_remote) {
425
- delete config.r2_buckets[0].experimental_remote;
426
- changed = true;
427
- }
428
- if (config.r2_buckets[0].bucket_name !== projectName) {
429
- config.r2_buckets[0].bucket_name = projectName;
430
- changed = true;
431
- }
432
-
433
- // Also update the vars section
434
- if (config.vars && config.vars.R2_BUCKET_NAME !== projectName) {
435
- config.vars.R2_BUCKET_NAME = projectName;
436
- changed = true;
437
- }
438
- }
439
-
440
- return changed;
441
- }
442
-
443
- async start(port?: number): Promise<ServerHandle> {
444
- const serverDir = path.join(this.projectRootDir, '.positronic');
445
-
446
- // Start wrangler dev server
447
- const wranglerArgs = ['dev', '--x-remote-bindings'];
448
-
449
- if (port) {
450
- wranglerArgs.push('--port', String(port));
451
- }
452
-
453
- const wranglerProcess = spawn('npx', ['wrangler', ...wranglerArgs], {
454
- cwd: serverDir,
455
- stdio: ['inherit', 'pipe', 'pipe'], // stdin inherit, stdout/stderr piped
456
- });
457
-
458
- // Capture and forward stdout
459
- wranglerProcess.stdout?.on('data', (data) => {
460
- const message = data.toString();
461
-
462
- // Send to registered callbacks
463
- this.logCallbacks.forEach((cb) => cb(message));
464
-
465
- // Always forward to console
466
- process.stdout.write(data);
467
- });
468
-
469
- // Capture and forward stderr
470
- wranglerProcess.stderr?.on('data', (data) => {
471
- const message = data.toString();
472
-
473
- // Parse for warnings vs errors
474
- if (message.includes('WARNING') || message.includes('⚠')) {
475
- this.warningCallbacks.forEach((cb) => cb(message));
476
- } else {
477
- this.errorCallbacks.forEach((cb) => cb(message));
478
- }
479
-
480
- // Always forward to console
481
- process.stderr.write(data);
482
- });
483
-
484
- wranglerProcess.on('error', (err) => {
485
- const errorMessage = `Failed to start Wrangler dev server: ${err.message}\n`;
486
- this.errorCallbacks.forEach((cb) => cb(errorMessage));
487
- console.error('Failed to start Wrangler dev server:', err);
488
- });
489
-
490
- return new ProcessServerHandle(wranglerProcess, port);
491
- }
492
-
493
- async watch(
494
- filePath: string,
495
- event: 'add' | 'change' | 'unlink'
496
- ): Promise<void> {
497
- const projectRoot = this.projectRootDir;
498
- // Regenerate manifest when brain files change
499
- const serverDir = path.join(projectRoot, '.positronic');
500
- const srcDir = path.join(serverDir, 'src');
501
-
502
- console.log(`Brain file ${event}: ${path.relative(projectRoot, filePath)}`);
503
- await regenerateManifestFile(projectRoot, srcDir);
504
- }
505
-
506
- async deploy(): Promise<void> {
507
- const projectRoot = this.projectRootDir;
508
- const serverDir = path.join(projectRoot, '.positronic');
509
-
510
- // Ensure .positronic directory and manifest are up to date, but don't force regeneration
511
- await this.setup();
512
-
513
- // Check for required Cloudflare credentials in environment variables
514
- if (
515
- !process.env.CLOUDFLARE_API_TOKEN ||
516
- !process.env.CLOUDFLARE_ACCOUNT_ID
517
- ) {
518
- throw new Error(
519
- 'Missing required Cloudflare credentials.\n' +
520
- 'Please set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.\n' +
521
- 'For example:\n' +
522
- ' export CLOUDFLARE_API_TOKEN=your-api-token\n' +
523
- ' export CLOUDFLARE_ACCOUNT_ID=your-account-id'
524
- );
525
- }
526
-
527
- console.log('🚀 Deploying to Cloudflare Workers (production)...');
528
-
529
- // Deploy to production using wrangler
530
- return new Promise<void>((resolve, reject) => {
531
- const wranglerProcess = spawn(
532
- 'npx',
533
- ['wrangler', 'deploy', '--env', 'production'],
534
- {
535
- cwd: serverDir,
536
- stdio: ['inherit', 'pipe', 'pipe'],
537
- env: {
538
- ...process.env,
539
- CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN,
540
- CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
541
- },
542
- }
543
- );
544
-
545
- // Capture and forward stdout
546
- wranglerProcess.stdout?.on('data', (data) => {
547
- const message = data.toString();
548
- this.logCallbacks.forEach((cb) => cb(message));
549
- process.stdout.write(data);
550
- });
551
-
552
- // Capture and forward stderr
553
- wranglerProcess.stderr?.on('data', (data) => {
554
- const message = data.toString();
555
- if (message.includes('WARNING') || message.includes('⚠')) {
556
- this.warningCallbacks.forEach((cb) => cb(message));
557
- } else {
558
- this.errorCallbacks.forEach((cb) => cb(message));
559
- }
560
- process.stderr.write(data);
561
- });
562
-
563
- wranglerProcess.on('error', (err) => {
564
- const errorMessage = `Failed to start Wrangler deploy: ${err.message}\n`;
565
- this.errorCallbacks.forEach((cb) => cb(errorMessage));
566
- console.error('Failed to start Wrangler deploy:', err);
567
- reject(err);
568
- });
569
-
570
- wranglerProcess.on('exit', (code) => {
571
- if (code === 0) {
572
- const successMessage = '✅ Deployment complete!\n';
573
- this.logCallbacks.forEach((cb) => cb(successMessage));
574
- console.log('✅ Deployment complete!');
575
- resolve();
576
- } else {
577
- reject(new Error(`Wrangler deploy exited with code ${code}`));
578
- }
579
- });
580
- });
581
- }
582
-
583
- onLog(callback: (message: string) => void): void {
584
- this.logCallbacks.push(callback);
585
- }
586
-
587
- onError(callback: (message: string) => void): void {
588
- this.errorCallbacks.push(callback);
589
- }
590
-
591
- onWarning(callback: (message: string) => void): void {
592
- this.warningCallbacks.push(callback);
593
- }
594
-
595
- async listSecrets(): Promise<
596
- Array<{ name: string; createdAt?: Date; updatedAt?: Date }>
597
- > {
598
- const serverDir = path.join(this.projectRootDir, '.positronic');
599
-
600
- // Get auth from environment variables
601
- if (
602
- !process.env.CLOUDFLARE_API_TOKEN ||
603
- !process.env.CLOUDFLARE_ACCOUNT_ID
604
- ) {
605
- throw new Error(
606
- 'Missing Cloudflare credentials. Please set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.'
607
- );
608
- }
609
-
610
- return new Promise((resolve, reject) => {
611
- const child = spawn('npx', ['wrangler', 'secret', 'list'], {
612
- cwd: serverDir,
613
- env: {
614
- ...process.env,
615
- CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN,
616
- CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
617
- },
618
- stdio: 'inherit', // Pass through all output directly to the terminal
619
- });
620
-
621
- child.on('close', (code) => {
622
- if (code !== 0) {
623
- // Don't wrap the error - backend CLI already printed it
624
- reject(new Error(''));
625
- } else {
626
- // Return empty array - output was already printed
627
- resolve([]);
628
- }
629
- });
630
-
631
- child.on('error', (err) => {
632
- reject(err);
633
- });
634
- });
635
- }
636
-
637
- async setSecret(name: string, value: string): Promise<void> {
638
- const serverDir = path.join(this.projectRootDir, '.positronic');
639
-
640
- // Get auth from environment variables
641
- if (
642
- !process.env.CLOUDFLARE_API_TOKEN ||
643
- !process.env.CLOUDFLARE_ACCOUNT_ID
644
- ) {
645
- throw new Error(
646
- 'Missing Cloudflare credentials. Please set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.'
647
- );
648
- }
649
-
650
- return new Promise((resolve, reject) => {
651
- const child = spawn('npx', ['wrangler', 'secret', 'put', name], {
652
- cwd: serverDir,
653
- env: {
654
- ...process.env,
655
- CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN,
656
- CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
657
- },
658
- stdio: ['pipe', 'inherit', 'inherit'], // stdin pipe, stdout/stderr inherit
659
- });
660
-
661
- child.stdin.write(value);
662
- child.stdin.end();
663
-
664
- child.on('close', (code) => {
665
- if (code !== 0) {
666
- // Don't wrap the error - backend CLI already printed it
667
- reject(new Error(''));
668
- } else {
669
- resolve();
670
- }
671
- });
672
-
673
- child.on('error', (err) => {
674
- reject(err);
675
- });
676
- });
677
- }
678
-
679
- async deleteSecret(name: string): Promise<boolean> {
680
- const serverDir = path.join(this.projectRootDir, '.positronic');
681
-
682
- // Get auth from environment variables
683
- if (
684
- !process.env.CLOUDFLARE_API_TOKEN ||
685
- !process.env.CLOUDFLARE_ACCOUNT_ID
686
- ) {
687
- throw new Error(
688
- 'Missing Cloudflare credentials. Please set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.'
689
- );
690
- }
691
-
692
- return new Promise((resolve, reject) => {
693
- const child = spawn('npx', ['wrangler', 'secret', 'delete', name], {
694
- cwd: serverDir,
695
- env: {
696
- ...process.env,
697
- CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN,
698
- CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
699
- },
700
- stdio: 'inherit', // Pass through all output directly to the terminal
701
- });
702
-
703
- child.on('close', (code) => {
704
- if (code !== 0) {
705
- // Don't wrap the error - backend CLI already printed it
706
- reject(new Error(''));
707
- } else {
708
- resolve(true);
709
- }
710
- });
711
-
712
- child.on('error', (err) => {
713
- reject(err);
714
- });
715
- });
716
- }
717
-
718
- async bulkSecrets(filePath: string): Promise<void> {
719
- const serverDir = path.join(this.projectRootDir, '.positronic');
720
-
721
- // Check auth credentials
722
- if (
723
- !process.env.CLOUDFLARE_API_TOKEN ||
724
- !process.env.CLOUDFLARE_ACCOUNT_ID
725
- ) {
726
- throw new Error(
727
- 'Missing Cloudflare credentials. Please set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.'
728
- );
729
- }
730
-
731
- // Read and parse the .env file
732
- if (!fs.existsSync(filePath)) {
733
- throw new Error(`File not found: ${filePath}`);
734
- }
735
-
736
- const envContent = fs.readFileSync(filePath, 'utf8');
737
- const secrets = dotenv.parse(envContent);
738
-
739
- if (Object.keys(secrets).length === 0) {
740
- throw new Error('No secrets found in the .env file');
741
- }
742
-
743
- // Convert to JSON format that wrangler expects
744
- const jsonContent = JSON.stringify(secrets);
745
-
746
- return new Promise((resolve, reject) => {
747
- // Use wrangler secret:bulk command
748
- const child = spawn('npx', ['wrangler', 'secret:bulk'], {
749
- cwd: serverDir,
750
- env: {
751
- ...process.env,
752
- CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN,
753
- CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
754
- },
755
- stdio: ['pipe', 'inherit', 'inherit'], // stdin pipe, stdout/stderr inherit
756
- });
757
-
758
- // Write JSON to stdin
759
- child.stdin.write(jsonContent);
760
- child.stdin.end();
761
-
762
- child.on('close', (code) => {
763
- if (code !== 0) {
764
- // Don't wrap the error - backend CLI already printed it
765
- reject(new Error(''));
766
- } else {
767
- resolve();
768
- }
769
- });
770
-
771
- child.on('error', (err) => {
772
- reject(err);
773
- });
774
- });
775
- }
776
- }