@crowi/admin-cli 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bin.js +380 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +386 -0
- package/dist/cli.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013 Sotaro KARASAWA <sotaro.k@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/migrate.ts
|
|
30
|
+
var import_node_path = __toESM(require("path"));
|
|
31
|
+
var import_dotenv = __toESM(require("dotenv"));
|
|
32
|
+
function loadApi() {
|
|
33
|
+
let apiPkgPath;
|
|
34
|
+
try {
|
|
35
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const apiRoot = import_node_path.default.dirname(apiPkgPath);
|
|
40
|
+
const distDir = import_node_path.default.join(apiRoot, "dist");
|
|
41
|
+
const crowiModule = require(import_node_path.default.join(distDir, "crowi"));
|
|
42
|
+
const cliApiModule = require(import_node_path.default.join(distDir, "migration", "cli-api"));
|
|
43
|
+
return { Crowi: crowiModule.default, createMigrationCliApi: cliApiModule.createMigrationCliApi };
|
|
44
|
+
}
|
|
45
|
+
async function withMigrationApi(fn) {
|
|
46
|
+
import_dotenv.default.config();
|
|
47
|
+
const loaded = loadApi();
|
|
48
|
+
if (!loaded) {
|
|
49
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const crowi = new loaded.Crowi(process.cwd(), process.env);
|
|
53
|
+
try {
|
|
54
|
+
await crowi.initForCli();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
57
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
let exitCode = 0;
|
|
61
|
+
try {
|
|
62
|
+
await fn(loaded.createMigrationCliApi(crowi));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error("crowi-admin: migrate command failed.");
|
|
65
|
+
console.error(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
66
|
+
exitCode = 1;
|
|
67
|
+
} finally {
|
|
68
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
69
|
+
}
|
|
70
|
+
process.exit(exitCode);
|
|
71
|
+
}
|
|
72
|
+
function formatRange(entry) {
|
|
73
|
+
return `${entry.fromVersion} \u2192 ${entry.toVersion}`;
|
|
74
|
+
}
|
|
75
|
+
function registerMigrate(program) {
|
|
76
|
+
const migrate = program.command("migrate").description("Forward-only data migrations (plan / apply / status / list).");
|
|
77
|
+
migrate.command("list").description("List every registered migration with its version range and layer.").option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
78
|
+
await withMigrationApi(async (api) => {
|
|
79
|
+
const rows = api.list();
|
|
80
|
+
if (opts.json) {
|
|
81
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (rows.length === 0) {
|
|
85
|
+
console.log("No migrations are registered.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log("ID from \u2192 to layer description");
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${r.description}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
migrate.command("plan").description("Preview pending migrations (preflight by default).").option("--all-layers", "Include boot-layer migrations as well as preflight.", false).option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
95
|
+
await withMigrationApi(async (api) => {
|
|
96
|
+
const entries = await api.plan({ allLayers: opts.allLayers });
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
console.log(JSON.stringify({ latestTarget: api.latestTarget(), entries }, null, 2));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
console.log(`Latest target: ${api.latestTarget() ?? "(none)"}`);
|
|
102
|
+
console.log("");
|
|
103
|
+
const pending = entries.filter((e) => e.pending);
|
|
104
|
+
if (pending.length === 0) {
|
|
105
|
+
console.log("No pending migrations.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
pending.forEach((e, i) => {
|
|
109
|
+
console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} (${formatRange(e)})`);
|
|
110
|
+
console.log(` ${e.description}`);
|
|
111
|
+
console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : "Detected: details unavailable (no detect stage; isPending = true)"}`);
|
|
112
|
+
});
|
|
113
|
+
console.log("");
|
|
114
|
+
console.log("Run `crowi-admin migrate apply` to execute preflight migrations.");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
migrate.command("apply").description("Apply pending migrations (preflight by default), in version-range + order sequence.").option("--all-layers", "Include boot-layer migrations as well as preflight.", false).option("--dry-run", "Run detect only; stages no-op and nothing is recorded.", false).option("--id <id>", "Apply only the migration with this id.").option("--continue-on-error", "Continue with later migrations after a failure (default: abort).", false).action(async (opts) => {
|
|
118
|
+
await withMigrationApi(async (api) => {
|
|
119
|
+
const outcomes = await api.apply({ allLayers: opts.allLayers, dryRun: opts.dryRun, id: opts.id, continueOnError: opts.continueOnError });
|
|
120
|
+
if (outcomes.length === 0) {
|
|
121
|
+
console.log("No migrations to apply.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const o of outcomes) {
|
|
125
|
+
console.log(` ${o.id.padEnd(25)} \u2192 ${o.result} (${o.durationMs}ms)`);
|
|
126
|
+
}
|
|
127
|
+
const failed = outcomes.filter((o) => o.result === "failed");
|
|
128
|
+
if (failed.length > 0) {
|
|
129
|
+
throw new Error(`${failed.length} migration(s) failed: ${failed.map((f) => f.id).join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
migrate.command("status").description("Show recent migration applications and pending counts.").option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
134
|
+
await withMigrationApi(async (api) => {
|
|
135
|
+
const status = await api.status();
|
|
136
|
+
if (opts.json) {
|
|
137
|
+
console.log(JSON.stringify(status, null, 2));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log(`Latest target: ${status.latestTarget ?? "(none)"}`);
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log("Recent applications (last 10):");
|
|
143
|
+
if (status.recent.length === 0) {
|
|
144
|
+
console.log(" (none)");
|
|
145
|
+
} else {
|
|
146
|
+
for (const r of status.recent) {
|
|
147
|
+
const date = r.appliedAt.toISOString().slice(0, 10);
|
|
148
|
+
const elapsed = r.durationMs !== void 0 ? `${r.durationMs}ms` : "-";
|
|
149
|
+
console.log(` ${date} ${r.result.padEnd(14)} ${r.migrationId.padEnd(25)} (${elapsed}, ${r.appliedBy ?? "-"})`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(`Pending preflight: ${status.pendingPreflight} migration(s)`);
|
|
154
|
+
console.log(`Pending boot: ${status.pendingBoot} migration(s)`);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/commands/rebuild.ts
|
|
160
|
+
var import_node_path2 = __toESM(require("path"));
|
|
161
|
+
var import_dotenv2 = __toESM(require("dotenv"));
|
|
162
|
+
function loadApi2() {
|
|
163
|
+
let apiPkgPath;
|
|
164
|
+
try {
|
|
165
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const apiRoot = import_node_path2.default.dirname(apiPkgPath);
|
|
170
|
+
const distDir = import_node_path2.default.join(apiRoot, "dist");
|
|
171
|
+
const crowiModule = require(import_node_path2.default.join(distDir, "crowi"));
|
|
172
|
+
const apiModule = require(import_node_path2.default.join(distDir, "migration", "rebuild-api"));
|
|
173
|
+
return { Crowi: crowiModule.default, createRebuildCliApi: apiModule.createRebuildCliApi };
|
|
174
|
+
}
|
|
175
|
+
function rebuildExitCode(outcome) {
|
|
176
|
+
const failed = outcome.stats.failed;
|
|
177
|
+
if (typeof failed === "number" && failed > 0) return 2;
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
async function withRebuildApi(fn) {
|
|
181
|
+
import_dotenv2.default.config();
|
|
182
|
+
const loaded = loadApi2();
|
|
183
|
+
if (!loaded) {
|
|
184
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const crowi = new loaded.Crowi(process.cwd(), process.env);
|
|
188
|
+
try {
|
|
189
|
+
await crowi.initForCli();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
192
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
let exitCode = 0;
|
|
196
|
+
try {
|
|
197
|
+
exitCode = await fn(loaded.createRebuildCliApi(crowi)) ?? 0;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error("crowi-admin: rebuild failed.");
|
|
200
|
+
printError(err);
|
|
201
|
+
exitCode = 1;
|
|
202
|
+
} finally {
|
|
203
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
204
|
+
}
|
|
205
|
+
process.exit(exitCode);
|
|
206
|
+
}
|
|
207
|
+
function registerRebuild(program) {
|
|
208
|
+
const rebuild = program.command("rebuild").description("Operational rebuilds of derived data (renderer / search / backlink / storage copy).");
|
|
209
|
+
rebuild.command("renderer").description("Regenerate cached rendered HTML for pages.").option("--only-stale", "Only re-render pages whose cache is stale.", false).option("--dry-run", "Report what would be rebuilt without writing.", false).action(async (opts) => {
|
|
210
|
+
await withRebuildApi(async (api) => {
|
|
211
|
+
const outcome = await api.rebuildRenderer({ onlyStale: opts.onlyStale, dryRun: opts.dryRun, progress: liveProgress() });
|
|
212
|
+
printOutcome("renderer", outcome);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
rebuild.command("search").description("Rebuild the search index from scratch using the active driver's rebuild().").option("--dry-run", "Report what would be rebuilt without writing.", false).action(async (opts) => {
|
|
216
|
+
await withRebuildApi(async (api) => {
|
|
217
|
+
const outcome = await api.rebuildSearch({ dryRun: opts.dryRun, progress: liveProgress() });
|
|
218
|
+
printOutcome("search", outcome);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
rebuild.command("backlink").description("Rebuild the backlink index across all pages.").option("--dry-run", "Report what would be rebuilt without writing.", false).action(async (opts) => {
|
|
222
|
+
await withRebuildApi(async (api) => {
|
|
223
|
+
const outcome = await api.rebuildBacklink({ dryRun: opts.dryRun, progress: liveProgress() });
|
|
224
|
+
printOutcome("backlink", outcome);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
const storage = rebuild.command("storage").description("Storage driver rebuilds.");
|
|
228
|
+
storage.command("copy").description("Copy every stored object from one driver to another.").requiredOption("--from <name>", "Source storage driver name (e.g. local, s3).").requiredOption("--to <name>", "Destination storage driver name.").option("--dry-run", "List candidate keys without copying anything.", false).action(async (opts) => {
|
|
229
|
+
await withRebuildApi(async (api) => {
|
|
230
|
+
const outcome = await api.rebuildStorageCopy({ from: opts.from, to: opts.to, dryRun: opts.dryRun, progress: liveProgress() });
|
|
231
|
+
printOutcome("storage copy", outcome);
|
|
232
|
+
return rebuildExitCode(outcome);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function liveProgress() {
|
|
237
|
+
return {
|
|
238
|
+
onLabel: (label) => {
|
|
239
|
+
process.stderr.write(` ${label}
|
|
240
|
+
`);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function printOutcome(target, outcome) {
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log("--- summary ---");
|
|
247
|
+
console.log(`target: ${target}`);
|
|
248
|
+
for (const [key, value] of Object.entries(outcome.stats)) {
|
|
249
|
+
console.log(`${`${key}:`.padEnd(10)}${formatStat(value)}`);
|
|
250
|
+
}
|
|
251
|
+
console.log(`elapsed: ${formatElapsed(outcome.durationMs)}`);
|
|
252
|
+
if (outcome.interrupted) {
|
|
253
|
+
console.log("");
|
|
254
|
+
console.log("Interrupted by SIGINT before completion \u2014 re-run to finish.");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log(`Rebuild '${target}' complete.`);
|
|
259
|
+
}
|
|
260
|
+
function formatStat(value) {
|
|
261
|
+
if (Array.isArray(value)) return value.length === 0 ? "(none)" : value.join(", ");
|
|
262
|
+
return String(value);
|
|
263
|
+
}
|
|
264
|
+
function printError(err) {
|
|
265
|
+
if (err instanceof Error) {
|
|
266
|
+
if (err.message) console.error(` message: ${err.message}`);
|
|
267
|
+
const meta = err.meta;
|
|
268
|
+
if (meta) {
|
|
269
|
+
if (meta.statusCode !== void 0) console.error(` status: ${meta.statusCode}`);
|
|
270
|
+
if (meta.body !== void 0) {
|
|
271
|
+
try {
|
|
272
|
+
console.error(` body: ${JSON.stringify(meta.body, null, 2)}`);
|
|
273
|
+
} catch {
|
|
274
|
+
console.error(` body: ${String(meta.body)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const cause = err.cause;
|
|
279
|
+
if (cause !== void 0) console.error(` cause: ${cause instanceof Error ? cause.message || cause.name : String(cause)}`);
|
|
280
|
+
if (err.stack) console.error(err.stack);
|
|
281
|
+
} else {
|
|
282
|
+
console.error(` thrown: ${String(err)}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function formatElapsed(ms) {
|
|
286
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
287
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
288
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
289
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
290
|
+
const seconds = totalSeconds % 60;
|
|
291
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/commands/watcher-backfill.ts
|
|
295
|
+
var import_node_path3 = __toESM(require("path"));
|
|
296
|
+
var import_dotenv3 = __toESM(require("dotenv"));
|
|
297
|
+
function loadApi3() {
|
|
298
|
+
let apiPkgPath;
|
|
299
|
+
try {
|
|
300
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const distDir = import_node_path3.default.join(import_node_path3.default.dirname(apiPkgPath), "dist");
|
|
305
|
+
const crowiModule = require(import_node_path3.default.join(distDir, "crowi"));
|
|
306
|
+
const backfillModule = require(import_node_path3.default.join(distDir, "util", "watcher-backfill"));
|
|
307
|
+
return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };
|
|
308
|
+
}
|
|
309
|
+
function registerWatcherBackfill(program) {
|
|
310
|
+
const watcher = program.command("watcher").description("Watcher / notification subscription utilities.");
|
|
311
|
+
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
|
+
import_dotenv3.default.config();
|
|
313
|
+
const api = loadApi3();
|
|
314
|
+
if (!api) {
|
|
315
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
const crowi = new api.Crowi(process.cwd(), process.env);
|
|
319
|
+
const dryRun = Boolean(opts.dryRun);
|
|
320
|
+
console.log(`[crowi-admin] watcher backfill: starting${dryRun ? " (dry-run)" : ""}`);
|
|
321
|
+
try {
|
|
322
|
+
await crowi.initForCli();
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
325
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
let exitCode = 0;
|
|
329
|
+
try {
|
|
330
|
+
const startedAt = Date.now();
|
|
331
|
+
const summary = await api.runWatcherBackfill(crowi, { dryRun });
|
|
332
|
+
const elapsedMs = Date.now() - startedAt;
|
|
333
|
+
console.log("");
|
|
334
|
+
console.log("--- summary ---");
|
|
335
|
+
console.log(`pages scanned: ${summary.pagesScanned}`);
|
|
336
|
+
console.log(`watchers ${summary.dryRun ? "to create" : "created"}: ${summary.watchersCreated}`);
|
|
337
|
+
console.log(`elapsed: ${formatElapsed2(elapsedMs)}`);
|
|
338
|
+
console.log("");
|
|
339
|
+
console.log(summary.dryRun ? "Dry-run complete \u2014 no rows written." : "Backfill complete.");
|
|
340
|
+
} catch (err) {
|
|
341
|
+
console.error("crowi-admin: watcher backfill failed.");
|
|
342
|
+
if (err instanceof Error) {
|
|
343
|
+
if (err.message) console.error(` message: ${err.message}`);
|
|
344
|
+
if (err.stack) console.error(err.stack);
|
|
345
|
+
} else {
|
|
346
|
+
console.error(` thrown: ${String(err)}`);
|
|
347
|
+
}
|
|
348
|
+
exitCode = 1;
|
|
349
|
+
} finally {
|
|
350
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
351
|
+
}
|
|
352
|
+
process.exit(exitCode);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function formatElapsed2(ms) {
|
|
356
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
357
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
358
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
359
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
360
|
+
const seconds = totalSeconds % 60;
|
|
361
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/cli.ts
|
|
365
|
+
function createProgram() {
|
|
366
|
+
const program = new import_commander.Command();
|
|
367
|
+
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
|
+
registerMigrate(program);
|
|
369
|
+
registerRebuild(program);
|
|
370
|
+
registerWatcherBackfill(program);
|
|
371
|
+
return program;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/bin.ts
|
|
375
|
+
createProgram().parseAsync(process.argv).catch((err) => {
|
|
376
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
377
|
+
console.error(`crowi-admin: ${message}`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
});
|
|
380
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +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"]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the root commander program. Exported so the bin entry point
|
|
5
|
+
* (`bin.ts`) can call `parseAsync` on it, and so future test harnesses
|
|
6
|
+
* can drive the CLI without spawning a child process.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands are registered via small per-command helpers
|
|
9
|
+
* (`registerXxx(program)`) so each command keeps its own arg / option
|
|
10
|
+
* declarations next to its implementation.
|
|
11
|
+
*/
|
|
12
|
+
declare function createProgram(): Command;
|
|
13
|
+
|
|
14
|
+
export { createProgram };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/cli.ts
|
|
31
|
+
var cli_exports = {};
|
|
32
|
+
__export(cli_exports, {
|
|
33
|
+
createProgram: () => createProgram
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(cli_exports);
|
|
36
|
+
var import_commander = require("commander");
|
|
37
|
+
|
|
38
|
+
// src/commands/migrate.ts
|
|
39
|
+
var import_node_path = __toESM(require("path"));
|
|
40
|
+
var import_dotenv = __toESM(require("dotenv"));
|
|
41
|
+
function loadApi() {
|
|
42
|
+
let apiPkgPath;
|
|
43
|
+
try {
|
|
44
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const apiRoot = import_node_path.default.dirname(apiPkgPath);
|
|
49
|
+
const distDir = import_node_path.default.join(apiRoot, "dist");
|
|
50
|
+
const crowiModule = require(import_node_path.default.join(distDir, "crowi"));
|
|
51
|
+
const cliApiModule = require(import_node_path.default.join(distDir, "migration", "cli-api"));
|
|
52
|
+
return { Crowi: crowiModule.default, createMigrationCliApi: cliApiModule.createMigrationCliApi };
|
|
53
|
+
}
|
|
54
|
+
async function withMigrationApi(fn) {
|
|
55
|
+
import_dotenv.default.config();
|
|
56
|
+
const loaded = loadApi();
|
|
57
|
+
if (!loaded) {
|
|
58
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const crowi = new loaded.Crowi(process.cwd(), process.env);
|
|
62
|
+
try {
|
|
63
|
+
await crowi.initForCli();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
66
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
let exitCode = 0;
|
|
70
|
+
try {
|
|
71
|
+
await fn(loaded.createMigrationCliApi(crowi));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error("crowi-admin: migrate command failed.");
|
|
74
|
+
console.error(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
75
|
+
exitCode = 1;
|
|
76
|
+
} finally {
|
|
77
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
78
|
+
}
|
|
79
|
+
process.exit(exitCode);
|
|
80
|
+
}
|
|
81
|
+
function formatRange(entry) {
|
|
82
|
+
return `${entry.fromVersion} \u2192 ${entry.toVersion}`;
|
|
83
|
+
}
|
|
84
|
+
function registerMigrate(program) {
|
|
85
|
+
const migrate = program.command("migrate").description("Forward-only data migrations (plan / apply / status / list).");
|
|
86
|
+
migrate.command("list").description("List every registered migration with its version range and layer.").option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
87
|
+
await withMigrationApi(async (api) => {
|
|
88
|
+
const rows = api.list();
|
|
89
|
+
if (opts.json) {
|
|
90
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (rows.length === 0) {
|
|
94
|
+
console.log("No migrations are registered.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log("ID from \u2192 to layer description");
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
console.log(`${r.id.padEnd(25)} ${formatRange(r).padEnd(12)} ${r.layer.padEnd(11)} ${r.description}`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
migrate.command("plan").description("Preview pending migrations (preflight by default).").option("--all-layers", "Include boot-layer migrations as well as preflight.", false).option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
104
|
+
await withMigrationApi(async (api) => {
|
|
105
|
+
const entries = await api.plan({ allLayers: opts.allLayers });
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
console.log(JSON.stringify({ latestTarget: api.latestTarget(), entries }, null, 2));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`Latest target: ${api.latestTarget() ?? "(none)"}`);
|
|
111
|
+
console.log("");
|
|
112
|
+
const pending = entries.filter((e) => e.pending);
|
|
113
|
+
if (pending.length === 0) {
|
|
114
|
+
console.log("No pending migrations.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
pending.forEach((e, i) => {
|
|
118
|
+
console.log(` [${i + 1}/${pending.length}] ${e.id.padEnd(25)} (${formatRange(e)})`);
|
|
119
|
+
console.log(` ${e.description}`);
|
|
120
|
+
console.log(` ${e.detail ? `Detected: ${e.detail.summary}` : "Detected: details unavailable (no detect stage; isPending = true)"}`);
|
|
121
|
+
});
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log("Run `crowi-admin migrate apply` to execute preflight migrations.");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
migrate.command("apply").description("Apply pending migrations (preflight by default), in version-range + order sequence.").option("--all-layers", "Include boot-layer migrations as well as preflight.", false).option("--dry-run", "Run detect only; stages no-op and nothing is recorded.", false).option("--id <id>", "Apply only the migration with this id.").option("--continue-on-error", "Continue with later migrations after a failure (default: abort).", false).action(async (opts) => {
|
|
127
|
+
await withMigrationApi(async (api) => {
|
|
128
|
+
const outcomes = await api.apply({ allLayers: opts.allLayers, dryRun: opts.dryRun, id: opts.id, continueOnError: opts.continueOnError });
|
|
129
|
+
if (outcomes.length === 0) {
|
|
130
|
+
console.log("No migrations to apply.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (const o of outcomes) {
|
|
134
|
+
console.log(` ${o.id.padEnd(25)} \u2192 ${o.result} (${o.durationMs}ms)`);
|
|
135
|
+
}
|
|
136
|
+
const failed = outcomes.filter((o) => o.result === "failed");
|
|
137
|
+
if (failed.length > 0) {
|
|
138
|
+
throw new Error(`${failed.length} migration(s) failed: ${failed.map((f) => f.id).join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
migrate.command("status").description("Show recent migration applications and pending counts.").option("--json", "Emit machine-readable JSON.", false).action(async (opts) => {
|
|
143
|
+
await withMigrationApi(async (api) => {
|
|
144
|
+
const status = await api.status();
|
|
145
|
+
if (opts.json) {
|
|
146
|
+
console.log(JSON.stringify(status, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
console.log(`Latest target: ${status.latestTarget ?? "(none)"}`);
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log("Recent applications (last 10):");
|
|
152
|
+
if (status.recent.length === 0) {
|
|
153
|
+
console.log(" (none)");
|
|
154
|
+
} else {
|
|
155
|
+
for (const r of status.recent) {
|
|
156
|
+
const date = r.appliedAt.toISOString().slice(0, 10);
|
|
157
|
+
const elapsed = r.durationMs !== void 0 ? `${r.durationMs}ms` : "-";
|
|
158
|
+
console.log(` ${date} ${r.result.padEnd(14)} ${r.migrationId.padEnd(25)} (${elapsed}, ${r.appliedBy ?? "-"})`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log(`Pending preflight: ${status.pendingPreflight} migration(s)`);
|
|
163
|
+
console.log(`Pending boot: ${status.pendingBoot} migration(s)`);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/commands/rebuild.ts
|
|
169
|
+
var import_node_path2 = __toESM(require("path"));
|
|
170
|
+
var import_dotenv2 = __toESM(require("dotenv"));
|
|
171
|
+
function loadApi2() {
|
|
172
|
+
let apiPkgPath;
|
|
173
|
+
try {
|
|
174
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const apiRoot = import_node_path2.default.dirname(apiPkgPath);
|
|
179
|
+
const distDir = import_node_path2.default.join(apiRoot, "dist");
|
|
180
|
+
const crowiModule = require(import_node_path2.default.join(distDir, "crowi"));
|
|
181
|
+
const apiModule = require(import_node_path2.default.join(distDir, "migration", "rebuild-api"));
|
|
182
|
+
return { Crowi: crowiModule.default, createRebuildCliApi: apiModule.createRebuildCliApi };
|
|
183
|
+
}
|
|
184
|
+
function rebuildExitCode(outcome) {
|
|
185
|
+
const failed = outcome.stats.failed;
|
|
186
|
+
if (typeof failed === "number" && failed > 0) return 2;
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
async function withRebuildApi(fn) {
|
|
190
|
+
import_dotenv2.default.config();
|
|
191
|
+
const loaded = loadApi2();
|
|
192
|
+
if (!loaded) {
|
|
193
|
+
console.error("crowi-admin: could not locate @crowi/api. Run from a directory that has @crowi/api installed (e.g. the runner package).");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const crowi = new loaded.Crowi(process.cwd(), process.env);
|
|
197
|
+
try {
|
|
198
|
+
await crowi.initForCli();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
201
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
let exitCode = 0;
|
|
205
|
+
try {
|
|
206
|
+
exitCode = await fn(loaded.createRebuildCliApi(crowi)) ?? 0;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error("crowi-admin: rebuild failed.");
|
|
209
|
+
printError(err);
|
|
210
|
+
exitCode = 1;
|
|
211
|
+
} finally {
|
|
212
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
213
|
+
}
|
|
214
|
+
process.exit(exitCode);
|
|
215
|
+
}
|
|
216
|
+
function registerRebuild(program) {
|
|
217
|
+
const rebuild = program.command("rebuild").description("Operational rebuilds of derived data (renderer / search / backlink / storage copy).");
|
|
218
|
+
rebuild.command("renderer").description("Regenerate cached rendered HTML for pages.").option("--only-stale", "Only re-render pages whose cache is stale.", false).option("--dry-run", "Report what would be rebuilt without writing.", false).action(async (opts) => {
|
|
219
|
+
await withRebuildApi(async (api) => {
|
|
220
|
+
const outcome = await api.rebuildRenderer({ onlyStale: opts.onlyStale, dryRun: opts.dryRun, progress: liveProgress() });
|
|
221
|
+
printOutcome("renderer", outcome);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
rebuild.command("search").description("Rebuild the search index from scratch using the active driver's rebuild().").option("--dry-run", "Report what would be rebuilt without writing.", false).action(async (opts) => {
|
|
225
|
+
await withRebuildApi(async (api) => {
|
|
226
|
+
const outcome = await api.rebuildSearch({ dryRun: opts.dryRun, progress: liveProgress() });
|
|
227
|
+
printOutcome("search", outcome);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
rebuild.command("backlink").description("Rebuild the backlink index across all pages.").option("--dry-run", "Report what would be rebuilt without writing.", false).action(async (opts) => {
|
|
231
|
+
await withRebuildApi(async (api) => {
|
|
232
|
+
const outcome = await api.rebuildBacklink({ dryRun: opts.dryRun, progress: liveProgress() });
|
|
233
|
+
printOutcome("backlink", outcome);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
const storage = rebuild.command("storage").description("Storage driver rebuilds.");
|
|
237
|
+
storage.command("copy").description("Copy every stored object from one driver to another.").requiredOption("--from <name>", "Source storage driver name (e.g. local, s3).").requiredOption("--to <name>", "Destination storage driver name.").option("--dry-run", "List candidate keys without copying anything.", false).action(async (opts) => {
|
|
238
|
+
await withRebuildApi(async (api) => {
|
|
239
|
+
const outcome = await api.rebuildStorageCopy({ from: opts.from, to: opts.to, dryRun: opts.dryRun, progress: liveProgress() });
|
|
240
|
+
printOutcome("storage copy", outcome);
|
|
241
|
+
return rebuildExitCode(outcome);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function liveProgress() {
|
|
246
|
+
return {
|
|
247
|
+
onLabel: (label) => {
|
|
248
|
+
process.stderr.write(` ${label}
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function printOutcome(target, outcome) {
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log("--- summary ---");
|
|
256
|
+
console.log(`target: ${target}`);
|
|
257
|
+
for (const [key, value] of Object.entries(outcome.stats)) {
|
|
258
|
+
console.log(`${`${key}:`.padEnd(10)}${formatStat(value)}`);
|
|
259
|
+
}
|
|
260
|
+
console.log(`elapsed: ${formatElapsed(outcome.durationMs)}`);
|
|
261
|
+
if (outcome.interrupted) {
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log("Interrupted by SIGINT before completion \u2014 re-run to finish.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(`Rebuild '${target}' complete.`);
|
|
268
|
+
}
|
|
269
|
+
function formatStat(value) {
|
|
270
|
+
if (Array.isArray(value)) return value.length === 0 ? "(none)" : value.join(", ");
|
|
271
|
+
return String(value);
|
|
272
|
+
}
|
|
273
|
+
function printError(err) {
|
|
274
|
+
if (err instanceof Error) {
|
|
275
|
+
if (err.message) console.error(` message: ${err.message}`);
|
|
276
|
+
const meta = err.meta;
|
|
277
|
+
if (meta) {
|
|
278
|
+
if (meta.statusCode !== void 0) console.error(` status: ${meta.statusCode}`);
|
|
279
|
+
if (meta.body !== void 0) {
|
|
280
|
+
try {
|
|
281
|
+
console.error(` body: ${JSON.stringify(meta.body, null, 2)}`);
|
|
282
|
+
} catch {
|
|
283
|
+
console.error(` body: ${String(meta.body)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const cause = err.cause;
|
|
288
|
+
if (cause !== void 0) console.error(` cause: ${cause instanceof Error ? cause.message || cause.name : String(cause)}`);
|
|
289
|
+
if (err.stack) console.error(err.stack);
|
|
290
|
+
} else {
|
|
291
|
+
console.error(` thrown: ${String(err)}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function formatElapsed(ms) {
|
|
295
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
296
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
297
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
298
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
299
|
+
const seconds = totalSeconds % 60;
|
|
300
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/commands/watcher-backfill.ts
|
|
304
|
+
var import_node_path3 = __toESM(require("path"));
|
|
305
|
+
var import_dotenv3 = __toESM(require("dotenv"));
|
|
306
|
+
function loadApi3() {
|
|
307
|
+
let apiPkgPath;
|
|
308
|
+
try {
|
|
309
|
+
apiPkgPath = require.resolve("@crowi/api/package.json", { paths: [process.cwd(), __dirname] });
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const distDir = import_node_path3.default.join(import_node_path3.default.dirname(apiPkgPath), "dist");
|
|
314
|
+
const crowiModule = require(import_node_path3.default.join(distDir, "crowi"));
|
|
315
|
+
const backfillModule = require(import_node_path3.default.join(distDir, "util", "watcher-backfill"));
|
|
316
|
+
return { Crowi: crowiModule.default, runWatcherBackfill: backfillModule.runWatcherBackfill };
|
|
317
|
+
}
|
|
318
|
+
function registerWatcherBackfill(program) {
|
|
319
|
+
const watcher = program.command("watcher").description("Watcher / notification subscription utilities.");
|
|
320
|
+
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
|
+
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 crowi = new api.Crowi(process.cwd(), process.env);
|
|
328
|
+
const dryRun = Boolean(opts.dryRun);
|
|
329
|
+
console.log(`[crowi-admin] watcher backfill: starting${dryRun ? " (dry-run)" : ""}`);
|
|
330
|
+
try {
|
|
331
|
+
await crowi.initForCli();
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error("crowi-admin: failed to initialise Crowi:", err.message);
|
|
334
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
let exitCode = 0;
|
|
338
|
+
try {
|
|
339
|
+
const startedAt = Date.now();
|
|
340
|
+
const summary = await api.runWatcherBackfill(crowi, { dryRun });
|
|
341
|
+
const elapsedMs = Date.now() - startedAt;
|
|
342
|
+
console.log("");
|
|
343
|
+
console.log("--- summary ---");
|
|
344
|
+
console.log(`pages scanned: ${summary.pagesScanned}`);
|
|
345
|
+
console.log(`watchers ${summary.dryRun ? "to create" : "created"}: ${summary.watchersCreated}`);
|
|
346
|
+
console.log(`elapsed: ${formatElapsed2(elapsedMs)}`);
|
|
347
|
+
console.log("");
|
|
348
|
+
console.log(summary.dryRun ? "Dry-run complete \u2014 no rows written." : "Backfill complete.");
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error("crowi-admin: watcher backfill failed.");
|
|
351
|
+
if (err instanceof Error) {
|
|
352
|
+
if (err.message) console.error(` message: ${err.message}`);
|
|
353
|
+
if (err.stack) console.error(err.stack);
|
|
354
|
+
} else {
|
|
355
|
+
console.error(` thrown: ${String(err)}`);
|
|
356
|
+
}
|
|
357
|
+
exitCode = 1;
|
|
358
|
+
} finally {
|
|
359
|
+
await crowi.teardownForCli().catch(() => void 0);
|
|
360
|
+
}
|
|
361
|
+
process.exit(exitCode);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function formatElapsed2(ms) {
|
|
365
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
366
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
367
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
368
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
369
|
+
const seconds = totalSeconds % 60;
|
|
370
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/cli.ts
|
|
374
|
+
function createProgram() {
|
|
375
|
+
const program = new import_commander.Command();
|
|
376
|
+
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
|
+
registerMigrate(program);
|
|
378
|
+
registerRebuild(program);
|
|
379
|
+
registerWatcherBackfill(program);
|
|
380
|
+
return program;
|
|
381
|
+
}
|
|
382
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
383
|
+
0 && (module.exports = {
|
|
384
|
+
createProgram
|
|
385
|
+
});
|
|
386
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAwB;;;ACAxB,uBAAiB;AACjB,oBAAmB;AAoEnB,SAAS,UAAwF;AAC/F,MAAI;AACJ,MAAI;AACF,iBAAa,QAAQ,QAAQ,2BAA2B,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG,SAAS,EAAE,CAAC;AAAA,EAC/F,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,UAAU,iBAAAA,QAAK,QAAQ,UAAU;AACvC,QAAM,UAAU,iBAAAA,QAAK,KAAK,SAAS,MAAM;AAEzC,QAAM,cAAc,QAAQ,iBAAAA,QAAK,KAAK,SAAS,OAAO,CAAC;AACvD,QAAM,eAAe,QAAQ,iBAAAA,QAAK,KAAK,SAAS,aAAa,SAAS,CAAC;AAEvE,SAAO,EAAE,OAAO,YAAY,SAAS,uBAAuB,aAAa,sBAAsB;AACjG;AAOA,eAAe,iBAAiB,IAA4D;AAC1F,gBAAAC,QAAO,OAAO;AAEd,QAAM,SAAS,QAAQ;AACvB,MAAI,CAAC,QAAQ;AACX,YAAQ,MAAM,yHAAyH;AACvI,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,QAAQ,IAAI,OAAO,MAAM,QAAQ,IAAI,GAAG,QAAQ,GAAG;AACzD,MAAI;AACF,UAAM,MAAM,WAAW;AAAA,EACzB,SAAS,KAAK;AACZ,YAAQ,MAAM,4CAA6C,IAAc,OAAO;AAChF,UAAM,MAAM,eAAe,EAAE,MAAM,MAAM,MAAS;AAClD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,WAAW;AACf,MAAI;AACF,UAAM,GAAG,OAAO,sBAAsB,KAAK,CAAC;AAAA,EAC9C,SAAS,KAAK;AACZ,YAAQ,MAAM,sCAAsC;AACpD,YAAQ,MAAM,eAAe,QAAS,IAAI,SAAS,IAAI,UAAW,OAAO,GAAG,CAAC;AAC7E,eAAW;AAAA,EACb,UAAE;AACA,UAAM,MAAM,eAAe,EAAE,MAAM,MAAM,MAAS;AAAA,EACpD;AACA,UAAQ,KAAK,QAAQ;AACvB;AAEA,SAAS,YAAY,OAA2D;AAC9E,SAAO,GAAG,MAAM,WAAW,WAAM,MAAM,SAAS;AAClD;AAEO,SAAS,gBAAgB,SAAwB;AACtD,QAAM,UAAU,QAAQ,QAAQ,SAAS,EAAE,YAAY,8DAA8D;AAErH,UACG,QAAQ,MAAM,EACd,YAAY,mEAAmE,EAC/E,OAAO,UAAU,+BAA+B,KAAK,EACrD,OAAO,OAAO,SAA4B;AACzC,UAAM,iBAAiB,OAAO,QAAQ;AACpC,YAAM,OAAO,IAAI,KAAK;AACtB,UAAI,KAAK,MAAM;AACb,gBAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AACzC;AAAA,MACF;AACA,UAAI,KAAK,WAAW,GAAG;AACrB,gBAAQ,IAAI,+BAA+B;AAC3C;AAAA,MACF;AACA,cAAQ,IAAI,qEAAgE;AAC5E,iBAAW,KAAK,MAAM;AACpB,gBAAQ,IAAI,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE;AAAA,MACtG;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAEH,UACG,QAAQ,MAAM,EACd,YAAY,oDAAoD,EAChE,OAAO,gBAAgB,uDAAuD,KAAK,EACnF,OAAO,UAAU,+BAA+B,KAAK,EACrD,OAAO,OAAO,SAAgD;AAC7D,UAAM,iBAAiB,OAAO,QAAQ;AACpC,YAAM,UAAU,MAAM,IAAI,KAAK,EAAE,WAAW,KAAK,UAAU,CAAC;AAC5D,UAAI,KAAK,MAAM;AACb,gBAAQ,IAAI,KAAK,UAAU,EAAE,cAAc,IAAI,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC,CAAC;AAClF;AAAA,MACF;AACA,cAAQ,IAAI,kBAAkB,IAAI,aAAa,KAAK,QAAQ,EAAE;AAC9D,cAAQ,IAAI,EAAE;AACd,YAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO;AAC/C,UAAI,QAAQ,WAAW,GAAG;AACxB,gBAAQ,IAAI,wBAAwB;AACpC;AAAA,MACF;AACA,cAAQ,QAAQ,CAAC,GAAG,MAAM;AACxB,gBAAQ,IAAI,MAAM,IAAI,CAAC,IAAI,QAAQ,MAAM,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,KAAK,YAAY,CAAC,CAAC,GAAG;AACnF,gBAAQ,IAAI,WAAW,EAAE,WAAW,EAAE;AACtC,gBAAQ,IAAI,WAAW,EAAE,SAAS,aAAa,EAAE,OAAO,OAAO,KAAK,mEAAmE,EAAE;AAAA,MAC3I,CAAC;AACD,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,kEAAkE;AAAA,IAChF,CAAC;AAAA,EACH,CAAC;AAEH,UACG,QAAQ,OAAO,EACf,YAAY,qFAAqF,EACjG,OAAO,gBAAgB,uDAAuD,KAAK,EACnF,OAAO,aAAa,0DAA0D,KAAK,EACnF,OAAO,aAAa,wCAAwC,EAC5D,OAAO,uBAAuB,oEAAoE,KAAK,EACvG,OAAO,OAAO,SAAyF;AACtG,UAAM,iBAAiB,OAAO,QAAQ;AACpC,YAAM,WAAW,MAAM,IAAI,MAAM,EAAE,WAAW,KAAK,WAAW,QAAQ,KAAK,QAAQ,IAAI,KAAK,IAAI,iBAAiB,KAAK,gBAAgB,CAAC;AACvI,UAAI,SAAS,WAAW,GAAG;AACzB,gBAAQ,IAAI,yBAAyB;AACrC;AAAA,MACF;AACA,iBAAW,KAAK,UAAU;AACxB,gBAAQ,IAAI,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,WAAM,EAAE,MAAM,KAAK,EAAE,UAAU,KAAK;AAAA,MACtE;AACA,YAAM,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,QAAQ;AAC3D,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,IAAI,MAAM,GAAG,OAAO,MAAM,yBAAyB,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,MAC/F;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAEH,UACG,QAAQ,QAAQ,EAChB,YAAY,wDAAwD,EACpE,OAAO,UAAU,+BAA+B,KAAK,EACrD,OAAO,OAAO,SAA4B;AACzC,UAAM,iBAAiB,OAAO,QAAQ;AACpC,YAAM,SAAS,MAAM,IAAI,OAAO;AAChC,UAAI,KAAK,MAAM;AACb,gBAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC3C;AAAA,MACF;AACA,cAAQ,IAAI,kBAAkB,OAAO,gBAAgB,QAAQ,EAAE;AAC/D,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,gCAAgC;AAC5C,UAAI,OAAO,OAAO,WAAW,GAAG;AAC9B,gBAAQ,IAAI,UAAU;AAAA,MACxB,OAAO;AACL,mBAAW,KAAK,OAAO,QAAQ;AAC7B,gBAAM,OAAO,EAAE,UAAU,YAAY,EAAE,MAAM,GAAG,EAAE;AAClD,gBAAM,UAAU,EAAE,eAAe,SAAY,GAAG,EAAE,UAAU,OAAO;AACnE,kBAAQ,IAAI,KAAK,IAAI,KAAK,EAAE,OAAO,OAAO,EAAE,CAAC,IAAI,EAAE,YAAY,OAAO,EAAE,CAAC,KAAK,OAAO,KAAK,EAAE,aAAa,GAAG,GAAG;AAAA,QACjH;AAAA,MACF;AACA,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,uBAAuB,OAAO,gBAAgB,eAAe;AACzE,cAAQ,IAAI,uBAAuB,OAAO,WAAW,eAAe;AAAA,IACtE,CAAC;AAAA,EACH,CAAC;AACL;;;ACvOA,IAAAC,oBAAiB;AACjB,IAAAC,iBAAmB;AAoDnB,SAASC,WAAoF;AAC3F,MAAI;AACJ,MAAI;AACF,iBAAa,QAAQ,QAAQ,2BAA2B,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG,SAAS,EAAE,CAAC;AAAA,EAC/F,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,UAAU,kBAAAC,QAAK,QAAQ,UAAU;AACvC,QAAM,UAAU,kBAAAA,QAAK,KAAK,SAAS,MAAM;AAEzC,QAAM,cAAc,QAAQ,kBAAAA,QAAK,KAAK,SAAS,OAAO,CAAC;AACvD,QAAM,YAAY,QAAQ,kBAAAA,QAAK,KAAK,SAAS,aAAa,aAAa,CAAC;AAExE,SAAO,EAAE,OAAO,YAAY,SAAS,qBAAqB,UAAU,oBAAoB;AAC1F;AAiBO,SAAS,gBAAgB,SAAiC;AAC/D,QAAM,SAAS,QAAQ,MAAM;AAC7B,MAAI,OAAO,WAAW,YAAY,SAAS,EAAG,QAAO;AACrD,SAAO;AACT;AAYA,eAAe,eAAe,IAAmE;AAC/F,iBAAAC,QAAO,OAAO;AAEd,QAAM,SAASF,SAAQ;AACvB,MAAI,CAAC,QAAQ;AACX,YAAQ,MAAM,yHAAyH;AACvI,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,QAAQ,IAAI,OAAO,MAAM,QAAQ,IAAI,GAAG,QAAQ,GAAG;AACzD,MAAI;AACF,UAAM,MAAM,WAAW;AAAA,EACzB,SAAS,KAAK;AACZ,YAAQ,MAAM,4CAA6C,IAAc,OAAO;AAChF,UAAM,MAAM,eAAe,EAAE,MAAM,MAAM,MAAS;AAClD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,WAAW;AACf,MAAI;AACF,eAAY,MAAM,GAAG,OAAO,oBAAoB,KAAK,CAAC,KAAM;AAAA,EAC9D,SAAS,KAAK;AACZ,YAAQ,MAAM,8BAA8B;AAC5C,eAAW,GAAG;AACd,eAAW;AAAA,EACb,UAAE;AACA,UAAM,MAAM,eAAe,EAAE,MAAM,MAAM,MAAS;AAAA,EACpD;AACA,UAAQ,KAAK,QAAQ;AACvB;AAEO,SAAS,gBAAgB,SAAwB;AACtD,QAAM,UAAU,QAAQ,QAAQ,SAAS,EAAE,YAAY,qFAAqF;AAE5I,UACG,QAAQ,UAAU,EAClB,YAAY,4CAA4C,EACxD,OAAO,gBAAgB,8CAA8C,KAAK,EAC1E,OAAO,aAAa,iDAAiD,KAAK,EAC1E,OAAO,OAAO,SAAkD;AAC/D,UAAM,eAAe,OAAO,QAAQ;AAClC,YAAM,UAAU,MAAM,IAAI,gBAAgB,EAAE,WAAW,KAAK,WAAW,QAAQ,KAAK,QAAQ,UAAU,aAAa,EAAE,CAAC;AACtH,mBAAa,YAAY,OAAO;AAAA,IAClC,CAAC;AAAA,EACH,CAAC;AAEH,UACG,QAAQ,QAAQ,EAChB,YAAY,4EAA4E,EACxF,OAAO,aAAa,iDAAiD,KAAK,EAC1E,OAAO,OAAO,SAA8B;AAC3C,UAAM,eAAe,OAAO,QAAQ;AAClC,YAAM,UAAU,MAAM,IAAI,cAAc,EAAE,QAAQ,KAAK,QAAQ,UAAU,aAAa,EAAE,CAAC;AACzF,mBAAa,UAAU,OAAO;AAAA,IAChC,CAAC;AAAA,EACH,CAAC;AAEH,UACG,QAAQ,UAAU,EAClB,YAAY,8CAA8C,EAC1D,OAAO,aAAa,iDAAiD,KAAK,EAC1E,OAAO,OAAO,SAA8B;AAC3C,UAAM,eAAe,OAAO,QAAQ;AAClC,YAAM,UAAU,MAAM,IAAI,gBAAgB,EAAE,QAAQ,KAAK,QAAQ,UAAU,aAAa,EAAE,CAAC;AAC3F,mBAAa,YAAY,OAAO;AAAA,IAClC,CAAC;AAAA,EACH,CAAC;AAEH,QAAM,UAAU,QAAQ,QAAQ,SAAS,EAAE,YAAY,0BAA0B;AACjF,UACG,QAAQ,MAAM,EACd,YAAY,sDAAsD,EAClE,eAAe,iBAAiB,8CAA8C,EAC9E,eAAe,eAAe,kCAAkC,EAChE,OAAO,aAAa,iDAAiD,KAAK,EAC1E,OAAO,OAAO,SAAwD;AACrE,UAAM,eAAe,OAAO,QAAQ;AAClC,YAAM,UAAU,MAAM,IAAI,mBAAmB,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,IAAI,QAAQ,KAAK,QAAQ,UAAU,aAAa,EAAE,CAAC;AAC5H,mBAAa,gBAAgB,OAAO;AAEpC,aAAO,gBAAgB,OAAO;AAAA,IAChC,CAAC;AAAA,EACH,CAAC;AACL;AAOA,SAAS,eAAgC;AACvC,SAAO;AAAA,IACL,SAAS,CAAC,UAAU;AAElB,cAAQ,OAAO,MAAM,KAAK,KAAK;AAAA,CAAI;AAAA,IACrC;AAAA,EACF;AACF;AAGA,SAAS,aAAa,QAAgB,SAA+B;AACnE,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,iBAAiB;AAC7B,UAAQ,IAAI,aAAa,MAAM,EAAE;AACjC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,KAAK,GAAG;AACxD,YAAQ,IAAI,GAAG,GAAG,GAAG,IAAI,OAAO,EAAE,CAAC,GAAG,WAAW,KAAK,CAAC,EAAE;AAAA,EAC3D;AACA,UAAQ,IAAI,aAAa,cAAc,QAAQ,UAAU,CAAC,EAAE;AAC5D,MAAI,QAAQ,aAAa;AACvB,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,kEAA6D;AACzE;AAAA,EACF;AACA,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,YAAY,MAAM,aAAa;AAC7C;AAEA,SAAS,WAAW,OAAwB;AAC1C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,WAAW,IAAI,WAAW,MAAM,KAAK,IAAI;AAChF,SAAO,OAAO,KAAK;AACrB;AAQA,SAAS,WAAW,KAAoB;AACtC,MAAI,eAAe,OAAO;AACxB,QAAI,IAAI,QAAS,SAAQ,MAAM,cAAc,IAAI,OAAO,EAAE;AAC1D,UAAM,OAAQ,IAAmE;AACjF,QAAI,MAAM;AACR,UAAI,KAAK,eAAe,OAAW,SAAQ,MAAM,cAAc,KAAK,UAAU,EAAE;AAChF,UAAI,KAAK,SAAS,QAAW;AAC3B,YAAI;AACF,kBAAQ,MAAM,cAAc,KAAK,UAAU,KAAK,MAAM,MAAM,CAAC,CAAC,EAAE;AAAA,QAClE,QAAQ;AACN,kBAAQ,MAAM,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAS,IAAoC;AACnD,QAAI,UAAU,OAAW,SAAQ,MAAM,cAAc,iBAAiB,QAAQ,MAAM,WAAW,MAAM,OAAO,OAAO,KAAK,CAAC,EAAE;AAC3H,QAAI,IAAI,MAAO,SAAQ,MAAM,IAAI,KAAK;AAAA,EACxC,OAAO;AACL,YAAQ,MAAM,cAAc,OAAO,GAAG,CAAC,EAAE;AAAA,EAC3C;AACF;AAGA,SAAS,cAAc,IAAoB;AACzC,MAAI,KAAK,IAAM,QAAO,GAAG,EAAE;AAC3B,QAAM,eAAe,KAAK,MAAM,KAAK,GAAI;AACzC,MAAI,eAAe,GAAI,QAAO,GAAG,YAAY;AAC7C,QAAM,UAAU,KAAK,MAAM,eAAe,EAAE;AAC5C,QAAM,UAAU,eAAe;AAC/B,SAAO,GAAG,OAAO,IAAI,QAAQ,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAC1D;;;AClQA,IAAAG,oBAAiB;AACjB,IAAAC,iBAAmB;AAUnB,SAASC,WAAkF;AACzF,MAAI;AACJ,MAAI;AACF,iBAAa,QAAQ,QAAQ,2BAA2B,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG,SAAS,EAAE,CAAC;AAAA,EAC/F,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,UAAU,kBAAAC,QAAK,KAAK,kBAAAA,QAAK,QAAQ,UAAU,GAAG,MAAM;AAC1D,QAAM,cAAc,QAAQ,kBAAAA,QAAK,KAAK,SAAS,OAAO,CAAC;AACvD,QAAM,iBAAiB,QAAQ,kBAAAA,QAAK,KAAK,SAAS,QAAQ,kBAAkB,CAAC;AAC7E,SAAO,EAAE,OAAO,YAAY,SAAS,oBAAoB,eAAe,mBAAmB;AAC7F;AA4BO,SAAS,wBAAwB,SAAwB;AAC9D,QAAM,UAAU,QAAQ,QAAQ,SAAS,EAAE,YAAY,gDAAgD;AAEvG,UACG,QAAQ,UAAU,EAClB,YAAY,2GAA2G,EACvH,OAAO,aAAa,yEAAyE,KAAK,EAClG,OAAO,OAAO,SAA+B;AAG5C,mBAAAC,QAAO,OAAO;AAEd,UAAM,MAAMF,SAAQ;AACpB,QAAI,CAAC,KAAK;AACR,cAAQ,MAAM,yHAAyH;AACvI,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,QAAQ,IAAI,IAAI,MAAM,QAAQ,IAAI,GAAG,QAAQ,GAAG;AACtD,UAAM,SAAS,QAAQ,KAAK,MAAM;AAClC,YAAQ,IAAI,2CAA2C,SAAS,eAAe,EAAE,EAAE;AAEnF,QAAI;AACF,YAAM,MAAM,WAAW;AAAA,IACzB,SAAS,KAAK;AACZ,cAAQ,MAAM,4CAA6C,IAAc,OAAO;AAChF,YAAM,MAAM,eAAe,EAAE,MAAM,MAAM,MAAS;AAClD,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,QAAI,WAAW;AACf,QAAI;AACF,YAAM,YAAY,KAAK,IAAI;AAC3B,YAAM,UAAU,MAAM,IAAI,mBAAmB,OAAO,EAAE,OAAO,CAAC;AAC9D,YAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,iBAAiB;AAC7B,cAAQ,IAAI,qBAAqB,QAAQ,YAAY,EAAE;AACvD,cAAQ,IAAI,YAAY,QAAQ,SAAS,cAAc,SAAS,KAAK,QAAQ,eAAe,EAAE;AAC9F,cAAQ,IAAI,qBAAqBG,eAAc,SAAS,CAAC,EAAE;AAC3D,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,QAAQ,SAAS,6CAAwC,oBAAoB;AAAA,IAC3F,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC;AACrD,UAAI,eAAe,OAAO;AACxB,YAAI,IAAI,QAAS,SAAQ,MAAM,cAAc,IAAI,OAAO,EAAE;AAC1D,YAAI,IAAI,MAAO,SAAQ,MAAM,IAAI,KAAK;AAAA,MACxC,OAAO;AACL,gBAAQ,MAAM,cAAc,OAAO,GAAG,CAAC,EAAE;AAAA,MAC3C;AACA,iBAAW;AAAA,IACb,UAAE;AACA,YAAM,MAAM,eAAe,EAAE,MAAM,MAAM,MAAS;AAAA,IACpD;AACA,YAAQ,KAAK,QAAQ;AAAA,EACvB,CAAC;AACL;AAGA,SAASA,eAAc,IAAoB;AACzC,MAAI,KAAK,IAAM,QAAO,GAAG,EAAE;AAC3B,QAAM,eAAe,KAAK,MAAM,KAAK,GAAI;AACzC,MAAI,eAAe,GAAI,QAAO,GAAG,YAAY;AAC7C,QAAM,UAAU,KAAK,MAAM,eAAe,EAAE;AAC5C,QAAM,UAAU,eAAe;AAC/B,SAAO,GAAG,OAAO,IAAI,QAAQ,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAC1D;;;AHtGO,SAAS,gBAAyB;AACvC,QAAM,UAAU,IAAI,yBAAQ;AAC5B,UACG,KAAK,aAAa,EAClB,YAAY,4HAA4H,EACxI,QAAQ,WAAW;AAOtB,kBAAgB,OAAO;AACvB,kBAAgB,OAAO;AAIvB,0BAAwB,OAAO;AAE/B,SAAO;AACT;","names":["path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","import_node_path","import_dotenv","loadApi","path","dotenv","formatElapsed"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crowi/admin-cli",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
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
|
+
"main": "dist/cli.js",
|
|
6
|
+
"types": "dist/cli.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"crowi-admin": "dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/cli.d.ts",
|
|
13
|
+
"require": "./dist/cli.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"commander": "^12.1.0",
|
|
25
|
+
"dotenv": "^16.5.0",
|
|
26
|
+
"@crowi/api": "^2.0.0-alpha.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/jest": "^29.5.14",
|
|
30
|
+
"@types/node": "^24",
|
|
31
|
+
"jest": "^29.7.0",
|
|
32
|
+
"ts-jest": "^29.3.4",
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.8.3",
|
|
35
|
+
"@crowi/tsconfig": "0.1.0-alpha.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"dev": "tsup --watch --no-clean",
|
|
40
|
+
"type-check": "tsc --noEmit",
|
|
41
|
+
"test": "jest --passWithNoTests"
|
|
42
|
+
}
|
|
43
|
+
}
|