teamix-evo 0.9.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.
@@ -369,13 +369,20 @@ function resolveSourcePath(source, variantDir, packageRoot) {
369
369
  }
370
370
  return path6.join(variantDir, source);
371
371
  }
372
- 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) {
373
379
  const files = [];
374
380
  const entries = await fs4.readdir(dir, { withFileTypes: true });
375
381
  for (const entry of entries) {
376
382
  const fullPath = path6.join(dir, entry.name);
377
383
  if (entry.isDirectory()) {
378
- files.push(...await walkDir(fullPath));
384
+ if (skipDirs && skipDirs.has(entry.name)) continue;
385
+ files.push(...await walkDir(fullPath, skipDirs));
379
386
  } else if (entry.isFile()) {
380
387
  files.push(fullPath);
381
388
  }
@@ -1251,13 +1258,16 @@ Run \`npx teamix-evo@latest tokens list-variants\` to see all options.`
1251
1258
  if (!await fileExists(overridesAbs)) {
1252
1259
  await writeFileSafe(overridesAbs, EMPTY_OVERRIDES_TEMPLATE);
1253
1260
  }
1254
- const overridesContent = await fs6.readFile(overridesAbs, "utf-8");
1255
- installed.push({
1256
- id: `tokens:${CONSUMER_OVERRIDES_FILE}`,
1257
- target: path9.posix.join(CONSUMER_TOKENS_DIR, CONSUMER_OVERRIDES_FILE),
1258
- hash: computeHash(overridesContent),
1259
- strategy: "frozen"
1260
- });
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
+ }
1261
1271
  const lock = {
1262
1272
  schemaVersion: 1,
1263
1273
  variant: {
@@ -2208,6 +2218,33 @@ async function runLintInit(options) {
2208
2218
  wroteStylelint = true;
2209
2219
  }
2210
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
+ }
2211
2248
  return {
2212
2249
  status: "installed",
2213
2250
  eslint: wroteEslint,
@@ -2216,7 +2253,8 @@ async function runLintInit(options) {
2216
2253
  stylelintMergeRequested: wroteStylelint && stylelintStrategy === "merge" && stylelintExistingPaths.length > 0,
2217
2254
  eslintSkipped: eslintSkipRequested,
2218
2255
  stylelintSkipped: stylelintSkipRequested,
2219
- packageJsonPatched
2256
+ packageJsonPatched,
2257
+ stylelintIgnoreFilesWarning
2220
2258
  };
2221
2259
  }
2222
2260
  function detectPm(projectRoot) {
@@ -2364,7 +2402,7 @@ function renderManagedBlockBody(args) {
2364
2402
  return `# AGENTS.md
2365
2403
 
2366
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
2367
- > \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
2368
2406
 
2369
2407
  ## \u5DF2\u88C5 Skills\uFF08variant: ${variant}\uFF09
2370
2408
 
@@ -2377,6 +2415,13 @@ ${skillBlock}
2377
2415
  - \u6A21\u7CCA\u573A\u666F\uFF1A\u5148\u6309 SKIP \u53CD\u5411\u6392\u9664\uFF0C\u5269\u4F59\u552F\u4E00 skill \u5373\u4E3A\u5165\u53E3
2378
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
2379
2417
 
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
+
2380
2425
  > \u5237\u65B0\u672C\u6587\u4EF6\uFF1A\`npx teamix-evo skills add\` \u6216\u91CD\u8DD1 \`npm create teamix-evo\` / \`teamix-evo init\`\u3002`;
2381
2426
  }
2382
2427
  function wrapManagedBlock(body) {
@@ -2933,1546 +2978,2361 @@ async function migrateLegacyTokens(options) {
2933
2978
  };
2934
2979
  }
2935
2980
 
2936
- // src/core/snapshot.ts
2981
+ // src/core/ui-conflict-detector.ts
2937
2982
  import * as fs14 from "fs/promises";
2938
2983
  import * as path18 from "path";
2939
- var TEAMIX_DIR2 = ".teamix-evo";
2940
- var SNAPSHOTS_DIR = ".snapshots";
2941
- var LOGS_DIR = "logs";
2942
- var META_FILE = "_meta.json";
2943
- var DEFAULT_KEEP = 5;
2944
- function isoToFsSafe(iso) {
2945
- return iso.replace(/[:.]/g, "-");
2946
- }
2947
- function fsSafeToIso(safe) {
2948
- return safe.replace(
2949
- /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})(Z)$/,
2950
- "$1T$2:$3:$4.$5$6"
2951
- );
2952
- }
2953
- async function createSnapshot(projectRoot, opts = {}) {
2954
- const teamixDir = path18.join(projectRoot, TEAMIX_DIR2);
2955
- try {
2956
- const stat5 = await fs14.stat(teamixDir);
2957
- if (!stat5.isDirectory()) return null;
2958
- } catch (err) {
2959
- if (err.code === "ENOENT") return null;
2960
- throw err;
2961
- }
2962
- const isoTs = (/* @__PURE__ */ new Date()).toISOString();
2963
- const ts = isoToFsSafe(isoTs);
2964
- const snapshotRoot = path18.join(teamixDir, SNAPSHOTS_DIR);
2965
- const target = path18.join(snapshotRoot, ts);
2966
- await fs14.mkdir(target, { recursive: true });
2967
- const entries = await fs14.readdir(teamixDir, { withFileTypes: true });
2968
- for (const entry of entries) {
2969
- if (entry.name === SNAPSHOTS_DIR) continue;
2970
- if (entry.name === LOGS_DIR) continue;
2971
- const src = path18.join(teamixDir, entry.name);
2972
- const dst = path18.join(target, entry.name);
2973
- 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
+ }
2974
3018
  }
2975
- const meta = {
2976
- ts: isoTs,
2977
- 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]
2978
3025
  };
2979
- await fs14.writeFile(
2980
- path18.join(target, META_FILE),
2981
- JSON.stringify(meta, null, 2) + "\n",
2982
- "utf-8"
2983
- );
2984
- logger.debug(
2985
- `Snapshot created \u2192 ${path18.relative(projectRoot, target)} (${meta.reason})`
2986
- );
2987
- const keep = opts.keep ?? DEFAULT_KEEP;
2988
- await pruneSnapshots(projectRoot, keep, { protectedTs: opts.protectedTs });
2989
- return { ts, path: target };
2990
3026
  }
2991
- async function listSnapshots(projectRoot) {
2992
- const snapshotRoot = path18.join(projectRoot, TEAMIX_DIR2, SNAPSHOTS_DIR);
2993
- let entries;
2994
- try {
2995
- entries = await fs14.readdir(snapshotRoot, { withFileTypes: true });
2996
- } catch (err) {
2997
- if (err.code === "ENOENT") return [];
2998
- 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
+ );
2999
3080
  }
3000
- const result = [];
3001
- for (const entry of entries) {
3002
- if (!entry.isDirectory()) continue;
3003
- const dir = path18.join(snapshotRoot, entry.name);
3004
- let isoTs = null;
3005
- let reason = null;
3006
- try {
3007
- const raw = await fs14.readFile(path18.join(dir, META_FILE), "utf-8");
3008
- const parsed = JSON.parse(raw);
3009
- if (typeof parsed.ts === "string") isoTs = parsed.ts;
3010
- if (typeof parsed.reason === "string" && ["init", "update", "switch", "restore", "manual"].includes(
3011
- parsed.reason
3012
- )) {
3013
- 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);
3014
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");
3015
3144
  } catch {
3016
- isoTs = fsSafeToIso(entry.name);
3145
+ logger.warn(" could not remove components.json");
3017
3146
  }
3018
- result.push({ ts: entry.name, isoTs, reason, path: dir });
3019
3147
  }
3020
- result.sort((a, b) => a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
3021
- return result;
3022
- }
3023
- async function pruneSnapshots(projectRoot, keep = DEFAULT_KEEP, opts = {}) {
3024
- if (keep < 0)
3025
- throw new Error(`pruneSnapshots: keep must be >= 0, got ${keep}`);
3026
- const snapshots = await listSnapshots(projectRoot);
3027
- if (snapshots.length <= keep) return [];
3028
- const tail = snapshots.slice(keep);
3029
- const toRemove = opts.protectedTs ? tail.filter((s) => s.ts !== opts.protectedTs) : tail;
3030
- const removed = [];
3031
- for (const snap of toRemove) {
3032
- await fs14.rm(snap.path, { recursive: true, force: true });
3033
- removed.push(snap.ts);
3034
- logger.debug(`Pruned snapshot ${snap.ts}`);
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);
3035
3159
  }
3036
- return removed.reverse();
3037
- }
3038
-
3039
- // src/core/file-changes.ts
3040
- import * as fs15 from "fs/promises";
3041
- import * as path19 from "path";
3042
- function toRelativePosix(p, projectRoot) {
3043
- let rel2 = p;
3044
- if (path19.isAbsolute(p)) {
3045
- rel2 = path19.relative(projectRoot, p);
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
+ }
3046
3167
  }
3047
- return rel2.split(path19.sep).join("/");
3168
+ return {
3169
+ movedFiles,
3170
+ importRewrites,
3171
+ componentsJsonRemoved,
3172
+ backedUpFiles
3173
+ };
3048
3174
  }
3049
- async function listBackupOriginals(projectRoot) {
3050
- const backupsDir = path19.join(projectRoot, ".teamix-evo", ".backups");
3051
- const out = /* @__PURE__ */ new Set();
3052
- const stack = [backupsDir];
3053
- while (stack.length > 0) {
3054
- const dir = stack.pop();
3055
- let entries;
3056
- try {
3057
- entries = await fs15.readdir(dir, { withFileTypes: true });
3058
- } catch (err) {
3059
- if (err.code === "ENOENT") continue;
3060
- throw err;
3061
- }
3062
- for (const e of entries) {
3063
- const full = path19.join(dir, e.name);
3064
- if (e.isDirectory()) {
3065
- stack.push(full);
3066
- } else if (e.isFile() && e.name.endsWith(".bak")) {
3067
- const rel2 = path19.relative(backupsDir, full);
3068
- const original = stripBackupSuffix(rel2);
3069
- if (original) out.add(original.split(path19.sep).join("/"));
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));
3070
3184
  }
3071
3185
  }
3186
+ const remaining = await fs15.readdir(dir);
3187
+ if (remaining.length === 0) {
3188
+ await fs15.rmdir(dir);
3189
+ }
3190
+ } catch {
3072
3191
  }
3073
- return out;
3074
3192
  }
3075
- function stripBackupSuffix(rel2) {
3076
- const m = rel2.match(
3077
- /^(.+)\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.bak$/
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")
3078
3224
  );
3079
- return m?.[1] ?? null;
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
+ };
3080
3247
  }
3081
- function diffBackupSet(before, after) {
3082
- const out = /* @__PURE__ */ new Set();
3083
- for (const p of after) {
3084
- if (!before.has(p)) out.add(p);
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);
3085
3258
  }
3086
- return out;
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";
3087
3287
  }
3088
3288
 
3089
- // src/core/project-init.ts
3090
- var BASELINE_UI_ENTRIES = [
3091
- "button",
3092
- "button-group",
3093
- "input",
3094
- "form",
3095
- "card",
3096
- "collapsible",
3097
- "dialog",
3098
- "dropdown-menu",
3099
- "tabs",
3100
- "table",
3101
- "sidebar",
3102
- "page-shell",
3103
- "page-header"
3104
- ];
3105
- var CRITICAL_STEPS = /* @__PURE__ */ new Set([
3106
- "tokens",
3107
- "skills",
3108
- "ui-init"
3109
- ]);
3110
- var IMPLEMENTED_STRATEGIES = {
3111
- // 'agents-md': both 'merge-managed' (Phase 2.B — splice the
3112
- // teamix-evo-skills managed region) and 'overwrite' (full rewrite) write
3113
- // through `runGenerateAgentsMd`.
3114
- "agents-md": ["merge-managed", "overwrite", "skip"],
3115
- tokens: ["migrate", "overwrite", "skip"],
3116
- "components-json": ["overwrite", "skip"],
3117
- "shadcn-source": ["overwrite", "skip-existing", "skip"],
3118
- "tailwind-config": ["skip"],
3119
- "index-css": ["skip"],
3120
- // Phase 3.E: lint conflict strategies are honored end-to-end by
3121
- // `runLintInit` (backup user file + write template, optional AI-assist hint).
3122
- "eslint-config": ["merge", "backup-overwrite", "skip", "overwrite"],
3123
- "stylelint-config": ["merge", "backup-overwrite", "skip", "overwrite"]
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"
3124
3296
  };
