create-windy 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +467 -59
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1040,6 +1040,7 @@ var ignoredFiles = new Set([
|
|
|
1040
1040
|
]);
|
|
1041
1041
|
var projectOwnedRoots = [
|
|
1042
1042
|
"apps/web/src/app",
|
|
1043
|
+
"apps/server/src/installed-business-modules.ts",
|
|
1043
1044
|
"apps/server/src/work-order",
|
|
1044
1045
|
"packages/example-work-order"
|
|
1045
1046
|
];
|
|
@@ -1048,8 +1049,13 @@ var sharedScaffoldRoots = [
|
|
|
1048
1049
|
"AGENTS.md",
|
|
1049
1050
|
".env.example",
|
|
1050
1051
|
"docker-compose.yml",
|
|
1052
|
+
"drizzle.config.ts",
|
|
1053
|
+
"apps/server/Dockerfile",
|
|
1054
|
+
"apps/server/src/index.ts",
|
|
1055
|
+
"apps/web/Dockerfile",
|
|
1051
1056
|
"apps/web/src/layout",
|
|
1052
|
-
"apps/web/src/router"
|
|
1057
|
+
"apps/web/src/router",
|
|
1058
|
+
"packages/database/src/schema/index.ts"
|
|
1053
1059
|
];
|
|
1054
1060
|
async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
|
|
1055
1061
|
const paths = await collectFiles(targetDirectory);
|
|
@@ -4063,7 +4069,8 @@ var recipes = [
|
|
|
4063
4069
|
{ id: "starter-0.2.14-to-0.2.15", from: "0.2.14", to: "0.2.15" },
|
|
4064
4070
|
{ id: "starter-0.2.15-to-0.2.16", from: "0.2.15", to: "0.2.16" },
|
|
4065
4071
|
{ id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" },
|
|
4066
|
-
{ id: "starter-0.2.17-to-0.2.18", from: "0.2.17", to: "0.2.18" }
|
|
4072
|
+
{ id: "starter-0.2.17-to-0.2.18", from: "0.2.17", to: "0.2.18" },
|
|
4073
|
+
{ id: "starter-0.2.18-to-0.2.19", from: "0.2.18", to: "0.2.19" }
|
|
4067
4074
|
];
|
|
4068
4075
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
4069
4076
|
if (sourceVersion === targetVersion)
|
|
@@ -4088,8 +4095,8 @@ function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
|
4088
4095
|
}
|
|
4089
4096
|
|
|
4090
4097
|
// src/update/updater.ts
|
|
4091
|
-
import { mkdir as mkdir7, readFile as
|
|
4092
|
-
import { dirname as dirname4, join as
|
|
4098
|
+
import { mkdir as mkdir7, readFile as readFile15, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
|
|
4099
|
+
import { dirname as dirname4, join as join16 } from "node:path";
|
|
4093
4100
|
|
|
4094
4101
|
// src/update/artifacts.ts
|
|
4095
4102
|
import { mkdir as mkdir6, mkdtemp, readFile as readFile12 } from "node:fs/promises";
|
|
@@ -4151,8 +4158,8 @@ async function readProjectName2(projectDirectory) {
|
|
|
4151
4158
|
}
|
|
4152
4159
|
|
|
4153
4160
|
// src/update/planner.ts
|
|
4154
|
-
import { access as access3, readFile as
|
|
4155
|
-
import { join as
|
|
4161
|
+
import { access as access3, readFile as readFile14 } from "node:fs/promises";
|
|
4162
|
+
import { join as join15 } from "node:path";
|
|
4156
4163
|
|
|
4157
4164
|
// src/update/known-repairs.ts
|
|
4158
4165
|
var wrongTextVersions = new Set(["0.2.12", "0.2.13"]);
|
|
@@ -4213,13 +4220,397 @@ async function mergeTextFile(base, ours, theirs) {
|
|
|
4213
4220
|
if (result.code === 0) {
|
|
4214
4221
|
return { content: new TextEncoder().encode(result.stdout) };
|
|
4215
4222
|
}
|
|
4216
|
-
if (result.code === 1)
|
|
4217
|
-
|
|
4223
|
+
if (result.code === 1) {
|
|
4224
|
+
const resolved = resolveConservativeConflicts(result.stdout);
|
|
4225
|
+
return resolved ? { content: new TextEncoder().encode(resolved) } : { conflict: "三方合并存在重叠修改" };
|
|
4226
|
+
}
|
|
4218
4227
|
return { conflict: result.stderr.trim() || "git merge-file 执行失败" };
|
|
4219
4228
|
} finally {
|
|
4220
4229
|
await rm6(root, { recursive: true, force: true });
|
|
4221
4230
|
}
|
|
4222
4231
|
}
|
|
4232
|
+
function resolveConservativeConflicts(content) {
|
|
4233
|
+
const lines = content.split(`
|
|
4234
|
+
`);
|
|
4235
|
+
const output = [];
|
|
4236
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
4237
|
+
if (!lines[index]?.startsWith("<<<<<<< ")) {
|
|
4238
|
+
output.push(lines[index] ?? "");
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
const baseMarker = lines.findIndex((line, candidate) => candidate > index && line.startsWith("||||||| "));
|
|
4242
|
+
const separator = lines.indexOf("=======", baseMarker + 1);
|
|
4243
|
+
const endMarker = lines.findIndex((line, candidate) => candidate > separator && line.startsWith(">>>>>>> "));
|
|
4244
|
+
if (baseMarker < 0 || separator < 0 || endMarker < 0)
|
|
4245
|
+
return;
|
|
4246
|
+
const ours = lines.slice(index + 1, baseMarker);
|
|
4247
|
+
const base = lines.slice(baseMarker + 1, separator);
|
|
4248
|
+
const theirs = lines.slice(separator + 1, endMarker);
|
|
4249
|
+
const resolved = resolveRegion(ours, base, theirs);
|
|
4250
|
+
if (!resolved)
|
|
4251
|
+
return;
|
|
4252
|
+
output.push(...resolved);
|
|
4253
|
+
index = endMarker;
|
|
4254
|
+
}
|
|
4255
|
+
return output.join(`
|
|
4256
|
+
`);
|
|
4257
|
+
}
|
|
4258
|
+
function resolveRegion(ours, base, theirs) {
|
|
4259
|
+
const oursRebased = replaceSubsequence(ours, base, theirs);
|
|
4260
|
+
if (oursRebased)
|
|
4261
|
+
return oursRebased;
|
|
4262
|
+
const theirsRebased = replaceSubsequence(theirs, base, ours);
|
|
4263
|
+
if (theirsRebased)
|
|
4264
|
+
return theirsRebased;
|
|
4265
|
+
if (normalizeWhitespace(ours) === normalizeWhitespace(base))
|
|
4266
|
+
return theirs;
|
|
4267
|
+
if (normalizeWhitespace(theirs) === normalizeWhitespace(base))
|
|
4268
|
+
return ours;
|
|
4269
|
+
return applySemanticTokenEdits(ours, base, theirs) ?? mergeMarkdownTable(ours, base, theirs);
|
|
4270
|
+
}
|
|
4271
|
+
function replaceSubsequence(content, baseline, replacement) {
|
|
4272
|
+
if (!baseline.length || baseline.length > content.length)
|
|
4273
|
+
return;
|
|
4274
|
+
const starts = [];
|
|
4275
|
+
for (let index = 0;index <= content.length - baseline.length; index += 1) {
|
|
4276
|
+
if (baseline.every((line, offset) => content[index + offset] === line)) {
|
|
4277
|
+
starts.push(index);
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
if (starts.length !== 1)
|
|
4281
|
+
return;
|
|
4282
|
+
const start = starts[0] ?? 0;
|
|
4283
|
+
return [
|
|
4284
|
+
...content.slice(0, start),
|
|
4285
|
+
...replacement,
|
|
4286
|
+
...content.slice(start + baseline.length)
|
|
4287
|
+
];
|
|
4288
|
+
}
|
|
4289
|
+
function normalizeWhitespace(lines) {
|
|
4290
|
+
return lines.join(" ").replaceAll(/\s+/g, " ").trim();
|
|
4291
|
+
}
|
|
4292
|
+
function applySemanticTokenEdits(oursLines, baseLines, theirsLines) {
|
|
4293
|
+
const oursText = oursLines.join(`
|
|
4294
|
+
`);
|
|
4295
|
+
const baseText = baseLines.join(`
|
|
4296
|
+
`);
|
|
4297
|
+
const theirsText = theirsLines.join(`
|
|
4298
|
+
`);
|
|
4299
|
+
const ours = semanticTokens(oursText);
|
|
4300
|
+
const base = semanticTokens(baseText);
|
|
4301
|
+
const theirs = semanticTokens(theirsText);
|
|
4302
|
+
let prefix = 0;
|
|
4303
|
+
while (prefix < base.length && prefix < theirs.length && base[prefix]?.value === theirs[prefix]?.value) {
|
|
4304
|
+
prefix += 1;
|
|
4305
|
+
}
|
|
4306
|
+
let suffix = 0;
|
|
4307
|
+
while (suffix < base.length - prefix && suffix < theirs.length - prefix && base[base.length - 1 - suffix]?.value === theirs[theirs.length - 1 - suffix]?.value) {
|
|
4308
|
+
suffix += 1;
|
|
4309
|
+
}
|
|
4310
|
+
const baseEnd = base.length - suffix;
|
|
4311
|
+
const targetEnd = theirs.length - suffix;
|
|
4312
|
+
const needle = base.slice(prefix, baseEnd).map(({ value }) => value);
|
|
4313
|
+
if (!needle.length)
|
|
4314
|
+
return;
|
|
4315
|
+
const starts = findTokenSequence(ours, needle);
|
|
4316
|
+
if (starts.length !== 1)
|
|
4317
|
+
return;
|
|
4318
|
+
const oursStart = starts[0] ?? 0;
|
|
4319
|
+
const oursEnd = oursStart + needle.length - 1;
|
|
4320
|
+
const targetFirst = theirs[prefix];
|
|
4321
|
+
const targetLast = theirs[targetEnd - 1];
|
|
4322
|
+
const replacement = targetFirst && targetLast ? theirsText.slice(targetFirst.start, targetLast.end) : "";
|
|
4323
|
+
const merged = oursText.slice(0, ours[oursStart]?.start ?? 0) + replacement + oursText.slice(ours[oursEnd]?.end ?? 0);
|
|
4324
|
+
return merged.split(`
|
|
4325
|
+
`);
|
|
4326
|
+
}
|
|
4327
|
+
function semanticTokens(content) {
|
|
4328
|
+
return [...content.matchAll(/[A-Za-z0-9_@./:-]+|[^\s]/g)].map((match) => ({
|
|
4329
|
+
value: match[0],
|
|
4330
|
+
start: match.index,
|
|
4331
|
+
end: match.index + match[0].length
|
|
4332
|
+
}));
|
|
4333
|
+
}
|
|
4334
|
+
function findTokenSequence(tokens, needle) {
|
|
4335
|
+
const starts = [];
|
|
4336
|
+
for (let index = 0;index <= tokens.length - needle.length; index += 1) {
|
|
4337
|
+
if (needle.every((value, offset) => tokens[index + offset]?.value === value)) {
|
|
4338
|
+
starts.push(index);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
return starts;
|
|
4342
|
+
}
|
|
4343
|
+
function mergeMarkdownTable(ours, base, theirs) {
|
|
4344
|
+
if (![ours, base, theirs].every((rows) => rows.length === base.length && rows.every(isMarkdownTableRow))) {
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
const parsed = [ours, base, theirs].map((rows) => rows.map(parseTableRow));
|
|
4348
|
+
const [oursRows, baseRows, theirsRows] = parsed;
|
|
4349
|
+
if (!oursRows || !baseRows || !theirsRows || !oursRows.every((cells, index) => cells.length === baseRows[index]?.length && cells.length === theirsRows[index]?.length)) {
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
const merged = [];
|
|
4353
|
+
for (let row = 0;row < baseRows.length; row += 1) {
|
|
4354
|
+
const cells = [];
|
|
4355
|
+
for (let column = 0;column < (baseRows[row]?.length ?? 0); column += 1) {
|
|
4356
|
+
const baseCell = baseRows[row]?.[column] ?? "";
|
|
4357
|
+
const oursCell = oursRows[row]?.[column] ?? "";
|
|
4358
|
+
const theirsCell = theirsRows[row]?.[column] ?? "";
|
|
4359
|
+
if (isTableSeparator(baseCell)) {
|
|
4360
|
+
cells.push("---");
|
|
4361
|
+
} else if (oursCell === baseCell || oursCell === theirsCell) {
|
|
4362
|
+
cells.push(theirsCell);
|
|
4363
|
+
} else if (theirsCell === baseCell) {
|
|
4364
|
+
cells.push(oursCell);
|
|
4365
|
+
} else {
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
merged.push(`| ${cells.join(" | ")} |`);
|
|
4370
|
+
}
|
|
4371
|
+
return merged;
|
|
4372
|
+
}
|
|
4373
|
+
function isMarkdownTableRow(line) {
|
|
4374
|
+
const trimmed = line.trim();
|
|
4375
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|");
|
|
4376
|
+
}
|
|
4377
|
+
function parseTableRow(line) {
|
|
4378
|
+
return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
|
|
4379
|
+
}
|
|
4380
|
+
function isTableSeparator(cell) {
|
|
4381
|
+
return /^:?-{3,}:?$/.test(cell);
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
// src/update/migration-reconciler.ts
|
|
4385
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
4386
|
+
import { join as join14 } from "node:path";
|
|
4387
|
+
|
|
4388
|
+
// src/update/snapshot-delta.ts
|
|
4389
|
+
function applySnapshotDelta(before, after, current, path2, conflicts, conflictPath) {
|
|
4390
|
+
if (deepEqual(before, after))
|
|
4391
|
+
return structuredClone(current);
|
|
4392
|
+
if (!isRecord(before) || !isRecord(after) || !isRecord(current)) {
|
|
4393
|
+
if (deepEqual(current, before) || deepEqual(current, after)) {
|
|
4394
|
+
return structuredClone(after);
|
|
4395
|
+
}
|
|
4396
|
+
conflicts.push(`${conflictPath}:平台与业务同时修改 snapshot 节点 ${path2.join(".") || "<root>"}`);
|
|
4397
|
+
return structuredClone(current);
|
|
4398
|
+
}
|
|
4399
|
+
const merged = structuredClone(current);
|
|
4400
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
4401
|
+
for (const key of keys) {
|
|
4402
|
+
if (path2.length === 0 && (key === "id" || key === "prevId"))
|
|
4403
|
+
continue;
|
|
4404
|
+
const nextPath = [...path2, key];
|
|
4405
|
+
if (!(key in after)) {
|
|
4406
|
+
if (!(key in current))
|
|
4407
|
+
continue;
|
|
4408
|
+
if (deepEqual(current[key], before[key]))
|
|
4409
|
+
delete merged[key];
|
|
4410
|
+
else
|
|
4411
|
+
conflicts.push(`${conflictPath}:平台删除了业务已修改的 snapshot 节点 ${nextPath.join(".")}`);
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (!(key in before)) {
|
|
4415
|
+
if (!(key in current) || deepEqual(current[key], after[key])) {
|
|
4416
|
+
merged[key] = structuredClone(after[key]);
|
|
4417
|
+
} else {
|
|
4418
|
+
conflicts.push(`${conflictPath}:平台新增的 snapshot 节点已被业务占用 ${nextPath.join(".")}`);
|
|
4419
|
+
}
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
merged[key] = applySnapshotDelta(before[key], after[key], current[key], nextPath, conflicts, conflictPath);
|
|
4423
|
+
}
|
|
4424
|
+
return merged;
|
|
4425
|
+
}
|
|
4426
|
+
function isRecord(value) {
|
|
4427
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4428
|
+
}
|
|
4429
|
+
function deepEqual(left, right) {
|
|
4430
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
// src/update/migration-reconciler.ts
|
|
4434
|
+
var drizzleRoot = "packages/database/drizzle";
|
|
4435
|
+
var journalPath = `${drizzleRoot}/meta/_journal.json`;
|
|
4436
|
+
var migrationPattern = /^(\d{4})_(.+)$/;
|
|
4437
|
+
async function reconcileDrizzleMigrations(input) {
|
|
4438
|
+
const journals = await readJournals(input);
|
|
4439
|
+
if (!journals) {
|
|
4440
|
+
return unchanged(input.targetManifest);
|
|
4441
|
+
}
|
|
4442
|
+
const { base, project, target } = journals;
|
|
4443
|
+
const conflicts = [];
|
|
4444
|
+
validateUniqueNames(base, "来源模板", conflicts);
|
|
4445
|
+
validateUniqueNames(project, "当前项目", conflicts);
|
|
4446
|
+
validateUniqueNames(target, "目标模板", conflicts);
|
|
4447
|
+
if (conflicts.length) {
|
|
4448
|
+
return { ...unchanged(input.targetManifest), conflicts };
|
|
4449
|
+
}
|
|
4450
|
+
const baseNames = new Set(base.entries.map(({ tag }) => stableName(tag)));
|
|
4451
|
+
const projectNames = new Set(project.entries.map(({ tag }) => stableName(tag)));
|
|
4452
|
+
const managedNames = new Set(input.sourceManifest.files.filter(({ path: path2 }) => isSqlMigration(path2)).map(({ path: path2 }) => stableName(path2.slice(drizzleRoot.length + 1, -4))));
|
|
4453
|
+
const candidates = target.entries.filter(({ tag }) => !baseNames.has(stableName(tag)));
|
|
4454
|
+
for (const { tag } of candidates) {
|
|
4455
|
+
const name = stableName(tag);
|
|
4456
|
+
if (projectNames.has(name) && !managedNames.has(name)) {
|
|
4457
|
+
conflicts.push(`${journalPath}:平台 migration 名称与下游业务历史冲突 ${name}`);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
const additions = candidates.filter(({ tag }) => !projectNames.has(stableName(tag)));
|
|
4461
|
+
const contentOverrides = new Map;
|
|
4462
|
+
const files = await preservePublishedHistory(input.sourceManifest, input.projectDirectory, contentOverrides);
|
|
4463
|
+
const publishedPaths = new Set(files.map(({ path: path2 }) => path2));
|
|
4464
|
+
files.push(...input.targetManifest.files.filter(({ path: path2 }) => !isDrizzleHistoryPath(path2)));
|
|
4465
|
+
if (!additions.length) {
|
|
4466
|
+
const journalContent2 = encodeJson(project);
|
|
4467
|
+
contentOverrides.set(journalPath, journalContent2);
|
|
4468
|
+
files.push(managedEntry(journalPath, journalContent2));
|
|
4469
|
+
return result(input.targetManifest, files, contentOverrides, publishedPaths, conflicts);
|
|
4470
|
+
}
|
|
4471
|
+
let nextPrefix = highestPrefix(project) + 1;
|
|
4472
|
+
let nextIndex = highestIndex(project) + 1;
|
|
4473
|
+
let previousWhen = project.entries.at(-1)?.when ?? 0;
|
|
4474
|
+
let currentSnapshot = await readLatestSnapshot(input.projectDirectory, project);
|
|
4475
|
+
const mergedJournal = structuredClone(project);
|
|
4476
|
+
for (const addition of additions) {
|
|
4477
|
+
const targetPosition = target.entries.indexOf(addition);
|
|
4478
|
+
const previousTarget = target.entries[targetPosition - 1];
|
|
4479
|
+
if (!previousTarget) {
|
|
4480
|
+
conflicts.push(`${journalPath}:目标 migration 缺少前置快照`);
|
|
4481
|
+
break;
|
|
4482
|
+
}
|
|
4483
|
+
const targetBefore = await readSnapshot(input.targetDirectory, previousTarget.tag);
|
|
4484
|
+
const targetAfter = await readSnapshot(input.targetDirectory, addition.tag);
|
|
4485
|
+
const merged = applySnapshotDelta(targetBefore, targetAfter, currentSnapshot, [], conflicts, journalPath);
|
|
4486
|
+
if (conflicts.length)
|
|
4487
|
+
break;
|
|
4488
|
+
const prefix = String(nextPrefix).padStart(4, "0");
|
|
4489
|
+
const name = stableName(addition.tag);
|
|
4490
|
+
const tag = `${prefix}_${name}`;
|
|
4491
|
+
const sqlPath = `${drizzleRoot}/${tag}.sql`;
|
|
4492
|
+
const snapshotPath = `${drizzleRoot}/meta/${prefix}_snapshot.json`;
|
|
4493
|
+
const sql = await readFile13(join14(input.targetDirectory, `${drizzleRoot}/${addition.tag}.sql`));
|
|
4494
|
+
merged.id = targetAfter.id;
|
|
4495
|
+
merged.prevId = currentSnapshot.id;
|
|
4496
|
+
const snapshot = encodeJson(merged);
|
|
4497
|
+
contentOverrides.set(sqlPath, sql);
|
|
4498
|
+
contentOverrides.set(snapshotPath, snapshot);
|
|
4499
|
+
files.push(managedEntry(sqlPath, sql), managedEntry(snapshotPath, snapshot));
|
|
4500
|
+
previousWhen = Math.max(previousWhen + 1, addition.when);
|
|
4501
|
+
mergedJournal.entries.push({
|
|
4502
|
+
...addition,
|
|
4503
|
+
idx: nextIndex,
|
|
4504
|
+
when: previousWhen,
|
|
4505
|
+
tag
|
|
4506
|
+
});
|
|
4507
|
+
currentSnapshot = merged;
|
|
4508
|
+
nextPrefix += 1;
|
|
4509
|
+
nextIndex += 1;
|
|
4510
|
+
}
|
|
4511
|
+
const journalContent = encodeJson(mergedJournal);
|
|
4512
|
+
contentOverrides.set(journalPath, journalContent);
|
|
4513
|
+
files.push(managedEntry(journalPath, journalContent));
|
|
4514
|
+
return result(input.targetManifest, files, contentOverrides, publishedPaths, conflicts);
|
|
4515
|
+
}
|
|
4516
|
+
function result(targetManifest, files, contentOverrides, forcePreserves, conflicts) {
|
|
4517
|
+
return {
|
|
4518
|
+
targetManifest: {
|
|
4519
|
+
...targetManifest,
|
|
4520
|
+
files: deduplicate(files).sort((left, right) => left.path.localeCompare(right.path))
|
|
4521
|
+
},
|
|
4522
|
+
contentOverrides,
|
|
4523
|
+
forceUpdates: new Set([journalPath]),
|
|
4524
|
+
forcePreserves,
|
|
4525
|
+
conflicts
|
|
4526
|
+
};
|
|
4527
|
+
}
|
|
4528
|
+
function unchanged(targetManifest) {
|
|
4529
|
+
return {
|
|
4530
|
+
targetManifest,
|
|
4531
|
+
contentOverrides: new Map,
|
|
4532
|
+
forceUpdates: new Set,
|
|
4533
|
+
forcePreserves: new Set,
|
|
4534
|
+
conflicts: []
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
async function readJournals(input) {
|
|
4538
|
+
try {
|
|
4539
|
+
const [base, project, target] = await Promise.all([
|
|
4540
|
+
readJson(join14(input.baseDirectory, journalPath)),
|
|
4541
|
+
readJson(join14(input.projectDirectory, journalPath)),
|
|
4542
|
+
readJson(join14(input.targetDirectory, journalPath))
|
|
4543
|
+
]);
|
|
4544
|
+
return { base, project, target };
|
|
4545
|
+
} catch (error) {
|
|
4546
|
+
if (isMissing4(error))
|
|
4547
|
+
return;
|
|
4548
|
+
throw error;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
async function preservePublishedHistory(manifest, projectDirectory, overrides) {
|
|
4552
|
+
const entries = manifest.files.filter(({ path: path2 }) => isMigrationArtifact(path2) && path2 !== journalPath);
|
|
4553
|
+
await Promise.all(entries.map(async ({ path: path2 }) => {
|
|
4554
|
+
overrides.set(path2, await readFile13(join14(projectDirectory, path2)));
|
|
4555
|
+
}));
|
|
4556
|
+
return entries;
|
|
4557
|
+
}
|
|
4558
|
+
async function readLatestSnapshot(root, journal) {
|
|
4559
|
+
const latest = journal.entries.at(-1);
|
|
4560
|
+
if (!latest)
|
|
4561
|
+
throw new Error(`${journalPath}:当前项目没有 migration`);
|
|
4562
|
+
return await readSnapshot(root, latest.tag);
|
|
4563
|
+
}
|
|
4564
|
+
async function readSnapshot(root, tag) {
|
|
4565
|
+
const match = migrationPattern.exec(tag);
|
|
4566
|
+
if (!match)
|
|
4567
|
+
throw new Error(`${journalPath}:migration tag 格式无效:${tag}`);
|
|
4568
|
+
return await readJson(join14(root, `${drizzleRoot}/meta/${match[1]}_snapshot.json`));
|
|
4569
|
+
}
|
|
4570
|
+
function validateUniqueNames(journal, label, conflicts) {
|
|
4571
|
+
const seen = new Set;
|
|
4572
|
+
for (const { tag } of journal.entries) {
|
|
4573
|
+
const name = stableName(tag);
|
|
4574
|
+
if (seen.has(name)) {
|
|
4575
|
+
conflicts.push(`${journalPath}:${label}存在重复 migration 名称 ${name}`);
|
|
4576
|
+
}
|
|
4577
|
+
seen.add(name);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
function stableName(tag) {
|
|
4581
|
+
return migrationPattern.exec(tag)?.[2] ?? tag;
|
|
4582
|
+
}
|
|
4583
|
+
function highestPrefix(journal) {
|
|
4584
|
+
return Math.max(-1, ...journal.entries.map(({ tag }) => Number(migrationPattern.exec(tag)?.[1] ?? -1)));
|
|
4585
|
+
}
|
|
4586
|
+
function highestIndex(journal) {
|
|
4587
|
+
return Math.max(-1, ...journal.entries.map(({ idx }) => idx));
|
|
4588
|
+
}
|
|
4589
|
+
function isDrizzleHistoryPath(path2) {
|
|
4590
|
+
return path2 === journalPath || isMigrationArtifact(path2);
|
|
4591
|
+
}
|
|
4592
|
+
function isMigrationArtifact(path2) {
|
|
4593
|
+
return isSqlMigration(path2) || new RegExp(`^${drizzleRoot}/meta/\\d{4}_snapshot\\.json$`).test(path2);
|
|
4594
|
+
}
|
|
4595
|
+
function isSqlMigration(path2) {
|
|
4596
|
+
return new RegExp(`^${drizzleRoot}/\\d{4}_.+\\.sql$`).test(path2);
|
|
4597
|
+
}
|
|
4598
|
+
function managedEntry(path2, content) {
|
|
4599
|
+
return { path: path2, ownership: "windy-managed", hash: hashContent(content) };
|
|
4600
|
+
}
|
|
4601
|
+
function deduplicate(files) {
|
|
4602
|
+
return [...new Map(files.map((entry) => [entry.path, entry])).values()];
|
|
4603
|
+
}
|
|
4604
|
+
function encodeJson(value) {
|
|
4605
|
+
return new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
|
|
4606
|
+
`);
|
|
4607
|
+
}
|
|
4608
|
+
async function readJson(path2) {
|
|
4609
|
+
return JSON.parse(await readFile13(path2, "utf8"));
|
|
4610
|
+
}
|
|
4611
|
+
function isMissing4(error) {
|
|
4612
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4613
|
+
}
|
|
4223
4614
|
|
|
4224
4615
|
// src/update/package-merge.ts
|
|
4225
4616
|
function mergePackageJson(base, ours, theirs) {
|
|
@@ -4284,10 +4675,18 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4284
4675
|
throw new Error(`当前 CLI 只能更新到 ${targetProvenance.generatorVersion},请运行 create-windy@${targetVersion}`);
|
|
4285
4676
|
}
|
|
4286
4677
|
const recipes2 = resolveRecipeChain(provenance.windyVersion, targetVersion);
|
|
4287
|
-
const [sourceManifest,
|
|
4678
|
+
const [sourceManifest, generatedTargetManifest] = await Promise.all([
|
|
4288
4679
|
readManagedFilesManifest(request.projectDirectory),
|
|
4289
4680
|
readManagedFilesManifest(artifactSet.targetDirectory)
|
|
4290
4681
|
]);
|
|
4682
|
+
const migrations = await reconcileDrizzleMigrations({
|
|
4683
|
+
projectDirectory: request.projectDirectory,
|
|
4684
|
+
baseDirectory: artifactSet.baseDirectory,
|
|
4685
|
+
targetDirectory: artifactSet.targetDirectory,
|
|
4686
|
+
sourceManifest,
|
|
4687
|
+
targetManifest: generatedTargetManifest
|
|
4688
|
+
});
|
|
4689
|
+
const targetManifest = migrations.targetManifest;
|
|
4291
4690
|
targetProvenance.appliedRecipes = [
|
|
4292
4691
|
...provenance.appliedRecipes,
|
|
4293
4692
|
...recipes2.map(({ id }) => id)
|
|
@@ -4300,7 +4699,9 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4300
4699
|
for (const path2 of [...paths].sort()) {
|
|
4301
4700
|
if (metadataPaths.has(path2))
|
|
4302
4701
|
continue;
|
|
4303
|
-
const
|
|
4702
|
+
const forcedContent = migrations.forceUpdates.has(path2) ? migrations.contentOverrides.get(path2) : undefined;
|
|
4703
|
+
const forcePreserve = migrations.forcePreserves.has(path2);
|
|
4704
|
+
const file = forcePreserve ? ready(path2, sourceEntries.get(path2)?.ownership ?? classifyOwnership(path2), "preserve", "保留项目已发布 migration 历史") : forcedContent ? ready(path2, targetEntries.get(path2)?.ownership ?? sourceEntries.get(path2)?.ownership ?? classifyOwnership(path2), "update", "合并平台与下游 migration 历史", forcedContent) : await planFile(path2, request.projectDirectory, artifactSet.baseDirectory, artifactSet.targetDirectory, sourceEntries.get(path2), targetEntries.get(path2), provenance, migrations.contentOverrides.get(path2));
|
|
4304
4705
|
if (oxcManagedPaths.has(path2) && file.reason === "客户已修改") {
|
|
4305
4706
|
file.action = "preserve";
|
|
4306
4707
|
delete file.conflict;
|
|
@@ -4308,7 +4709,10 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4308
4709
|
}
|
|
4309
4710
|
files.push(file);
|
|
4310
4711
|
}
|
|
4311
|
-
const conflicts =
|
|
4712
|
+
const conflicts = [
|
|
4713
|
+
...migrations.conflicts,
|
|
4714
|
+
...files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : [])
|
|
4715
|
+
];
|
|
4312
4716
|
return {
|
|
4313
4717
|
id: createUpdateId(provenance.windyVersion, targetVersion),
|
|
4314
4718
|
projectDirectory: request.projectDirectory,
|
|
@@ -4323,11 +4727,11 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4323
4727
|
temporaryRoot: artifactSet.temporaryRoot
|
|
4324
4728
|
};
|
|
4325
4729
|
}
|
|
4326
|
-
async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance) {
|
|
4730
|
+
async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance, targetContentOverride) {
|
|
4327
4731
|
const [ours, base, theirs] = await Promise.all([
|
|
4328
|
-
readOptional(
|
|
4329
|
-
readOptional(
|
|
4330
|
-
readOptional(
|
|
4732
|
+
readOptional(join15(project, path2)),
|
|
4733
|
+
readOptional(join15(baseRoot, path2)),
|
|
4734
|
+
targetContentOverride ? Promise.resolve(targetContentOverride) : readOptional(join15(targetRoot, path2))
|
|
4331
4735
|
]);
|
|
4332
4736
|
const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
|
|
4333
4737
|
const repaired = ours && theirs && provenance ? repairKnownDerivedFile({
|
|
@@ -4372,16 +4776,17 @@ async function planFile(path2, project, baseRoot, targetRoot, source, target, pr
|
|
|
4372
4776
|
return conflict(path2, ownership, "文件状态不完整,不能安全推断");
|
|
4373
4777
|
}
|
|
4374
4778
|
const oursChanged = hashContent(ours) !== source.hash;
|
|
4375
|
-
const targetChanged =
|
|
4779
|
+
const targetChanged = targetChangedSinceBase(source.hash, base, theirs);
|
|
4780
|
+
const reconstructedBaseDrifted = !!base && hashContent(base) !== source.hash;
|
|
4376
4781
|
if (!oursChanged && !targetChanged)
|
|
4377
4782
|
return ready(path2, ownership, "preserve", "双方未修改");
|
|
4378
|
-
if (!oursChanged)
|
|
4783
|
+
if (!oursChanged && !reconstructedBaseDrifted)
|
|
4379
4784
|
return ready(path2, ownership, "update", "仅 Windy 修改", theirs);
|
|
4380
4785
|
if (!targetChanged)
|
|
4381
4786
|
return ready(path2, ownership, "preserve", "客户已修改");
|
|
4382
4787
|
if (hashContent(ours) === target.hash)
|
|
4383
4788
|
return ready(path2, ownership, "preserve", "已包含目标内容");
|
|
4384
|
-
if (path2
|
|
4789
|
+
if (path2.endsWith("package.json") && base) {
|
|
4385
4790
|
const merged = mergePackageJson(base, ours, theirs);
|
|
4386
4791
|
return merged.content ? ready(path2, ownership, "update", "package.json 字段级合并", merged.content) : conflict(path2, ownership, `字段冲突:${merged.conflicts.join("、")}`);
|
|
4387
4792
|
}
|
|
@@ -4391,6 +4796,9 @@ async function planFile(path2, project, baseRoot, targetRoot, source, target, pr
|
|
|
4391
4796
|
}
|
|
4392
4797
|
return conflict(path2, ownership, "客户和 Windy 均已修改");
|
|
4393
4798
|
}
|
|
4799
|
+
function targetChangedSinceBase(sourceHash, base, target) {
|
|
4800
|
+
return base && target ? hashContent(base) !== hashContent(target) : !!target && hashContent(target) !== sourceHash;
|
|
4801
|
+
}
|
|
4394
4802
|
function ready(path2, ownership, action, reason, content) {
|
|
4395
4803
|
return { path: path2, ownership, action, reason, content };
|
|
4396
4804
|
}
|
|
@@ -4405,7 +4813,7 @@ function conflict(path2, ownership, reason) {
|
|
|
4405
4813
|
}
|
|
4406
4814
|
async function readOptional(path2) {
|
|
4407
4815
|
try {
|
|
4408
|
-
return await
|
|
4816
|
+
return await readFile14(path2);
|
|
4409
4817
|
} catch (error) {
|
|
4410
4818
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
4411
4819
|
return;
|
|
@@ -4413,15 +4821,15 @@ async function readOptional(path2) {
|
|
|
4413
4821
|
}
|
|
4414
4822
|
}
|
|
4415
4823
|
async function assertGitClean(projectDirectory) {
|
|
4416
|
-
const
|
|
4417
|
-
if (
|
|
4824
|
+
const result2 = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
|
|
4825
|
+
if (result2.code !== 0)
|
|
4418
4826
|
throw new Error("更新要求当前目录是有效的 Git 仓库");
|
|
4419
|
-
if (
|
|
4827
|
+
if (result2.stdout.trim())
|
|
4420
4828
|
throw new Error("更新前请先提交或暂存之外妥善处理 Git 工作区改动");
|
|
4421
4829
|
}
|
|
4422
4830
|
async function assertNoPendingUpdate(projectDirectory) {
|
|
4423
4831
|
try {
|
|
4424
|
-
await access3(
|
|
4832
|
+
await access3(join15(projectDirectory, ".windy/update-state.json"));
|
|
4425
4833
|
throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
|
|
4426
4834
|
} catch (error) {
|
|
4427
4835
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
@@ -4452,75 +4860,75 @@ class DefaultStarterUpdater {
|
|
|
4452
4860
|
throw error;
|
|
4453
4861
|
}
|
|
4454
4862
|
}
|
|
4455
|
-
async verify(
|
|
4863
|
+
async verify(result2) {
|
|
4456
4864
|
const steps = [];
|
|
4457
4865
|
try {
|
|
4458
|
-
await this.#execute("bun", ["install", "--force"],
|
|
4866
|
+
await this.#execute("bun", ["install", "--force"], result2.plan.projectDirectory);
|
|
4459
4867
|
steps.push({ name: "bun install --force", status: "passed" });
|
|
4460
|
-
const packageJson = JSON.parse(await
|
|
4868
|
+
const packageJson = JSON.parse(await readFile15(join16(result2.plan.projectDirectory, "package.json"), "utf8"));
|
|
4461
4869
|
for (const name of ["lint", "typecheck", "test", "build"]) {
|
|
4462
4870
|
if (!packageJson.scripts?.[name]) {
|
|
4463
4871
|
steps.push({ name: `bun run ${name}`, status: "skipped" });
|
|
4464
4872
|
continue;
|
|
4465
4873
|
}
|
|
4466
|
-
await this.#execute("bun", ["run", name],
|
|
4874
|
+
await this.#execute("bun", ["run", name], result2.plan.projectDirectory);
|
|
4467
4875
|
steps.push({ name: `bun run ${name}`, status: "passed" });
|
|
4468
4876
|
}
|
|
4469
|
-
await finalize(
|
|
4470
|
-
const reportPath = `.windy/reports/${
|
|
4471
|
-
await writeReport(
|
|
4472
|
-
await clearUpdateState(
|
|
4473
|
-
await rm7(
|
|
4474
|
-
return { updateId:
|
|
4877
|
+
await finalize(result2);
|
|
4878
|
+
const reportPath = `.windy/reports/${result2.plan.id}.md`;
|
|
4879
|
+
await writeReport(result2, steps, reportPath);
|
|
4880
|
+
await clearUpdateState(result2.plan.projectDirectory);
|
|
4881
|
+
await rm7(result2.plan.temporaryRoot, { recursive: true, force: true });
|
|
4882
|
+
return { updateId: result2.plan.id, status: "passed", steps, reportPath };
|
|
4475
4883
|
} catch (error) {
|
|
4476
|
-
await restoreUpdateState(
|
|
4884
|
+
await restoreUpdateState(result2.plan.projectDirectory, {
|
|
4477
4885
|
schemaVersion: 1,
|
|
4478
|
-
updateId:
|
|
4479
|
-
backupDirectory:
|
|
4480
|
-
entries:
|
|
4886
|
+
updateId: result2.plan.id,
|
|
4887
|
+
backupDirectory: result2.backupDirectory,
|
|
4888
|
+
entries: result2.backupEntries
|
|
4481
4889
|
});
|
|
4482
|
-
await rm7(
|
|
4483
|
-
throw new Error(`更新验证失败,项目已回滚到 ${
|
|
4890
|
+
await rm7(result2.plan.temporaryRoot, { recursive: true, force: true });
|
|
4891
|
+
throw new Error(`更新验证失败,项目已回滚到 ${result2.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4484
4892
|
}
|
|
4485
4893
|
}
|
|
4486
4894
|
}
|
|
4487
|
-
async function finalize(
|
|
4488
|
-
const root =
|
|
4489
|
-
const generationContent = `${JSON.stringify(
|
|
4895
|
+
async function finalize(result2) {
|
|
4896
|
+
const root = result2.plan.projectDirectory;
|
|
4897
|
+
const generationContent = `${JSON.stringify(result2.plan.targetProvenance, null, 2)}
|
|
4490
4898
|
`;
|
|
4491
|
-
await writeFile11(
|
|
4899
|
+
await writeFile11(join16(root, ".windy/generation.json"), generationContent);
|
|
4492
4900
|
const refreshedPaths = new Set([
|
|
4493
4901
|
".windy/generation.json",
|
|
4494
|
-
...
|
|
4902
|
+
...result2.plan.files.filter(({ action }) => action === "create" || action === "update").map(({ path: path2 }) => path2)
|
|
4495
4903
|
]);
|
|
4496
|
-
const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await
|
|
4904
|
+
const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile15(join16(root, path2)))])));
|
|
4497
4905
|
const targetManifest = {
|
|
4498
|
-
...
|
|
4499
|
-
files:
|
|
4906
|
+
...result2.plan.targetManifest,
|
|
4907
|
+
files: result2.plan.targetManifest.files.map((entry) => ({
|
|
4500
4908
|
...entry,
|
|
4501
4909
|
hash: refreshedHashes.get(entry.path) ?? entry.hash
|
|
4502
4910
|
}))
|
|
4503
4911
|
};
|
|
4504
|
-
await writeFile11(
|
|
4912
|
+
await writeFile11(join16(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
|
|
4505
4913
|
`);
|
|
4506
4914
|
}
|
|
4507
|
-
async function writeReport(
|
|
4508
|
-
const path2 =
|
|
4915
|
+
async function writeReport(result2, steps, relativePath2) {
|
|
4916
|
+
const path2 = join16(result2.plan.projectDirectory, relativePath2);
|
|
4509
4917
|
await mkdir7(dirname4(path2), { recursive: true });
|
|
4510
4918
|
const lines = [
|
|
4511
|
-
`# Windy 更新报告 ${
|
|
4919
|
+
`# Windy 更新报告 ${result2.plan.id}`,
|
|
4512
4920
|
"",
|
|
4513
|
-
`- 来源版本:${
|
|
4514
|
-
`- 目标版本:${
|
|
4515
|
-
`- Recipe:${
|
|
4516
|
-
`- 修改文件:${
|
|
4517
|
-
`- Oxc 代管:${
|
|
4921
|
+
`- 来源版本:${result2.plan.sourceVersion}`,
|
|
4922
|
+
`- 目标版本:${result2.plan.targetVersion}`,
|
|
4923
|
+
`- Recipe:${result2.plan.recipes.join("、") || "无"}`,
|
|
4924
|
+
`- 修改文件:${result2.changedFiles.length}`,
|
|
4925
|
+
`- Oxc 代管:${result2.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
|
|
4518
4926
|
"",
|
|
4519
4927
|
"## 验证",
|
|
4520
4928
|
"",
|
|
4521
4929
|
...steps.map(({ name, status }) => `- ${name}:${status}`),
|
|
4522
4930
|
"",
|
|
4523
|
-
`备份保留在 \`.windy/backups/${
|
|
4931
|
+
`备份保留在 \`.windy/backups/${result2.plan.id}\`,确认稳定后可人工删除。`,
|
|
4524
4932
|
""
|
|
4525
4933
|
];
|
|
4526
4934
|
await writeFile11(path2, lines.join(`
|
|
@@ -4584,8 +4992,8 @@ async function runUpdate(args, projectDirectory) {
|
|
|
4584
4992
|
await rm8(plan.temporaryRoot, { recursive: true, force: true });
|
|
4585
4993
|
throw new Error("存在更新冲突;请按上方清单处理后重新运行,项目未被修改");
|
|
4586
4994
|
}
|
|
4587
|
-
const
|
|
4588
|
-
const report = await updater.verify(
|
|
4995
|
+
const result2 = await updater.apply(plan);
|
|
4996
|
+
const report = await updater.verify(result2);
|
|
4589
4997
|
console.log(`
|
|
4590
4998
|
更新完成:${plan.sourceVersion} → ${plan.targetVersion}`);
|
|
4591
4999
|
console.log(`报告:${report.reportPath}`);
|
package/package.json
CHANGED