@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.
- package/dist/src/api.js +1270 -0
- package/dist/src/brain-runner-do.js +654 -0
- package/dist/src/dev-server.js +1357 -0
- package/{src/index.ts → dist/src/index.js} +1 -6
- package/dist/src/manifest.js +278 -0
- package/dist/src/monitor-do.js +408 -0
- package/{src/node-index.ts → dist/src/node-index.js} +3 -7
- package/dist/src/r2-loader.js +207 -0
- package/dist/src/schedule-do.js +705 -0
- package/dist/src/sqlite-adapter.js +69 -0
- package/dist/types/api.d.ts +21 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/brain-runner-do.d.ts +25 -0
- package/dist/types/brain-runner-do.d.ts.map +1 -0
- package/dist/types/dev-server.d.ts +45 -0
- package/dist/types/dev-server.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/manifest.d.ts +11 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/monitor-do.d.ts +16 -0
- package/dist/types/monitor-do.d.ts.map +1 -0
- package/dist/types/node-index.d.ts +10 -0
- package/dist/types/node-index.d.ts.map +1 -0
- package/dist/types/r2-loader.d.ts +10 -0
- package/dist/types/r2-loader.d.ts.map +1 -0
- package/dist/types/schedule-do.d.ts +47 -0
- package/dist/types/schedule-do.d.ts.map +1 -0
- package/dist/types/sqlite-adapter.d.ts +10 -0
- package/dist/types/sqlite-adapter.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/api.ts +0 -579
- package/src/brain-runner-do.ts +0 -309
- package/src/dev-server.ts +0 -776
- package/src/manifest.ts +0 -69
- package/src/monitor-do.ts +0 -268
- package/src/r2-loader.ts +0 -27
- package/src/schedule-do.ts +0 -377
- package/src/sqlite-adapter.ts +0 -50
- package/test-project/package-lock.json +0 -3010
- package/test-project/package.json +0 -21
- package/test-project/src/index.ts +0 -70
- package/test-project/src/runner.ts +0 -24
- package/test-project/tests/api.test.ts +0 -1005
- package/test-project/tests/r2loader.test.ts +0 -73
- package/test-project/tests/resources-api.test.ts +0 -671
- package/test-project/tests/spec.test.ts +0 -135
- package/test-project/tests/tsconfig.json +0 -7
- package/test-project/tsconfig.json +0 -20
- package/test-project/vitest.config.ts +0 -12
- package/test-project/wrangler.jsonc +0 -53
- 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
|
-
}
|