@crowi/admin-cli 0.1.0-alpha.0 → 0.1.0-alpha.2
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/bin.js +168 -9
- package/dist/bin.js.map +1 -1
- package/dist/cli.js +168 -9
- package/dist/cli.js.map +1 -1
- package/package.json +7 -2
package/dist/bin.js
CHANGED
|
@@ -72,6 +72,9 @@ async function withMigrationApi(fn) {
|
|
|
72
72
|
function formatRange(entry) {
|
|
73
73
|
return `${entry.fromVersion} \u2192 ${entry.toVersion}`;
|
|
74
74
|
}
|
|
75
|
+
function severityTag(severity) {
|
|
76
|
+
return severity ? `[${severity}]` : "\u2014";
|
|
77
|
+
}
|
|
75
78
|
function registerMigrate(program) {
|
|
76
79
|
const migrate = program.command("migrate").description("Forward-only data migrations (plan / apply / status / list).");
|
|
77
80
|
migrate.command("list").description("List every registered migration with its version range and layer.").option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
@@ -85,9 +88,9 @@ function registerMigrate(program) {
|
|
|
85
88
|
console.log("No migrations are registered.");
|
|
86
89
|
return;
|
|
87
90
|
}
|
|
88
|
-
console.log("ID from \u2192 to layer description");
|
|
91
|
+
console.log("ID from \u2192 to layer severity description");
|
|
89
92
|
for (const r of rows) {
|
|
90
|
-
console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${r.description}`);
|
|
93
|
+
console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${severityTag(r.severity).padEnd(11)} ${r.description}`);
|
|
91
94
|
}
|
|
92
95
|
});
|
|
93
96
|
});
|
|
@@ -106,7 +109,7 @@ function registerMigrate(program) {
|
|
|
106
109
|
return;
|
|
107
110
|
}
|
|
108
111
|
pending.forEach((e, i) => {
|
|
109
|
-
console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} (${formatRange(e)})`);
|
|
112
|
+
console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} ${severityTag(e.severity)} (${formatRange(e)})`);
|
|
110
113
|
console.log(` ${e.description}`);
|
|
111
114
|
console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : "Detected: details unavailable (no detect stage; isPending = true)"}`);
|
|
112
115
|
});
|
|
@@ -291,8 +294,9 @@ function formatElapsed(ms) {
|
|
|
291
294
|
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
292
295
|
}
|
|
293
296
|
|
|
294
|
-
// src/commands/
|
|
297
|
+
// src/commands/replace.ts
|
|
295
298
|
var import_node_path3 = __toESM(require("path"));
|
|
299
|
+
var import_promises = __toESM(require("readline/promises"));
|
|
296
300
|
var import_dotenv3 = __toESM(require("dotenv"));
|
|
297
301
|
function loadApi3() {
|
|
298
302
|
let apiPkgPath;
|
|
@@ -303,14 +307,168 @@ function loadApi3() {
|
|
|
303
307
|
}
|
|
304
308
|
const distDir = import_node_path3.default.join(import_node_path3.default.dirname(apiPkgPath), "dist");
|
|
305
309
|
const crowiModule = require(import_node_path3.default.join(distDir, "crowi"));
|
|
306
|
-
const
|
|
310
|
+
const replaceModule = require(import_node_path3.default.join(distDir, "util", "replace-url"));
|
|
311
|
+
return { Crowi: crowiModule.default, runReplaceUrl: replaceModule.runReplaceUrl, assessReplaceSafety: replaceModule.assessReplaceSafety };
|
|
312
|
+
}
|
|
313
|
+
function replaceExitCode(summary) {
|
|
314
|
+
return summary.failed > 0 ? 2 : 0;
|
|
315
|
+
}
|
|
316
|
+
function registerReplace(program) {
|
|
317
|
+
const replace = program.command("replace").description("Bulk content replacements across page bodies.");
|
|
318
|
+
replace.command("url").description(
|
|
319
|
+
"Replace a literal URL/host string in every page body (e.g. after a domain change). Pushes a new revision per page WITHOUT bumping updatedAt or notifying watchers."
|
|
320
|
+
).requiredOption("--from <s>", "String to replace. Use a full origin to be safe, e.g. https://old.example.").requiredOption("--to <s>", "Replacement string, e.g. https://new.example.").option("--dry-run", "Report what would change without writing.", false).option("--include-trash", "Also rewrite trashed / deprecated pages (default: published only).", false).option("--user <email>", "Author recorded on the new revisions (defaults to the oldest admin).").option("--yes", "Skip the interactive confirmation prompt.", false).option("--force", "Proceed even when --from looks unsafe (e.g. a bare host without a scheme).", false).action(async (opts) => {
|
|
321
|
+
import_dotenv3.default.config();
|
|
322
|
+
const api = loadApi3();
|
|
323
|
+
if (!api) {
|
|
324
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const from = String(opts.from);
|
|
328
|
+
const to = String(opts.to);
|
|
329
|
+
const safety = api.assessReplaceSafety(from, to);
|
|
330
|
+
for (const w of safety.warnings) console.warn(`crowi-admin: warning: ${w}`);
|
|
331
|
+
if (safety.errors.length > 0) {
|
|
332
|
+
for (const e of safety.errors) console.error(`crowi-admin: ${e}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
if (safety.bareHostFrom && !opts.force) {
|
|
336
|
+
console.error(
|
|
337
|
+
`crowi-admin: --from='${from}' has no scheme. A bare host can corrupt longer hosts that start with it (e.g. '${from}' is a prefix of '${from}t'). Re-run with a full origin (e.g. https://${from}) or pass --force to override.`
|
|
338
|
+
);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
const crowi = new api.Crowi(process.cwd(), process.env);
|
|
342
|
+
console.log(`[crowi-admin] replace url: '${from}' \u2192 '${to}'${opts.dryRun ? " (dry-run)" : ""}`);
|
|
343
|
+
try {
|
|
344
|
+
await crowi.initForCli();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
347
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
let exitCode = 0;
|
|
351
|
+
try {
|
|
352
|
+
const startedAt = Date.now();
|
|
353
|
+
const summary = await api.runReplaceUrl(crowi, {
|
|
354
|
+
from,
|
|
355
|
+
to,
|
|
356
|
+
userEmail: opts.user,
|
|
357
|
+
dryRun: Boolean(opts.dryRun),
|
|
358
|
+
includeTrash: Boolean(opts.includeTrash),
|
|
359
|
+
confirm: opts.dryRun ? void 0 : (preview) => confirmProceed(preview, Boolean(opts.yes))
|
|
360
|
+
});
|
|
361
|
+
printSummary(summary, Date.now() - startedAt);
|
|
362
|
+
exitCode = replaceExitCode(summary);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.error("crowi-admin: replace url failed.");
|
|
365
|
+
if (err instanceof Error) {
|
|
366
|
+
if (err.message) console.error(` message: ${err.message}`);
|
|
367
|
+
if (err.stack) console.error(err.stack);
|
|
368
|
+
} else {
|
|
369
|
+
console.error(` thrown: ${String(err)}`);
|
|
370
|
+
}
|
|
371
|
+
exitCode = 1;
|
|
372
|
+
} finally {
|
|
373
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
374
|
+
}
|
|
375
|
+
process.exit(exitCode);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async function confirmProceed(preview, yes) {
|
|
379
|
+
printPreview(preview);
|
|
380
|
+
if (preview.pagesMatched === 0) return false;
|
|
381
|
+
if (yes) return true;
|
|
382
|
+
if (!process.stdin.isTTY) {
|
|
383
|
+
console.error("");
|
|
384
|
+
console.error("crowi-admin: refusing to write without confirmation (no TTY). Re-run with --yes to proceed, or --dry-run to preview.");
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
388
|
+
try {
|
|
389
|
+
const answer = (await rl.question(`Rewrite ${preview.pagesMatched} page(s)? [y/N] `)).trim().toLowerCase();
|
|
390
|
+
return answer === "y" || answer === "yes";
|
|
391
|
+
} finally {
|
|
392
|
+
rl.close();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function printPreview(preview) {
|
|
396
|
+
console.log("");
|
|
397
|
+
console.log(`Matched ${preview.pagesMatched} page(s), ${preview.occurrences} occurrence(s).`);
|
|
398
|
+
for (const s of preview.samples) {
|
|
399
|
+
console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);
|
|
400
|
+
}
|
|
401
|
+
const more = preview.pagesMatched - preview.samples.length;
|
|
402
|
+
if (more > 0) console.log(` \u2026 and ${more} more page(s)`);
|
|
403
|
+
}
|
|
404
|
+
function printSummary(summary, elapsedMs) {
|
|
405
|
+
console.log("");
|
|
406
|
+
console.log("--- summary ---");
|
|
407
|
+
console.log(`from: ${summary.from}`);
|
|
408
|
+
console.log(`to: ${summary.to}`);
|
|
409
|
+
console.log(`scanned: ${summary.pagesScanned} page(s)`);
|
|
410
|
+
console.log(`matched: ${summary.pagesMatched} page(s), ${summary.occurrences} occurrence(s)`);
|
|
411
|
+
if (summary.pagesMatched === 0) {
|
|
412
|
+
console.log("");
|
|
413
|
+
console.log(`No pages contain '${summary.from}'.`);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (summary.dryRun) {
|
|
417
|
+
for (const s of summary.samples) {
|
|
418
|
+
console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);
|
|
419
|
+
}
|
|
420
|
+
const more = summary.pagesMatched - summary.samples.length;
|
|
421
|
+
if (more > 0) console.log(` \u2026 and ${more} more page(s)`);
|
|
422
|
+
console.log("");
|
|
423
|
+
console.log("Dry-run complete \u2014 no pages written.");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (summary.aborted) {
|
|
427
|
+
console.log("");
|
|
428
|
+
console.log("Aborted \u2014 no pages written.");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
console.log(`rewritten: ${summary.pagesRewritten} page(s)`);
|
|
432
|
+
if (summary.failed > 0) console.log(`failed: ${summary.failed} page(s)`);
|
|
433
|
+
if (summary.actingUserEmail) console.log(`author: ${summary.actingUserEmail}`);
|
|
434
|
+
console.log(`elapsed: ${formatElapsed2(elapsedMs)}`);
|
|
435
|
+
if (summary.interrupted) {
|
|
436
|
+
console.log("");
|
|
437
|
+
console.log("Interrupted by SIGINT before completion \u2014 re-run to finish.");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
console.log("");
|
|
441
|
+
console.log("Replacement complete. Run 'crowi-admin rebuild search' to refresh the search index (page rendering is already up to date).");
|
|
442
|
+
}
|
|
443
|
+
function formatElapsed2(ms) {
|
|
444
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
445
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
446
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
447
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
448
|
+
const seconds = totalSeconds % 60;
|
|
449
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/commands/watcher-backfill.ts
|
|
453
|
+
var import_node_path4 = __toESM(require("path"));
|
|
454
|
+
var import_dotenv4 = __toESM(require("dotenv"));
|
|
455
|
+
function loadApi4() {
|
|
456
|
+
let apiPkgPath;
|
|
457
|
+
try {
|
|
458
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const distDir = import_node_path4.default.join(import_node_path4.default.dirname(apiPkgPath), "dist");
|
|
463
|
+
const crowiModule = require(import_node_path4.default.join(distDir, "crowi"));
|
|
464
|
+
const backfillModule = require(import_node_path4.default.join(distDir, "util", "watcher-backfill"));
|
|
307
465
|
return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };
|
|
308
466
|
}
|
|
309
467
|
function registerWatcherBackfill(program) {
|
|
310
468
|
const watcher = program.command("watcher").description("Watcher / notification subscription utilities.");
|
|
311
469
|
watcher.command("backfill").description("Backfill WATCH rows for pages created before auto-watch (creator + comment/revision authors). Idempotent.").option("--dry-run", "Report how many WATCH rows would be created without writing anything.", false).action(async (opts) => {
|
|
312
|
-
|
|
313
|
-
const api =
|
|
470
|
+
import_dotenv4.default.config();
|
|
471
|
+
const api = loadApi4();
|
|
314
472
|
if (!api) {
|
|
315
473
|
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
316
474
|
process.exit(1);
|
|
@@ -334,7 +492,7 @@ function registerWatcherBackfill(program) {
|
|
|
334
492
|
console.log("--- summary ---");
|
|
335
493
|
console.log(`pages scanned: ${summary.pagesScanned}`);
|
|
336
494
|
console.log(`watchers ${summary.dryRun ? "to create" : "created"}: ${summary.watchersCreated}`);
|
|
337
|
-
console.log(`elapsed: ${
|
|
495
|
+
console.log(`elapsed: ${formatElapsed3(elapsedMs)}`);
|
|
338
496
|
console.log("");
|
|
339
497
|
console.log(summary.dryRun ? "Dry-run complete \u2014 no rows written." : "Backfill complete.");
|
|
340
498
|
} catch (err) {
|
|
@@ -352,7 +510,7 @@ function registerWatcherBackfill(program) {
|
|
|
352
510
|
process.exit(exitCode);
|
|
353
511
|
});
|
|
354
512
|
}
|
|
355
|
-
function
|
|
513
|
+
function formatElapsed3(ms) {
|
|
356
514
|
if (ms < 1e3) return `${ms}ms`;
|
|
357
515
|
const totalSeconds = Math.round(ms / 1e3);
|
|
358
516
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -367,6 +525,7 @@ function createProgram() {
|
|
|
367
525
|
program.name("crowi-admin").description("Operator-side admin CLI for Crowi 2.0. Talks directly to MongoDB; intended for use inside the server (ssh / kubectl exec).").version("0.1.0-dev");
|
|
368
526
|
registerMigrate(program);
|
|
369
527
|
registerRebuild(program);
|
|
528
|
+
registerReplace(program);
|
|
370
529
|
registerWatcherBackfill(program);
|
|
371
530
|
return program;
|
|
372
531
|
}
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/commands/migrate.ts","../src/commands/rebuild.ts","../src/commands/watcher-backfill.ts","../src/bin.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { registerMigrate } from './commands/migrate';\nimport { registerRebuild } from './commands/rebuild';\nimport { registerWatcherBackfill } from './commands/watcher-backfill';\n\n/**\n * Build the root commander program. Exported so the bin entry point\n * (`bin.ts`) can call `parseAsync` on it, and so future test harnesses\n * can drive the CLI without spawning a child process.\n *\n * Subcommands are registered via small per-command helpers\n * (`registerXxx(program)`) so each command keeps its own arg / option\n * declarations next to its implementation.\n */\nexport function createProgram(): Command {\n const program = new Command();\n program\n .name('crowi-admin')\n .description('Operator-side admin CLI for Crowi 2.0. Talks directly to MongoDB; intended for use inside the server (ssh / kubectl exec).')\n .version('0.1.0-dev');\n\n // RFC-0008: the unified migration framework namespaces. The wikilink\n // migration lives under `migrate apply --id wikilink-format` (phase 3); the\n // legacy top-level `storage copy` / `search rebuild` forms are gone (phase\n // 4) — their tasks now ride the shared runner under `rebuild storage copy` /\n // `rebuild search`. No compatibility aliases (CHANGELOG / upgrade guide).\n registerMigrate(program);\n registerRebuild(program);\n // `watcher backfill` (idempotent WATCH-row backfill) landed on main as a\n // standalone command; kept as-is here. Could fold into the framework as a\n // `rebuild` / `migrate` task later (see TODO backlog).\n registerWatcherBackfill(program);\n\n return program;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8 — the `crowi-admin migrate plan|apply|status|list` namespace.\n *\n * One-shot, forward-only migrations. `plan` / `apply` default to the\n * `preflight` layer (§4.2.2); `--all-layers` extends to boot migrations too\n * (debugging / investigation). The boot layer is normally applied by the api\n * boot sequence, not from here.\n *\n * Like the other admin commands, this loads the api's compiled `dist/`\n * lazily (see `storage-copy.ts` for the `require.resolve` rationale — we\n * avoid importing `@crowi/api` directly so its `app.ts` auto-boot doesn't\n * fire) and talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `MigrationCliApi` façade. */\ninterface MigrationSummary {\n id: string;\n fromVersion: string;\n toVersion: string;\n layer: 'boot' | 'preflight';\n description: string;\n}\ninterface DetectReport {\n summary: string;\n counts?: Record<string, number>;\n}\ninterface MigrationPlanEntry extends MigrationSummary {\n pending: boolean;\n detail: DetectReport | null;\n}\ninterface MigrationStatusEntry {\n migrationId: string;\n result: string;\n appliedAt: Date;\n durationMs?: number;\n appliedBy?: string;\n}\ninterface MigrationStatus {\n latestTarget: string | null;\n recent: MigrationStatusEntry[];\n pendingPreflight: number;\n pendingBoot: number;\n}\ninterface ApplyOutcome {\n id: string;\n result: string;\n durationMs: number;\n}\ninterface MigrationCliApi {\n list(): MigrationSummary[];\n latestTarget(): string | null;\n plan(options: { allLayers?: boolean }): Promise<MigrationPlanEntry[]>;\n apply(options: { allLayers?: boolean; dryRun?: boolean; id?: string; continueOnError?: boolean }): Promise<ApplyOutcome[]>;\n status(recentLimit?: number): Promise<MigrationStatus>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateMigrationCliApi = (crowi: ApiCrowi) => MigrationCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createMigrationCliApi: CreateMigrationCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const cliApiModule = require(path.join(distDir, 'migration', 'cli-api')) as { createMigrationCliApi: CreateMigrationCliApi };\n\n return { Crowi: crowiModule.default, createMigrationCliApi: cliApiModule.createMigrationCliApi };\n}\n\n/**\n * Boot a lightweight Crowi, hand it to `fn`, then tear it down. Centralizes\n * the .env load / loadApi guard / init / teardown ceremony shared by every\n * `migrate` subcommand. Exits the process with a non-zero code on failure.\n */\nasync function withMigrationApi(fn: (api: MigrationCliApi) => Promise<void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n await fn(loaded.createMigrationCliApi(crowi));\n } catch (err) {\n console.error('crowi-admin: migrate command failed.');\n console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nfunction formatRange(entry: { fromVersion: string; toVersion: string }): string {\n return `${entry.fromVersion} → ${entry.toVersion}`;\n}\n\nexport function registerMigrate(program: Command): void {\n const migrate = program.command('migrate').description('Forward-only data migrations (plan / apply / status / list).');\n\n migrate\n .command('list')\n .description('List every registered migration with its version range and layer.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const rows = api.list();\n if (opts.json) {\n console.log(JSON.stringify(rows, null, 2));\n return;\n }\n if (rows.length === 0) {\n console.log('No migrations are registered.');\n return;\n }\n console.log('ID from → to layer description');\n for (const r of rows) {\n console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${r.description}`);\n }\n });\n });\n\n migrate\n .command('plan')\n .description('Preview pending migrations (preflight by default).')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { allLayers: boolean; json: boolean }) => {\n await withMigrationApi(async (api) => {\n const entries = await api.plan({ allLayers: opts.allLayers });\n if (opts.json) {\n console.log(JSON.stringify({ latestTarget: api.latestTarget(), entries }, null, 2));\n return;\n }\n console.log(`Latest target: ${api.latestTarget() ?? '(none)'}`);\n console.log('');\n const pending = entries.filter((e) => e.pending);\n if (pending.length === 0) {\n console.log('No pending migrations.');\n return;\n }\n pending.forEach((e, i) => {\n console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} (${formatRange(e)})`);\n console.log(` ${e.description}`);\n console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : 'Detected: details unavailable (no detect stage; isPending = true)'}`);\n });\n console.log('');\n console.log('Run `crowi-admin migrate apply` to execute preflight migrations.');\n });\n });\n\n migrate\n .command('apply')\n .description('Apply pending migrations (preflight by default), in version-range + order sequence.')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--dry-run', 'Run detect only; stages no-op and nothing is recorded.', false)\n .option('--id <id>', 'Apply only the migration with this id.')\n .option('--continue-on-error', 'Continue with later migrations after a failure (default: abort).', false)\n .action(async (opts: { allLayers: boolean; dryRun: boolean; id?: string; continueOnError: boolean }) => {\n await withMigrationApi(async (api) => {\n const outcomes = await api.apply({ allLayers: opts.allLayers, dryRun: opts.dryRun, id: opts.id, continueOnError: opts.continueOnError });\n if (outcomes.length === 0) {\n console.log('No migrations to apply.');\n return;\n }\n for (const o of outcomes) {\n console.log(` ${o.id.padEnd(25)} → ${o.result} (${o.durationMs}ms)`);\n }\n const failed = outcomes.filter((o) => o.result === 'failed');\n if (failed.length > 0) {\n throw new Error(`${failed.length} migration(s) failed: ${failed.map((f) => f.id).join(', ')}`);\n }\n });\n });\n\n migrate\n .command('status')\n .description('Show recent migration applications and pending counts.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const status = await api.status();\n if (opts.json) {\n console.log(JSON.stringify(status, null, 2));\n return;\n }\n console.log(`Latest target: ${status.latestTarget ?? '(none)'}`);\n console.log('');\n console.log('Recent applications (last 10):');\n if (status.recent.length === 0) {\n console.log(' (none)');\n } else {\n for (const r of status.recent) {\n const date = r.appliedAt.toISOString().slice(0, 10);\n const elapsed = r.durationMs !== undefined ? `${r.durationMs}ms` : '-';\n console.log(` ${date} ${r.result.padEnd(14)} ${r.migrationId.padEnd(25)} (${elapsed}, ${r.appliedBy ?? '-'})`);\n }\n }\n console.log('');\n console.log(`Pending preflight: ${status.pendingPreflight} migration(s)`);\n console.log(`Pending boot: ${status.pendingBoot} migration(s)`);\n });\n });\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8.5 — the `crowi-admin rebuild <target>` namespace.\n *\n * Operational rebuilds of derived data — version-independent, runnable any\n * time, any number of times (no pending/applied concept). All targets route\n * through the api-side `RebuildRunner`, so they share the framework runner's\n * `--dry-run` / progress / SIGINT / structured-logging conventions with\n * `migrate` (§4.3) — but a rebuild never touches `migrationApplications`\n * (§8.5).\n *\n * Targets:\n * - `rebuild search` ← ported from the old top-level `search rebuild`\n * - `rebuild storage copy` ← ported from the old top-level `storage copy`\n * - `rebuild renderer` ← new; util/rebuild-renderer.ts skeleton (TODO)\n * - `rebuild backlink` ← new; util/rebuild-backlink.ts skeleton (TODO)\n *\n * Like the other admin commands, this loads the api's compiled `dist/` lazily\n * (see `storage-copy.ts` for the `require.resolve` rationale — we avoid\n * importing `@crowi/api` directly so its `app.ts` auto-boot doesn't fire) and\n * talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `RebuildOutcome`. */\ninterface RebuildOutcome {\n id: string;\n durationMs: number;\n interrupted: boolean;\n stats: Record<string, unknown>;\n}\ninterface RebuildProgress {\n onLabel?: (label: string) => void;\n onIncrement?: (current: number) => void;\n}\ninterface RebuildCliApi {\n rebuildSearch(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildStorageCopy(opts: { from: string; to: string; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildRenderer(opts?: { onlyStale?: boolean; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildBacklink(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateRebuildCliApi = (crowi: ApiCrowi) => RebuildCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createRebuildCliApi: CreateRebuildCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const apiModule = require(path.join(distDir, 'migration', 'rebuild-api')) as { createRebuildCliApi: CreateRebuildCliApi };\n\n return { Crowi: crowiModule.default, createRebuildCliApi: apiModule.createRebuildCliApi };\n}\n\n/**\n * Map a completed rebuild outcome to a process exit code, mirroring the legacy\n * storage-copy convention:\n * - 0 — success (everything copied / rebuilt, or dry-run)\n * - 2 — partial: the run completed but >=1 unit failed (operator should retry)\n *\n * Kept as a pure function (no `process.exit`) so the partial→2 mapping is unit\n * testable without the surrounding boot ceremony. `process.exit(code)` ignores\n * `process.exitCode`, so the exit code must flow through here and be passed\n * explicitly — a fn that merely sets `process.exitCode = 2` would be clobbered\n * by the `process.exit(0)` in `withRebuildApi`.\n *\n * Fatal failures (init failed, or a task threw — e.g. renderer/backlink NOT_YET)\n * are exit 1 and handled in `withRebuildApi`; they never reach here.\n */\nexport function rebuildExitCode(outcome: RebuildOutcome): number {\n const failed = outcome.stats.failed;\n if (typeof failed === 'number' && failed > 0) return 2;\n return 0;\n}\n\n/**\n * Boot a lightweight Crowi, hand the rebuild façade to `fn`, then tear it\n * down. Centralizes the .env load / loadApi guard / init / teardown ceremony\n * shared by every `rebuild` subcommand.\n *\n * `fn` returns the success exit code (0 normally, 2 for a partial run — see\n * `rebuildExitCode`); a fatal failure (init error or a thrown task) overrides\n * it with exit 1. The resolved code is passed explicitly to `process.exit`,\n * since an explicit argument ignores any `process.exitCode` a callee set.\n */\nasync function withRebuildApi(fn: (api: RebuildCliApi) => Promise<number | void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n exitCode = (await fn(loaded.createRebuildCliApi(crowi))) ?? 0;\n } catch (err) {\n console.error('crowi-admin: rebuild failed.');\n printError(err);\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nexport function registerRebuild(program: Command): void {\n const rebuild = program.command('rebuild').description('Operational rebuilds of derived data (renderer / search / backlink / storage copy).');\n\n rebuild\n .command('renderer')\n .description('Regenerate cached rendered HTML for pages.')\n .option('--only-stale', 'Only re-render pages whose cache is stale.', false)\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { onlyStale: boolean; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildRenderer({ onlyStale: opts.onlyStale, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('renderer', outcome);\n });\n });\n\n rebuild\n .command('search')\n .description(\"Rebuild the search index from scratch using the active driver's rebuild().\")\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildSearch({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('search', outcome);\n });\n });\n\n rebuild\n .command('backlink')\n .description('Rebuild the backlink index across all pages.')\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildBacklink({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('backlink', outcome);\n });\n });\n\n const storage = rebuild.command('storage').description('Storage driver rebuilds.');\n storage\n .command('copy')\n .description('Copy every stored object from one driver to another.')\n .requiredOption('--from <name>', 'Source storage driver name (e.g. local, s3).')\n .requiredOption('--to <name>', 'Destination storage driver name.')\n .option('--dry-run', 'List candidate keys without copying anything.', false)\n .action(async (opts: { from: string; to: string; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildStorageCopy({ from: opts.from, to: opts.to, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('storage copy', outcome);\n // Mirror the legacy exit-code convention: partial (>=1 key failed) → 2.\n return rebuildExitCode(outcome);\n });\n });\n}\n\n/**\n * A progress sink that renders the runner's per-unit label to stderr so a\n * long rebuild shows live activity without flooding stdout (which carries the\n * final summary). Mirrors the spirit of `storage-copy.ts`'s `renderProgress`.\n */\nfunction liveProgress(): RebuildProgress {\n return {\n onLabel: (label) => {\n // Single-line, low-noise: enough to confirm the run is alive.\n process.stderr.write(` ${label}\\n`);\n },\n };\n}\n\n/** Print the final summary block, including each stat key the task returned. */\nfunction printOutcome(target: string, outcome: RebuildOutcome): void {\n console.log('');\n console.log('--- summary ---');\n console.log(`target: ${target}`);\n for (const [key, value] of Object.entries(outcome.stats)) {\n console.log(`${`${key}:`.padEnd(10)}${formatStat(value)}`);\n }\n console.log(`elapsed: ${formatElapsed(outcome.durationMs)}`);\n if (outcome.interrupted) {\n console.log('');\n console.log('Interrupted by SIGINT before completion — re-run to finish.');\n return;\n }\n console.log('');\n console.log(`Rebuild '${target}' complete.`);\n}\n\nfunction formatStat(value: unknown): string {\n if (Array.isArray(value)) return value.length === 0 ? '(none)' : value.join(', ');\n return String(value);\n}\n\n/**\n * Render whatever detail we can extract from a thrown error. The ES JS\n * client's `ResponseError` puts the cluster's actual response on `meta.body`\n * and leaves `.message` as just the HTTP status string, so walk the common\n * shapes (preserved from the old `search rebuild` command).\n */\nfunction printError(err: unknown): void {\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n const meta = (err as Error & { meta?: { statusCode?: number; body?: unknown } }).meta;\n if (meta) {\n if (meta.statusCode !== undefined) console.error(` status: ${meta.statusCode}`);\n if (meta.body !== undefined) {\n try {\n console.error(` body: ${JSON.stringify(meta.body, null, 2)}`);\n } catch {\n console.error(` body: ${String(meta.body)}`);\n }\n }\n }\n const cause = (err as Error & { cause?: unknown }).cause;\n if (cause !== undefined) console.error(` cause: ${cause instanceof Error ? cause.message || cause.name : String(cause)}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n}\n\n/** Render an elapsed millisecond duration (\"412ms\" / \"28m12s\"). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * Resolve @crowi/api's installed location relative to the caller's CWD\n * (= the runner directory) and load the bits we need, the same way\n * `search-rebuild.ts` / `storage-copy.ts` do (manual `require` so\n * `@crowi/api`'s `app.ts` auto-boot doesn't fire). Returns `null` when\n * the package isn't found so the caller can print a friendly error.\n */\nfunction loadApi(): { Crowi: ApiCrowiCtor; runWatcherBackfill: RunWatcherBackfill } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const distDir = path.join(path.dirname(apiPkgPath), 'dist');\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const backfillModule = require(path.join(distDir, 'util', 'watcher-backfill')) as { runWatcherBackfill: RunWatcherBackfill };\n return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ninterface WatcherBackfillSummary {\n pagesScanned: number;\n watchersCreated: number;\n dryRun: boolean;\n}\ntype RunWatcherBackfill = (crowi: ApiCrowi, opts?: { dryRun?: boolean }) => Promise<WatcherBackfillSummary>;\n\n/**\n * Wire the `watcher backfill` subcommand into the root program.\n *\n * Invocation:\n * crowi-admin watcher backfill [--dry-run]\n *\n * One-shot migration for pages that predate auto-watch: materialises a\n * WATCH row for each page's implicit notification set (creator + comment\n * authors + revision authors), respecting existing IGNORE opt-outs and\n * existing WATCH rows. Idempotent — safe to re-run. See\n * `@crowi/api`'s `util/watcher-backfill.ts` for the semantics.\n */\nexport function registerWatcherBackfill(program: Command): void {\n const watcher = program.command('watcher').description('Watcher / notification subscription utilities.');\n\n watcher\n .command('backfill')\n .description('Backfill WATCH rows for pages created before auto-watch (creator + comment/revision authors). Idempotent.')\n .option('--dry-run', 'Report how many WATCH rows would be created without writing anything.', false)\n .action(async (opts: { dryRun?: boolean }) => {\n // Load .env so MONGO_URI / CROWI_ENCRYPTION_KEY flow into Crowi the\n // same way `app.ts` does at boot. Silent if no .env present.\n dotenv.config();\n\n const api = loadApi();\n if (!api) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new api.Crowi(process.cwd(), process.env);\n const dryRun = Boolean(opts.dryRun);\n console.log(`[crowi-admin] watcher backfill: starting${dryRun ? ' (dry-run)' : ''}`);\n\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n const startedAt = Date.now();\n const summary = await api.runWatcherBackfill(crowi, { dryRun });\n const elapsedMs = Date.now() - startedAt;\n console.log('');\n console.log('--- summary ---');\n console.log(`pages scanned: ${summary.pagesScanned}`);\n console.log(`watchers ${summary.dryRun ? 'to create' : 'created'}: ${summary.watchersCreated}`);\n console.log(`elapsed: ${formatElapsed(elapsedMs)}`);\n console.log('');\n console.log(summary.dryRun ? 'Dry-run complete — no rows written.' : 'Backfill complete.');\n } catch (err) {\n console.error('crowi-admin: watcher backfill failed.');\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n });\n}\n\n/** Elapsed-duration formatter (mirrors search-rebuild's). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","#!/usr/bin/env node\nimport { createProgram } from './cli';\n\ncreateProgram()\n .parseAsync(process.argv)\n .catch((err: unknown) => {\n // commander throws on parse errors and on `--help` / `--version`\n // (which exits 0 internally); any error reaching here is a real\n // failure. Log + exit with a non-zero code so shell scripts can\n // detect it.\n const message = err instanceof Error ? err.message : String(err);\n console.error(`crowi-admin: ${message}`);\n process.exit(1);\n });\n"],"mappings":"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","names":["path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","formatElapsed"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/migrate.ts","../src/commands/rebuild.ts","../src/commands/replace.ts","../src/commands/watcher-backfill.ts","../src/bin.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { registerMigrate } from './commands/migrate';\nimport { registerRebuild } from './commands/rebuild';\nimport { registerReplace } from './commands/replace';\nimport { registerWatcherBackfill } from './commands/watcher-backfill';\n\n/**\n * Build the root commander program. Exported so the bin entry point\n * (`bin.ts`) can call `parseAsync` on it, and so future test harnesses\n * can drive the CLI without spawning a child process.\n *\n * Subcommands are registered via small per-command helpers\n * (`registerXxx(program)`) so each command keeps its own arg / option\n * declarations next to its implementation.\n */\nexport function createProgram(): Command {\n const program = new Command();\n program\n .name('crowi-admin')\n .description('Operator-side admin CLI for Crowi 2.0. Talks directly to MongoDB; intended for use inside the server (ssh / kubectl exec).')\n .version('0.1.0-dev');\n\n // RFC-0008: the unified migration framework namespaces. The wikilink\n // migration lives under `migrate apply --id wikilink-format` (phase 3); the\n // legacy top-level `storage copy` / `search rebuild` forms are gone (phase\n // 4) — their tasks now ride the shared runner under `rebuild storage copy` /\n // `rebuild search`. No compatibility aliases (CHANGELOG / upgrade guide).\n registerMigrate(program);\n registerRebuild(program);\n // `replace url` (literal in-body URL/host swap for v1→v2 domain changes).\n // Not a versioned migration (arbitrary from/to, re-runnable) nor a derived-\n // data rebuild (it mutates revision bodies) — its own namespace.\n registerReplace(program);\n // `watcher backfill` (idempotent WATCH-row backfill) landed on main as a\n // standalone command; kept as-is here. Could fold into the framework as a\n // `rebuild` / `migrate` task later (see TODO backlog).\n registerWatcherBackfill(program);\n\n return program;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8 — the `crowi-admin migrate plan|apply|status|list` namespace.\n *\n * One-shot, forward-only migrations. `plan` / `apply` default to the\n * `preflight` layer (§4.2.2); `--all-layers` extends to boot migrations too\n * (debugging / investigation). The boot layer is normally applied by the api\n * boot sequence, not from here.\n *\n * Like the other admin commands, this loads the api's compiled `dist/`\n * lazily (see `storage-copy.ts` for the `require.resolve` rationale — we\n * avoid importing `@crowi/api` directly so its `app.ts` auto-boot doesn't\n * fire) and talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `MigrationCliApi` façade. */\ninterface MigrationSummary {\n id: string;\n fromVersion: string;\n toVersion: string;\n layer: 'boot' | 'preflight';\n /** Present only for `preflight` migrations; `boot` rows have no severity. */\n severity?: 'blocking' | 'cosmetic';\n description: string;\n}\ninterface DetectReport {\n summary: string;\n counts?: Record<string, number>;\n}\ninterface MigrationPlanEntry extends MigrationSummary {\n pending: boolean;\n detail: DetectReport | null;\n}\ninterface MigrationStatusEntry {\n migrationId: string;\n result: string;\n appliedAt: Date;\n durationMs?: number;\n appliedBy?: string;\n}\ninterface MigrationStatus {\n latestTarget: string | null;\n recent: MigrationStatusEntry[];\n pendingPreflight: number;\n pendingBoot: number;\n}\ninterface ApplyOutcome {\n id: string;\n result: string;\n durationMs: number;\n}\ninterface MigrationCliApi {\n list(): MigrationSummary[];\n latestTarget(): string | null;\n plan(options: { allLayers?: boolean }): Promise<MigrationPlanEntry[]>;\n apply(options: { allLayers?: boolean; dryRun?: boolean; id?: string; continueOnError?: boolean }): Promise<ApplyOutcome[]>;\n status(recentLimit?: number): Promise<MigrationStatus>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateMigrationCliApi = (crowi: ApiCrowi) => MigrationCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createMigrationCliApi: CreateMigrationCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const cliApiModule = require(path.join(distDir, 'migration', 'cli-api')) as { createMigrationCliApi: CreateMigrationCliApi };\n\n return { Crowi: crowiModule.default, createMigrationCliApi: cliApiModule.createMigrationCliApi };\n}\n\n/**\n * Boot a lightweight Crowi, hand it to `fn`, then tear it down. Centralizes\n * the .env load / loadApi guard / init / teardown ceremony shared by every\n * `migrate` subcommand. Exits the process with a non-zero code on failure.\n */\nasync function withMigrationApi(fn: (api: MigrationCliApi) => Promise<void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n await fn(loaded.createMigrationCliApi(crowi));\n } catch (err) {\n console.error('crowi-admin: migrate command failed.');\n console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nfunction formatRange(entry: { fromVersion: string; toVersion: string }): string {\n return `${entry.fromVersion} → ${entry.toVersion}`;\n}\n\n/** Boot-block severity tag, or an em-dash for `boot` rows (which have no severity). */\nfunction severityTag(severity?: 'blocking' | 'cosmetic'): string {\n return severity ? `[${severity}]` : '—';\n}\n\nexport function registerMigrate(program: Command): void {\n const migrate = program.command('migrate').description('Forward-only data migrations (plan / apply / status / list).');\n\n migrate\n .command('list')\n .description('List every registered migration with its version range and layer.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const rows = api.list();\n if (opts.json) {\n console.log(JSON.stringify(rows, null, 2));\n return;\n }\n if (rows.length === 0) {\n console.log('No migrations are registered.');\n return;\n }\n console.log('ID from → to layer severity description');\n for (const r of rows) {\n console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${severityTag(r.severity).padEnd(11)} ${r.description}`);\n }\n });\n });\n\n migrate\n .command('plan')\n .description('Preview pending migrations (preflight by default).')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { allLayers: boolean; json: boolean }) => {\n await withMigrationApi(async (api) => {\n const entries = await api.plan({ allLayers: opts.allLayers });\n if (opts.json) {\n console.log(JSON.stringify({ latestTarget: api.latestTarget(), entries }, null, 2));\n return;\n }\n console.log(`Latest target: ${api.latestTarget() ?? '(none)'}`);\n console.log('');\n const pending = entries.filter((e) => e.pending);\n if (pending.length === 0) {\n console.log('No pending migrations.');\n return;\n }\n pending.forEach((e, i) => {\n console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} ${severityTag(e.severity)} (${formatRange(e)})`);\n console.log(` ${e.description}`);\n console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : 'Detected: details unavailable (no detect stage; isPending = true)'}`);\n });\n console.log('');\n console.log('Run `crowi-admin migrate apply` to execute preflight migrations.');\n });\n });\n\n migrate\n .command('apply')\n .description('Apply pending migrations (preflight by default), in version-range + order sequence.')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--dry-run', 'Run detect only; stages no-op and nothing is recorded.', false)\n .option('--id <id>', 'Apply only the migration with this id.')\n .option('--continue-on-error', 'Continue with later migrations after a failure (default: abort).', false)\n .action(async (opts: { allLayers: boolean; dryRun: boolean; id?: string; continueOnError: boolean }) => {\n await withMigrationApi(async (api) => {\n const outcomes = await api.apply({ allLayers: opts.allLayers, dryRun: opts.dryRun, id: opts.id, continueOnError: opts.continueOnError });\n if (outcomes.length === 0) {\n console.log('No migrations to apply.');\n return;\n }\n for (const o of outcomes) {\n console.log(` ${o.id.padEnd(25)} → ${o.result} (${o.durationMs}ms)`);\n }\n const failed = outcomes.filter((o) => o.result === 'failed');\n if (failed.length > 0) {\n throw new Error(`${failed.length} migration(s) failed: ${failed.map((f) => f.id).join(', ')}`);\n }\n });\n });\n\n migrate\n .command('status')\n .description('Show recent migration applications and pending counts.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const status = await api.status();\n if (opts.json) {\n console.log(JSON.stringify(status, null, 2));\n return;\n }\n console.log(`Latest target: ${status.latestTarget ?? '(none)'}`);\n console.log('');\n console.log('Recent applications (last 10):');\n if (status.recent.length === 0) {\n console.log(' (none)');\n } else {\n for (const r of status.recent) {\n const date = r.appliedAt.toISOString().slice(0, 10);\n const elapsed = r.durationMs !== undefined ? `${r.durationMs}ms` : '-';\n console.log(` ${date} ${r.result.padEnd(14)} ${r.migrationId.padEnd(25)} (${elapsed}, ${r.appliedBy ?? '-'})`);\n }\n }\n console.log('');\n console.log(`Pending preflight: ${status.pendingPreflight} migration(s)`);\n console.log(`Pending boot: ${status.pendingBoot} migration(s)`);\n });\n });\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8.5 — the `crowi-admin rebuild <target>` namespace.\n *\n * Operational rebuilds of derived data — version-independent, runnable any\n * time, any number of times (no pending/applied concept). All targets route\n * through the api-side `RebuildRunner`, so they share the framework runner's\n * `--dry-run` / progress / SIGINT / structured-logging conventions with\n * `migrate` (§4.3) — but a rebuild never touches `migrationApplications`\n * (§8.5).\n *\n * Targets:\n * - `rebuild search` ← ported from the old top-level `search rebuild`\n * - `rebuild storage copy` ← ported from the old top-level `storage copy`\n * - `rebuild renderer` ← new; util/rebuild-renderer.ts skeleton (TODO)\n * - `rebuild backlink` ← new; util/rebuild-backlink.ts skeleton (TODO)\n *\n * Like the other admin commands, this loads the api's compiled `dist/` lazily\n * (see `storage-copy.ts` for the `require.resolve` rationale — we avoid\n * importing `@crowi/api` directly so its `app.ts` auto-boot doesn't fire) and\n * talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `RebuildOutcome`. */\ninterface RebuildOutcome {\n id: string;\n durationMs: number;\n interrupted: boolean;\n stats: Record<string, unknown>;\n}\ninterface RebuildProgress {\n onLabel?: (label: string) => void;\n onIncrement?: (current: number) => void;\n}\ninterface RebuildCliApi {\n rebuildSearch(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildStorageCopy(opts: { from: string; to: string; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildRenderer(opts?: { onlyStale?: boolean; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildBacklink(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateRebuildCliApi = (crowi: ApiCrowi) => RebuildCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createRebuildCliApi: CreateRebuildCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const apiModule = require(path.join(distDir, 'migration', 'rebuild-api')) as { createRebuildCliApi: CreateRebuildCliApi };\n\n return { Crowi: crowiModule.default, createRebuildCliApi: apiModule.createRebuildCliApi };\n}\n\n/**\n * Map a completed rebuild outcome to a process exit code, mirroring the legacy\n * storage-copy convention:\n * - 0 — success (everything copied / rebuilt, or dry-run)\n * - 2 — partial: the run completed but >=1 unit failed (operator should retry)\n *\n * Kept as a pure function (no `process.exit`) so the partial→2 mapping is unit\n * testable without the surrounding boot ceremony. `process.exit(code)` ignores\n * `process.exitCode`, so the exit code must flow through here and be passed\n * explicitly — a fn that merely sets `process.exitCode = 2` would be clobbered\n * by the `process.exit(0)` in `withRebuildApi`.\n *\n * Fatal failures (init failed, or a task threw — e.g. renderer/backlink NOT_YET)\n * are exit 1 and handled in `withRebuildApi`; they never reach here.\n */\nexport function rebuildExitCode(outcome: RebuildOutcome): number {\n const failed = outcome.stats.failed;\n if (typeof failed === 'number' && failed > 0) return 2;\n return 0;\n}\n\n/**\n * Boot a lightweight Crowi, hand the rebuild façade to `fn`, then tear it\n * down. Centralizes the .env load / loadApi guard / init / teardown ceremony\n * shared by every `rebuild` subcommand.\n *\n * `fn` returns the success exit code (0 normally, 2 for a partial run — see\n * `rebuildExitCode`); a fatal failure (init error or a thrown task) overrides\n * it with exit 1. The resolved code is passed explicitly to `process.exit`,\n * since an explicit argument ignores any `process.exitCode` a callee set.\n */\nasync function withRebuildApi(fn: (api: RebuildCliApi) => Promise<number | void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n exitCode = (await fn(loaded.createRebuildCliApi(crowi))) ?? 0;\n } catch (err) {\n console.error('crowi-admin: rebuild failed.');\n printError(err);\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nexport function registerRebuild(program: Command): void {\n const rebuild = program.command('rebuild').description('Operational rebuilds of derived data (renderer / search / backlink / storage copy).');\n\n rebuild\n .command('renderer')\n .description('Regenerate cached rendered HTML for pages.')\n .option('--only-stale', 'Only re-render pages whose cache is stale.', false)\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { onlyStale: boolean; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildRenderer({ onlyStale: opts.onlyStale, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('renderer', outcome);\n });\n });\n\n rebuild\n .command('search')\n .description(\"Rebuild the search index from scratch using the active driver's rebuild().\")\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildSearch({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('search', outcome);\n });\n });\n\n rebuild\n .command('backlink')\n .description('Rebuild the backlink index across all pages.')\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildBacklink({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('backlink', outcome);\n });\n });\n\n const storage = rebuild.command('storage').description('Storage driver rebuilds.');\n storage\n .command('copy')\n .description('Copy every stored object from one driver to another.')\n .requiredOption('--from <name>', 'Source storage driver name (e.g. local, s3).')\n .requiredOption('--to <name>', 'Destination storage driver name.')\n .option('--dry-run', 'List candidate keys without copying anything.', false)\n .action(async (opts: { from: string; to: string; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildStorageCopy({ from: opts.from, to: opts.to, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('storage copy', outcome);\n // Mirror the legacy exit-code convention: partial (>=1 key failed) → 2.\n return rebuildExitCode(outcome);\n });\n });\n}\n\n/**\n * A progress sink that renders the runner's per-unit label to stderr so a\n * long rebuild shows live activity without flooding stdout (which carries the\n * final summary). Mirrors the spirit of `storage-copy.ts`'s `renderProgress`.\n */\nfunction liveProgress(): RebuildProgress {\n return {\n onLabel: (label) => {\n // Single-line, low-noise: enough to confirm the run is alive.\n process.stderr.write(` ${label}\\n`);\n },\n };\n}\n\n/** Print the final summary block, including each stat key the task returned. */\nfunction printOutcome(target: string, outcome: RebuildOutcome): void {\n console.log('');\n console.log('--- summary ---');\n console.log(`target: ${target}`);\n for (const [key, value] of Object.entries(outcome.stats)) {\n console.log(`${`${key}:`.padEnd(10)}${formatStat(value)}`);\n }\n console.log(`elapsed: ${formatElapsed(outcome.durationMs)}`);\n if (outcome.interrupted) {\n console.log('');\n console.log('Interrupted by SIGINT before completion — re-run to finish.');\n return;\n }\n console.log('');\n console.log(`Rebuild '${target}' complete.`);\n}\n\nfunction formatStat(value: unknown): string {\n if (Array.isArray(value)) return value.length === 0 ? '(none)' : value.join(', ');\n return String(value);\n}\n\n/**\n * Render whatever detail we can extract from a thrown error. The ES JS\n * client's `ResponseError` puts the cluster's actual response on `meta.body`\n * and leaves `.message` as just the HTTP status string, so walk the common\n * shapes (preserved from the old `search rebuild` command).\n */\nfunction printError(err: unknown): void {\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n const meta = (err as Error & { meta?: { statusCode?: number; body?: unknown } }).meta;\n if (meta) {\n if (meta.statusCode !== undefined) console.error(` status: ${meta.statusCode}`);\n if (meta.body !== undefined) {\n try {\n console.error(` body: ${JSON.stringify(meta.body, null, 2)}`);\n } catch {\n console.error(` body: ${String(meta.body)}`);\n }\n }\n }\n const cause = (err as Error & { cause?: unknown }).cause;\n if (cause !== undefined) console.error(` cause: ${cause instanceof Error ? cause.message || cause.name : String(cause)}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n}\n\n/** Render an elapsed millisecond duration (\"412ms\" / \"28m12s\"). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","import path from 'node:path';\nimport readline from 'node:readline/promises';\nimport type { Command } from 'commander';\nimport dotenv from 'dotenv';\n\n/**\n * feature-url-replace-admin-cli — the `crowi-admin replace` namespace.\n *\n * `replace url --from <url> --to <url>` swaps a literal URL/host string in every\n * page body — the fix for a v1→v2 migration that changed the public domain and\n * left absolute URLs (image embeds / links) pinned to the old host. Page / file\n * ids are carried over unchanged, so this is a literal host swap, not an id remap.\n *\n * Like the other admin commands this loads the api's compiled `dist/` lazily\n * (see `migrate.ts` for the `require.resolve` rationale — we avoid importing\n * `@crowi/api` directly so its `app.ts` auto-boot doesn't fire) and talks to\n * MongoDB directly. The heavy lifting (scan + quiet rewrite that pushes a new\n * revision WITHOUT bumping updatedAt / notifying watchers) lives in\n * `@crowi/api`'s `util/replace-url.ts`; this file is the CLI surface:\n * arg-safety guard, preview, confirmation, summary, exit code.\n */\n\n/** Structural mirror of the api-side `ReplaceSafety`. */\ninterface ReplaceSafety {\n errors: string[];\n warnings: string[];\n bareHostFrom: boolean;\n}\n/** Structural mirror of the api-side preview / summary shapes. */\ninterface ReplaceUrlSample {\n path: string;\n occurrences: number;\n snippet: string;\n}\ninterface ReplaceUrlPreview {\n pagesMatched: number;\n occurrences: number;\n samples: ReplaceUrlSample[];\n}\ninterface ReplaceUrlSummary extends ReplaceUrlPreview {\n from: string;\n to: string;\n dryRun: boolean;\n aborted: boolean;\n pagesScanned: number;\n pagesRewritten: number;\n failed: number;\n interrupted: boolean;\n actingUserEmail?: string;\n}\ninterface ReplaceUrlOptions {\n from: string;\n to: string;\n userEmail?: string;\n dryRun?: boolean;\n includeTrash?: boolean;\n confirm?: (preview: ReplaceUrlPreview) => Promise<boolean>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype RunReplaceUrl = (crowi: ApiCrowi, opts: ReplaceUrlOptions) => Promise<ReplaceUrlSummary>;\ntype AssessReplaceSafety = (from: string, to: string) => ReplaceSafety;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; runReplaceUrl: RunReplaceUrl; assessReplaceSafety: AssessReplaceSafety } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const distDir = path.join(path.dirname(apiPkgPath), 'dist');\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const replaceModule = require(path.join(distDir, 'util', 'replace-url')) as { runReplaceUrl: RunReplaceUrl; assessReplaceSafety: AssessReplaceSafety };\n return { Crowi: crowiModule.default, runReplaceUrl: replaceModule.runReplaceUrl, assessReplaceSafety: replaceModule.assessReplaceSafety };\n}\n\n/**\n * Map a completed summary to a process exit code (mirrors `rebuildExitCode`):\n * - 2 — partial: the run finished but >=1 page failed (operator should retry)\n * - 0 — success / dry-run / declined\n * Fatal failures (init / thrown) are exit 1 and handled in the action.\n */\nexport function replaceExitCode(summary: { failed: number }): number {\n return summary.failed > 0 ? 2 : 0;\n}\n\nexport function registerReplace(program: Command): void {\n const replace = program.command('replace').description('Bulk content replacements across page bodies.');\n\n replace\n .command('url')\n .description(\n 'Replace a literal URL/host string in every page body (e.g. after a domain change). Pushes a new revision per page WITHOUT bumping updatedAt or notifying watchers.',\n )\n .requiredOption('--from <s>', 'String to replace. Use a full origin to be safe, e.g. https://old.example.')\n .requiredOption('--to <s>', 'Replacement string, e.g. https://new.example.')\n .option('--dry-run', 'Report what would change without writing.', false)\n .option('--include-trash', 'Also rewrite trashed / deprecated pages (default: published only).', false)\n .option('--user <email>', 'Author recorded on the new revisions (defaults to the oldest admin).')\n .option('--yes', 'Skip the interactive confirmation prompt.', false)\n .option('--force', 'Proceed even when --from looks unsafe (e.g. a bare host without a scheme).', false)\n .action(async (opts: { from: string; to: string; dryRun?: boolean; includeTrash?: boolean; user?: string; yes?: boolean; force?: boolean }) => {\n // Load .env so MONGO_URI / CROWI_ENCRYPTION_KEY flow into Crowi the same\n // way app.ts does at boot. Silent if no .env present.\n dotenv.config();\n\n const api = loadApi();\n if (!api) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const from = String(opts.from);\n const to = String(opts.to);\n\n // Cheap pre-flight guard (no DB): fail fast on footguns before booting.\n const safety = api.assessReplaceSafety(from, to);\n for (const w of safety.warnings) console.warn(`crowi-admin: warning: ${w}`);\n if (safety.errors.length > 0) {\n for (const e of safety.errors) console.error(`crowi-admin: ${e}`);\n process.exit(1);\n }\n if (safety.bareHostFrom && !opts.force) {\n console.error(\n `crowi-admin: --from='${from}' has no scheme. A bare host can corrupt longer hosts that start with it (e.g. '${from}' is a prefix of '${from}t'). Re-run with a full origin (e.g. https://${from}) or pass --force to override.`,\n );\n process.exit(1);\n }\n\n const crowi = new api.Crowi(process.cwd(), process.env);\n console.log(`[crowi-admin] replace url: '${from}' → '${to}'${opts.dryRun ? ' (dry-run)' : ''}`);\n\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n const startedAt = Date.now();\n const summary = await api.runReplaceUrl(crowi, {\n from,\n to,\n userEmail: opts.user,\n dryRun: Boolean(opts.dryRun),\n includeTrash: Boolean(opts.includeTrash),\n confirm: opts.dryRun ? undefined : (preview) => confirmProceed(preview, Boolean(opts.yes)),\n });\n printSummary(summary, Date.now() - startedAt);\n exitCode = replaceExitCode(summary);\n } catch (err) {\n console.error('crowi-admin: replace url failed.');\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n });\n}\n\n/** Print the matched pages + samples, then ask for confirmation (unless --yes). */\nasync function confirmProceed(preview: ReplaceUrlPreview, yes: boolean): Promise<boolean> {\n printPreview(preview);\n if (preview.pagesMatched === 0) return false; // nothing to do\n if (yes) return true;\n // A bulk body rewrite is hard to undo en masse, so refuse to write blind.\n if (!process.stdin.isTTY) {\n console.error('');\n console.error('crowi-admin: refusing to write without confirmation (no TTY). Re-run with --yes to proceed, or --dry-run to preview.');\n return false;\n }\n const rl = readline.createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = (await rl.question(`Rewrite ${preview.pagesMatched} page(s)? [y/N] `)).trim().toLowerCase();\n return answer === 'y' || answer === 'yes';\n } finally {\n rl.close();\n }\n}\n\nfunction printPreview(preview: ReplaceUrlPreview): void {\n console.log('');\n console.log(`Matched ${preview.pagesMatched} page(s), ${preview.occurrences} occurrence(s).`);\n for (const s of preview.samples) {\n console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);\n }\n const more = preview.pagesMatched - preview.samples.length;\n if (more > 0) console.log(` … and ${more} more page(s)`);\n}\n\nfunction printSummary(summary: ReplaceUrlSummary, elapsedMs: number): void {\n console.log('');\n console.log('--- summary ---');\n console.log(`from: ${summary.from}`);\n console.log(`to: ${summary.to}`);\n console.log(`scanned: ${summary.pagesScanned} page(s)`);\n console.log(`matched: ${summary.pagesMatched} page(s), ${summary.occurrences} occurrence(s)`);\n\n if (summary.pagesMatched === 0) {\n console.log('');\n console.log(`No pages contain '${summary.from}'.`);\n return;\n }\n\n if (summary.dryRun) {\n for (const s of summary.samples) {\n console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);\n }\n const more = summary.pagesMatched - summary.samples.length;\n if (more > 0) console.log(` … and ${more} more page(s)`);\n console.log('');\n console.log('Dry-run complete — no pages written.');\n return;\n }\n\n if (summary.aborted) {\n console.log('');\n console.log('Aborted — no pages written.');\n return;\n }\n\n console.log(`rewritten: ${summary.pagesRewritten} page(s)`);\n if (summary.failed > 0) console.log(`failed: ${summary.failed} page(s)`);\n if (summary.actingUserEmail) console.log(`author: ${summary.actingUserEmail}`);\n console.log(`elapsed: ${formatElapsed(elapsedMs)}`);\n\n if (summary.interrupted) {\n console.log('');\n console.log('Interrupted by SIGINT before completion — re-run to finish.');\n return;\n }\n console.log('');\n console.log(\"Replacement complete. Run 'crowi-admin rebuild search' to refresh the search index (page rendering is already up to date).\");\n}\n\n/** Elapsed-duration formatter (mirrors watcher-backfill / rebuild). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * Resolve @crowi/api's installed location relative to the caller's CWD\n * (= the runner directory) and load the bits we need, the same way\n * `search-rebuild.ts` / `storage-copy.ts` do (manual `require` so\n * `@crowi/api`'s `app.ts` auto-boot doesn't fire). Returns `null` when\n * the package isn't found so the caller can print a friendly error.\n */\nfunction loadApi(): { Crowi: ApiCrowiCtor; runWatcherBackfill: RunWatcherBackfill } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const distDir = path.join(path.dirname(apiPkgPath), 'dist');\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const backfillModule = require(path.join(distDir, 'util', 'watcher-backfill')) as { runWatcherBackfill: RunWatcherBackfill };\n return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ninterface WatcherBackfillSummary {\n pagesScanned: number;\n watchersCreated: number;\n dryRun: boolean;\n}\ntype RunWatcherBackfill = (crowi: ApiCrowi, opts?: { dryRun?: boolean }) => Promise<WatcherBackfillSummary>;\n\n/**\n * Wire the `watcher backfill` subcommand into the root program.\n *\n * Invocation:\n * crowi-admin watcher backfill [--dry-run]\n *\n * One-shot migration for pages that predate auto-watch: materialises a\n * WATCH row for each page's implicit notification set (creator + comment\n * authors + revision authors), respecting existing IGNORE opt-outs and\n * existing WATCH rows. Idempotent — safe to re-run. See\n * `@crowi/api`'s `util/watcher-backfill.ts` for the semantics.\n */\nexport function registerWatcherBackfill(program: Command): void {\n const watcher = program.command('watcher').description('Watcher / notification subscription utilities.');\n\n watcher\n .command('backfill')\n .description('Backfill WATCH rows for pages created before auto-watch (creator + comment/revision authors). Idempotent.')\n .option('--dry-run', 'Report how many WATCH rows would be created without writing anything.', false)\n .action(async (opts: { dryRun?: boolean }) => {\n // Load .env so MONGO_URI / CROWI_ENCRYPTION_KEY flow into Crowi the\n // same way `app.ts` does at boot. Silent if no .env present.\n dotenv.config();\n\n const api = loadApi();\n if (!api) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new api.Crowi(process.cwd(), process.env);\n const dryRun = Boolean(opts.dryRun);\n console.log(`[crowi-admin] watcher backfill: starting${dryRun ? ' (dry-run)' : ''}`);\n\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n const startedAt = Date.now();\n const summary = await api.runWatcherBackfill(crowi, { dryRun });\n const elapsedMs = Date.now() - startedAt;\n console.log('');\n console.log('--- summary ---');\n console.log(`pages scanned: ${summary.pagesScanned}`);\n console.log(`watchers ${summary.dryRun ? 'to create' : 'created'}: ${summary.watchersCreated}`);\n console.log(`elapsed: ${formatElapsed(elapsedMs)}`);\n console.log('');\n console.log(summary.dryRun ? 'Dry-run complete — no rows written.' : 'Backfill complete.');\n } catch (err) {\n console.error('crowi-admin: watcher backfill failed.');\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n });\n}\n\n/** Elapsed-duration formatter (mirrors search-rebuild's). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","#!/usr/bin/env node\nimport { createProgram } from './cli';\n\ncreateProgram()\n .parseAsync(process.argv)\n .catch((err: unknown) => {\n // commander throws on parse errors and on `--help` / `--version`\n // (which exits 0 internally); any error reaching here is a real\n // failure. Log + exit with a non-zero code so shell scripts can\n // detect it.\n const message = err instanceof Error ? err.message : String(err);\n console.error(`crowi-admin: ${message}`);\n process.exit(1);\n });\n"],"mappings":"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","names":["path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","readline","formatElapsed","import_node_path","import_dotenv","loadApi","path","dotenv","formatElapsed"]}
|
package/dist/cli.js
CHANGED
|
@@ -81,6 +81,9 @@ async function withMigrationApi(fn) {
|
|
|
81
81
|
function formatRange(entry) {
|
|
82
82
|
return `${entry.fromVersion} \u2192 ${entry.toVersion}`;
|
|
83
83
|
}
|
|
84
|
+
function severityTag(severity) {
|
|
85
|
+
return severity ? `[${severity}]` : "\u2014";
|
|
86
|
+
}
|
|
84
87
|
function registerMigrate(program) {
|
|
85
88
|
const migrate = program.command("migrate").description("Forward-only data migrations (plan / apply / status / list).");
|
|
86
89
|
migrate.command("list").description("List every registered migration with its version range and layer.").option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
@@ -94,9 +97,9 @@ function registerMigrate(program) {
|
|
|
94
97
|
console.log("No migrations are registered.");
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
97
|
-
console.log("ID from \u2192 to layer description");
|
|
100
|
+
console.log("ID from \u2192 to layer severity description");
|
|
98
101
|
for (const r of rows) {
|
|
99
|
-
console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${r.description}`);
|
|
102
|
+
console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${severityTag(r.severity).padEnd(11)} ${r.description}`);
|
|
100
103
|
}
|
|
101
104
|
});
|
|
102
105
|
});
|
|
@@ -115,7 +118,7 @@ function registerMigrate(program) {
|
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
117
120
|
pending.forEach((e, i) => {
|
|
118
|
-
console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} (${formatRange(e)})`);
|
|
121
|
+
console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} ${severityTag(e.severity)} (${formatRange(e)})`);
|
|
119
122
|
console.log(` ${e.description}`);
|
|
120
123
|
console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : "Detected: details unavailable (no detect stage; isPending = true)"}`);
|
|
121
124
|
});
|
|
@@ -300,8 +303,9 @@ function formatElapsed(ms) {
|
|
|
300
303
|
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
301
304
|
}
|
|
302
305
|
|
|
303
|
-
// src/commands/
|
|
306
|
+
// src/commands/replace.ts
|
|
304
307
|
var import_node_path3 = __toESM(require("path"));
|
|
308
|
+
var import_promises = __toESM(require("readline/promises"));
|
|
305
309
|
var import_dotenv3 = __toESM(require("dotenv"));
|
|
306
310
|
function loadApi3() {
|
|
307
311
|
let apiPkgPath;
|
|
@@ -312,14 +316,168 @@ function loadApi3() {
|
|
|
312
316
|
}
|
|
313
317
|
const distDir = import_node_path3.default.join(import_node_path3.default.dirname(apiPkgPath), "dist");
|
|
314
318
|
const crowiModule = require(import_node_path3.default.join(distDir, "crowi"));
|
|
315
|
-
const
|
|
319
|
+
const replaceModule = require(import_node_path3.default.join(distDir, "util", "replace-url"));
|
|
320
|
+
return { Crowi: crowiModule.default, runReplaceUrl: replaceModule.runReplaceUrl, assessReplaceSafety: replaceModule.assessReplaceSafety };
|
|
321
|
+
}
|
|
322
|
+
function replaceExitCode(summary) {
|
|
323
|
+
return summary.failed > 0 ? 2 : 0;
|
|
324
|
+
}
|
|
325
|
+
function registerReplace(program) {
|
|
326
|
+
const replace = program.command("replace").description("Bulk content replacements across page bodies.");
|
|
327
|
+
replace.command("url").description(
|
|
328
|
+
"Replace a literal URL/host string in every page body (e.g. after a domain change). Pushes a new revision per page WITHOUT bumping updatedAt or notifying watchers."
|
|
329
|
+
).requiredOption("--from <s>", "String to replace. Use a full origin to be safe, e.g. https://old.example.").requiredOption("--to <s>", "Replacement string, e.g. https://new.example.").option("--dry-run", "Report what would change without writing.", false).option("--include-trash", "Also rewrite trashed / deprecated pages (default: published only).", false).option("--user <email>", "Author recorded on the new revisions (defaults to the oldest admin).").option("--yes", "Skip the interactive confirmation prompt.", false).option("--force", "Proceed even when --from looks unsafe (e.g. a bare host without a scheme).", false).action(async (opts) => {
|
|
330
|
+
import_dotenv3.default.config();
|
|
331
|
+
const api = loadApi3();
|
|
332
|
+
if (!api) {
|
|
333
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
const from = String(opts.from);
|
|
337
|
+
const to = String(opts.to);
|
|
338
|
+
const safety = api.assessReplaceSafety(from, to);
|
|
339
|
+
for (const w of safety.warnings) console.warn(`crowi-admin: warning: ${w}`);
|
|
340
|
+
if (safety.errors.length > 0) {
|
|
341
|
+
for (const e of safety.errors) console.error(`crowi-admin: ${e}`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
if (safety.bareHostFrom && !opts.force) {
|
|
345
|
+
console.error(
|
|
346
|
+
`crowi-admin: --from='${from}' has no scheme. A bare host can corrupt longer hosts that start with it (e.g. '${from}' is a prefix of '${from}t'). Re-run with a full origin (e.g. https://${from}) or pass --force to override.`
|
|
347
|
+
);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
const crowi = new api.Crowi(process.cwd(), process.env);
|
|
351
|
+
console.log(`[crowi-admin] replace url: '${from}' \u2192 '${to}'${opts.dryRun ? " (dry-run)" : ""}`);
|
|
352
|
+
try {
|
|
353
|
+
await crowi.initForCli();
|
|
354
|
+
} catch (err) {
|
|
355
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
356
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
let exitCode = 0;
|
|
360
|
+
try {
|
|
361
|
+
const startedAt = Date.now();
|
|
362
|
+
const summary = await api.runReplaceUrl(crowi, {
|
|
363
|
+
from,
|
|
364
|
+
to,
|
|
365
|
+
userEmail: opts.user,
|
|
366
|
+
dryRun: Boolean(opts.dryRun),
|
|
367
|
+
includeTrash: Boolean(opts.includeTrash),
|
|
368
|
+
confirm: opts.dryRun ? void 0 : (preview) => confirmProceed(preview, Boolean(opts.yes))
|
|
369
|
+
});
|
|
370
|
+
printSummary(summary, Date.now() - startedAt);
|
|
371
|
+
exitCode = replaceExitCode(summary);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.error("crowi-admin: replace url failed.");
|
|
374
|
+
if (err instanceof Error) {
|
|
375
|
+
if (err.message) console.error(` message: ${err.message}`);
|
|
376
|
+
if (err.stack) console.error(err.stack);
|
|
377
|
+
} else {
|
|
378
|
+
console.error(` thrown: ${String(err)}`);
|
|
379
|
+
}
|
|
380
|
+
exitCode = 1;
|
|
381
|
+
} finally {
|
|
382
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
383
|
+
}
|
|
384
|
+
process.exit(exitCode);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async function confirmProceed(preview, yes) {
|
|
388
|
+
printPreview(preview);
|
|
389
|
+
if (preview.pagesMatched === 0) return false;
|
|
390
|
+
if (yes) return true;
|
|
391
|
+
if (!process.stdin.isTTY) {
|
|
392
|
+
console.error("");
|
|
393
|
+
console.error("crowi-admin: refusing to write without confirmation (no TTY). Re-run with --yes to proceed, or --dry-run to preview.");
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
397
|
+
try {
|
|
398
|
+
const answer = (await rl.question(`Rewrite ${preview.pagesMatched} page(s)? [y/N] `)).trim().toLowerCase();
|
|
399
|
+
return answer === "y" || answer === "yes";
|
|
400
|
+
} finally {
|
|
401
|
+
rl.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function printPreview(preview) {
|
|
405
|
+
console.log("");
|
|
406
|
+
console.log(`Matched ${preview.pagesMatched} page(s), ${preview.occurrences} occurrence(s).`);
|
|
407
|
+
for (const s of preview.samples) {
|
|
408
|
+
console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);
|
|
409
|
+
}
|
|
410
|
+
const more = preview.pagesMatched - preview.samples.length;
|
|
411
|
+
if (more > 0) console.log(` \u2026 and ${more} more page(s)`);
|
|
412
|
+
}
|
|
413
|
+
function printSummary(summary, elapsedMs) {
|
|
414
|
+
console.log("");
|
|
415
|
+
console.log("--- summary ---");
|
|
416
|
+
console.log(`from: ${summary.from}`);
|
|
417
|
+
console.log(`to: ${summary.to}`);
|
|
418
|
+
console.log(`scanned: ${summary.pagesScanned} page(s)`);
|
|
419
|
+
console.log(`matched: ${summary.pagesMatched} page(s), ${summary.occurrences} occurrence(s)`);
|
|
420
|
+
if (summary.pagesMatched === 0) {
|
|
421
|
+
console.log("");
|
|
422
|
+
console.log(`No pages contain '${summary.from}'.`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (summary.dryRun) {
|
|
426
|
+
for (const s of summary.samples) {
|
|
427
|
+
console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);
|
|
428
|
+
}
|
|
429
|
+
const more = summary.pagesMatched - summary.samples.length;
|
|
430
|
+
if (more > 0) console.log(` \u2026 and ${more} more page(s)`);
|
|
431
|
+
console.log("");
|
|
432
|
+
console.log("Dry-run complete \u2014 no pages written.");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (summary.aborted) {
|
|
436
|
+
console.log("");
|
|
437
|
+
console.log("Aborted \u2014 no pages written.");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
console.log(`rewritten: ${summary.pagesRewritten} page(s)`);
|
|
441
|
+
if (summary.failed > 0) console.log(`failed: ${summary.failed} page(s)`);
|
|
442
|
+
if (summary.actingUserEmail) console.log(`author: ${summary.actingUserEmail}`);
|
|
443
|
+
console.log(`elapsed: ${formatElapsed2(elapsedMs)}`);
|
|
444
|
+
if (summary.interrupted) {
|
|
445
|
+
console.log("");
|
|
446
|
+
console.log("Interrupted by SIGINT before completion \u2014 re-run to finish.");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
console.log("");
|
|
450
|
+
console.log("Replacement complete. Run 'crowi-admin rebuild search' to refresh the search index (page rendering is already up to date).");
|
|
451
|
+
}
|
|
452
|
+
function formatElapsed2(ms) {
|
|
453
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
454
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
455
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
456
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
457
|
+
const seconds = totalSeconds % 60;
|
|
458
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/commands/watcher-backfill.ts
|
|
462
|
+
var import_node_path4 = __toESM(require("path"));
|
|
463
|
+
var import_dotenv4 = __toESM(require("dotenv"));
|
|
464
|
+
function loadApi4() {
|
|
465
|
+
let apiPkgPath;
|
|
466
|
+
try {
|
|
467
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
468
|
+
} catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
const distDir = import_node_path4.default.join(import_node_path4.default.dirname(apiPkgPath), "dist");
|
|
472
|
+
const crowiModule = require(import_node_path4.default.join(distDir, "crowi"));
|
|
473
|
+
const backfillModule = require(import_node_path4.default.join(distDir, "util", "watcher-backfill"));
|
|
316
474
|
return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };
|
|
317
475
|
}
|
|
318
476
|
function registerWatcherBackfill(program) {
|
|
319
477
|
const watcher = program.command("watcher").description("Watcher / notification subscription utilities.");
|
|
320
478
|
watcher.command("backfill").description("Backfill WATCH rows for pages created before auto-watch (creator + comment/revision authors). Idempotent.").option("--dry-run", "Report how many WATCH rows would be created without writing anything.", false).action(async (opts) => {
|
|
321
|
-
|
|
322
|
-
const api =
|
|
479
|
+
import_dotenv4.default.config();
|
|
480
|
+
const api = loadApi4();
|
|
323
481
|
if (!api) {
|
|
324
482
|
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
325
483
|
process.exit(1);
|
|
@@ -343,7 +501,7 @@ function registerWatcherBackfill(program) {
|
|
|
343
501
|
console.log("--- summary ---");
|
|
344
502
|
console.log(`pages scanned: ${summary.pagesScanned}`);
|
|
345
503
|
console.log(`watchers ${summary.dryRun ? "to create" : "created"}: ${summary.watchersCreated}`);
|
|
346
|
-
console.log(`elapsed: ${
|
|
504
|
+
console.log(`elapsed: ${formatElapsed3(elapsedMs)}`);
|
|
347
505
|
console.log("");
|
|
348
506
|
console.log(summary.dryRun ? "Dry-run complete \u2014 no rows written." : "Backfill complete.");
|
|
349
507
|
} catch (err) {
|
|
@@ -361,7 +519,7 @@ function registerWatcherBackfill(program) {
|
|
|
361
519
|
process.exit(exitCode);
|
|
362
520
|
});
|
|
363
521
|
}
|
|
364
|
-
function
|
|
522
|
+
function formatElapsed3(ms) {
|
|
365
523
|
if (ms < 1e3) return `${ms}ms`;
|
|
366
524
|
const totalSeconds = Math.round(ms / 1e3);
|
|
367
525
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -376,6 +534,7 @@ function createProgram() {
|
|
|
376
534
|
program.name("crowi-admin").description("Operator-side admin CLI for Crowi 2.0. Talks directly to MongoDB; intended for use inside the server (ssh / kubectl exec).").version("0.1.0-dev");
|
|
377
535
|
registerMigrate(program);
|
|
378
536
|
registerRebuild(program);
|
|
537
|
+
registerReplace(program);
|
|
379
538
|
registerWatcherBackfill(program);
|
|
380
539
|
return program;
|
|
381
540
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/commands/migrate.ts","../src/commands/rebuild.ts","../src/commands/watcher-backfill.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { registerMigrate } from './commands/migrate';\nimport { registerRebuild } from './commands/rebuild';\nimport { registerWatcherBackfill } from './commands/watcher-backfill';\n\n/**\n * Build the root commander program. Exported so the bin entry point\n * (`bin.ts`) can call `parseAsync` on it, and so future test harnesses\n * can drive the CLI without spawning a child process.\n *\n * Subcommands are registered via small per-command helpers\n * (`registerXxx(program)`) so each command keeps its own arg / option\n * declarations next to its implementation.\n */\nexport function createProgram(): Command {\n const program = new Command();\n program\n .name('crowi-admin')\n .description('Operator-side admin CLI for Crowi 2.0. Talks directly to MongoDB; intended for use inside the server (ssh / kubectl exec).')\n .version('0.1.0-dev');\n\n // RFC-0008: the unified migration framework namespaces. The wikilink\n // migration lives under `migrate apply --id wikilink-format` (phase 3); the\n // legacy top-level `storage copy` / `search rebuild` forms are gone (phase\n // 4) — their tasks now ride the shared runner under `rebuild storage copy` /\n // `rebuild search`. No compatibility aliases (CHANGELOG / upgrade guide).\n registerMigrate(program);\n registerRebuild(program);\n // `watcher backfill` (idempotent WATCH-row backfill) landed on main as a\n // standalone command; kept as-is here. Could fold into the framework as a\n // `rebuild` / `migrate` task later (see TODO backlog).\n registerWatcherBackfill(program);\n\n return program;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8 — the `crowi-admin migrate plan|apply|status|list` namespace.\n *\n * One-shot, forward-only migrations. `plan` / `apply` default to the\n * `preflight` layer (§4.2.2); `--all-layers` extends to boot migrations too\n * (debugging / investigation). The boot layer is normally applied by the api\n * boot sequence, not from here.\n *\n * Like the other admin commands, this loads the api's compiled `dist/`\n * lazily (see `storage-copy.ts` for the `require.resolve` rationale — we\n * avoid importing `@crowi/api` directly so its `app.ts` auto-boot doesn't\n * fire) and talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `MigrationCliApi` façade. */\ninterface MigrationSummary {\n id: string;\n fromVersion: string;\n toVersion: string;\n layer: 'boot' | 'preflight';\n description: string;\n}\ninterface DetectReport {\n summary: string;\n counts?: Record<string, number>;\n}\ninterface MigrationPlanEntry extends MigrationSummary {\n pending: boolean;\n detail: DetectReport | null;\n}\ninterface MigrationStatusEntry {\n migrationId: string;\n result: string;\n appliedAt: Date;\n durationMs?: number;\n appliedBy?: string;\n}\ninterface MigrationStatus {\n latestTarget: string | null;\n recent: MigrationStatusEntry[];\n pendingPreflight: number;\n pendingBoot: number;\n}\ninterface ApplyOutcome {\n id: string;\n result: string;\n durationMs: number;\n}\ninterface MigrationCliApi {\n list(): MigrationSummary[];\n latestTarget(): string | null;\n plan(options: { allLayers?: boolean }): Promise<MigrationPlanEntry[]>;\n apply(options: { allLayers?: boolean; dryRun?: boolean; id?: string; continueOnError?: boolean }): Promise<ApplyOutcome[]>;\n status(recentLimit?: number): Promise<MigrationStatus>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateMigrationCliApi = (crowi: ApiCrowi) => MigrationCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createMigrationCliApi: CreateMigrationCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const cliApiModule = require(path.join(distDir, 'migration', 'cli-api')) as { createMigrationCliApi: CreateMigrationCliApi };\n\n return { Crowi: crowiModule.default, createMigrationCliApi: cliApiModule.createMigrationCliApi };\n}\n\n/**\n * Boot a lightweight Crowi, hand it to `fn`, then tear it down. Centralizes\n * the .env load / loadApi guard / init / teardown ceremony shared by every\n * `migrate` subcommand. Exits the process with a non-zero code on failure.\n */\nasync function withMigrationApi(fn: (api: MigrationCliApi) => Promise<void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n await fn(loaded.createMigrationCliApi(crowi));\n } catch (err) {\n console.error('crowi-admin: migrate command failed.');\n console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nfunction formatRange(entry: { fromVersion: string; toVersion: string }): string {\n return `${entry.fromVersion} → ${entry.toVersion}`;\n}\n\nexport function registerMigrate(program: Command): void {\n const migrate = program.command('migrate').description('Forward-only data migrations (plan / apply / status / list).');\n\n migrate\n .command('list')\n .description('List every registered migration with its version range and layer.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const rows = api.list();\n if (opts.json) {\n console.log(JSON.stringify(rows, null, 2));\n return;\n }\n if (rows.length === 0) {\n console.log('No migrations are registered.');\n return;\n }\n console.log('ID from → to layer description');\n for (const r of rows) {\n console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${r.description}`);\n }\n });\n });\n\n migrate\n .command('plan')\n .description('Preview pending migrations (preflight by default).')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { allLayers: boolean; json: boolean }) => {\n await withMigrationApi(async (api) => {\n const entries = await api.plan({ allLayers: opts.allLayers });\n if (opts.json) {\n console.log(JSON.stringify({ latestTarget: api.latestTarget(), entries }, null, 2));\n return;\n }\n console.log(`Latest target: ${api.latestTarget() ?? '(none)'}`);\n console.log('');\n const pending = entries.filter((e) => e.pending);\n if (pending.length === 0) {\n console.log('No pending migrations.');\n return;\n }\n pending.forEach((e, i) => {\n console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} (${formatRange(e)})`);\n console.log(` ${e.description}`);\n console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : 'Detected: details unavailable (no detect stage; isPending = true)'}`);\n });\n console.log('');\n console.log('Run `crowi-admin migrate apply` to execute preflight migrations.');\n });\n });\n\n migrate\n .command('apply')\n .description('Apply pending migrations (preflight by default), in version-range + order sequence.')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--dry-run', 'Run detect only; stages no-op and nothing is recorded.', false)\n .option('--id <id>', 'Apply only the migration with this id.')\n .option('--continue-on-error', 'Continue with later migrations after a failure (default: abort).', false)\n .action(async (opts: { allLayers: boolean; dryRun: boolean; id?: string; continueOnError: boolean }) => {\n await withMigrationApi(async (api) => {\n const outcomes = await api.apply({ allLayers: opts.allLayers, dryRun: opts.dryRun, id: opts.id, continueOnError: opts.continueOnError });\n if (outcomes.length === 0) {\n console.log('No migrations to apply.');\n return;\n }\n for (const o of outcomes) {\n console.log(` ${o.id.padEnd(25)} → ${o.result} (${o.durationMs}ms)`);\n }\n const failed = outcomes.filter((o) => o.result === 'failed');\n if (failed.length > 0) {\n throw new Error(`${failed.length} migration(s) failed: ${failed.map((f) => f.id).join(', ')}`);\n }\n });\n });\n\n migrate\n .command('status')\n .description('Show recent migration applications and pending counts.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const status = await api.status();\n if (opts.json) {\n console.log(JSON.stringify(status, null, 2));\n return;\n }\n console.log(`Latest target: ${status.latestTarget ?? '(none)'}`);\n console.log('');\n console.log('Recent applications (last 10):');\n if (status.recent.length === 0) {\n console.log(' (none)');\n } else {\n for (const r of status.recent) {\n const date = r.appliedAt.toISOString().slice(0, 10);\n const elapsed = r.durationMs !== undefined ? `${r.durationMs}ms` : '-';\n console.log(` ${date} ${r.result.padEnd(14)} ${r.migrationId.padEnd(25)} (${elapsed}, ${r.appliedBy ?? '-'})`);\n }\n }\n console.log('');\n console.log(`Pending preflight: ${status.pendingPreflight} migration(s)`);\n console.log(`Pending boot: ${status.pendingBoot} migration(s)`);\n });\n });\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8.5 — the `crowi-admin rebuild <target>` namespace.\n *\n * Operational rebuilds of derived data — version-independent, runnable any\n * time, any number of times (no pending/applied concept). All targets route\n * through the api-side `RebuildRunner`, so they share the framework runner's\n * `--dry-run` / progress / SIGINT / structured-logging conventions with\n * `migrate` (§4.3) — but a rebuild never touches `migrationApplications`\n * (§8.5).\n *\n * Targets:\n * - `rebuild search` ← ported from the old top-level `search rebuild`\n * - `rebuild storage copy` ← ported from the old top-level `storage copy`\n * - `rebuild renderer` ← new; util/rebuild-renderer.ts skeleton (TODO)\n * - `rebuild backlink` ← new; util/rebuild-backlink.ts skeleton (TODO)\n *\n * Like the other admin commands, this loads the api's compiled `dist/` lazily\n * (see `storage-copy.ts` for the `require.resolve` rationale — we avoid\n * importing `@crowi/api` directly so its `app.ts` auto-boot doesn't fire) and\n * talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `RebuildOutcome`. */\ninterface RebuildOutcome {\n id: string;\n durationMs: number;\n interrupted: boolean;\n stats: Record<string, unknown>;\n}\ninterface RebuildProgress {\n onLabel?: (label: string) => void;\n onIncrement?: (current: number) => void;\n}\ninterface RebuildCliApi {\n rebuildSearch(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildStorageCopy(opts: { from: string; to: string; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildRenderer(opts?: { onlyStale?: boolean; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildBacklink(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateRebuildCliApi = (crowi: ApiCrowi) => RebuildCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createRebuildCliApi: CreateRebuildCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const apiModule = require(path.join(distDir, 'migration', 'rebuild-api')) as { createRebuildCliApi: CreateRebuildCliApi };\n\n return { Crowi: crowiModule.default, createRebuildCliApi: apiModule.createRebuildCliApi };\n}\n\n/**\n * Map a completed rebuild outcome to a process exit code, mirroring the legacy\n * storage-copy convention:\n * - 0 — success (everything copied / rebuilt, or dry-run)\n * - 2 — partial: the run completed but >=1 unit failed (operator should retry)\n *\n * Kept as a pure function (no `process.exit`) so the partial→2 mapping is unit\n * testable without the surrounding boot ceremony. `process.exit(code)` ignores\n * `process.exitCode`, so the exit code must flow through here and be passed\n * explicitly — a fn that merely sets `process.exitCode = 2` would be clobbered\n * by the `process.exit(0)` in `withRebuildApi`.\n *\n * Fatal failures (init failed, or a task threw — e.g. renderer/backlink NOT_YET)\n * are exit 1 and handled in `withRebuildApi`; they never reach here.\n */\nexport function rebuildExitCode(outcome: RebuildOutcome): number {\n const failed = outcome.stats.failed;\n if (typeof failed === 'number' && failed > 0) return 2;\n return 0;\n}\n\n/**\n * Boot a lightweight Crowi, hand the rebuild façade to `fn`, then tear it\n * down. Centralizes the .env load / loadApi guard / init / teardown ceremony\n * shared by every `rebuild` subcommand.\n *\n * `fn` returns the success exit code (0 normally, 2 for a partial run — see\n * `rebuildExitCode`); a fatal failure (init error or a thrown task) overrides\n * it with exit 1. The resolved code is passed explicitly to `process.exit`,\n * since an explicit argument ignores any `process.exitCode` a callee set.\n */\nasync function withRebuildApi(fn: (api: RebuildCliApi) => Promise<number | void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n exitCode = (await fn(loaded.createRebuildCliApi(crowi))) ?? 0;\n } catch (err) {\n console.error('crowi-admin: rebuild failed.');\n printError(err);\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nexport function registerRebuild(program: Command): void {\n const rebuild = program.command('rebuild').description('Operational rebuilds of derived data (renderer / search / backlink / storage copy).');\n\n rebuild\n .command('renderer')\n .description('Regenerate cached rendered HTML for pages.')\n .option('--only-stale', 'Only re-render pages whose cache is stale.', false)\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { onlyStale: boolean; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildRenderer({ onlyStale: opts.onlyStale, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('renderer', outcome);\n });\n });\n\n rebuild\n .command('search')\n .description(\"Rebuild the search index from scratch using the active driver's rebuild().\")\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildSearch({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('search', outcome);\n });\n });\n\n rebuild\n .command('backlink')\n .description('Rebuild the backlink index across all pages.')\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildBacklink({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('backlink', outcome);\n });\n });\n\n const storage = rebuild.command('storage').description('Storage driver rebuilds.');\n storage\n .command('copy')\n .description('Copy every stored object from one driver to another.')\n .requiredOption('--from <name>', 'Source storage driver name (e.g. local, s3).')\n .requiredOption('--to <name>', 'Destination storage driver name.')\n .option('--dry-run', 'List candidate keys without copying anything.', false)\n .action(async (opts: { from: string; to: string; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildStorageCopy({ from: opts.from, to: opts.to, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('storage copy', outcome);\n // Mirror the legacy exit-code convention: partial (>=1 key failed) → 2.\n return rebuildExitCode(outcome);\n });\n });\n}\n\n/**\n * A progress sink that renders the runner's per-unit label to stderr so a\n * long rebuild shows live activity without flooding stdout (which carries the\n * final summary). Mirrors the spirit of `storage-copy.ts`'s `renderProgress`.\n */\nfunction liveProgress(): RebuildProgress {\n return {\n onLabel: (label) => {\n // Single-line, low-noise: enough to confirm the run is alive.\n process.stderr.write(` ${label}\\n`);\n },\n };\n}\n\n/** Print the final summary block, including each stat key the task returned. */\nfunction printOutcome(target: string, outcome: RebuildOutcome): void {\n console.log('');\n console.log('--- summary ---');\n console.log(`target: ${target}`);\n for (const [key, value] of Object.entries(outcome.stats)) {\n console.log(`${`${key}:`.padEnd(10)}${formatStat(value)}`);\n }\n console.log(`elapsed: ${formatElapsed(outcome.durationMs)}`);\n if (outcome.interrupted) {\n console.log('');\n console.log('Interrupted by SIGINT before completion — re-run to finish.');\n return;\n }\n console.log('');\n console.log(`Rebuild '${target}' complete.`);\n}\n\nfunction formatStat(value: unknown): string {\n if (Array.isArray(value)) return value.length === 0 ? '(none)' : value.join(', ');\n return String(value);\n}\n\n/**\n * Render whatever detail we can extract from a thrown error. The ES JS\n * client's `ResponseError` puts the cluster's actual response on `meta.body`\n * and leaves `.message` as just the HTTP status string, so walk the common\n * shapes (preserved from the old `search rebuild` command).\n */\nfunction printError(err: unknown): void {\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n const meta = (err as Error & { meta?: { statusCode?: number; body?: unknown } }).meta;\n if (meta) {\n if (meta.statusCode !== undefined) console.error(` status: ${meta.statusCode}`);\n if (meta.body !== undefined) {\n try {\n console.error(` body: ${JSON.stringify(meta.body, null, 2)}`);\n } catch {\n console.error(` body: ${String(meta.body)}`);\n }\n }\n }\n const cause = (err as Error & { cause?: unknown }).cause;\n if (cause !== undefined) console.error(` cause: ${cause instanceof Error ? cause.message || cause.name : String(cause)}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n}\n\n/** Render an elapsed millisecond duration (\"412ms\" / \"28m12s\"). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * Resolve @crowi/api's installed location relative to the caller's CWD\n * (= the runner directory) and load the bits we need, the same way\n * `search-rebuild.ts` / `storage-copy.ts` do (manual `require` so\n * `@crowi/api`'s `app.ts` auto-boot doesn't fire). Returns `null` when\n * the package isn't found so the caller can print a friendly error.\n */\nfunction loadApi(): { Crowi: ApiCrowiCtor; runWatcherBackfill: RunWatcherBackfill } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const distDir = path.join(path.dirname(apiPkgPath), 'dist');\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const backfillModule = require(path.join(distDir, 'util', 'watcher-backfill')) as { runWatcherBackfill: RunWatcherBackfill };\n return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ninterface WatcherBackfillSummary {\n pagesScanned: number;\n watchersCreated: number;\n dryRun: boolean;\n}\ntype RunWatcherBackfill = (crowi: ApiCrowi, opts?: { dryRun?: boolean }) => Promise<WatcherBackfillSummary>;\n\n/**\n * Wire the `watcher backfill` subcommand into the root program.\n *\n * Invocation:\n * crowi-admin watcher backfill [--dry-run]\n *\n * One-shot migration for pages that predate auto-watch: materialises a\n * WATCH row for each page's implicit notification set (creator + comment\n * authors + revision authors), respecting existing IGNORE opt-outs and\n * existing WATCH rows. Idempotent — safe to re-run. See\n * `@crowi/api`'s `util/watcher-backfill.ts` for the semantics.\n */\nexport function registerWatcherBackfill(program: Command): void {\n const watcher = program.command('watcher').description('Watcher / notification subscription utilities.');\n\n watcher\n .command('backfill')\n .description('Backfill WATCH rows for pages created before auto-watch (creator + comment/revision authors). Idempotent.')\n .option('--dry-run', 'Report how many WATCH rows would be created without writing anything.', false)\n .action(async (opts: { dryRun?: boolean }) => {\n // Load .env so MONGO_URI / CROWI_ENCRYPTION_KEY flow into Crowi the\n // same way `app.ts` does at boot. Silent if no .env present.\n dotenv.config();\n\n const api = loadApi();\n if (!api) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new api.Crowi(process.cwd(), process.env);\n const dryRun = Boolean(opts.dryRun);\n console.log(`[crowi-admin] watcher backfill: starting${dryRun ? ' (dry-run)' : ''}`);\n\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n const startedAt = Date.now();\n const summary = await api.runWatcherBackfill(crowi, { dryRun });\n const elapsedMs = Date.now() - startedAt;\n console.log('');\n console.log('--- summary ---');\n console.log(`pages scanned: ${summary.pagesScanned}`);\n console.log(`watchers ${summary.dryRun ? 'to create' : 'created'}: ${summary.watchersCreated}`);\n console.log(`elapsed: ${formatElapsed(elapsedMs)}`);\n console.log('');\n console.log(summary.dryRun ? 'Dry-run complete — no rows written.' : 'Backfill complete.');\n } catch (err) {\n console.error('crowi-admin: watcher backfill failed.');\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n });\n}\n\n/** Elapsed-duration formatter (mirrors search-rebuild's). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n"],"mappings":"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","names":["path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","formatElapsed"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/migrate.ts","../src/commands/rebuild.ts","../src/commands/replace.ts","../src/commands/watcher-backfill.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { registerMigrate } from './commands/migrate';\nimport { registerRebuild } from './commands/rebuild';\nimport { registerReplace } from './commands/replace';\nimport { registerWatcherBackfill } from './commands/watcher-backfill';\n\n/**\n * Build the root commander program. Exported so the bin entry point\n * (`bin.ts`) can call `parseAsync` on it, and so future test harnesses\n * can drive the CLI without spawning a child process.\n *\n * Subcommands are registered via small per-command helpers\n * (`registerXxx(program)`) so each command keeps its own arg / option\n * declarations next to its implementation.\n */\nexport function createProgram(): Command {\n const program = new Command();\n program\n .name('crowi-admin')\n .description('Operator-side admin CLI for Crowi 2.0. Talks directly to MongoDB; intended for use inside the server (ssh / kubectl exec).')\n .version('0.1.0-dev');\n\n // RFC-0008: the unified migration framework namespaces. The wikilink\n // migration lives under `migrate apply --id wikilink-format` (phase 3); the\n // legacy top-level `storage copy` / `search rebuild` forms are gone (phase\n // 4) — their tasks now ride the shared runner under `rebuild storage copy` /\n // `rebuild search`. No compatibility aliases (CHANGELOG / upgrade guide).\n registerMigrate(program);\n registerRebuild(program);\n // `replace url` (literal in-body URL/host swap for v1→v2 domain changes).\n // Not a versioned migration (arbitrary from/to, re-runnable) nor a derived-\n // data rebuild (it mutates revision bodies) — its own namespace.\n registerReplace(program);\n // `watcher backfill` (idempotent WATCH-row backfill) landed on main as a\n // standalone command; kept as-is here. Could fold into the framework as a\n // `rebuild` / `migrate` task later (see TODO backlog).\n registerWatcherBackfill(program);\n\n return program;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8 — the `crowi-admin migrate plan|apply|status|list` namespace.\n *\n * One-shot, forward-only migrations. `plan` / `apply` default to the\n * `preflight` layer (§4.2.2); `--all-layers` extends to boot migrations too\n * (debugging / investigation). The boot layer is normally applied by the api\n * boot sequence, not from here.\n *\n * Like the other admin commands, this loads the api's compiled `dist/`\n * lazily (see `storage-copy.ts` for the `require.resolve` rationale — we\n * avoid importing `@crowi/api` directly so its `app.ts` auto-boot doesn't\n * fire) and talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `MigrationCliApi` façade. */\ninterface MigrationSummary {\n id: string;\n fromVersion: string;\n toVersion: string;\n layer: 'boot' | 'preflight';\n /** Present only for `preflight` migrations; `boot` rows have no severity. */\n severity?: 'blocking' | 'cosmetic';\n description: string;\n}\ninterface DetectReport {\n summary: string;\n counts?: Record<string, number>;\n}\ninterface MigrationPlanEntry extends MigrationSummary {\n pending: boolean;\n detail: DetectReport | null;\n}\ninterface MigrationStatusEntry {\n migrationId: string;\n result: string;\n appliedAt: Date;\n durationMs?: number;\n appliedBy?: string;\n}\ninterface MigrationStatus {\n latestTarget: string | null;\n recent: MigrationStatusEntry[];\n pendingPreflight: number;\n pendingBoot: number;\n}\ninterface ApplyOutcome {\n id: string;\n result: string;\n durationMs: number;\n}\ninterface MigrationCliApi {\n list(): MigrationSummary[];\n latestTarget(): string | null;\n plan(options: { allLayers?: boolean }): Promise<MigrationPlanEntry[]>;\n apply(options: { allLayers?: boolean; dryRun?: boolean; id?: string; continueOnError?: boolean }): Promise<ApplyOutcome[]>;\n status(recentLimit?: number): Promise<MigrationStatus>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateMigrationCliApi = (crowi: ApiCrowi) => MigrationCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createMigrationCliApi: CreateMigrationCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const cliApiModule = require(path.join(distDir, 'migration', 'cli-api')) as { createMigrationCliApi: CreateMigrationCliApi };\n\n return { Crowi: crowiModule.default, createMigrationCliApi: cliApiModule.createMigrationCliApi };\n}\n\n/**\n * Boot a lightweight Crowi, hand it to `fn`, then tear it down. Centralizes\n * the .env load / loadApi guard / init / teardown ceremony shared by every\n * `migrate` subcommand. Exits the process with a non-zero code on failure.\n */\nasync function withMigrationApi(fn: (api: MigrationCliApi) => Promise<void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n await fn(loaded.createMigrationCliApi(crowi));\n } catch (err) {\n console.error('crowi-admin: migrate command failed.');\n console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nfunction formatRange(entry: { fromVersion: string; toVersion: string }): string {\n return `${entry.fromVersion} → ${entry.toVersion}`;\n}\n\n/** Boot-block severity tag, or an em-dash for `boot` rows (which have no severity). */\nfunction severityTag(severity?: 'blocking' | 'cosmetic'): string {\n return severity ? `[${severity}]` : '—';\n}\n\nexport function registerMigrate(program: Command): void {\n const migrate = program.command('migrate').description('Forward-only data migrations (plan / apply / status / list).');\n\n migrate\n .command('list')\n .description('List every registered migration with its version range and layer.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const rows = api.list();\n if (opts.json) {\n console.log(JSON.stringify(rows, null, 2));\n return;\n }\n if (rows.length === 0) {\n console.log('No migrations are registered.');\n return;\n }\n console.log('ID from → to layer severity description');\n for (const r of rows) {\n console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${severityTag(r.severity).padEnd(11)} ${r.description}`);\n }\n });\n });\n\n migrate\n .command('plan')\n .description('Preview pending migrations (preflight by default).')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { allLayers: boolean; json: boolean }) => {\n await withMigrationApi(async (api) => {\n const entries = await api.plan({ allLayers: opts.allLayers });\n if (opts.json) {\n console.log(JSON.stringify({ latestTarget: api.latestTarget(), entries }, null, 2));\n return;\n }\n console.log(`Latest target: ${api.latestTarget() ?? '(none)'}`);\n console.log('');\n const pending = entries.filter((e) => e.pending);\n if (pending.length === 0) {\n console.log('No pending migrations.');\n return;\n }\n pending.forEach((e, i) => {\n console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} ${severityTag(e.severity)} (${formatRange(e)})`);\n console.log(` ${e.description}`);\n console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : 'Detected: details unavailable (no detect stage; isPending = true)'}`);\n });\n console.log('');\n console.log('Run `crowi-admin migrate apply` to execute preflight migrations.');\n });\n });\n\n migrate\n .command('apply')\n .description('Apply pending migrations (preflight by default), in version-range + order sequence.')\n .option('--all-layers', 'Include boot-layer migrations as well as preflight.', false)\n .option('--dry-run', 'Run detect only; stages no-op and nothing is recorded.', false)\n .option('--id <id>', 'Apply only the migration with this id.')\n .option('--continue-on-error', 'Continue with later migrations after a failure (default: abort).', false)\n .action(async (opts: { allLayers: boolean; dryRun: boolean; id?: string; continueOnError: boolean }) => {\n await withMigrationApi(async (api) => {\n const outcomes = await api.apply({ allLayers: opts.allLayers, dryRun: opts.dryRun, id: opts.id, continueOnError: opts.continueOnError });\n if (outcomes.length === 0) {\n console.log('No migrations to apply.');\n return;\n }\n for (const o of outcomes) {\n console.log(` ${o.id.padEnd(25)} → ${o.result} (${o.durationMs}ms)`);\n }\n const failed = outcomes.filter((o) => o.result === 'failed');\n if (failed.length > 0) {\n throw new Error(`${failed.length} migration(s) failed: ${failed.map((f) => f.id).join(', ')}`);\n }\n });\n });\n\n migrate\n .command('status')\n .description('Show recent migration applications and pending counts.')\n .option('--json', 'Emit machine-readable JSON.', false)\n .action(async (opts: { json: boolean }) => {\n await withMigrationApi(async (api) => {\n const status = await api.status();\n if (opts.json) {\n console.log(JSON.stringify(status, null, 2));\n return;\n }\n console.log(`Latest target: ${status.latestTarget ?? '(none)'}`);\n console.log('');\n console.log('Recent applications (last 10):');\n if (status.recent.length === 0) {\n console.log(' (none)');\n } else {\n for (const r of status.recent) {\n const date = r.appliedAt.toISOString().slice(0, 10);\n const elapsed = r.durationMs !== undefined ? `${r.durationMs}ms` : '-';\n console.log(` ${date} ${r.result.padEnd(14)} ${r.migrationId.padEnd(25)} (${elapsed}, ${r.appliedBy ?? '-'})`);\n }\n }\n console.log('');\n console.log(`Pending preflight: ${status.pendingPreflight} migration(s)`);\n console.log(`Pending boot: ${status.pendingBoot} migration(s)`);\n });\n });\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * RFC-0008 §8.5 — the `crowi-admin rebuild <target>` namespace.\n *\n * Operational rebuilds of derived data — version-independent, runnable any\n * time, any number of times (no pending/applied concept). All targets route\n * through the api-side `RebuildRunner`, so they share the framework runner's\n * `--dry-run` / progress / SIGINT / structured-logging conventions with\n * `migrate` (§4.3) — but a rebuild never touches `migrationApplications`\n * (§8.5).\n *\n * Targets:\n * - `rebuild search` ← ported from the old top-level `search rebuild`\n * - `rebuild storage copy` ← ported from the old top-level `storage copy`\n * - `rebuild renderer` ← new; util/rebuild-renderer.ts skeleton (TODO)\n * - `rebuild backlink` ← new; util/rebuild-backlink.ts skeleton (TODO)\n *\n * Like the other admin commands, this loads the api's compiled `dist/` lazily\n * (see `storage-copy.ts` for the `require.resolve` rationale — we avoid\n * importing `@crowi/api` directly so its `app.ts` auto-boot doesn't fire) and\n * talks to MongoDB directly.\n */\n\n/** Minimal structural mirror of the api-side `RebuildOutcome`. */\ninterface RebuildOutcome {\n id: string;\n durationMs: number;\n interrupted: boolean;\n stats: Record<string, unknown>;\n}\ninterface RebuildProgress {\n onLabel?: (label: string) => void;\n onIncrement?: (current: number) => void;\n}\ninterface RebuildCliApi {\n rebuildSearch(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildStorageCopy(opts: { from: string; to: string; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildRenderer(opts?: { onlyStale?: boolean; dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n rebuildBacklink(opts?: { dryRun?: boolean; progress?: RebuildProgress }): Promise<RebuildOutcome>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype CreateRebuildCliApi = (crowi: ApiCrowi) => RebuildCliApi;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; createRebuildCliApi: CreateRebuildCliApi } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const apiRoot = path.dirname(apiPkgPath);\n const distDir = path.join(apiRoot, 'dist');\n\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const apiModule = require(path.join(distDir, 'migration', 'rebuild-api')) as { createRebuildCliApi: CreateRebuildCliApi };\n\n return { Crowi: crowiModule.default, createRebuildCliApi: apiModule.createRebuildCliApi };\n}\n\n/**\n * Map a completed rebuild outcome to a process exit code, mirroring the legacy\n * storage-copy convention:\n * - 0 — success (everything copied / rebuilt, or dry-run)\n * - 2 — partial: the run completed but >=1 unit failed (operator should retry)\n *\n * Kept as a pure function (no `process.exit`) so the partial→2 mapping is unit\n * testable without the surrounding boot ceremony. `process.exit(code)` ignores\n * `process.exitCode`, so the exit code must flow through here and be passed\n * explicitly — a fn that merely sets `process.exitCode = 2` would be clobbered\n * by the `process.exit(0)` in `withRebuildApi`.\n *\n * Fatal failures (init failed, or a task threw — e.g. renderer/backlink NOT_YET)\n * are exit 1 and handled in `withRebuildApi`; they never reach here.\n */\nexport function rebuildExitCode(outcome: RebuildOutcome): number {\n const failed = outcome.stats.failed;\n if (typeof failed === 'number' && failed > 0) return 2;\n return 0;\n}\n\n/**\n * Boot a lightweight Crowi, hand the rebuild façade to `fn`, then tear it\n * down. Centralizes the .env load / loadApi guard / init / teardown ceremony\n * shared by every `rebuild` subcommand.\n *\n * `fn` returns the success exit code (0 normally, 2 for a partial run — see\n * `rebuildExitCode`); a fatal failure (init error or a thrown task) overrides\n * it with exit 1. The resolved code is passed explicitly to `process.exit`,\n * since an explicit argument ignores any `process.exitCode` a callee set.\n */\nasync function withRebuildApi(fn: (api: RebuildCliApi) => Promise<number | void>): Promise<void> {\n dotenv.config();\n\n const loaded = loadApi();\n if (!loaded) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new loaded.Crowi(process.cwd(), process.env);\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n exitCode = (await fn(loaded.createRebuildCliApi(crowi))) ?? 0;\n } catch (err) {\n console.error('crowi-admin: rebuild failed.');\n printError(err);\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nexport function registerRebuild(program: Command): void {\n const rebuild = program.command('rebuild').description('Operational rebuilds of derived data (renderer / search / backlink / storage copy).');\n\n rebuild\n .command('renderer')\n .description('Regenerate cached rendered HTML for pages.')\n .option('--only-stale', 'Only re-render pages whose cache is stale.', false)\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { onlyStale: boolean; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildRenderer({ onlyStale: opts.onlyStale, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('renderer', outcome);\n });\n });\n\n rebuild\n .command('search')\n .description(\"Rebuild the search index from scratch using the active driver's rebuild().\")\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildSearch({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('search', outcome);\n });\n });\n\n rebuild\n .command('backlink')\n .description('Rebuild the backlink index across all pages.')\n .option('--dry-run', 'Report what would be rebuilt without writing.', false)\n .action(async (opts: { dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildBacklink({ dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('backlink', outcome);\n });\n });\n\n const storage = rebuild.command('storage').description('Storage driver rebuilds.');\n storage\n .command('copy')\n .description('Copy every stored object from one driver to another.')\n .requiredOption('--from <name>', 'Source storage driver name (e.g. local, s3).')\n .requiredOption('--to <name>', 'Destination storage driver name.')\n .option('--dry-run', 'List candidate keys without copying anything.', false)\n .action(async (opts: { from: string; to: string; dryRun: boolean }) => {\n await withRebuildApi(async (api) => {\n const outcome = await api.rebuildStorageCopy({ from: opts.from, to: opts.to, dryRun: opts.dryRun, progress: liveProgress() });\n printOutcome('storage copy', outcome);\n // Mirror the legacy exit-code convention: partial (>=1 key failed) → 2.\n return rebuildExitCode(outcome);\n });\n });\n}\n\n/**\n * A progress sink that renders the runner's per-unit label to stderr so a\n * long rebuild shows live activity without flooding stdout (which carries the\n * final summary). Mirrors the spirit of `storage-copy.ts`'s `renderProgress`.\n */\nfunction liveProgress(): RebuildProgress {\n return {\n onLabel: (label) => {\n // Single-line, low-noise: enough to confirm the run is alive.\n process.stderr.write(` ${label}\\n`);\n },\n };\n}\n\n/** Print the final summary block, including each stat key the task returned. */\nfunction printOutcome(target: string, outcome: RebuildOutcome): void {\n console.log('');\n console.log('--- summary ---');\n console.log(`target: ${target}`);\n for (const [key, value] of Object.entries(outcome.stats)) {\n console.log(`${`${key}:`.padEnd(10)}${formatStat(value)}`);\n }\n console.log(`elapsed: ${formatElapsed(outcome.durationMs)}`);\n if (outcome.interrupted) {\n console.log('');\n console.log('Interrupted by SIGINT before completion — re-run to finish.');\n return;\n }\n console.log('');\n console.log(`Rebuild '${target}' complete.`);\n}\n\nfunction formatStat(value: unknown): string {\n if (Array.isArray(value)) return value.length === 0 ? '(none)' : value.join(', ');\n return String(value);\n}\n\n/**\n * Render whatever detail we can extract from a thrown error. The ES JS\n * client's `ResponseError` puts the cluster's actual response on `meta.body`\n * and leaves `.message` as just the HTTP status string, so walk the common\n * shapes (preserved from the old `search rebuild` command).\n */\nfunction printError(err: unknown): void {\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n const meta = (err as Error & { meta?: { statusCode?: number; body?: unknown } }).meta;\n if (meta) {\n if (meta.statusCode !== undefined) console.error(` status: ${meta.statusCode}`);\n if (meta.body !== undefined) {\n try {\n console.error(` body: ${JSON.stringify(meta.body, null, 2)}`);\n } catch {\n console.error(` body: ${String(meta.body)}`);\n }\n }\n }\n const cause = (err as Error & { cause?: unknown }).cause;\n if (cause !== undefined) console.error(` cause: ${cause instanceof Error ? cause.message || cause.name : String(cause)}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n}\n\n/** Render an elapsed millisecond duration (\"412ms\" / \"28m12s\"). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","import path from 'node:path';\nimport readline from 'node:readline/promises';\nimport type { Command } from 'commander';\nimport dotenv from 'dotenv';\n\n/**\n * feature-url-replace-admin-cli — the `crowi-admin replace` namespace.\n *\n * `replace url --from <url> --to <url>` swaps a literal URL/host string in every\n * page body — the fix for a v1→v2 migration that changed the public domain and\n * left absolute URLs (image embeds / links) pinned to the old host. Page / file\n * ids are carried over unchanged, so this is a literal host swap, not an id remap.\n *\n * Like the other admin commands this loads the api's compiled `dist/` lazily\n * (see `migrate.ts` for the `require.resolve` rationale — we avoid importing\n * `@crowi/api` directly so its `app.ts` auto-boot doesn't fire) and talks to\n * MongoDB directly. The heavy lifting (scan + quiet rewrite that pushes a new\n * revision WITHOUT bumping updatedAt / notifying watchers) lives in\n * `@crowi/api`'s `util/replace-url.ts`; this file is the CLI surface:\n * arg-safety guard, preview, confirmation, summary, exit code.\n */\n\n/** Structural mirror of the api-side `ReplaceSafety`. */\ninterface ReplaceSafety {\n errors: string[];\n warnings: string[];\n bareHostFrom: boolean;\n}\n/** Structural mirror of the api-side preview / summary shapes. */\ninterface ReplaceUrlSample {\n path: string;\n occurrences: number;\n snippet: string;\n}\ninterface ReplaceUrlPreview {\n pagesMatched: number;\n occurrences: number;\n samples: ReplaceUrlSample[];\n}\ninterface ReplaceUrlSummary extends ReplaceUrlPreview {\n from: string;\n to: string;\n dryRun: boolean;\n aborted: boolean;\n pagesScanned: number;\n pagesRewritten: number;\n failed: number;\n interrupted: boolean;\n actingUserEmail?: string;\n}\ninterface ReplaceUrlOptions {\n from: string;\n to: string;\n userEmail?: string;\n dryRun?: boolean;\n includeTrash?: boolean;\n confirm?: (preview: ReplaceUrlPreview) => Promise<boolean>;\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ntype RunReplaceUrl = (crowi: ApiCrowi, opts: ReplaceUrlOptions) => Promise<ReplaceUrlSummary>;\ntype AssessReplaceSafety = (from: string, to: string) => ReplaceSafety;\n\nfunction loadApi(): { Crowi: ApiCrowiCtor; runReplaceUrl: RunReplaceUrl; assessReplaceSafety: AssessReplaceSafety } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const distDir = path.join(path.dirname(apiPkgPath), 'dist');\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const replaceModule = require(path.join(distDir, 'util', 'replace-url')) as { runReplaceUrl: RunReplaceUrl; assessReplaceSafety: AssessReplaceSafety };\n return { Crowi: crowiModule.default, runReplaceUrl: replaceModule.runReplaceUrl, assessReplaceSafety: replaceModule.assessReplaceSafety };\n}\n\n/**\n * Map a completed summary to a process exit code (mirrors `rebuildExitCode`):\n * - 2 — partial: the run finished but >=1 page failed (operator should retry)\n * - 0 — success / dry-run / declined\n * Fatal failures (init / thrown) are exit 1 and handled in the action.\n */\nexport function replaceExitCode(summary: { failed: number }): number {\n return summary.failed > 0 ? 2 : 0;\n}\n\nexport function registerReplace(program: Command): void {\n const replace = program.command('replace').description('Bulk content replacements across page bodies.');\n\n replace\n .command('url')\n .description(\n 'Replace a literal URL/host string in every page body (e.g. after a domain change). Pushes a new revision per page WITHOUT bumping updatedAt or notifying watchers.',\n )\n .requiredOption('--from <s>', 'String to replace. Use a full origin to be safe, e.g. https://old.example.')\n .requiredOption('--to <s>', 'Replacement string, e.g. https://new.example.')\n .option('--dry-run', 'Report what would change without writing.', false)\n .option('--include-trash', 'Also rewrite trashed / deprecated pages (default: published only).', false)\n .option('--user <email>', 'Author recorded on the new revisions (defaults to the oldest admin).')\n .option('--yes', 'Skip the interactive confirmation prompt.', false)\n .option('--force', 'Proceed even when --from looks unsafe (e.g. a bare host without a scheme).', false)\n .action(async (opts: { from: string; to: string; dryRun?: boolean; includeTrash?: boolean; user?: string; yes?: boolean; force?: boolean }) => {\n // Load .env so MONGO_URI / CROWI_ENCRYPTION_KEY flow into Crowi the same\n // way app.ts does at boot. Silent if no .env present.\n dotenv.config();\n\n const api = loadApi();\n if (!api) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const from = String(opts.from);\n const to = String(opts.to);\n\n // Cheap pre-flight guard (no DB): fail fast on footguns before booting.\n const safety = api.assessReplaceSafety(from, to);\n for (const w of safety.warnings) console.warn(`crowi-admin: warning: ${w}`);\n if (safety.errors.length > 0) {\n for (const e of safety.errors) console.error(`crowi-admin: ${e}`);\n process.exit(1);\n }\n if (safety.bareHostFrom && !opts.force) {\n console.error(\n `crowi-admin: --from='${from}' has no scheme. A bare host can corrupt longer hosts that start with it (e.g. '${from}' is a prefix of '${from}t'). Re-run with a full origin (e.g. https://${from}) or pass --force to override.`,\n );\n process.exit(1);\n }\n\n const crowi = new api.Crowi(process.cwd(), process.env);\n console.log(`[crowi-admin] replace url: '${from}' → '${to}'${opts.dryRun ? ' (dry-run)' : ''}`);\n\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n const startedAt = Date.now();\n const summary = await api.runReplaceUrl(crowi, {\n from,\n to,\n userEmail: opts.user,\n dryRun: Boolean(opts.dryRun),\n includeTrash: Boolean(opts.includeTrash),\n confirm: opts.dryRun ? undefined : (preview) => confirmProceed(preview, Boolean(opts.yes)),\n });\n printSummary(summary, Date.now() - startedAt);\n exitCode = replaceExitCode(summary);\n } catch (err) {\n console.error('crowi-admin: replace url failed.');\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n });\n}\n\n/** Print the matched pages + samples, then ask for confirmation (unless --yes). */\nasync function confirmProceed(preview: ReplaceUrlPreview, yes: boolean): Promise<boolean> {\n printPreview(preview);\n if (preview.pagesMatched === 0) return false; // nothing to do\n if (yes) return true;\n // A bulk body rewrite is hard to undo en masse, so refuse to write blind.\n if (!process.stdin.isTTY) {\n console.error('');\n console.error('crowi-admin: refusing to write without confirmation (no TTY). Re-run with --yes to proceed, or --dry-run to preview.');\n return false;\n }\n const rl = readline.createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = (await rl.question(`Rewrite ${preview.pagesMatched} page(s)? [y/N] `)).trim().toLowerCase();\n return answer === 'y' || answer === 'yes';\n } finally {\n rl.close();\n }\n}\n\nfunction printPreview(preview: ReplaceUrlPreview): void {\n console.log('');\n console.log(`Matched ${preview.pagesMatched} page(s), ${preview.occurrences} occurrence(s).`);\n for (const s of preview.samples) {\n console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);\n }\n const more = preview.pagesMatched - preview.samples.length;\n if (more > 0) console.log(` … and ${more} more page(s)`);\n}\n\nfunction printSummary(summary: ReplaceUrlSummary, elapsedMs: number): void {\n console.log('');\n console.log('--- summary ---');\n console.log(`from: ${summary.from}`);\n console.log(`to: ${summary.to}`);\n console.log(`scanned: ${summary.pagesScanned} page(s)`);\n console.log(`matched: ${summary.pagesMatched} page(s), ${summary.occurrences} occurrence(s)`);\n\n if (summary.pagesMatched === 0) {\n console.log('');\n console.log(`No pages contain '${summary.from}'.`);\n return;\n }\n\n if (summary.dryRun) {\n for (const s of summary.samples) {\n console.log(` ${s.path} (${s.occurrences}) ${s.snippet}`);\n }\n const more = summary.pagesMatched - summary.samples.length;\n if (more > 0) console.log(` … and ${more} more page(s)`);\n console.log('');\n console.log('Dry-run complete — no pages written.');\n return;\n }\n\n if (summary.aborted) {\n console.log('');\n console.log('Aborted — no pages written.');\n return;\n }\n\n console.log(`rewritten: ${summary.pagesRewritten} page(s)`);\n if (summary.failed > 0) console.log(`failed: ${summary.failed} page(s)`);\n if (summary.actingUserEmail) console.log(`author: ${summary.actingUserEmail}`);\n console.log(`elapsed: ${formatElapsed(elapsedMs)}`);\n\n if (summary.interrupted) {\n console.log('');\n console.log('Interrupted by SIGINT before completion — re-run to finish.');\n return;\n }\n console.log('');\n console.log(\"Replacement complete. Run 'crowi-admin rebuild search' to refresh the search index (page rendering is already up to date).\");\n}\n\n/** Elapsed-duration formatter (mirrors watcher-backfill / rebuild). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n","import path from 'node:path';\nimport dotenv from 'dotenv';\nimport type { Command } from 'commander';\n\n/**\n * Resolve @crowi/api's installed location relative to the caller's CWD\n * (= the runner directory) and load the bits we need, the same way\n * `search-rebuild.ts` / `storage-copy.ts` do (manual `require` so\n * `@crowi/api`'s `app.ts` auto-boot doesn't fire). Returns `null` when\n * the package isn't found so the caller can print a friendly error.\n */\nfunction loadApi(): { Crowi: ApiCrowiCtor; runWatcherBackfill: RunWatcherBackfill } | null {\n let apiPkgPath: string;\n try {\n apiPkgPath = require.resolve('@crowi/api/package.json', { paths: [process.cwd(), __dirname] });\n } catch {\n return null;\n }\n const distDir = path.join(path.dirname(apiPkgPath), 'dist');\n const crowiModule = require(path.join(distDir, 'crowi')) as { default: ApiCrowiCtor };\n const backfillModule = require(path.join(distDir, 'util', 'watcher-backfill')) as { runWatcherBackfill: RunWatcherBackfill };\n return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };\n}\n\ninterface ApiCrowi {\n initForCli(): Promise<void>;\n teardownForCli(): Promise<void>;\n}\ninterface ApiCrowiCtor {\n new (rootDir: string, env: NodeJS.ProcessEnv): ApiCrowi;\n}\ninterface WatcherBackfillSummary {\n pagesScanned: number;\n watchersCreated: number;\n dryRun: boolean;\n}\ntype RunWatcherBackfill = (crowi: ApiCrowi, opts?: { dryRun?: boolean }) => Promise<WatcherBackfillSummary>;\n\n/**\n * Wire the `watcher backfill` subcommand into the root program.\n *\n * Invocation:\n * crowi-admin watcher backfill [--dry-run]\n *\n * One-shot migration for pages that predate auto-watch: materialises a\n * WATCH row for each page's implicit notification set (creator + comment\n * authors + revision authors), respecting existing IGNORE opt-outs and\n * existing WATCH rows. Idempotent — safe to re-run. See\n * `@crowi/api`'s `util/watcher-backfill.ts` for the semantics.\n */\nexport function registerWatcherBackfill(program: Command): void {\n const watcher = program.command('watcher').description('Watcher / notification subscription utilities.');\n\n watcher\n .command('backfill')\n .description('Backfill WATCH rows for pages created before auto-watch (creator + comment/revision authors). Idempotent.')\n .option('--dry-run', 'Report how many WATCH rows would be created without writing anything.', false)\n .action(async (opts: { dryRun?: boolean }) => {\n // Load .env so MONGO_URI / CROWI_ENCRYPTION_KEY flow into Crowi the\n // same way `app.ts` does at boot. Silent if no .env present.\n dotenv.config();\n\n const api = loadApi();\n if (!api) {\n console.error('crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).');\n process.exit(1);\n }\n\n const crowi = new api.Crowi(process.cwd(), process.env);\n const dryRun = Boolean(opts.dryRun);\n console.log(`[crowi-admin] watcher backfill: starting${dryRun ? ' (dry-run)' : ''}`);\n\n try {\n await crowi.initForCli();\n } catch (err) {\n console.error('crowi-admin: failed to initialise Crowi:', (err as Error).message);\n await crowi.teardownForCli().catch(() => undefined);\n process.exit(1);\n }\n\n let exitCode = 0;\n try {\n const startedAt = Date.now();\n const summary = await api.runWatcherBackfill(crowi, { dryRun });\n const elapsedMs = Date.now() - startedAt;\n console.log('');\n console.log('--- summary ---');\n console.log(`pages scanned: ${summary.pagesScanned}`);\n console.log(`watchers ${summary.dryRun ? 'to create' : 'created'}: ${summary.watchersCreated}`);\n console.log(`elapsed: ${formatElapsed(elapsedMs)}`);\n console.log('');\n console.log(summary.dryRun ? 'Dry-run complete — no rows written.' : 'Backfill complete.');\n } catch (err) {\n console.error('crowi-admin: watcher backfill failed.');\n if (err instanceof Error) {\n if (err.message) console.error(` message: ${err.message}`);\n if (err.stack) console.error(err.stack);\n } else {\n console.error(` thrown: ${String(err)}`);\n }\n exitCode = 1;\n } finally {\n await crowi.teardownForCli().catch(() => undefined);\n }\n process.exit(exitCode);\n });\n}\n\n/** Elapsed-duration formatter (mirrors search-rebuild's). */\nfunction formatElapsed(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n return `${minutes}m${seconds.toString().padStart(2, '0')}s`;\n}\n"],"mappings":"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","names":["path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","readline","formatElapsed","import_node_path","import_dotenv","loadApi","path","dotenv","formatElapsed"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowi/admin-cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Operator-side admin CLI (`crowi-admin`) for Crowi 2.0. Talks directly to MongoDB through @crowi/api's lightweight CLI bootstrap; not a replacement for the upcoming end-user @crowi/cli.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/crowi/crowi.git",
|
|
8
|
+
"directory": "packages/admin-cli"
|
|
9
|
+
},
|
|
5
10
|
"main": "dist/cli.js",
|
|
6
11
|
"types": "dist/cli.d.ts",
|
|
7
12
|
"bin": {
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
"dependencies": {
|
|
24
29
|
"commander": "^12.1.0",
|
|
25
30
|
"dotenv": "^16.5.0",
|
|
26
|
-
"@crowi/api": "^2.0.0-alpha.
|
|
31
|
+
"@crowi/api": "^2.0.0-alpha.3"
|
|
27
32
|
},
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@types/jest": "^29.5.14",
|