@sebgroup/green-core 2.37.2 → 2.37.3

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.
@@ -14,6 +14,7 @@ import {
14
14
  DOCS_FRAMEWORK_CANONICAL,
15
15
  normalizeDocsFramework
16
16
  } from "./framework.js";
17
+ import { handleMigrate } from "./migrate/index.js";
17
18
  import { parseArgs } from "./parse-args.js";
18
19
  function getPrimaryTextContent(result) {
19
20
  const textBlock = result.content.find(
@@ -43,6 +44,7 @@ COMMANDS
43
44
  guides List available guides
44
45
  guide <name> Get a specific guide's content
45
46
  instructions Get base usage instructions
47
+ migrate <subcommand> Run code migrations
46
48
 
47
49
  GLOBAL OPTIONS
48
50
  -h, --help Show this help message
@@ -337,6 +339,9 @@ async function main() {
337
339
  case "instructions":
338
340
  await runInstructions(args);
339
341
  break;
342
+ case "migrate":
343
+ await handleMigrate(args);
344
+ break;
340
345
  default:
341
346
  process.stderr.write(`Error: Unknown command '${args.command}'.
342
347
 
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Migration engine — file discovery and migration orchestration.
3
+ *
4
+ * The engine walks a directory tree, filters files by extension, and runs
5
+ * migration units in detect-only or detect+apply mode. All file I/O uses
6
+ * Node built-in modules (no external dependencies).
7
+ *
8
+ * Security:
9
+ * - Writes files only when `apply: true` is explicitly set
10
+ * - Skips unreadable files and directories silently
11
+ * - Normalises all output paths to forward slashes
12
+ *
13
+ * @module migrate/engine
14
+ */
15
+ import type { MigrationResult, MigrationUnit } from './types.js';
16
+ /** Directories that are always skipped during file discovery. */
17
+ export declare const DEFAULT_SKIP_DIRS: Set<string>;
18
+ /**
19
+ * Discover all files under `basePath` whose extension is in `extensions`.
20
+ *
21
+ * @param basePath - Root directory to scan
22
+ * @param extensions - Set of extensions to include (e.g. `new Set(['.ts', '.html'])`)
23
+ * @param skipDirs - Directory names to skip (defaults to {@link DEFAULT_SKIP_DIRS})
24
+ * @returns Sorted array of absolute file paths
25
+ */
26
+ export declare function discoverFiles(basePath: string, extensions: Set<string>, skipDirs?: Set<string>): Promise<string[]>;
27
+ /** Options for {@link runMigrations}. */
28
+ export interface RunOptions {
29
+ /** Root directory to scan for files */
30
+ basePath: string;
31
+ /** Migration units to execute */
32
+ units: MigrationUnit[];
33
+ /** When true, apply transformations and write files. Default: false (detect only). */
34
+ apply?: boolean;
35
+ /** Additional directory names to skip during file discovery */
36
+ skipDirs?: string[];
37
+ }
38
+ /**
39
+ * Run one or more migration units against a directory tree.
40
+ *
41
+ * In detect-only mode (default), files are scanned but never modified.
42
+ * In apply mode (`apply: true`), each unit's `apply()` function is called
43
+ * and the result is written back to disk.
44
+ *
45
+ * @param options - Configuration for the migration run
46
+ * @returns One {@link MigrationResult} per unit, in the same order as `options.units`
47
+ */
48
+ export declare function runMigrations(options: RunOptions): Promise<MigrationResult[]>;
@@ -0,0 +1,111 @@
1
+ import "../../../chunks/chunk.QU3DSPNU.js";
2
+ import { readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { extname, join, relative, resolve } from "node:path";
4
+ const DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
5
+ "node_modules",
6
+ ".git",
7
+ "dist",
8
+ ".nx",
9
+ ".cache",
10
+ "coverage",
11
+ ".angular",
12
+ ".next",
13
+ "__pycache__"
14
+ ]);
15
+ async function* walkDir(dir, skipDirs) {
16
+ let entries;
17
+ try {
18
+ entries = await readdir(dir, { withFileTypes: true });
19
+ } catch {
20
+ return;
21
+ }
22
+ for (const entry of entries) {
23
+ if (entry.isDirectory()) {
24
+ if (skipDirs.has(entry.name)) continue;
25
+ yield* walkDir(join(dir, entry.name), skipDirs);
26
+ } else if (entry.isFile()) {
27
+ yield join(dir, entry.name);
28
+ }
29
+ }
30
+ }
31
+ async function discoverFiles(basePath, extensions, skipDirs) {
32
+ const skip = skipDirs ?? DEFAULT_SKIP_DIRS;
33
+ const files = [];
34
+ for await (const filePath of walkDir(basePath, skip)) {
35
+ if (extensions.has(extname(filePath))) {
36
+ files.push(filePath);
37
+ }
38
+ }
39
+ return files.sort();
40
+ }
41
+ async function runMigrations(options) {
42
+ const { basePath, units, apply = false, skipDirs: extraSkip } = options;
43
+ const resolvedBase = resolve(basePath);
44
+ const skipDirs = new Set(DEFAULT_SKIP_DIRS);
45
+ if (extraSkip) {
46
+ for (const dir of extraSkip) skipDirs.add(dir);
47
+ }
48
+ const allExtensions = /* @__PURE__ */ new Set();
49
+ for (const unit of units) {
50
+ for (const ext of unit.fileExtensions) {
51
+ allExtensions.add(ext.startsWith(".") ? ext : `.${ext}`);
52
+ }
53
+ }
54
+ const allFiles = await discoverFiles(resolvedBase, allExtensions, skipDirs);
55
+ const unitExtSets = /* @__PURE__ */ new Map();
56
+ for (const unit of units) {
57
+ unitExtSets.set(
58
+ unit.id,
59
+ new Set(
60
+ unit.fileExtensions.map((e) => e.startsWith(".") ? e : `.${e}`)
61
+ )
62
+ );
63
+ }
64
+ const results = [];
65
+ for (const unit of units) {
66
+ const extSet = unitExtSets.get(unit.id);
67
+ const relevantFiles = allFiles.filter((f) => extSet.has(extname(f)));
68
+ const allMatches = [];
69
+ const manualReview = [];
70
+ let filesModified = 0;
71
+ for (const filePath of relevantFiles) {
72
+ let content;
73
+ try {
74
+ content = await readFile(filePath, "utf-8");
75
+ } catch {
76
+ continue;
77
+ }
78
+ const matches = unit.detect(filePath, content);
79
+ if (matches.length === 0) continue;
80
+ const relFile = relative(resolvedBase, filePath).replace(/\\/g, "/");
81
+ for (const m of matches) {
82
+ m.file = relFile;
83
+ if (m.manualReview) {
84
+ manualReview.push(m);
85
+ }
86
+ }
87
+ allMatches.push(...matches);
88
+ if (apply) {
89
+ const transformed = unit.apply(filePath, content);
90
+ if (transformed !== null && transformed !== content) {
91
+ await writeFile(filePath, transformed, "utf-8");
92
+ filesModified++;
93
+ }
94
+ }
95
+ }
96
+ results.push({
97
+ unitId: unit.id,
98
+ summary: unit.summary,
99
+ filesScanned: relevantFiles.length,
100
+ matches: allMatches,
101
+ filesModified,
102
+ manualReview
103
+ });
104
+ }
105
+ return results;
106
+ }
107
+ export {
108
+ DEFAULT_SKIP_DIRS,
109
+ discoverFiles,
110
+ runMigrations
111
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CLI handler for the `migrate` command.
3
+ *
4
+ * Subcommands:
5
+ * - `list` — List available migration units
6
+ * - `run` — Detect (and optionally apply) migrations
7
+ *
8
+ * @module migrate
9
+ */
10
+ import type { ParsedArgs } from '../parse-args.js';
11
+ export declare const MIGRATE_HELP: string;
12
+ /**
13
+ * Route the `migrate` command to the appropriate subcommand.
14
+ */
15
+ export declare function handleMigrate(args: ParsedArgs): Promise<void>;
@@ -0,0 +1,232 @@
1
+ import "../../../chunks/chunk.QU3DSPNU.js";
2
+ import { runMigrations } from "./engine.js";
3
+ import { getMigrationUnits } from "./registry.js";
4
+ const PROGRAM_NAME = "green-core-context";
5
+ const MIGRATE_HELP = `
6
+ Detect and apply automated code migrations for Green Design System
7
+ major version upgrades.
8
+
9
+ USAGE
10
+ ${PROGRAM_NAME} migrate <subcommand> [options]
11
+
12
+ SUBCOMMANDS
13
+ list List available migration units
14
+ run Detect issues (default: detect only)
15
+
16
+ OPTIONS (for 'run')
17
+ --apply Apply changes (default: detect only)
18
+ --unit <id> Run a specific migration unit by ID
19
+ --major <n> Run all migrations for major version n
20
+ --path <dir> Target directory (default: current dir)
21
+ --json Output results as JSON
22
+ -h, --help Show this help message
23
+
24
+ EXAMPLES
25
+ ${PROGRAM_NAME} migrate list
26
+ ${PROGRAM_NAME} migrate run --path ./src
27
+ ${PROGRAM_NAME} migrate run --unit 3.0.0/my-migration --apply
28
+ ${PROGRAM_NAME} migrate run --major 3 --json
29
+ `.trim();
30
+ function runList(args) {
31
+ return runListAsync(args);
32
+ }
33
+ async function runListAsync(args) {
34
+ const majorFlag = args.flags["major"];
35
+ const jsonFlag = args.flags["json"];
36
+ const filterMajor = majorFlag !== void 0 && majorFlag !== true ? Number(majorFlag) : void 0;
37
+ if (filterMajor !== void 0 && Number.isNaN(filterMajor)) {
38
+ process.stderr.write("Error: --major must be a number.\n");
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ const resolved = await getMigrationUnits({ major: filterMajor });
43
+ if (jsonFlag) {
44
+ const json = resolved.map((r) => ({
45
+ id: r.unit.id,
46
+ major: r.major,
47
+ summary: r.unit.summary,
48
+ safe: r.unit.safe,
49
+ fileExtensions: r.unit.fileExtensions,
50
+ exampleBefore: r.unit.exampleBefore,
51
+ exampleAfter: r.unit.exampleAfter
52
+ }));
53
+ process.stdout.write(JSON.stringify(json, null, 2) + "\n");
54
+ return;
55
+ }
56
+ if (resolved.length === 0) {
57
+ process.stdout.write("No migration units available.\n");
58
+ if (filterMajor !== void 0) {
59
+ process.stdout.write(`(filtered by --major ${filterMajor})
60
+ `);
61
+ }
62
+ return;
63
+ }
64
+ process.stdout.write("Available migrations:\n\n");
65
+ const idWidth = Math.max(4, ...resolved.map((r) => r.unit.id.length));
66
+ const header = ` ${"ID".padEnd(idWidth)} Major Safe Summary`;
67
+ const separator = ` ${"\u2500".repeat(idWidth)} \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 ${"\u2500".repeat(40)}`;
68
+ process.stdout.write(header + "\n");
69
+ process.stdout.write(separator + "\n");
70
+ for (const r of resolved) {
71
+ const safe = r.unit.safe ? "yes " : "no ";
72
+ const line = ` ${r.unit.id.padEnd(idWidth)} ${String(r.major).padEnd(5)} ${safe} ${r.unit.summary}`;
73
+ process.stdout.write(line + "\n");
74
+ }
75
+ process.stdout.write(`
76
+ ${resolved.length} migration(s) available.
77
+ `);
78
+ }
79
+ async function runRun(args) {
80
+ const applyFlag = args.flags["apply"] === true;
81
+ const jsonFlag = args.flags["json"] === true;
82
+ const pathFlag = args.flags["path"];
83
+ const unitFlag = args.flags["unit"];
84
+ const majorFlag = args.flags["major"];
85
+ const targetPath = typeof pathFlag === "string" ? pathFlag : process.cwd();
86
+ const filterMajor = majorFlag !== void 0 && majorFlag !== true ? Number(majorFlag) : void 0;
87
+ if (filterMajor !== void 0 && Number.isNaN(filterMajor)) {
88
+ process.stderr.write("Error: --major must be a number.\n");
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ const filterId = typeof unitFlag === "string" ? unitFlag : void 0;
93
+ const resolved = await getMigrationUnits({ major: filterMajor, id: filterId });
94
+ if (resolved.length === 0) {
95
+ if (filterId) {
96
+ process.stderr.write(
97
+ `Error: No migration unit found with ID '${filterId}'.
98
+ `
99
+ );
100
+ } else if (filterMajor !== void 0) {
101
+ process.stderr.write(
102
+ `Error: No migration units found for major version ${filterMajor}.
103
+ `
104
+ );
105
+ } else {
106
+ process.stderr.write("No migration units are registered.\n");
107
+ }
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ const mode = applyFlag ? "apply" : "detect";
112
+ if (!jsonFlag) {
113
+ process.stderr.write(
114
+ `Running ${resolved.length} migration(s) in ${mode} mode...
115
+
116
+ `
117
+ );
118
+ }
119
+ const results = await runMigrations({
120
+ basePath: targetPath,
121
+ units: resolved.map((r) => r.unit),
122
+ apply: applyFlag
123
+ });
124
+ if (jsonFlag) {
125
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
126
+ return;
127
+ }
128
+ formatTextResults(results, applyFlag);
129
+ }
130
+ function formatTextResults(results, applied) {
131
+ let totalMatches = 0;
132
+ let totalModified = 0;
133
+ let totalManual = 0;
134
+ let totalScanned = 0;
135
+ for (const result of results) {
136
+ process.stdout.write(`\u2500\u2500 ${result.unitId} \u2500\u2500
137
+ `);
138
+ process.stdout.write(`${result.summary}
139
+
140
+ `);
141
+ totalScanned += result.filesScanned;
142
+ totalMatches += result.matches.length;
143
+ totalModified += result.filesModified;
144
+ totalManual += result.manualReview.length;
145
+ if (result.matches.length === 0) {
146
+ process.stdout.write(" No issues found.\n\n");
147
+ continue;
148
+ }
149
+ if (applied) {
150
+ const byFile = /* @__PURE__ */ new Map();
151
+ for (const m of result.matches) {
152
+ byFile.set(m.file, (byFile.get(m.file) ?? 0) + 1);
153
+ }
154
+ for (const [file, count] of byFile) {
155
+ const prefix = result.manualReview.some((m) => m.file === file) ? "!" : "\u2713";
156
+ process.stdout.write(
157
+ ` ${prefix} ${file} (${count} change${count > 1 ? "s" : ""})
158
+ `
159
+ );
160
+ }
161
+ } else {
162
+ for (const m of result.matches) {
163
+ const marker = m.manualReview ? " [manual]" : "";
164
+ process.stdout.write(
165
+ ` ${m.file}:${m.line}${marker}
166
+ ${m.message}
167
+
168
+ `
169
+ );
170
+ }
171
+ }
172
+ process.stdout.write(
173
+ ` ${result.matches.length} issue(s) in ${filesWithMatches(result)} file(s) (scanned ${result.filesScanned})
174
+
175
+ `
176
+ );
177
+ }
178
+ process.stdout.write("\u2500\u2500 Summary \u2500\u2500\n");
179
+ process.stdout.write(`Scanned: ${totalScanned} files
180
+ `);
181
+ process.stdout.write(`Found: ${totalMatches} issue(s)
182
+ `);
183
+ if (applied) {
184
+ process.stdout.write(`Applied: ${totalModified} file(s) modified
185
+ `);
186
+ } else if (totalMatches > 0) {
187
+ process.stdout.write(`
188
+ Run with --apply to fix automatically.
189
+ `);
190
+ }
191
+ if (totalManual > 0) {
192
+ process.stdout.write(
193
+ `Manual: ${totalManual} issue(s) require manual review
194
+ `
195
+ );
196
+ }
197
+ }
198
+ function filesWithMatches(result) {
199
+ return new Set(result.matches.map((m) => m.file)).size;
200
+ }
201
+ async function handleMigrate(args) {
202
+ if (args.flags["h"] || args.flags["help"]) {
203
+ process.stdout.write(MIGRATE_HELP + "\n");
204
+ return;
205
+ }
206
+ const subcommand = args.positional[0];
207
+ switch (subcommand) {
208
+ case "list":
209
+ await runList(args);
210
+ break;
211
+ case "run":
212
+ await runRun(args);
213
+ break;
214
+ default:
215
+ if (subcommand) {
216
+ process.stderr.write(
217
+ `Error: Unknown migrate subcommand '${subcommand}'.
218
+
219
+ `
220
+ );
221
+ }
222
+ process.stdout.write(MIGRATE_HELP + "\n");
223
+ if (!subcommand) {
224
+ } else {
225
+ process.exitCode = 1;
226
+ }
227
+ }
228
+ }
229
+ export {
230
+ MIGRATE_HELP,
231
+ handleMigrate
232
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Migration unit registry — auto-discovery from the file system.
3
+ *
4
+ * Units are discovered by scanning the `units/` directory for numbered
5
+ * subdirectories (each representing a major version). Every `.ts` / `.js`
6
+ * file inside a major directory that exports a `unit` constant is
7
+ * registered automatically.
8
+ *
9
+ * units/
10
+ * 0/ ← major 0 (examples / disabled units)
11
+ * example.ts
12
+ * 3/ ← major 3
13
+ * remove-dropdown-value-generic.ts
14
+ *
15
+ * @module migrate/registry
16
+ */
17
+ import type { ResolvedMigrationUnit } from './types.js';
18
+ /**
19
+ * Discover migration units from the `units/<major>/` directory structure.
20
+ *
21
+ * @param basePath - Root `units/` directory to scan. Defaults to the
22
+ * co-located `./units` directory next to this module.
23
+ * @returns All discovered units, sorted by major then ID.
24
+ */
25
+ export declare function discoverMigrationUnits(basePath?: string): Promise<ResolvedMigrationUnit[]>;
26
+ /**
27
+ * Get migration units matching optional filter criteria.
28
+ *
29
+ * By default only enabled units are returned. Set `includeDisabled: true`
30
+ * to include disabled units (useful for tooling / debugging).
31
+ *
32
+ * @param options.major - Filter by target major version
33
+ * @param options.id - Filter by exact unit ID
34
+ * @param options.includeDisabled - Include units with `enabled: false`
35
+ * @param options.basePath - Override the units directory (for tests)
36
+ * @returns Filtered array of resolved migration units
37
+ */
38
+ export declare function getMigrationUnits(options?: {
39
+ major?: number;
40
+ id?: string;
41
+ includeDisabled?: boolean;
42
+ basePath?: string;
43
+ }): Promise<ResolvedMigrationUnit[]>;
@@ -0,0 +1,75 @@
1
+ import "../../../chunks/chunk.QU3DSPNU.js";
2
+ import { readdir } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const DEFAULT_UNITS_DIR = join(__dirname, "units");
7
+ function isUnitFile(name) {
8
+ if (name.endsWith(".d.ts")) return false;
9
+ if (name.endsWith(".test.ts") || name.endsWith(".test.js")) return false;
10
+ if (name.endsWith(".spec.ts") || name.endsWith(".spec.js")) return false;
11
+ return name.endsWith(".ts") || name.endsWith(".js");
12
+ }
13
+ async function discoverMigrationUnits(basePath) {
14
+ const unitsDir = basePath ?? DEFAULT_UNITS_DIR;
15
+ const results = [];
16
+ let topEntries;
17
+ try {
18
+ topEntries = await readdir(unitsDir, { withFileTypes: true });
19
+ } catch {
20
+ return results;
21
+ }
22
+ for (const entry of topEntries) {
23
+ if (!entry.isDirectory()) continue;
24
+ const major = Number(entry.name);
25
+ if (Number.isNaN(major)) continue;
26
+ const majorDir = join(unitsDir, entry.name);
27
+ let files;
28
+ try {
29
+ files = await readdir(majorDir, { withFileTypes: true });
30
+ } catch {
31
+ continue;
32
+ }
33
+ for (const file of files) {
34
+ if (!file.isFile() || !isUnitFile(file.name)) continue;
35
+ try {
36
+ const moduleUrl = pathToFileURL(join(majorDir, file.name)).href;
37
+ const mod = await import(moduleUrl);
38
+ if (mod.unit && typeof mod.unit === "object" && "id" in mod.unit) {
39
+ results.push({ major, unit: mod.unit });
40
+ } else {
41
+ process.stderr.write(
42
+ `Warning: ${entry.name}/${file.name} does not export a 'unit' constant \u2014 skipped.
43
+ `
44
+ );
45
+ }
46
+ } catch (err) {
47
+ process.stderr.write(
48
+ `Warning: Failed to import ${entry.name}/${file.name}: ${err instanceof Error ? err.message : String(err)}
49
+ `
50
+ );
51
+ }
52
+ }
53
+ }
54
+ results.sort(
55
+ (a, b) => a.major - b.major || a.unit.id.localeCompare(b.unit.id)
56
+ );
57
+ return results;
58
+ }
59
+ async function getMigrationUnits(options) {
60
+ let units = await discoverMigrationUnits(options?.basePath);
61
+ if (!options?.includeDisabled) {
62
+ units = units.filter((r) => r.unit.enabled);
63
+ }
64
+ if (options?.major !== void 0) {
65
+ units = units.filter((r) => r.major === options.major);
66
+ }
67
+ if (options?.id !== void 0) {
68
+ units = units.filter((r) => r.unit.id === options.id);
69
+ }
70
+ return units;
71
+ }
72
+ export {
73
+ discoverMigrationUnits,
74
+ getMigrationUnits
75
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Types for the Green Design System migration engine.
3
+ *
4
+ * Migration units are versioned, self-contained modules that detect and
5
+ * optionally fix breaking changes in consumer codebases. Each unit targets
6
+ * a specific major version upgrade and encapsulates:
7
+ * - Detection logic (regex-based, zero external dependencies)
8
+ * - Transformation logic (string replacement or manual-review flagging)
9
+ *
10
+ * ## Directory layout
11
+ *
12
+ * Units are organized by major version in the file system:
13
+ *
14
+ * units/
15
+ * 3/
16
+ * remove-dropdown-value-generic.ts
17
+ * rename-datepicker-slot.ts
18
+ * 4/
19
+ * some-future-migration.ts
20
+ *
21
+ * The major version is derived from the directory name — not from a field
22
+ * on the unit itself. Units that should not appear in listings or be run
23
+ * can set `enabled: false`.
24
+ *
25
+ * @module migrate/types
26
+ */
27
+ /**
28
+ * A single match found by a migration unit's detect function.
29
+ */
30
+ export interface MigrationMatch {
31
+ /** File path (made relative by the engine after detect returns) */
32
+ file: string;
33
+ /** 1-based line number where the match was found */
34
+ line: number;
35
+ /** The matched text snippet (the relevant line fragment) */
36
+ match: string;
37
+ /** Human-readable description of what needs to change */
38
+ message: string;
39
+ /**
40
+ * If true, this match cannot be auto-fixed and requires manual
41
+ * intervention. The engine will report it separately.
42
+ */
43
+ manualReview?: boolean;
44
+ }
45
+ /**
46
+ * Aggregated result from running a single migration unit across a project.
47
+ */
48
+ export interface MigrationResult {
49
+ /** The migration unit ID */
50
+ unitId: string;
51
+ /** Short summary of the migration */
52
+ summary: string;
53
+ /** Number of files scanned */
54
+ filesScanned: number;
55
+ /** All matches found */
56
+ matches: MigrationMatch[];
57
+ /** Number of files that were modified (0 in detect-only mode) */
58
+ filesModified: number;
59
+ /** Matches that require manual review (subset of matches) */
60
+ manualReview: MigrationMatch[];
61
+ }
62
+ /**
63
+ * A migration unit describes a single automated migration step.
64
+ *
65
+ * ## Creating a new unit
66
+ *
67
+ * 1. Create a new file under `migrate/units/<major>/`
68
+ * (e.g. `units/3/remove-dropdown-value-generic.ts`)
69
+ * 2. Export a `MigrationUnit` object named `unit`
70
+ * 3. The registry auto-discovers it from the directory structure
71
+ *
72
+ * See `migrate/units/0/example.ts` for a documented template.
73
+ */
74
+ export interface MigrationUnit {
75
+ /**
76
+ * Unique identifier (kebab-case slug).
77
+ * Example: `remove-dropdown-value-generic`
78
+ */
79
+ id: string;
80
+ /**
81
+ * Whether this unit should be listed and available for execution.
82
+ * Set to `false` for drafts, examples, or temporarily disabled units.
83
+ */
84
+ enabled: boolean;
85
+ /** Short human-readable summary (one line) */
86
+ summary: string;
87
+ /**
88
+ * Code example showing the pattern **before** the migration is applied.
89
+ * Primarily serves as context for coding models applying changes manually.
90
+ */
91
+ exampleBefore: string;
92
+ /**
93
+ * Code example showing the expected result **after** migration.
94
+ * Primarily serves as context for coding models applying changes manually.
95
+ */
96
+ exampleAfter: string;
97
+ /**
98
+ * File extensions to scan, including the leading dot.
99
+ * Example: `['.ts', '.tsx', '.html']`
100
+ */
101
+ fileExtensions: string[];
102
+ /**
103
+ * Whether this migration is safe to auto-apply without manual review.
104
+ * When `true`, `apply()` is guaranteed to produce correct output for
105
+ * every match that `detect()` finds. When `false`, some matches may
106
+ * require manual intervention.
107
+ */
108
+ safe: boolean;
109
+ /**
110
+ * Detect occurrences of the deprecated pattern in a file.
111
+ *
112
+ * @param filePath - Absolute path to the file being scanned
113
+ * @param content - The file's full text content (UTF-8)
114
+ * @returns Array of matches found in the file (may be empty)
115
+ */
116
+ detect(filePath: string, content: string): MigrationMatch[];
117
+ /**
118
+ * Apply the migration transformation to a file's content.
119
+ *
120
+ * @param filePath - Absolute path to the file being transformed
121
+ * @param content - The file's full text content (UTF-8)
122
+ * @returns The transformed content, or `null` if no changes were made
123
+ */
124
+ apply(filePath: string, content: string): string | null;
125
+ }
126
+ /**
127
+ * A migration unit enriched with metadata derived from the directory
128
+ * structure during auto-discovery. Returned by the registry.
129
+ */
130
+ export interface ResolvedMigrationUnit {
131
+ /** Major version, derived from the `units/<major>/` directory name */
132
+ major: number;
133
+ /** The migration unit definition */
134
+ unit: MigrationUnit;
135
+ }
File without changes