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 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 readFile14, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
4092
- import { dirname as dirname4, join as join15 } from "node:path";
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 readFile13 } from "node:fs/promises";
4155
- import { join as join14 } from "node:path";
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
- return { conflict: "三方合并存在重叠修改" };
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, targetManifest] = await Promise.all([
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 file = await planFile(path2, request.projectDirectory, artifactSet.baseDirectory, artifactSet.targetDirectory, sourceEntries.get(path2), targetEntries.get(path2), provenance);
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 = files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : []);
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(join14(project, path2)),
4329
- readOptional(join14(baseRoot, path2)),
4330
- readOptional(join14(targetRoot, path2))
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 = target.hash !== source.hash;
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 === "package.json" && base) {
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 readFile13(path2);
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 result = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4417
- if (result.code !== 0)
4824
+ const result2 = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4825
+ if (result2.code !== 0)
4418
4826
  throw new Error("更新要求当前目录是有效的 Git 仓库");
4419
- if (result.stdout.trim())
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(join14(projectDirectory, ".windy/update-state.json"));
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(result) {
4863
+ async verify(result2) {
4456
4864
  const steps = [];
4457
4865
  try {
4458
- await this.#execute("bun", ["install", "--force"], result.plan.projectDirectory);
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 readFile14(join15(result.plan.projectDirectory, "package.json"), "utf8"));
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], result.plan.projectDirectory);
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(result);
4470
- const reportPath = `.windy/reports/${result.plan.id}.md`;
4471
- await writeReport(result, steps, reportPath);
4472
- await clearUpdateState(result.plan.projectDirectory);
4473
- await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4474
- return { updateId: result.plan.id, status: "passed", steps, reportPath };
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(result.plan.projectDirectory, {
4884
+ await restoreUpdateState(result2.plan.projectDirectory, {
4477
4885
  schemaVersion: 1,
4478
- updateId: result.plan.id,
4479
- backupDirectory: result.backupDirectory,
4480
- entries: result.backupEntries
4886
+ updateId: result2.plan.id,
4887
+ backupDirectory: result2.backupDirectory,
4888
+ entries: result2.backupEntries
4481
4889
  });
4482
- await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4483
- throw new Error(`更新验证失败,项目已回滚到 ${result.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: 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(result) {
4488
- const root = result.plan.projectDirectory;
4489
- const generationContent = `${JSON.stringify(result.plan.targetProvenance, null, 2)}
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(join15(root, ".windy/generation.json"), generationContent);
4899
+ await writeFile11(join16(root, ".windy/generation.json"), generationContent);
4492
4900
  const refreshedPaths = new Set([
4493
4901
  ".windy/generation.json",
4494
- ...result.plan.files.filter(({ action }) => action === "create" || action === "update").map(({ path: path2 }) => path2)
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 readFile14(join15(root, path2)))])));
4904
+ const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile15(join16(root, path2)))])));
4497
4905
  const targetManifest = {
4498
- ...result.plan.targetManifest,
4499
- files: result.plan.targetManifest.files.map((entry) => ({
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(join15(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
4912
+ await writeFile11(join16(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
4505
4913
  `);
4506
4914
  }
4507
- async function writeReport(result, steps, relativePath2) {
4508
- const path2 = join15(result.plan.projectDirectory, relativePath2);
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 更新报告 ${result.plan.id}`,
4919
+ `# Windy 更新报告 ${result2.plan.id}`,
4512
4920
  "",
4513
- `- 来源版本:${result.plan.sourceVersion}`,
4514
- `- 目标版本:${result.plan.targetVersion}`,
4515
- `- Recipe:${result.plan.recipes.join("、") || "无"}`,
4516
- `- 修改文件:${result.changedFiles.length}`,
4517
- `- Oxc 代管:${result.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
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/${result.plan.id}\`,确认稳定后可人工删除。`,
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 result = await updater.apply(plan);
4588
- const report = await updater.verify(result);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "1d4ba94edf509d57d0e3bd9ca36902b4e3778526",
4
- "generatorVersion": "0.2.18",
3
+ "templateCommit": "7b28912bcf1a776aa3328e0ea36c014cd494461f",
4
+ "generatorVersion": "0.2.19",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",