3125
- function pickIde(ides) {
3126
- return ides[0] ?? "qoder";
3127
- }
3128
- function strategyImplemented(key, strategy) {
3129
- return IMPLEMENTED_STRATEGIES[key]?.includes(strategy) ?? false;
3297
+ function isoToFsSafe(iso) {
3298
+ return iso.replace(/[:.]/g, "-");
3130
3299
  }
3131
- async function resolveUiEntries(options) {
3132
- if (options.uiEntries && options.uiEntries.length > 0) {
3133
- return [...options.uiEntries];
3134
- }
3135
- if (options.answers.uiSelection === "all") {
3136
- const { manifest } = await loadUiData("@teamix-evo/ui");
3137
- return manifest.entries.map((e) => e.id);
3300
+ async function buildUiUpgradeStaging(options) {
3301
+ const { lineageReport, category } = options;
3302
+ if (lineageReport.lineage !== "teamix-evo" && lineageReport.lineage !== "mixed") {
3303
+ return null;
3138
3304
  }
3139
- return [...BASELINE_UI_ENTRIES];
3140
- }
3141
- function deriveTokensChanges(result, projectRoot) {
3142
- if (result.status !== "installed") return [];
3143
- return result.resources.map((r) => ({
3144
- kind: "created",
3145
- path: toRelativePosix(r.target, projectRoot),
3146
- step: "tokens",
3147
- detail: r.strategy
3148
- }));
3149
- }
3150
- function deriveSkillsChanges(result, projectRoot) {
3151
- if (result.status !== "installed") return [];
3152
- return result.addedSkillIds.map((id) => ({
3153
- kind: "created",
3154
- path: `.teamix-evo/skills/${id}/SKILL.md`,
3155
- step: "skills",
3156
- detail: "skill installed (source mirror + IDE mirrors)"
3157
- }));
3158
- }
3159
- function deriveUiAddChanges(result, projectRoot) {
3160
- const out = [];
3161
- let remaining = result.written;
3162
- for (let i = result.resources.length - 1; i >= 0 && remaining > 0; i--) {
3163
- const r = result.resources[i];
3164
- out.unshift({
3165
- kind: "created",
3166
- path: toRelativePosix(r.target, projectRoot),
3167
- step: "ui-add",
3168
- detail: r.strategy
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
3169
3335
  });
3170
- remaining--;
3336
+ if (built) entries.push(built);
3171
3337
  }
3172
- return out;
3173
- }
3174
- function deriveLintChanges(result) {
3175
- if (result.status !== "installed") return [];
3176
- const out = [];
3177
- if (result.eslint) {
3178
- out.push({
3179
- kind: "created",
3180
- path: "eslint.config.js",
3181
- step: "lint",
3182
- detail: "@teamix-evo/eslint-config consumer preset"
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
3183
3346
  });
3347
+ if (built) entries.push(built);
3184
3348
  }
3185
- if (result.stylelint) {
3186
- out.push({
3187
- kind: "created",
3188
- path: "stylelint.config.cjs",
3189
- step: "lint",
3190
- detail: "@teamix-evo/stylelint-config consumer preset"
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"
3191
3383
  });
3192
3384
  }
3193
- if (result.packageJsonPatched) {
3194
- out.push({
3195
- kind: "created",
3196
- path: "package.json",
3197
- step: "lint",
3198
- detail: 'scripts.lint / scripts["lint:css"]'
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"
3199
3394
  });
3200
3395
  }
3201
- return out;
3202
- }
3203
- async function runProjectInit(options) {
3204
- const { projectRoot, answers, dryRun = false, onStep } = options;
3205
- const ide = pickIde(answers.ides);
3206
- const steps = [];
3207
- const pending = [];
3208
- const allChanges = [];
3209
- const backupsBefore = dryRun ? /* @__PURE__ */ new Set() : await listBackupOriginals(projectRoot).catch(() => /* @__PURE__ */ new Set());
3210
- let snapshot = null;
3211
- let snapshotError;
3212
- if (!dryRun) {
3213
- try {
3214
- snapshot = await createSnapshot(projectRoot, { reason: "init" });
3215
- } catch (err) {
3216
- snapshotError = getErrorMessage(err);
3217
- }
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;
3218
3403
  }
3219
- let aborted = false;
3220
- const firstFailure = {
3221
- value: null
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
3222
3439
  };
3223
- function record(step) {
3224
- steps.push(step);
3225
- onStep?.(step);
3226
- if (step.changes && step.changes.length > 0) {
3227
- allChanges.push(...step.changes);
3228
- }
3229
- }
3230
- function recordPending(key) {
3231
- const strategy = answers.conflictDecisions[key];
3232
- if (!strategy) return;
3233
- if (strategyImplemented(key, strategy)) return;
3234
- pending.push({
3235
- key,
3236
- strategy,
3237
- reason: `Strategy "${strategy}" requires the managed-region engine (batch 4); recorded for follow-up.`
3238
- });
3239
- }
3240
- function recordFailure(name, err) {
3241
- const message = getErrorMessage(err);
3242
- record({ name, status: "fail", detail: message });
3243
- if (!firstFailure.value)
3244
- firstFailure.value = { step: name, error: message };
3245
- if (CRITICAL_STEPS.has(name)) aborted = true;
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 };
3246
3495
  }
3247
- const tokensDecision = answers.conflictDecisions.tokens;
3248
- const legacyTokensPaths = options.legacyTokensPaths ?? [];
3249
- const wantMigrate = tokensDecision === "migrate" && legacyTokensPaths.length > 0;
3250
- if (tokensDecision === "skip") {
3251
- record({
3252
- name: "tokens",
3253
- status: "skip",
3254
- detail: "conflict strategy = skip"
3255
- });
3256
- } else if (dryRun) {
3257
- const planDetail = wantMigrate ? `runTokensInit(variant=${answers.variant}); migrateLegacyTokens(${legacyTokensPaths.length} file${legacyTokensPaths.length === 1 ? "" : "s"})` : `runTokensInit(variant=${answers.variant})`;
3258
- record({
3259
- name: "tokens",
3260
- status: "planned",
3261
- detail: planDetail
3262
- });
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";
3263
3515
  } else {
3264
- try {
3265
- const result = await runTokensInit({
3266
- projectRoot,
3267
- variant: answers.variant,
3268
- ide
3269
- });
3270
- let detail = result.status === "installed" ? `${result.packageName}@${result.version} (${result.count} files)` : result.status;
3271
- if (wantMigrate) {
3272
- try {
3273
- const m = await migrateLegacyTokens({
3274
- projectRoot,
3275
- legacyPaths: legacyTokensPaths
3276
- });
3277
- detail += `; migrated ${m.migrated.length}/${legacyTokensPaths.length} legacy file${legacyTokensPaths.length === 1 ? "" : "s"} \u2192 ${m.overridesPath}`;
3278
- if (m.skipped.length > 0) {
3279
- detail += ` (skipped ${m.skipped.length})`;
3280
- }
3281
- } catch (err) {
3282
- detail += `; migrate failed: ${getErrorMessage(err)}`;
3283
- }
3284
- }
3285
- record({
3286
- name: "tokens",
3287
- status: "ok",
3288
- detail,
3289
- changes: deriveTokensChanges(result, projectRoot)
3290
- });
3291
- } catch (err) {
3292
- recordFailure("tokens", err);
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]);
3293
3539
  }
3294
3540
  }
3295
- recordPending("tokens");
3296
- const codeSkillId = `teamix-evo-code-${answers.variant}`;
3297
- if (dryRun) {
3298
- record({
3299
- name: "skills",
3300
- status: "planned",
3301
- detail: `runSkillsAdd(${codeSkillId})`
3302
- });
3303
- } else if (aborted) {
3304
- record({
3305
- name: "skills",
3306
- status: "skip",
3307
- detail: "aborted: earlier critical step failed"
3308
- });
3309
- } else {
3310
- try {
3311
- const result = await runSkillsAdd({
3312
- projectRoot,
3313
- names: [codeSkillId],
3314
- ides: answers.ides,
3315
- scope: answers.scope,
3316
- ide
3317
- });
3318
- record({
3319
- name: "skills",
3320
- status: "ok",
3321
- detail: result.status === "installed" ? `added: ${result.addedSkillIds.join(", ") || "none"}; existing: ${result.skippedSkillIds.join(", ") || "none"}` : result.status,
3322
- changes: deriveSkillsChanges(result, projectRoot)
3323
- });
3324
- } catch (err) {
3325
- recordFailure("skills", err);
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);
3326
3555
  }
3327
3556
  }
3328
- const agentsMdDecision = answers.conflictDecisions["agents-md"];
3329
- const agentsMdSkillIds = [
3330
- `teamix-evo-design-${answers.variant}`,
3331
- codeSkillId
3332
- ];
3333
- if (agentsMdDecision === "skip") {
3334
- record({
3335
- name: "agents-md",
3336
- status: "skip",
3337
- detail: "conflict strategy = skip"
3338
- });
3339
- } else if (dryRun) {
3340
- record({
3341
- name: "agents-md",
3342
- status: "planned",
3343
- detail: `runGenerateAgentsMd(${agentsMdSkillIds.length} skills)`
3344
- });
3345
- } else {
3346
- try {
3347
- const result = await runGenerateAgentsMd({
3348
- projectRoot,
3349
- variant: answers.variant,
3350
- skillIds: agentsMdSkillIds,
3351
- // Phase 2.B: when the user picked `merge-managed` on conflict, only
3352
- // rewrite the `teamix-evo-skills` managed region so hand-written
3353
- // sections survive the regenerate step. `overwrite` keeps the
3354
- // historical full-rewrite default.
3355
- mode: agentsMdDecision === "merge-managed" ? "merge-managed" : "overwrite"
3356
- });
3357
- record({
3358
- name: "agents-md",
3359
- status: "ok",
3360
- detail: `${result.skillCount} skill index${result.missingSkillIds.length > 0 ? ` (missing SKILL.md: ${result.missingSkillIds.join(", ")})` : ""}`,
3361
- changes: [
3362
- {
3363
- kind: result.backedUp ? "modified" : "created",
3364
- path: toRelativePosix(result.path, projectRoot),
3365
- step: "agents-md",
3366
- detail: "skill-trigger fallback (ADR 0038)"
3367
- }
3368
- ]
3369
- });
3370
- } catch (err) {
3371
- recordFailure("agents-md", err);
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
+ }
3372
3577
  }
3373
3578
  }
