alchemy 0.60.2 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alchemy.js +77 -37
- package/bin/commands/init.ts +65 -23
- package/bin/services/execute-alchemy.ts +33 -23
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +1 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +8 -4
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-migrations.js +3 -2
- package/lib/cloudflare/d1-migrations.js.map +1 -1
- package/lib/cloudflare/index.d.ts +2 -1
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -1
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +16 -3
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +3 -0
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/quick-tunnel.d.ts +17 -0
- package/lib/cloudflare/quick-tunnel.d.ts.map +1 -0
- package/lib/cloudflare/quick-tunnel.js +26 -0
- package/lib/cloudflare/quick-tunnel.js.map +1 -0
- package/lib/cloudflare/sveltekit/plugin.d.ts +2 -2
- package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.js +5 -4
- package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +2 -1
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/scope.d.ts +36 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +20 -0
- package/lib/scope.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts +22 -0
- package/lib/util/idempotent-spawn.d.ts.map +1 -0
- package/lib/util/idempotent-spawn.js +218 -0
- package/lib/util/idempotent-spawn.js.map +1 -0
- package/package.json +2 -1
- package/src/alchemy.ts +1 -0
- package/src/cloudflare/bucket.ts +14 -6
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-migrations.ts +4 -5
- package/src/cloudflare/index.ts +2 -1
- package/src/cloudflare/miniflare/build-worker-options.ts +16 -3
- package/src/cloudflare/miniflare/miniflare-controller.ts +4 -0
- package/src/cloudflare/quick-tunnel.ts +30 -0
- package/src/cloudflare/sveltekit/plugin.ts +5 -5
- package/src/cloudflare/worker-bundle.ts +2 -1
- package/src/scope.ts +61 -0
- package/src/util/idempotent-spawn.ts +286 -0
- package/templates/rwsdk/package.json +1 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import find from "find-process";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import fsp from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Idempotently ensure a long-lived child is running with stdout/stderr -> files,
|
|
9
|
+
* and mirror those logs to THIS process's console with persisted offsets.
|
|
10
|
+
*
|
|
11
|
+
* Use: await ensureLoggedChildAndMirror({ cmd: "vite dev" })
|
|
12
|
+
*/
|
|
13
|
+
export async function idempotentSpawn({
|
|
14
|
+
cmd,
|
|
15
|
+
cwd,
|
|
16
|
+
stateFile = "state.json",
|
|
17
|
+
overlapBytes = 0,
|
|
18
|
+
resume = false,
|
|
19
|
+
log = "log.txt",
|
|
20
|
+
processName,
|
|
21
|
+
extract,
|
|
22
|
+
isSameProcess,
|
|
23
|
+
quiet = false,
|
|
24
|
+
}: {
|
|
25
|
+
cmd: string;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
log: string;
|
|
28
|
+
stateFile?: string;
|
|
29
|
+
overlapBytes?: number;
|
|
30
|
+
resume?: boolean;
|
|
31
|
+
isSameProcess?: (pid: number) => Promise<boolean>;
|
|
32
|
+
processName?: string;
|
|
33
|
+
extract?: (line: string) => string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* If true, the child's stdout and stderr will not be mirrored to this process's console.
|
|
36
|
+
*/
|
|
37
|
+
quiet?: boolean;
|
|
38
|
+
}): Promise<string | undefined> {
|
|
39
|
+
if (!processName && !isSameProcess) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Either processName or isSameProcess must be provided to resume a process",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const outPath = log;
|
|
46
|
+
await Promise.all([
|
|
47
|
+
fsp.mkdir(path.dirname(stateFile), { recursive: true }),
|
|
48
|
+
fsp.mkdir(path.dirname(log), { recursive: true }),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const { promise: extracted, resolve: resolveExtracted } =
|
|
52
|
+
Promise.withResolvers<string | undefined>();
|
|
53
|
+
|
|
54
|
+
if (!extract) {
|
|
55
|
+
console.warn(
|
|
56
|
+
"No extract function provided, will not return a value. This is probably a bug.",
|
|
57
|
+
);
|
|
58
|
+
resolveExtracted(undefined);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ------------------------ helpers ------------------------
|
|
62
|
+
|
|
63
|
+
function isPidAlive(pid: number) {
|
|
64
|
+
if (!pid || Number.isNaN(pid)) return false;
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, 0);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function writeJsonAtomic(file: string, data: any) {
|
|
74
|
+
const dir = path.dirname(file);
|
|
75
|
+
const tmp = path.join(
|
|
76
|
+
dir,
|
|
77
|
+
`.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`,
|
|
78
|
+
);
|
|
79
|
+
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
80
|
+
await fsp.rename(tmp, file); // atomic on POSIX
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function readJson(file: string) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(await fsp.readFile(file, "utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function spawnLoggedChild() {
|
|
92
|
+
const out = await fsp.open(outPath, "a");
|
|
93
|
+
|
|
94
|
+
// shell:true, NO args — pass a single command string
|
|
95
|
+
const child = spawn(cmd, {
|
|
96
|
+
shell: true,
|
|
97
|
+
cwd,
|
|
98
|
+
stdio: ["ignore", out.fd, out.fd], // stdout/stderr -> files (OS-level)
|
|
99
|
+
env: process.env,
|
|
100
|
+
detached: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Child now owns dup'd fds; close our handles.
|
|
104
|
+
await out.close();
|
|
105
|
+
|
|
106
|
+
// Persist PID into the shared state file
|
|
107
|
+
const stateAll = (await readJson(stateFile)) ?? {};
|
|
108
|
+
stateAll.pid = child.pid;
|
|
109
|
+
await writeJsonAtomic(stateFile, stateAll);
|
|
110
|
+
return child;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function ensureChildRunning() {
|
|
114
|
+
const stateAll = await readJson(stateFile);
|
|
115
|
+
if (stateAll) {
|
|
116
|
+
const pid = Number.parseInt(stateAll.pid, 10);
|
|
117
|
+
if (isPidAlive(pid)) return pid;
|
|
118
|
+
if (await isSameProcess?.(pid)) return pid;
|
|
119
|
+
if (processName) {
|
|
120
|
+
const processes = await find("pid", pid);
|
|
121
|
+
if (processes.length > 1) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`Found multiple processes with PID ${pid}, using the first one`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (processes.length > 0) {
|
|
127
|
+
return processes[0].name.startsWith(processName);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// not running, let's clear pid and state
|
|
132
|
+
await Promise.all([
|
|
133
|
+
fsp.rm(stateFile).catch(() => {}),
|
|
134
|
+
fsp.rm(log).catch(() => {}),
|
|
135
|
+
]);
|
|
136
|
+
return (await spawnLoggedChild()).pid;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Follow a file from persisted offset and mirror to a sink (stdout/stderr)
|
|
140
|
+
async function followFilePersisted(
|
|
141
|
+
logPath: string,
|
|
142
|
+
{
|
|
143
|
+
write = (buf: Buffer) => process.stdout.write(buf),
|
|
144
|
+
chunkSize = 64 * 1024,
|
|
145
|
+
tickMs = 100,
|
|
146
|
+
}: {
|
|
147
|
+
stateKey: string;
|
|
148
|
+
write: (buf: Buffer) => boolean;
|
|
149
|
+
chunkSize?: number;
|
|
150
|
+
tickMs?: number;
|
|
151
|
+
},
|
|
152
|
+
) {
|
|
153
|
+
logPath = path.resolve(logPath);
|
|
154
|
+
let state = (await readJson(stateFile)) ?? {};
|
|
155
|
+
|
|
156
|
+
let fh = await fsp.open(logPath, "r");
|
|
157
|
+
let st = await fh.stat();
|
|
158
|
+
let ino = Number(st.ino ?? 0);
|
|
159
|
+
let dev = Number(st.dev ?? 0);
|
|
160
|
+
|
|
161
|
+
// Resume policy: if resume is enabled and same file, start at saved offset (with overlap); else start from end.
|
|
162
|
+
let offset =
|
|
163
|
+
resume &&
|
|
164
|
+
state.ino === ino &&
|
|
165
|
+
state.dev === dev &&
|
|
166
|
+
typeof state.offset === "number"
|
|
167
|
+
? Math.max(0, Math.min(st.size, state.offset - overlapBytes))
|
|
168
|
+
: st.size;
|
|
169
|
+
|
|
170
|
+
let closed = false;
|
|
171
|
+
|
|
172
|
+
async function persist() {
|
|
173
|
+
try {
|
|
174
|
+
const cur = await fh.stat();
|
|
175
|
+
state = {
|
|
176
|
+
...state,
|
|
177
|
+
offset,
|
|
178
|
+
ino: Number(cur.ino ?? ino),
|
|
179
|
+
dev: Number(cur.dev ?? dev),
|
|
180
|
+
size: cur.size,
|
|
181
|
+
mtimeMs: cur.mtimeMs,
|
|
182
|
+
};
|
|
183
|
+
await writeJsonAtomic(stateFile, state);
|
|
184
|
+
} catch {
|
|
185
|
+
// If the file vanished mid-rotation, we'll catch up on the next event.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function drain() {
|
|
190
|
+
while (true) {
|
|
191
|
+
const cur = await fh.stat().catch(() => null);
|
|
192
|
+
if (!cur) break;
|
|
193
|
+
|
|
194
|
+
if (cur.size < offset) offset = 0; // truncated
|
|
195
|
+
|
|
196
|
+
if (offset > 0 && extract) {
|
|
197
|
+
// read from start to offset
|
|
198
|
+
const fullBuffer = Buffer.allocUnsafe(offset);
|
|
199
|
+
await fh.read({
|
|
200
|
+
position: 0,
|
|
201
|
+
buffer: fullBuffer,
|
|
202
|
+
length: offset,
|
|
203
|
+
});
|
|
204
|
+
const content = fullBuffer.toString("utf8");
|
|
205
|
+
const lines = content.split("\n");
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
const extracted = extract(line);
|
|
208
|
+
if (extracted) {
|
|
209
|
+
resolveExtracted(extracted);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const toRead = Math.min(chunkSize, cur.size - offset);
|
|
216
|
+
if (toRead <= 0) break;
|
|
217
|
+
|
|
218
|
+
const { bytesRead, buffer } = await fh.read({
|
|
219
|
+
position: offset,
|
|
220
|
+
buffer: Buffer.allocUnsafe(toRead),
|
|
221
|
+
length: toRead,
|
|
222
|
+
});
|
|
223
|
+
if (!bytesRead) break;
|
|
224
|
+
|
|
225
|
+
offset += bytesRead;
|
|
226
|
+
write(buffer.subarray(0, bytesRead));
|
|
227
|
+
}
|
|
228
|
+
await persist();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await drain();
|
|
232
|
+
|
|
233
|
+
const watcher = fs.watch(logPath, async () => {
|
|
234
|
+
if (closed) return;
|
|
235
|
+
try {
|
|
236
|
+
const cur = await fh.stat();
|
|
237
|
+
const curIno = Number(cur.ino ?? 0);
|
|
238
|
+
const curDev = Number(cur.dev ?? 0);
|
|
239
|
+
if (curIno !== ino || curDev !== dev) {
|
|
240
|
+
// Rotated/replaced: reopen and start at beginning of new file
|
|
241
|
+
await fh.close().catch(() => {});
|
|
242
|
+
fh = await fsp.open(logPath, "r");
|
|
243
|
+
const cur2 = await fh.stat();
|
|
244
|
+
ino = Number(cur2.ino ?? 0);
|
|
245
|
+
dev = Number(cur2.dev ?? 0);
|
|
246
|
+
offset = 0;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// File might briefly disappear during rotation
|
|
250
|
+
}
|
|
251
|
+
await drain();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// TODO(sam): do we need this?
|
|
255
|
+
// const tick = setInterval(() => {
|
|
256
|
+
// drain().catch(() => {});
|
|
257
|
+
// }, tickMs);
|
|
258
|
+
|
|
259
|
+
return async function stop() {
|
|
260
|
+
closed = true;
|
|
261
|
+
watcher.close();
|
|
262
|
+
// clearInterval(tick);
|
|
263
|
+
await fh.close().catch(() => {});
|
|
264
|
+
await persist();
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ------------------------ main flow ------------------------
|
|
269
|
+
|
|
270
|
+
await ensureChildRunning();
|
|
271
|
+
|
|
272
|
+
if (!quiet) {
|
|
273
|
+
const write = quiet
|
|
274
|
+
? () => false
|
|
275
|
+
: (buf: Buffer) => process.stdout.write(buf);
|
|
276
|
+
|
|
277
|
+
// Start followers (stdout/stderr) and mirror to this process
|
|
278
|
+
await followFilePersisted(outPath, {
|
|
279
|
+
stateKey: `${path.resolve(outPath)}::stdout`,
|
|
280
|
+
write,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Return a stopper for this instance's mirroring (child keeps running)
|
|
285
|
+
return extracted;
|
|
286
|
+
}
|