kizlo 0.28.0 → 0.30.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/dist/adapters/geo.cloudflare.d.ts +1 -1
- package/dist/adapters/geo.cloudflare.js +1 -1
- package/dist/adapters/request-context.d.ts +12 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/commands/_setup.js +1 -1
- package/dist/cli/commands/dev.js +82 -37
- package/dist/cli/daemon/lock.js +62 -19
- package/dist/cli/daemon/watch.js +1 -1
- package/dist/cli/process-owner.js +27 -25
- package/dist/cli/wp/compose/export.php +82 -0
- package/dist/cli/wp/docker.d.ts +6 -2
- package/dist/cli/wp/docker.js +12 -5
- package/dist/cli/wp/export.js +355 -0
- package/dist/cli/wp/session.js +33 -10
- package/dist/cli/wp/watchdog.js +18 -19
- package/dist/context.d.ts +13 -30
- package/dist/context.js +15 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/integrations/nextjs/api.d.ts +13 -0
- package/dist/integrations/nextjs/api.js +17 -0
- package/dist/integrations/tanstack-start/react/cloudflare/cache-policy.js +15 -0
- package/dist/integrations/tanstack-start/react/cloudflare/cache.d.ts +34 -0
- package/dist/integrations/tanstack-start/react/cloudflare/cache.js +82 -0
- package/dist/integrations/tanstack-start/react/cookies.js +20 -0
- package/dist/integrations/tanstack-start/react/integration.d.ts +25 -0
- package/dist/integrations/tanstack-start/react/integration.js +18 -0
- package/dist/integrations/tanstack-start/react/middleware.d.ts +9 -0
- package/dist/integrations/tanstack-start/react/middleware.js +32 -0
- package/dist/integrations/tanstack-start/react/request-context.d.ts +8 -0
- package/dist/integrations/tanstack-start/react/request-context.js +24 -0
- package/dist/kizlo.d.ts +5 -4
- package/dist/kizlo.js +27 -16
- package/dist/nextjs/server.d.ts +2 -1
- package/dist/nextjs/server.js +2 -1
- package/dist/package.js +1 -1
- package/dist/page/schema.d.ts +3 -3
- package/dist/post/schema.d.ts +7 -7
- package/dist/procedures.d.ts +8 -8
- package/dist/seo/schema.d.ts +5 -5
- package/dist/tanstack-start/react/cloudflare.d.ts +27 -0
- package/dist/tanstack-start/react/cloudflare.js +33 -0
- package/dist/tanstack-start/react/server.d.ts +14 -0
- package/dist/tanstack-start/react/server.js +13 -0
- package/dist/test.d.ts +2 -2
- package/dist/test.js +2 -2
- package/dist/wordpress/endpoint.d.ts +3 -4
- package/dist/wordpress/index.d.ts +1 -1
- package/package.json +16 -20
- package/dist/cloudflare/vinext.d.ts +0 -7
- package/dist/cloudflare/vinext.js +0 -44
- package/dist/cloudflare.d.ts +0 -3
- package/dist/cloudflare.js +0 -4
- package/dist/integrations/cloudflare/integration.d.ts +0 -8
- package/dist/integrations/cloudflare/integration.js +0 -16
- package/dist/integrations/cloudflare/request-context.d.ts +0 -9
- package/dist/integrations/cloudflare/request-context.js +0 -58
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { createStack, docker, dockerHint, dockerStatus, runningProjectContainers } from "./docker.js";
|
|
2
|
+
import { ensureGitignored } from "../utils.js";
|
|
3
|
+
import { ownerStatus } from "../process-owner.js";
|
|
4
|
+
import { acquire, operationLock, releaseOwned } from "../daemon/lock.js";
|
|
5
|
+
import { projectSession } from "./session.js";
|
|
6
|
+
import { resolveDevConfig, usesLocalWordPress } from "../daemon/config.js";
|
|
7
|
+
import { devStack } from "./stack.js";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { chmodSync, existsSync, linkSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { setTimeout } from "node:timers/promises";
|
|
13
|
+
|
|
14
|
+
//#region src/cli/wp/export.ts
|
|
15
|
+
const ARCHIVE_SCRIPT = fileURLToPath(new URL("./compose/export.php", import.meta.url));
|
|
16
|
+
const COMMAND_TIMEOUT = 15 * 6e4;
|
|
17
|
+
function checked(result, action) {
|
|
18
|
+
if (result.code !== 0) throw new Error(`${action} failed: ${result.stderr || result.stdout || "command interrupted or timed out"}`);
|
|
19
|
+
return result.stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
async function endDevSession(project, file, session = projectSession(project)) {
|
|
22
|
+
if (!session) return;
|
|
23
|
+
if (!("token" in session) || !session.token || session.operationFile !== file) {
|
|
24
|
+
if (await ownerStatus(session) === "dead") return;
|
|
25
|
+
throw new Error("This dev session predates export coordination. Stop it and restart `kizlo dev` with the updated CLI before exporting.");
|
|
26
|
+
}
|
|
27
|
+
if (session.pid === process.pid) throw new Error("Cannot export from inside the foreground dev owner. Run `kizlo dev --export` in another terminal.");
|
|
28
|
+
const deadline = Date.now() + 1e4;
|
|
29
|
+
let signalled = false;
|
|
30
|
+
while (Date.now() < deadline) {
|
|
31
|
+
const status = await ownerStatus(session);
|
|
32
|
+
if (status === "dead") return;
|
|
33
|
+
if (status === "alive" && !signalled) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(session.pid, "SIGTERM");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === "ESRCH") return;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
signalled = true;
|
|
41
|
+
}
|
|
42
|
+
await setTimeout(200);
|
|
43
|
+
}
|
|
44
|
+
throw new Error("Could not confirm dev session shutdown within 10 seconds. Export was cancelled.");
|
|
45
|
+
}
|
|
46
|
+
function restoreReadme(metadata) {
|
|
47
|
+
return `Kizlo WordPress export
|
|
48
|
+
|
|
49
|
+
This archive contains private site data and user credentials. Store it securely.
|
|
50
|
+
Source WordPress: ${metadata.wordpress}
|
|
51
|
+
Source database: ${metadata.database}
|
|
52
|
+
Source home URL: ${metadata.home}
|
|
53
|
+
Source site URL: ${metadata.siteurl}
|
|
54
|
+
Table prefix: ${metadata.prefix}
|
|
55
|
+
Source collations: ${metadata.collations}
|
|
56
|
+
|
|
57
|
+
Manual restore
|
|
58
|
+
1. Back up the destination database and files. Install WordPress core separately,
|
|
59
|
+
with compatible PHP and plugins. This archive supplies only wp-content and SQL.
|
|
60
|
+
2. Extract the outer ZIP and copy wp-content to the destination WordPress install.
|
|
61
|
+
3. Keep database.sql.zip compressed. In phpMyAdmin, select the intended destination
|
|
62
|
+
database (its name may differ from the source), open Import, choose
|
|
63
|
+
database.sql.zip, select SQL format, and import. Do not upload the outer ZIP.
|
|
64
|
+
Import replaces matching tables. Use an account scoped to the destination DB.
|
|
65
|
+
4. Keep the destination's own wp-config.php, database connection, salts and server
|
|
66
|
+
configuration. Set its table prefix to the source prefix shown above.
|
|
67
|
+
5. Replace each distinct source URL with your destination URL using a
|
|
68
|
+
serialization-aware tool. This is a separate post-import step. For example:
|
|
69
|
+
wp search-replace 'SOURCE_URL' 'DESTINATION_URL' --all-tables --precise --dry-run
|
|
70
|
+
Check the dry run, then repeat without --dry-run. Do not edit serialized SQL
|
|
71
|
+
with text replacement. This export has not rewritten the source database.
|
|
72
|
+
6. Check admin login, uploads, active plugins and representative site content.
|
|
73
|
+
|
|
74
|
+
Compatibility and limits
|
|
75
|
+
The SQL is a single-database MySQL dump. The inner ZIP contains only database.sql.
|
|
76
|
+
Schema and collations are preserved, so the destination must support the source
|
|
77
|
+
schema and collations above. Compressed imports through phpMyAdmin 5.2.3 have been
|
|
78
|
+
validated against MySQL 8.4.10 and MariaDB 11.8.9 with utf8mb4_0900_ai_ci and
|
|
79
|
+
utf8mb4_unicode_520_ci tables. Other versions/collations need their own check.
|
|
80
|
+
DEFINER-bearing views, triggers, routines and events
|
|
81
|
+
are rejected rather than silently omitted. A compatible SQL format cannot make an
|
|
82
|
+
older database engine accept unsupported collations or features.
|
|
83
|
+
PHP/phpMyAdmin upload_max_filesize, post_max_size and execution time limits may
|
|
84
|
+
need increasing for a large import. These are separate from database compatibility.
|
|
85
|
+
The PHP ZIP writer supports exports up to 4 GiB and 65,535 entries.
|
|
86
|
+
See https://docs.phpmyadmin.net/en/latest/import_export.html for import limits.
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
/** Export existing data only; provisioning and foreground startup never run on this path. */
|
|
90
|
+
async function exportDev(cwd) {
|
|
91
|
+
if (!await usesLocalWordPress(cwd)) throw new Error("Export requires an enabled local dev WordPress in kizlo.config.ts.");
|
|
92
|
+
const status = await dockerStatus();
|
|
93
|
+
if (status !== "running") throw new Error(dockerHint(status));
|
|
94
|
+
const cfg = await resolveDevConfig(cwd);
|
|
95
|
+
for (const file of [
|
|
96
|
+
"wp-config.php",
|
|
97
|
+
"wp-includes/version.php",
|
|
98
|
+
"wp-content"
|
|
99
|
+
]) if (!existsSync(join(cfg.wordpressDir, file))) throw new Error("No initialized local WordPress site found. Run `kizlo dev` before exporting.");
|
|
100
|
+
const lock = operationLock(cfg.configDir);
|
|
101
|
+
if (!await acquire(lock)) throw new Error("Another dev operation is active. Wait for it to finish and retry the export.");
|
|
102
|
+
const abort = new AbortController();
|
|
103
|
+
const cancel = () => abort.abort(/* @__PURE__ */ new Error("Export interrupted"));
|
|
104
|
+
const signals = [
|
|
105
|
+
"SIGINT",
|
|
106
|
+
"SIGTERM",
|
|
107
|
+
"SIGHUP"
|
|
108
|
+
];
|
|
109
|
+
for (const signal of signals) process.once(signal, cancel);
|
|
110
|
+
let staging;
|
|
111
|
+
let mysql;
|
|
112
|
+
let controlled = false;
|
|
113
|
+
let containerCreated = false;
|
|
114
|
+
let session;
|
|
115
|
+
const id = randomUUID();
|
|
116
|
+
const container = `${cfg.project}-export-${id}`;
|
|
117
|
+
const workspace = `/tmp/kizlo-export-${id}`;
|
|
118
|
+
const dump = `${workspace}.sql`;
|
|
119
|
+
const command = async (args, action, timeout = COMMAND_TIMEOUT) => {
|
|
120
|
+
abort.signal.throwIfAborted();
|
|
121
|
+
return checked(await docker(args, {
|
|
122
|
+
signal: abort.signal,
|
|
123
|
+
timeout
|
|
124
|
+
}), action);
|
|
125
|
+
};
|
|
126
|
+
let failure;
|
|
127
|
+
let output;
|
|
128
|
+
let published = false;
|
|
129
|
+
try {
|
|
130
|
+
const ids = await command([
|
|
131
|
+
"ps",
|
|
132
|
+
"-aq",
|
|
133
|
+
"--filter",
|
|
134
|
+
`label=com.docker.compose.project=${cfg.project}`,
|
|
135
|
+
"--filter",
|
|
136
|
+
"label=com.docker.compose.service=mysql"
|
|
137
|
+
], "Find existing database");
|
|
138
|
+
if (ids.length === 0 || ids.includes("\n")) throw new Error("No unique existing local database container found. Start and stop `kizlo dev` before exporting.");
|
|
139
|
+
mysql = ids;
|
|
140
|
+
ensureGitignored(cfg.configDir, ".kizlo/");
|
|
141
|
+
const exportsDir = join(cfg.configDir, ".kizlo/exports");
|
|
142
|
+
mkdirSync(exportsDir, {
|
|
143
|
+
recursive: true,
|
|
144
|
+
mode: 448
|
|
145
|
+
});
|
|
146
|
+
staging = mkdtempSync(join(exportsDir, ".staging-"));
|
|
147
|
+
chmodSync(staging, 448);
|
|
148
|
+
await command([
|
|
149
|
+
"cp",
|
|
150
|
+
`${mysql}:/var/lib/mysql/auto.cnf`,
|
|
151
|
+
join(staging, "initialized")
|
|
152
|
+
], "Validate initialized database volume");
|
|
153
|
+
session = projectSession(cfg.project);
|
|
154
|
+
if (session && await ownerStatus(session) !== "dead" && (!("token" in session) || session.operationFile !== lock)) throw new Error("Stop the existing dev session and restart it with the updated CLI before exporting.");
|
|
155
|
+
const stack = createStack(devStack(cfg));
|
|
156
|
+
const compose = async (args, action) => {
|
|
157
|
+
abort.signal.throwIfAborted();
|
|
158
|
+
return checked(await stack.compose(args, {
|
|
159
|
+
signal: abort.signal,
|
|
160
|
+
timeout: COMMAND_TIMEOUT
|
|
161
|
+
}), action);
|
|
162
|
+
};
|
|
163
|
+
controlled = true;
|
|
164
|
+
await compose([
|
|
165
|
+
"stop",
|
|
166
|
+
"wordpress",
|
|
167
|
+
"wp-cli"
|
|
168
|
+
], "Stop web and background WordPress requests");
|
|
169
|
+
await endDevSession(cfg.project, lock, session);
|
|
170
|
+
await command(["start", mysql], "Start existing database");
|
|
171
|
+
const sql = async (query, timeout = COMMAND_TIMEOUT) => command([
|
|
172
|
+
"exec",
|
|
173
|
+
mysql,
|
|
174
|
+
"sh",
|
|
175
|
+
"-c",
|
|
176
|
+
"export MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\"; exec mysql -uroot --batch --raw --skip-column-names --default-character-set=utf8mb4 \"$MYSQL_DATABASE\" -e \"$1\"",
|
|
177
|
+
"kizlo-export",
|
|
178
|
+
query
|
|
179
|
+
], "Read source database", timeout);
|
|
180
|
+
let ready = false;
|
|
181
|
+
const readinessDeadline = Date.now() + 3e4;
|
|
182
|
+
while (Date.now() < readinessDeadline) {
|
|
183
|
+
try {
|
|
184
|
+
await sql("SELECT 1", 1e3);
|
|
185
|
+
ready = true;
|
|
186
|
+
break;
|
|
187
|
+
} catch {
|
|
188
|
+
abort.signal.throwIfAborted();
|
|
189
|
+
}
|
|
190
|
+
await setTimeout(500, void 0, { signal: abort.signal });
|
|
191
|
+
}
|
|
192
|
+
if (!ready) throw new Error("Existing database did not become ready within 30 seconds.");
|
|
193
|
+
const objects = await sql("SELECT CONCAT('trigger: ', TRIGGER_NAME) FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA=DATABASE() UNION ALL SELECT CONCAT('routine: ', ROUTINE_NAME) FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA=DATABASE() UNION ALL SELECT CONCAT('event: ', EVENT_NAME) FROM information_schema.EVENTS WHERE EVENT_SCHEMA=DATABASE() UNION ALL SELECT CONCAT('view: ', TABLE_NAME) FROM information_schema.VIEWS WHERE TABLE_SCHEMA=DATABASE()");
|
|
194
|
+
if (objects) throw new Error(`Database contains objects requiring hosting-specific DEFINER privileges. Export cannot omit them safely:\n${objects}`);
|
|
195
|
+
containerCreated = true;
|
|
196
|
+
await compose([
|
|
197
|
+
"run",
|
|
198
|
+
"-d",
|
|
199
|
+
"--no-deps",
|
|
200
|
+
"--name",
|
|
201
|
+
container,
|
|
202
|
+
"--user",
|
|
203
|
+
"0:0",
|
|
204
|
+
"--entrypoint",
|
|
205
|
+
"tail",
|
|
206
|
+
"wp-cli",
|
|
207
|
+
"-f",
|
|
208
|
+
"/dev/null"
|
|
209
|
+
], "Start temporary export tooling");
|
|
210
|
+
await command([
|
|
211
|
+
"exec",
|
|
212
|
+
container,
|
|
213
|
+
"php",
|
|
214
|
+
"-r",
|
|
215
|
+
"if (!class_exists(\"PharData\") || !Phar::canCompress(Phar::GZ)) { fwrite(STDERR, \"PHP requires PharData and zlib\"); exit(1); }"
|
|
216
|
+
], "Check PHP ZIP support");
|
|
217
|
+
const prefix = await command([
|
|
218
|
+
"exec",
|
|
219
|
+
container,
|
|
220
|
+
"wp",
|
|
221
|
+
"--allow-root",
|
|
222
|
+
"config",
|
|
223
|
+
"get",
|
|
224
|
+
"table_prefix",
|
|
225
|
+
"--type=variable"
|
|
226
|
+
], "Read table prefix");
|
|
227
|
+
if (!/^[a-zA-Z0-9_]+$/.test(prefix)) throw new Error("WordPress table prefix is invalid; export cannot read site metadata safely.");
|
|
228
|
+
const metadata = JSON.parse(await sql(`SELECT JSON_OBJECT('database', VERSION(), 'prefix', '${prefix}', 'home', (SELECT option_value FROM \`${prefix}options\` WHERE option_name='home'), 'siteurl', (SELECT option_value FROM \`${prefix}options\` WHERE option_name='siteurl'), 'collations', (SELECT GROUP_CONCAT(DISTINCT COLLATION_NAME ORDER BY COLLATION_NAME) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE()))`));
|
|
229
|
+
const wordpress = await command([
|
|
230
|
+
"exec",
|
|
231
|
+
container,
|
|
232
|
+
"php",
|
|
233
|
+
"-r",
|
|
234
|
+
"require \"/var/www/html/wp-includes/version.php\"; echo $wp_version;"
|
|
235
|
+
], "Read WordPress version");
|
|
236
|
+
writeFileSync(join(staging, "README.txt"), restoreReadme({
|
|
237
|
+
...metadata,
|
|
238
|
+
wordpress
|
|
239
|
+
}), { mode: 384 });
|
|
240
|
+
await command([
|
|
241
|
+
"exec",
|
|
242
|
+
mysql,
|
|
243
|
+
"sh",
|
|
244
|
+
"-c",
|
|
245
|
+
"umask 077; export MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\"; exec mysqldump -uroot --single-transaction --quick --skip-lock-tables --skip-add-locks --no-tablespaces --set-gtid-purged=OFF --hex-blob --default-character-set=utf8mb4 --routines --events --triggers --result-file=\"$1\" \"$MYSQL_DATABASE\"",
|
|
246
|
+
"kizlo-export",
|
|
247
|
+
dump
|
|
248
|
+
], "Dump source database");
|
|
249
|
+
await command([
|
|
250
|
+
"cp",
|
|
251
|
+
`${mysql}:${dump}`,
|
|
252
|
+
join(staging, "database.sql")
|
|
253
|
+
], "Copy SQL to staging");
|
|
254
|
+
if (!statSync(join(staging, "database.sql")).size) throw new Error("The database dump is empty.");
|
|
255
|
+
await command([
|
|
256
|
+
"exec",
|
|
257
|
+
container,
|
|
258
|
+
"mkdir",
|
|
259
|
+
"-m",
|
|
260
|
+
"700",
|
|
261
|
+
workspace
|
|
262
|
+
], "Create temporary archive workspace");
|
|
263
|
+
for (const [source, destination] of [
|
|
264
|
+
[join(staging, "database.sql"), "database.sql"],
|
|
265
|
+
[join(staging, "README.txt"), "README.txt"],
|
|
266
|
+
[ARCHIVE_SCRIPT, "export.php"]
|
|
267
|
+
]) await command([
|
|
268
|
+
"cp",
|
|
269
|
+
source,
|
|
270
|
+
`${container}:${workspace}/${destination}`
|
|
271
|
+
], "Copy archive inputs");
|
|
272
|
+
await command([
|
|
273
|
+
"exec",
|
|
274
|
+
container,
|
|
275
|
+
"php",
|
|
276
|
+
"-d",
|
|
277
|
+
"memory_limit=-1",
|
|
278
|
+
`${workspace}/export.php`,
|
|
279
|
+
workspace,
|
|
280
|
+
"/var/www/html/wp-content"
|
|
281
|
+
], "Build export ZIP");
|
|
282
|
+
await command([
|
|
283
|
+
"cp",
|
|
284
|
+
`${container}:${workspace}/site.zip`,
|
|
285
|
+
join(staging, "site.zip")
|
|
286
|
+
], "Copy completed export");
|
|
287
|
+
if (!statSync(join(staging, "site.zip")).size) throw new Error("The completed export is empty.");
|
|
288
|
+
output = join(exportsDir, `wordpress-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${id}.zip`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
failure = error;
|
|
291
|
+
}
|
|
292
|
+
const cleanupErrors = [];
|
|
293
|
+
const cleanup = async (args, action) => {
|
|
294
|
+
try {
|
|
295
|
+
checked(await docker(args, {
|
|
296
|
+
timeout: 6e4,
|
|
297
|
+
detached: true
|
|
298
|
+
}), action);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
cleanupErrors.push(error);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
if (controlled && mysql) await cleanup([
|
|
304
|
+
"exec",
|
|
305
|
+
mysql,
|
|
306
|
+
"rm",
|
|
307
|
+
"-f",
|
|
308
|
+
dump
|
|
309
|
+
], "Remove temporary SQL");
|
|
310
|
+
if (containerCreated) await cleanup([
|
|
311
|
+
"rm",
|
|
312
|
+
"-f",
|
|
313
|
+
container
|
|
314
|
+
], "Remove temporary export container");
|
|
315
|
+
if (controlled) try {
|
|
316
|
+
const ids = await runningProjectContainers(cfg.project, { timeout: 6e4 });
|
|
317
|
+
if (ids.length) await cleanup(["stop", ...ids], "Stop local dev stack");
|
|
318
|
+
if ((await runningProjectContainers(cfg.project, { timeout: 6e4 })).length) throw new Error("Local dev containers are still running.");
|
|
319
|
+
await endDevSession(cfg.project, lock, session);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
cleanupErrors.push(error);
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
abort.signal.throwIfAborted();
|
|
325
|
+
if (!failure && !cleanupErrors.length && output && staging) {
|
|
326
|
+
chmodSync(join(staging, "site.zip"), 384);
|
|
327
|
+
linkSync(join(staging, "site.zip"), output);
|
|
328
|
+
published = true;
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
failure ??= error;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
if (staging) rmSync(staging, {
|
|
335
|
+
recursive: true,
|
|
336
|
+
force: true
|
|
337
|
+
});
|
|
338
|
+
} catch (error) {
|
|
339
|
+
cleanupErrors.push(error);
|
|
340
|
+
}
|
|
341
|
+
for (const signal of signals) process.removeListener(signal, cancel);
|
|
342
|
+
try {
|
|
343
|
+
await releaseOwned(lock);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
cleanupErrors.push(error);
|
|
346
|
+
}
|
|
347
|
+
if (failure || cleanupErrors.length || !output) {
|
|
348
|
+
if (published && output) rmSync(output, { force: true });
|
|
349
|
+
throw new AggregateError([...failure ? [failure] : [], ...cleanupErrors], [failure instanceof Error ? failure.message : "Export failed", ...cleanupErrors.map(String)].join("\n"));
|
|
350
|
+
}
|
|
351
|
+
return output;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
//#endregion
|
|
355
|
+
export { exportDev };
|
package/dist/cli/wp/session.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { stopProjectContainers } from "./docker.js";
|
|
2
|
-
import { currentProcessOwner,
|
|
2
|
+
import { currentProcessOwner, ownerStatus } from "../process-owner.js";
|
|
3
|
+
import { acquire, releaseOwned } from "../daemon/lock.js";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { execFile, spawn } from "node:child_process";
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
+
import { promisify } from "node:util";
|
|
9
10
|
|
|
10
11
|
//#region src/cli/wp/session.ts
|
|
11
12
|
const exec = promisify(execFile);
|
|
@@ -17,6 +18,9 @@ const exec = promisify(execFile);
|
|
|
17
18
|
function registryPath() {
|
|
18
19
|
return join(homedir(), ".cache", "kizlo", "dev-sessions.json");
|
|
19
20
|
}
|
|
21
|
+
function projectSession(project) {
|
|
22
|
+
return readRegistry()[project];
|
|
23
|
+
}
|
|
20
24
|
function readRegistry() {
|
|
21
25
|
const file = registryPath();
|
|
22
26
|
if (!existsSync(file)) return {};
|
|
@@ -68,7 +72,7 @@ async function removeProjectContainers(project) {
|
|
|
68
72
|
* being stopped, so the watchdog would have nothing to do). Best effort: a failure
|
|
69
73
|
* here just means we fall back to signal handlers + the startup reaper.
|
|
70
74
|
*/
|
|
71
|
-
function spawnWatchdog(project, owner) {
|
|
75
|
+
function spawnWatchdog(project, owner, operationFile) {
|
|
72
76
|
try {
|
|
73
77
|
const watchdog = fileURLToPath(new URL("./watchdog.js", import.meta.url));
|
|
74
78
|
const child = spawn(process.execPath, [
|
|
@@ -76,7 +80,8 @@ function spawnWatchdog(project, owner) {
|
|
|
76
80
|
String(owner.pid),
|
|
77
81
|
String(owner.port),
|
|
78
82
|
owner.token,
|
|
79
|
-
project
|
|
83
|
+
project,
|
|
84
|
+
...operationFile ? [operationFile] : []
|
|
80
85
|
], {
|
|
81
86
|
detached: true,
|
|
82
87
|
stdio: "ignore"
|
|
@@ -88,10 +93,13 @@ function spawnWatchdog(project, owner) {
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
/** Record this process as the owner of `project`'s foreground stack. */
|
|
91
|
-
async function registerSession(project) {
|
|
96
|
+
async function registerSession(project, operationFile) {
|
|
92
97
|
const owner = await currentProcessOwner();
|
|
93
98
|
const registry = readRegistry();
|
|
94
|
-
registry[project] =
|
|
99
|
+
registry[project] = {
|
|
100
|
+
...owner,
|
|
101
|
+
...operationFile ? { operationFile } : {}
|
|
102
|
+
};
|
|
95
103
|
writeRegistry(registry);
|
|
96
104
|
return owner;
|
|
97
105
|
}
|
|
@@ -105,6 +113,19 @@ function unregisterSession(project) {
|
|
|
105
113
|
}
|
|
106
114
|
} catch {}
|
|
107
115
|
}
|
|
116
|
+
/** Defer crash cleanup while an export/lifecycle operation owns the project's resources. */
|
|
117
|
+
async function retireSession(project, session) {
|
|
118
|
+
const file = session.operationFile;
|
|
119
|
+
if (file && !await acquire(file)) return false;
|
|
120
|
+
try {
|
|
121
|
+
const successor = projectSession(project);
|
|
122
|
+
if (successor && "token" in successor && (!("token" in session) || successor.token !== session.token) && await ownerStatus(successor) !== "dead") return true;
|
|
123
|
+
await stopProjectContainers(project);
|
|
124
|
+
return true;
|
|
125
|
+
} finally {
|
|
126
|
+
if (file) await releaseOwned(file);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
108
129
|
/**
|
|
109
130
|
* Stop any foreground stacks whose owning session has died (SIGKILL, crash, power
|
|
110
131
|
* loss) — the cases a signal handler can never catch. Runs at the start of every
|
|
@@ -116,11 +137,13 @@ async function reapOrphans() {
|
|
|
116
137
|
const reaped = [];
|
|
117
138
|
let changed = false;
|
|
118
139
|
for (const [project, session] of Object.entries(registry)) {
|
|
119
|
-
if (await
|
|
140
|
+
if (await ownerStatus(session) !== "dead") continue;
|
|
120
141
|
try {
|
|
121
|
-
await
|
|
142
|
+
if (!await retireSession(project, session)) continue;
|
|
122
143
|
reaped.push(project);
|
|
123
|
-
} catch {
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
124
147
|
delete registry[project];
|
|
125
148
|
changed = true;
|
|
126
149
|
}
|
|
@@ -129,4 +152,4 @@ async function reapOrphans() {
|
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
//#endregion
|
|
132
|
-
export { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession };
|
|
155
|
+
export { projectSession, reapOrphans, registerSession, removeProjectContainers, retireSession, spawnWatchdog, unregisterSession };
|
package/dist/cli/wp/watchdog.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { ownerStatus } from "../process-owner.js";
|
|
2
|
+
import { retireSession } from "./session.js";
|
|
4
3
|
|
|
5
4
|
//#region src/cli/wp/watchdog.ts
|
|
6
|
-
const exec = promisify(execFile);
|
|
7
5
|
/**
|
|
8
6
|
* Standalone watchdog process spawned (detached, in its own session) by a foreground
|
|
9
7
|
* `kizlo dev`. It polls the parent's identity and, the moment the parent is gone for *any*
|
|
@@ -17,18 +15,7 @@ const parentPid = Number(process.argv[2]);
|
|
|
17
15
|
const parentPort = Number(process.argv[3]);
|
|
18
16
|
const parentToken = process.argv[4];
|
|
19
17
|
const project = process.argv[5];
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const { stdout } = await exec("docker", [
|
|
23
|
-
"ps",
|
|
24
|
-
"-q",
|
|
25
|
-
"--filter",
|
|
26
|
-
`label=com.docker.compose.project=${project}`
|
|
27
|
-
]);
|
|
28
|
-
const ids = stdout.split("\n").filter(Boolean);
|
|
29
|
-
if (ids.length) await exec("docker", ["stop", ...ids]);
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
18
|
+
const operationFile = process.argv[6];
|
|
32
19
|
if (!parentPid || !parentPort || !parentToken || !project) process.exit(1);
|
|
33
20
|
const parent = {
|
|
34
21
|
pid: parentPid,
|
|
@@ -48,8 +35,8 @@ let missed = 0;
|
|
|
48
35
|
const timer = setInterval(() => {
|
|
49
36
|
if (checking) return;
|
|
50
37
|
checking = true;
|
|
51
|
-
|
|
52
|
-
if (
|
|
38
|
+
ownerStatus(parent).then(async (status) => {
|
|
39
|
+
if (status !== "dead") {
|
|
53
40
|
missed = 0;
|
|
54
41
|
checking = false;
|
|
55
42
|
return;
|
|
@@ -58,8 +45,20 @@ const timer = setInterval(() => {
|
|
|
58
45
|
checking = false;
|
|
59
46
|
return;
|
|
60
47
|
}
|
|
48
|
+
try {
|
|
49
|
+
if (!await retireSession(project, {
|
|
50
|
+
...parent,
|
|
51
|
+
operationFile
|
|
52
|
+
})) {
|
|
53
|
+
checking = false;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
checking = false;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
61
60
|
clearInterval(timer);
|
|
62
|
-
|
|
61
|
+
process.exit(0);
|
|
63
62
|
});
|
|
64
63
|
}, 1e3);
|
|
65
64
|
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ActiveWordPressClient, WordPressCredentials } from "./wordpress/types.js";
|
|
2
|
-
import { WordPressTransport } from "./wordpress/transport.js";
|
|
3
2
|
import "./wordpress/index.js";
|
|
4
3
|
import { AuthUser } from "./adapters/auth.js";
|
|
5
4
|
import { PreviewTokenPayload } from "./shared/schema.js";
|
|
@@ -31,8 +30,8 @@ type VerifyPreviewTokenFn = (token: string) => Promise<PreviewTokenPayload | nul
|
|
|
31
30
|
* the server builds. A procedure's handler receives this base plus whatever its middleware injected
|
|
32
31
|
* via `next({ context })`.
|
|
33
32
|
*/
|
|
34
|
-
interface
|
|
35
|
-
/** The
|
|
33
|
+
interface BuiltinProcedureContext {
|
|
34
|
+
/** The active HTTP `Request`, or `null` outside a request scope. */
|
|
36
35
|
request: Request | null;
|
|
37
36
|
/** Response headers to be sent (e.g. `Set-Cookie`), or `null` server-side. */
|
|
38
37
|
headers: Headers | null;
|
|
@@ -59,6 +58,10 @@ interface ProcedureContext {
|
|
|
59
58
|
/** How the procedure was called: `"client"` (over HTTP) or `"server"` (direct server invocation). */
|
|
60
59
|
invokedBy: "client" | "server";
|
|
61
60
|
}
|
|
61
|
+
/** Augment in the application to add typed, server-supplied request fields. */
|
|
62
|
+
interface ProcedureContext extends BuiltinProcedureContext {}
|
|
63
|
+
/** The application fields accepted by handler(request, values), excluding Kizlo-owned fields. */
|
|
64
|
+
type PlatformRequestCtx = Omit<ProcedureContext, keyof BuiltinProcedureContext> & object;
|
|
62
65
|
declare class Context {
|
|
63
66
|
private readonly config;
|
|
64
67
|
private readonly logger;
|
|
@@ -77,35 +80,15 @@ declare class Context {
|
|
|
77
80
|
private warnIfOutdated;
|
|
78
81
|
/** Bold yellow, boxed in dashes so it stands out in a busy dev console. */
|
|
79
82
|
private warn;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
verifyCaptcha: VerifyCaptchaFn;
|
|
83
|
-
getSession: SessionFn;
|
|
84
|
-
verifyPreviewToken: VerifyPreviewTokenFn;
|
|
85
|
-
cookies: CookiesStorage;
|
|
86
|
-
invokedBy: "client";
|
|
87
|
-
wordpress: WordPressTransport;
|
|
88
|
-
settings: SettingsService;
|
|
89
|
-
email: EmailService;
|
|
90
|
-
headers: Headers;
|
|
83
|
+
withRequestContext(request: Request, values: PlatformRequestCtx | undefined, handle: () => Promise<Response>): Promise<Response>;
|
|
84
|
+
createRestContext(request: Request, values?: PlatformRequestCtx): ProcedureContext & {
|
|
91
85
|
request: Request;
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
headers: Headers;
|
|
87
|
+
invokedBy: "client";
|
|
94
88
|
};
|
|
95
|
-
createServerContext(): {
|
|
96
|
-
getSession: SessionFn;
|
|
97
|
-
getConnInfo: ConnInfoFn;
|
|
98
|
-
verifyCaptcha: VerifyCaptchaFn;
|
|
99
|
-
verifyPreviewToken: VerifyPreviewTokenFn;
|
|
100
|
-
invokedBy: "server";
|
|
101
|
-
wordpress: WordPressTransport;
|
|
102
|
-
settings: SettingsService;
|
|
103
|
-
email: EmailService;
|
|
104
|
-
cookies: CookiesStorage;
|
|
89
|
+
createServerContext(): ProcedureContext & {
|
|
105
90
|
headers: null;
|
|
106
|
-
|
|
107
|
-
config: ContextConfig;
|
|
108
|
-
logger: Logger;
|
|
91
|
+
invokedBy: "server";
|
|
109
92
|
};
|
|
110
93
|
private createRestCookieStorage;
|
|
111
94
|
private createVerifyCaptchaFn;
|
|
@@ -115,4 +98,4 @@ declare class Context {
|
|
|
115
98
|
private createLogger;
|
|
116
99
|
}
|
|
117
100
|
//#endregion
|
|
118
|
-
export { ConnInfoFn, Context, ContextConfig, ProcedureContext, SessionFn, VerifyCaptchaFn, VerifyPreviewTokenFn };
|
|
101
|
+
export { ConnInfoFn, Context, ContextConfig, PlatformRequestCtx, ProcedureContext, SessionFn, VerifyCaptchaFn, VerifyPreviewTokenFn };
|
package/dist/context.js
CHANGED
|
@@ -102,9 +102,15 @@ var Context = class {
|
|
|
102
102
|
const border = "-".repeat(inner.length + 2);
|
|
103
103
|
console.warn(`\n\x1b[1m\x1b[33m${border}\n|${inner}|\n${border}\x1b[0m\n`);
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
withRequestContext(request, values, handle) {
|
|
106
|
+
if (values !== void 0 && (values === null || typeof values !== "object" || Array.isArray(values))) throw new TypeError("Request context must be an object.");
|
|
107
|
+
return this.config.adapters?.requestContext?.run?.(request, values, handle) ?? handle();
|
|
108
|
+
}
|
|
109
|
+
createRestContext(request, values) {
|
|
106
110
|
const headers = new Headers();
|
|
107
111
|
return {
|
|
112
|
+
...this.config.adapters?.requestContext?.get()?.values,
|
|
113
|
+
...values,
|
|
108
114
|
headers,
|
|
109
115
|
request: request.clone(),
|
|
110
116
|
config: this.config,
|
|
@@ -123,21 +129,24 @@ var Context = class {
|
|
|
123
129
|
};
|
|
124
130
|
}
|
|
125
131
|
createServerContext() {
|
|
132
|
+
const scope = this.config.adapters?.requestContext?.get();
|
|
133
|
+
const request = scope?.request ?? null;
|
|
126
134
|
const cookies = new CookiesStorage(this.config.adapters?.cookies);
|
|
127
135
|
return {
|
|
136
|
+
...scope?.values,
|
|
128
137
|
cookies,
|
|
129
138
|
headers: null,
|
|
130
|
-
request
|
|
139
|
+
request,
|
|
131
140
|
config: this.config,
|
|
132
141
|
logger: this.logger,
|
|
133
|
-
...this.config.adapters?.geo ? this.createServices(
|
|
142
|
+
...this.config.adapters?.geo ? this.createServices(request) : {
|
|
134
143
|
wordpress: this.wordpress,
|
|
135
144
|
settings: this.settings,
|
|
136
145
|
email: this.email
|
|
137
146
|
},
|
|
138
|
-
getSession: this.createGetSessionFn(
|
|
139
|
-
getConnInfo: this.createConnInfoFn(
|
|
140
|
-
verifyCaptcha: this.createVerifyCaptchaFn(
|
|
147
|
+
getSession: this.createGetSessionFn(request, cookies),
|
|
148
|
+
getConnInfo: this.createConnInfoFn(request),
|
|
149
|
+
verifyCaptcha: this.createVerifyCaptchaFn(request),
|
|
141
150
|
verifyPreviewToken: this.createVerifyPreviewTokenFn(),
|
|
142
151
|
invokedBy: "server"
|
|
143
152
|
};
|