3374
- recordPending("agents-md");
3375
- const componentsJsonDecision = answers.conflictDecisions["components-json"];
3376
- const shadcnDecision = answers.conflictDecisions["shadcn-source"];
3377
- const skipUiInit = !answers.withUi || componentsJsonDecision === "skip";
3378
- if (skipUiInit) {
3379
- record({
3380
- name: "ui-init",
3381
- status: "skip",
3382
- detail: !answers.withUi ? "withUi = false" : "components.json conflict strategy = skip"
3383
- });
3384
- record({
3385
- name: "ui-add",
3386
- status: "skip",
3387
- detail: !answers.withUi ? "withUi = false" : "components.json conflict strategy = skip"
3388
- });
3389
- } else if (dryRun) {
3390
- record({
3391
- name: "ui-init",
3392
- status: "planned",
3393
- detail: "runUiInit()"
3394
- });
3395
- const entries = await resolveUiEntries(options).catch(() => [
3396
- ...BASELINE_UI_ENTRIES
3397
- ]);
3398
- record({
3399
- name: "ui-add",
3400
- status: "planned",
3401
- detail: `runUiAdd(${entries.length} entries: ${entries.slice(0, 3).join(", ")}${entries.length > 3 ? "\u2026" : ""})`
3402
- });
3403
- } else {
3404
- if (aborted) {
3405
- record({
3406
- name: "ui-init",
3407
- status: "skip",
3408
- detail: "aborted: earlier critical step failed"
3409
- });
3410
- record({
3411
- name: "ui-add",
3412
- status: "skip",
3413
- detail: "aborted: earlier critical step failed"
3414
- });
3415
- } else {
3416
- let uiInitOk = false;
3417
- try {
3418
- const initResult = await runUiInit({ projectRoot, ide });
3419
- record({
3420
- name: "ui-init",
3421
- status: "ok",
3422
- detail: initResult.status === "installed" ? "config.json packages.ui written" : initResult.status
3423
- });
3424
- uiInitOk = true;
3425
- } catch (err) {
3426
- recordFailure("ui-init", err);
3427
- }
3428
- if (!uiInitOk) {
3429
- record({
3430
- name: "ui-add",
3431
- status: "skip",
3432
- detail: "aborted: ui-init failed"
3433
- });
3434
- } else if (shadcnDecision === "skip") {
3435
- record({
3436
- name: "ui-add",
3437
- status: "skip",
3438
- detail: "shadcn-source conflict strategy = skip"
3439
- });
3440
- } else {
3441
- try {
3442
- const entries = await resolveUiEntries(options);
3443
- const addResult = await runUiAdd({
3444
- projectRoot,
3445
- ids: entries,
3446
- // 'overwrite' strategy → overwrite=true; everything else (incl.
3447
- // 'skip-existing' which is the default) → overwrite=false.
3448
- overwrite: shadcnDecision === "overwrite"
3449
- });
3450
- record({
3451
- name: "ui-add",
3452
- status: "ok",
3453
- detail: `${addResult.orderedIds.length} entries (${addResult.written} written, ${addResult.skipped} skipped)`,
3454
- changes: deriveUiAddChanges(addResult, projectRoot)
3455
- });
3456
- } catch (err) {
3457
- recordFailure("ui-add", err);
3458
- }
3459
- }
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);
3460
3637
  }
3461
3638
  }
3462
- recordPending("components-json");
3463
- recordPending("shadcn-source");
3464
- if (!answers.withLint) {
3465
- record({
3466
- name: "lint",
3467
- status: "skip",
3468
- detail: "withLint = false"
3469
- });
3470
- } else if (dryRun) {
3471
- record({
3472
- name: "lint",
3473
- status: "planned",
3474
- detail: "runLintInit()"
3475
- });
3476
- } else {
3477
- try {
3478
- const eslintStrategy = answers.conflictDecisions["eslint-config"];
3479
- const stylelintStrategy = answers.conflictDecisions["stylelint-config"];
3480
- const eslintExistingPaths = options.legacyEslintPaths ?? [];
3481
- const stylelintExistingPaths = options.legacyStylelintPaths ?? [];
3482
- const result = await runLintInit({
3483
- projectRoot,
3484
- skipInstall: options.skipInstall ?? false,
3485
- eslintStrategy: eslintStrategy === "merge" || eslintStrategy === "backup-overwrite" || eslintStrategy === "skip" || eslintStrategy === "overwrite" ? eslintStrategy : "overwrite",
3486
- stylelintStrategy: stylelintStrategy === "merge" || stylelintStrategy === "backup-overwrite" || stylelintStrategy === "skip" || stylelintStrategy === "overwrite" ? stylelintStrategy : "overwrite",
3487
- eslintExistingPaths,
3488
- stylelintExistingPaths
3489
- });
3490
- const detailParts = [];
3491
- if (result.status === "installed") {
3492
- detailParts.push(
3493
- `eslint=${result.eslint}, stylelint=${result.stylelint}`
3494
- );
3495
- if (result.eslintMergeRequested) {
3496
- detailParts.push("eslint:AI-merge-pending");
3497
- }
3498
- if (result.stylelintMergeRequested) {
3499
- detailParts.push("stylelint:AI-merge-pending");
3500
- }
3501
- if (result.eslintSkipped) detailParts.push("eslint:skipped");
3502
- if (result.stylelintSkipped) detailParts.push("stylelint:skipped");
3503
- } else {
3504
- detailParts.push(result.status);
3505
- }
3506
- record({
3507
- name: "lint",
3508
- status: "ok",
3509
- detail: detailParts.join(" / "),
3510
- changes: deriveLintChanges(result)
3511
- });
3512
- } catch (err) {
3513
- recordFailure("lint", err);
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);
3514
3734
  }
3515
3735
  }
3516
- recordPending("tailwind-config");
3517
- recordPending("index-css");
3518
- if (!dryRun) {
3519
- try {
3520
- const backupsAfter = await listBackupOriginals(projectRoot);
3521
- const newlyBackedUp = diffBackupSet(backupsBefore, backupsAfter);
3522
- if (newlyBackedUp.size > 0) {
3523
- for (const change of allChanges) {
3524
- if (change.kind === "created" && newlyBackedUp.has(change.path)) {
3525
- change.kind = "modified";
3526
- }
3527
- }
3528
- for (const rel2 of newlyBackedUp) {
3529
- allChanges.push({
3530
- kind: "backed-up",
3531
- path: rel2,
3532
- step: "backup",
3533
- detail: ".teamix-evo/.backups/<\u540C\u8DEF\u5F84>.<isoTs>.bak"
3534
- });
3535
- }
3536
- }
3537
- } catch {
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--;
3538
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]);
3539
3763
  }
3540
- const status = dryRun ? "dry-run" : steps.some((s) => s.status === "fail") ? "partial" : "installed";
3541
- const out = {
3542
- status,
3543
- steps,
3544
- pendingConflictWork: pending,
3545
- changes: allChanges,
3546
- snapshot
3547
- };
3548
- if (snapshotError) out.snapshotError = snapshotError;
3549
- if (firstFailure.value) {
3550
- out.resumeHint = {
3551
- failedAt: firstFailure.value.step,
3552
- completed: steps.filter((s) => s.status === "ok").map((s) => s.name),
3553
- failed: steps.filter((s) => s.status === "fail").map((s) => s.name),
3554
- error: firstFailure.value.error,
3555
- // Every sub-step is idempotent (returns `already-initialized` when its
3556
- // state file already exists), so a plain re-run resumes from the
3557
- // failed step. A richer --resume model lands with batch 3 (lock
3558
- // snapshot + restore).
3559
- resumeCommand: "teamix-evo init"
3560
- };
3764
+ for (const m of src.matchAll(/--([a-z][a-z0-9-]*)\s*:/g)) {
3765
+ if (m[1]) out.add(m[1]);
3561
3766
  }
3562
3767
  return out;
3563
3768
  }
3564
-
3565
- // src/core/project-update.ts
3566
- import * as path25 from "path";
3567
- import {
3568
- loadTokensPackageManifest as loadTokensPackageManifest3,
3569
- getVariantEntry as getVariantEntry3
3570
- } from "@teamix-evo/registry";
3571
-
3572
- // src/core/tokens-update.ts
3573
- import * as path21 from "path";
3574
- import * as fs16 from "fs/promises";
3575
- import {
3576
- loadTokensPackageManifest as loadTokensPackageManifest2,
3577
- getVariantEntry as getVariantEntry2
3578
- } from "@teamix-evo/registry";
3579
-
3580
- // src/core/upgrade-hints.ts
3581
- import * as path20 from "path";
3582
- var TEAMIX_DIR3 = ".teamix-evo";
3583
- var HINTS_DIR = ".upgrade-hints";
3584
- function isoToFsSafe2(iso) {
3585
- return iso.replace(/[:.]/g, "-");
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;
3586
3775
  }
3587
- async function writeTokensUpgradeHint(options) {
3588
- if (options.renames.length === 0) return null;
3589
- const isoTs = options.isoTs ?? (/* @__PURE__ */ new Date()).toISOString();
3590
- const fsTs = isoToFsSafe2(isoTs);
3591
- const filename = `tokens-${fsTs}.json`;
3592
- const target = path20.join(
3593
- options.projectRoot,
3594
- TEAMIX_DIR3,
3595
- HINTS_DIR,
3596
- filename
3597
- );
3598
- const payload = {
3599
- schemaVersion: 1,
3600
- ts: isoTs,
3601
- package: "tokens",
3602
- trigger: options.trigger,
3603
- fromVariant: options.fromVariant,
3604
- toVariant: options.toVariant,
3605
- fromVersion: options.fromVersion,
3606
- toVersion: options.toVersion,
3607
- renames: options.renames
3608
- };
3609
- await writeFileSafe(target, JSON.stringify(payload, null, 2) + "\n");
3610
- return {
3611
- path: target,
3612
- ts: fsTs,
3613
- renameCount: options.renames.length
3614
- };
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() ?? "";
3615
3779
  }
3616
- function selectApplicableRenames(renames, fromVersion, toVersion) {
3617
- return renames.filter(
3618
- (r) => compareSemver2(r.sinceVersion, fromVersion) > 0 && compareSemver2(r.sinceVersion, toVersion) <= 0
3619
- ).sort((a, b) => compareSemver2(a.sinceVersion, b.sinceVersion));
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);
3620
3786
  }
3621
- function compareSemver2(a, b) {
3622
- const [aMain = "", aRest = ""] = a.split("-", 2);
3623
- const [bMain = "", bRest = ""] = b.split("-", 2);
3624
- const aParts = aMain.split(".").map((n) => Number.parseInt(n, 10));
3625
- const bParts = bMain.split(".").map((n) => Number.parseInt(n, 10));
3626
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
3627
- const ai = aParts[i] ?? 0;
3628
- const bi = bParts[i] ?? 0;
3629
- if (ai !== bi) return ai - bi;
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` };
3630
3794
  }
3631
- if (aRest === "" && bRest !== "") return 1;
3632
- if (aRest !== "" && bRest === "") return -1;
3633
- return aRest.localeCompare(bRest, void 0, { numeric: true });
3634
- }
3635
-
3636
- // src/core/managed-merge.ts
3637
- import { hasManagedRegion as hasManagedRegion3, replaceManagedRegion as replaceManagedRegion3 } from "@teamix-evo/registry";
3638
- function mergeManagedRegions(upstreamContent, consumerContent) {
3639
- let updated = consumerContent;
3640
- const re = /<!-- teamix-evo:managed:start id="([^"]+)" -->([\s\S]*?)<!-- teamix-evo:managed:end(?: id="\1")? -->/g;
3641
- let match;
3642
- while ((match = re.exec(upstreamContent)) !== null) {
3643
- const id = match[1];
3644
- const body = match[2].replace(/^\n/, "").replace(/\n$/, "");
3645
- if (!hasManagedRegion3(updated, id)) {
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) {
3646
3823
  throw new Error(
3647
- `Managed region "${id}" missing from consumer file \u2014 refusing to silently rewrite (ADR 0003).`
3824
+ `Unknown ${category} component id(s): ${unknown.map((s) => `"${s}"`).join(
3825
+ ", "
3826
+ )}. Hint: \`teamix-evo ${category} list\` shows available ids.`
3648
3827
  );
3649
3828
  }
3650
- updated = replaceManagedRegion3(updated, id, body);
3651
3829
  }
3652
- return updated;
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
+ };
3653
3852
  }
