teamix-evo 0.8.0 → 0.10.1

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.
@@ -113,7 +113,8 @@ var TEAMIX_DIR = ".teamix-evo";
113
113
  var CONFIG_FILE = "config.json";
114
114
  var MANIFEST_FILE = "manifest.json";
115
115
  var TOKENS_LOCK_FILE = "tokens-lock.json";
116
- var SKILLS_DIR = "skills";
116
+ var SKILLS_DIR = "skills-source";
117
+ var LEGACY_SKILLS_DIR = "skills";
117
118
  var SKILLS_LOCK_FILE = "manifest.lock.json";
118
119
  function getTeamixDir(projectRoot) {
119
120
  return path2.join(projectRoot, TEAMIX_DIR);
@@ -197,6 +198,9 @@ function getSkillsSourceDir(projectRoot, skillName) {
197
198
  const base = path2.join(projectRoot, TEAMIX_DIR, SKILLS_DIR);
198
199
  return skillName ? path2.join(base, skillName) : base;
199
200
  }
201
+ function getLegacySkillsSourceDir(projectRoot) {
202
+ return path2.join(projectRoot, TEAMIX_DIR, LEGACY_SKILLS_DIR);
203
+ }
200
204
  async function readSkillsLock(projectRoot) {
201
205
  const lockPath = path2.join(
202
206
  projectRoot,
@@ -365,13 +369,20 @@ function resolveSourcePath(source, variantDir, packageRoot) {
365
369
  }
366
370
  return path6.join(variantDir, source);
367
371
  }
368
- async function walkDir(dir) {
372
+ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
373
+ "node_modules",
374
+ "dist",
375
+ "build",
376
+ ".teamix-evo"
377
+ ]);
378
+ async function walkDir(dir, skipDirs) {
369
379
  const files = [];
370
380
  const entries = await fs4.readdir(dir, { withFileTypes: true });
371
381
  for (const entry of entries) {
372
382
  const fullPath = path6.join(dir, entry.name);
373
383
  if (entry.isDirectory()) {
374
- files.push(...await walkDir(fullPath));
384
+ if (skipDirs && skipDirs.has(entry.name)) continue;
385
+ files.push(...await walkDir(fullPath, skipDirs));
375
386
  } else if (entry.isFile()) {
376
387
  files.push(fullPath);
377
388
  }
@@ -385,6 +396,7 @@ function resolveTokensPackageRoot(packageName) {
385
396
 
386
397
  // src/core/skills-installer.ts
387
398
  async function installSkills(options) {
399
+ await migrateLegacySkillsSourceDir(options.projectRoot);
388
400
  const { manifest, ides, scope, onlyIds } = options;
389
401
  const installed = [];
390
402
  const targets = manifest.skills.filter(
@@ -677,6 +689,7 @@ function escapeRegExp(str) {
677
689
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
678
690
  }
679
691
  async function syncSkillsToIdes(options) {
692
+ await migrateLegacySkillsSourceDir(options.projectRoot);
680
693
  const { projectRoot, skills, ides, scope, onlyIds } = options;
681
694
  const out = [];
682
695
  const targets = skills.filter((s) => !onlyIds || onlyIds.includes(s.id));
@@ -720,6 +733,118 @@ async function syncSkillsToIdes(options) {
720
733
  }
721
734
  return { resources: out, count: out.length };
722
735
  }
736
+ async function migrateLegacySkillsSourceDir(projectRoot) {
737
+ const legacyDir = getLegacySkillsSourceDir(projectRoot);
738
+ const newDir = getSkillsSourceDir(projectRoot);
739
+ let legacyExists = false;
740
+ let newExists = false;
741
+ try {
742
+ legacyExists = (await fs5.stat(legacyDir)).isDirectory();
743
+ } catch {
744
+ legacyExists = false;
745
+ }
746
+ try {
747
+ newExists = (await fs5.stat(newDir)).isDirectory();
748
+ } catch {
749
+ newExists = false;
750
+ }
751
+ if (!legacyExists) return;
752
+ if (newExists) {
753
+ logger.warn(
754
+ `Detected stale legacy skills source dir at ${legacyDir} alongside ${newDir}; the new layout takes precedence \u2014 you can safely delete the legacy dir.`
755
+ );
756
+ return;
757
+ }
758
+ try {
759
+ await fs5.rename(legacyDir, newDir);
760
+ logger.info(
761
+ `Migrated skills source dir: \`.teamix-evo/${LEGACY_SKILLS_DIR}/\` \u2192 \`.teamix-evo/skills-source/\``
762
+ );
763
+ } catch (err) {
764
+ logger.warn(
765
+ `Failed to rename legacy skills source dir (${getErrorMessage(
766
+ err
767
+ )}); leaving as-is. New skills will install under the new layout.`
768
+ );
769
+ return;
770
+ }
771
+ try {
772
+ const manifest = await readInstalledManifest(projectRoot);
773
+ if (!manifest) return;
774
+ const legacyFragmentPosix = `/.teamix-evo/${LEGACY_SKILLS_DIR}/`;
775
+ const newFragmentPosix = `/.teamix-evo/skills-source/`;
776
+ const legacyFragmentNative = `${path7.sep}.teamix-evo${path7.sep}${LEGACY_SKILLS_DIR}${path7.sep}`;
777
+ const newFragmentNative = `${path7.sep}.teamix-evo${path7.sep}skills-source${path7.sep}`;
778
+ let touched = 0;
779
+ for (const pkg of manifest.installed) {
780
+ for (const r of pkg.resources) {
781
+ if (typeof r.target !== "string") continue;
782
+ const before = r.target;
783
+ let after = before.replace(legacyFragmentPosix, newFragmentPosix);
784
+ after = after.replace(legacyFragmentNative, newFragmentNative);
785
+ if (after !== before) {
786
+ r.target = after;
787
+ touched += 1;
788
+ }
789
+ }
790
+ }
791
+ if (touched > 0) {
792
+ await writeInstalledManifest(projectRoot, manifest);
793
+ logger.debug(
794
+ `Rewrote ${touched} manifest target(s) to the new skills-source path.`
795
+ );
796
+ }
797
+ } catch (err) {
798
+ logger.warn(
799
+ `Migrated skills source dir but failed to update manifest paths (${getErrorMessage(
800
+ err
801
+ )}); manifest may still reference legacy paths.`
802
+ );
803
+ }
804
+ }
805
+ async function pruneEmptyIdeSkillDirs(args) {
806
+ const removed = [];
807
+ for (const ide of args.ides) {
808
+ const adapter = getAdapter(ide);
809
+ const placeholderDir = adapter.getSkillTargetDir(
810
+ "__placeholder__",
811
+ args.scope,
812
+ args.projectRoot
813
+ );
814
+ const skillsRoot = path7.dirname(placeholderDir);
815
+ let entries;
816
+ try {
817
+ entries = await fs5.readdir(skillsRoot);
818
+ } catch {
819
+ continue;
820
+ }
821
+ for (const name of entries) {
822
+ const dir = path7.join(skillsRoot, name);
823
+ let stat5;
824
+ try {
825
+ stat5 = await fs5.stat(dir);
826
+ } catch {
827
+ continue;
828
+ }
829
+ if (!stat5.isDirectory()) continue;
830
+ let children;
831
+ try {
832
+ children = await fs5.readdir(dir);
833
+ } catch {
834
+ continue;
835
+ }
836
+ if (children.some((c) => c === "SKILL.md")) continue;
837
+ if (children.length !== 0) continue;
838
+ try {
839
+ await fs5.rmdir(dir);
840
+ removed.push(dir);
841
+ logger.debug(`Pruned empty IDE skill dir: ${dir}`);
842
+ } catch {
843
+ }
844
+ }
845
+ }
846
+ return removed;
847
+ }
723
848
  async function removeSkillFiles(records) {
724
849
  const removed = [];
725
850
  for (const r of records) {
@@ -815,13 +940,29 @@ async function runSkillsInit(options) {
815
940
  }
816
941
  return true;
817
942
  }).map((s) => s.id);
818
- const skippedSkillIds = candidateIds.filter(
819
- (id) => existing.skillIds.has(id)
943
+ const { onlyIds, skippedSkillIds, outdatedSkills } = partitionByVersion(
944
+ candidateIds,
945
+ manifest,
946
+ existing
820
947
  );
821
- const onlyIds = candidateIds.filter((id) => !existing.skillIds.has(id));
822
- if (existingSkillsCfg && onlyIds.length === 0) {
948
+ if (existingSkillsCfg && onlyIds.length === 0 && outdatedSkills.length === 0) {
823
949
  return { status: "already-initialized" };
824
950
  }
951
+ if (onlyIds.length === 0) {
952
+ return {
953
+ status: "installed",
954
+ packageName,
955
+ version: existingSkillsCfg?.version ?? manifest.version,
956
+ ides,
957
+ scope,
958
+ skillCount: 0,
959
+ fileCount: 0,
960
+ resources: [],
961
+ addedSkillIds: [],
962
+ skippedSkillIds,
963
+ outdatedSkills
964
+ };
965
+ }
825
966
  return finalizeSkillsInstall({
826
967
  projectRoot,
827
968
  packageName,
@@ -833,6 +974,7 @@ async function runSkillsInit(options) {
833
974
  scope,
834
975
  onlyIds,
835
976
  skippedSkillIds,
977
+ outdatedSkills,
836
978
  existing,
837
979
  existingConfig
838
980
  });
@@ -873,10 +1015,11 @@ async function runSkillsAdd(options) {
873
1015
  }
874
1016
  }
875
1017
  const existing = await readExistingState(projectRoot, packageName);
876
- const skippedSkillIds = requestedNames.filter(
877
- (n) => existing.skillIds.has(n)
1018
+ const { onlyIds, skippedSkillIds, outdatedSkills } = partitionByVersion(
1019
+ requestedNames,
1020
+ manifest,
1021
+ existing
878
1022
  );
879
- const onlyIds = requestedNames.filter((n) => !existing.skillIds.has(n));
880
1023
  if (onlyIds.length === 0) {
881
1024
  return {
882
1025
  status: "installed",
@@ -888,7 +1031,8 @@ async function runSkillsAdd(options) {
888
1031
  fileCount: 0,
889
1032
  resources: [],
890
1033
  addedSkillIds: [],
891
- skippedSkillIds
1034
+ skippedSkillIds,
1035
+ outdatedSkills
892
1036
  };
893
1037
  }
894
1038
  return finalizeSkillsInstall({
@@ -902,10 +1046,52 @@ async function runSkillsAdd(options) {
902
1046
  scope,
903
1047
  onlyIds,
904
1048
  skippedSkillIds,
1049
+ outdatedSkills,
905
1050
  existing,
906
1051
  existingConfig
907
1052
  });
908
1053
  }
1054
+ function partitionByVersion(ids, manifest, existing) {
1055
+ const manifestById = new Map(manifest.skills.map((s) => [s.id, s]));
1056
+ const onlyIds = [];
1057
+ const skippedSkillIds = [];
1058
+ const outdatedSkills = [];
1059
+ for (const name of ids) {
1060
+ if (!existing.skillIds.has(name)) {
1061
+ onlyIds.push(name);
1062
+ continue;
1063
+ }
1064
+ const installedVer = existing.lock?.skills?.[name]?.version;
1065
+ const latestVer = manifestById.get(name)?.version;
1066
+ if (installedVer && latestVer && compareSemver(installedVer, latestVer) < 0) {
1067
+ outdatedSkills.push({
1068
+ id: name,
1069
+ installed: installedVer,
1070
+ latest: latestVer
1071
+ });
1072
+ } else {
1073
+ skippedSkillIds.push(name);
1074
+ }
1075
+ }
1076
+ return { onlyIds, skippedSkillIds, outdatedSkills };
1077
+ }
1078
+ function parseSemverTriple(v) {
1079
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
1080
+ if (!m) return null;
1081
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
1082
+ }
1083
+ function compareSemver(a, b) {
1084
+ const pa = parseSemverTriple(a);
1085
+ const pb = parseSemverTriple(b);
1086
+ if (!pa || !pb) {
1087
+ if (a === b) return 0;
1088
+ return a < b ? -1 : 1;
1089
+ }
1090
+ for (let i = 0; i < 3; i++) {
1091
+ if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
1092
+ }
1093
+ return 0;
1094
+ }
909
1095
  async function readExistingState(projectRoot, packageName) {
910
1096
  const installed = await readInstalledManifest(projectRoot);
911
1097
  const pkg = installed?.installed.find((p) => p.package === packageName);
@@ -930,6 +1116,7 @@ async function finalizeSkillsInstall(args) {
930
1116
  scope,
931
1117
  onlyIds,
932
1118
  skippedSkillIds,
1119
+ outdatedSkills,
933
1120
  existing,
934
1121
  existingConfig
935
1122
  } = args;
@@ -995,6 +1182,10 @@ async function finalizeSkillsInstall(args) {
995
1182
  }
996
1183
  await writeSkillsLock(projectRoot, lock);
997
1184
  await ensureMcpJson(projectRoot);
1185
+ try {
1186
+ await pruneEmptyIdeSkillDirs({ projectRoot, ides, scope });
1187
+ } catch {
1188
+ }
998
1189
  return {
999
1190
  status: "installed",
1000
1191
  packageName,
@@ -1005,7 +1196,8 @@ async function finalizeSkillsInstall(args) {
1005
1196
  fileCount: result.count,
1006
1197
  resources: result.resources,
1007
1198
  addedSkillIds: onlyIds,
1008
- skippedSkillIds
1199
+ skippedSkillIds,
1200
+ outdatedSkills: outdatedSkills ?? []
1009
1201
  };
1010
1202
  }
1011
1203
  function mergeInstalledResources(existing, next) {
@@ -1066,13 +1258,16 @@ Run \`npx teamix-evo@latest tokens list-variants\` to see all options.`
1066
1258
  if (!await fileExists(overridesAbs)) {
1067
1259
  await writeFileSafe(overridesAbs, EMPTY_OVERRIDES_TEMPLATE);
1068
1260
  }
1069
- const overridesContent = await fs6.readFile(overridesAbs, "utf-8");
1070
- installed.push({
1071
- id: `tokens:${CONSUMER_OVERRIDES_FILE}`,
1072
- target: path9.posix.join(CONSUMER_TOKENS_DIR, CONSUMER_OVERRIDES_FILE),
1073
- hash: computeHash(overridesContent),
1074
- strategy: "frozen"
1075
- });
1261
+ const overridesId = `tokens:${CONSUMER_OVERRIDES_FILE}`;
1262
+ if (!installed.some((r) => r.id === overridesId)) {
1263
+ const overridesContent = await fs6.readFile(overridesAbs, "utf-8");
1264
+ installed.push({
1265
+ id: overridesId,
1266
+ target: path9.posix.join(CONSUMER_TOKENS_DIR, CONSUMER_OVERRIDES_FILE),
1267
+ hash: computeHash(overridesContent),
1268
+ strategy: "frozen"
1269
+ });
1270
+ }
1076
1271
  const lock = {
1077
1272
  schemaVersion: 1,
1078
1273
  variant: {
@@ -1213,6 +1408,9 @@ async function installVariantFile(fileRelToPackage, packageRoot, projectRoot) {
1213
1408
  const targetRel = path9.posix.join(CONSUMER_TOKENS_DIR, CONSUMER_THEME_FILE);
1214
1409
  const targetAbs = path9.join(projectRoot, targetRel);
1215
1410
  const content = await fs6.readFile(sourceAbs, "utf-8");
1411
+ if (await fileExists(targetAbs)) {
1412
+ await backupFile(targetAbs, projectRoot);
1413
+ }
1216
1414
  await writeFileSafe(targetAbs, content);
1217
1415
  return {
1218
1416
  id: `tokens:${CONSUMER_THEME_FILE}`,
@@ -1593,6 +1791,9 @@ async function installUiEntries(options) {
1593
1791
  const sourceAbs = path11.resolve(rootForEntry, file.source);
1594
1792
  const raw = await fs8.readFile(sourceAbs, "utf-8");
1595
1793
  const transformed = rewriteImports(raw, aliases);
1794
+ if (exists) {
1795
+ await backupFile(targetAbs, projectRoot);
1796
+ }
1596
1797
  await writeFileSafe(targetAbs, transformed);
1597
1798
  written++;
1598
1799
  logger.info(` write: ${rel(projectRoot, targetAbs)}`);
@@ -1963,18 +2164,29 @@ var ESLINT_DEPS = [
1963
2164
  ];
1964
2165
  var STYLELINT_DEPS = ["@teamix-evo/stylelint-config", "stylelint"];
1965
2166
  async function runLintInit(options) {
1966
- const { projectRoot, skipInstall } = options;
2167
+ const {
2168
+ projectRoot,
2169
+ skipInstall,
2170
+ eslintStrategy = "overwrite",
2171
+ stylelintStrategy = "overwrite",
2172
+ eslintExistingPaths = [],
2173
+ stylelintExistingPaths = []
2174
+ } = options;
1967
2175
  const eslintConfigPath = path13.join(projectRoot, "eslint.config.js");
1968
2176
  const stylelintConfigPath = path13.join(projectRoot, "stylelint.config.cjs");
1969
- const eslintExists = await fileExists(eslintConfigPath);
1970
- const stylelintExists = await fileExists(stylelintConfigPath);
1971
- if (eslintExists && stylelintExists) {
2177
+ const eslintTemplateExists = await fileExists(eslintConfigPath);
2178
+ const stylelintTemplateExists = await fileExists(stylelintConfigPath);
2179
+ const eslintSkipRequested = eslintStrategy === "skip" && eslintExistingPaths.length > 0;
2180
+ const stylelintSkipRequested = stylelintStrategy === "skip" && stylelintExistingPaths.length > 0;
2181
+ const eslintNeedsWrite = !eslintTemplateExists && !eslintSkipRequested;
2182
+ const stylelintNeedsWrite = !stylelintTemplateExists && !stylelintSkipRequested;
2183
+ if (!eslintNeedsWrite && !stylelintNeedsWrite) {
1972
2184
  return { status: "already-initialized" };
1973
2185
  }
1974
2186
  if (!skipInstall) {
1975
2187
  const depsToInstall = [
1976
- ...eslintExists ? [] : ESLINT_DEPS,
1977
- ...stylelintExists ? [] : STYLELINT_DEPS
2188
+ ...eslintNeedsWrite ? ESLINT_DEPS : [],
2189
+ ...stylelintNeedsWrite ? STYLELINT_DEPS : []
1978
2190
  ];
1979
2191
  if (depsToInstall.length > 0) {
1980
2192
  const pm = detectPm(projectRoot);
@@ -1983,23 +2195,66 @@ async function runLintInit(options) {
1983
2195
  await execa(pm, args, { cwd: projectRoot, stdio: "inherit" });
1984
2196
  }
1985
2197
  }
2198
+ if (eslintNeedsWrite && eslintExistingPaths.length > 0) {
2199
+ for (const rel2 of eslintExistingPaths) {
2200
+ await backupFile(path13.join(projectRoot, rel2), projectRoot);
2201
+ }
2202
+ }
2203
+ if (stylelintNeedsWrite && stylelintExistingPaths.length > 0) {
2204
+ for (const rel2 of stylelintExistingPaths) {
2205
+ await backupFile(path13.join(projectRoot, rel2), projectRoot);
2206
+ }
2207
+ }
1986
2208
  let wroteEslint = false;
1987
2209
  let wroteStylelint = false;
1988
- if (!eslintExists) {
2210
+ if (eslintNeedsWrite) {
1989
2211
  await writeFileSafe(eslintConfigPath, ESLINT_CONFIG_CONTENT);
1990
2212
  logger.debug(`Wrote eslint.config.js \u2192 ${eslintConfigPath}`);
1991
2213
  wroteEslint = true;
1992
2214
  }
1993
- if (!stylelintExists) {
2215
+ if (stylelintNeedsWrite) {
1994
2216
  await writeFileSafe(stylelintConfigPath, STYLELINT_CONFIG_CONTENT);
1995
2217
  logger.debug(`Wrote stylelint.config.cjs \u2192 ${stylelintConfigPath}`);
1996
2218
  wroteStylelint = true;
1997
2219
  }
1998
- await patchPackageJsonScripts(projectRoot);
2220
+ const packageJsonPatched = await patchPackageJsonScripts(projectRoot);
2221
+ let stylelintIgnoreFilesWarning = false;
2222
+ if (!stylelintNeedsWrite && stylelintTemplateExists) {
2223
+ try {
2224
+ const existingContent = fs9.readFileSync(stylelintConfigPath, "utf-8");
2225
+ const usesTeamixPreset = existingContent.includes("@teamix-evo/stylelint-config/presets/") || existingContent.includes("@teamix-evo/stylelint-config/preset/");
2226
+ const hasTokenIgnore = existingContent.includes("tokens.theme.css") && existingContent.includes("tokens.overrides.css");
2227
+ if (!usesTeamixPreset && !hasTokenIgnore) {
2228
+ stylelintIgnoreFilesWarning = true;
2229
+ logger.warn(
2230
+ [
2231
+ "\u68C0\u6D4B\u5230\u73B0\u6709 stylelint \u914D\u7F6E\u672A\u6392\u9664 token \u5B9A\u4E49\u6587\u4EF6\u3002",
2232
+ "\u5EFA\u8BAE\u5728 stylelint.config.cjs \u4E2D\u6DFB\u52A0 ignoreFiles:",
2233
+ "",
2234
+ " ignoreFiles: [",
2235
+ " '**/tokens.theme.css',",
2236
+ " '**/tokens.overrides.css',",
2237
+ " ]",
2238
+ "",
2239
+ "\u6216\u5207\u6362\u5230 teamix-evo \u9884\u8BBE\u4EE5\u81EA\u52A8\u83B7\u5F97\u6392\u9664\u89C4\u5219:",
2240
+ "",
2241
+ " extends: ['@teamix-evo/stylelint-config/presets/consumer']"
2242
+ ].join("\n")
2243
+ );
2244
+ }
2245
+ } catch {
2246
+ }
2247
+ }
1999
2248
  return {
2000
2249
  status: "installed",
2001
2250
  eslint: wroteEslint,
2002
- stylelint: wroteStylelint
2251
+ stylelint: wroteStylelint,
2252
+ eslintMergeRequested: wroteEslint && eslintStrategy === "merge" && eslintExistingPaths.length > 0,
2253
+ stylelintMergeRequested: wroteStylelint && stylelintStrategy === "merge" && stylelintExistingPaths.length > 0,
2254
+ eslintSkipped: eslintSkipRequested,
2255
+ stylelintSkipped: stylelintSkipRequested,
2256
+ packageJsonPatched,
2257
+ stylelintIgnoreFilesWarning
2003
2258
  };
2004
2259
  }
2005
2260
  function detectPm(projectRoot) {
@@ -2010,12 +2265,12 @@ function detectPm(projectRoot) {
2010
2265
  async function patchPackageJsonScripts(projectRoot) {
2011
2266
  const pkgPath = path13.join(projectRoot, "package.json");
2012
2267
  const raw = await readFileOrNull(pkgPath);
2013
- if (!raw) return;
2268
+ if (!raw) return false;
2014
2269
  let pkg;
2015
2270
  try {
2016
2271
  pkg = JSON.parse(raw);
2017
2272
  } catch {
2018
- return;
2273
+ return false;
2019
2274
  }
2020
2275
  const scripts = pkg.scripts ?? {};
2021
2276
  let changed = false;
@@ -2028,17 +2283,22 @@ async function patchPackageJsonScripts(projectRoot) {
2028
2283
  changed = true;
2029
2284
  }
2030
2285
  if (changed) {
2286
+ await backupFile(pkgPath, projectRoot);
2031
2287
  pkg.scripts = scripts;
2032
2288
  await writeFileSafe(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
2033
2289
  logger.debug("Patched package.json scripts with lint / lint:css");
2034
2290
  }
2291
+ return changed;
2035
2292
  }
2036
2293
 
2037
2294
  // src/core/agents-md.ts
2038
2295
  import * as fs10 from "fs/promises";
2039
2296
  import * as path14 from "path";
2297
+ import { hasManagedRegion as hasManagedRegion2, replaceManagedRegion as replaceManagedRegion2 } from "@teamix-evo/registry";
2298
+ var AGENTS_MD_MANAGED_ID = "teamix-evo-skills";
2040
2299
  async function runGenerateAgentsMd(options) {
2041
2300
  const { projectRoot, variant, skillIds } = options;
2301
+ const mode = options.mode ?? "overwrite";
2042
2302
  const ordered = [...skillIds].sort(
2043
2303
  (a, b) => bucketRank(a) - bucketRank(b) || a.localeCompare(b)
2044
2304
  );
@@ -2049,13 +2309,47 @@ async function runGenerateAgentsMd(options) {
2049
2309
  sections.push(section);
2050
2310
  if (missing) missingSkillIds.push(id);
2051
2311
  }
2052
- const body = renderAgentsMd({ variant, sections });
2053
2312
  const target = path14.join(projectRoot, "AGENTS.md");
2054
- await fs10.writeFile(target, body, "utf8");
2313
+ const targetExists = await fileExists(target);
2314
+ const fullTemplate = renderAgentsMd({ variant, sections });
2315
+ const managedBody = renderManagedBlockBody({ variant, sections });
2316
+ let outputContent;
2317
+ let merge;
2318
+ if (!targetExists) {
2319
+ outputContent = fullTemplate;
2320
+ merge = "created";
2321
+ } else {
2322
+ await backupFile(target, projectRoot);
2323
+ if (mode === "merge-managed") {
2324
+ const existing = await readFileOrNull(target) ?? "";
2325
+ if (hasManagedRegion2(existing, AGENTS_MD_MANAGED_ID)) {
2326
+ outputContent = replaceManagedRegion2(
2327
+ existing,
2328
+ AGENTS_MD_MANAGED_ID,
2329
+ managedBody
2330
+ );
2331
+ merge = "managed-replaced";
2332
+ } else {
2333
+ const wrapped = wrapManagedBlock(managedBody);
2334
+ outputContent = `${wrapped}
2335
+
2336
+ ${PRECEDENCE_NOTICE}
2337
+
2338
+ ${existing.trimStart()}`;
2339
+ merge = "managed-prepended";
2340
+ }
2341
+ } else {
2342
+ outputContent = fullTemplate;
2343
+ merge = "overwritten";
2344
+ }
2345
+ }
2346
+ await fs10.writeFile(target, outputContent, "utf8");
2055
2347
  return {
2056
2348
  path: target,
2057
2349
  skillCount: ordered.length,
2058
- missingSkillIds
2350
+ missingSkillIds,
2351
+ backedUp: targetExists,
2352
+ merge
2059
2353
  };
2060
2354
  }
2061
2355
  function bucketRank(id) {
@@ -2065,10 +2359,7 @@ function bucketRank(id) {
2065
2359
  }
2066
2360
  async function renderSkillSection(projectRoot, skillId) {
2067
2361
  const skillPath = path14.join(
2068
- projectRoot,
2069
- ".teamix-evo",
2070
- "skills",
2071
- skillId,
2362
+ getSkillsSourceDir(projectRoot, skillId),
2072
2363
  "SKILL.md"
2073
2364
  );
2074
2365
  const lines = [];
@@ -2093,16 +2384,25 @@ async function renderSkillSection(projectRoot, skillId) {
2093
2384
  if (parts?.coordinates) {
2094
2385
  lines.push(`- **Coordinates with**: ${parts.coordinates}`);
2095
2386
  }
2096
- lines.push(`- **\u4F4D\u7F6E**: \`.teamix-evo/skills/${skillId}/SKILL.md\``);
2387
+ lines.push(`- **\u4F4D\u7F6E**: \`.teamix-evo/skills-source/${skillId}/SKILL.md\``);
2097
2388
  return { section: lines.join("\n"), missing };
2098
2389
  }
2099
2390
  function renderAgentsMd(args) {
2391
+ const { variant, sections } = args;
2392
+ const managedBody = renderManagedBlockBody({ variant, sections });
2393
+ const wrapped = wrapManagedBlock(managedBody);
2394
+ return `${wrapped}
2395
+
2396
+ ${PRECEDENCE_NOTICE}
2397
+ `;
2398
+ }
2399
+ function renderManagedBlockBody(args) {
2100
2400
  const { variant, sections } = args;
2101
2401
  const skillBlock = sections.length > 0 ? sections.join("\n\n") : "_\uFF08\u672C\u5DE5\u7A0B\u672A\u88C5\u914D\u5DE5\u7A0B\u7EA7 skill\u3002\uFF09_";
2102
2402
  return `# AGENTS.md
2103
2403
 
2104
2404
  > \u672C\u5DE5\u7A0B\u5DF2\u88C5\u914D Teamix Evo AI skills\u3002AI \u52A9\u624B\u5728\u4EE5\u4E0B\u573A\u666F\u4E0B**\u5FC5\u987B\u5148\u8BFB\u5BF9\u5E94 skill** \u518D\u52A8\u624B\u3002
2105
- > \u672C\u6587\u4EF6\u7531 \`teamix-evo init\` / \`create-teamix-evo\` \u81EA\u52A8\u751F\u6210\uFF08regenerable\uFF0C[ADR 0038](https://github.com/teamix-evo/teamix-evo/blob/main/docs/adr/0038-create-agents-md-skill-trigger-fallback.md)\uFF09\uFF0C\u5237\u65B0\u65B9\u5F0F\u89C1\u5E95\u90E8\u3002
2405
+ > \u672C\u6587\u4EF6\u7531 \`teamix-evo init\` / \`create-teamix-evo\` \u81EA\u52A8\u751F\u6210\uFF08regenerable\uFF0C\u9075\u5FAA ADR 0038\uFF09\uFF0C\u5237\u65B0\u65B9\u5F0F\u89C1\u5E95\u90E8\u3002
2106
2406
 
2107
2407
  ## \u5DF2\u88C5 Skills\uFF08variant: ${variant}\uFF09
2108
2408
 
@@ -2115,9 +2415,22 @@ ${skillBlock}
2115
2415
  - \u6A21\u7CCA\u573A\u666F\uFF1A\u5148\u6309 SKIP \u53CD\u5411\u6392\u9664\uFF0C\u5269\u4F59\u552F\u4E00 skill \u5373\u4E3A\u5165\u53E3
2116
2416
  - \u751F\u547D\u5468\u671F\u547D\u4EE4\uFF08\`init\` / \`update\` / \`add\`\uFF09\u8D70 \`teamix-evo-manage\`\uFF08\u5168\u5C40 skill\uFF0C\u672C\u6587\u4EF6\u4E0D\u5217\uFF09
2117
2417
 
2118
- > \u5237\u65B0\u672C\u6587\u4EF6\uFF1A\`npx teamix-evo skills add\` \u6216\u91CD\u8DD1 \`npm create teamix-evo\` / \`teamix-evo init\`\u3002
2119
- `;
2418
+ ## UI \u7EC4\u4EF6\u9886\u5730\uFF08init \u540E\u7EA6\u675F\uFF09
2419
+
2420
+ - \`src/components/ui/\` \u7531 teamix-evo \u63A5\u7BA1\uFF0C\u7981\u6B62\u624B\u5DE5\u6216\u901A\u8FC7 shadcn CLI \u6DFB\u52A0\u65B0\u7EC4\u4EF6
2421
+ - \u5982\u9700\u65B0\u589E UI \u7EC4\u4EF6\uFF0C\u4F7F\u7528 \`npx teamix-evo ui add <id>\`
2422
+ - \`src/components/shadcn-ui/\` \u662F init \u524D legacy \u7EC4\u4EF6\u5F52\u6863\uFF0C\u53EA\u8BFB\uFF1B\u65B0\u4EE3\u7801\u4E0D\u5E94\u518D import
2423
+ - \u5347\u7EA7 legacy \u7EC4\u4EF6\uFF1A\u89E6\u53D1 teamix-evo-upgrade skill
2424
+
2425
+ > \u5237\u65B0\u672C\u6587\u4EF6\uFF1A\`npx teamix-evo skills add\` \u6216\u91CD\u8DD1 \`npm create teamix-evo\` / \`teamix-evo init\`\u3002`;
2426
+ }
2427
+ function wrapManagedBlock(body) {
2428
+ return `<!-- teamix-evo:managed:start id="${AGENTS_MD_MANAGED_ID}" -->
2429
+ ${body}
2430
+ <!-- teamix-evo:managed:end id="${AGENTS_MD_MANAGED_ID}" -->`;
2120
2431
  }
2432
+ var PRECEDENCE_NOTICE = `<!-- teamix-evo:precedence -->
2433
+ > \u51B2\u7A81\u4EE5\u4E0A\u65B9\u7684 **Skills** \u7D22\u5F15\u4E3A\u51C6\uFF08\u4E0A\u6E38\u8DEF\u5F84\u4E0E TRIGGER/SKIP \u5951\u7EA6\uFF09\uFF1B\u9879\u76EE\u7279\u6709\u7684\u4EBA\u5DE5\u7EC6\u5219\u8BF7\u5199\u5728\u672C\u5904\u4EE5\u4E0B\u3001\u4E0D\u8981\u8986\u76D6\u4E0A\u65B9 managed \u533A\u57DF\u3002`;
2121
2434
  function extractDescriptionParts(fileContent) {
2122
2435
  const description = extractDescriptionBlock(fileContent);
2123
2436
  if (description == null) return null;
@@ -2311,6 +2624,26 @@ var INDEX_CSS_CANDIDATES = [
2311
2624
  ];
2312
2625
  var SHADCN_FILE_CANDIDATES = ["src/lib/utils.ts"];
2313
2626
  var SHADCN_DIR_CANDIDATES = ["src/components/ui"];
2627
+ var ESLINT_CONFIG_CANDIDATES = [
2628
+ ".eslintrc.cjs",
2629
+ ".eslintrc.js",
2630
+ ".eslintrc.json",
2631
+ ".eslintrc.yml",
2632
+ "eslint.config.js",
2633
+ "eslint.config.cjs",
2634
+ "eslint.config.mjs",
2635
+ "eslint.config.ts"
2636
+ ];
2637
+ var STYLELINT_CONFIG_CANDIDATES = [
2638
+ ".stylelintrc.cjs",
2639
+ ".stylelintrc.js",
2640
+ ".stylelintrc.json",
2641
+ ".stylelintrc.yml",
2642
+ "stylelint.config.cjs",
2643
+ "stylelint.config.js",
2644
+ "stylelint.config.mjs",
2645
+ "stylelint.config.ts"
2646
+ ];
2314
2647
  async function isDir(target) {
2315
2648
  try {
2316
2649
  const stat5 = await fs12.stat(target);
@@ -2487,7 +2820,9 @@ async function detectConflicts(cwd) {
2487
2820
  detectTailwindConfig(absCwd),
2488
2821
  detectTokens(absCwd),
2489
2822
  detectIndexCss(absCwd),
2490
- detectShadcnSource(absCwd)
2823
+ detectShadcnSource(absCwd),
2824
+ detectEslintConfig(absCwd),
2825
+ detectStylelintConfig(absCwd)
2491
2826
  ]);
2492
2827
  return {
2493
2828
  cwd: absCwd,
@@ -2495,6 +2830,46 @@ async function detectConflicts(cwd) {
2495
2830
  hasAnyConflict: items.some((i) => i.exists)
2496
2831
  };
2497
2832
  }
2833
+ async function detectEslintConfig(cwd) {
2834
+ const matched = [];
2835
+ const contents = [];
2836
+ for (const rel2 of ESLINT_CONFIG_CANDIDATES) {
2837
+ const c = await readFileOrNull(path16.join(cwd, rel2));
2838
+ if (c !== null) {
2839
+ matched.push(rel2);
2840
+ contents.push(c);
2841
+ }
2842
+ }
2843
+ const exists = matched.length > 0;
2844
+ return {
2845
+ key: "eslint-config",
2846
+ exists,
2847
+ paths: matched,
2848
+ fingerprint: exists ? fingerprint(contents) : void 0,
2849
+ recommendedStrategy: exists ? "merge" : "overwrite",
2850
+ availableStrategies: ["merge", "backup-overwrite", "overwrite", "skip"]
2851
+ };
2852
+ }
2853
+ async function detectStylelintConfig(cwd) {
2854
+ const matched = [];
2855
+ const contents = [];
2856
+ for (const rel2 of STYLELINT_CONFIG_CANDIDATES) {
2857
+ const c = await readFileOrNull(path16.join(cwd, rel2));
2858
+ if (c !== null) {
2859
+ matched.push(rel2);
2860
+ contents.push(c);
2861
+ }
2862
+ }
2863
+ const exists = matched.length > 0;
2864
+ return {
2865
+ key: "stylelint-config",
2866
+ exists,
2867
+ paths: matched,
2868
+ fingerprint: exists ? fingerprint(contents) : void 0,
2869
+ recommendedStrategy: exists ? "merge" : "overwrite",
2870
+ availableStrategies: ["merge", "backup-overwrite", "overwrite", "skip"]
2871
+ };
2872
+ }
2498
2873
 
2499
2874
  // src/core/legacy-tokens-migrate.ts
2500
2875
  import * as path17 from "path";
@@ -2603,92 +2978,1301 @@ async function migrateLegacyTokens(options) {
2603
2978
  };
2604
2979
  }
2605
2980
 
2606
- // src/core/snapshot.ts
2981
+ // src/core/ui-conflict-detector.ts
2607
2982
  import * as fs14 from "fs/promises";
2608
2983
  import * as path18 from "path";
2609
- var TEAMIX_DIR2 = ".teamix-evo";
2610
- var SNAPSHOTS_DIR = ".snapshots";
2611
- var LOGS_DIR = "logs";
2612
- var META_FILE = "_meta.json";
2613
- var DEFAULT_KEEP = 5;
2614
- function isoToFsSafe(iso) {
2615
- return iso.replace(/[:.]/g, "-");
2616
- }
2617
- function fsSafeToIso(safe) {
2618
- return safe.replace(
2619
- /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})(Z)$/,
2620
- "$1T$2:$3:$4.$5$6"
2621
- );
2622
- }
2623
- async function createSnapshot(projectRoot, opts = {}) {
2624
- const teamixDir = path18.join(projectRoot, TEAMIX_DIR2);
2625
- try {
2626
- const stat5 = await fs14.stat(teamixDir);
2627
- if (!stat5.isDirectory()) return null;
2628
- } catch (err) {
2629
- if (err.code === "ENOENT") return null;
2630
- throw err;
2631
- }
2632
- const isoTs = (/* @__PURE__ */ new Date()).toISOString();
2633
- const ts = isoToFsSafe(isoTs);
2634
- const snapshotRoot = path18.join(teamixDir, SNAPSHOTS_DIR);
2635
- const target = path18.join(snapshotRoot, ts);
2636
- await fs14.mkdir(target, { recursive: true });
2637
- const entries = await fs14.readdir(teamixDir, { withFileTypes: true });
2638
- for (const entry of entries) {
2639
- if (entry.name === SNAPSHOTS_DIR) continue;
2640
- if (entry.name === LOGS_DIR) continue;
2641
- const src = path18.join(teamixDir, entry.name);
2642
- const dst = path18.join(target, entry.name);
2643
- await fs14.cp(src, dst, { recursive: true });
2984
+ function looksLikeShadcnOriginal(content) {
2985
+ if (content.includes("data-slot=")) return false;
2986
+ if (content.includes("@teamix-evo")) return false;
2987
+ if (content.includes("teamix-evo:managed")) return false;
2988
+ return true;
2989
+ }
2990
+ async function detectUiConflicts(options) {
2991
+ const { projectRoot, aliases, manifest } = options;
2992
+ const conflicts = [];
2993
+ const conflictDirSet = /* @__PURE__ */ new Set();
2994
+ let totalChecked = 0;
2995
+ for (const entry of manifest.entries) {
2996
+ for (const file of entry.files) {
2997
+ totalChecked++;
2998
+ const aliasDir = aliases[file.targetAlias];
2999
+ if (!aliasDir) continue;
3000
+ const targetAbs = path18.join(projectRoot, aliasDir, file.targetName);
3001
+ const exists = await fileExists(targetAbs);
3002
+ if (!exists) continue;
3003
+ let isShadcnOriginal = true;
3004
+ try {
3005
+ const content = await fs14.readFile(targetAbs, "utf-8");
3006
+ isShadcnOriginal = looksLikeShadcnOriginal(content);
3007
+ } catch {
3008
+ }
3009
+ const relativePath = path18.relative(projectRoot, targetAbs).replace(/\\/g, "/");
3010
+ conflicts.push({
3011
+ id: entry.id,
3012
+ targetPath: targetAbs,
3013
+ relativePath,
3014
+ isShadcnOriginal
3015
+ });
3016
+ conflictDirSet.add(aliasDir);
3017
+ }
2644
3018
  }
2645
- const meta = {
2646
- ts: isoTs,
2647
- reason: opts.reason ?? "manual"
3019
+ return {
3020
+ conflictEntries: conflicts,
3021
+ unconflictedTargets: totalChecked - conflicts.length,
3022
+ totalEntries: totalChecked,
3023
+ shouldBlock: conflicts.length > 0,
3024
+ conflictDirs: [...conflictDirSet]
2648
3025
  };
2649
- await fs14.writeFile(
2650
- path18.join(target, META_FILE),
2651
- JSON.stringify(meta, null, 2) + "\n",
2652
- "utf-8"
2653
- );
2654
- logger.debug(
2655
- `Snapshot created \u2192 ${path18.relative(projectRoot, target)} (${meta.reason})`
2656
- );
2657
- const keep = opts.keep ?? DEFAULT_KEEP;
2658
- await pruneSnapshots(projectRoot, keep, { protectedTs: opts.protectedTs });
2659
- return { ts, path: target };
2660
3026
  }
2661
- async function listSnapshots(projectRoot) {
2662
- const snapshotRoot = path18.join(projectRoot, TEAMIX_DIR2, SNAPSHOTS_DIR);
2663
- let entries;
2664
- try {
2665
- entries = await fs14.readdir(snapshotRoot, { withFileTypes: true });
2666
- } catch (err) {
2667
- if (err.code === "ENOENT") return [];
2668
- throw err;
3027
+
3028
+ // src/core/ui-isolate.ts
3029
+ import * as fs15 from "fs/promises";
3030
+ import * as path19 from "path";
3031
+ var IGNORE_DIR_NAMES = DEFAULT_SKIP_DIRS;
3032
+ async function runUiIsolate(options) {
3033
+ const { projectRoot, aliases } = options;
3034
+ const componentsDir = path19.join(projectRoot, aliases.components);
3035
+ const legacyDir = path19.join(
3036
+ projectRoot,
3037
+ aliases.components.replace(/\/ui\/?$/, "/shadcn-ui")
3038
+ );
3039
+ const movedFiles = [];
3040
+ const backedUpFiles = [];
3041
+ let componentsJsonRemoved = false;
3042
+ if (await fileExists(componentsDir)) {
3043
+ await ensureDir(legacyDir);
3044
+ const UI_EXTENSIONS = /* @__PURE__ */ new Set([
3045
+ ".ts",
3046
+ ".tsx",
3047
+ ".js",
3048
+ ".jsx",
3049
+ ".css",
3050
+ ".json",
3051
+ ".md",
3052
+ ".mdx"
3053
+ ]);
3054
+ const allUiFiles = await walkDir(componentsDir);
3055
+ const uiFiles = allUiFiles.filter(
3056
+ (f) => UI_EXTENSIONS.has(path19.extname(f))
3057
+ );
3058
+ for (const srcFile of uiFiles) {
3059
+ const relFromUi = path19.relative(componentsDir, srcFile);
3060
+ const destFile = path19.join(legacyDir, relFromUi);
3061
+ await ensureDir(path19.dirname(destFile));
3062
+ const content = await fs15.readFile(srcFile, "utf-8");
3063
+ await fs15.writeFile(destFile, content, "utf-8");
3064
+ try {
3065
+ await fs15.unlink(srcFile);
3066
+ } catch {
3067
+ logger.warn(`Could not remove: ${srcFile}`);
3068
+ }
3069
+ const fromRel = path19.relative(projectRoot, srcFile).replace(/\\/g, "/");
3070
+ const toRel = path19.relative(projectRoot, destFile).replace(/\\/g, "/");
3071
+ movedFiles.push({ from: fromRel, to: toRel });
3072
+ }
3073
+ await pruneEmptyDirs(componentsDir);
3074
+ logger.info(
3075
+ ` moved ${movedFiles.length} files \u2192 ${path19.relative(
3076
+ projectRoot,
3077
+ legacyDir
3078
+ )}/`
3079
+ );
2669
3080
  }
2670
- const result = [];
2671
- for (const entry of entries) {
2672
- if (!entry.isDirectory()) continue;
2673
- const dir = path18.join(snapshotRoot, entry.name);
2674
- let isoTs = null;
2675
- let reason = null;
2676
- try {
2677
- const raw = await fs14.readFile(path18.join(dir, META_FILE), "utf-8");
2678
- const parsed = JSON.parse(raw);
2679
- if (typeof parsed.ts === "string") isoTs = parsed.ts;
2680
- if (typeof parsed.reason === "string" && ["init", "update", "switch", "restore", "manual"].includes(
2681
- parsed.reason
2682
- )) {
2683
- reason = parsed.reason;
3081
+ const importRewrites = /* @__PURE__ */ new Map();
3082
+ const srcDir = path19.join(projectRoot, "src");
3083
+ if (await fileExists(srcDir)) {
3084
+ const SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mdx"]);
3085
+ const allScanFiles = await walkDir(srcDir, DEFAULT_SKIP_DIRS);
3086
+ const scanFiles = allScanFiles.filter((f) => {
3087
+ const rel2 = path19.relative(projectRoot, f);
3088
+ const segments = rel2.split(path19.sep);
3089
+ if (segments.some((s) => IGNORE_DIR_NAMES.has(s))) return false;
3090
+ return SCAN_EXTENSIONS.has(path19.extname(f));
3091
+ });
3092
+ const oldAlias = aliases.components;
3093
+ const newAlias = oldAlias.replace(/\/ui\/?$/, "/shadcn-ui");
3094
+ const oldWithoutSrc = oldAlias.replace(/^src\//, "");
3095
+ const newWithoutSrc = newAlias.replace(/^src\//, "");
3096
+ const dirName = path19.basename(oldAlias);
3097
+ const newDirName = path19.basename(newAlias);
3098
+ for (const file of scanFiles) {
3099
+ const content = await fs15.readFile(file, "utf-8");
3100
+ let modified = content;
3101
+ let count = 0;
3102
+ modified = modified.replace(
3103
+ new RegExp(`(['"'\`])@/${escapeRegExp2(oldWithoutSrc)}/`, "g"),
3104
+ (match, quote) => {
3105
+ count++;
3106
+ return `${quote}@/${newWithoutSrc}/`;
3107
+ }
3108
+ );
3109
+ modified = modified.replace(
3110
+ new RegExp(`(['"'\`])~/${escapeRegExp2(oldWithoutSrc)}/`, "g"),
3111
+ (match, quote) => {
3112
+ count++;
3113
+ return `${quote}~/${newWithoutSrc}/`;
3114
+ }
3115
+ );
3116
+ modified = modified.replace(
3117
+ new RegExp(
3118
+ `(from\\s+['"'\`](?:\\.\\.?\\/)+(?:[\\w.-]+\\/)*)${escapeRegExp2(
3119
+ dirName
3120
+ )}/`,
3121
+ "g"
3122
+ ),
3123
+ (match) => {
3124
+ count++;
3125
+ return match.slice(0, -dirName.length - 1) + `${newDirName}/`;
3126
+ }
3127
+ );
3128
+ if (count > 0) {
3129
+ await fs15.writeFile(file, modified, "utf-8");
3130
+ const relPath = path19.relative(projectRoot, file).replace(/\\/g, "/");
3131
+ importRewrites.set(relPath, count);
2684
3132
  }
3133
+ }
3134
+ logger.info(` rewrote imports in ${importRewrites.size} files`);
3135
+ }
3136
+ const componentsJsonPath = path19.join(projectRoot, "components.json");
3137
+ if (await fileExists(componentsJsonPath)) {
3138
+ await backupFile(componentsJsonPath, projectRoot);
3139
+ backedUpFiles.push("components.json");
3140
+ try {
3141
+ await fs15.unlink(componentsJsonPath);
3142
+ componentsJsonRemoved = true;
3143
+ logger.info(" backed up and removed components.json");
2685
3144
  } catch {
2686
- isoTs = fsSafeToIso(entry.name);
3145
+ logger.warn(" could not remove components.json");
2687
3146
  }
2688
- result.push({ ts: entry.name, isoTs, reason, path: dir });
2689
3147
  }
2690
- result.sort((a, b) => a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
2691
- return result;
3148
+ const libBackupTargets = [
3149
+ path19.join(projectRoot, aliases.utils + ".ts"),
3150
+ path19.join(projectRoot, aliases.lib, "color.ts")
3151
+ ];
3152
+ const hooksDir = path19.join(projectRoot, aliases.hooks);
3153
+ if (await fileExists(hooksDir)) {
3154
+ const allHookFiles = await walkDir(hooksDir, DEFAULT_SKIP_DIRS);
3155
+ const hookFiles = allHookFiles.filter(
3156
+ (f) => path19.extname(f) === ".ts" || path19.extname(f) === ".tsx"
3157
+ );
3158
+ libBackupTargets.push(...hookFiles);
3159
+ }
3160
+ for (const target of libBackupTargets) {
3161
+ if (await fileExists(target)) {
3162
+ await backupFile(target, projectRoot);
3163
+ backedUpFiles.push(
3164
+ path19.relative(projectRoot, target).replace(/\\/g, "/")
3165
+ );
3166
+ }
3167
+ }
3168
+ return {
3169
+ movedFiles,
3170
+ importRewrites,
3171
+ componentsJsonRemoved,
3172
+ backedUpFiles
3173
+ };
3174
+ }
3175
+ function escapeRegExp2(s) {
3176
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3177
+ }
3178
+ async function pruneEmptyDirs(dir) {
3179
+ try {
3180
+ const entries = await fs15.readdir(dir, { withFileTypes: true });
3181
+ for (const entry of entries) {
3182
+ if (entry.isDirectory()) {
3183
+ await pruneEmptyDirs(path19.join(dir, entry.name));
3184
+ }
3185
+ }
3186
+ const remaining = await fs15.readdir(dir);
3187
+ if (remaining.length === 0) {
3188
+ await fs15.rmdir(dir);
3189
+ }
3190
+ } catch {
3191
+ }
3192
+ }
3193
+
3194
+ // src/core/ui-upgrade.ts
3195
+ import * as path22 from "path";
3196
+ import { createRequire as createRequire5 } from "module";
3197
+ import {
3198
+ loadUiPackageManifest as loadUiPackageManifest3,
3199
+ loadVariantUiPackageManifest as loadVariantUiPackageManifest2
3200
+ } from "@teamix-evo/registry";
3201
+
3202
+ // src/core/ui-upgrade-detector.ts
3203
+ import * as fs16 from "fs/promises";
3204
+ import * as path20 from "path";
3205
+ var PACKAGE_NAME = {
3206
+ ui: "@teamix-evo/ui",
3207
+ "biz-ui": "@teamix-evo/biz-ui"
3208
+ };
3209
+ var ALIAS_KEY = {
3210
+ ui: "components",
3211
+ "biz-ui": "business"
3212
+ };
3213
+ var COMPONENT_FILE_RE = /\.(tsx|ts)$/;
3214
+ var SKIP_FILENAMES = /* @__PURE__ */ new Set(["index.ts", "index.tsx"]);
3215
+ async function detectComponentLineage(options) {
3216
+ const { projectRoot, category } = options;
3217
+ const config = options.config ?? await readProjectConfig(projectRoot);
3218
+ const installed = options.installed ?? await readInstalledManifest(projectRoot);
3219
+ const installDir = resolveInstallDir(category, config);
3220
+ const installDirAbs = path20.join(projectRoot, installDir);
3221
+ const installDirExists = await directoryExists(installDirAbs);
3222
+ const hasComponentsJson = await fileExists(
3223
+ path20.join(projectRoot, "components.json")
3224
+ );
3225
+ const installedPkg = findInstalledPackage(installed, PACKAGE_NAME[category]);
3226
+ const registeredIds = installedPkg ? extractIds(installedPkg).sort() : [];
3227
+ const onDiskIds = installDirExists ? await listComponentIds(installDirAbs) : [];
3228
+ const registeredSet = new Set(registeredIds);
3229
+ const unregisteredIds = onDiskIds.filter((id) => !registeredSet.has(id)).sort();
3230
+ const lineage = classifyLineage({
3231
+ hasInstalled: installedPkg !== null,
3232
+ hasComponentsJson,
3233
+ onDiskIds,
3234
+ unregisteredIds
3235
+ });
3236
+ return {
3237
+ category,
3238
+ lineage,
3239
+ installDir,
3240
+ installDirExists,
3241
+ hasComponentsJson,
3242
+ registeredIds,
3243
+ unregisteredIds,
3244
+ installedVersion: installedPkg?.version ?? null,
3245
+ installedVariant: installedPkg?.variant ?? null
3246
+ };
3247
+ }
3248
+ function resolveInstallDir(category, config) {
3249
+ const aliasMap = config?.packages?.ui?.aliases ?? config?.packages?.["biz-ui"]?.aliases ?? DEFAULT_UI_ALIASES;
3250
+ const key = ALIAS_KEY[category];
3251
+ return aliasMap[key] ?? DEFAULT_UI_ALIASES[key];
3252
+ }
3253
+ function extractIds(pkg) {
3254
+ const ids = /* @__PURE__ */ new Set();
3255
+ for (const r of pkg.resources) {
3256
+ const colon = r.id.indexOf(":");
3257
+ ids.add(colon >= 0 ? r.id.slice(0, colon) : r.id);
3258
+ }
3259
+ return [...ids];
3260
+ }
3261
+ async function directoryExists(p) {
3262
+ try {
3263
+ const stat5 = await fs16.stat(p);
3264
+ return stat5.isDirectory();
3265
+ } catch {
3266
+ return false;
3267
+ }
3268
+ }
3269
+ async function listComponentIds(installDirAbs) {
3270
+ const entries = await fs16.readdir(installDirAbs, { withFileTypes: true });
3271
+ const ids = [];
3272
+ for (const e of entries) {
3273
+ if (!e.isFile()) continue;
3274
+ if (SKIP_FILENAMES.has(e.name)) continue;
3275
+ if (!COMPONENT_FILE_RE.test(e.name)) continue;
3276
+ ids.push(e.name.replace(COMPONENT_FILE_RE, ""));
3277
+ }
3278
+ return ids.sort();
3279
+ }
3280
+ function classifyLineage(args) {
3281
+ const { hasInstalled, hasComponentsJson, onDiskIds, unregisteredIds } = args;
3282
+ if (hasInstalled) {
3283
+ return unregisteredIds.length === 0 ? "teamix-evo" : "mixed";
3284
+ }
3285
+ if (onDiskIds.length === 0) return "absent";
3286
+ return hasComponentsJson ? "shadcn-native" : "custom-only";
3287
+ }
3288
+
3289
+ // src/core/ui-upgrade-staging.ts
3290
+ import * as path21 from "path";
3291
+ var TEAMIX_DIR2 = ".teamix-evo";
3292
+ var STAGING_DIR = ".upgrade-staging";
3293
+ var PACKAGE_NAME2 = {
3294
+ ui: "@teamix-evo/ui",
3295
+ "biz-ui": "@teamix-evo/biz-ui"
3296
+ };
3297
+ function isoToFsSafe(iso) {
3298
+ return iso.replace(/[:.]/g, "-");
3299
+ }
3300
+ async function buildUiUpgradeStaging(options) {
3301
+ const { lineageReport, category } = options;
3302
+ if (lineageReport.lineage !== "teamix-evo" && lineageReport.lineage !== "mixed") {
3303
+ return null;
3304
+ }
3305
+ const installed = options.installed ?? await readInstalledManifest(options.projectRoot);
3306
+ const installedPkg = findInstalledPackage(installed, PACKAGE_NAME2[category]);
3307
+ if (!installedPkg) return null;
3308
+ const isoTs = options.isoTs ?? (/* @__PURE__ */ new Date()).toISOString();
3309
+ const fsTs = isoToFsSafe(isoTs);
3310
+ const stagingDir = path21.join(
3311
+ options.projectRoot,
3312
+ TEAMIX_DIR2,
3313
+ STAGING_DIR,
3314
+ `${category}-${fsTs}`
3315
+ );
3316
+ const entryMap = new Map(
3317
+ options.manifest.entries.map((e) => [e.id, e])
3318
+ );
3319
+ const resByEntryId = collectResourcesByEntry(installedPkg.resources);
3320
+ const onlyIds = options.onlyIds && options.onlyIds.length > 0 ? new Set(options.onlyIds) : null;
3321
+ const entries = [];
3322
+ for (const id of lineageReport.registeredIds) {
3323
+ if (onlyIds && !onlyIds.has(id)) continue;
3324
+ const built = await processRegistered({
3325
+ id,
3326
+ entry: entryMap.get(id),
3327
+ resource: resByEntryId.get(id),
3328
+ packageRoot: options.packageRoot,
3329
+ entryPackageRoot: options.entryPackageRoot,
3330
+ aliases: options.aliases,
3331
+ stagingDir,
3332
+ projectRoot: options.projectRoot,
3333
+ category,
3334
+ sourceVersion: options.manifest.version
3335
+ });
3336
+ if (built) entries.push(built);
3337
+ }
3338
+ for (const id of lineageReport.unregisteredIds) {
3339
+ if (onlyIds && !onlyIds.has(id)) continue;
3340
+ const built = await processForeign({
3341
+ id,
3342
+ installDirAbs: path21.join(options.projectRoot, lineageReport.installDir),
3343
+ stagingDir,
3344
+ projectRoot: options.projectRoot,
3345
+ category
3346
+ });
3347
+ if (built) entries.push(built);
3348
+ }
3349
+ if (entries.length === 0) return null;
3350
+ const byRisk = aggregateByRisk(entries);
3351
+ const manifestOut = {
3352
+ schemaVersion: 1,
3353
+ ts: isoTs,
3354
+ package: category,
3355
+ trigger: options.trigger,
3356
+ variant: lineageReport.installedVariant ?? "_flat",
3357
+ fromVersion: lineageReport.installedVersion ?? "",
3358
+ toVersion: options.manifest.version,
3359
+ lineage: lineageReport.lineage,
3360
+ summary: { total: entries.length, byRisk },
3361
+ entries
3362
+ };
3363
+ await ensureDir(stagingDir);
3364
+ await writeFileSafe(
3365
+ path21.join(stagingDir, "meta.json"),
3366
+ JSON.stringify(manifestOut, null, 2) + "\n"
3367
+ );
3368
+ return { stagingDir, manifest: manifestOut };
3369
+ }
3370
+ async function processRegistered(args) {
3371
+ const { id, entry, resource, stagingDir, projectRoot, category } = args;
3372
+ if (!resource) return null;
3373
+ const currentSource = await readFileOrNull(resource.target);
3374
+ if (currentSource === null) {
3375
+ return buildBreakingEntry({
3376
+ id,
3377
+ category,
3378
+ resource,
3379
+ projectRoot,
3380
+ stagingDir,
3381
+ currentSource: "",
3382
+ hint: "installed file missing on disk"
3383
+ });
3384
+ }
3385
+ if (!entry) {
3386
+ return buildBreakingEntry({
3387
+ id,
3388
+ category,
3389
+ resource,
3390
+ projectRoot,
3391
+ stagingDir,
3392
+ currentSource,
3393
+ hint: "entry removed in upstream package"
3394
+ });
3395
+ }
3396
+ const file = entry.files[0];
3397
+ if (!file) return null;
3398
+ const rootForEntry = args.entryPackageRoot?.get(id) ?? args.packageRoot;
3399
+ const sourceAbs = path21.resolve(rootForEntry, file.source);
3400
+ const raw = await readFileOrNull(sourceAbs);
3401
+ if (raw === null) {
3402
+ return null;
3403
+ }
3404
+ const incomingTransformed = rewriteImports(raw, args.aliases);
3405
+ const incomingHash = computeHash(incomingTransformed);
3406
+ const currentExt = path21.extname(resource.target) || ".tsx";
3407
+ const incomingExt = path21.extname(file.targetName) || currentExt;
3408
+ const currentRel = `${id}/current${currentExt}`;
3409
+ const incomingRel = `${id}/incoming${incomingExt}`;
3410
+ await writeFileSafe(path21.join(stagingDir, currentRel), currentSource);
3411
+ await writeFileSafe(path21.join(stagingDir, incomingRel), incomingTransformed);
3412
+ const diff = classifyRisk({
3413
+ currentHash: resource.hash,
3414
+ incomingHash,
3415
+ currentSource,
3416
+ incomingSource: incomingTransformed,
3417
+ multiFile: entry.files.length > 1
3418
+ });
3419
+ const promotion = derivePromotion({
3420
+ currentSource,
3421
+ incomingSource: incomingTransformed,
3422
+ targetName: entry.files[0]?.targetName ?? `${id}.tsx`
3423
+ });
3424
+ return {
3425
+ id,
3426
+ category,
3427
+ current: {
3428
+ target: path21.relative(projectRoot, resource.target),
3429
+ hash: resource.hash,
3430
+ sourceLineage: "teamix-evo"
3431
+ },
3432
+ incoming: {
3433
+ sourceVersion: args.sourceVersion,
3434
+ hash: incomingHash,
3435
+ relPath: incomingRel
3436
+ },
3437
+ diff,
3438
+ promotion
3439
+ };
3440
+ }
3441
+ async function processForeign(args) {
3442
+ const { id, installDirAbs, stagingDir, projectRoot, category } = args;
3443
+ const tsx = path21.join(installDirAbs, `${id}.tsx`);
3444
+ const ts = path21.join(installDirAbs, `${id}.ts`);
3445
+ const target = await fileExists(tsx) ? tsx : await fileExists(ts) ? ts : null;
3446
+ if (!target) return null;
3447
+ const raw = await readFileOrNull(target);
3448
+ if (raw === null) return null;
3449
+ const ext = path21.extname(target);
3450
+ const currentRel = `${id}/current${ext}`;
3451
+ await writeFileSafe(path21.join(stagingDir, currentRel), raw);
3452
+ return {
3453
+ id,
3454
+ category,
3455
+ current: {
3456
+ target: path21.relative(projectRoot, target),
3457
+ hash: computeHash(raw),
3458
+ sourceLineage: "custom"
3459
+ },
3460
+ diff: {
3461
+ riskLevel: "foreign",
3462
+ hints: [
3463
+ "component is on disk but not registered in .teamix-evo/manifest.json",
3464
+ "AI should propose: (a) ignore, (b) re-register via teamix-evo ui add, or (c) remove"
3465
+ ],
3466
+ filesChangedCount: 0
3467
+ }
3468
+ };
3469
+ }
3470
+ async function buildBreakingEntry(args) {
3471
+ const ext = path21.extname(args.resource.target) || ".tsx";
3472
+ const currentRel = `${args.id}/current${ext}`;
3473
+ await writeFileSafe(
3474
+ path21.join(args.stagingDir, currentRel),
3475
+ args.currentSource
3476
+ );
3477
+ return {
3478
+ id: args.id,
3479
+ category: args.category,
3480
+ current: {
3481
+ target: path21.relative(args.projectRoot, args.resource.target),
3482
+ hash: args.resource.hash,
3483
+ sourceLineage: "teamix-evo"
3484
+ },
3485
+ diff: {
3486
+ riskLevel: "breaking",
3487
+ hints: [args.hint],
3488
+ filesChangedCount: 0
3489
+ }
3490
+ };
3491
+ }
3492
+ function classifyRisk(args) {
3493
+ if (args.currentHash === args.incomingHash) {
3494
+ return { riskLevel: "unchanged", hints: [], filesChangedCount: 0 };
3495
+ }
3496
+ const curExports = extractExportNames(args.currentSource);
3497
+ const newExports = extractExportNames(args.incomingSource);
3498
+ const removedExports = setDiff(curExports, newExports);
3499
+ const addedExports = setDiff(newExports, curExports);
3500
+ const curVariants = extractCvaVariantValues(args.currentSource);
3501
+ const newVariants = extractCvaVariantValues(args.incomingSource);
3502
+ const removedVariants = setDiff(curVariants, newVariants);
3503
+ const addedVariants = setDiff(newVariants, curVariants);
3504
+ const hints = [];
3505
+ for (const e of removedExports) hints.push(`removed export: ${e}`);
3506
+ for (const e of addedExports) hints.push(`new export: ${e}`);
3507
+ for (const v of removedVariants) hints.push(`removed cva variant: ${v}`);
3508
+ for (const v of addedVariants) hints.push(`new cva variant: ${v}`);
3509
+ if (args.multiFile) hints.push("multi-file entry; only first file staged");
3510
+ let riskLevel;
3511
+ if (removedExports.length > 0 || removedVariants.length > 0) {
3512
+ riskLevel = "risky";
3513
+ } else if (addedExports.length > 0 || addedVariants.length > 0 || args.multiFile) {
3514
+ riskLevel = "upgradable-medium";
3515
+ } else {
3516
+ riskLevel = "upgradable-low";
3517
+ }
3518
+ return { riskLevel, hints, filesChangedCount: 1 };
3519
+ }
3520
+ function extractExportNames(src) {
3521
+ const names = /* @__PURE__ */ new Set();
3522
+ const re = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/gm;
3523
+ let m;
3524
+ while ((m = re.exec(src)) !== null) {
3525
+ if (m[1]) names.add(m[1]);
3526
+ }
3527
+ for (const dm of src.matchAll(/^\s*export\s+default\s+(\w+)\s*;/gm)) {
3528
+ if (dm[1]) names.add(dm[1]);
3529
+ }
3530
+ return [...names];
3531
+ }
3532
+ function extractCvaVariantValues(src) {
3533
+ const block = extractVariantsBlock(src);
3534
+ if (block === null) return [];
3535
+ const names = /* @__PURE__ */ new Set();
3536
+ for (const groupBody of extractGroupBodies(block)) {
3537
+ for (const km of groupBody.matchAll(/^\s*(?:['"]?)(\w+)(?:['"]?)\s*:/gm)) {
3538
+ if (km[1]) names.add(km[1]);
3539
+ }
3540
+ }
3541
+ return [...names];
3542
+ }
3543
+ function extractVariantsBlock(src) {
3544
+ const idx = src.search(/\bvariants\s*:\s*\{/);
3545
+ if (idx < 0) return null;
3546
+ const open = src.indexOf("{", idx);
3547
+ if (open < 0) return null;
3548
+ let depth = 0;
3549
+ for (let i = open; i < src.length; i++) {
3550
+ const c = src[i];
3551
+ if (c === "{") depth++;
3552
+ else if (c === "}") {
3553
+ depth--;
3554
+ if (depth === 0) return src.slice(open + 1, i);
3555
+ }
3556
+ }
3557
+ return null;
3558
+ }
3559
+ function* extractGroupBodies(block) {
3560
+ const re = /(\w+)\s*:\s*\{/g;
3561
+ let m;
3562
+ while ((m = re.exec(block)) !== null) {
3563
+ const open = block.indexOf("{", m.index);
3564
+ if (open < 0) continue;
3565
+ let depth = 0;
3566
+ for (let i = open; i < block.length; i++) {
3567
+ const c = block[i];
3568
+ if (c === "{") depth++;
3569
+ else if (c === "}") {
3570
+ depth--;
3571
+ if (depth === 0) {
3572
+ yield block.slice(open + 1, i);
3573
+ re.lastIndex = i + 1;
3574
+ break;
3575
+ }
3576
+ }
3577
+ }
3578
+ }
3579
+ }
3580
+ function setDiff(a, b) {
3581
+ const bset = new Set(b);
3582
+ return a.filter((x) => !bset.has(x)).sort();
3583
+ }
3584
+ function collectResourcesByEntry(resources) {
3585
+ const out = /* @__PURE__ */ new Map();
3586
+ for (const r of resources) {
3587
+ const colon = r.id.indexOf(":");
3588
+ const eid = colon >= 0 ? r.id.slice(0, colon) : r.id;
3589
+ if (!out.has(eid)) out.set(eid, r);
3590
+ }
3591
+ return out;
3592
+ }
3593
+ function aggregateByRisk(entries) {
3594
+ const out = {};
3595
+ for (const e of entries) {
3596
+ const k = e.diff.riskLevel;
3597
+ out[k] = (out[k] ?? 0) + 1;
3598
+ }
3599
+ return out;
3600
+ }
3601
+ function derivePromotion(args) {
3602
+ const fileType = classifyPromoteFileType(args.targetName, args.currentSource);
3603
+ const featureVector = buildFeatureVector(
3604
+ args.currentSource,
3605
+ args.incomingSource
3606
+ );
3607
+ const { recommendedModes, confidence, reasons } = scorePromotionModes(
3608
+ fileType,
3609
+ featureVector
3610
+ );
3611
+ return { fileType, featureVector, recommendedModes, confidence, reasons };
3612
+ }
3613
+ function classifyPromoteFileType(targetName, src) {
3614
+ if (targetName.endsWith(".d.ts")) return "type";
3615
+ if (/^use-[a-z0-9-]+\.tsx?$/i.test(targetName)) return "hook";
3616
+ const hasJsx = /<[A-Za-z][^>]*?>/.test(src);
3617
+ const hasReactImport = /from ['"]react['"]/.test(src);
3618
+ const hasProvider = /\.Provider\b/.test(src) || /createContext\s*[<(]/.test(src);
3619
+ if (hasProvider && (hasJsx || hasReactImport)) return "provider";
3620
+ if (hasJsx || /forwardRef\s*[<(]/.test(src)) return "component";
3621
+ if (!hasJsx && !hasReactImport) return "util";
3622
+ return "component";
3623
+ }
3624
+ function buildFeatureVector(current, incoming) {
3625
+ const curExports = extractExportNames(current);
3626
+ const newExports = extractExportNames(incoming);
3627
+ const apiAdded = setDiff(curExports, newExports);
3628
+ const apiRemoved = setDiff(newExports, curExports);
3629
+ const curVariants = extractCvaVariantValues(current);
3630
+ const newVariants = extractCvaVariantValues(incoming);
3631
+ const cvaAdded = setDiff(curVariants, newVariants);
3632
+ const cvaModified = [];
3633
+ const sharedVariants = curVariants.filter((v) => newVariants.includes(v));
3634
+ for (const v of sharedVariants) {
3635
+ if (extractVariantBody(current, v) !== extractVariantBody(incoming, v)) {
3636
+ cvaModified.push(v);
3637
+ }
3638
+ }
3639
+ const curClass = extractClassNameLiterals(current);
3640
+ const newClass = extractClassNameLiterals(incoming);
3641
+ const classNameDiff = curClass !== newClass;
3642
+ const curTokens = extractTokenRefs(current);
3643
+ const newTokens = extractTokenRefs(incoming);
3644
+ const tokenUsageDiff = curTokens.size !== newTokens.size || [...curTokens].some((t) => !newTokens.has(t));
3645
+ const hasState = /\buseState\s*[<(]/.test(current);
3646
+ const hasEffect = /\b(useEffect|useLayoutEffect|useMemo|useCallback)\s*[<(]/.test(current);
3647
+ const curImports = extractImportSources(current);
3648
+ const newImports = extractImportSources(incoming);
3649
+ const hasExtraImports = [...curImports].some((src) => !newImports.has(src));
3650
+ const tagSet = /* @__PURE__ */ new Set();
3651
+ for (const m of current.matchAll(/<([A-Z]\w+)[\s/>]/g)) {
3652
+ if (m[1]) tagSet.add(m[1]);
3653
+ }
3654
+ const atomicChildren = [...tagSet];
3655
+ const isComposition = atomicChildren.length > 2;
3656
+ const signatureChanged = extractDefaultParamList(current) !== extractDefaultParamList(incoming);
3657
+ return {
3658
+ apiDelta: { added: apiAdded, removed: apiRemoved, signatureChanged },
3659
+ styleDelta: { classNameDiff, tokenUsageDiff },
3660
+ logicDelta: { hasState, hasEffect, hasExtraImports },
3661
+ cvaDelta: { addedVariants: cvaAdded, modifiedVariants: cvaModified },
3662
+ structureDelta: { isComposition, atomicChildren }
3663
+ };
3664
+ }
3665
+ function scorePromotionModes(fileType, fv) {
3666
+ const reasons = [];
3667
+ if (fileType === "hook" || fileType === "util" || fileType === "type") {
3668
+ reasons.push(
3669
+ `fileType=${fileType} \u2014 not a component, deferred to ManualReview`
3670
+ );
3671
+ return { recommendedModes: ["ManualReview"], confidence: 0.5, reasons };
3672
+ }
3673
+ const apiNoChange = fv.apiDelta.added.length === 0 && fv.apiDelta.removed.length === 0 && !fv.apiDelta.signatureChanged;
3674
+ const logicMinimal = !fv.logicDelta.hasState && !fv.logicDelta.hasEffect && !fv.logicDelta.hasExtraImports;
3675
+ if (fv.apiDelta.removed.length > 0 || fv.apiDelta.signatureChanged) {
3676
+ reasons.push(
3677
+ "signature changed or props removed \u2014 Coexist preserves user version"
3678
+ );
3679
+ return { recommendedModes: ["Coexist"], confidence: 0.85, reasons };
3680
+ }
3681
+ if (apiNoChange && logicMinimal && (fv.styleDelta.classNameDiff || fv.styleDelta.tokenUsageDiff) && fv.cvaDelta.addedVariants.length === 0 && fv.cvaDelta.modifiedVariants.length === 0) {
3682
+ reasons.push(
3683
+ "only style / token differences \u2014 push to tokens.overrides.css"
3684
+ );
3685
+ return { recommendedModes: ["TokenOnly"], confidence: 0.8, reasons };
3686
+ }
3687
+ const modes = [];
3688
+ let score = 0;
3689
+ if (fv.cvaDelta.addedVariants.length > 0 || fv.cvaDelta.modifiedVariants.length > 0) {
3690
+ modes.push("Variant");
3691
+ reasons.push(
3692
+ `cva variants delta: +${fv.cvaDelta.addedVariants.length} ~${fv.cvaDelta.modifiedVariants.length}`
3693
+ );
3694
+ score = Math.max(score, 0.7);
3695
+ }
3696
+ if (fv.logicDelta.hasState || fv.logicDelta.hasEffect || fv.logicDelta.hasExtraImports || fv.apiDelta.added.length > 0) {
3697
+ modes.push("Wrapper");
3698
+ reasons.push("user added state / effect / imports / props");
3699
+ score = Math.max(score, 0.75);
3700
+ }
3701
+ if (apiNoChange && logicMinimal && !fv.styleDelta.classNameDiff && !fv.styleDelta.tokenUsageDiff && fv.cvaDelta.addedVariants.length === 0 && fv.cvaDelta.modifiedVariants.length === 0) {
3702
+ modes.push("Preset");
3703
+ reasons.push("no API/logic delta \u2014 Preset captures default-prop tweaks");
3704
+ score = Math.max(score, 0.6);
3705
+ }
3706
+ if (fv.structureDelta.isComposition) {
3707
+ modes.push("Compose");
3708
+ reasons.push(
3709
+ `composition of ${fv.structureDelta.atomicChildren.length} atomic children`
3710
+ );
3711
+ score = Math.max(score, 0.65);
3712
+ }
3713
+ if (modes.length === 0) {
3714
+ reasons.push("no axis crossed the 0.6 threshold");
3715
+ return { recommendedModes: ["ManualReview"], confidence: 0.4, reasons };
3716
+ }
3717
+ return { recommendedModes: modes, confidence: score, reasons };
3718
+ }
3719
+ function extractVariantBody(src, key) {
3720
+ const block = extractVariantsBlock(src);
3721
+ if (block === null) return "";
3722
+ const re = new RegExp(`(?:["']?${key}["']?)\\s*:\\s*\\{`);
3723
+ const idx = block.search(re);
3724
+ if (idx < 0) return "";
3725
+ const open = block.indexOf("{", idx);
3726
+ if (open < 0) return "";
3727
+ let depth = 0;
3728
+ for (let i = open; i < block.length; i++) {
3729
+ const c = block[i];
3730
+ if (c === "{") depth++;
3731
+ else if (c === "}") {
3732
+ depth--;
3733
+ if (depth === 0) return block.slice(open + 1, i);
3734
+ }
3735
+ }
3736
+ return "";
3737
+ }
3738
+ function extractClassNameLiterals(src) {
3739
+ const out = [];
3740
+ for (const m of src.matchAll(/className\s*=\s*["'`]([^"'`]*)["'`]/g)) {
3741
+ if (m[1]) out.push(m[1]);
3742
+ }
3743
+ for (const m of src.matchAll(/\b(?:cn|clsx|cva)\s*\(/g)) {
3744
+ const open = (m.index ?? 0) + m[0].length - 1;
3745
+ let depth = 1;
3746
+ let i = open + 1;
3747
+ for (; i < src.length && depth > 0; i++) {
3748
+ const c = src[i];
3749
+ if (c === "(") depth++;
3750
+ else if (c === ")") depth--;
3751
+ }
3752
+ const body = src.slice(open + 1, i - 1);
3753
+ for (const lit of body.matchAll(/["'`]([^"'`]*)["'`]/g)) {
3754
+ if (lit[1]) out.push(lit[1]);
3755
+ }
3756
+ }
3757
+ return out.sort().join("|");
3758
+ }
3759
+ function extractTokenRefs(src) {
3760
+ const out = /* @__PURE__ */ new Set();
3761
+ for (const m of src.matchAll(/var\(--([a-z0-9-]+)\)/g)) {
3762
+ if (m[1]) out.add(m[1]);
3763
+ }
3764
+ for (const m of src.matchAll(/--([a-z][a-z0-9-]*)\s*:/g)) {
3765
+ if (m[1]) out.add(m[1]);
3766
+ }
3767
+ return out;
3768
+ }
3769
+ function extractImportSources(src) {
3770
+ const out = /* @__PURE__ */ new Set();
3771
+ for (const m of src.matchAll(/^\s*import\b[^'"]*['"]([^'"]+)['"]/gm)) {
3772
+ if (m[1]) out.add(m[1]);
3773
+ }
3774
+ return out;
3775
+ }
3776
+ function extractDefaultParamList(src) {
3777
+ const m = /export\s+default\s+(?:async\s+)?function\s+\w*\s*\(([^)]*)\)/.exec(src) ?? /export\s+default\s+(?:\([^)]*\)|\w+)\s*=>/.exec(src) ?? /(?:const|function)\s+\w+\s*=?\s*(?:\(([^)]*)\)|\w+)\s*(?:=>|\{)/.exec(src);
3778
+ return m?.[1]?.replace(/\s+/g, " ").trim() ?? "";
3779
+ }
3780
+
3781
+ // src/core/ui-upgrade.ts
3782
+ var nodeRequire = createRequire5(import.meta.url);
3783
+ function resolvePackageRoot4(packageName) {
3784
+ const pkgJsonPath = nodeRequire.resolve(`${packageName}/package.json`);
3785
+ return path22.dirname(pkgJsonPath);
3786
+ }
3787
+ async function runUiUpgrade(options) {
3788
+ const { projectRoot, category, ids = [], trigger } = options;
3789
+ const config = await readProjectConfig(projectRoot);
3790
+ if (!config) return { status: "not-initialized" };
3791
+ const cfgKey = category === "ui" ? "ui" : "biz-ui";
3792
+ if (!config.packages?.[cfgKey]) {
3793
+ return { status: "not-installed", detail: `${category} not installed` };
3794
+ }
3795
+ const aliases = config.packages.ui?.aliases ?? config.packages["biz-ui"]?.aliases;
3796
+ if (!aliases) {
3797
+ return {
3798
+ status: "not-installed",
3799
+ detail: `${category} aliases not configured (run \`teamix-evo ui init\` first)`
3800
+ };
3801
+ }
3802
+ const installed = await readInstalledManifest(projectRoot);
3803
+ const lineageReport = await detectComponentLineage({
3804
+ projectRoot,
3805
+ category,
3806
+ config,
3807
+ installed
3808
+ });
3809
+ if (lineageReport.lineage !== "teamix-evo" && lineageReport.lineage !== "mixed") {
3810
+ return {
3811
+ status: "skipped",
3812
+ detail: `lineage=${lineageReport.lineage}; nothing to stage`,
3813
+ lineage: lineageReport.lineage
3814
+ };
3815
+ }
3816
+ if (ids.length > 0) {
3817
+ const knownIds = /* @__PURE__ */ new Set([
3818
+ ...lineageReport.registeredIds,
3819
+ ...lineageReport.unregisteredIds
3820
+ ]);
3821
+ const unknown = ids.filter((id) => !knownIds.has(id));
3822
+ if (unknown.length > 0) {
3823
+ throw new Error(
3824
+ `Unknown ${category} component id(s): ${unknown.map((s) => `"${s}"`).join(
3825
+ ", "
3826
+ )}. Hint: \`teamix-evo ${category} list\` shows available ids.`
3827
+ );
3828
+ }
3829
+ }
3830
+ const built = await buildStaging({
3831
+ category,
3832
+ projectRoot,
3833
+ aliases,
3834
+ lineageReport,
3835
+ trigger,
3836
+ onlyIds: ids,
3837
+ uiPackageRoot: options.uiPackageRoot,
3838
+ bizUiPackageRoot: options.bizUiPackageRoot
3839
+ });
3840
+ if (built === null) {
3841
+ return {
3842
+ status: "skipped",
3843
+ detail: "no entries to stage",
3844
+ lineage: lineageReport.lineage
3845
+ };
3846
+ }
3847
+ return {
3848
+ status: "staged",
3849
+ stagingDir: built.stagingDir,
3850
+ manifest: built.manifest
3851
+ };
3852
+ }
3853
+ async function buildStaging(args) {
3854
+ const { category, projectRoot, aliases, lineageReport, trigger, onlyIds } = args;
3855
+ if (category === "ui") {
3856
+ const root = args.uiPackageRoot ?? resolvePackageRoot4("@teamix-evo/ui");
3857
+ const manifest = await loadUiPackageManifest3(root);
3858
+ return buildUiUpgradeStaging({
3859
+ projectRoot,
3860
+ category,
3861
+ manifest,
3862
+ packageRoot: root,
3863
+ aliases,
3864
+ lineageReport,
3865
+ trigger,
3866
+ onlyIds
3867
+ });
3868
+ }
3869
+ const bizRoot = args.bizUiPackageRoot ?? resolvePackageRoot4("@teamix-evo/biz-ui");
3870
+ const variant = lineageReport.installedVariant ?? "_flat";
3871
+ const variantDir = path22.join(bizRoot, "variants", variant);
3872
+ const variantManifest = await loadVariantUiPackageManifest2(variantDir);
3873
+ const uiRoot = args.uiPackageRoot ?? resolvePackageRoot4("@teamix-evo/ui");
3874
+ const uiManifest = await loadUiPackageManifest3(uiRoot);
3875
+ const entryPackageRoot = /* @__PURE__ */ new Map();
3876
+ const merged = [];
3877
+ for (const e of variantManifest.entries) {
3878
+ entryPackageRoot.set(e.id, variantDir);
3879
+ merged.push(e);
3880
+ }
3881
+ for (const e of uiManifest.entries) {
3882
+ if (entryPackageRoot.has(e.id)) continue;
3883
+ entryPackageRoot.set(e.id, uiRoot);
3884
+ merged.push(e);
3885
+ }
3886
+ const synthetic = {
3887
+ schemaVersion: 1,
3888
+ package: "ui",
3889
+ version: variantManifest.version,
3890
+ engines: variantManifest.engines,
3891
+ entries: merged
3892
+ };
3893
+ return buildUiUpgradeStaging({
3894
+ projectRoot,
3895
+ category,
3896
+ manifest: synthetic,
3897
+ packageRoot: variantDir,
3898
+ entryPackageRoot,
3899
+ aliases,
3900
+ lineageReport,
3901
+ trigger,
3902
+ onlyIds
3903
+ });
3904
+ }
3905
+
3906
+ // src/core/deps-install.ts
3907
+ import * as fs17 from "fs/promises";
3908
+ import * as path23 from "path";
3909
+ import { exec } from "child_process";
3910
+ import { promisify } from "util";
3911
+ var execAsync = promisify(exec);
3912
+ async function detectPackageManager(projectRoot) {
3913
+ if (await fileExists(path23.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3914
+ if (await fileExists(path23.join(projectRoot, "pnpm-workspace.yaml")))
3915
+ return "pnpm";
3916
+ if (await fileExists(path23.join(projectRoot, "bun.lockb"))) return "bun";
3917
+ if (await fileExists(path23.join(projectRoot, "bun.lock"))) return "bun";
3918
+ if (await fileExists(path23.join(projectRoot, "yarn.lock"))) return "yarn";
3919
+ return "npm";
3920
+ }
3921
+ function getInstallCommand(pm) {
3922
+ switch (pm) {
3923
+ case "pnpm":
3924
+ return "pnpm install";
3925
+ case "yarn":
3926
+ return "yarn install";
3927
+ case "bun":
3928
+ return "bun install";
3929
+ case "npm":
3930
+ return "npm install";
3931
+ }
3932
+ }
3933
+ async function installProjectDeps(options) {
3934
+ const { projectRoot, npmDependencies, skipInstall = false } = options;
3935
+ const pkgPath = path23.join(projectRoot, "package.json");
3936
+ const raw = await readFileOrNull(pkgPath);
3937
+ if (!raw) {
3938
+ throw new Error(
3939
+ `package.json not found at ${projectRoot}. Cannot install dependencies.`
3940
+ );
3941
+ }
3942
+ const pkg = JSON.parse(raw);
3943
+ const existingDeps = {
3944
+ ...pkg.dependencies,
3945
+ ...pkg.devDependencies
3946
+ };
3947
+ const added = {};
3948
+ const existed = {};
3949
+ for (const [name, range] of Object.entries(npmDependencies)) {
3950
+ if (existingDeps[name]) {
3951
+ existed[name] = existingDeps[name];
3952
+ } else {
3953
+ added[name] = range;
3954
+ }
3955
+ }
3956
+ if (Object.keys(added).length > 0) {
3957
+ if (!pkg.dependencies) pkg.dependencies = {};
3958
+ for (const [name, range] of Object.entries(added)) {
3959
+ pkg.dependencies[name] = range;
3960
+ }
3961
+ pkg.dependencies = Object.fromEntries(
3962
+ Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b))
3963
+ );
3964
+ await fs17.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
3965
+ logger.info(
3966
+ ` patched package.json: +${Object.keys(added).length} dependencies`
3967
+ );
3968
+ }
3969
+ const packageManager = await detectPackageManager(projectRoot);
3970
+ let installed = false;
3971
+ if (!skipInstall && Object.keys(added).length > 0) {
3972
+ const cmd = getInstallCommand(packageManager);
3973
+ logger.info(` running ${cmd}...`);
3974
+ try {
3975
+ await execAsync(cmd, { cwd: projectRoot, timeout: 12e4 });
3976
+ installed = true;
3977
+ logger.info(" install complete");
3978
+ } catch (err) {
3979
+ logger.warn(
3980
+ ` install failed: ${err instanceof Error ? err.message : String(err)}`
3981
+ );
3982
+ logger.warn(" please run install manually");
3983
+ }
3984
+ }
3985
+ return { added, existed, installed, packageManager };
3986
+ }
3987
+
3988
+ // src/core/ui-migration-plan-template.ts
3989
+ import * as fs18 from "fs/promises";
3990
+ import * as path24 from "path";
3991
+ async function generateMigrationPlan(options) {
3992
+ const { projectRoot, strategy, isolateResult, addResult, residualImports } = options;
3993
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3994
+ const planDir = path24.join(projectRoot, ".qoder", "plans");
3995
+ await ensureDir(planDir);
3996
+ const planPath = path24.join(planDir, "teamix-evo-ui-migration.md");
3997
+ const movedSection = isolateResult.movedFiles.length > 0 ? isolateResult.movedFiles.map((f) => `| \`${f.from}\` | \`${f.to}\` |`).join("\n") : "| _(\u65E0\u642C\u8FC1)_ | |";
3998
+ const installedSection = addResult.orderedIds.map((id) => `- \`${id}\``).join("\n");
3999
+ const rewriteCount = isolateResult.importRewrites.size;
4000
+ const rewriteSection = rewriteCount > 0 ? [...isolateResult.importRewrites.entries()].map(([file, count]) => `- \`${file}\`: ${count} \u5904`).join("\n") : "_(\u65E0 import \u91CD\u5199)_";
4001
+ const strategyLabel = strategy === "isolate-progressive" ? "\u9694\u79BB + \u6E10\u8FDB\u8FC1\u79FB\uFF08Path A\uFF09" : strategy === "isolate-aggressive" ? "\u9694\u79BB + \u4E3B\u52A8\u5347\u7EA7\uFF08Path C\uFF09" : "\u8DF3\u8FC7\u5DF2\u6709\u6587\u4EF6";
4002
+ const residualSection = residualImports != null && residualImports > 0 ? `
4003
+ ## \u6B8B\u7559 import \u6E05\u5355
4004
+
4005
+ \u53D1\u73B0 ${residualImports} \u5904\u6B8B\u7559\u5F15\u7528\uFF0C\u9700\u4EBA\u5DE5\u4FEE\u590D\u3002\u8BE6\u89C1 lint \u8F93\u51FA\u4E2D\u7684 \`no-legacy-shadcn-import\` \u8B66\u544A\u3002
4006
+ ` : "";
4007
+ const nextSteps = strategy === "isolate-aggressive" ? `1. \u67E5\u770B \`.teamix-evo/.upgrade-staging/\` \u4E2D\u7684\u5347\u7EA7 staging
4008
+ 2. \u89E6\u53D1 \`teamix-evo-upgrade\` skill \u8FDB\u884C\u5206\u6279\u66FF\u6362
4009
+ 3. \u9010\u6B65\u5C06 \`shadcn-ui/\` \u4E2D\u7684\u7EC4\u4EF6\u66FF\u6362\u4E3A \`ui/\` \u7248\u672C
4010
+ 4. \u66FF\u6362\u5B8C\u6210\u540E\u5220\u9664 \`shadcn-ui/\` \u76EE\u5F55
4011
+ 5. \u8FD0\u884C \`npx teamix-evo tokens treat\` \u8FDB\u884C token \u6CBB\u7406` : `1. \u9010\u6B65\u5C06\u4E1A\u52A1\u4EE3\u7801\u4E2D\u7684 \`@/components/shadcn-ui/...\` \u66FF\u6362\u4E3A \`@/components/ui/...\`
4012
+ 2. ESLint \`no-legacy-shadcn-import\` \u89C4\u5219\u4F1A\u63D0\u793A\u54EA\u4E9B\u6587\u4EF6\u8FD8\u5728\u5F15\u7528\u65E7\u7EC4\u4EF6
4013
+ 3. \u5168\u90E8\u66FF\u6362\u5B8C\u6210\u540E\u5220\u9664 \`src/components/shadcn-ui/\` \u76EE\u5F55
4014
+ 4. \u8FD0\u884C \`npx teamix-evo tokens treat\` \u8FDB\u884C token \u6CBB\u7406`;
4015
+ const content = `# teamix-evo UI \u8FC1\u79FB\u8BA1\u5212
4016
+
4017
+ > \u751F\u6210\u65F6\u95F4: ${now}
4018
+ > \u8FC1\u79FB\u7B56\u7565: ${strategyLabel}
4019
+
4020
+ ## \u642C\u8FC1\u8BB0\u5F55
4021
+
4022
+ | \u539F\u8DEF\u5F84 | \u65B0\u8DEF\u5F84 |
4023
+ | ------ | ------ |
4024
+ ${movedSection}
4025
+
4026
+ ## \u5DF2\u5B89\u88C5 teamix-evo \u7EC4\u4EF6\uFF08${addResult.orderedIds.length} \u4E2A\uFF09
4027
+
4028
+ ${installedSection}
4029
+
4030
+ ## Import \u91CD\u5199\u8BB0\u5F55\uFF08${rewriteCount} \u4E2A\u6587\u4EF6\uFF09
4031
+
4032
+ ${rewriteSection}
4033
+
4034
+ ## components.json
4035
+
4036
+ ${isolateResult.componentsJsonRemoved ? "\u5DF2\u5907\u4EFD\u81F3 `.teamix-evo/.backups/` \u5E76\u5220\u9664\u539F\u6587\u4EF6\u3002" : "\u672A\u53D1\u73B0 components.json\u3002"}
4037
+
4038
+ ## \u5907\u4EFD\u6587\u4EF6
4039
+
4040
+ ${isolateResult.backedUpFiles.length > 0 ? isolateResult.backedUpFiles.map((f) => `- \`${f}\``).join("\n") : "_(\u65E0\u5907\u4EFD)_"}
4041
+ ${residualSection}
4042
+ ## \u540E\u7EED\u6B65\u9AA4
4043
+
4044
+ ${nextSteps}
4045
+
4046
+ ## npm \u4F9D\u8D56
4047
+
4048
+ \u5DF2\u81EA\u52A8\u5B89\u88C5\u7684\u4F9D\u8D56:
4049
+ ${Object.entries(addResult.npmDependencies).length > 0 ? Object.entries(addResult.npmDependencies).map(([name, version]) => `- \`${name}@${version}\``).join("\n") : "_(\u65E0\u989D\u5916\u4F9D\u8D56)_"}
4050
+ `;
4051
+ await fs18.writeFile(planPath, content, "utf-8");
4052
+ return planPath;
4053
+ }
4054
+
4055
+ // src/core/residual-import-detector.ts
4056
+ import * as fs19 from "fs/promises";
4057
+ import * as path25 from "path";
4058
+ var LEGACY_PATTERNS = [
4059
+ /['"`]@\/components\/shadcn-ui\//,
4060
+ /['"`]~\/components\/shadcn-ui\//,
4061
+ /['"`]\.\.?\/.*shadcn-ui\//,
4062
+ /from\s+['"].*shadcn-ui\//
4063
+ ];
4064
+ async function detectResidualImports(projectRoot) {
4065
+ const srcDir = path25.join(projectRoot, "src");
4066
+ const SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
4067
+ ".ts",
4068
+ ".tsx",
4069
+ ".js",
4070
+ ".jsx",
4071
+ ".mdx",
4072
+ ".css",
4073
+ ".scss"
4074
+ ]);
4075
+ let filesScanned = 0;
4076
+ const entries = [];
4077
+ const affectedFileSet = /* @__PURE__ */ new Set();
4078
+ if (await fileExists(srcDir)) {
4079
+ const allFiles = await walkDir(srcDir, DEFAULT_SKIP_DIRS);
4080
+ const scanFiles = allFiles.filter(
4081
+ (f) => SCAN_EXTENSIONS.has(path25.extname(f))
4082
+ );
4083
+ for (const file of scanFiles) {
4084
+ filesScanned++;
4085
+ const content = await fs19.readFile(file, "utf-8");
4086
+ const lines = content.split("\n");
4087
+ const relPath = path25.relative(projectRoot, file).replace(/\\/g, "/");
4088
+ for (let i = 0; i < lines.length; i++) {
4089
+ const line = lines[i];
4090
+ for (const pattern of LEGACY_PATTERNS) {
4091
+ if (pattern.test(line)) {
4092
+ entries.push({
4093
+ file: relPath,
4094
+ line: i + 1,
4095
+ content: line.trim()
4096
+ });
4097
+ affectedFileSet.add(relPath);
4098
+ break;
4099
+ }
4100
+ }
4101
+ }
4102
+ }
4103
+ }
4104
+ const rootConfigFiles = [
4105
+ "vite.config.ts",
4106
+ "vite.config.js",
4107
+ "next.config.ts",
4108
+ "next.config.js",
4109
+ "tsconfig.json",
4110
+ ".storybook/main.ts",
4111
+ ".storybook/main.js"
4112
+ ];
4113
+ for (const configFile of rootConfigFiles) {
4114
+ const configPath = path25.join(projectRoot, configFile);
4115
+ if (await fileExists(configPath)) {
4116
+ filesScanned++;
4117
+ const content = await fs19.readFile(configPath, "utf-8");
4118
+ const lines = content.split("\n");
4119
+ for (let i = 0; i < lines.length; i++) {
4120
+ const line = lines[i];
4121
+ for (const pattern of LEGACY_PATTERNS) {
4122
+ if (pattern.test(line)) {
4123
+ entries.push({
4124
+ file: configFile,
4125
+ line: i + 1,
4126
+ content: line.trim()
4127
+ });
4128
+ affectedFileSet.add(configFile);
4129
+ break;
4130
+ }
4131
+ }
4132
+ }
4133
+ }
4134
+ }
4135
+ return {
4136
+ filesScanned,
4137
+ entries,
4138
+ affectedFiles: affectedFileSet.size
4139
+ };
4140
+ }
4141
+
4142
+ // src/core/init-checklist-template.ts
4143
+ var STEP_LABELS = {
4144
+ tokens: "tokens \u2014 design tokens \u5B89\u88C5",
4145
+ skills: "skills \u2014 AI skills \u5B89\u88C5",
4146
+ "agents-md": "agents-md \u2014 AGENTS.md \u751F\u6210",
4147
+ "ui-init": "ui-init \u2014 UI \u914D\u7F6E\u521D\u59CB\u5316",
4148
+ "ui-add": "ui-add \u2014 UI \u7EC4\u4EF6\u5B89\u88C5",
4149
+ lint: "lint \u2014 ESLint + Stylelint \u914D\u7F6E",
4150
+ gitignore: "gitignore \u2014 .gitignore \u8FFD\u52A0"
4151
+ };
4152
+ function renderInitChecklist(args) {
4153
+ const { variant, status, steps } = args;
4154
+ const ts = args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
4155
+ const phaseA = steps.map((s) => {
4156
+ const label = STEP_LABELS[s.name] ?? s.name;
4157
+ const checked = s.status === "ok" ? "x" : " ";
4158
+ const suffix = s.status === "ok" ? "" : s.status === "skip" ? "\uFF08\u8DF3\u8FC7\uFF09" : s.status === "fail" ? `\uFF08\u5931\u8D25\uFF1A${s.detail ?? "unknown"}\uFF09` : "\uFF08\u8BA1\u5212\u4E2D\uFF09";
4159
+ return `- [${checked}] ${label}${suffix}`;
4160
+ }).join("\n");
4161
+ return `# teamix-evo init \u68C0\u67E5\u6E05\u5355
4162
+
4163
+ > \u7531 \`teamix-evo init\` \u81EA\u52A8\u751F\u6210 \xB7 ${ts}
4164
+ > variant: ${variant} \xB7 status: ${status}
4165
+
4166
+ ## Phase A \xB7 CLI \u843D\u5730\uFF08\u81EA\u52A8\u5B8C\u6210\uFF09
4167
+
4168
+ ${phaseA}
4169
+
4170
+ ## Phase B \xB7 AI \u4E32\u573A\uFF086 \u6B65\u95ED\u73AF\uFF09
4171
+
4172
+ > \u4EE5\u4E0B\u6B65\u9AA4\u7531\u5BA2\u6237\u4FA7 AI \u5728 IDE \u4E2D\u9010\u6B65\u6267\u884C\u3002\u26A0\uFE0F \u6807\u8BB0\u7684\u6B65\u9AA4\u4E3A HARD GATE\uFF0C
4173
+ > \u5FC5\u987B\u6267\u884C\u5B8C\u6BD5\u5E76\u5F81\u5F97\u7528\u6237\u786E\u8BA4\u540E\u624D\u80FD\u7EE7\u7EED\uFF08\u53C2\u89C1 manage SKILL.md\u300CHARD GATE \u534F\u8BAE\u300D\uFF09\u3002
4174
+
4175
+ - [ ] \u26A0\uFE0F HARD GATE\uFF1Aadopt \u2014 \`npx teamix-evo ui add --adopt\`
4176
+ - [ ] \u26A0\uFE0F HARD GATE\uFF1Aupgrade staging \u2014 \`npx teamix-evo ui upgrade\`\uFF08+ \`biz-ui upgrade\`\uFF09
4177
+ - [ ] \u26A0\uFE0F HARD GATE\uFF1Atokens audit \u2014 \`npx teamix-evo tokens audit\`
4178
+ - [ ] AGENTS.md \u56DE\u586B \u2014 \u5F52\u7C7B\u9879\u76EE\u7279\u6709\u89C4\u5219\uFF0C\u585E\u56DE managed \u533A\u57DF\u4E4B\u524D
4179
+ - [ ] lint \u2014 \`npx teamix-evo lint init -y\` + \`pnpm lint\`
4180
+ - [ ] \u26A0\uFE0F HARD GATE\uFF1Atoken \u6CBB\u7406 \u2014 \u89E6\u53D1 \`teamix-evo-upgrade\` skill Part C\uFF08Token treatment pipeline\uFF09
4181
+
4182
+ ## \u5907\u6CE8
4183
+
4184
+ - Phase A \u4E2D\u5931\u8D25\u6216\u8DF3\u8FC7\u7684\u6B65\u9AA4\u4E0D\u5F71\u54CD Phase B \u542F\u52A8\uFF0C\u4F46\u5EFA\u8BAE\u5148\u4FEE\u590D\u518D\u7EE7\u7EED
4185
+ - \u4FEE\u590D\u540E\u91CD\u8DD1 \`teamix-evo init\`\uFF08\u6BCF\u6B65\u5E42\u7B49\uFF0C\u81EA\u52A8\u8DF3\u8FC7\u5DF2\u5B8C\u6210\u9879\uFF09
4186
+ - \u6062\u590D\u5230\u6267\u884C\u524D\u72B6\u6001\uFF1A\`teamix-evo restore --list\` \u2192 \`teamix-evo restore <ts>\`
4187
+ `;
4188
+ }
4189
+
4190
+ // src/core/snapshot.ts
4191
+ import * as fs20 from "fs/promises";
4192
+ import * as path26 from "path";
4193
+ var TEAMIX_DIR3 = ".teamix-evo";
4194
+ var SNAPSHOTS_DIR = ".snapshots";
4195
+ var LOGS_DIR = "logs";
4196
+ var META_FILE = "_meta.json";
4197
+ var DEFAULT_KEEP = 5;
4198
+ function isoToFsSafe2(iso) {
4199
+ return iso.replace(/[:.]/g, "-");
4200
+ }
4201
+ function fsSafeToIso(safe) {
4202
+ return safe.replace(
4203
+ /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})(Z)$/,
4204
+ "$1T$2:$3:$4.$5$6"
4205
+ );
4206
+ }
4207
+ async function createSnapshot(projectRoot, opts = {}) {
4208
+ const teamixDir = path26.join(projectRoot, TEAMIX_DIR3);
4209
+ try {
4210
+ const stat5 = await fs20.stat(teamixDir);
4211
+ if (!stat5.isDirectory()) return null;
4212
+ } catch (err) {
4213
+ if (err.code === "ENOENT") return null;
4214
+ throw err;
4215
+ }
4216
+ const isoTs = (/* @__PURE__ */ new Date()).toISOString();
4217
+ const ts = isoToFsSafe2(isoTs);
4218
+ const snapshotRoot = path26.join(teamixDir, SNAPSHOTS_DIR);
4219
+ const target = path26.join(snapshotRoot, ts);
4220
+ await fs20.mkdir(target, { recursive: true });
4221
+ const entries = await fs20.readdir(teamixDir, { withFileTypes: true });
4222
+ for (const entry of entries) {
4223
+ if (entry.name === SNAPSHOTS_DIR) continue;
4224
+ if (entry.name === LOGS_DIR) continue;
4225
+ const src = path26.join(teamixDir, entry.name);
4226
+ const dst = path26.join(target, entry.name);
4227
+ await fs20.cp(src, dst, { recursive: true });
4228
+ }
4229
+ const meta = {
4230
+ ts: isoTs,
4231
+ reason: opts.reason ?? "manual"
4232
+ };
4233
+ await fs20.writeFile(
4234
+ path26.join(target, META_FILE),
4235
+ JSON.stringify(meta, null, 2) + "\n",
4236
+ "utf-8"
4237
+ );
4238
+ logger.debug(
4239
+ `Snapshot created \u2192 ${path26.relative(projectRoot, target)} (${meta.reason})`
4240
+ );
4241
+ const keep = opts.keep ?? DEFAULT_KEEP;
4242
+ await pruneSnapshots(projectRoot, keep, { protectedTs: opts.protectedTs });
4243
+ return { ts, path: target };
4244
+ }
4245
+ async function listSnapshots(projectRoot) {
4246
+ const snapshotRoot = path26.join(projectRoot, TEAMIX_DIR3, SNAPSHOTS_DIR);
4247
+ let entries;
4248
+ try {
4249
+ entries = await fs20.readdir(snapshotRoot, { withFileTypes: true });
4250
+ } catch (err) {
4251
+ if (err.code === "ENOENT") return [];
4252
+ throw err;
4253
+ }
4254
+ const result = [];
4255
+ for (const entry of entries) {
4256
+ if (!entry.isDirectory()) continue;
4257
+ const dir = path26.join(snapshotRoot, entry.name);
4258
+ let isoTs = null;
4259
+ let reason = null;
4260
+ try {
4261
+ const raw = await fs20.readFile(path26.join(dir, META_FILE), "utf-8");
4262
+ const parsed = JSON.parse(raw);
4263
+ if (typeof parsed.ts === "string") isoTs = parsed.ts;
4264
+ if (typeof parsed.reason === "string" && ["init", "update", "switch", "restore", "manual"].includes(
4265
+ parsed.reason
4266
+ )) {
4267
+ reason = parsed.reason;
4268
+ }
4269
+ } catch {
4270
+ isoTs = fsSafeToIso(entry.name);
4271
+ }
4272
+ result.push({ ts: entry.name, isoTs, reason, path: dir });
4273
+ }
4274
+ result.sort((a, b) => a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
4275
+ return result;
2692
4276
  }
2693
4277
  async function pruneSnapshots(projectRoot, keep = DEFAULT_KEEP, opts = {}) {
2694
4278
  if (keep < 0)
@@ -2699,14 +4283,66 @@ async function pruneSnapshots(projectRoot, keep = DEFAULT_KEEP, opts = {}) {
2699
4283
  const toRemove = opts.protectedTs ? tail.filter((s) => s.ts !== opts.protectedTs) : tail;
2700
4284
  const removed = [];
2701
4285
  for (const snap of toRemove) {
2702
- await fs14.rm(snap.path, { recursive: true, force: true });
4286
+ await fs20.rm(snap.path, { recursive: true, force: true });
2703
4287
  removed.push(snap.ts);
2704
4288
  logger.debug(`Pruned snapshot ${snap.ts}`);
2705
4289
  }
2706
4290
  return removed.reverse();
2707
4291
  }
2708
4292
 
4293
+ // src/core/file-changes.ts
4294
+ import * as fs21 from "fs/promises";
4295
+ import * as path27 from "path";
4296
+ function toRelativePosix(p, projectRoot) {
4297
+ let rel2 = p;
4298
+ if (path27.isAbsolute(p)) {
4299
+ rel2 = path27.relative(projectRoot, p);
4300
+ }
4301
+ return rel2.split(path27.sep).join("/");
4302
+ }
4303
+ async function listBackupOriginals(projectRoot) {
4304
+ const backupsDir = path27.join(projectRoot, ".teamix-evo", ".backups");
4305
+ const out = /* @__PURE__ */ new Set();
4306
+ const stack = [backupsDir];
4307
+ while (stack.length > 0) {
4308
+ const dir = stack.pop();
4309
+ let entries;
4310
+ try {
4311
+ entries = await fs21.readdir(dir, { withFileTypes: true });
4312
+ } catch (err) {
4313
+ if (err.code === "ENOENT") continue;
4314
+ throw err;
4315
+ }
4316
+ for (const e of entries) {
4317
+ const full = path27.join(dir, e.name);
4318
+ if (e.isDirectory()) {
4319
+ stack.push(full);
4320
+ } else if (e.isFile() && e.name.endsWith(".bak")) {
4321
+ const rel2 = path27.relative(backupsDir, full);
4322
+ const original = stripBackupSuffix(rel2);
4323
+ if (original) out.add(original.split(path27.sep).join("/"));
4324
+ }
4325
+ }
4326
+ }
4327
+ return out;
4328
+ }
4329
+ function stripBackupSuffix(rel2) {
4330
+ const m = rel2.match(
4331
+ /^(.+)\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.bak$/
4332
+ );
4333
+ return m?.[1] ?? null;
4334
+ }
4335
+ function diffBackupSet(before, after) {
4336
+ const out = /* @__PURE__ */ new Set();
4337
+ for (const p of after) {
4338
+ if (!before.has(p)) out.add(p);
4339
+ }
4340
+ return out;
4341
+ }
4342
+
2709
4343
  // src/core/project-init.ts
4344
+ import * as fsNode from "fs/promises";
4345
+ import * as path28 from "path";
2710
4346
  var BASELINE_UI_ENTRIES = [
2711
4347
  "button",
2712
4348
  "button-group",
@@ -2728,12 +4364,19 @@ var CRITICAL_STEPS = /* @__PURE__ */ new Set([
2728
4364
  "ui-init"
2729
4365
  ]);
2730
4366
  var IMPLEMENTED_STRATEGIES = {
4367
+ // 'agents-md': both 'merge-managed' (Phase 2.B — splice the
4368
+ // teamix-evo-skills managed region) and 'overwrite' (full rewrite) write
4369
+ // through `runGenerateAgentsMd`.
2731
4370
  "agents-md": ["merge-managed", "overwrite", "skip"],
2732
4371
  tokens: ["migrate", "overwrite", "skip"],
2733
4372
  "components-json": ["overwrite", "skip"],
2734
4373
  "shadcn-source": ["overwrite", "skip-existing", "skip"],
2735
4374
  "tailwind-config": ["skip"],
2736
- "index-css": ["skip"]
4375
+ "index-css": ["skip"],
4376
+ // Phase 3.E: lint conflict strategies are honored end-to-end by
4377
+ // `runLintInit` (backup user file + write template, optional AI-assist hint).
4378
+ "eslint-config": ["merge", "backup-overwrite", "skip", "overwrite"],
4379
+ "stylelint-config": ["merge", "backup-overwrite", "skip", "overwrite"]
2737
4380
  };
2738
4381
  function pickIde(ides) {
2739
4382
  return ides[0] ?? "qoder";
@@ -2751,11 +4394,75 @@ async function resolveUiEntries(options) {
2751
4394
  }
2752
4395
  return [...BASELINE_UI_ENTRIES];
2753
4396
  }
4397
+ function deriveTokensChanges(result, projectRoot) {
4398
+ if (result.status !== "installed") return [];
4399
+ return result.resources.map((r) => ({
4400
+ kind: "created",
4401
+ path: toRelativePosix(r.target, projectRoot),
4402
+ step: "tokens",
4403
+ detail: r.strategy
4404
+ }));
4405
+ }
4406
+ function deriveSkillsChanges(result, projectRoot) {
4407
+ if (result.status !== "installed") return [];
4408
+ return result.addedSkillIds.map((id) => ({
4409
+ kind: "created",
4410
+ path: `.teamix-evo/skills/${id}/SKILL.md`,
4411
+ step: "skills",
4412
+ detail: "skill installed (source mirror + IDE mirrors)"
4413
+ }));
4414
+ }
4415
+ function deriveUiAddChanges(result, projectRoot) {
4416
+ const out = [];
4417
+ let remaining = result.written;
4418
+ for (let i = result.resources.length - 1; i >= 0 && remaining > 0; i--) {
4419
+ const r = result.resources[i];
4420
+ out.unshift({
4421
+ kind: "created",
4422
+ path: toRelativePosix(r.target, projectRoot),
4423
+ step: "ui-add",
4424
+ detail: r.strategy
4425
+ });
4426
+ remaining--;
4427
+ }
4428
+ return out;
4429
+ }
4430
+ function deriveLintChanges(result) {
4431
+ if (result.status !== "installed") return [];
4432
+ const out = [];
4433
+ if (result.eslint) {
4434
+ out.push({
4435
+ kind: "created",
4436
+ path: "eslint.config.js",
4437
+ step: "lint",
4438
+ detail: "@teamix-evo/eslint-config consumer preset"
4439
+ });
4440
+ }
4441
+ if (result.stylelint) {
4442
+ out.push({
4443
+ kind: "created",
4444
+ path: "stylelint.config.cjs",
4445
+ step: "lint",
4446
+ detail: "@teamix-evo/stylelint-config consumer preset"
4447
+ });
4448
+ }
4449
+ if (result.packageJsonPatched) {
4450
+ out.push({
4451
+ kind: "created",
4452
+ path: "package.json",
4453
+ step: "lint",
4454
+ detail: 'scripts.lint / scripts["lint:css"]'
4455
+ });
4456
+ }
4457
+ return out;
4458
+ }
2754
4459
  async function runProjectInit(options) {
2755
4460
  const { projectRoot, answers, dryRun = false, onStep } = options;
2756
4461
  const ide = pickIde(answers.ides);
2757
4462
  const steps = [];
2758
4463
  const pending = [];
4464
+ const allChanges = [];
4465
+ const backupsBefore = dryRun ? /* @__PURE__ */ new Set() : await listBackupOriginals(projectRoot).catch(() => /* @__PURE__ */ new Set());
2759
4466
  let snapshot = null;
2760
4467
  let snapshotError;
2761
4468
  if (!dryRun) {
@@ -2772,6 +4479,9 @@ async function runProjectInit(options) {
2772
4479
  function record(step) {
2773
4480
  steps.push(step);
2774
4481
  onStep?.(step);
4482
+ if (step.changes && step.changes.length > 0) {
4483
+ allChanges.push(...step.changes);
4484
+ }
2775
4485
  }
2776
4486
  function recordPending(key) {
2777
4487
  const strategy = answers.conflictDecisions[key];
@@ -2831,7 +4541,8 @@ async function runProjectInit(options) {
2831
4541
  record({
2832
4542
  name: "tokens",
2833
4543
  status: "ok",
2834
- detail
4544
+ detail,
4545
+ changes: deriveTokensChanges(result, projectRoot)
2835
4546
  });
2836
4547
  } catch (err) {
2837
4548
  recordFailure("tokens", err);
@@ -2863,7 +4574,8 @@ async function runProjectInit(options) {
2863
4574
  record({
2864
4575
  name: "skills",
2865
4576
  status: "ok",
2866
- detail: result.status === "installed" ? `added: ${result.addedSkillIds.join(", ") || "none"}; existing: ${result.skippedSkillIds.join(", ") || "none"}` : result.status
4577
+ detail: result.status === "installed" ? `added: ${result.addedSkillIds.join(", ") || "none"}; existing: ${result.skippedSkillIds.join(", ") || "none"}` : result.status,
4578
+ changes: deriveSkillsChanges(result, projectRoot)
2867
4579
  });
2868
4580
  } catch (err) {
2869
4581
  recordFailure("skills", err);
@@ -2891,12 +4603,25 @@ async function runProjectInit(options) {
2891
4603
  const result = await runGenerateAgentsMd({
2892
4604
  projectRoot,
2893
4605
  variant: answers.variant,
2894
- skillIds: agentsMdSkillIds
4606
+ skillIds: agentsMdSkillIds,
4607
+ // Phase 2.B: when the user picked `merge-managed` on conflict, only
4608
+ // rewrite the `teamix-evo-skills` managed region so hand-written
4609
+ // sections survive the regenerate step. `overwrite` keeps the
4610
+ // historical full-rewrite default.
4611
+ mode: agentsMdDecision === "merge-managed" ? "merge-managed" : "overwrite"
2895
4612
  });
2896
4613
  record({
2897
4614
  name: "agents-md",
2898
4615
  status: "ok",
2899
- detail: `${result.skillCount} skill index${result.missingSkillIds.length > 0 ? ` (missing SKILL.md: ${result.missingSkillIds.join(", ")})` : ""}`
4616
+ detail: `${result.skillCount} skill index${result.missingSkillIds.length > 0 ? ` (missing SKILL.md: ${result.missingSkillIds.join(", ")})` : ""}`,
4617
+ changes: [
4618
+ {
4619
+ kind: result.backedUp ? "modified" : "created",
4620
+ path: toRelativePosix(result.path, projectRoot),
4621
+ step: "agents-md",
4622
+ detail: "skill-trigger fallback (ADR 0038)"
4623
+ }
4624
+ ]
2900
4625
  });
2901
4626
  } catch (err) {
2902
4627
  recordFailure("agents-md", err);
@@ -2906,6 +4631,7 @@ async function runProjectInit(options) {
2906
4631
  const componentsJsonDecision = answers.conflictDecisions["components-json"];
2907
4632
  const shadcnDecision = answers.conflictDecisions["shadcn-source"];
2908
4633
  const skipUiInit = !answers.withUi || componentsJsonDecision === "skip";
4634
+ let uiDecisionRequired;
2909
4635
  if (skipUiInit) {
2910
4636
  record({
2911
4637
  name: "ui-init",
@@ -2971,18 +4697,177 @@ async function runProjectInit(options) {
2971
4697
  } else {
2972
4698
  try {
2973
4699
  const entries = await resolveUiEntries(options);
2974
- const addResult = await runUiAdd({
4700
+ const { manifest } = await loadUiData("@teamix-evo/ui");
4701
+ const existingConfig = await readProjectConfig(projectRoot).catch(
4702
+ () => null
4703
+ );
4704
+ const aliases = existingConfig?.packages?.ui?.aliases ?? DEFAULT_UI_ALIASES;
4705
+ const conflictReport = await detectUiConflicts({
2975
4706
  projectRoot,
2976
- ids: entries,
2977
- // 'overwrite' strategy → overwrite=true; everything else (incl.
2978
- // 'skip-existing' which is the default) → overwrite=false.
2979
- overwrite: shadcnDecision === "overwrite"
2980
- });
2981
- record({
2982
- name: "ui-add",
2983
- status: "ok",
2984
- detail: `${addResult.orderedIds.length} entries (${addResult.written} written, ${addResult.skipped} skipped)`
4707
+ aliases,
4708
+ manifest
2985
4709
  });
4710
+ if (conflictReport.shouldBlock) {
4711
+ let effectiveStrategy = options.uiConflictStrategy;
4712
+ if (!effectiveStrategy) {
4713
+ if (options.nonInteractive) {
4714
+ effectiveStrategy = "isolate-progressive";
4715
+ } else {
4716
+ uiDecisionRequired = {
4717
+ report: conflictReport,
4718
+ options: [
4719
+ {
4720
+ strategy: "isolate-progressive",
4721
+ label: "\u9694\u79BB + \u6E10\u8FDB\u8FC1\u79FB\uFF08\u63A8\u8350\uFF09",
4722
+ description: "\u5C06\u73B0\u6709 UI \u7EC4\u4EF6\u642C\u8FC1\u5230 shadcn-ui/ \u76EE\u5F55\uFF0C\u81EA\u52A8\u91CD\u5199 import \u8DEF\u5F84\uFF0C\u843D\u5730\u5168\u65B0 teamix-evo \u7EC4\u4EF6\u5230 ui/\uFF0C\u6309\u81EA\u5DF1\u8282\u594F\u9010\u6B65\u8FC1\u79FB\u3002"
4723
+ },
4724
+ {
4725
+ strategy: "isolate-aggressive",
4726
+ label: "\u9694\u79BB + \u4E3B\u52A8\u5347\u7EA7",
4727
+ description: "\u5728\u6E10\u8FDB\u8FC1\u79FB\u57FA\u7840\u4E0A\uFF0C\u7ACB\u5373\u751F\u6210\u5347\u7EA7 staging\uFF0C\u7531 teamix-evo-upgrade skill \u5F15\u5BFC\u5206\u6279\u66FF\u6362\u3002"
4728
+ },
4729
+ {
4730
+ strategy: "frozen-skip",
4731
+ label: "\u8DF3\u8FC7\u5DF2\u6709\u6587\u4EF6\uFF08\u65E7\u6A21\u5F0F\uFF09",
4732
+ description: "\u4FDD\u6301\u73B0\u6709\u884C\u4E3A\uFF1A\u5DF2\u6709\u7EC4\u4EF6\u6587\u4EF6\u4E0D\u8986\u76D6\uFF0C\u65B0\u7EC4\u4EF6\u6B63\u5E38\u5B89\u88C5\u3002\u4E0D\u63A8\u8350 \u2014\u2014 \u53EF\u80FD\u5BFC\u81F4\u98CE\u683C\u6DF7\u7528\u3002"
4733
+ }
4734
+ ]
4735
+ };
4736
+ record({
4737
+ name: "ui-add",
4738
+ status: "skip",
4739
+ detail: `UI conflict detected (${conflictReport.conflictEntries.length} files), awaiting user decision`
4740
+ });
4741
+ effectiveStrategy = void 0;
4742
+ }
4743
+ }
4744
+ if (effectiveStrategy) {
4745
+ if (effectiveStrategy === "frozen-skip") {
4746
+ const addResult = await runUiAdd({
4747
+ projectRoot,
4748
+ ids: entries,
4749
+ overwrite: shadcnDecision === "overwrite"
4750
+ });
4751
+ record({
4752
+ name: "ui-add",
4753
+ status: "ok",
4754
+ detail: `${addResult.orderedIds.length} entries [frozen-skip] (${addResult.written} written, ${addResult.skipped} skipped)`,
4755
+ changes: deriveUiAddChanges(addResult, projectRoot)
4756
+ });
4757
+ } else {
4758
+ const isolateResult = await runUiIsolate({
4759
+ projectRoot,
4760
+ aliases,
4761
+ conflictReport
4762
+ });
4763
+ logger.info(
4764
+ ` isolated ${isolateResult.movedFiles.length} files, rewrote imports in ${isolateResult.importRewrites.size} files`
4765
+ );
4766
+ const addResult = await runUiAdd({
4767
+ projectRoot,
4768
+ ids: entries,
4769
+ overwrite: true
4770
+ // after isolation, ui/ is empty → safe to write
4771
+ });
4772
+ let stagingDetail = "";
4773
+ if (effectiveStrategy === "isolate-aggressive") {
4774
+ try {
4775
+ const upgradeResult = await runUiUpgrade({
4776
+ projectRoot,
4777
+ category: "ui",
4778
+ trigger: "ui-upgrade"
4779
+ });
4780
+ if (upgradeResult.status === "staged") {
4781
+ stagingDetail = `; upgrade staging generated (${upgradeResult.manifest.entries.length} entries)`;
4782
+ }
4783
+ } catch (upgradeErr) {
4784
+ stagingDetail = `; upgrade staging failed: ${getErrorMessage(
4785
+ upgradeErr
4786
+ )}`;
4787
+ }
4788
+ }
4789
+ record({
4790
+ name: "ui-add",
4791
+ status: "ok",
4792
+ detail: `${addResult.orderedIds.length} entries [${effectiveStrategy}] (${addResult.written} written, ${addResult.skipped} skipped; isolated ${isolateResult.movedFiles.length} files${stagingDetail})`,
4793
+ changes: deriveUiAddChanges(addResult, projectRoot)
4794
+ });
4795
+ try {
4796
+ const deps = addResult.npmDependencies;
4797
+ if (deps && Object.keys(deps).length > 0) {
4798
+ await installProjectDeps({
4799
+ projectRoot,
4800
+ npmDependencies: deps,
4801
+ skipInstall: options.skipInstall
4802
+ });
4803
+ }
4804
+ } catch (depsErr) {
4805
+ logger.warn(
4806
+ ` deps install failed (non-fatal): ${getErrorMessage(
4807
+ depsErr
4808
+ )}`
4809
+ );
4810
+ }
4811
+ let residualCount = 0;
4812
+ try {
4813
+ const residualReport = await detectResidualImports(
4814
+ projectRoot
4815
+ );
4816
+ residualCount = residualReport.entries.length;
4817
+ if (residualCount > 0) {
4818
+ logger.warn(
4819
+ ` \u26A0\uFE0F ${residualCount} residual import(s) to shadcn-ui/ detected \u2014 see lint warnings`
4820
+ );
4821
+ }
4822
+ } catch {
4823
+ }
4824
+ try {
4825
+ const planPath = await generateMigrationPlan({
4826
+ projectRoot,
4827
+ strategy: effectiveStrategy,
4828
+ isolateResult,
4829
+ addResult,
4830
+ residualImports: residualCount
4831
+ });
4832
+ logger.info(
4833
+ ` wrote migration plan: ${path28.relative(
4834
+ projectRoot,
4835
+ planPath
4836
+ )}`
4837
+ );
4838
+ } catch {
4839
+ }
4840
+ }
4841
+ }
4842
+ } else {
4843
+ const addResult = await runUiAdd({
4844
+ projectRoot,
4845
+ ids: entries,
4846
+ overwrite: shadcnDecision === "overwrite"
4847
+ });
4848
+ record({
4849
+ name: "ui-add",
4850
+ status: "ok",
4851
+ detail: `${addResult.orderedIds.length} entries (${addResult.written} written, ${addResult.skipped} skipped)`,
4852
+ changes: deriveUiAddChanges(addResult, projectRoot)
4853
+ });
4854
+ try {
4855
+ const deps = addResult.npmDependencies;
4856
+ if (deps && Object.keys(deps).length > 0) {
4857
+ await installProjectDeps({
4858
+ projectRoot,
4859
+ npmDependencies: deps,
4860
+ skipInstall: options.skipInstall
4861
+ });
4862
+ }
4863
+ } catch (depsErr) {
4864
+ logger.warn(
4865
+ ` deps install failed (non-fatal): ${getErrorMessage(
4866
+ depsErr
4867
+ )}`
4868
+ );
4869
+ }
4870
+ }
2986
4871
  } catch (err) {
2987
4872
  recordFailure("ui-add", err);
2988
4873
  }
@@ -3005,771 +4890,449 @@ async function runProjectInit(options) {
3005
4890
  });
3006
4891
  } else {
3007
4892
  try {
4893
+ const eslintStrategy = answers.conflictDecisions["eslint-config"];
4894
+ const stylelintStrategy = answers.conflictDecisions["stylelint-config"];
4895
+ const eslintExistingPaths = options.legacyEslintPaths ?? [];
4896
+ const stylelintExistingPaths = options.legacyStylelintPaths ?? [];
3008
4897
  const result = await runLintInit({
3009
4898
  projectRoot,
3010
- skipInstall: options.skipInstall ?? false
3011
- });
3012
- record({
3013
- name: "lint",
3014
- status: "ok",
3015
- detail: result.status === "installed" ? `eslint=${result.eslint}, stylelint=${result.stylelint}` : result.status
3016
- });
3017
- } catch (err) {
3018
- recordFailure("lint", err);
3019
- }
3020
- }
3021
- recordPending("tailwind-config");
3022
- recordPending("index-css");
3023
- const status = dryRun ? "dry-run" : steps.some((s) => s.status === "fail") ? "partial" : "installed";
3024
- const out = {
3025
- status,
3026
- steps,
3027
- pendingConflictWork: pending,
3028
- snapshot
3029
- };
3030
- if (snapshotError) out.snapshotError = snapshotError;
3031
- if (firstFailure.value) {
3032
- out.resumeHint = {
3033
- failedAt: firstFailure.value.step,
3034
- completed: steps.filter((s) => s.status === "ok").map((s) => s.name),
3035
- failed: steps.filter((s) => s.status === "fail").map((s) => s.name),
3036
- error: firstFailure.value.error,
3037
- // Every sub-step is idempotent (returns `already-initialized` when its
3038
- // state file already exists), so a plain re-run resumes from the
3039
- // failed step. A richer --resume model lands with batch 3 (lock
3040
- // snapshot + restore).
3041
- resumeCommand: "teamix-evo init"
3042
- };
3043
- }
3044
- return out;
3045
- }
3046
-
3047
- // src/core/project-update.ts
3048
- import * as path24 from "path";
3049
- import {
3050
- loadTokensPackageManifest as loadTokensPackageManifest3,
3051
- getVariantEntry as getVariantEntry3
3052
- } from "@teamix-evo/registry";
3053
-
3054
- // src/core/tokens-update.ts
3055
- import * as path20 from "path";
3056
- import * as fs15 from "fs/promises";
3057
- import {
3058
- loadTokensPackageManifest as loadTokensPackageManifest2,
3059
- getVariantEntry as getVariantEntry2
3060
- } from "@teamix-evo/registry";
3061
-
3062
- // src/core/upgrade-hints.ts
3063
- import * as path19 from "path";
3064
- var TEAMIX_DIR3 = ".teamix-evo";
3065
- var HINTS_DIR = ".upgrade-hints";
3066
- function isoToFsSafe2(iso) {
3067
- return iso.replace(/[:.]/g, "-");
3068
- }
3069
- async function writeTokensUpgradeHint(options) {
3070
- if (options.renames.length === 0) return null;
3071
- const isoTs = options.isoTs ?? (/* @__PURE__ */ new Date()).toISOString();
3072
- const fsTs = isoToFsSafe2(isoTs);
3073
- const filename = `tokens-${fsTs}.json`;
3074
- const target = path19.join(
3075
- options.projectRoot,
3076
- TEAMIX_DIR3,
3077
- HINTS_DIR,
3078
- filename
3079
- );
3080
- const payload = {
3081
- schemaVersion: 1,
3082
- ts: isoTs,
3083
- package: "tokens",
3084
- trigger: options.trigger,
3085
- fromVariant: options.fromVariant,
3086
- toVariant: options.toVariant,
3087
- fromVersion: options.fromVersion,
3088
- toVersion: options.toVersion,
3089
- renames: options.renames
3090
- };
3091
- await writeFileSafe(target, JSON.stringify(payload, null, 2) + "\n");
3092
- return {
3093
- path: target,
3094
- ts: fsTs,
3095
- renameCount: options.renames.length
3096
- };
3097
- }
3098
- function selectApplicableRenames(renames, fromVersion, toVersion) {
3099
- return renames.filter(
3100
- (r) => compareSemver(r.sinceVersion, fromVersion) > 0 && compareSemver(r.sinceVersion, toVersion) <= 0
3101
- ).sort((a, b) => compareSemver(a.sinceVersion, b.sinceVersion));
3102
- }
3103
- function compareSemver(a, b) {
3104
- const [aMain = "", aRest = ""] = a.split("-", 2);
3105
- const [bMain = "", bRest = ""] = b.split("-", 2);
3106
- const aParts = aMain.split(".").map((n) => Number.parseInt(n, 10));
3107
- const bParts = bMain.split(".").map((n) => Number.parseInt(n, 10));
3108
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
3109
- const ai = aParts[i] ?? 0;
3110
- const bi = bParts[i] ?? 0;
3111
- if (ai !== bi) return ai - bi;
3112
- }
3113
- if (aRest === "" && bRest !== "") return 1;
3114
- if (aRest !== "" && bRest === "") return -1;
3115
- return aRest.localeCompare(bRest, void 0, { numeric: true });
3116
- }
3117
-
3118
- // src/core/managed-merge.ts
3119
- import { hasManagedRegion as hasManagedRegion2, replaceManagedRegion as replaceManagedRegion2 } from "@teamix-evo/registry";
3120
- function mergeManagedRegions(upstreamContent, consumerContent) {
3121
- let updated = consumerContent;
3122
- const re = /<!-- teamix-evo:managed:start id="([^"]+)" -->([\s\S]*?)<!-- teamix-evo:managed:end(?: id="\1")? -->/g;
3123
- let match;
3124
- while ((match = re.exec(upstreamContent)) !== null) {
3125
- const id = match[1];
3126
- const body = match[2].replace(/^\n/, "").replace(/\n$/, "");
3127
- if (!hasManagedRegion2(updated, id)) {
3128
- throw new Error(
3129
- `Managed region "${id}" missing from consumer file \u2014 refusing to silently rewrite (ADR 0003).`
3130
- );
3131
- }
3132
- updated = replaceManagedRegion2(updated, id, body);
3133
- }
3134
- return updated;
3135
- }
3136
-
3137
- // src/core/tokens-update.ts
3138
- var DEFAULT_TOKENS_PACKAGE2 = "@teamix-evo/tokens";
3139
- var CONSUMER_BASENAME_BY_UPSTREAM = {
3140
- "theme.css": "tokens.theme.css",
3141
- "overrides.css": "tokens.overrides.css"
3142
- };
3143
- async function runTokensUpdate(options) {
3144
- const { projectRoot } = options;
3145
- const packageName = options.packageName ?? DEFAULT_TOKENS_PACKAGE2;
3146
- const config = await readProjectConfig(projectRoot);
3147
- if (!config?.packages?.tokens) {
3148
- return { status: "not-initialized" };
3149
- }
3150
- const currentVariant = config.packages.tokens.variant;
3151
- const currentVersion = config.packages.tokens.version;
3152
- const packageRoot = options.packageRoot ?? resolveTokensPackageRoot(packageName);
3153
- const catalog = await loadTokensPackageManifest2(packageRoot);
3154
- const variantEntry = getVariantEntry2(catalog, currentVariant);
3155
- if (!variantEntry) {
3156
- throw new Error(
3157
- `Currently installed variant "${currentVariant}" no longer exists in ${packageName}@${catalog.version}. Available: ${catalog.variants.map((v) => v.name).join(", ")}. Run \`npx teamix-evo@latest tokens uninstall\` then \`npx teamix-evo@latest tokens init <variant>\` to switch.`
3158
- );
3159
- }
3160
- const upstreamByBasename = /* @__PURE__ */ new Map();
3161
- for (const fileRel of variantEntry.files) {
3162
- upstreamByBasename.set(
3163
- path20.basename(fileRel),
3164
- path20.join(packageRoot, fileRel)
3165
- );
3166
- }
3167
- const prior = await readInstalledManifest(projectRoot) ?? {
3168
- schemaVersion: 1,
3169
- installed: []
3170
- };
3171
- const installedIdx = prior.installed.findIndex(
3172
- (p) => p.package === packageName
3173
- );
3174
- const priorResources = installedIdx >= 0 ? prior.installed[installedIdx].resources : [];
3175
- const rewritten = [];
3176
- const managedReplaced = [];
3177
- const preserved = [];
3178
- const frozenDrift = [];
3179
- const refreshedResources = [];
3180
- for (const resource of priorResources) {
3181
- const consumerAbs = path20.isAbsolute(resource.target) ? resource.target : path20.join(projectRoot, resource.target);
3182
- const consumerBasename = path20.basename(resource.target);
3183
- const upstreamBasename = lookupUpstreamBasename(consumerBasename);
3184
- const upstreamAbs = upstreamBasename ? upstreamByBasename.get(upstreamBasename) : void 0;
3185
- if (resource.strategy === "regenerable") {
3186
- if (!upstreamAbs) {
3187
- refreshedResources.push(resource);
3188
- continue;
3189
- }
3190
- const content = await fs15.readFile(upstreamAbs, "utf-8");
3191
- await writeFileSafe(consumerAbs, content);
3192
- rewritten.push(resource.target);
3193
- refreshedResources.push({
3194
- ...resource,
3195
- hash: computeHash(content)
4899
+ skipInstall: options.skipInstall ?? false,
4900
+ eslintStrategy: eslintStrategy === "merge" || eslintStrategy === "backup-overwrite" || eslintStrategy === "skip" || eslintStrategy === "overwrite" ? eslintStrategy : "overwrite",
4901
+ stylelintStrategy: stylelintStrategy === "merge" || stylelintStrategy === "backup-overwrite" || stylelintStrategy === "skip" || stylelintStrategy === "overwrite" ? stylelintStrategy : "overwrite",
4902
+ eslintExistingPaths,
4903
+ stylelintExistingPaths
3196
4904
  });
3197
- continue;
3198
- }
3199
- if (resource.strategy === "managed") {
3200
- if (!upstreamAbs || !await fileExists(consumerAbs)) {
3201
- refreshedResources.push(resource);
3202
- continue;
3203
- }
3204
- const upstreamContent = await fs15.readFile(upstreamAbs, "utf-8");
3205
- const consumerContent = await fs15.readFile(consumerAbs, "utf-8");
3206
- const merged = mergeManagedRegions(upstreamContent, consumerContent);
3207
- if (merged !== consumerContent) {
3208
- await writeFileSafe(consumerAbs, merged);
3209
- managedReplaced.push(resource.target);
4905
+ const detailParts = [];
4906
+ if (result.status === "installed") {
4907
+ detailParts.push(
4908
+ `eslint=${result.eslint}, stylelint=${result.stylelint}`
4909
+ );
4910
+ if (result.eslintMergeRequested) {
4911
+ detailParts.push("eslint:AI-merge-pending");
4912
+ }
4913
+ if (result.stylelintMergeRequested) {
4914
+ detailParts.push("stylelint:AI-merge-pending");
4915
+ }
4916
+ if (result.eslintSkipped) detailParts.push("eslint:skipped");
4917
+ if (result.stylelintSkipped) detailParts.push("stylelint:skipped");
4918
+ if (result.stylelintIgnoreFilesWarning) {
4919
+ detailParts.push("stylelint:ignoreFiles-warning");
4920
+ }
4921
+ } else {
4922
+ detailParts.push(result.status);
3210
4923
  }
3211
- refreshedResources.push({
3212
- ...resource,
3213
- hash: computeHash(merged)
4924
+ record({
4925
+ name: "lint",
4926
+ status: "ok",
4927
+ detail: detailParts.join(" / "),
4928
+ changes: deriveLintChanges(result)
3214
4929
  });
3215
- continue;
4930
+ } catch (err) {
4931
+ recordFailure("lint", err);
3216
4932
  }
3217
- if (await fileExists(consumerAbs)) preserved.push(resource.target);
3218
- if (upstreamAbs) {
3219
- const upstreamContent = await fs15.readFile(upstreamAbs, "utf-8");
3220
- const upstreamHash = computeHash(upstreamContent);
3221
- if (resource.hash && upstreamHash !== resource.hash) {
3222
- frozenDrift.push({
3223
- target: resource.target,
3224
- reason: "upstream-changed"
4933
+ }
4934
+ recordPending("tailwind-config");
4935
+ recordPending("index-css");
4936
+ const GITIGNORE_MARKER_START = "# >>> teamix-evo:managed >>>";
4937
+ const GITIGNORE_MARKER_END = "# <<< teamix-evo:managed <<<";
4938
+ const GITIGNORE_RULES = [
4939
+ ".teamix-evo/.backups/",
4940
+ ".teamix-evo/.upgrade-staging/",
4941
+ ".teamix-evo/.upgrade-hints/"
4942
+ ];
4943
+ if (dryRun) {
4944
+ record({
4945
+ name: "gitignore",
4946
+ status: "planned",
4947
+ detail: "append teamix-evo runtime artifact rules"
4948
+ });
4949
+ } else {
4950
+ try {
4951
+ try {
4952
+ await fsNode.access(projectRoot);
4953
+ } catch {
4954
+ record({
4955
+ name: "gitignore",
4956
+ status: "skip",
4957
+ detail: "projectRoot does not exist"
4958
+ });
4959
+ throw { __skip: true };
4960
+ }
4961
+ const giPath = path28.join(projectRoot, ".gitignore");
4962
+ let giContent = "";
4963
+ try {
4964
+ giContent = await fsNode.readFile(giPath, "utf-8");
4965
+ } catch {
4966
+ }
4967
+ if (giContent.includes(GITIGNORE_MARKER_START)) {
4968
+ record({
4969
+ name: "gitignore",
4970
+ status: "skip",
4971
+ detail: "teamix-evo markers already present"
4972
+ });
4973
+ } else {
4974
+ const block = [
4975
+ "",
4976
+ GITIGNORE_MARKER_START,
4977
+ ...GITIGNORE_RULES,
4978
+ GITIGNORE_MARKER_END,
4979
+ ""
4980
+ ].join("\n");
4981
+ const separator = giContent.length > 0 && !giContent.endsWith("\n") ? "\n" : "";
4982
+ await fsNode.writeFile(giPath, giContent + separator + block, "utf-8");
4983
+ record({
4984
+ name: "gitignore",
4985
+ status: "ok",
4986
+ detail: GITIGNORE_RULES.length + " rules appended",
4987
+ changes: [
4988
+ {
4989
+ kind: "modified",
4990
+ path: ".gitignore",
4991
+ step: "gitignore",
4992
+ detail: "teamix-evo runtime artifact rules"
4993
+ }
4994
+ ]
3225
4995
  });
3226
4996
  }
4997
+ } catch (err) {
4998
+ if (err && typeof err === "object" && "__skip" in err) {
4999
+ } else {
5000
+ recordFailure("gitignore", err);
5001
+ }
3227
5002
  }
3228
- refreshedResources.push(resource);
3229
5003
  }
3230
- if (variantEntry.version === currentVersion) {
3231
- if (installedIdx >= 0) {
3232
- prior.installed[installedIdx] = {
3233
- ...prior.installed[installedIdx],
3234
- resources: refreshedResources
3235
- };
3236
- await writeInstalledManifest(projectRoot, prior);
5004
+ if (!dryRun) {
5005
+ try {
5006
+ const backupsAfter = await listBackupOriginals(projectRoot);
5007
+ const newlyBackedUp = diffBackupSet(backupsBefore, backupsAfter);
5008
+ if (newlyBackedUp.size > 0) {
5009
+ for (const change of allChanges) {
5010
+ if (change.kind === "created" && newlyBackedUp.has(change.path)) {
5011
+ change.kind = "modified";
5012
+ }
5013
+ }
5014
+ for (const rel2 of newlyBackedUp) {
5015
+ allChanges.push({
5016
+ kind: "backed-up",
5017
+ path: rel2,
5018
+ step: "backup",
5019
+ detail: ".teamix-evo/.backups/<\u540C\u8DEF\u5F84>.<isoTs>.bak"
5020
+ });
5021
+ }
5022
+ }
5023
+ } catch {
3237
5024
  }
3238
- return {
3239
- status: "up-to-date",
3240
- packageName,
3241
- variant: currentVariant,
3242
- version: currentVersion,
3243
- frozenDrift
3244
- };
3245
5025
  }
3246
- const lock = {
3247
- schemaVersion: 1,
3248
- variant: {
3249
- name: variantEntry.name,
3250
- displayName: variantEntry.displayName,
3251
- version: variantEntry.version,
3252
- from: packageName
3253
- },
3254
- packageVersion: catalog.version,
3255
- linked: variantEntry.linked,
3256
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
5026
+ const status = dryRun ? "dry-run" : steps.some((s) => s.status === "fail") ? "partial" : "installed";
5027
+ const out = {
5028
+ status,
5029
+ steps,
5030
+ pendingConflictWork: pending,
5031
+ changes: allChanges,
5032
+ snapshot
3257
5033
  };
3258
- await writeFileSafe(
3259
- path20.join(projectRoot, ".teamix-evo", "tokens-lock.json"),
3260
- JSON.stringify(lock, null, 2) + "\n"
3261
- );
3262
- config.packages.tokens.version = variantEntry.version;
3263
- await writeProjectConfig(projectRoot, config);
3264
- if (installedIdx >= 0) {
3265
- prior.installed[installedIdx] = {
3266
- ...prior.installed[installedIdx],
3267
- version: variantEntry.version,
3268
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
3269
- resources: refreshedResources
5034
+ if (snapshotError) out.snapshotError = snapshotError;
5035
+ if (uiDecisionRequired) out.uiDecisionRequired = uiDecisionRequired;
5036
+ if (firstFailure.value) {
5037
+ out.resumeHint = {
5038
+ failedAt: firstFailure.value.step,
5039
+ completed: steps.filter((s) => s.status === "ok").map((s) => s.name),
5040
+ failed: steps.filter((s) => s.status === "fail").map((s) => s.name),
5041
+ error: firstFailure.value.error,
5042
+ // Every sub-step is idempotent (returns `already-initialized` when its
5043
+ // state file already exists), so a plain re-run resumes from the
5044
+ // failed step. A richer --resume model lands with batch 3 (lock
5045
+ // snapshot + restore).
5046
+ resumeCommand: "teamix-evo init"
3270
5047
  };
3271
- await writeInstalledManifest(projectRoot, prior);
3272
- }
3273
- const renames = selectApplicableRenames(
3274
- variantEntry.renames ?? [],
3275
- currentVersion,
3276
- variantEntry.version
3277
- );
3278
- let hintPath;
3279
- if (renames.length > 0) {
3280
- const hint = await writeTokensUpgradeHint({
3281
- projectRoot,
3282
- trigger: "update",
3283
- fromVariant: currentVariant,
3284
- toVariant: currentVariant,
3285
- fromVersion: currentVersion,
3286
- toVersion: variantEntry.version,
3287
- renames
3288
- });
3289
- if (hint) hintPath = hint.path;
3290
5048
  }
3291
- return {
3292
- status: "updated",
3293
- packageName,
3294
- variant: currentVariant,
3295
- from: currentVersion,
3296
- to: variantEntry.version,
3297
- rewritten,
3298
- managedReplaced,
3299
- preserved,
3300
- frozenDrift,
3301
- renames,
3302
- ...hintPath ? { hintPath } : {}
3303
- };
3304
- }
3305
- function lookupUpstreamBasename(consumerBasename) {
3306
- for (const [upstream, consumer] of Object.entries(
3307
- CONSUMER_BASENAME_BY_UPSTREAM
3308
- )) {
3309
- if (consumer === consumerBasename) return upstream;
5049
+ if (!dryRun) {
5050
+ try {
5051
+ const checklistContent = renderInitChecklist({
5052
+ variant: answers.variant,
5053
+ status,
5054
+ steps
5055
+ });
5056
+ const checklistPath = path28.join(
5057
+ projectRoot,
5058
+ ".teamix-evo",
5059
+ "init-checklist.md"
5060
+ );
5061
+ await fsNode.mkdir(path28.dirname(checklistPath), { recursive: true });
5062
+ await fsNode.writeFile(checklistPath, checklistContent, "utf-8");
5063
+ logger.info(" wrote .teamix-evo/init-checklist.md");
5064
+ } catch {
5065
+ logger.warn(" failed to write init-checklist.md (non-fatal)");
5066
+ }
3310
5067
  }
3311
- return consumerBasename;
5068
+ return out;
3312
5069
  }
3313
5070
 
3314
- // src/core/ui-upgrade-detector.ts
3315
- import * as fs16 from "fs/promises";
3316
- import * as path21 from "path";
3317
- var PACKAGE_NAME = {
3318
- ui: "@teamix-evo/ui",
3319
- "biz-ui": "@teamix-evo/biz-ui"
3320
- };
3321
- var ALIAS_KEY = {
3322
- ui: "components",
3323
- "biz-ui": "business"
3324
- };
3325
- var COMPONENT_FILE_RE = /\.(tsx|ts)$/;
3326
- var SKIP_FILENAMES = /* @__PURE__ */ new Set(["index.ts", "index.tsx"]);
3327
- async function detectComponentLineage(options) {
3328
- const { projectRoot, category } = options;
3329
- const config = options.config ?? await readProjectConfig(projectRoot);
3330
- const installed = options.installed ?? await readInstalledManifest(projectRoot);
3331
- const installDir = resolveInstallDir(category, config);
3332
- const installDirAbs = path21.join(projectRoot, installDir);
3333
- const installDirExists = await directoryExists(installDirAbs);
3334
- const hasComponentsJson = await fileExists(
3335
- path21.join(projectRoot, "components.json")
3336
- );
3337
- const installedPkg = findInstalledPackage(installed, PACKAGE_NAME[category]);
3338
- const registeredIds = installedPkg ? extractIds(installedPkg).sort() : [];
3339
- const onDiskIds = installDirExists ? await listComponentIds(installDirAbs) : [];
3340
- const registeredSet = new Set(registeredIds);
3341
- const unregisteredIds = onDiskIds.filter((id) => !registeredSet.has(id)).sort();
3342
- const lineage = classifyLineage({
3343
- hasInstalled: installedPkg !== null,
3344
- hasComponentsJson,
3345
- onDiskIds,
3346
- unregisteredIds
3347
- });
3348
- return {
3349
- category,
3350
- lineage,
3351
- installDir,
3352
- installDirExists,
3353
- hasComponentsJson,
3354
- registeredIds,
3355
- unregisteredIds,
3356
- installedVersion: installedPkg?.version ?? null,
3357
- installedVariant: installedPkg?.variant ?? null
3358
- };
3359
- }
3360
- function resolveInstallDir(category, config) {
3361
- const aliasMap = config?.packages?.ui?.aliases ?? config?.packages?.["biz-ui"]?.aliases ?? DEFAULT_UI_ALIASES;
3362
- const key = ALIAS_KEY[category];
3363
- return aliasMap[key] ?? DEFAULT_UI_ALIASES[key];
3364
- }
3365
- function extractIds(pkg) {
3366
- const ids = /* @__PURE__ */ new Set();
3367
- for (const r of pkg.resources) {
3368
- const colon = r.id.indexOf(":");
3369
- ids.add(colon >= 0 ? r.id.slice(0, colon) : r.id);
3370
- }
3371
- return [...ids];
3372
- }
3373
- async function directoryExists(p) {
3374
- try {
3375
- const stat5 = await fs16.stat(p);
3376
- return stat5.isDirectory();
3377
- } catch {
3378
- return false;
3379
- }
3380
- }
3381
- async function listComponentIds(installDirAbs) {
3382
- const entries = await fs16.readdir(installDirAbs, { withFileTypes: true });
3383
- const ids = [];
3384
- for (const e of entries) {
3385
- if (!e.isFile()) continue;
3386
- if (SKIP_FILENAMES.has(e.name)) continue;
3387
- if (!COMPONENT_FILE_RE.test(e.name)) continue;
3388
- ids.push(e.name.replace(COMPONENT_FILE_RE, ""));
3389
- }
3390
- return ids.sort();
3391
- }
3392
- function classifyLineage(args) {
3393
- const { hasInstalled, hasComponentsJson, onDiskIds, unregisteredIds } = args;
3394
- if (hasInstalled) {
3395
- return unregisteredIds.length === 0 ? "teamix-evo" : "mixed";
3396
- }
3397
- if (onDiskIds.length === 0) return "absent";
3398
- return hasComponentsJson ? "shadcn-native" : "custom-only";
3399
- }
5071
+ // src/core/project-update.ts
5072
+ import * as path31 from "path";
5073
+ import {
5074
+ loadTokensPackageManifest as loadTokensPackageManifest3,
5075
+ getVariantEntry as getVariantEntry3
5076
+ } from "@teamix-evo/registry";
3400
5077
 
3401
- // src/core/ui-upgrade.ts
3402
- import * as path23 from "path";
3403
- import { createRequire as createRequire5 } from "module";
5078
+ // src/core/tokens-update.ts
5079
+ import * as path30 from "path";
5080
+ import * as fs22 from "fs/promises";
3404
5081
  import {
3405
- loadUiPackageManifest as loadUiPackageManifest3,
3406
- loadVariantUiPackageManifest as loadVariantUiPackageManifest2
5082
+ loadTokensPackageManifest as loadTokensPackageManifest2,
5083
+ getVariantEntry as getVariantEntry2
3407
5084
  } from "@teamix-evo/registry";
3408
5085
 
3409
- // src/core/ui-upgrade-staging.ts
3410
- import * as path22 from "path";
5086
+ // src/core/upgrade-hints.ts
5087
+ import * as path29 from "path";
3411
5088
  var TEAMIX_DIR4 = ".teamix-evo";
3412
- var STAGING_DIR = ".upgrade-staging";
3413
- var PACKAGE_NAME2 = {
3414
- ui: "@teamix-evo/ui",
3415
- "biz-ui": "@teamix-evo/biz-ui"
3416
- };
5089
+ var HINTS_DIR = ".upgrade-hints";
3417
5090
  function isoToFsSafe3(iso) {
3418
5091
  return iso.replace(/[:.]/g, "-");
3419
5092
  }
3420
- async function buildUiUpgradeStaging(options) {
3421
- const { lineageReport, category } = options;
3422
- if (lineageReport.lineage !== "teamix-evo" && lineageReport.lineage !== "mixed") {
3423
- return null;
3424
- }
3425
- const installed = options.installed ?? await readInstalledManifest(options.projectRoot);
3426
- const installedPkg = findInstalledPackage(installed, PACKAGE_NAME2[category]);
3427
- if (!installedPkg) return null;
5093
+ async function writeTokensUpgradeHint(options) {
5094
+ if (options.renames.length === 0) return null;
3428
5095
  const isoTs = options.isoTs ?? (/* @__PURE__ */ new Date()).toISOString();
3429
5096
  const fsTs = isoToFsSafe3(isoTs);
3430
- const stagingDir = path22.join(
5097
+ const filename = `tokens-${fsTs}.json`;
5098
+ const target = path29.join(
3431
5099
  options.projectRoot,
3432
5100
  TEAMIX_DIR4,
3433
- STAGING_DIR,
3434
- `${category}-${fsTs}`
3435
- );
3436
- const entryMap = new Map(
3437
- options.manifest.entries.map((e) => [e.id, e])
5101
+ HINTS_DIR,
5102
+ filename
3438
5103
  );
3439
- const resByEntryId = collectResourcesByEntry(installedPkg.resources);
3440
- const onlyIds = options.onlyIds && options.onlyIds.length > 0 ? new Set(options.onlyIds) : null;
3441
- const entries = [];
3442
- for (const id of lineageReport.registeredIds) {
3443
- if (onlyIds && !onlyIds.has(id)) continue;
3444
- const built = await processRegistered({
3445
- id,
3446
- entry: entryMap.get(id),
3447
- resource: resByEntryId.get(id),
3448
- packageRoot: options.packageRoot,
3449
- entryPackageRoot: options.entryPackageRoot,
3450
- aliases: options.aliases,
3451
- stagingDir,
3452
- projectRoot: options.projectRoot,
3453
- category,
3454
- sourceVersion: options.manifest.version
3455
- });
3456
- if (built) entries.push(built);
3457
- }
3458
- for (const id of lineageReport.unregisteredIds) {
3459
- if (onlyIds && !onlyIds.has(id)) continue;
3460
- const built = await processForeign({
3461
- id,
3462
- installDirAbs: path22.join(options.projectRoot, lineageReport.installDir),
3463
- stagingDir,
3464
- projectRoot: options.projectRoot,
3465
- category
3466
- });
3467
- if (built) entries.push(built);
3468
- }
3469
- if (entries.length === 0) return null;
3470
- const byRisk = aggregateByRisk(entries);
3471
- const manifestOut = {
5104
+ const payload = {
3472
5105
  schemaVersion: 1,
3473
5106
  ts: isoTs,
3474
- package: category,
5107
+ package: "tokens",
3475
5108
  trigger: options.trigger,
3476
- variant: lineageReport.installedVariant ?? "_flat",
3477
- fromVersion: lineageReport.installedVersion ?? "",
3478
- toVersion: options.manifest.version,
3479
- lineage: lineageReport.lineage,
3480
- summary: { total: entries.length, byRisk },
3481
- entries
3482
- };
3483
- await ensureDir(stagingDir);
3484
- await writeFileSafe(
3485
- path22.join(stagingDir, "meta.json"),
3486
- JSON.stringify(manifestOut, null, 2) + "\n"
3487
- );
3488
- return { stagingDir, manifest: manifestOut };
3489
- }
3490
- async function processRegistered(args) {
3491
- const { id, entry, resource, stagingDir, projectRoot, category } = args;
3492
- if (!resource) return null;
3493
- const currentSource = await readFileOrNull(resource.target);
3494
- if (currentSource === null) {
3495
- return buildBreakingEntry({
3496
- id,
3497
- category,
3498
- resource,
3499
- projectRoot,
3500
- stagingDir,
3501
- currentSource: "",
3502
- hint: "installed file missing on disk"
3503
- });
3504
- }
3505
- if (!entry) {
3506
- return buildBreakingEntry({
3507
- id,
3508
- category,
3509
- resource,
3510
- projectRoot,
3511
- stagingDir,
3512
- currentSource,
3513
- hint: "entry removed in upstream package"
3514
- });
3515
- }
3516
- const file = entry.files[0];
3517
- if (!file) return null;
3518
- const rootForEntry = args.entryPackageRoot?.get(id) ?? args.packageRoot;
3519
- const sourceAbs = path22.resolve(rootForEntry, file.source);
3520
- const raw = await readFileOrNull(sourceAbs);
3521
- if (raw === null) {
3522
- return null;
3523
- }
3524
- const incomingTransformed = rewriteImports(raw, args.aliases);
3525
- const incomingHash = computeHash(incomingTransformed);
3526
- const currentExt = path22.extname(resource.target) || ".tsx";
3527
- const incomingExt = path22.extname(file.targetName) || currentExt;
3528
- const currentRel = `${id}/current${currentExt}`;
3529
- const incomingRel = `${id}/incoming${incomingExt}`;
3530
- await writeFileSafe(path22.join(stagingDir, currentRel), currentSource);
3531
- await writeFileSafe(path22.join(stagingDir, incomingRel), incomingTransformed);
3532
- const diff = classifyRisk({
3533
- currentHash: resource.hash,
3534
- incomingHash,
3535
- currentSource,
3536
- incomingSource: incomingTransformed,
3537
- multiFile: entry.files.length > 1
3538
- });
3539
- return {
3540
- id,
3541
- category,
3542
- current: {
3543
- target: path22.relative(projectRoot, resource.target),
3544
- hash: resource.hash,
3545
- sourceLineage: "teamix-evo"
3546
- },
3547
- incoming: {
3548
- sourceVersion: args.sourceVersion,
3549
- hash: incomingHash,
3550
- relPath: incomingRel
3551
- },
3552
- diff
5109
+ fromVariant: options.fromVariant,
5110
+ toVariant: options.toVariant,
5111
+ fromVersion: options.fromVersion,
5112
+ toVersion: options.toVersion,
5113
+ renames: options.renames
3553
5114
  };
3554
- }
3555
- async function processForeign(args) {
3556
- const { id, installDirAbs, stagingDir, projectRoot, category } = args;
3557
- const tsx = path22.join(installDirAbs, `${id}.tsx`);
3558
- const ts = path22.join(installDirAbs, `${id}.ts`);
3559
- const target = await fileExists(tsx) ? tsx : await fileExists(ts) ? ts : null;
3560
- if (!target) return null;
3561
- const raw = await readFileOrNull(target);
3562
- if (raw === null) return null;
3563
- const ext = path22.extname(target);
3564
- const currentRel = `${id}/current${ext}`;
3565
- await writeFileSafe(path22.join(stagingDir, currentRel), raw);
5115
+ await writeFileSafe(target, JSON.stringify(payload, null, 2) + "\n");
3566
5116
  return {
3567
- id,
3568
- category,
3569
- current: {
3570
- target: path22.relative(projectRoot, target),
3571
- hash: computeHash(raw),
3572
- sourceLineage: "custom"
3573
- },
3574
- diff: {
3575
- riskLevel: "foreign",
3576
- hints: [
3577
- "component is on disk but not registered in .teamix-evo/manifest.json",
3578
- "AI should propose: (a) ignore, (b) re-register via teamix-evo ui add, or (c) remove"
3579
- ],
3580
- filesChangedCount: 0
3581
- }
5117
+ path: target,
5118
+ ts: fsTs,
5119
+ renameCount: options.renames.length
3582
5120
  };
3583
5121
  }
3584
- async function buildBreakingEntry(args) {
3585
- const ext = path22.extname(args.resource.target) || ".tsx";
3586
- const currentRel = `${args.id}/current${ext}`;
3587
- await writeFileSafe(
3588
- path22.join(args.stagingDir, currentRel),
3589
- args.currentSource
3590
- );
3591
- return {
3592
- id: args.id,
3593
- category: args.category,
3594
- current: {
3595
- target: path22.relative(args.projectRoot, args.resource.target),
3596
- hash: args.resource.hash,
3597
- sourceLineage: "teamix-evo"
3598
- },
3599
- diff: {
3600
- riskLevel: "breaking",
3601
- hints: [args.hint],
3602
- filesChangedCount: 0
3603
- }
3604
- };
5122
+ function selectApplicableRenames(renames, fromVersion, toVersion) {
5123
+ return renames.filter(
5124
+ (r) => compareSemver2(r.sinceVersion, fromVersion) > 0 && compareSemver2(r.sinceVersion, toVersion) <= 0
5125
+ ).sort((a, b) => compareSemver2(a.sinceVersion, b.sinceVersion));
3605
5126
  }
3606
- function classifyRisk(args) {
3607
- if (args.currentHash === args.incomingHash) {
3608
- return { riskLevel: "unchanged", hints: [], filesChangedCount: 0 };
5127
+ function compareSemver2(a, b) {
5128
+ const [aMain = "", aRest = ""] = a.split("-", 2);
5129
+ const [bMain = "", bRest = ""] = b.split("-", 2);
5130
+ const aParts = aMain.split(".").map((n) => Number.parseInt(n, 10));
5131
+ const bParts = bMain.split(".").map((n) => Number.parseInt(n, 10));
5132
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
5133
+ const ai = aParts[i] ?? 0;
5134
+ const bi = bParts[i] ?? 0;
5135
+ if (ai !== bi) return ai - bi;
3609
5136
  }
3610
- const curExports = extractExportNames(args.currentSource);
3611
- const newExports = extractExportNames(args.incomingSource);
3612
- const removedExports = setDiff(curExports, newExports);
3613
- const addedExports = setDiff(newExports, curExports);
3614
- const curVariants = extractCvaVariantValues(args.currentSource);
3615
- const newVariants = extractCvaVariantValues(args.incomingSource);
3616
- const removedVariants = setDiff(curVariants, newVariants);
3617
- const addedVariants = setDiff(newVariants, curVariants);
3618
- const hints = [];
3619
- for (const e of removedExports) hints.push(`removed export: ${e}`);
3620
- for (const e of addedExports) hints.push(`new export: ${e}`);
3621
- for (const v of removedVariants) hints.push(`removed cva variant: ${v}`);
3622
- for (const v of addedVariants) hints.push(`new cva variant: ${v}`);
3623
- if (args.multiFile) hints.push("multi-file entry; only first file staged");
3624
- let riskLevel;
3625
- if (removedExports.length > 0 || removedVariants.length > 0) {
3626
- riskLevel = "risky";
3627
- } else if (addedExports.length > 0 || addedVariants.length > 0 || args.multiFile) {
3628
- riskLevel = "upgradable-medium";
3629
- } else {
3630
- riskLevel = "upgradable-low";
5137
+ if (aRest === "" && bRest !== "") return 1;
5138
+ if (aRest !== "" && bRest === "") return -1;
5139
+ return aRest.localeCompare(bRest, void 0, { numeric: true });
5140
+ }
5141
+
5142
+ // src/core/managed-merge.ts
5143
+ import { hasManagedRegion as hasManagedRegion3, replaceManagedRegion as replaceManagedRegion3 } from "@teamix-evo/registry";
5144
+ function mergeManagedRegions(upstreamContent, consumerContent) {
5145
+ let updated = consumerContent;
5146
+ const re = /<!-- teamix-evo:managed:start id="([^"]+)" -->([\s\S]*?)<!-- teamix-evo:managed:end(?: id="\1")? -->/g;
5147
+ let match;
5148
+ while ((match = re.exec(upstreamContent)) !== null) {
5149
+ const id = match[1];
5150
+ const body = match[2].replace(/^\n/, "").replace(/\n$/, "");
5151
+ if (!hasManagedRegion3(updated, id)) {
5152
+ throw new Error(
5153
+ `Managed region "${id}" missing from consumer file \u2014 refusing to silently rewrite (ADR 0003).`
5154
+ );
5155
+ }
5156
+ updated = replaceManagedRegion3(updated, id, body);
3631
5157
  }
3632
- return { riskLevel, hints, filesChangedCount: 1 };
5158
+ return updated;
3633
5159
  }
3634
- function extractExportNames(src) {
3635
- const names = /* @__PURE__ */ new Set();
3636
- const re = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/gm;
3637
- let m;
3638
- while ((m = re.exec(src)) !== null) {
3639
- if (m[1]) names.add(m[1]);
5160
+
5161
+ // src/core/tokens-update.ts
5162
+ var DEFAULT_TOKENS_PACKAGE2 = "@teamix-evo/tokens";
5163
+ var CONSUMER_BASENAME_BY_UPSTREAM = {
5164
+ "theme.css": "tokens.theme.css",
5165
+ "overrides.css": "tokens.overrides.css"
5166
+ };
5167
+ async function runTokensUpdate(options) {
5168
+ const { projectRoot } = options;
5169
+ const packageName = options.packageName ?? DEFAULT_TOKENS_PACKAGE2;
5170
+ const config = await readProjectConfig(projectRoot);
5171
+ if (!config?.packages?.tokens) {
5172
+ return { status: "not-initialized" };
5173
+ }
5174
+ const currentVariant = config.packages.tokens.variant;
5175
+ const currentVersion = config.packages.tokens.version;
5176
+ const packageRoot = options.packageRoot ?? resolveTokensPackageRoot(packageName);
5177
+ const catalog = await loadTokensPackageManifest2(packageRoot);
5178
+ const variantEntry = getVariantEntry2(catalog, currentVariant);
5179
+ if (!variantEntry) {
5180
+ throw new Error(
5181
+ `Currently installed variant "${currentVariant}" no longer exists in ${packageName}@${catalog.version}. Available: ${catalog.variants.map((v) => v.name).join(", ")}. Run \`npx teamix-evo@latest tokens uninstall\` then \`npx teamix-evo@latest tokens init <variant>\` to switch.`
5182
+ );
3640
5183
  }
3641
- for (const dm of src.matchAll(/^\s*export\s+default\s+(\w+)\s*;/gm)) {
3642
- if (dm[1]) names.add(dm[1]);
5184
+ const upstreamByBasename = /* @__PURE__ */ new Map();
5185
+ for (const fileRel of variantEntry.files) {
5186
+ upstreamByBasename.set(
5187
+ path30.basename(fileRel),
5188
+ path30.join(packageRoot, fileRel)
5189
+ );
3643
5190
  }
3644
- return [...names];
3645
- }
3646
- function extractCvaVariantValues(src) {
3647
- const block = extractVariantsBlock(src);
3648
- if (block === null) return [];
3649
- const names = /* @__PURE__ */ new Set();
3650
- for (const groupBody of extractGroupBodies(block)) {
3651
- for (const km of groupBody.matchAll(/^\s*(?:['"]?)(\w+)(?:['"]?)\s*:/gm)) {
3652
- if (km[1]) names.add(km[1]);
5191
+ const prior = await readInstalledManifest(projectRoot) ?? {
5192
+ schemaVersion: 1,
5193
+ installed: []
5194
+ };
5195
+ const installedIdx = prior.installed.findIndex(
5196
+ (p) => p.package === packageName
5197
+ );
5198
+ const priorResources = installedIdx >= 0 ? prior.installed[installedIdx].resources : [];
5199
+ const rewritten = [];
5200
+ const managedReplaced = [];
5201
+ const preserved = [];
5202
+ const frozenDrift = [];
5203
+ const refreshedResources = [];
5204
+ for (const resource of priorResources) {
5205
+ const consumerAbs = path30.isAbsolute(resource.target) ? resource.target : path30.join(projectRoot, resource.target);
5206
+ const consumerBasename = path30.basename(resource.target);
5207
+ const upstreamBasename = lookupUpstreamBasename(consumerBasename);
5208
+ const upstreamAbs = upstreamBasename ? upstreamByBasename.get(upstreamBasename) : void 0;
5209
+ if (resource.strategy === "regenerable") {
5210
+ if (!upstreamAbs) {
5211
+ refreshedResources.push(resource);
5212
+ continue;
5213
+ }
5214
+ const content = await fs22.readFile(upstreamAbs, "utf-8");
5215
+ await writeFileSafe(consumerAbs, content);
5216
+ rewritten.push(resource.target);
5217
+ refreshedResources.push({
5218
+ ...resource,
5219
+ hash: computeHash(content)
5220
+ });
5221
+ continue;
3653
5222
  }
3654
- }
3655
- return [...names];
3656
- }
3657
- function extractVariantsBlock(src) {
3658
- const idx = src.search(/\bvariants\s*:\s*\{/);
3659
- if (idx < 0) return null;
3660
- const open = src.indexOf("{", idx);
3661
- if (open < 0) return null;
3662
- let depth = 0;
3663
- for (let i = open; i < src.length; i++) {
3664
- const c = src[i];
3665
- if (c === "{") depth++;
3666
- else if (c === "}") {
3667
- depth--;
3668
- if (depth === 0) return src.slice(open + 1, i);
5223
+ if (resource.strategy === "managed") {
5224
+ if (!upstreamAbs || !await fileExists(consumerAbs)) {
5225
+ refreshedResources.push(resource);
5226
+ continue;
5227
+ }
5228
+ const upstreamContent = await fs22.readFile(upstreamAbs, "utf-8");
5229
+ const consumerContent = await fs22.readFile(consumerAbs, "utf-8");
5230
+ const merged = mergeManagedRegions(upstreamContent, consumerContent);
5231
+ if (merged !== consumerContent) {
5232
+ await writeFileSafe(consumerAbs, merged);
5233
+ managedReplaced.push(resource.target);
5234
+ }
5235
+ refreshedResources.push({
5236
+ ...resource,
5237
+ hash: computeHash(merged)
5238
+ });
5239
+ continue;
3669
5240
  }
3670
- }
3671
- return null;
3672
- }
3673
- function* extractGroupBodies(block) {
3674
- const re = /(\w+)\s*:\s*\{/g;
3675
- let m;
3676
- while ((m = re.exec(block)) !== null) {
3677
- const open = block.indexOf("{", m.index);
3678
- if (open < 0) continue;
3679
- let depth = 0;
3680
- for (let i = open; i < block.length; i++) {
3681
- const c = block[i];
3682
- if (c === "{") depth++;
3683
- else if (c === "}") {
3684
- depth--;
3685
- if (depth === 0) {
3686
- yield block.slice(open + 1, i);
3687
- re.lastIndex = i + 1;
3688
- break;
3689
- }
5241
+ if (await fileExists(consumerAbs)) preserved.push(resource.target);
5242
+ if (upstreamAbs) {
5243
+ const upstreamContent = await fs22.readFile(upstreamAbs, "utf-8");
5244
+ const upstreamHash = computeHash(upstreamContent);
5245
+ if (resource.hash && upstreamHash !== resource.hash) {
5246
+ frozenDrift.push({
5247
+ target: resource.target,
5248
+ reason: "upstream-changed"
5249
+ });
3690
5250
  }
3691
5251
  }
5252
+ refreshedResources.push(resource);
3692
5253
  }
3693
- }
3694
- function setDiff(a, b) {
3695
- const bset = new Set(b);
3696
- return a.filter((x) => !bset.has(x)).sort();
3697
- }
3698
- function collectResourcesByEntry(resources) {
3699
- const out = /* @__PURE__ */ new Map();
3700
- for (const r of resources) {
3701
- const colon = r.id.indexOf(":");
3702
- const eid = colon >= 0 ? r.id.slice(0, colon) : r.id;
3703
- if (!out.has(eid)) out.set(eid, r);
5254
+ if (variantEntry.version === currentVersion) {
5255
+ if (installedIdx >= 0) {
5256
+ prior.installed[installedIdx] = {
5257
+ ...prior.installed[installedIdx],
5258
+ resources: refreshedResources
5259
+ };
5260
+ await writeInstalledManifest(projectRoot, prior);
5261
+ }
5262
+ return {
5263
+ status: "up-to-date",
5264
+ packageName,
5265
+ variant: currentVariant,
5266
+ version: currentVersion,
5267
+ frozenDrift
5268
+ };
3704
5269
  }
3705
- return out;
3706
- }
3707
- function aggregateByRisk(entries) {
3708
- const out = {};
3709
- for (const e of entries) {
3710
- const k = e.diff.riskLevel;
3711
- out[k] = (out[k] ?? 0) + 1;
5270
+ const lock = {
5271
+ schemaVersion: 1,
5272
+ variant: {
5273
+ name: variantEntry.name,
5274
+ displayName: variantEntry.displayName,
5275
+ version: variantEntry.version,
5276
+ from: packageName
5277
+ },
5278
+ packageVersion: catalog.version,
5279
+ linked: variantEntry.linked,
5280
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
5281
+ };
5282
+ await writeFileSafe(
5283
+ path30.join(projectRoot, ".teamix-evo", "tokens-lock.json"),
5284
+ JSON.stringify(lock, null, 2) + "\n"
5285
+ );
5286
+ config.packages.tokens.version = variantEntry.version;
5287
+ await writeProjectConfig(projectRoot, config);
5288
+ if (installedIdx >= 0) {
5289
+ prior.installed[installedIdx] = {
5290
+ ...prior.installed[installedIdx],
5291
+ version: variantEntry.version,
5292
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5293
+ resources: refreshedResources
5294
+ };
5295
+ await writeInstalledManifest(projectRoot, prior);
3712
5296
  }
3713
- return out;
3714
- }
3715
-
3716
- // src/core/ui-upgrade.ts
3717
- var nodeRequire = createRequire5(import.meta.url);
3718
- function resolvePackageRoot4(packageName) {
3719
- const pkgJsonPath = nodeRequire.resolve(`${packageName}/package.json`);
3720
- return path23.dirname(pkgJsonPath);
3721
- }
3722
- async function buildStaging(args) {
3723
- const { category, projectRoot, aliases, lineageReport, trigger, onlyIds } = args;
3724
- if (category === "ui") {
3725
- const root = args.uiPackageRoot ?? resolvePackageRoot4("@teamix-evo/ui");
3726
- const manifest = await loadUiPackageManifest3(root);
3727
- return buildUiUpgradeStaging({
5297
+ const renames = selectApplicableRenames(
5298
+ variantEntry.renames ?? [],
5299
+ currentVersion,
5300
+ variantEntry.version
5301
+ );
5302
+ let hintPath;
5303
+ if (renames.length > 0) {
5304
+ const hint = await writeTokensUpgradeHint({
3728
5305
  projectRoot,
3729
- category,
3730
- manifest,
3731
- packageRoot: root,
3732
- aliases,
3733
- lineageReport,
3734
- trigger,
3735
- onlyIds
5306
+ trigger: "update",
5307
+ fromVariant: currentVariant,
5308
+ toVariant: currentVariant,
5309
+ fromVersion: currentVersion,
5310
+ toVersion: variantEntry.version,
5311
+ renames
3736
5312
  });
5313
+ if (hint) hintPath = hint.path;
3737
5314
  }
3738
- const bizRoot = args.bizUiPackageRoot ?? resolvePackageRoot4("@teamix-evo/biz-ui");
3739
- const variant = lineageReport.installedVariant ?? "_flat";
3740
- const variantDir = path23.join(bizRoot, "variants", variant);
3741
- const variantManifest = await loadVariantUiPackageManifest2(variantDir);
3742
- const uiRoot = args.uiPackageRoot ?? resolvePackageRoot4("@teamix-evo/ui");
3743
- const uiManifest = await loadUiPackageManifest3(uiRoot);
3744
- const entryPackageRoot = /* @__PURE__ */ new Map();
3745
- const merged = [];
3746
- for (const e of variantManifest.entries) {
3747
- entryPackageRoot.set(e.id, variantDir);
3748
- merged.push(e);
3749
- }
3750
- for (const e of uiManifest.entries) {
3751
- if (entryPackageRoot.has(e.id)) continue;
3752
- entryPackageRoot.set(e.id, uiRoot);
3753
- merged.push(e);
3754
- }
3755
- const synthetic = {
3756
- schemaVersion: 1,
3757
- package: "ui",
3758
- version: variantManifest.version,
3759
- engines: variantManifest.engines,
3760
- entries: merged
5315
+ return {
5316
+ status: "updated",
5317
+ packageName,
5318
+ variant: currentVariant,
5319
+ from: currentVersion,
5320
+ to: variantEntry.version,
5321
+ rewritten,
5322
+ managedReplaced,
5323
+ preserved,
5324
+ frozenDrift,
5325
+ renames,
5326
+ ...hintPath ? { hintPath } : {}
3761
5327
  };
3762
- return buildUiUpgradeStaging({
3763
- projectRoot,
3764
- category,
3765
- manifest: synthetic,
3766
- packageRoot: variantDir,
3767
- entryPackageRoot,
3768
- aliases,
3769
- lineageReport,
3770
- trigger,
3771
- onlyIds
3772
- });
5328
+ }
5329
+ function lookupUpstreamBasename(consumerBasename) {
5330
+ for (const [upstream, consumer] of Object.entries(
5331
+ CONSUMER_BASENAME_BY_UPSTREAM
5332
+ )) {
5333
+ if (consumer === consumerBasename) return upstream;
5334
+ }
5335
+ return consumerBasename;
3773
5336
  }
3774
5337
 
3775
5338
  // src/core/project-update.ts
@@ -4019,7 +5582,7 @@ async function runComponentSourceStep(category, args) {
4019
5582
  });
4020
5583
  return;
4021
5584
  }
4022
- const stagingRel = path24.relative(projectRoot, built.stagingDir);
5585
+ const stagingRel = path31.relative(projectRoot, built.stagingDir);
4023
5586
  const summary = summarizeStagingRisk(built.manifest.summary.byRisk);
4024
5587
  record({
4025
5588
  name: category,
@@ -4062,8 +5625,8 @@ async function planTokensUpdate(tokensPackage, config) {
4062
5625
  }
4063
5626
 
4064
5627
  // src/core/installer.ts
4065
- import * as path25 from "path";
4066
- import * as fs17 from "fs/promises";
5628
+ import * as path32 from "path";
5629
+ import * as fs23 from "fs/promises";
4067
5630
  async function installResources(options) {
4068
5631
  const { projectRoot, manifest, data, variantDir, packageRoot } = options;
4069
5632
  const installedResources = [];
@@ -4100,13 +5663,13 @@ async function installSingleResource(resource, projectRoot, data, variantDir, pa
4100
5663
  variantDir,
4101
5664
  packageRoot
4102
5665
  );
4103
- const targetPath = path25.join(projectRoot, resource.target);
5666
+ const targetPath = path32.join(projectRoot, resource.target);
4104
5667
  let content;
4105
5668
  if (resource.template) {
4106
5669
  const templateContent = await loadTemplateFile(sourcePath);
4107
5670
  content = renderTemplate(templateContent, data);
4108
5671
  } else {
4109
- content = await fs17.readFile(sourcePath, "utf-8");
5672
+ content = await fs23.readFile(sourcePath, "utf-8");
4110
5673
  }
4111
5674
  await writeFileSafe(targetPath, content);
4112
5675
  const hash = computeHash(content);
@@ -4124,13 +5687,13 @@ async function installRecursiveResource(resource, projectRoot, data, variantDir,
4124
5687
  variantDir,
4125
5688
  packageRoot
4126
5689
  );
4127
- const targetDir = path25.join(projectRoot, resource.target);
5690
+ const targetDir = path32.join(projectRoot, resource.target);
4128
5691
  const results = [];
4129
5692
  await ensureDir(targetDir);
4130
5693
  const entries = await walkDir(sourcePath);
4131
5694
  for (const entry of entries) {
4132
- const relPath = path25.relative(sourcePath, entry);
4133
- let targetFile = path25.join(targetDir, relPath);
5695
+ const relPath = path32.relative(sourcePath, entry);
5696
+ let targetFile = path32.join(targetDir, relPath);
4134
5697
  if (resource.template && targetFile.endsWith(".hbs")) {
4135
5698
  targetFile = targetFile.slice(0, -4);
4136
5699
  }
@@ -4139,11 +5702,11 @@ async function installRecursiveResource(resource, projectRoot, data, variantDir,
4139
5702
  const templateContent = await loadTemplateFile(entry);
4140
5703
  content = renderTemplate(templateContent, data);
4141
5704
  } else {
4142
- content = await fs17.readFile(entry, "utf-8");
5705
+ content = await fs23.readFile(entry, "utf-8");
4143
5706
  }
4144
5707
  await writeFileSafe(targetFile, content);
4145
5708
  const hash = computeHash(content);
4146
- const targetRel = path25.relative(projectRoot, targetFile);
5709
+ const targetRel = path32.relative(projectRoot, targetFile);
4147
5710
  results.push({
4148
5711
  id: `${resource.id}:${relPath}`,
4149
5712
  target: targetRel,
@@ -4156,25 +5719,25 @@ async function installRecursiveResource(resource, projectRoot, data, variantDir,
4156
5719
  }
4157
5720
 
4158
5721
  // src/core/registry-client.ts
4159
- import * as path26 from "path";
4160
- import * as fs18 from "fs/promises";
5722
+ import * as path33 from "path";
5723
+ import * as fs24 from "fs/promises";
4161
5724
  import { createRequire as createRequire6 } from "module";
4162
5725
  import { loadVariantManifest } from "@teamix-evo/registry";
4163
5726
  var require6 = createRequire6(import.meta.url);
4164
5727
  function resolvePackageRoot5(packageName) {
4165
5728
  const pkgJsonPath = require6.resolve(`${packageName}/package.json`);
4166
- return path26.dirname(pkgJsonPath);
5729
+ return path33.dirname(pkgJsonPath);
4167
5730
  }
4168
5731
  async function loadVariantData(packageName, variant) {
4169
5732
  const packageRoot = resolvePackageRoot5(packageName);
4170
- const variantDir = path26.join(packageRoot, "library", variant);
5733
+ const variantDir = path33.join(packageRoot, "library", variant);
4171
5734
  logger.debug(`Resolved variant dir: ${variantDir}`);
4172
5735
  logger.debug(`Package root: ${packageRoot}`);
4173
5736
  const manifest = await loadVariantManifest(variantDir);
4174
5737
  let data = {};
4175
- const dataPath = path26.join(variantDir, "_data.json");
5738
+ const dataPath = path33.join(variantDir, "_data.json");
4176
5739
  try {
4177
- const raw = await fs18.readFile(dataPath, "utf-8");
5740
+ const raw = await fs24.readFile(dataPath, "utf-8");
4178
5741
  data = JSON.parse(raw);
4179
5742
  } catch (err) {
4180
5743
  if (err.code !== "ENOENT") {