486 lines
17 KiB
JavaScript
486 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
||||
|
|
"use strict";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* scripts/migrate-control-plane-to-sqlite.js
|
|||
|
|
*
|
|||
|
|
* Phase 0 dry-run 迁移工具 — 读取本地 control-plane 数据和工作区清单,
|
|||
|
|
* 生成 migration-report.json 建议(不写 DB,不改现有文件)。
|
|||
|
|
*
|
|||
|
|
* 用法:
|
|||
|
|
* node scripts/migrate-control-plane-to-sqlite.js --dry-run --out /tmp/report.json
|
|||
|
|
* node scripts/migrate-control-plane-to-sqlite.js --dry-run --access-policy-path /custom/path/access-policy.json
|
|||
|
|
* node scripts/migrate-control-plane-to-sqlite.js --dry-run --users-root /custom/users/root
|
|||
|
|
* node scripts/migrate-control-plane-to-sqlite.js --dry-run --skip-users
|
|||
|
|
*
|
|||
|
|
* 参考设计稿:
|
|||
|
|
* design/02-convex-rust-long-term-architecture/process/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md
|
|||
|
|
*
|
|||
|
|
* 如运行时文件系统扫描过慢,可使用 --from-snapshot 传入预收集的数据快照:
|
|||
|
|
* 1. 用 Reasonix 工具扫描:list_directory + read_file 收集所有用户 workpace.json
|
|||
|
|
* 2. 写入 JSON 文件(格式见 README)
|
|||
|
|
* 3. 传入工具: --from-snapshot /tmp/data-snapshot.json --skip-users
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require("node:fs");
|
|||
|
|
const path = require("node:path");
|
|||
|
|
|
|||
|
|
// ─── 默认路径 ───────────────────────────────────────────────
|
|||
|
|
const DEFAULT_DATA_ROOT = "/mnt/Data1T/Mnote_data";
|
|||
|
|
const DEFAULT_ACCESS_POLICY = path.join(DEFAULT_DATA_ROOT, "control-plane", "access-policy.json");
|
|||
|
|
const DEFAULT_USERS_ROOT = path.join(DEFAULT_DATA_ROOT, "users");
|
|||
|
|
|
|||
|
|
// ─── 版本标记 ───────────────────────────────────────────────
|
|||
|
|
const TOOL_VERSION = "0.1.0";
|
|||
|
|
const MIGRATION_PHASE = "Phase 0: dry-run & inventory";
|
|||
|
|
|
|||
|
|
// ─── CLI 参数解析 ────────────────────────────────────────────
|
|||
|
|
function usage() {
|
|||
|
|
return [
|
|||
|
|
`migrate-control-plane-to-sqlite.js v${TOOL_VERSION}`,
|
|||
|
|
"",
|
|||
|
|
"用法:",
|
|||
|
|
" node scripts/migrate-control-plane-to-sqlite.js --dry-run --out <路径>",
|
|||
|
|
"",
|
|||
|
|
"参数:",
|
|||
|
|
" --dry-run 必要参数。启动干跑模式(仅扫描、不写 DB)。",
|
|||
|
|
" --out <路径> 输出 migration-report.json 路径。",
|
|||
|
|
" --access-policy-path <路径> 自定义 access-policy.json 路径。",
|
|||
|
|
" --users-root <路径> 自定义用户根目录。",
|
|||
|
|
" --from-snapshot <路径> 从预收集的数据快照文件读取,替代直接文件系统扫描。",
|
|||
|
|
" --skip-users 跳过用户扫描(仅读取 access-policy)。",
|
|||
|
|
" --verbose 输出详细扫描日志。",
|
|||
|
|
" --help, -h 显示此帮助。",
|
|||
|
|
"",
|
|||
|
|
"示例:",
|
|||
|
|
" node scripts/migrate-control-plane-to-sqlite.js --dry-run --out /tmp/mnote-control-plane-migration-report.json",
|
|||
|
|
" node scripts/migrate-control-plane-to-sqlite.js --dry-run --out ./migration-report.json --verbose",
|
|||
|
|
" node scripts/migrate-control-plane-to-sqlite.js --dry-run --from-snapshot ./data-snapshot.json --out /tmp/report.json",
|
|||
|
|
].join("\n");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseArgs(argv) {
|
|||
|
|
const opts = {
|
|||
|
|
dryRun: false,
|
|||
|
|
outPath: null,
|
|||
|
|
accessPolicyPath: DEFAULT_ACCESS_POLICY,
|
|||
|
|
usersRoot: DEFAULT_USERS_ROOT,
|
|||
|
|
fromSnapshot: null,
|
|||
|
|
skipUsers: false,
|
|||
|
|
verbose: false,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (let i = 0; i < argv.length; i++) {
|
|||
|
|
const arg = argv[i];
|
|||
|
|
if (arg === "--help" || arg === "-h") {
|
|||
|
|
console.log(usage());
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
if (arg === "--dry-run") {
|
|||
|
|
opts.dryRun = true;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (arg === "--out") {
|
|||
|
|
opts.outPath = argv[++i];
|
|||
|
|
if (!opts.outPath) throw new Error("--out 需要参数");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (arg === "--access-policy-path") {
|
|||
|
|
opts.accessPolicyPath = argv[++i];
|
|||
|
|
if (!opts.accessPolicyPath) throw new Error("--access-policy-path 需要参数");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (arg === "--users-root") {
|
|||
|
|
opts.usersRoot = argv[++i];
|
|||
|
|
if (!opts.usersRoot) throw new Error("--users-root 需要参数");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (arg === "--from-snapshot") {
|
|||
|
|
opts.fromSnapshot = argv[++i];
|
|||
|
|
if (!opts.fromSnapshot) throw new Error("--from-snapshot 需要参数");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (arg === "--skip-users") {
|
|||
|
|
opts.skipUsers = true;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (arg === "--verbose") {
|
|||
|
|
opts.verbose = true;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
throw new Error(`未知参数:${arg}\n\n${usage()}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!opts.dryRun) {
|
|||
|
|
throw new Error("必须指定 --dry-run(当前版本仅支持干跑模式)\n\n" + usage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return opts;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 扫描工具函数 ──────────────────────────────────────────
|
|||
|
|
|
|||
|
|
function readJsonSafe(filePath, label) {
|
|||
|
|
try {
|
|||
|
|
if (!fs.existsSync(filePath)) {
|
|||
|
|
return { ok: false, error: `${label} 文件不存在:${filePath}` };
|
|||
|
|
}
|
|||
|
|
const raw = fs.readFileSync(filePath, "utf8");
|
|||
|
|
const data = JSON.parse(raw);
|
|||
|
|
return { ok: true, data };
|
|||
|
|
} catch (err) {
|
|||
|
|
return { ok: false, error: `读取 ${label} 失败:${err.message}` };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function collectUsers(usersRoot) {
|
|||
|
|
const results = [];
|
|||
|
|
if (!fs.existsSync(usersRoot)) {
|
|||
|
|
return { ok: false, error: `用户目录不存在:${usersRoot}`, users: [] };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const entries = fs.readdirSync(usersRoot, { withFileTypes: true });
|
|||
|
|
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
if (!entry.isDirectory()) continue;
|
|||
|
|
if (entry.name.startsWith(".")) continue;
|
|||
|
|
|
|||
|
|
const userId = entry.name;
|
|||
|
|
const userDir = path.join(usersRoot, userId);
|
|||
|
|
const workspacesDir = path.join(userDir, "workspaces");
|
|||
|
|
|
|||
|
|
const userEntry = {
|
|||
|
|
userId,
|
|||
|
|
userDir,
|
|||
|
|
workspacesDir,
|
|||
|
|
workspaceCount: 0,
|
|||
|
|
workspaces: [],
|
|||
|
|
errors: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(workspacesDir)) {
|
|||
|
|
userEntry.errors.push("无 workspaces 目录");
|
|||
|
|
results.push(userEntry);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const wsEntries = fs.readdirSync(workspacesDir, { withFileTypes: true });
|
|||
|
|
for (const ws of wsEntries) {
|
|||
|
|
if (!ws.isDirectory()) continue;
|
|||
|
|
if (ws.name.startsWith(".")) continue;
|
|||
|
|
|
|||
|
|
const wsDir = path.join(workspacesDir, ws.name);
|
|||
|
|
const wsMetaDir = path.join(wsDir, ".mnote");
|
|||
|
|
const wsMetaFile = path.join(wsMetaDir, "workspace.json");
|
|||
|
|
|
|||
|
|
const wsEntry = {
|
|||
|
|
workspaceName: ws.name,
|
|||
|
|
workspaceDir: wsDir,
|
|||
|
|
metaFile: wsMetaFile,
|
|||
|
|
meta: null,
|
|||
|
|
pageCount: 0,
|
|||
|
|
pages: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 读取 workspace.json
|
|||
|
|
const metaResult = readJsonSafe(wsMetaFile, `${userId}/${ws.name}/workspace.json`);
|
|||
|
|
if (metaResult.ok) {
|
|||
|
|
wsEntry.meta = metaResult.data;
|
|||
|
|
} else {
|
|||
|
|
userEntry.errors.push(`${ws.name}:${metaResult.error}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 统计页面(一级 .md 文件)
|
|||
|
|
if (fs.existsSync(wsDir)) {
|
|||
|
|
const pageFiles = fs.readdirSync(wsDir).filter((f) => f.endsWith(".md"));
|
|||
|
|
wsEntry.pageCount = pageFiles.length;
|
|||
|
|
wsEntry.pages = pageFiles;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
userEntry.workspaces.push(wsEntry);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
userEntry.workspaceCount = userEntry.workspaces.length;
|
|||
|
|
results.push(userEntry);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { ok: true, users: results };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 构建迁移报告 ──────────────────────────────────────────
|
|||
|
|
|
|||
|
|
function buildReport(opts, accessPolicyResult, usersResult) {
|
|||
|
|
const report = {
|
|||
|
|
meta: {
|
|||
|
|
toolVersion: TOOL_VERSION,
|
|||
|
|
migrationPhase: MIGRATION_PHASE,
|
|||
|
|
generatedAt: new Date().toISOString(),
|
|||
|
|
source: {
|
|||
|
|
accessPolicy: opts.accessPolicyPath,
|
|||
|
|
usersRoot: opts.usersRoot,
|
|||
|
|
},
|
|||
|
|
flags: {
|
|||
|
|
dryRun: true,
|
|||
|
|
skipUsers: opts.skipUsers,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
accessPolicy: {
|
|||
|
|
path: opts.accessPolicyPath,
|
|||
|
|
readOk: accessPolicyResult.ok,
|
|||
|
|
error: accessPolicyResult.ok ? null : accessPolicyResult.error,
|
|||
|
|
admins: accessPolicyResult.ok ? (accessPolicyResult.data.admins || []) : [],
|
|||
|
|
grants: accessPolicyResult.ok ? (accessPolicyResult.data.grants || []) : [],
|
|||
|
|
},
|
|||
|
|
users: {
|
|||
|
|
scanOk: usersResult.ok,
|
|||
|
|
error: usersResult.ok ? null : usersResult.error,
|
|||
|
|
total: usersResult.ok ? usersResult.users.length : 0,
|
|||
|
|
items: [],
|
|||
|
|
},
|
|||
|
|
suggestions: {
|
|||
|
|
users: [],
|
|||
|
|
workspaces: [],
|
|||
|
|
grants: [],
|
|||
|
|
legacyIdMap: [],
|
|||
|
|
},
|
|||
|
|
summary: {
|
|||
|
|
adminCount: 0,
|
|||
|
|
userCount: 0,
|
|||
|
|
workspaceCount: 0,
|
|||
|
|
grantCount: 0,
|
|||
|
|
legacyMappingCount: 0,
|
|||
|
|
errors: [],
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!usersResult.ok) {
|
|||
|
|
report.summary.errors.push(usersResult.error);
|
|||
|
|
return report;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const u of usersResult.users) {
|
|||
|
|
const userItem = {
|
|||
|
|
userId: u.userId,
|
|||
|
|
workspaceCount: u.workspaceCount,
|
|||
|
|
errors: u.errors,
|
|||
|
|
workspaces: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (const ws of u.workspaces) {
|
|||
|
|
const wsItem = {
|
|||
|
|
workspaceName: ws.workspaceName,
|
|||
|
|
meta: ws.meta,
|
|||
|
|
pageCount: ws.pageCount,
|
|||
|
|
};
|
|||
|
|
userItem.workspaces.push(wsItem);
|
|||
|
|
|
|||
|
|
// ── 生成 workspace 建议 ──
|
|||
|
|
if (ws.meta && ws.meta.workspaceId) {
|
|||
|
|
const parts = ws.meta.workspaceId.split(":");
|
|||
|
|
const newWsId = `sqlite-ws:${u.userId}:${ws.workspaceName}`;
|
|||
|
|
report.suggestions.workspaces.push({
|
|||
|
|
legacyWorkspaceId: ws.meta.workspaceId,
|
|||
|
|
suggestedNewId: newWsId,
|
|||
|
|
ownerId: u.userId,
|
|||
|
|
name: ws.workspaceName,
|
|||
|
|
rootPath: ws.workspaceDir,
|
|||
|
|
source: "workspace.json",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── 生成 user 建议 ──
|
|||
|
|
const existingUser = report.suggestions.users.find((x) => x.legacyUserId === u.userId);
|
|||
|
|
if (!existingUser) {
|
|||
|
|
// 检查是否在 access-policy 的 admins 中
|
|||
|
|
const isAdmin = accessPolicyResult.ok && (accessPolicyResult.data.admins || []).includes(u.userId);
|
|||
|
|
const newUserId = `usr_${u.userId.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|||
|
|
report.suggestions.users.push({
|
|||
|
|
legacyUserId: u.userId,
|
|||
|
|
suggestedNewId: newUserId,
|
|||
|
|
role: isAdmin ? "admin" : "user",
|
|||
|
|
isAdmin,
|
|||
|
|
defaultWorkspace: ws.workspaceName,
|
|||
|
|
source: "workspace.json",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── 生成 legacy_id_map 建议 ──
|
|||
|
|
report.suggestions.legacyIdMap.push({
|
|||
|
|
legacySystem: "local_workspace_manifest",
|
|||
|
|
legacyKind: "user",
|
|||
|
|
legacyId: u.userId,
|
|||
|
|
newKind: "user",
|
|||
|
|
newId: newUserId,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
report.users.items.push(userItem);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 生成 grant 建议 ──
|
|||
|
|
if (accessPolicyResult.ok && accessPolicyResult.data.admins) {
|
|||
|
|
for (const adminId of accessPolicyResult.data.admins) {
|
|||
|
|
// 找到该 admin 对应的 user 建议
|
|||
|
|
const adminUser = report.suggestions.users.find((x) => x.legacyUserId === adminId);
|
|||
|
|
report.suggestions.grants.push({
|
|||
|
|
legacyAdminId: adminId,
|
|||
|
|
suggestedNewUserId: adminUser ? adminUser.suggestedNewId : adminId,
|
|||
|
|
type: "admin",
|
|||
|
|
rootUri: `mnote://users/${adminId}`,
|
|||
|
|
rootPath: path.join(opts.usersRoot, adminId),
|
|||
|
|
permission: "admin",
|
|||
|
|
capabilities: ["admin", "share", "ai"],
|
|||
|
|
source: "access-policy.json:admins",
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (accessPolicyResult.ok && accessPolicyResult.data.grants) {
|
|||
|
|
for (const grant of accessPolicyResult.data.grants) {
|
|||
|
|
report.suggestions.grants.push({
|
|||
|
|
legacySource: "access-policy.json:grants",
|
|||
|
|
source: grant,
|
|||
|
|
type: "directory_grant",
|
|||
|
|
...grant,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 生成 legacy_id_map 建议(workspaces)──
|
|||
|
|
for (const ws of report.suggestions.workspaces) {
|
|||
|
|
report.suggestions.legacyIdMap.push({
|
|||
|
|
legacySystem: "local_workspace_manifest",
|
|||
|
|
legacyKind: "workspace",
|
|||
|
|
legacyId: ws.legacyWorkspaceId,
|
|||
|
|
newKind: "workspace",
|
|||
|
|
newId: ws.suggestedNewId,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 填写概要 ──
|
|||
|
|
report.summary.adminCount = report.accessPolicy.admins.length;
|
|||
|
|
report.summary.userCount = report.users.total;
|
|||
|
|
report.summary.workspaceCount = report.suggestions.workspaces.length;
|
|||
|
|
report.summary.grantCount = report.suggestions.grants.length;
|
|||
|
|
report.summary.legacyMappingCount = report.suggestions.legacyIdMap.length;
|
|||
|
|
|
|||
|
|
// 收集所有错误
|
|||
|
|
for (const u of usersResult.users) {
|
|||
|
|
for (const err of u.errors) {
|
|||
|
|
report.summary.errors.push(`[${u.userId}] ${err}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return report;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 入口 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
function main() {
|
|||
|
|
const opts = parseArgs(process.argv.slice(2));
|
|||
|
|
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
console.error(`[migrate-control-plane] 版本 ${TOOL_VERSION}`);
|
|||
|
|
console.error(`[migrate-control-plane] access-policy: ${opts.accessPolicyPath}`);
|
|||
|
|
console.error(`[migrate-control-plane] users-root: ${opts.usersRoot}`);
|
|||
|
|
console.error(`[migrate-control-plane] skip-users: ${opts.skipUsers}`);
|
|||
|
|
console.error(`[migrate-control-plane] dry-run: ${opts.dryRun}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. 读取 access-policy.json(可能被快照覆盖,用 let)
|
|||
|
|
let accessPolicyResult = readJsonSafe(opts.accessPolicyPath, "access-policy.json");
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
if (accessPolicyResult.ok) {
|
|||
|
|
console.error(`[migrate-control-plane] access-policy.json 读取成功`);
|
|||
|
|
console.error(`[migrate-control-plane] admins: ${JSON.stringify(accessPolicyResult.data.admins)}`);
|
|||
|
|
console.error(`[migrate-control-plane] grants: ${accessPolicyResult.data.grants ? accessPolicyResult.data.grants.length : 0} 条`);
|
|||
|
|
} else {
|
|||
|
|
console.error(`[migrate-control-plane] access-policy.json 读取失败: ${accessPolicyResult.error}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 扫描用户(支持从快照文件读取 vs 直接文件系统扫描)
|
|||
|
|
let usersResult = { ok: true, users: [] };
|
|||
|
|
if (opts.fromSnapshot) {
|
|||
|
|
const snapshotResult = readJsonSafe(opts.fromSnapshot, "数据快照");
|
|||
|
|
if (snapshotResult.ok) {
|
|||
|
|
const snapshot = snapshotResult.data;
|
|||
|
|
if (snapshot.users) {
|
|||
|
|
usersResult = { ok: true, users: snapshot.users };
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
console.error(`[migrate-control-plane] 从快照读取 ${snapshot.users.length} 个用户`);
|
|||
|
|
}
|
|||
|
|
// 如果快照中已有 accessPolicy,覆盖 access-policy.json 的读取
|
|||
|
|
if (snapshot.accessPolicy && !opts.accessPolicyPath) {
|
|||
|
|
const policyData = { ok: true, data: snapshot.accessPolicy };
|
|||
|
|
// 替换 accessPolicyResult
|
|||
|
|
accessPolicyResult = policyData;
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
console.error(`[migrate-control-plane] 快照中的 accessPolicy 已生效`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
usersResult = { ok: false, error: "快照文件中缺少 users 字段", users: [] };
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
usersResult = { ok: false, error: snapshotResult.error, users: [] };
|
|||
|
|
}
|
|||
|
|
} else if (!opts.skipUsers) {
|
|||
|
|
usersResult = collectUsers(opts.usersRoot);
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
if (usersResult.ok) {
|
|||
|
|
console.error(`[migrate-control-plane] 用户扫描完成: ${usersResult.users.length} 个用户`);
|
|||
|
|
for (const u of usersResult.users) {
|
|||
|
|
console.error(`[migrate-control-plane] ${u.userId}: ${u.workspaceCount} 个工作区`);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
console.error(`[migrate-control-plane] 用户扫描失败: ${usersResult.error}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
console.error(`[migrate-control-plane] 用户扫描已跳过 (--skip-users)`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 构建报告
|
|||
|
|
const report = buildReport(opts, accessPolicyResult, usersResult);
|
|||
|
|
|
|||
|
|
// 4. 输出
|
|||
|
|
const json = JSON.stringify(report, null, 2);
|
|||
|
|
|
|||
|
|
if (opts.outPath) {
|
|||
|
|
fs.mkdirSync(path.dirname(opts.outPath), { recursive: true });
|
|||
|
|
fs.writeFileSync(opts.outPath, json, "utf8");
|
|||
|
|
if (opts.verbose) {
|
|||
|
|
console.error(`[migrate-control-plane] 报告已写入: ${opts.outPath}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// stdout 只输出概要(便于管道处理)
|
|||
|
|
const brief = {
|
|||
|
|
ok: true,
|
|||
|
|
tool: "migrate-control-plane-to-sqlite",
|
|||
|
|
dryRun: true,
|
|||
|
|
summary: report.summary,
|
|||
|
|
outPath: opts.outPath || "(stdout)",
|
|||
|
|
};
|
|||
|
|
console.log(JSON.stringify(brief, null, 2));
|
|||
|
|
|
|||
|
|
// 如果有错误但不太严重,仍然 exit 0
|
|||
|
|
// 严重错误(access-policy 读不到、用户目录不存在)通过 summary.errors 和 accessPolicy.error 体现
|
|||
|
|
const fatalCount = report.summary.errors.length;
|
|||
|
|
if (fatalCount > 0) {
|
|||
|
|
console.error(`[migrate-control-plane] 警告:扫描中发现 ${fatalCount} 个非致命错误,详见报告 errors`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
try {
|
|||
|
|
main();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|