3654
-
3655
- // src/core/tokens-update.ts
3656
- var DEFAULT_TOKENS_PACKAGE2 = "@teamix-evo/tokens";
3657
- var CONSUMER_BASENAME_BY_UPSTREAM = {
3658
- "theme.css": "tokens.theme.css",
3659
- "overrides.css": "tokens.overrides.css"
3660
- };
3661
- async function runTokensUpdate(options) {
3662
- const { projectRoot } = options;
3663
- const packageName = options.packageName ?? DEFAULT_TOKENS_PACKAGE2;
3664
- const config = await readProjectConfig(projectRoot);
3665
- if (!config?.packages?.tokens) {
3666
- return { status: "not-initialized" };
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
+ });
3667
3868
  }
3668
- const currentVariant = config.packages.tokens.variant;
3669
- const currentVersion = config.packages.tokens.version;
3670
- const packageRoot = options.packageRoot ?? resolveTokensPackageRoot(packageName);
3671
- const catalog = await loadTokensPackageManifest2(packageRoot);
3672
- const variantEntry = getVariantEntry2(catalog, currentVariant);
3673
- if (!variantEntry) {
3674
- throw new Error(
3675
- `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.`
3676
- );
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);
3677
3880
  }
3678
- const upstreamByBasename = /* @__PURE__ */ new Map();
3679
- for (const fileRel of variantEntry.files) {
3680
- upstreamByBasename.set(
3681
- path21.basename(fileRel),
3682
- path21.join(packageRoot, fileRel)
3683
- );
3881
+ for (const e of uiManifest.entries) {
3882
+ if (entryPackageRoot.has(e.id)) continue;
3883
+ entryPackageRoot.set(e.id, uiRoot);
3884
+ merged.push(e);
3684
3885
  }
3685
- const prior = await readInstalledManifest(projectRoot) ?? {
3886
+ const synthetic = {
3686
3887
  schemaVersion: 1,
3687
- installed: []
3888
+ package: "ui",
3889
+ version: variantManifest.version,
3890
+ engines: variantManifest.engines,
3891
+ entries: merged
3688
3892
  };
3689
- const installedIdx = prior.installed.findIndex(
3690
- (p) => p.package === packageName
3691
- );
3692
- const priorResources = installedIdx >= 0 ? prior.installed[installedIdx].resources : [];
3693
- const rewritten = [];
3694
- const managedReplaced = [];
3695
- const preserved = [];
3696
- const frozenDrift = [];
3697
- const refreshedResources = [];
3698
- for (const resource of priorResources) {
3699
- const consumerAbs = path21.isAbsolute(resource.target) ? resource.target : path21.join(projectRoot, resource.target);
3700
- const consumerBasename = path21.basename(resource.target);
3701
- const upstreamBasename = lookupUpstreamBasename(consumerBasename);
3702
- const upstreamAbs = upstreamBasename ? upstreamByBasename.get(upstreamBasename) : void 0;
3703
- if (resource.strategy === "regenerable") {
3704
- if (!upstreamAbs) {
3705
- refreshedResources.push(resource);
3706
- continue;
3707
- }
3708
- const content = await fs16.readFile(upstreamAbs, "utf-8");
3709
- await writeFileSafe(consumerAbs, content);
3710
- rewritten.push(resource.target);
3711
- refreshedResources.push({
3712
- ...resource,
3713
- hash: computeHash(content)
3714
- });
3715
- continue;
3716
- }
3717
- if (resource.strategy === "managed") {
3718
- if (!upstreamAbs || !await fileExists(consumerAbs)) {
3719
- refreshedResources.push(resource);
3720
- continue;
3721
- }
3722
- const upstreamContent = await fs16.readFile(upstreamAbs, "utf-8");
3723
- const consumerContent = await fs16.readFile(consumerAbs, "utf-8");
3724
- const merged = mergeManagedRegions(upstreamContent, consumerContent);
3725
- if (merged !== consumerContent) {
3726
- await writeFileSafe(consumerAbs, merged);
3727
- managedReplaced.push(resource.target);
3728
- }
3729
- refreshedResources.push({
3730
- ...resource,
3731
- hash: computeHash(merged)
3732
- });
3733
- continue;
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;
3734
3954
  }
3735
- if (await fileExists(consumerAbs)) preserved.push(resource.target);
3736
- if (upstreamAbs) {
3737
- const upstreamContent = await fs16.readFile(upstreamAbs, "utf-8");
3738
- const upstreamHash = computeHash(upstreamContent);
3739
- if (resource.hash && upstreamHash !== resource.hash) {
3740
- frozenDrift.push({
3741
- target: resource.target,
3742
- reason: "upstream-changed"
3743
- });
3744
- }
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;
3745
3960
  }
3746
- refreshedResources.push(resource);
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
+ );
3747
3968
  }
3748
- if (variantEntry.version === currentVersion) {
3749
- if (installedIdx >= 0) {
3750
- prior.installed[installedIdx] = {
3751
- ...prior.installed[installedIdx],
3752
- resources: refreshedResources
3753
- };
3754
- await writeInstalledManifest(projectRoot, prior);
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");
3755
3983
  }
3756
- return {
3757
- status: "up-to-date",
3758
- packageName,
3759
- variant: currentVariant,
3760
- version: currentVersion,
3761
- frozenDrift
3762
- };
3763
3984
  }
3764
- const lock = {
3765
- schemaVersion: 1,
3766
- variant: {
3767
- name: variantEntry.name,
3768
- displayName: variantEntry.displayName,
3769
- version: variantEntry.version,
3770
- from: packageName
3771
- },
3772
- packageVersion: catalog.version,
3773
- linked: variantEntry.linked,
3774
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
3775
- };
3776
- await writeFileSafe(
3777
- path21.join(projectRoot, ".teamix-evo", "tokens-lock.json"),
3778
- JSON.stringify(lock, null, 2) + "\n"
3779
- );
3780
- config.packages.tokens.version = variantEntry.version;
3781
- await writeProjectConfig(projectRoot, config);
3782
- if (installedIdx >= 0) {
3783
- prior.installed[installedIdx] = {
3784
- ...prior.installed[installedIdx],
3785
- version: variantEntry.version,
3786
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
3787
- resources: refreshedResources
3788
- };
3789
- await writeInstalledManifest(projectRoot, prior);
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
+ }
3790
4103
  }
3791
- const renames = selectApplicableRenames(
3792
- variantEntry.renames ?? [],
3793
- currentVersion,
3794
- variantEntry.version
3795
- );
3796
- let hintPath;
3797
- if (renames.length > 0) {
3798
- const hint = await writeTokensUpgradeHint({
3799
- projectRoot,
3800
- trigger: "update",
3801
- fromVariant: currentVariant,
3802
- toVariant: currentVariant,
3803
- fromVersion: currentVersion,
3804
- toVersion: variantEntry.version,
3805
- renames
3806
- });
3807
- if (hint) hintPath = hint.path;
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
+ }
3808
4134
  }
3809
4135
  return {
3810
- status: "updated",
3811
- packageName,
3812
- variant: currentVariant,
3813
- from: currentVersion,
3814
- to: variantEntry.version,
3815
- rewritten,
3816
- managedReplaced,
3817
- preserved,
3818
- frozenDrift,
3819
- renames,
3820
- ...hintPath ? { hintPath } : {}
4136
+ filesScanned,
4137
+ entries,
4138
+ affectedFiles: affectedFileSet.size
3821
4139
  };
3822
4140
  }
3823
- function lookupUpstreamBasename(consumerBasename) {
3824
- for (const [upstream, consumer] of Object.entries(
3825
- CONSUMER_BASENAME_BY_UPSTREAM
3826
- )) {
3827
- if (consumer === consumerBasename) return upstream;
3828
- }
3829
- return consumerBasename;
3830
- }
3831
4141
 
3832
- // src/core/ui-upgrade-detector.ts
3833
- import * as fs17 from "fs/promises";
3834
- import * as path22 from "path";
3835
- var PACKAGE_NAME = {
3836
- ui: "@teamix-evo/ui",
3837
- "biz-ui": "@teamix-evo/biz-ui"
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"
3838
4151
  };
3839
- var ALIAS_KEY = {
3840
- ui: "components",
3841
- "biz-ui": "business"
3842
- };
3843
- var COMPONENT_FILE_RE = /\.(tsx|ts)$/;
3844
- var SKIP_FILENAMES = /* @__PURE__ */ new Set(["index.ts", "index.tsx"]);
3845
- async function detectComponentLineage(options) {
3846
- const { projectRoot, category } = options;
3847
- const config = options.config ?? await readProjectConfig(projectRoot);
3848
- const installed = options.installed ?? await readInstalledManifest(projectRoot);
3849
- const installDir = resolveInstallDir(category, config);
3850
- const installDirAbs = path22.join(projectRoot, installDir);
3851
- const installDirExists = await directoryExists(installDirAbs);
3852
- const hasComponentsJson = await fileExists(
3853
- path22.join(projectRoot, "components.json")
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"
3854
4205
  );
3855
- const installedPkg = findInstalledPackage(installed, PACKAGE_NAME[category]);
3856
- const registeredIds = installedPkg ? extractIds(installedPkg).sort() : [];
3857
- const onDiskIds = installDirExists ? await listComponentIds(installDirAbs) : [];
3858
- const registeredSet = new Set(registeredIds);
3859
- const unregisteredIds = onDiskIds.filter((id) => !registeredSet.has(id)).sort();
3860
- const lineage = classifyLineage({
3861
- hasInstalled: installedPkg !== null,
3862
- hasComponentsJson,
3863
- onDiskIds,
3864
- unregisteredIds
3865
- });
3866
- return {
3867
- category,
3868
- lineage,
3869
- installDir,
3870
- installDirExists,
3871
- hasComponentsJson,
3872
- registeredIds,
3873
- unregisteredIds,
3874
- installedVersion: installedPkg?.version ?? null,
3875
- installedVariant: installedPkg?.variant ?? null
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"
3876
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 };
3877
4244
  }
3878
- function resolveInstallDir(category, config) {
3879
- const aliasMap = config?.packages?.ui?.aliases ?? config?.packages?.["biz-ui"]?.aliases ?? DEFAULT_UI_ALIASES;
3880
- const key = ALIAS_KEY[category];
3881
- return aliasMap[key] ?? DEFAULT_UI_ALIASES[key];
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;
3882
4276
  }
3883
- function extractIds(pkg) {
3884
- const ids = /* @__PURE__ */ new Set();
3885
- for (const r of pkg.resources) {
3886
- const colon = r.id.indexOf(":");
3887
- ids.add(colon >= 0 ? r.id.slice(0, colon) : r.id);
4277
+ async function pruneSnapshots(projectRoot, keep = DEFAULT_KEEP, opts = {}) {
4278
+ if (keep < 0)
4279
+ throw new Error(`pruneSnapshots: keep must be >= 0, got ${keep}`);
4280
+ const snapshots = await listSnapshots(projectRoot);
4281
+ if (snapshots.length <= keep) return [];
4282
+ const tail = snapshots.slice(keep);
4283
+ const toRemove = opts.protectedTs ? tail.filter((s) => s.ts !== opts.protectedTs) : tail;
4284
+ const removed = [];
4285
+ for (const snap of toRemove) {
4286
+ await fs20.rm(snap.path, { recursive: true, force: true });
4287
+ removed.push(snap.ts);
4288
+ logger.debug(`Pruned snapshot ${snap.ts}`);
3888
4289
  }
3889
- return [...ids];
4290
+ return removed.reverse();
3890
4291
  }
3891
- async function directoryExists(p) {
3892
- try {
3893
- const stat5 = await fs17.stat(p);
3894
- return stat5.isDirectory();
3895
- } catch {
3896
- return false;
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);
3897
4300
  }
4301
+ return rel2.split(path27.sep).join("/");
3898
4302
  }
3899
- async function listComponentIds(installDirAbs) {
3900
- const entries = await fs17.readdir(installDirAbs, { withFileTypes: true });
3901
- const ids = [];
3902
- for (const e of entries) {
3903
- if (!e.isFile()) continue;
3904
- if (SKIP_FILENAMES.has(e.name)) continue;
3905
- if (!COMPONENT_FILE_RE.test(e.name)) continue;
3906
- ids.push(e.name.replace(COMPONENT_FILE_RE, ""));
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
+ }
3907
4326
  }
3908
- return ids.sort();
4327
+ return out;
3909
4328
  }
