@flue/sdk 0.3.1 → 0.3.3
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 +11 -0
- package/dist/index.d.mts +39 -1
- package/dist/index.mjs +208 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -318,6 +318,17 @@ Defaults to port `3583` ("FLUE" on a phone keypad). Override with `--port`.
|
|
|
318
318
|
|
|
319
319
|
`flue dev --target cloudflare` requires `wrangler` as a peer dependency in your project (`npm install --save-dev wrangler`).
|
|
320
320
|
|
|
321
|
+
#### Loading environment variables
|
|
322
|
+
|
|
323
|
+
Pass `--env <path>` to load a `.env`-format file. Works for both targets:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
flue dev --target node --env .env
|
|
327
|
+
flue dev --target cloudflare --env .env
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Repeatable; later files override earlier ones on key collision. Shell-set env vars win over file values. Edits to the file trigger a reload. Same flag works for `flue run`.
|
|
331
|
+
|
|
321
332
|
### Trigger From the CLI (`flue run`)
|
|
322
333
|
|
|
323
334
|
Build and run any agent locally, perfect for running in CI or for one-shot scripted invocations. Production-shaped — builds the deployable artifact and starts it once.
|
package/dist/index.d.mts
CHANGED
|
@@ -43,6 +43,24 @@ interface DevOptions {
|
|
|
43
43
|
target: 'node' | 'cloudflare';
|
|
44
44
|
/** Defaults to 3583 ("FLUE" on a phone keypad). */
|
|
45
45
|
port?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Absolute paths to env files (`.env`-format) to load before starting the
|
|
48
|
+
* dev server. Repeatable; later files override earlier ones on key
|
|
49
|
+
* collision (matching wrangler's `envFiles` semantics and standard
|
|
50
|
+
* dotenv composition patterns).
|
|
51
|
+
*
|
|
52
|
+
* - Node: parsed with `node:util.parseEnv` and merged into the child
|
|
53
|
+
* server process's env. Shell-set env vars win over file values.
|
|
54
|
+
* - Cloudflare: passed through to wrangler's `unstable_startWorker` as
|
|
55
|
+
* `envFiles`, which loads them as `secret_text` bindings.
|
|
56
|
+
*
|
|
57
|
+
* If empty/undefined, no env loading happens. Cloudflare's auto-discovery
|
|
58
|
+
* of `.dev.vars` is disabled in either case (we always pass an explicit
|
|
59
|
+
* `envFiles` array to wrangler so its default search is suppressed).
|
|
60
|
+
*
|
|
61
|
+
* Each path must exist; otherwise dev fails fast with a clear error.
|
|
62
|
+
*/
|
|
63
|
+
envFiles?: string[];
|
|
46
64
|
}
|
|
47
65
|
/** Default port for `flue dev`. F=3, L=5, U=8, E=3 on a phone keypad. */
|
|
48
66
|
declare const DEFAULT_DEV_PORT = 3583;
|
|
@@ -53,6 +71,26 @@ declare const DEFAULT_DEV_PORT = 3583;
|
|
|
53
71
|
* — the user is editing code, after all, and we want to recover when they fix it.
|
|
54
72
|
*/
|
|
55
73
|
declare function dev(options: DevOptions): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve and validate a list of env-file paths. Returns absolute paths.
|
|
76
|
+
*
|
|
77
|
+
* Throws a friendly `[flue]`-prefixed error if any path doesn't exist. The
|
|
78
|
+
* goal of `--env` is explicitness — silent skip on a typo would defeat
|
|
79
|
+
* the purpose.
|
|
80
|
+
*/
|
|
81
|
+
declare function resolveEnvFiles(envFiles: string[] | undefined, cwd: string): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Parse one or more `.env`-format files and return their merged contents.
|
|
84
|
+
* Later files override earlier files on key collision.
|
|
85
|
+
*
|
|
86
|
+
* Uses Node's built-in `util.parseEnv` (Node 20.6+; Flue requires Node 22+).
|
|
87
|
+
* No `dotenv` package needed.
|
|
88
|
+
*
|
|
89
|
+
* Parse-only — doesn't touch `process.env`. Caller composes with
|
|
90
|
+
* `process.env` as needed (typical pattern: spread file vars first, then
|
|
91
|
+
* `process.env`, so shell-set values win).
|
|
92
|
+
*/
|
|
93
|
+
declare function parseEnvFiles(absolutePaths: string[]): Record<string, string>;
|
|
56
94
|
//#endregion
|
|
57
95
|
//#region src/agent.d.ts
|
|
58
96
|
declare const BUILTIN_TOOL_NAMES: Set<string>;
|
|
@@ -75,4 +113,4 @@ interface CreateToolsOptions {
|
|
|
75
113
|
}
|
|
76
114
|
declare function createTools(env: SessionEnv, options?: CreateToolsOptions): AgentTool<any>[];
|
|
77
115
|
//#endregion
|
|
78
|
-
export { type AgentConfig, type AgentInfo, type AgentInit, BUILTIN_TOOL_NAMES, type BashFactory, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, DEFAULT_DEV_PORT, type DevOptions, type FileStat, type FlueAgent, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type FlueSessions, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionOptions, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, type ToolParameters, build, createTools, dev, resolveWorkspaceFromCwd };
|
|
116
|
+
export { type AgentConfig, type AgentInfo, type AgentInit, BUILTIN_TOOL_NAMES, type BashFactory, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, DEFAULT_DEV_PORT, type DevOptions, type FileStat, type FlueAgent, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type FlueSessions, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionOptions, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, type ToolParameters, build, createTools, dev, parseEnvFiles, resolveEnvFiles, resolveWorkspaceFromCwd };
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { packageUpSync } from "package-up";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { parseEnv } from "node:util";
|
|
7
8
|
|
|
8
9
|
//#region src/cloudflare-wrangler-merge.ts
|
|
9
10
|
/**
|
|
@@ -126,6 +127,83 @@ function validateUserWranglerConfig(config) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
/**
|
|
130
|
+
* Compute Flue's migration contributions for a build.
|
|
131
|
+
*
|
|
132
|
+
* Algorithm:
|
|
133
|
+
*
|
|
134
|
+
* 1. Walk every existing migration entry in the user's config and union the
|
|
135
|
+
* SQLite-backed classes it declares — across `new_sqlite_classes` and
|
|
136
|
+
* the `to` side of `renamed_classes` / `transferred_classes`. The
|
|
137
|
+
* resulting set is "already-declared": every SQLite-backed class
|
|
138
|
+
* Cloudflare's runtime currently knows about for this Worker.
|
|
139
|
+
* `deleted_classes` and the `from` side of renames are subtracted, since
|
|
140
|
+
* they've been explicitly removed.
|
|
141
|
+
*
|
|
142
|
+
* KV-backed classes (`new_classes`) are deliberately NOT added to the
|
|
143
|
+
* "declared" set. Flue agents always need a SQLite-backed class for
|
|
144
|
+
* session storage; if a user happens to have a KV-backed DO with the
|
|
145
|
+
* same name as a Flue agent, we still need to emit our SQLite migration.
|
|
146
|
+
* The deploy itself will then surface a clear "class already defined"
|
|
147
|
+
* error from Cloudflare rather than silently shipping a broken worker
|
|
148
|
+
* where the agent has no working session store.
|
|
149
|
+
* 2. For each class in `currentClasses` that isn't already-declared, emit a
|
|
150
|
+
* deterministic per-class migration: one tag, one class. Per-class tags
|
|
151
|
+
* are essential because Cloudflare migration tags are immutable once
|
|
152
|
+
* deployed — packing all classes under a single shared tag (the original
|
|
153
|
+
* bug in issue #15) means new classes added on a redeploy are silently
|
|
154
|
+
* ignored. With per-class tags, every redeploy is a no-op except for
|
|
155
|
+
* the truly net-new classes.
|
|
156
|
+
*
|
|
157
|
+
* Renames and deletes are not auto-detected. If an agent file disappears,
|
|
158
|
+
* Flue silently emits no migration for it — Cloudflare's runtime keeps the
|
|
159
|
+
* orphaned class data alive but unbound, and the user can clean up (or
|
|
160
|
+
* rename to recover) by adding a manual `renamed_classes` / `deleted_classes`
|
|
161
|
+
* migration to their own wrangler.jsonc. Auto-emitting `deleted_classes`
|
|
162
|
+
* would destroy stored DO data on every accidental file removal, which is
|
|
163
|
+
* never the right default.
|
|
164
|
+
*
|
|
165
|
+
* Returned in alphabetical order so a regenerated `dist/wrangler.jsonc` is
|
|
166
|
+
* byte-identical across machines and CI runs.
|
|
167
|
+
*
|
|
168
|
+
* Pure function: takes the current class list + the user's existing
|
|
169
|
+
* migrations array (typically `userConfig.migrations` straight from
|
|
170
|
+
* wrangler's reader) and returns the migrations to append. Doesn't read or
|
|
171
|
+
* write any files.
|
|
172
|
+
*/
|
|
173
|
+
function computeFlueMigrations(currentClasses, userMigrations) {
|
|
174
|
+
const migrationsArray = Array.isArray(userMigrations) ? userMigrations : [];
|
|
175
|
+
const declared = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const raw of migrationsArray) {
|
|
177
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
178
|
+
const m = raw;
|
|
179
|
+
const collectClassList = (key) => {
|
|
180
|
+
const v = m[key];
|
|
181
|
+
return Array.isArray(v) ? v.filter((c) => typeof c === "string") : [];
|
|
182
|
+
};
|
|
183
|
+
for (const c of collectClassList("new_sqlite_classes")) declared.add(c);
|
|
184
|
+
for (const c of collectClassList("deleted_classes")) declared.delete(c);
|
|
185
|
+
const renamed = Array.isArray(m.renamed_classes) ? m.renamed_classes : [];
|
|
186
|
+
for (const r of renamed) {
|
|
187
|
+
if (typeof r !== "object" || r === null) continue;
|
|
188
|
+
const obj = r;
|
|
189
|
+
if (typeof obj.from === "string") declared.delete(obj.from);
|
|
190
|
+
if (typeof obj.to === "string") declared.add(obj.to);
|
|
191
|
+
}
|
|
192
|
+
const transferred = Array.isArray(m.transferred_classes) ? m.transferred_classes : [];
|
|
193
|
+
for (const t of transferred) {
|
|
194
|
+
if (typeof t !== "object" || t === null) continue;
|
|
195
|
+
const obj = t;
|
|
196
|
+
if (typeof obj.to === "string") declared.add(obj.to);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const additions = [];
|
|
200
|
+
for (const c of [...currentClasses].sort()) if (!declared.has(c)) additions.push({
|
|
201
|
+
tag: `flue-class-${c}`,
|
|
202
|
+
new_sqlite_classes: [c]
|
|
203
|
+
});
|
|
204
|
+
return additions;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
129
207
|
* Produce the merged wrangler config: start from the user's, layer Flue's
|
|
130
208
|
* contributions on top. Pure function — caller handles reading and writing.
|
|
131
209
|
*/
|
|
@@ -148,11 +226,46 @@ function mergeFlueAdditions(userConfig, additions) {
|
|
|
148
226
|
const existingMigrations = Array.isArray(merged.migrations) ? merged.migrations : [];
|
|
149
227
|
const existingMigrationTags = new Set(existingMigrations.filter((m) => typeof m === "object" && m !== null).map((m) => m.tag).filter((t) => typeof t === "string"));
|
|
150
228
|
const migrationsOut = [...existingMigrations];
|
|
151
|
-
if (!existingMigrationTags.has(
|
|
229
|
+
for (const migration of additions.migrations) if (!existingMigrationTags.has(migration.tag)) {
|
|
230
|
+
migrationsOut.push(migration);
|
|
231
|
+
existingMigrationTags.add(migration.tag);
|
|
232
|
+
}
|
|
152
233
|
merged.migrations = migrationsOut;
|
|
153
234
|
return merged;
|
|
154
235
|
}
|
|
155
236
|
/**
|
|
237
|
+
* Strip wrangler-normalizer defaults that cause spurious warnings when wrangler
|
|
238
|
+
* re-parses our generated dist/wrangler.jsonc.
|
|
239
|
+
*
|
|
240
|
+
* Background: `unstable_readConfig` returns a fully-normalized `Unstable_Config`
|
|
241
|
+
* with every section populated to a default — including `unsafe: {}`. Wrangler's
|
|
242
|
+
* own validator then emits a `"unsafe" fields are experimental` warning whenever
|
|
243
|
+
* the field is *present*, regardless of whether it's empty. So our merged file,
|
|
244
|
+
* which inherits the empty default, would trip the warning at every dev start
|
|
245
|
+
* and every deploy.
|
|
246
|
+
*
|
|
247
|
+
* We delete `unsafe` only when it's an empty object (the exact shape wrangler's
|
|
248
|
+
* normalizer produces). If a user has actually written `unsafe: {...}` in their
|
|
249
|
+
* own wrangler.jsonc, the value will be non-empty and we leave it alone — the
|
|
250
|
+
* warning in that case is wrangler's intended diagnostic, not noise.
|
|
251
|
+
*
|
|
252
|
+
* Other normalizer-defaulted-empty fields (`vars: {}`, `kv_namespaces: []`,
|
|
253
|
+
* `python_modules: { exclude: ['**\/*.pyc'] }`, etc.) are left in place. They're
|
|
254
|
+
* harmless: wrangler doesn't warn about them, dist/wrangler.jsonc is an
|
|
255
|
+
* internal build artifact, and stripping them only saves bytes. Only `unsafe`
|
|
256
|
+
* has a user-visible side effect we need to fix.
|
|
257
|
+
*
|
|
258
|
+
* If wrangler adds another field to its `experimental()` warning list in a
|
|
259
|
+
* future version (today there are only two: `unsafe` and `secrets`), this
|
|
260
|
+
* function is the place to extend.
|
|
261
|
+
*
|
|
262
|
+
* Mutates `merged` in place to match the shallow-clone pattern in
|
|
263
|
+
* `mergeFlueAdditions`.
|
|
264
|
+
*/
|
|
265
|
+
function stripNoisyWranglerDefaults(merged) {
|
|
266
|
+
if ("unsafe" in merged && typeof merged.unsafe === "object" && merged.unsafe !== null && !Array.isArray(merged.unsafe) && Object.keys(merged.unsafe).length === 0) delete merged.unsafe;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
156
269
|
* Return the list of `class_name`s declared in the user's wrangler
|
|
157
270
|
* `durable_objects.bindings` that contain the literal substring `Sandbox`
|
|
158
271
|
* (case-sensitive).
|
|
@@ -283,6 +396,9 @@ var CloudflarePlugin = class {
|
|
|
283
396
|
}`;
|
|
284
397
|
}).join("\n\n");
|
|
285
398
|
const { config: userConfig } = await this.getUserConfig(ctx.outputDir);
|
|
399
|
+
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
400
|
+
const sandboxReExports = sandboxClassNames.map((name) => `export { Sandbox as ${name} } from '@cloudflare/sandbox';`).join("\n");
|
|
401
|
+
const sandboxClassImport = sandboxClassNames.length > 0 ? `import { Sandbox as __FlueCfSandbox } from '@cloudflare/sandbox';` : "";
|
|
286
402
|
return `
|
|
287
403
|
// Auto-generated by @flue/sdk build (cloudflare)
|
|
288
404
|
import { Agent, routeAgentRequest } from 'agents';
|
|
@@ -294,7 +410,7 @@ import {
|
|
|
294
410
|
resolveModel,
|
|
295
411
|
} from '@flue/sdk/internal';
|
|
296
412
|
import { runWithCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
|
|
297
|
-
|
|
413
|
+
${sandboxClassImport ? "\n" + sandboxClassImport : ""}
|
|
298
414
|
${agentImports}
|
|
299
415
|
|
|
300
416
|
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
@@ -338,19 +454,19 @@ async function createLocalEnv() {
|
|
|
338
454
|
|
|
339
455
|
/**
|
|
340
456
|
* Detect and wrap external sandbox instances (e.g. from @cloudflare/sandbox's
|
|
341
|
-
* getSandbox()). Returns SessionEnv if the
|
|
342
|
-
*
|
|
457
|
+
* getSandbox()). Returns SessionEnv if the value is a @cloudflare/sandbox
|
|
458
|
+
* RPC stub, null otherwise.
|
|
459
|
+
*
|
|
460
|
+
* NOTE: We must use \`instanceof\` here, not structural duck-typing. The value
|
|
461
|
+
* returned by \`getSandbox()\` is a workerd RPC Proxy that returns \`true\` for
|
|
462
|
+
* any \`in\` check and \`'function'\` for \`typeof <anything>\`, so structural
|
|
463
|
+
* checks (positive or negative) are unreliable against it. \`instanceof\` walks
|
|
464
|
+
* the prototype chain via the runtime, which the proxy can't fake.
|
|
343
465
|
*/
|
|
344
466
|
function resolveSandbox(sandbox) {
|
|
345
|
-
if (
|
|
346
|
-
sandbox && typeof sandbox === 'object' &&
|
|
347
|
-
typeof sandbox.exec === 'function' &&
|
|
348
|
-
typeof sandbox.readFile === 'function' &&
|
|
349
|
-
typeof sandbox.destroy === 'function' &&
|
|
350
|
-
!('getCwd' in sandbox) && !('fs' in sandbox)
|
|
351
|
-
) {
|
|
467
|
+
${sandboxClassNames.length > 0 ? `if (sandbox instanceof __FlueCfSandbox) {
|
|
352
468
|
return cfSandboxToSessionEnv(sandbox);
|
|
353
|
-
}
|
|
469
|
+
}` : "/* no @cloudflare/sandbox bindings declared in wrangler config */"}
|
|
354
470
|
return null;
|
|
355
471
|
}
|
|
356
472
|
|
|
@@ -580,7 +696,7 @@ ${agentClasses}
|
|
|
580
696
|
// \`@cloudflare/sandbox\` so each user-chosen class_name resolves at the
|
|
581
697
|
// bundle's top level. The binding + container image configuration is owned
|
|
582
698
|
// by the user's wrangler.jsonc.
|
|
583
|
-
${
|
|
699
|
+
${sandboxReExports}
|
|
584
700
|
|
|
585
701
|
// ─── Worker Fetch Handler ───────────────────────────────────────────────────
|
|
586
702
|
|
|
@@ -619,24 +735,23 @@ export default {
|
|
|
619
735
|
name: agentClassName(a.name)
|
|
620
736
|
}));
|
|
621
737
|
const flueSqliteClasses = flueBindings.map((b) => b.class_name);
|
|
738
|
+
const { config: userConfig, path: userConfigPath } = await this.getUserConfig(ctx.outputDir);
|
|
739
|
+
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
740
|
+
validateUserWranglerConfig(userConfig);
|
|
741
|
+
const flueMigrations = computeFlueMigrations(flueSqliteClasses, userConfig.migrations);
|
|
622
742
|
const additions = {
|
|
623
743
|
defaultName: path.basename(ctx.outputDir) || "flue-agents",
|
|
624
744
|
main: "_entry.ts",
|
|
625
745
|
doBindings: flueBindings,
|
|
626
|
-
|
|
627
|
-
tag: "flue-v1",
|
|
628
|
-
new_sqlite_classes: flueSqliteClasses
|
|
629
|
-
}
|
|
746
|
+
migrations: flueMigrations
|
|
630
747
|
};
|
|
631
|
-
const { config: userConfig, path: userConfigPath } = await this.getUserConfig(ctx.outputDir);
|
|
632
|
-
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
633
|
-
validateUserWranglerConfig(userConfig);
|
|
634
748
|
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
635
749
|
if (sandboxClassNames.length > 0) {
|
|
636
750
|
assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
|
|
637
751
|
for (const className of sandboxClassNames) console.log(`[flue] Detected Sandbox-named DO binding "${className}" — re-exporting from @cloudflare/sandbox.`);
|
|
638
752
|
}
|
|
639
753
|
const merged = mergeFlueAdditions(userConfig, additions);
|
|
754
|
+
stripNoisyWranglerDefaults(merged);
|
|
640
755
|
if (typeof merged.$schema !== "string") merged.$schema = "https://workers.cloudflare.com/schema/wrangler.json";
|
|
641
756
|
outputs["wrangler.jsonc"] = JSON.stringify(merged, null, 2);
|
|
642
757
|
writeDeployRedirectIfMissing(ctx.outputDir);
|
|
@@ -1155,6 +1270,8 @@ async function dev(options) {
|
|
|
1155
1270
|
const workspaceDir = path.resolve(options.workspaceDir);
|
|
1156
1271
|
const outputDir = path.resolve(options.outputDir);
|
|
1157
1272
|
const port = options.port ?? DEFAULT_DEV_PORT;
|
|
1273
|
+
const envFiles = resolveEnvFiles(options.envFiles, outputDir);
|
|
1274
|
+
for (const f of envFiles) console.error(`[flue] Loading env from: ${f}`);
|
|
1158
1275
|
const buildOptions = {
|
|
1159
1276
|
workspaceDir,
|
|
1160
1277
|
outputDir,
|
|
@@ -1172,10 +1289,12 @@ async function dev(options) {
|
|
|
1172
1289
|
console.error(`[flue] Built in ${Date.now() - initialStart}ms`);
|
|
1173
1290
|
const reloader = options.target === "node" ? new NodeReloader({
|
|
1174
1291
|
outputDir,
|
|
1175
|
-
port
|
|
1292
|
+
port,
|
|
1293
|
+
envFiles
|
|
1176
1294
|
}) : await createCloudflareReloader({
|
|
1177
1295
|
outputDir,
|
|
1178
|
-
port
|
|
1296
|
+
port,
|
|
1297
|
+
envFiles
|
|
1179
1298
|
});
|
|
1180
1299
|
await reloader.start();
|
|
1181
1300
|
if (reloader.url) {
|
|
@@ -1188,14 +1307,17 @@ async function dev(options) {
|
|
|
1188
1307
|
}
|
|
1189
1308
|
console.error(`[flue] Press Ctrl+C to stop\n`);
|
|
1190
1309
|
const rebuilder = createRebuilder(buildOptions, reloader);
|
|
1310
|
+
const envFileSet = new Set(envFiles);
|
|
1191
1311
|
const watcher = createWatcher({
|
|
1192
1312
|
workspaceDir,
|
|
1193
1313
|
outputDir,
|
|
1194
1314
|
target: options.target,
|
|
1315
|
+
envFiles,
|
|
1195
1316
|
onChange: (relPath) => {
|
|
1196
1317
|
if (!reloader.shouldRebuildOn(relPath)) return;
|
|
1318
|
+
const isEnvFile = envFileSet.has(relPath);
|
|
1197
1319
|
console.error(`[flue] Change detected: ${relPath}`);
|
|
1198
|
-
rebuilder.schedule();
|
|
1320
|
+
rebuilder.schedule(isEnvFile);
|
|
1199
1321
|
}
|
|
1200
1322
|
});
|
|
1201
1323
|
let shuttingDown = false;
|
|
@@ -1224,31 +1346,40 @@ async function dev(options) {
|
|
|
1224
1346
|
function createRebuilder(buildOptions, reloader) {
|
|
1225
1347
|
let running = false;
|
|
1226
1348
|
let queued = false;
|
|
1349
|
+
let queuedForce = false;
|
|
1350
|
+
let pendingForce = false;
|
|
1227
1351
|
let debounceTimer = null;
|
|
1228
|
-
const runOnce = async () => {
|
|
1352
|
+
const runOnce = async (force) => {
|
|
1229
1353
|
running = true;
|
|
1230
1354
|
const start = Date.now();
|
|
1231
1355
|
console.error(`[flue] Rebuilding...`);
|
|
1232
1356
|
try {
|
|
1233
1357
|
const { changed } = await build(buildOptions);
|
|
1234
|
-
await reloader.reload(changed);
|
|
1358
|
+
await reloader.reload(changed || force);
|
|
1235
1359
|
console.error(`[flue] Reloaded in ${Date.now() - start}ms\n`);
|
|
1236
1360
|
} catch (err) {
|
|
1237
1361
|
console.error(`[flue] Rebuild failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1238
1362
|
} finally {
|
|
1239
1363
|
running = false;
|
|
1240
1364
|
if (queued) {
|
|
1365
|
+
const nextForce = queuedForce;
|
|
1241
1366
|
queued = false;
|
|
1242
|
-
|
|
1367
|
+
queuedForce = false;
|
|
1368
|
+
runOnce(nextForce);
|
|
1243
1369
|
}
|
|
1244
1370
|
}
|
|
1245
1371
|
};
|
|
1246
|
-
return { schedule() {
|
|
1372
|
+
return { schedule(forceReload = false) {
|
|
1373
|
+
if (forceReload) pendingForce = true;
|
|
1247
1374
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1248
1375
|
debounceTimer = setTimeout(() => {
|
|
1249
1376
|
debounceTimer = null;
|
|
1250
|
-
|
|
1251
|
-
|
|
1377
|
+
const force = pendingForce;
|
|
1378
|
+
pendingForce = false;
|
|
1379
|
+
if (running) {
|
|
1380
|
+
queued = true;
|
|
1381
|
+
if (force) queuedForce = true;
|
|
1382
|
+
} else runOnce(force);
|
|
1252
1383
|
}, 150);
|
|
1253
1384
|
} };
|
|
1254
1385
|
}
|
|
@@ -1267,7 +1398,7 @@ function createRebuilder(buildOptions, reloader) {
|
|
|
1267
1398
|
* - editor backup/swap suffixes
|
|
1268
1399
|
*/
|
|
1269
1400
|
function createWatcher(options) {
|
|
1270
|
-
const { workspaceDir, outputDir, target, onChange } = options;
|
|
1401
|
+
const { workspaceDir, outputDir, target, envFiles, onChange } = options;
|
|
1271
1402
|
const watchers = [];
|
|
1272
1403
|
const isIgnoredPath = (relPath) => {
|
|
1273
1404
|
const parts = relPath.replace(/\\/g, "/").split("/");
|
|
@@ -1307,6 +1438,10 @@ function createWatcher(options) {
|
|
|
1307
1438
|
watchers.push(w);
|
|
1308
1439
|
} catch {}
|
|
1309
1440
|
}
|
|
1441
|
+
for (const envPath of envFiles) try {
|
|
1442
|
+
const w = fs.watch(envPath, () => onChange(envPath));
|
|
1443
|
+
watchers.push(w);
|
|
1444
|
+
} catch {}
|
|
1310
1445
|
return { close() {
|
|
1311
1446
|
for (const w of watchers) try {
|
|
1312
1447
|
w.close();
|
|
@@ -1318,10 +1453,12 @@ var NodeReloader = class {
|
|
|
1318
1453
|
serverPath;
|
|
1319
1454
|
outputDir;
|
|
1320
1455
|
port;
|
|
1456
|
+
envFiles;
|
|
1321
1457
|
url;
|
|
1322
1458
|
constructor(opts) {
|
|
1323
1459
|
this.outputDir = opts.outputDir;
|
|
1324
1460
|
this.port = opts.port;
|
|
1461
|
+
this.envFiles = opts.envFiles;
|
|
1325
1462
|
this.serverPath = path.join(this.outputDir, "dist", "server.mjs");
|
|
1326
1463
|
this.url = `http://localhost:${this.port}`;
|
|
1327
1464
|
}
|
|
@@ -1346,6 +1483,7 @@ var NodeReloader = class {
|
|
|
1346
1483
|
} catch {}
|
|
1347
1484
|
}
|
|
1348
1485
|
async spawnAndWait() {
|
|
1486
|
+
const fromFiles = parseEnvFiles(this.envFiles);
|
|
1349
1487
|
const child = spawn("node", [this.serverPath], {
|
|
1350
1488
|
stdio: [
|
|
1351
1489
|
"ignore",
|
|
@@ -1354,6 +1492,7 @@ var NodeReloader = class {
|
|
|
1354
1492
|
],
|
|
1355
1493
|
cwd: this.outputDir,
|
|
1356
1494
|
env: {
|
|
1495
|
+
...fromFiles,
|
|
1357
1496
|
...process.env,
|
|
1358
1497
|
PORT: String(this.port),
|
|
1359
1498
|
FLUE_MODE: "local"
|
|
@@ -1436,11 +1575,13 @@ var CloudflareReloader = class {
|
|
|
1436
1575
|
outputDir;
|
|
1437
1576
|
port;
|
|
1438
1577
|
configPath;
|
|
1578
|
+
envFiles;
|
|
1439
1579
|
url;
|
|
1440
1580
|
constructor(wrangler, opts) {
|
|
1441
1581
|
this.wrangler = wrangler;
|
|
1442
1582
|
this.outputDir = opts.outputDir;
|
|
1443
1583
|
this.port = opts.port;
|
|
1584
|
+
this.envFiles = opts.envFiles;
|
|
1444
1585
|
this.configPath = path.join(this.outputDir, "dist", "wrangler.jsonc");
|
|
1445
1586
|
}
|
|
1446
1587
|
async start() {
|
|
@@ -1467,6 +1608,7 @@ var CloudflareReloader = class {
|
|
|
1467
1608
|
* baked into the entry).
|
|
1468
1609
|
*/
|
|
1469
1610
|
shouldRebuildOn(relPath) {
|
|
1611
|
+
if (this.envFiles.includes(relPath)) return true;
|
|
1470
1612
|
const normalized = relPath.replace(/\\/g, "/");
|
|
1471
1613
|
if (normalized === "wrangler.jsonc" || normalized === "wrangler.json" || normalized === "wrangler.toml") return true;
|
|
1472
1614
|
if (normalized.startsWith("agents/")) return true;
|
|
@@ -1491,6 +1633,7 @@ var CloudflareReloader = class {
|
|
|
1491
1633
|
if (!fs.existsSync(this.configPath)) throw new Error(`[flue] Expected ${this.configPath} after build, but it doesn't exist. Did the Cloudflare build succeed?`);
|
|
1492
1634
|
this.worker = await this.wrangler.unstable_startWorker({
|
|
1493
1635
|
config: this.configPath,
|
|
1636
|
+
envFiles: this.envFiles,
|
|
1494
1637
|
build: { nodejsCompatMode: "v2" },
|
|
1495
1638
|
dev: {
|
|
1496
1639
|
server: {
|
|
@@ -1518,6 +1661,40 @@ var CloudflareReloader = class {
|
|
|
1518
1661
|
}
|
|
1519
1662
|
}
|
|
1520
1663
|
};
|
|
1664
|
+
/**
|
|
1665
|
+
* Resolve and validate a list of env-file paths. Returns absolute paths.
|
|
1666
|
+
*
|
|
1667
|
+
* Throws a friendly `[flue]`-prefixed error if any path doesn't exist. The
|
|
1668
|
+
* goal of `--env` is explicitness — silent skip on a typo would defeat
|
|
1669
|
+
* the purpose.
|
|
1670
|
+
*/
|
|
1671
|
+
function resolveEnvFiles(envFiles, cwd) {
|
|
1672
|
+
if (!envFiles || envFiles.length === 0) return [];
|
|
1673
|
+
return envFiles.map((p) => {
|
|
1674
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
1675
|
+
if (!fs.existsSync(abs)) throw new Error(`[flue] --env points at a path that doesn't exist: ${p}`);
|
|
1676
|
+
return abs;
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Parse one or more `.env`-format files and return their merged contents.
|
|
1681
|
+
* Later files override earlier files on key collision.
|
|
1682
|
+
*
|
|
1683
|
+
* Uses Node's built-in `util.parseEnv` (Node 20.6+; Flue requires Node 22+).
|
|
1684
|
+
* No `dotenv` package needed.
|
|
1685
|
+
*
|
|
1686
|
+
* Parse-only — doesn't touch `process.env`. Caller composes with
|
|
1687
|
+
* `process.env` as needed (typical pattern: spread file vars first, then
|
|
1688
|
+
* `process.env`, so shell-set values win).
|
|
1689
|
+
*/
|
|
1690
|
+
function parseEnvFiles(absolutePaths) {
|
|
1691
|
+
const merged = {};
|
|
1692
|
+
for (const p of absolutePaths) {
|
|
1693
|
+
const parsed = parseEnv(fs.readFileSync(p, "utf-8"));
|
|
1694
|
+
Object.assign(merged, parsed);
|
|
1695
|
+
}
|
|
1696
|
+
return merged;
|
|
1697
|
+
}
|
|
1521
1698
|
async function waitForHealth(baseUrl, timeoutMs) {
|
|
1522
1699
|
const start = Date.now();
|
|
1523
1700
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -1565,4 +1742,4 @@ function pickExampleAgentName(outputDir, workspaceDir) {
|
|
|
1565
1742
|
}
|
|
1566
1743
|
|
|
1567
1744
|
//#endregion
|
|
1568
|
-
export { BUILTIN_TOOL_NAMES, DEFAULT_DEV_PORT, build, createTools, dev, resolveWorkspaceFromCwd };
|
|
1745
|
+
export { BUILTIN_TOOL_NAMES, DEFAULT_DEV_PORT, build, createTools, dev, parseEnvFiles, resolveEnvFiles, resolveWorkspaceFromCwd };
|