3910
- function classifyLineage(args) {
3911
- const { hasInstalled, hasComponentsJson, onDiskIds, unregisteredIds } = args;
3912
- if (hasInstalled) {
3913
- return unregisteredIds.length === 0 ? "teamix-evo" : "mixed";
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);
3914
4339
  }
3915
- if (onDiskIds.length === 0) return "absent";
3916
- return hasComponentsJson ? "shadcn-native" : "custom-only";
4340
+ return out;
3917
4341
  }
3918
4342
 
3919
- // src/core/ui-upgrade.ts
3920
- import * as path24 from "path";
3921
- import { createRequire as createRequire5 } from "module";
3922
- import {
3923
- loadUiPackageManifest as loadUiPackageManifest3,
3924
- loadVariantUiPackageManifest as loadVariantUiPackageManifest2
3925
- } from "@teamix-evo/registry";
3926
-
3927
- // src/core/ui-upgrade-staging.ts
3928
- import * as path23 from "path";
3929
- var TEAMIX_DIR4 = ".teamix-evo";
3930
- var STAGING_DIR = ".upgrade-staging";
3931
- var PACKAGE_NAME2 = {
3932
- ui: "@teamix-evo/ui",
3933
- "biz-ui": "@teamix-evo/biz-ui"
4343
+ // src/core/project-init.ts
4344
+ import * as fsNode from "fs/promises";
4345
+ import * as path28 from "path";
4346
+ var BASELINE_UI_ENTRIES = [
4347
+ "button",
4348
+ "button-group",
4349
+ "input",
4350
+ "form",
4351
+ "card",
4352
+ "collapsible",
4353
+ "dialog",
4354
+ "dropdown-menu",
4355
+ "tabs",
4356
+ "table",
4357
+ "sidebar",
4358
+ "page-shell",
4359
+ "page-header"
4360
+ ];
4361
+ var CRITICAL_STEPS = /* @__PURE__ */ new Set([
4362
+ "tokens",
4363
+ "skills",
4364
+ "ui-init"
4365
+ ]);
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`.
4370
+ "agents-md": ["merge-managed", "overwrite", "skip"],
4371
+ tokens: ["migrate", "overwrite", "skip"],
4372
+ "components-json": ["overwrite", "skip"],
4373
+ "shadcn-source": ["overwrite", "skip-existing", "skip"],
4374
+ "tailwind-config": ["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"]
3934
4380
  };
3935
- function isoToFsSafe3(iso) {
3936
- return iso.replace(/[:.]/g, "-");
4381
+ function pickIde(ides) {
4382
+ return ides[0] ?? "qoder";
3937
4383
  }
3938
- async function buildUiUpgradeStaging(options) {
3939
- const { lineageReport, category } = options;
3940
- if (lineageReport.lineage !== "teamix-evo" && lineageReport.lineage !== "mixed") {
3941
- return null;
4384
+ function strategyImplemented(key, strategy) {
4385
+ return IMPLEMENTED_STRATEGIES[key]?.includes(strategy) ?? false;
4386
+ }
4387
+ async function resolveUiEntries(options) {
4388
+ if (options.uiEntries && options.uiEntries.length > 0) {
4389
+ return [...options.uiEntries];
3942
4390
  }
3943
- const installed = options.installed ?? await readInstalledManifest(options.projectRoot);
3944
- const installedPkg = findInstalledPackage(installed, PACKAGE_NAME2[category]);
3945
- if (!installedPkg) return null;
3946
- const isoTs = options.isoTs ?? (/* @__PURE__ */ new Date()).toISOString();
3947
- const fsTs = isoToFsSafe3(isoTs);
3948
- const stagingDir = path23.join(
3949
- options.projectRoot,
3950
- TEAMIX_DIR4,
3951
- STAGING_DIR,
3952
- `${category}-${fsTs}`
3953
- );
3954
- const entryMap = new Map(
3955
- options.manifest.entries.map((e) => [e.id, e])
3956
- );
3957
- const resByEntryId = collectResourcesByEntry(installedPkg.resources);
3958
- const onlyIds = options.onlyIds && options.onlyIds.length > 0 ? new Set(options.onlyIds) : null;
3959
- const entries = [];
3960
- for (const id of lineageReport.registeredIds) {
3961
- if (onlyIds && !onlyIds.has(id)) continue;
3962
- const built = await processRegistered({
3963
- id,
3964
- entry: entryMap.get(id),
3965
- resource: resByEntryId.get(id),
3966
- packageRoot: options.packageRoot,
3967
- entryPackageRoot: options.entryPackageRoot,
3968
- aliases: options.aliases,
3969
- stagingDir,
3970
- projectRoot: options.projectRoot,
3971
- category,
3972
- sourceVersion: options.manifest.version
3973
- });
3974
- if (built) entries.push(built);
4391
+ if (options.answers.uiSelection === "all") {
4392
+ const { manifest } = await loadUiData("@teamix-evo/ui");
4393
+ return manifest.entries.map((e) => e.id);
3975
4394
  }
3976
- for (const id of lineageReport.unregisteredIds) {
3977
- if (onlyIds && !onlyIds.has(id)) continue;
3978
- const built = await processForeign({
3979
- id,
3980
- installDirAbs: path23.join(options.projectRoot, lineageReport.installDir),
3981
- stagingDir,
3982
- projectRoot: options.projectRoot,
3983
- category
4395
+ return [...BASELINE_UI_ENTRIES];
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
3984
4425
  });
3985
- if (built) entries.push(built);
4426
+ remaining--;
3986
4427
  }
3987
- if (entries.length === 0) return null;
3988
- const byRisk = aggregateByRisk(entries);
3989
- const manifestOut = {
3990
- schemaVersion: 1,
3991
- ts: isoTs,
3992
- package: category,
3993
- trigger: options.trigger,
3994
- variant: lineageReport.installedVariant ?? "_flat",
3995
- fromVersion: lineageReport.installedVersion ?? "",
3996
- toVersion: options.manifest.version,
3997
- lineage: lineageReport.lineage,
3998
- summary: { total: entries.length, byRisk },
3999
- entries
4000
- };
4001
- await ensureDir(stagingDir);
4002
- await writeFileSafe(
4003
- path23.join(stagingDir, "meta.json"),
4004
- JSON.stringify(manifestOut, null, 2) + "\n"
4005
- );
4006
- return { stagingDir, manifest: manifestOut };
4428
+ return out;
4007
4429
  }
4008
- async function processRegistered(args) {
4009
- const { id, entry, resource, stagingDir, projectRoot, category } = args;
4010
- if (!resource) return null;
4011
- const currentSource = await readFileOrNull(resource.target);
4012
- if (currentSource === null) {
4013
- return buildBreakingEntry({
4014
- id,
4015
- category,
4016
- resource,
4017
- projectRoot,
4018
- stagingDir,
4019
- currentSource: "",
4020
- hint: "installed file missing on disk"
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"
4021
4439
  });
4022
4440
  }
4023
- if (!entry) {
4024
- return buildBreakingEntry({
4025
- id,
4026
- category,
4027
- resource,
4028
- projectRoot,
4029
- stagingDir,
4030
- currentSource,
4031
- hint: "entry removed in upstream package"
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"
4032
4447
  });
4033
4448
  }
4034
- const file = entry.files[0];
4035
- if (!file) return null;
4036
- const rootForEntry = args.entryPackageRoot?.get(id) ?? args.packageRoot;
4037
- const sourceAbs = path23.resolve(rootForEntry, file.source);
4038
- const raw = await readFileOrNull(sourceAbs);
4039
- if (raw === null) {
4040
- return null;
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
+ });
4041
4456
  }
4042
- const incomingTransformed = rewriteImports(raw, args.aliases);
4043
- const incomingHash = computeHash(incomingTransformed);
4044
- const currentExt = path23.extname(resource.target) || ".tsx";
4045
- const incomingExt = path23.extname(file.targetName) || currentExt;
4046
- const currentRel = `${id}/current${currentExt}`;
4047
- const incomingRel = `${id}/incoming${incomingExt}`;
4048
- await writeFileSafe(path23.join(stagingDir, currentRel), currentSource);
4049
- await writeFileSafe(path23.join(stagingDir, incomingRel), incomingTransformed);
4050
- const diff = classifyRisk({
4051
- currentHash: resource.hash,
4052
- incomingHash,
4053
- currentSource,
4054
- incomingSource: incomingTransformed,
4055
- multiFile: entry.files.length > 1
4056
- });
4057
- const promotion = derivePromotion({
4058
- currentSource,
4059
- incomingSource: incomingTransformed,
4060
- targetName: entry.files[0]?.targetName ?? `${id}.tsx`
4061
- });
4062
- return {
4063
- id,
4064
- category,
4065
- current: {
4066
- target: path23.relative(projectRoot, resource.target),
4067
- hash: resource.hash,
4068
- sourceLineage: "teamix-evo"
4069
- },
4070
- incoming: {
4071
- sourceVersion: args.sourceVersion,
4072
- hash: incomingHash,
4073
- relPath: incomingRel
4074
- },
4075
- diff,
4076
- promotion
4077
- };
4457
+ return out;
4078
4458
  }
4079
- async function processForeign(args) {
4080
- const { id, installDirAbs, stagingDir, projectRoot, category } = args;
4081
- const tsx = path23.join(installDirAbs, `${id}.tsx`);
4082
- const ts = path23.join(installDirAbs, `${id}.ts`);
4083
- const target = await fileExists(tsx) ? tsx : await fileExists(ts) ? ts : null;
4084
- if (!target) return null;
4085
- const raw = await readFileOrNull(target);
4086
- if (raw === null) return null;
4087
- const ext = path23.extname(target);
4088
- const currentRel = `${id}/current${ext}`;
4089
- await writeFileSafe(path23.join(stagingDir, currentRel), raw);
4090
- return {
4091
- id,
4092
- category,
4093
- current: {
4094
- target: path23.relative(projectRoot, target),
4095
- hash: computeHash(raw),
4096
- sourceLineage: "custom"
4097
- },
4098
- diff: {
4099
- riskLevel: "foreign",
4100
- hints: [
4101
- "component is on disk but not registered in .teamix-evo/manifest.json",
4102
- "AI should propose: (a) ignore, (b) re-register via teamix-evo ui add, or (c) remove"
4103
- ],
4104
- filesChangedCount: 0
4459
+ async function runProjectInit(options) {
4460
+ const { projectRoot, answers, dryRun = false, onStep } = options;
4461
+ const ide = pickIde(answers.ides);
4462
+ const steps = [];
4463
+ const pending = [];
4464
+ const allChanges = [];
4465
+ const backupsBefore = dryRun ? /* @__PURE__ */ new Set() : await listBackupOriginals(projectRoot).catch(() => /* @__PURE__ */ new Set());
4466
+ let snapshot = null;
4467
+ let snapshotError;
4468
+ if (!dryRun) {
4469
+ try {
4470
+ snapshot = await createSnapshot(projectRoot, { reason: "init" });
4471
+ } catch (err) {
4472
+ snapshotError = getErrorMessage(err);
4105
4473
  }
4474
+ }
4475
+ let aborted = false;
4476
+ const firstFailure = {
4477
+ value: null
4106
4478
  };
4107
- }
4108
- async function buildBreakingEntry(args) {
4109
- const ext = path23.extname(args.resource.target) || ".tsx";
4110
- const currentRel = `${args.id}/current${ext}`;
4111
- await writeFileSafe(
4112
- path23.join(args.stagingDir, currentRel),
4113
- args.currentSource
4114
- );
4115
- return {
4116
- id: args.id,
4117
- category: args.category,
4118
- current: {
4119
- target: path23.relative(args.projectRoot, args.resource.target),
4120
- hash: args.resource.hash,
4121
- sourceLineage: "teamix-evo"
4122
- },
4123
- diff: {
4124
- riskLevel: "breaking",
4125
- hints: [args.hint],
4126
- filesChangedCount: 0
4479
+ function record(step) {
4480
+ steps.push(step);
4481
+ onStep?.(step);
4482
+ if (step.changes && step.changes.length > 0) {
4483
+ allChanges.push(...step.changes);
4484
+ }
4485
+ }
4486
+ function recordPending(key) {
4487
+ const strategy = answers.conflictDecisions[key];
4488
+ if (!strategy) return;
4489
+ if (strategyImplemented(key, strategy)) return;
4490
+ pending.push({
4491
+ key,
4492
+ strategy,
4493
+ reason: `Strategy "${strategy}" requires the managed-region engine (batch 4); recorded for follow-up.`
4494
+ });
4495
+ }
4496
+ function recordFailure(name, err) {
4497
+ const message = getErrorMessage(err);
4498
+ record({ name, status: "fail", detail: message });
4499
+ if (!firstFailure.value)
4500
+ firstFailure.value = { step: name, error: message };
4501
+ if (CRITICAL_STEPS.has(name)) aborted = true;
4502
+ }
4503
+ const tokensDecision = answers.conflictDecisions.tokens;
4504
+ const legacyTokensPaths = options.legacyTokensPaths ?? [];
4505
+ const wantMigrate = tokensDecision === "migrate" && legacyTokensPaths.length > 0;
4506
+ if (tokensDecision === "skip") {
4507
+ record({
4508
+ name: "tokens",
4509
+ status: "skip",
4510
+ detail: "conflict strategy = skip"
4511
+ });
4512
+ } else if (dryRun) {
4513
+ const planDetail = wantMigrate ? `runTokensInit(variant=${answers.variant}); migrateLegacyTokens(${legacyTokensPaths.length} file${legacyTokensPaths.length === 1 ? "" : "s"})` : `runTokensInit(variant=${answers.variant})`;
4514
+ record({
4515
+ name: "tokens",
4516
+ status: "planned",
4517
+ detail: planDetail
4518
+ });
4519
+ } else {
4520
+ try {
4521
+ const result = await runTokensInit({
4522
+ projectRoot,
4523
+ variant: answers.variant,
4524
+ ide
4525
+ });
4526
+ let detail = result.status === "installed" ? `${result.packageName}@${result.version} (${result.count} files)` : result.status;
4527
+ if (wantMigrate) {
4528
+ try {
4529
+ const m = await migrateLegacyTokens({
4530
+ projectRoot,
4531
+ legacyPaths: legacyTokensPaths
4532
+ });
4533
+ detail += `; migrated ${m.migrated.length}/${legacyTokensPaths.length} legacy file${legacyTokensPaths.length === 1 ? "" : "s"} \u2192 ${m.overridesPath}`;
4534
+ if (m.skipped.length > 0) {
4535
+ detail += ` (skipped ${m.skipped.length})`;
4536
+ }
4537
+ } catch (err) {
4538
+ detail += `; migrate failed: ${getErrorMessage(err)}`;
4539
+ }
4540
+ }
4541
+ record({
4542
+ name: "tokens",
4543
+ status: "ok",
4544
+ detail,
4545
+ changes: deriveTokensChanges(result, projectRoot)
4546
+ });
4547
+ } catch (err) {
4548
+ recordFailure("tokens", err);
4549
+ }
4550
+ }
4551
+ recordPending("tokens");
4552
+ const codeSkillId = `teamix-evo-code-${answers.variant}`;
4553
+ if (dryRun) {
4554
+ record({
4555
+ name: "skills",
4556
+ status: "planned",
4557
+ detail: `runSkillsAdd(${codeSkillId})`
4558
+ });
4559
+ } else if (aborted) {
4560
+ record({
4561
+ name: "skills",
4562
+ status: "skip",
4563
+ detail: "aborted: earlier critical step failed"
4564
+ });
4565
+ } else {
4566
+ try {
4567
+ const result = await runSkillsAdd({
4568
+ projectRoot,
4569
+ names: [codeSkillId],
4570
+ ides: answers.ides,
4571
+ scope: answers.scope,
4572
+ ide
4573
+ });
4574
+ record({
4575
+ name: "skills",
4576
+ status: "ok",
4577
+ detail: result.status === "installed" ? `added: ${result.addedSkillIds.join(", ") || "none"}; existing: ${result.skippedSkillIds.join(", ") || "none"}` : result.status,
4578
+ changes: deriveSkillsChanges(result, projectRoot)
4579
+ });
4580
+ } catch (err) {
4581
+ recordFailure("skills", err);
4582
+ }
4583
+ }
4584
+ const agentsMdDecision = answers.conflictDecisions["agents-md"];
4585
+ const agentsMdSkillIds = [
4586
+ `teamix-evo-design-${answers.variant}`,
4587
+ codeSkillId
4588
+ ];
4589
+ if (agentsMdDecision === "skip") {
4590
+ record({
4591
+ name: "agents-md",
4592
+ status: "skip",
4593
+ detail: "conflict strategy = skip"
4594
+ });
4595
+ } else if (dryRun) {
4596
+ record({
4597
+ name: "agents-md",
4598
+ status: "planned",
4599
+ detail: `runGenerateAgentsMd(${agentsMdSkillIds.length} skills)`
4600
+ });
4601
+ } else {
4602
+ try {
4603
+ const result = await runGenerateAgentsMd({
4604
+ projectRoot,
4605
+ variant: answers.variant,
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"
4612
+ });
4613
+ record({
4614
+ name: "agents-md",
4615
+ status: "ok",
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
+ ]
4625
+ });
4626
+ } catch (err) {
4627
+ recordFailure("agents-md", err);
4127
4628
  }
4128
- };
4129
- }
4130
- function classifyRisk(args) {
4131
- if (args.currentHash === args.incomingHash) {
4132
- return { riskLevel: "unchanged", hints: [], filesChangedCount: 0 };
4133
4629
  }
4134
- const curExports = extractExportNames(args.currentSource);
4135
- const newExports = extractExportNames(args.incomingSource);
4136
- const removedExports = setDiff(curExports, newExports);
4137
- const addedExports = setDiff(newExports, curExports);
4138
- const curVariants = extractCvaVariantValues(args.currentSource);
4139
- const newVariants = extractCvaVariantValues(args.incomingSource);
4140
- const removedVariants = setDiff(curVariants, newVariants);
4141
- const addedVariants = setDiff(newVariants, curVariants);
4142
- const hints = [];
4143
- for (const e of removedExports) hints.push(`removed export: ${e}`);
4144
- for (const e of addedExports) hints.push(`new export: ${e}`);
4145
- for (const v of removedVariants) hints.push(`removed cva variant: ${v}`);
4146
- for (const v of addedVariants) hints.push(`new cva variant: ${v}`);
4147
- if (args.multiFile) hints.push("multi-file entry; only first file staged");
4148
- let riskLevel;
4149
- if (removedExports.length > 0 || removedVariants.length > 0) {
4150
- riskLevel = "risky";
4151
- } else if (addedExports.length > 0 || addedVariants.length > 0 || args.multiFile) {
4152
- riskLevel = "upgradable-medium";
4630
+ recordPending("agents-md");
4631
+ const componentsJsonDecision = answers.conflictDecisions["components-json"];
4632
+ const shadcnDecision = answers.conflictDecisions["shadcn-source"];
4633
+ const skipUiInit = !answers.withUi || componentsJsonDecision === "skip";
4634
+ let uiDecisionRequired;
4635
+ if (skipUiInit) {
4636
+ record({
4637
+ name: "ui-init",
4638
+ status: "skip",
4639
+ detail: !answers.withUi ? "withUi = false" : "components.json conflict strategy = skip"
4640
+ });
4641
+ record({
4642
+ name: "ui-add",
4643
+ status: "skip",
4644
+ detail: !answers.withUi ? "withUi = false" : "components.json conflict strategy = skip"
4645
+ });
4646
+ } else if (dryRun) {
4647
+ record({
4648
+ name: "ui-init",
4649
+ status: "planned",
4650
+ detail: "runUiInit()"
4651
+ });
4652
+ const entries = await resolveUiEntries(options).catch(() => [
4653
+ ...BASELINE_UI_ENTRIES
4654
+ ]);
4655
+ record({
4656
+ name: "ui-add",
4657
+ status: "planned",
4658
+ detail: `runUiAdd(${entries.length} entries: ${entries.slice(0, 3).join(", ")}${entries.length > 3 ? "\u2026" : ""})`
4659
+ });
4153
4660
  } else {
4154
- riskLevel = "upgradable-low";
4155
- }
4156
- return { riskLevel, hints, filesChangedCount: 1 };
4157
- }
4158
- function extractExportNames(src) {
4159
- const names = /* @__PURE__ */ new Set();
4160
- const re = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/gm;
4161
- let m;
4162
- while ((m = re.exec(src)) !== null) {
4163
- if (m[1]) names.add(m[1]);
4164
- }
4165
- for (const dm of src.matchAll(/^\s*export\s+default\s+(\w+)\s*;/gm)) {
4166
- if (dm[1]) names.add(dm[1]);
4661
+ if (aborted) {
4662
+ record({
4663
+ name: "ui-init",
4664
+ status: "skip",
4665
+ detail: "aborted: earlier critical step failed"
4666
+ });
4667
+ record({
4668
+ name: "ui-add",
4669
+ status: "skip",
4670
+ detail: "aborted: earlier critical step failed"
4671
+ });
4672
+ } else {
4673
+ let uiInitOk = false;
4674
+ try {
4675
+ const initResult = await runUiInit({ projectRoot, ide });
4676
+ record({
4677
+ name: "ui-init",
4678
+ status: "ok",
4679
+ detail: initResult.status === "installed" ? "config.json packages.ui written" : initResult.status
4680
+ });
4681
+ uiInitOk = true;
4682
+ } catch (err) {
4683
+ recordFailure("ui-init", err);
4684
+ }
4685
+ if (!uiInitOk) {
4686
+ record({
4687
+ name: "ui-add",
4688
+ status: "skip",
4689
+ detail: "aborted: ui-init failed"
4690
+ });
4691
+ } else if (shadcnDecision === "skip") {
4692
+ record({
4693
+ name: "ui-add",
4694
+ status: "skip",
4695
+ detail: "shadcn-source conflict strategy = skip"
4696
+ });
4697
+ } else {
4698
+ try {
4699
+ const entries = await resolveUiEntries(options);
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({
4706
+ projectRoot,
4707
+ aliases,
4708
+ manifest
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
+ }
4871
+ } catch (err) {
4872
+ recordFailure("ui-add", err);
4873
+ }
4874
+ }
4875
+ }
4167
4876
  }
4168
- return [...names];
4169
- }
4170
- function extractCvaVariantValues(src) {
4171
- const block = extractVariantsBlock(src);
4172
- if (block === null) return [];
4173
- const names = /* @__PURE__ */ new Set();
4174
- for (const groupBody of extractGroupBodies(block)) {
4175
- for (const km of groupBody.matchAll(/^\s*(?:['"]?)(\w+)(?:['"]?)\s*:/gm)) {
4176
- if (km[1]) names.add(km[1]);
4877
+ recordPending("components-json");
4878
+ recordPending("shadcn-source");
4879
+ if (!answers.withLint) {
4880
+ record({
4881
+ name: "lint",
4882
+ status: "skip",
4883
+ detail: "withLint = false"
4884
+ });
4885
+ } else if (dryRun) {
4886
+ record({
4887
+ name: "lint",
4888
+ status: "planned",
4889
+ detail: "runLintInit()"
4890
+ });
4891
+ } else {
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 ?? [];
4897
+ const result = await runLintInit({
4898
+ projectRoot,
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
4904
+ });
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);
4923
+ }
4924
+ record({
4925
+ name: "lint",
4926
+ status: "ok",
4927
+ detail: detailParts.join(" / "),
4928
+ changes: deriveLintChanges(result)
4929
+ });
4930
+ } catch (err) {
4931
+ recordFailure("lint", err);
4177
4932
  }
4178
4933
  }
4179
- return [...names];
4180
- }
4181
- function extractVariantsBlock(src) {
4182
- const idx = src.search(/\bvariants\s*:\s*\{/);
4183
- if (idx < 0) return null;
4184
- const open = src.indexOf("{", idx);
4185
- if (open < 0) return null;
4186
- let depth = 0;
4187
- for (let i = open; i < src.length; i++) {
4188
- const c = src[i];
4189
- if (c === "{") depth++;
4190
- else if (c === "}") {
4191
- depth--;
4192
- if (depth === 0) return src.slice(open + 1, i);
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
+ ]
4995
+ });
4996
+ }
4997
+ } catch (err) {
4998
+ if (err && typeof err === "object" && "__skip" in err) {
4999
+ } else {
5000
+ recordFailure("gitignore", err);
5001
+ }
4193
5002
  }
4194
5003
  }
4195
- return null;
4196
- }
4197
- function* extractGroupBodies(block) {
4198
- const re = /(\w+)\s*:\s*\{/g;
4199
- let m;
4200
- while ((m = re.exec(block)) !== null) {
4201
- const open = block.indexOf("{", m.index);
4202
- if (open < 0) continue;
4203
- let depth = 0;
4204
- for (let i = open; i < block.length; i++) {
4205
- const c = block[i];
4206
- if (c === "{") depth++;
4207
- else if (c === "}") {
4208
- depth--;
4209
- if (depth === 0) {
4210
- yield block.slice(open + 1, i);
4211
- re.lastIndex = i + 1;
4212
- break;
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
+ });
4213
5021
  }
4214
5022
  }
5023
+ } catch {
4215
5024
  }
4216
5025
  }
4217
- }
4218
- function setDiff(a, b) {
4219
- const bset = new Set(b);
4220
- return a.filter((x) => !bset.has(x)).sort();
4221
- }
4222
- function collectResourcesByEntry(resources) {
4223
- const out = /* @__PURE__ */ new Map();
4224
- for (const r of resources) {
4225
- const colon = r.id.indexOf(":");
4226
- const eid = colon >= 0 ? r.id.slice(0, colon) : r.id;
4227
- if (!out.has(eid)) out.set(eid, r);
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
5033
+ };
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"
5047
+ };
4228
5048
  }
4229
- return out;
4230
- }
4231
- function aggregateByRisk(entries) {
4232
- const out = {};
4233
- for (const e of entries) {
4234
- const k = e.diff.riskLevel;
4235
- out[k] = (out[k] ?? 0) + 1;
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
+ }
4236
5067
  }
4237
5068
  return out;
4238
5069
  }
4239
- function derivePromotion(args) {
4240
- const fileType = classifyPromoteFileType(args.targetName, args.currentSource);
4241
- const featureVector = buildFeatureVector(
4242
- args.currentSource,
4243
- args.incomingSource
4244
- );
4245
- const { recommendedModes, confidence, reasons } = scorePromotionModes(
4246
- fileType,
4247
- featureVector
4248
- );
4249
- return { fileType, featureVector, recommendedModes, confidence, reasons };
4250
- }
4251
- function classifyPromoteFileType(targetName, src) {
4252
- if (targetName.endsWith(".d.ts")) return "type";
4253
- if (/^use-[a-z0-9-]+\.tsx?$/i.test(targetName)) return "hook";
4254
- const hasJsx = /<[A-Za-z][^>]*?>/.test(src);
4255
- const hasReactImport = /from ['"]react['"]/.test(src);
4256
- const hasProvider = /\.Provider\b/.test(src) || /createContext\s*[<(]/.test(src);
4257
- if (hasProvider && (hasJsx || hasReactImport)) return "provider";
4258
- if (hasJsx || /forwardRef\s*[<(]/.test(src)) return "component";
4259
- if (!hasJsx && !hasReactImport) return "util";
4260
- return "component";
5070
+
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";
5077
+
5078
+ // src/core/tokens-update.ts
5079
+ import * as path30 from "path";
5080
+ import * as fs22 from "fs/promises";
5081
+ import {
5082
+ loadTokensPackageManifest as loadTokensPackageManifest2,
5083
+ getVariantEntry as getVariantEntry2
5084
+ } from "@teamix-evo/registry";
5085
+
5086
+ // src/core/upgrade-hints.ts
5087
+ import * as path29 from "path";
5088
+ var TEAMIX_DIR4 = ".teamix-evo";
5089
+ var HINTS_DIR = ".upgrade-hints";
5090
+ function isoToFsSafe3(iso) {
5091
+ return iso.replace(/[:.]/g, "-");
4261
5092
  }
4262
- function buildFeatureVector(current, incoming) {
4263
- const curExports = extractExportNames(current);
4264
- const newExports = extractExportNames(incoming);
4265
- const apiAdded = setDiff(curExports, newExports);
4266
- const apiRemoved = setDiff(newExports, curExports);
4267
- const curVariants = extractCvaVariantValues(current);
4268
- const newVariants = extractCvaVariantValues(incoming);
4269
- const cvaAdded = setDiff(curVariants, newVariants);
4270
- const cvaModified = [];
4271
- const sharedVariants = curVariants.filter((v) => newVariants.includes(v));
4272
- for (const v of sharedVariants) {
4273
- if (extractVariantBody(current, v) !== extractVariantBody(incoming, v)) {
4274
- cvaModified.push(v);
4275
- }
4276
- }
4277
- const curClass = extractClassNameLiterals(current);
4278
- const newClass = extractClassNameLiterals(incoming);
4279
- const classNameDiff = curClass !== newClass;
4280
- const curTokens = extractTokenRefs(current);
4281
- const newTokens = extractTokenRefs(incoming);
4282
- const tokenUsageDiff = curTokens.size !== newTokens.size || [...curTokens].some((t) => !newTokens.has(t));
4283
- const hasState = /\buseState\s*[<(]/.test(current);
4284
- const hasEffect = /\b(useEffect|useLayoutEffect|useMemo|useCallback)\s*[<(]/.test(current);
4285
- const curImports = extractImportSources(current);
4286
- const newImports = extractImportSources(incoming);
4287
- const hasExtraImports = [...curImports].some((src) => !newImports.has(src));
4288
- const tagSet = /* @__PURE__ */ new Set();
4289
- for (const m of current.matchAll(/<([A-Z]\w+)[\s/>]/g)) {
4290
- if (m[1]) tagSet.add(m[1]);
4291
- }
4292
- const atomicChildren = [...tagSet];
4293
- const isComposition = atomicChildren.length > 2;
4294
- const signatureChanged = extractDefaultParamList(current) !== extractDefaultParamList(incoming);
5093
+ async function writeTokensUpgradeHint(options) {
5094
+ if (options.renames.length === 0) return null;
5095
+ const isoTs = options.isoTs ?? (/* @__PURE__ */ new Date()).toISOString();
5096
+ const fsTs = isoToFsSafe3(isoTs);
5097
+ const filename = `tokens-${fsTs}.json`;
5098
+ const target = path29.join(
5099
+ options.projectRoot,
5100
+ TEAMIX_DIR4,
5101
+ HINTS_DIR,
5102
+ filename
5103
+ );
5104
+ const payload = {
5105
+ schemaVersion: 1,
5106
+ ts: isoTs,
5107
+ package: "tokens",
5108
+ trigger: options.trigger,
5109
+ fromVariant: options.fromVariant,
5110
+ toVariant: options.toVariant,
5111
+ fromVersion: options.fromVersion,
5112
+ toVersion: options.toVersion,
5113
+ renames: options.renames
5114
+ };
5115
+ await writeFileSafe(target, JSON.stringify(payload, null, 2) + "\n");
4295
5116
  return {
4296
- apiDelta: { added: apiAdded, removed: apiRemoved, signatureChanged },
4297
- styleDelta: { classNameDiff, tokenUsageDiff },
4298
- logicDelta: { hasState, hasEffect, hasExtraImports },
4299
- cvaDelta: { addedVariants: cvaAdded, modifiedVariants: cvaModified },
4300
- structureDelta: { isComposition, atomicChildren }
5117
+ path: target,
5118
+ ts: fsTs,
5119
+ renameCount: options.renames.length
4301
5120
  };
4302
5121
  }
4303
- function scorePromotionModes(fileType, fv) {
4304
- const reasons = [];
4305
- if (fileType === "hook" || fileType === "util" || fileType === "type") {
4306
- reasons.push(
4307
- `fileType=${fileType} \u2014 not a component, deferred to ManualReview`
4308
- );
4309
- return { recommendedModes: ["ManualReview"], confidence: 0.5, reasons };
4310
- }
4311
- const apiNoChange = fv.apiDelta.added.length === 0 && fv.apiDelta.removed.length === 0 && !fv.apiDelta.signatureChanged;
4312
- const logicMinimal = !fv.logicDelta.hasState && !fv.logicDelta.hasEffect && !fv.logicDelta.hasExtraImports;
4313
- if (fv.apiDelta.removed.length > 0 || fv.apiDelta.signatureChanged) {
4314
- reasons.push(
4315
- "signature changed or props removed \u2014 Coexist preserves user version"
4316
- );
4317
- return { recommendedModes: ["Coexist"], confidence: 0.85, reasons };
4318
- }
4319
- if (apiNoChange && logicMinimal && (fv.styleDelta.classNameDiff || fv.styleDelta.tokenUsageDiff) && fv.cvaDelta.addedVariants.length === 0 && fv.cvaDelta.modifiedVariants.length === 0) {
4320
- reasons.push(
4321
- "only style / token differences \u2014 push to tokens.overrides.css"
4322
- );
4323
- return { recommendedModes: ["TokenOnly"], confidence: 0.8, reasons };
4324
- }
4325
- const modes = [];
4326
- let score = 0;
4327
- if (fv.cvaDelta.addedVariants.length > 0 || fv.cvaDelta.modifiedVariants.length > 0) {
4328
- modes.push("Variant");
4329
- reasons.push(
4330
- `cva variants delta: +${fv.cvaDelta.addedVariants.length} ~${fv.cvaDelta.modifiedVariants.length}`
4331
- );
4332
- score = Math.max(score, 0.7);
4333
- }
4334
- if (fv.logicDelta.hasState || fv.logicDelta.hasEffect || fv.logicDelta.hasExtraImports || fv.apiDelta.added.length > 0) {
4335
- modes.push("Wrapper");
4336
- reasons.push("user added state / effect / imports / props");
4337
- score = Math.max(score, 0.75);
4338
- }
4339
- if (apiNoChange && logicMinimal && !fv.styleDelta.classNameDiff && !fv.styleDelta.tokenUsageDiff && fv.cvaDelta.addedVariants.length === 0 && fv.cvaDelta.modifiedVariants.length === 0) {
4340
- modes.push("Preset");
4341
- reasons.push("no API/logic delta \u2014 Preset captures default-prop tweaks");
4342
- score = Math.max(score, 0.6);
4343
- }
4344
- if (fv.structureDelta.isComposition) {
4345
- modes.push("Compose");
4346
- reasons.push(
4347
- `composition of ${fv.structureDelta.atomicChildren.length} atomic children`
4348
- );
4349
- score = Math.max(score, 0.65);
4350
- }
4351
- if (modes.length === 0) {
4352
- reasons.push("no axis crossed the 0.6 threshold");
4353
- return { recommendedModes: ["ManualReview"], confidence: 0.4, reasons };
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));
5126
+ }
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;
4354
5136
  }
4355
- return { recommendedModes: modes, confidence: score, reasons };
5137
+ if (aRest === "" && bRest !== "") return 1;
5138
+ if (aRest !== "" && bRest === "") return -1;
5139
+ return aRest.localeCompare(bRest, void 0, { numeric: true });
4356
5140
  }
4357
- function extractVariantBody(src, key) {
4358
- const block = extractVariantsBlock(src);
4359
- if (block === null) return "";
4360
- const re = new RegExp(`(?:["']?${key}["']?)\\s*:\\s*\\{`);
4361
- const idx = block.search(re);
4362
- if (idx < 0) return "";
4363
- const open = block.indexOf("{", idx);
4364
- if (open < 0) return "";
4365
- let depth = 0;
4366
- for (let i = open; i < block.length; i++) {
4367
- const c = block[i];
4368
- if (c === "{") depth++;
4369
- else if (c === "}") {
4370
- depth--;
4371
- if (depth === 0) return block.slice(open + 1, i);
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
+ );
4372
5155
  }
5156
+ updated = replaceManagedRegion3(updated, id, body);
4373
5157
  }
4374
- return "";
5158
+ return updated;
4375
5159
  }
4376
- function extractClassNameLiterals(src) {
4377
- const out = [];
4378
- for (const m of src.matchAll(/className\s*=\s*["'`]([^"'`]*)["'`]/g)) {
4379
- if (m[1]) out.push(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" };
4380
5173
  }
4381
- for (const m of src.matchAll(/\b(?:cn|clsx|cva)\s*\(/g)) {
4382
- const open = (m.index ?? 0) + m[0].length - 1;
4383
- let depth = 1;
4384
- let i = open + 1;
4385
- for (; i < src.length && depth > 0; i++) {
4386
- const c = src[i];
4387
- if (c === "(") depth++;
4388
- else if (c === ")") depth--;
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
+ );
5183
+ }
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
+ );
5190
+ }
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;
4389
5222
  }
4390
- const body = src.slice(open + 1, i - 1);
4391
- for (const lit of body.matchAll(/["'`]([^"'`]*)["'`]/g)) {
4392
- if (lit[1]) out.push(lit[1]);
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;
4393
5240
  }
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
+ });
5250
+ }
5251
+ }
5252
+ refreshedResources.push(resource);
4394
5253
  }
4395
- return out.sort().join("|");
4396
- }
4397
- function extractTokenRefs(src) {
4398
- const out = /* @__PURE__ */ new Set();
4399
- for (const m of src.matchAll(/var\(--([a-z0-9-]+)\)/g)) {
4400
- if (m[1]) out.add(m[1]);
4401
- }
4402
- for (const m of src.matchAll(/--([a-z][a-z0-9-]*)\s*:/g)) {
4403
- if (m[1]) out.add(m[1]);
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
+ };
4404
5269
  }
4405
- return out;
4406
- }
4407
- function extractImportSources(src) {
4408
- const out = /* @__PURE__ */ new Set();
4409
- for (const m of src.matchAll(/^\s*import\b[^'"]*['"]([^'"]+)['"]/gm)) {
4410
- if (m[1]) out.add(m[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);
4411
5296
  }
4412
- return out;
4413
- }
4414
- function extractDefaultParamList(src) {
4415
- 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);
4416
- return m?.[1]?.replace(/\s+/g, " ").trim() ?? "";
4417
- }
4418
-
4419
- // src/core/ui-upgrade.ts
4420
- var nodeRequire = createRequire5(import.meta.url);
4421
- function resolvePackageRoot4(packageName) {
4422
- const pkgJsonPath = nodeRequire.resolve(`${packageName}/package.json`);
4423
- return path24.dirname(pkgJsonPath);
4424
- }
4425
- async function buildStaging(args) {
4426
- const { category, projectRoot, aliases, lineageReport, trigger, onlyIds } = args;
4427
- if (category === "ui") {
4428
- const root = args.uiPackageRoot ?? resolvePackageRoot4("@teamix-evo/ui");
4429
- const manifest = await loadUiPackageManifest3(root);
4430
- 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({
4431
5305
  projectRoot,
4432
- category,
4433
- manifest,
4434
- packageRoot: root,
4435
- aliases,
4436
- lineageReport,
4437
- trigger,
4438
- onlyIds
5306
+ trigger: "update",
5307
+ fromVariant: currentVariant,
5308
+ toVariant: currentVariant,
5309
+ fromVersion: currentVersion,
5310
+ toVersion: variantEntry.version,
5311
+ renames
4439
5312
  });
5313
+ if (hint) hintPath = hint.path;
4440
5314
  }
4441
- const bizRoot = args.bizUiPackageRoot ?? resolvePackageRoot4("@teamix-evo/biz-ui");
4442
- const variant = lineageReport.installedVariant ?? "_flat";
4443
- const variantDir = path24.join(bizRoot, "variants", variant);
4444
- const variantManifest = await loadVariantUiPackageManifest2(variantDir);
4445
- const uiRoot = args.uiPackageRoot ?? resolvePackageRoot4("@teamix-evo/ui");
4446
- const uiManifest = await loadUiPackageManifest3(uiRoot);
4447
- const entryPackageRoot = /* @__PURE__ */ new Map();
4448
- const merged = [];
4449
- for (const e of variantManifest.entries) {
4450
- entryPackageRoot.set(e.id, variantDir);
4451
- merged.push(e);
4452
- }
4453
- for (const e of uiManifest.entries) {
4454
- if (entryPackageRoot.has(e.id)) continue;
4455
- entryPackageRoot.set(e.id, uiRoot);
4456
- merged.push(e);
4457
- }
4458
- const synthetic = {
4459
- schemaVersion: 1,
4460
- package: "ui",
4461
- version: variantManifest.version,
4462
- engines: variantManifest.engines,
4463
- 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 } : {}
4464
5327
  };
4465
- return buildUiUpgradeStaging({
4466
- projectRoot,
4467
- category,
4468
- manifest: synthetic,
4469
- packageRoot: variantDir,
4470
- entryPackageRoot,
4471
- aliases,
4472
- lineageReport,
4473
- trigger,
4474
- onlyIds
4475
- });
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;
4476
5336
  }
4477
5337
 
4478
5338
  // src/core/project-update.ts
@@ -4722,7 +5582,7 @@ async function runComponentSourceStep(category, args) {
4722
5582
  });
4723
5583
  return;
4724
5584
  }
4725
- const stagingRel = path25.relative(projectRoot, built.stagingDir);
5585
+ const stagingRel = path31.relative(projectRoot, built.stagingDir);
4726
5586
  const summary = summarizeStagingRisk(built.manifest.summary.byRisk);
4727
5587
  record({
4728
5588
  name: category,
@@ -4765,8 +5625,8 @@ async function planTokensUpdate(tokensPackage, config) {
4765
5625
  }
4766
5626
 
4767
5627
  // src/core/installer.ts
4768
- import * as path26 from "path";
4769
- import * as fs18 from "fs/promises";
5628
+ import * as path32 from "path";
5629
+ import * as fs23 from "fs/promises";
4770
5630
  async function installResources(options) {
4771
5631
  const { projectRoot, manifest, data, variantDir, packageRoot } = options;
4772
5632
  const installedResources = [];
@@ -4803,13 +5663,13 @@ async function installSingleResource(resource, projectRoot, data, variantDir, pa
4803
5663
  variantDir,
4804
5664
  packageRoot
4805
5665
  );
4806
- const targetPath = path26.join(projectRoot, resource.target);
5666
+ const targetPath = path32.join(projectRoot, resource.target);
4807
5667
  let content;
4808
5668
  if (resource.template) {
4809
5669
  const templateContent = await loadTemplateFile(sourcePath);
4810
5670
  content = renderTemplate(templateContent, data);
4811
5671
  } else {
4812
- content = await fs18.readFile(sourcePath, "utf-8");
5672
+ content = await fs23.readFile(sourcePath, "utf-8");
4813
5673
  }
4814
5674
  await writeFileSafe(targetPath, content);
4815
5675
  const hash = computeHash(content);
@@ -4827,13 +5687,13 @@ async function installRecursiveResource(resource, projectRoot, data, variantDir,
4827
5687
  variantDir,
4828
5688
  packageRoot
4829
5689
  );
4830
- const targetDir = path26.join(projectRoot, resource.target);
5690
+ const targetDir = path32.join(projectRoot, resource.target);
4831
5691
  const results = [];
4832
5692
  await ensureDir(targetDir);
4833
5693
  const entries = await walkDir(sourcePath);
4834
5694
  for (const entry of entries) {
4835
- const relPath = path26.relative(sourcePath, entry);
4836
- let targetFile = path26.join(targetDir, relPath);
5695
+ const relPath = path32.relative(sourcePath, entry);
5696
+ let targetFile = path32.join(targetDir, relPath);
4837
5697
  if (resource.template && targetFile.endsWith(".hbs")) {
4838
5698
  targetFile = targetFile.slice(0, -4);
4839
5699
  }
@@ -4842,11 +5702,11 @@ async function installRecursiveResource(resource, projectRoot, data, variantDir,
4842
5702
  const templateContent = await loadTemplateFile(entry);
4843
5703
  content = renderTemplate(templateContent, data);
4844
5704
  } else {
4845
- content = await fs18.readFile(entry, "utf-8");
5705
+ content = await fs23.readFile(entry, "utf-8");
4846
5706
  }
4847
5707
  await writeFileSafe(targetFile, content);
4848
5708
  const hash = computeHash(content);
4849
- const targetRel = path26.relative(projectRoot, targetFile);
5709
+ const targetRel = path32.relative(projectRoot, targetFile);
4850
5710
  results.push({
4851
5711
  id: `${resource.id}:${relPath}`,
4852
5712
  target: targetRel,
@@ -4859,25 +5719,25 @@ async function installRecursiveResource(resource, projectRoot, data, variantDir,
4859
5719
  }
4860
5720
 
4861
5721
  // src/core/registry-client.ts
4862
- import * as path27 from "path";
4863
- import * as fs19 from "fs/promises";
5722
+ import * as path33 from "path";
5723
+ import * as fs24 from "fs/promises";
4864
5724
  import { createRequire as createRequire6 } from "module";
4865
5725
  import { loadVariantManifest } from "@teamix-evo/registry";
4866
5726
  var require6 = createRequire6(import.meta.url);
4867
5727
  function resolvePackageRoot5(packageName) {
4868
5728
  const pkgJsonPath = require6.resolve(`${packageName}/package.json`);
4869
- return path27.dirname(pkgJsonPath);
5729
+ return path33.dirname(pkgJsonPath);
4870
5730
  }
4871
5731
  async function loadVariantData(packageName, variant) {
4872
5732
  const packageRoot = resolvePackageRoot5(packageName);
4873
- const variantDir = path27.join(packageRoot, "library", variant);
5733
+ const variantDir = path33.join(packageRoot, "library", variant);
4874
5734
  logger.debug(`Resolved variant dir: ${variantDir}`);
4875
5735
  logger.debug(`Package root: ${packageRoot}`);
4876
5736
  const manifest = await loadVariantManifest(variantDir);
4877
5737
  let data = {};
4878
- const dataPath = path27.join(variantDir, "_data.json");
5738
+ const dataPath = path33.join(variantDir, "_data.json");
4879
5739
  try {
4880
- const raw = await fs19.readFile(dataPath, "utf-8");
5740
+ const raw = await fs24.readFile(dataPath, "utf-8");
4881
5741
  data = JSON.parse(raw);
4882
5742
  } catch (err) {
4883
5743
  if (err.code !== "ENOENT") {