@dialpad/dialtone-css 8.80.0-next.8 → 8.80.0-next.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build/js/dialtone_migrate/index.mjs +856 -0
- package/lib/build/js/dialtone_migrate_props/index.mjs +2 -1
- package/lib/build/js/dialtone_migration_helper/configs/physical-to-logical.mjs +7 -7
- package/lib/build/js/dialtone_migration_helper/configs/vue3-to-vue-imports.mjs +17 -0
- package/lib/build/less/components/box.less +2 -2
- package/lib/build/less/components/card.less +1 -1
- package/lib/build/less/components/image-viewer.less +0 -1
- package/lib/build/less/components/modal.less +1 -1
- package/lib/build/less/components/popover.less +1 -1
- package/lib/build/less/components/toast.less +2 -2
- package/lib/build/less/recipes/contact_info.less +0 -1
- package/lib/build/less/recipes/unread_pill.less +1 -1
- package/lib/build/less/utilities/effects.less +5 -11
- package/lib/dist/dialtone-default-theme.css +74 -184
- package/lib/dist/dialtone-default-theme.min.css +1 -1
- package/lib/dist/dialtone-docs.json +1 -1
- package/lib/dist/dialtone.css +30 -23
- package/lib/dist/dialtone.min.css +1 -1
- package/lib/dist/js/dialtone_migrate/index.mjs +856 -0
- package/lib/dist/js/dialtone_migrate_props/index.mjs +2 -1
- package/lib/dist/js/dialtone_migrate_tshirt_to_numeric/index.mjs +0 -0
- package/lib/dist/js/dialtone_migration_helper/configs/physical-to-logical.mjs +7 -7
- package/lib/dist/js/dialtone_migration_helper/configs/vue3-to-vue-imports.mjs +17 -0
- package/lib/dist/tokens/tokens-base-dark.css +126 -267
- package/lib/dist/tokens/tokens-base-light.css +44 -161
- package/lib/dist/tokens/tokens-debug-base.css +41 -158
- package/lib/dist/tokens-docs.json +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable max-lines */
|
|
3
|
+
/* eslint-disable complexity */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @fileoverview Master migration script for Dialtone next major version.
|
|
7
|
+
*
|
|
8
|
+
* Orchestrates all individual migration scripts in the correct order.
|
|
9
|
+
* Supports selective execution, dry-run mode, and a health-check to
|
|
10
|
+
* report which migrations are still needed.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx dialtone-migrate [options]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --cwd <path> Working directory (default: cwd)
|
|
17
|
+
* --dry-run Show changes without applying them
|
|
18
|
+
* --yes Apply all changes without prompting
|
|
19
|
+
* --health-check Report migration status without modifying files
|
|
20
|
+
* --all Run all required migrations without selection prompt
|
|
21
|
+
* --only <ids> Comma-separated list of migration IDs to run
|
|
22
|
+
* --help Show help
|
|
23
|
+
*
|
|
24
|
+
* Examples:
|
|
25
|
+
* npx dialtone-migrate
|
|
26
|
+
* npx dialtone-migrate --health-check --cwd ./src
|
|
27
|
+
* npx dialtone-migrate --all --dry-run
|
|
28
|
+
* npx dialtone-migrate --only color-stops,hsl-to-oklch
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawn } from 'node:child_process';
|
|
32
|
+
import fs from 'fs/promises';
|
|
33
|
+
import path from 'path';
|
|
34
|
+
import readline from 'readline';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
|
|
37
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
38
|
+
const __dirname = path.dirname(__filename);
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Migration registry
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} Migration
|
|
46
|
+
* @property {string} id - Unique identifier
|
|
47
|
+
* @property {string} name - Display name
|
|
48
|
+
* @property {string} description - Short description
|
|
49
|
+
* @property {'required'|'opt-in'} category
|
|
50
|
+
* @property {'config'|'standalone'|'manual'} type
|
|
51
|
+
* @property {string} [configName] - Config filename for migration-helper type
|
|
52
|
+
* @property {string} [scriptDir] - Directory name for standalone scripts
|
|
53
|
+
* @property {string[]} [extraArgs] - Extra CLI args to forward
|
|
54
|
+
* @property {RegExp[]} detectPatterns - Patterns that indicate migration is still needed
|
|
55
|
+
* @property {string[]} fileExtensions - File extensions to scan during health check
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** @type {Migration[]} */
|
|
59
|
+
const MIGRATIONS = [
|
|
60
|
+
// ── Required (breaking) ────────────────────────────────────────────
|
|
61
|
+
{
|
|
62
|
+
id: 'color-stops',
|
|
63
|
+
name: 'Color Stops',
|
|
64
|
+
description: 'Base color ramps standardized to a 12-stop scale.',
|
|
65
|
+
category: 'required',
|
|
66
|
+
type: 'config',
|
|
67
|
+
configName: 'color-stops',
|
|
68
|
+
detectPatterns: [
|
|
69
|
+
/var\(--dt-color-(?:purple|magenta)-(?:250|350)\)/,
|
|
70
|
+
/var\(--dt-color-(?:blue|green|red|gold)-(?:425|450|475)\)/,
|
|
71
|
+
/d-(?:bgc|fc|bc)-(?:purple|magenta)-(?:250|350)/,
|
|
72
|
+
],
|
|
73
|
+
fileExtensions: ['.css', '.less', '.html', '.vue', '.js', '.ts', '.jsx', '.tsx'],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'hsl-to-oklch',
|
|
77
|
+
name: 'HSL to OKLCH',
|
|
78
|
+
description: 'Color tokens moved from HSL to OKLCH. Per-channel breakout variables removed.',
|
|
79
|
+
category: 'required',
|
|
80
|
+
type: 'config',
|
|
81
|
+
configName: 'hsl-to-oklch',
|
|
82
|
+
detectPatterns: [
|
|
83
|
+
/var\(--[\w-]+-(?:hsl|hsla)\)/,
|
|
84
|
+
/var\(--[\w-]+-h\)\s*,\s*var\(--[\w-]+-s\)/,
|
|
85
|
+
],
|
|
86
|
+
fileExtensions: ['.css', '.less', '.scss', '.html', '.vue', '.js', '.ts', '.jsx', '.tsx'],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'space-to-spacing',
|
|
90
|
+
name: 'Space to Spacing Tokens',
|
|
91
|
+
description: '--dt-space-* becomes --dt-spacing-*.',
|
|
92
|
+
category: 'required',
|
|
93
|
+
type: 'config',
|
|
94
|
+
configName: 'space-to-spacing',
|
|
95
|
+
detectPatterns: [
|
|
96
|
+
/var\(--dt-space-\d+/,
|
|
97
|
+
],
|
|
98
|
+
fileExtensions: ['.css', '.less', '.scss', '.html', '.vue', '.js', '.ts', '.jsx', '.tsx'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'size-to-layout',
|
|
102
|
+
name: 'Size to Layout Tokens',
|
|
103
|
+
description: '--dt-size-* routed to --dt-layout-*, --dt-spacing-*, --dt-size-border-*, or --dt-size-radius-*.',
|
|
104
|
+
category: 'required',
|
|
105
|
+
type: 'config',
|
|
106
|
+
configName: 'size-to-layout',
|
|
107
|
+
detectPatterns: [
|
|
108
|
+
/var\(--dt-size-\d+\)/,
|
|
109
|
+
],
|
|
110
|
+
fileExtensions: ['.css', '.less', '.scss', '.html', '.vue', '.js', '.ts', '.jsx', '.tsx'],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'border-radius',
|
|
114
|
+
name: 'Border-Radius Logical Names',
|
|
115
|
+
description: 'Physical directional radius classes (d-btr*, d-bbr*, d-blr*, d-brr*) replaced by logical equivalents (d-bbsr*, d-bber*, d-bisr*, d-bier*). Numeric stops standardized.',
|
|
116
|
+
category: 'required',
|
|
117
|
+
type: 'standalone',
|
|
118
|
+
scriptDir: 'dialtone_migrate_border_radius',
|
|
119
|
+
detectPatterns: [
|
|
120
|
+
/(?:["'\s=`])d-bar(?:0|1|2|4|6|8|12|16|24|32)(?:["'\s>;`])/,
|
|
121
|
+
/(?:["'\s=`])d-(?:btr|bbr|blr|brr)(?:\d|-pill|-circle)(?:["'\s>;`])/,
|
|
122
|
+
],
|
|
123
|
+
fileExtensions: ['.vue', '.html', '.js', '.ts', '.jsx', '.tsx', '.md'],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'utility-class-to-token-stops',
|
|
127
|
+
name: 'Utility Class Token Stops',
|
|
128
|
+
description: 'Pixel-based utility class names migrated to token-stop-based names (d-h16 → d-h-25, d-p8 → d-p-100, etc.).',
|
|
129
|
+
category: 'required',
|
|
130
|
+
type: 'config',
|
|
131
|
+
configName: 'utility-class-to-token-stops',
|
|
132
|
+
detectPatterns: [
|
|
133
|
+
/(?:["'\s])d-(?:h|w|hmn|hmx|wmn|wmx)(?:16|32|48|64|96|128)(?:["'\s])/,
|
|
134
|
+
/(?:["'\s])d-(?:m|mt|mr|mb|ml|mx|my|p|pt|pr|pb|pl|px|py)(?:4|8|12|16|24|32|48|64)(?:["'\s])/,
|
|
135
|
+
/(?:["'\s])d-(?:g|rg|cg)(?:4|8|12|16|24|32)(?:["'\s])/,
|
|
136
|
+
],
|
|
137
|
+
fileExtensions: ['.vue', '.html', '.js', '.ts', '.jsx', '.tsx', '.md', '.css', '.less'],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'theme-to-mode',
|
|
141
|
+
name: 'Theme to Mode',
|
|
142
|
+
description: 'Deprecated setTheme() and data-dt-theme migrated to the layered theming API (setMode/setBrand/setContrast/initDialtoneTheme).',
|
|
143
|
+
category: 'required',
|
|
144
|
+
type: 'config',
|
|
145
|
+
configName: 'theme-to-mode',
|
|
146
|
+
detectPatterns: [
|
|
147
|
+
/(?<!\.)setTheme\s*\(/,
|
|
148
|
+
/\bdata-dt-theme\b/,
|
|
149
|
+
],
|
|
150
|
+
fileExtensions: ['.vue', '.html', '.js', '.ts', '.jsx', '.tsx', '.css', '.less', '.scss', '.mjs'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'component-props',
|
|
154
|
+
name: 'Component Props & Events',
|
|
155
|
+
description: 'Value renames, show→open, hide-* inversion, title→header-text, event/slot renames, rootClass removal.',
|
|
156
|
+
category: 'required',
|
|
157
|
+
type: 'standalone',
|
|
158
|
+
scriptDir: 'dialtone_migrate_props',
|
|
159
|
+
detectPatterns: [
|
|
160
|
+
/(?:show|hide-close|hide-icon|label-visible|selected-values)(?:=|[\s>])/,
|
|
161
|
+
/<(?:dt-(?:banner|notice|toast|modal)|Dt(?:Banner|Notice|Toast|Modal))\b[^>]*\btitle(?:=|[\s>])/,
|
|
162
|
+
/<(?:dt-[\w-]+|Dt\w+)\b[^>]*@(?:input|change)(?:=|\.)/,
|
|
163
|
+
/kind="(?:danger|error)"/,
|
|
164
|
+
/validation-state="(?:error|success)"/,
|
|
165
|
+
],
|
|
166
|
+
fileExtensions: ['.vue'],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'chip-interactive',
|
|
170
|
+
name: 'DtChip Interactive Default',
|
|
171
|
+
description: 'interactive prop default changed from true to false. Clickable chips must opt in.',
|
|
172
|
+
category: 'required',
|
|
173
|
+
type: 'standalone',
|
|
174
|
+
scriptDir: 'dialtone_migrate_chip_interactive',
|
|
175
|
+
detectPatterns: [
|
|
176
|
+
/<(?:dt-chip|DtChip)\b[^>]*(?:@click|v-on:click)[^>]*>/,
|
|
177
|
+
],
|
|
178
|
+
fileExtensions: ['.vue'],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'scrollbar-always',
|
|
182
|
+
name: 'Scrollbar :never → :always',
|
|
183
|
+
description: 'v-dt-scrollbar:never renamed to v-dt-scrollbar:always.',
|
|
184
|
+
category: 'required',
|
|
185
|
+
type: 'standalone',
|
|
186
|
+
scriptDir: 'dialtone_migrate_scrollbar_always',
|
|
187
|
+
detectPatterns: [
|
|
188
|
+
/v-dt-scrollbar:never/,
|
|
189
|
+
/scrollbar="never"/,
|
|
190
|
+
],
|
|
191
|
+
fileExtensions: ['.vue', '.html'],
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// ── Opt-in (deprecation, best practices) ──────────────────────────
|
|
195
|
+
{
|
|
196
|
+
id: 'base-to-semantic',
|
|
197
|
+
name: 'Base to Semantic Colors',
|
|
198
|
+
description: 'Upgrade base color utilities/tokens to theme-aware semantic equivalents.',
|
|
199
|
+
category: 'opt-in',
|
|
200
|
+
type: 'config',
|
|
201
|
+
configName: 'base-to-semantic',
|
|
202
|
+
detectPatterns: [
|
|
203
|
+
/d-fc-(?:black|red|green|gold)-\d+/,
|
|
204
|
+
/d-bgc-(?:black|red|green|gold|blue|purple)-\d+/,
|
|
205
|
+
],
|
|
206
|
+
fileExtensions: ['.css', '.less', '.html', '.vue', '.js', '.ts', '.jsx', '.tsx'],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'success-to-positive',
|
|
210
|
+
name: 'Success to Positive',
|
|
211
|
+
description: 'success* tokens and utility classes deprecated in favor of positive*.',
|
|
212
|
+
category: 'opt-in',
|
|
213
|
+
type: 'config',
|
|
214
|
+
configName: 'success-to-positive',
|
|
215
|
+
detectPatterns: [
|
|
216
|
+
/var\(--dt-color-(?:foreground|surface|border|link)-success/,
|
|
217
|
+
/d-(?:fc|bgc|bc)-success/,
|
|
218
|
+
],
|
|
219
|
+
fileExtensions: ['.css', '.less', '.html', '.vue', '.js', '.ts', '.jsx', '.tsx'],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'stack-gap-to-spacing',
|
|
223
|
+
name: 'Stack Gap to Spacing',
|
|
224
|
+
description: 'DtStack and DtDescriptionList gap prop values migrated from old size stops to new spacing stops.',
|
|
225
|
+
category: 'opt-in',
|
|
226
|
+
type: 'config',
|
|
227
|
+
configName: 'stack-gap-to-spacing',
|
|
228
|
+
detectPatterns: [
|
|
229
|
+
/gap="(?:50|100|200|300|350|400|450|500|525|550|600|625|650|700)"/,
|
|
230
|
+
/d-stack--gap-(?:50|100|200|300|350|400|450|500|525|550|600|625|650|700)/,
|
|
231
|
+
],
|
|
232
|
+
fileExtensions: ['.vue', '.html', '.js', '.ts', '.jsx', '.tsx', '.md'],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'flex-to-stack',
|
|
236
|
+
name: 'Flex to DtStack',
|
|
237
|
+
description: 'Replace d-d-flex utilities with the <dt-stack> component.',
|
|
238
|
+
category: 'opt-in',
|
|
239
|
+
type: 'standalone',
|
|
240
|
+
scriptDir: 'dialtone_migrate_flex_to_stack',
|
|
241
|
+
detectPatterns: [
|
|
242
|
+
/class="[^"]*d-d-flex[^"]*"/,
|
|
243
|
+
],
|
|
244
|
+
fileExtensions: ['.vue'],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'link-rendering',
|
|
248
|
+
name: 'Link and Button Navigation',
|
|
249
|
+
description: 'DtButton/DtLink gain to/href props; legacy anchor/router-link workarounds replaced.',
|
|
250
|
+
category: 'opt-in',
|
|
251
|
+
type: 'standalone',
|
|
252
|
+
scriptDir: 'dialtone_migrate_link_rendering',
|
|
253
|
+
detectPatterns: [
|
|
254
|
+
/<a\b[^>]*class="[^"]*d-btn/,
|
|
255
|
+
/<router-link\b[^>]*class="[^"]*d-link/,
|
|
256
|
+
/<dt-link\b[^>]*class="[^"]*d-td-/,
|
|
257
|
+
],
|
|
258
|
+
fileExtensions: ['.vue'],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'tshirt-to-numeric',
|
|
262
|
+
name: 'Component Sizes to Numeric',
|
|
263
|
+
description: 'size="sm" becomes :size="200" across all components.',
|
|
264
|
+
category: 'opt-in',
|
|
265
|
+
type: 'standalone',
|
|
266
|
+
scriptDir: 'dialtone_migrate_tshirt_to_numeric',
|
|
267
|
+
detectPatterns: [
|
|
268
|
+
/<(?:dt-[\w-]+|Dt\w+)\b[^>]*\bsize="(?:xs|sm|md|lg|xl|2xl|3xl)"/,
|
|
269
|
+
],
|
|
270
|
+
fileExtensions: ['.vue', '.html', '.md'],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'physical-to-logical',
|
|
274
|
+
name: 'Logical Naming',
|
|
275
|
+
description: 'Slots, props, events: left/right becomes start/end.',
|
|
276
|
+
category: 'opt-in',
|
|
277
|
+
type: 'config',
|
|
278
|
+
configName: 'physical-to-logical',
|
|
279
|
+
detectPatterns: [
|
|
280
|
+
/#leftIcon|#rightIcon|#alphaIcon|#omegaIcon/,
|
|
281
|
+
/(<(?:dt-(?:item-layout|list-item)|Dt(?:ItemLayout|ListItem))[\s\S]*?)#(?:left|right|bottom)(?=[\s"'>])/,
|
|
282
|
+
/alpha-(?:disabled|loading|active)/,
|
|
283
|
+
/omega-(?:disabled|active)/,
|
|
284
|
+
/icon-position="(?:left|right)"/,
|
|
285
|
+
],
|
|
286
|
+
fileExtensions: ['.vue', '.html'],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'typography',
|
|
290
|
+
name: 'Typography Utilities to DtText',
|
|
291
|
+
description: 'Replace legacy typography utility classes (d-headline--*, d-body--*, d-label--*, d-code--md, d-fw-*, d-fc-*, d-lh-*, d-truncate, d-ta-*) with the <dt-text> component.',
|
|
292
|
+
category: 'opt-in',
|
|
293
|
+
type: 'standalone',
|
|
294
|
+
scriptDir: 'dialtone_migrate_typography',
|
|
295
|
+
detectPatterns: [
|
|
296
|
+
/class="[^"]*d-(?:headline|body|label)--(?:sm|md|lg|xl|xxl)/,
|
|
297
|
+
/class="[^"]*d-(?:headline|body|label)-(?:small|medium|large)/,
|
|
298
|
+
/class="[^"]*d-code--(?:sm|md|lg)/,
|
|
299
|
+
],
|
|
300
|
+
fileExtensions: ['.vue', '.html'],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'vue3-to-vue-imports',
|
|
304
|
+
name: 'Vue 3 Import Paths',
|
|
305
|
+
description: '@dialpad/dialtone-icons/vue3 and @dialpad/dialtone-vue/vue3 import paths renamed to /vue.',
|
|
306
|
+
category: 'required',
|
|
307
|
+
type: 'config',
|
|
308
|
+
configName: 'vue3-to-vue-imports',
|
|
309
|
+
detectPatterns: [
|
|
310
|
+
/@dialpad\/dialtone-icons\/vue3/,
|
|
311
|
+
/@dialpad\/dialtone-vue\/vue3/,
|
|
312
|
+
],
|
|
313
|
+
fileExtensions: ['.vue', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts'],
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// CLI parsing
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
function parseArgs (args) {
|
|
322
|
+
const cwdIndex = args.indexOf('--cwd');
|
|
323
|
+
const onlyIndex = args.indexOf('--only');
|
|
324
|
+
return {
|
|
325
|
+
help: args.includes('--help'),
|
|
326
|
+
dryRun: args.includes('--dry-run'),
|
|
327
|
+
autoYes: args.includes('--yes'),
|
|
328
|
+
healthCheck: args.includes('--health-check'),
|
|
329
|
+
all: args.includes('--all'),
|
|
330
|
+
cwd: cwdIndex !== -1 && args[cwdIndex + 1]
|
|
331
|
+
? path.resolve(args[cwdIndex + 1])
|
|
332
|
+
: process.cwd(),
|
|
333
|
+
only: onlyIndex !== -1 && args[onlyIndex + 1]
|
|
334
|
+
? args[onlyIndex + 1].split(',').map(s => s.trim())
|
|
335
|
+
: null,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function printHelp () {
|
|
340
|
+
console.log(`
|
|
341
|
+
Usage: npx dialtone-migrate [options]
|
|
342
|
+
|
|
343
|
+
Master migration script for Dialtone next major version.
|
|
344
|
+
Orchestrates all individual migration tools in the correct order.
|
|
345
|
+
|
|
346
|
+
Options:
|
|
347
|
+
--cwd <path> Working directory (default: cwd)
|
|
348
|
+
--dry-run Show changes without applying them
|
|
349
|
+
--yes Apply all changes without prompting
|
|
350
|
+
--health-check Report migration status without modifying files
|
|
351
|
+
--all Run all required migrations without selection prompt
|
|
352
|
+
--only <ids> Comma-separated list of migration IDs to run
|
|
353
|
+
--help Show help
|
|
354
|
+
|
|
355
|
+
Available migration IDs (required):
|
|
356
|
+
${MIGRATIONS.filter(m => m.category === 'required').map(m => ` ${m.id.padEnd(32)} ${m.name}`).join('\n')}
|
|
357
|
+
|
|
358
|
+
Available migration IDs (opt-in):
|
|
359
|
+
${MIGRATIONS.filter(m => m.category === 'opt-in').map(m => ` ${m.id.padEnd(32)} ${m.name}`).join('\n')}
|
|
360
|
+
|
|
361
|
+
Examples:
|
|
362
|
+
npx dialtone-migrate # Interactive selection
|
|
363
|
+
npx dialtone-migrate --health-check --cwd ./src # Check migration status
|
|
364
|
+
npx dialtone-migrate --all --dry-run # Dry-run all required
|
|
365
|
+
npx dialtone-migrate --only color-stops,hsl-to-oklch --yes
|
|
366
|
+
`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// File walker (shared utility)
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
const DEFAULT_IGNORE = ['node_modules', 'dist', '.git', '.vuepress/public', '.vuepress/.temp', '.vuepress/.cache', 'storybook-static'];
|
|
374
|
+
|
|
375
|
+
function isIgnoredPath (fullPath) {
|
|
376
|
+
const segments = fullPath.split(path.sep);
|
|
377
|
+
return DEFAULT_IGNORE.some(ig => {
|
|
378
|
+
if (ig.includes('/')) {
|
|
379
|
+
const parts = ig.split('/');
|
|
380
|
+
for (let i = 0; i + parts.length <= segments.length; i++) {
|
|
381
|
+
if (parts.every((p, j) => segments[i + j] === p)) return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
return segments.includes(ig);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function findFiles (dir, extensions) {
|
|
390
|
+
const results = [];
|
|
391
|
+
async function walk (currentDir) {
|
|
392
|
+
let entries;
|
|
393
|
+
try {
|
|
394
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
395
|
+
} catch { return; }
|
|
396
|
+
for (const entry of entries) {
|
|
397
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
398
|
+
if (isIgnoredPath(fullPath)) continue;
|
|
399
|
+
if (entry.isDirectory()) {
|
|
400
|
+
await walk(fullPath);
|
|
401
|
+
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
402
|
+
results.push(fullPath);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
await walk(dir);
|
|
407
|
+
return results;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Health check
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
async function healthCheck (cwd) {
|
|
415
|
+
console.log(`\n${'━'.repeat(70)}`);
|
|
416
|
+
console.log(' DIALTONE MIGRATION HEALTH CHECK');
|
|
417
|
+
console.log(`${'━'.repeat(70)}\n`);
|
|
418
|
+
console.log(` Scanning: ${cwd}\n`);
|
|
419
|
+
|
|
420
|
+
// Collect all needed extensions
|
|
421
|
+
const allExtensions = [...new Set(MIGRATIONS.flatMap(m => m.fileExtensions))];
|
|
422
|
+
const files = await findFiles(cwd, allExtensions);
|
|
423
|
+
|
|
424
|
+
if (files.length === 0) {
|
|
425
|
+
console.log(' No scannable files found in the target directory.\n');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Read all files once
|
|
430
|
+
const fileContents = new Map();
|
|
431
|
+
for (const file of files) {
|
|
432
|
+
try {
|
|
433
|
+
fileContents.set(file, await fs.readFile(file, 'utf8'));
|
|
434
|
+
} catch { /* skip unreadable */ }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const requiredResults = [];
|
|
438
|
+
const optInResults = [];
|
|
439
|
+
|
|
440
|
+
for (const migration of MIGRATIONS) {
|
|
441
|
+
let matchCount = 0;
|
|
442
|
+
const matchedFiles = new Set();
|
|
443
|
+
|
|
444
|
+
for (const [file, content] of fileContents) {
|
|
445
|
+
if (!migration.fileExtensions.some(ext => file.endsWith(ext))) continue;
|
|
446
|
+
for (const pattern of migration.detectPatterns) {
|
|
447
|
+
// Reset lastIndex for global patterns
|
|
448
|
+
const re = new RegExp(pattern.source, pattern.flags.replace('g', ''));
|
|
449
|
+
if (re.test(content)) {
|
|
450
|
+
matchedFiles.add(path.relative(cwd, file));
|
|
451
|
+
// Count all matches
|
|
452
|
+
const globalRe = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
|
|
453
|
+
const matches = content.match(globalRe);
|
|
454
|
+
if (matches) matchCount += matches.length;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const result = {
|
|
460
|
+
migration,
|
|
461
|
+
matchCount,
|
|
462
|
+
fileCount: matchedFiles.size,
|
|
463
|
+
files: [...matchedFiles].slice(0, 5), // Show up to 5 sample files
|
|
464
|
+
totalFiles: matchedFiles.size,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
if (migration.category === 'required') {
|
|
468
|
+
requiredResults.push(result);
|
|
469
|
+
} else {
|
|
470
|
+
optInResults.push(result);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Print required migrations
|
|
475
|
+
console.log(' REQUIRED MIGRATIONS (breaking changes)\n');
|
|
476
|
+
printHealthResults(requiredResults);
|
|
477
|
+
|
|
478
|
+
// Print opt-in migrations
|
|
479
|
+
console.log('\n OPT-IN MIGRATIONS (best practices)\n');
|
|
480
|
+
printHealthResults(optInResults);
|
|
481
|
+
|
|
482
|
+
// Summary
|
|
483
|
+
const requiredPending = requiredResults.filter(r => r.matchCount > 0);
|
|
484
|
+
const optInPending = optInResults.filter(r => r.matchCount > 0);
|
|
485
|
+
|
|
486
|
+
console.log(`\n${'━'.repeat(70)}`);
|
|
487
|
+
console.log(' SUMMARY');
|
|
488
|
+
console.log(`${'━'.repeat(70)}\n`);
|
|
489
|
+
|
|
490
|
+
if (requiredPending.length === 0) {
|
|
491
|
+
console.log(' ✓ All required migrations are complete!\n');
|
|
492
|
+
} else {
|
|
493
|
+
console.log(` ✗ ${requiredPending.length} required migration(s) still pending:\n`);
|
|
494
|
+
for (const r of requiredPending) {
|
|
495
|
+
console.log(` - ${r.migration.name} (${r.matchCount} matches in ${r.fileCount} files)`);
|
|
496
|
+
}
|
|
497
|
+
console.log();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (optInPending.length > 0) {
|
|
501
|
+
console.log(` ○ ${optInPending.length} opt-in migration(s) available:\n`);
|
|
502
|
+
for (const r of optInPending) {
|
|
503
|
+
console.log(` - ${r.migration.name} (${r.matchCount} matches in ${r.fileCount} files)`);
|
|
504
|
+
}
|
|
505
|
+
console.log();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function printHealthResults (results) {
|
|
510
|
+
for (const r of results) {
|
|
511
|
+
const status = r.matchCount === 0 ? '✓' : '✗';
|
|
512
|
+
const statusLabel = r.matchCount === 0 ? 'DONE' : 'PENDING';
|
|
513
|
+
const color = r.matchCount === 0 ? '\x1b[32m' : '\x1b[33m';
|
|
514
|
+
const reset = '\x1b[0m';
|
|
515
|
+
|
|
516
|
+
console.log(` ${color}${status} [${statusLabel}]${reset} ${r.migration.name} (${r.migration.id})`);
|
|
517
|
+
console.log(` ${r.migration.description}`);
|
|
518
|
+
|
|
519
|
+
if (r.matchCount > 0) {
|
|
520
|
+
console.log(` → ${r.matchCount} pattern match(es) in ${r.fileCount} file(s)`);
|
|
521
|
+
if (r.files.length > 0) {
|
|
522
|
+
for (const f of r.files) {
|
|
523
|
+
console.log(` ${f}`);
|
|
524
|
+
}
|
|
525
|
+
if (r.totalFiles > 5) {
|
|
526
|
+
console.log(` ... and ${r.totalFiles - 5} more`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
console.log();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// Interactive selection
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
function createReadlineInterface () {
|
|
539
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function prompt (question) {
|
|
543
|
+
const rl = createReadlineInterface();
|
|
544
|
+
return new Promise(resolve => {
|
|
545
|
+
rl.question(question, answer => {
|
|
546
|
+
rl.close();
|
|
547
|
+
resolve(answer.trim().toLowerCase());
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function selectMigrations (migrations) {
|
|
553
|
+
console.log('\nAvailable migrations:\n');
|
|
554
|
+
|
|
555
|
+
const required = migrations.filter(m => m.category === 'required');
|
|
556
|
+
const optIn = migrations.filter(m => m.category === 'opt-in');
|
|
557
|
+
|
|
558
|
+
if (required.length > 0) {
|
|
559
|
+
console.log(' REQUIRED (breaking changes):');
|
|
560
|
+
required.forEach((m, i) => {
|
|
561
|
+
console.log(` [${i + 1}] ${m.name} (${m.id})`);
|
|
562
|
+
console.log(` ${m.description}`);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (optIn.length > 0) {
|
|
567
|
+
console.log('\n OPT-IN (best practices):');
|
|
568
|
+
optIn.forEach((m, i) => {
|
|
569
|
+
console.log(` [${required.length + i + 1}] ${m.name} (${m.id})`);
|
|
570
|
+
console.log(` ${m.description}`);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const all = [...required, ...optIn];
|
|
575
|
+
console.log(`\n [a] All required migrations`);
|
|
576
|
+
console.log(` [q] Quit\n`);
|
|
577
|
+
|
|
578
|
+
const answer = await prompt('Select migrations (comma-separated numbers, "a" for all required, or "q" to quit): ');
|
|
579
|
+
|
|
580
|
+
if (answer === 'q') return [];
|
|
581
|
+
if (answer === 'a') return required;
|
|
582
|
+
|
|
583
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1);
|
|
584
|
+
const selected = indices
|
|
585
|
+
.filter(i => i >= 0 && i < all.length)
|
|
586
|
+
.map(i => all[i]);
|
|
587
|
+
|
|
588
|
+
if (selected.length === 0) {
|
|
589
|
+
console.log('No valid selections. Exiting.');
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return selected;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
// Migration runner
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Run a config-based migration using only Node builtins.
|
|
602
|
+
* Reads the config's expressions and applies them to matching files
|
|
603
|
+
* without importing the migration-helper (which requires chalk/globby/inquirer).
|
|
604
|
+
*/
|
|
605
|
+
async function runConfigMigration (migration, opts) {
|
|
606
|
+
const configPath = path.resolve(
|
|
607
|
+
__dirname, '..', 'dialtone_migration_helper', 'configs', `${migration.configName}.mjs`,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const { default: configData } = await import(configPath);
|
|
611
|
+
|
|
612
|
+
// Derive file extensions from the config's glob patterns
|
|
613
|
+
const extPattern = /\{([^}]+)\}/;
|
|
614
|
+
const extensions = [];
|
|
615
|
+
for (const p of (configData.patterns || [])) {
|
|
616
|
+
const m = p.match(extPattern);
|
|
617
|
+
if (m) {
|
|
618
|
+
for (const ext of m[1].split(',')) {
|
|
619
|
+
const e = '.' + ext.trim();
|
|
620
|
+
if (!extensions.includes(e)) extensions.push(e);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Fallback to migration registry extensions if config patterns don't specify
|
|
625
|
+
if (extensions.length === 0) {
|
|
626
|
+
extensions.push(...migration.fileExtensions);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
console.log(` Configuration: ${migration.configName}`);
|
|
630
|
+
console.log(` ${configData.description.split('\n')[0]}\n`);
|
|
631
|
+
|
|
632
|
+
// Find files using the master script's own walker
|
|
633
|
+
const allFiles = await findFiles(opts.cwd, extensions);
|
|
634
|
+
|
|
635
|
+
// Read and filter files that have matches
|
|
636
|
+
const matched = [];
|
|
637
|
+
for (const file of allFiles) {
|
|
638
|
+
let data;
|
|
639
|
+
try {
|
|
640
|
+
data = await fs.readFile(file, 'utf8');
|
|
641
|
+
} catch { continue; }
|
|
642
|
+
// Skip likely minified files
|
|
643
|
+
if ((data.match(/[\n\r]/g) || []).length <= 3) continue;
|
|
644
|
+
let matchCount = 0;
|
|
645
|
+
for (const expr of configData.expressions) {
|
|
646
|
+
const testRe = new RegExp(expr.from.source, expr.from.flags.replace('g', ''));
|
|
647
|
+
if (testRe.test(data)) matchCount++;
|
|
648
|
+
}
|
|
649
|
+
if (matchCount > 0) {
|
|
650
|
+
matched.push({ file, data, matches: 0 });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (matched.length === 0) {
|
|
655
|
+
console.log(' No matches found. Skipping.\n');
|
|
656
|
+
return { skipped: true };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log(` ${matched.length} file(s) queued for modification.`);
|
|
660
|
+
|
|
661
|
+
if (opts.dryRun) {
|
|
662
|
+
console.log(' --dry-run: No files were modified.\n');
|
|
663
|
+
for (const f of matched.slice(0, 10)) {
|
|
664
|
+
console.log(` ${path.relative(opts.cwd, f.file)}`);
|
|
665
|
+
}
|
|
666
|
+
if (matched.length > 10) console.log(` ... and ${matched.length - 10} more`);
|
|
667
|
+
return { dryRun: true, fileCount: matched.length };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!opts.autoYes) {
|
|
671
|
+
const answer = await prompt(` Apply changes to ${matched.length} file(s)? (y/N) `);
|
|
672
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
673
|
+
console.log(' Skipped.\n');
|
|
674
|
+
return { skipped: true };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Apply expressions and write files.
|
|
679
|
+
// Loop until convergence: some config regexes only match one token per
|
|
680
|
+
// property declaration per pass (e.g. border-width with two var(--dt-size-*)).
|
|
681
|
+
for (const entry of matched) {
|
|
682
|
+
let changed = true;
|
|
683
|
+
while (changed) {
|
|
684
|
+
changed = false;
|
|
685
|
+
for (const expr of configData.expressions) {
|
|
686
|
+
const before = entry.data;
|
|
687
|
+
// String.prototype.replace handles both string ($1 backrefs) and
|
|
688
|
+
// function replacers natively — no callback wrapper needed for
|
|
689
|
+
// strings. The previous match.replace() approach broke lookaheads
|
|
690
|
+
// because the matched substring doesn't include lookahead chars.
|
|
691
|
+
entry.data = entry.data.replace(expr.from, expr.to);
|
|
692
|
+
if (entry.data !== before) {
|
|
693
|
+
entry.matches++;
|
|
694
|
+
changed = true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (entry.matches > 0) {
|
|
699
|
+
await fs.writeFile(entry.file, entry.data, 'utf8');
|
|
700
|
+
const shortname = path.relative(opts.cwd, entry.file);
|
|
701
|
+
console.log(` >> ${shortname}, ${entry.matches} changes`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
console.log();
|
|
706
|
+
return { applied: true, fileCount: matched.length };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Run a standalone migration script as a child process.
|
|
711
|
+
*/
|
|
712
|
+
async function runStandaloneMigration (migration, opts) {
|
|
713
|
+
const scriptPath = path.resolve(__dirname, '..', migration.scriptDir, 'index.mjs');
|
|
714
|
+
|
|
715
|
+
const args = ['--cwd', opts.cwd];
|
|
716
|
+
if (opts.dryRun) args.push('--dry-run');
|
|
717
|
+
if (opts.autoYes) args.push('--yes');
|
|
718
|
+
|
|
719
|
+
return new Promise((resolve, reject) => {
|
|
720
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
721
|
+
stdio: 'inherit',
|
|
722
|
+
cwd: process.cwd(),
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
child.on('close', code => {
|
|
726
|
+
// Exit code 0 means success (or nothing to do)
|
|
727
|
+
resolve({ exitCode: code });
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
child.on('error', reject);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function runMigration (migration, opts) {
|
|
735
|
+
if (migration.type === 'config') {
|
|
736
|
+
return runConfigMigration(migration, opts);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (migration.type === 'standalone') {
|
|
740
|
+
return runStandaloneMigration(migration, opts);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
console.log(` ⚠ Manual migration required. See migration guide for details.\n`);
|
|
744
|
+
return { manual: true };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
// Main
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
|
|
751
|
+
async function main () {
|
|
752
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
753
|
+
|
|
754
|
+
if (opts.help) {
|
|
755
|
+
printHelp();
|
|
756
|
+
process.exit(0);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
console.log(`\n${'━'.repeat(70)}`);
|
|
760
|
+
console.log(' DIALTONE MIGRATION TOOL');
|
|
761
|
+
console.log(`${'━'.repeat(70)}`);
|
|
762
|
+
|
|
763
|
+
// Health check mode
|
|
764
|
+
if (opts.healthCheck) {
|
|
765
|
+
await healthCheck(opts.cwd);
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Determine which migrations to run
|
|
770
|
+
let selected;
|
|
771
|
+
|
|
772
|
+
if (opts.only) {
|
|
773
|
+
selected = opts.only.map(id => {
|
|
774
|
+
const m = MIGRATIONS.find(m => m.id === id);
|
|
775
|
+
if (!m) {
|
|
776
|
+
console.error(`\n Unknown migration ID: "${id}"`);
|
|
777
|
+
console.error(` Available IDs: ${MIGRATIONS.map(m => m.id).join(', ')}\n`);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
return m;
|
|
781
|
+
});
|
|
782
|
+
} else if (opts.all) {
|
|
783
|
+
selected = MIGRATIONS.filter(m => m.category === 'required');
|
|
784
|
+
} else {
|
|
785
|
+
selected = await selectMigrations(MIGRATIONS);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (selected.length === 0) {
|
|
789
|
+
console.log('\n No migrations selected. Exiting.\n');
|
|
790
|
+
process.exit(0);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Confirmation
|
|
794
|
+
console.log(`\n Target directory: ${opts.cwd}`);
|
|
795
|
+
console.log(` Migrations to run (${selected.length}):\n`);
|
|
796
|
+
for (const m of selected) {
|
|
797
|
+
const tag = m.category === 'required' ? '[REQUIRED]' : '[OPT-IN]';
|
|
798
|
+
console.log(` ${tag} ${m.name}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (opts.dryRun) {
|
|
802
|
+
console.log('\n Mode: DRY RUN (no files will be modified)\n');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!opts.autoYes && !opts.dryRun) {
|
|
806
|
+
console.log(`\n ⚠ Please ensure you are running this in a repository where changes`);
|
|
807
|
+
console.log(` can be rolled back. Modifications will occur to files.\n`);
|
|
808
|
+
|
|
809
|
+
const answer = await prompt(' Proceed? (y/N) ');
|
|
810
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
811
|
+
console.log(' Cancelled.\n');
|
|
812
|
+
process.exit(0);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Run migrations in order
|
|
817
|
+
const results = [];
|
|
818
|
+
|
|
819
|
+
for (let i = 0; i < selected.length; i++) {
|
|
820
|
+
const migration = selected[i];
|
|
821
|
+
console.log(`\n${'─'.repeat(70)}`);
|
|
822
|
+
console.log(` [${i + 1}/${selected.length}] ${migration.name} (${migration.id})`);
|
|
823
|
+
console.log(`${'─'.repeat(70)}\n`);
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
const result = await runMigration(migration, opts);
|
|
827
|
+
results.push({ migration, ...result, success: true });
|
|
828
|
+
} catch (err) {
|
|
829
|
+
console.error(`\n ✗ Error running ${migration.name}: ${err.message}\n`);
|
|
830
|
+
results.push({ migration, success: false, error: err.message });
|
|
831
|
+
|
|
832
|
+
const answer = await prompt(' Continue with remaining migrations? (y/N) ');
|
|
833
|
+
if (answer !== 'y' && answer !== 'yes') break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Final summary
|
|
838
|
+
console.log(`\n${'━'.repeat(70)}`);
|
|
839
|
+
console.log(' MIGRATION SUMMARY');
|
|
840
|
+
console.log(`${'━'.repeat(70)}\n`);
|
|
841
|
+
|
|
842
|
+
for (const r of results) {
|
|
843
|
+
const status = r.success
|
|
844
|
+
? (r.skipped ? '○ SKIPPED' : r.dryRun ? '◐ DRY RUN' : r.manual ? '⚠ MANUAL' : '✓ DONE')
|
|
845
|
+
: '✗ FAILED';
|
|
846
|
+
console.log(` ${status} ${r.migration.name}`);
|
|
847
|
+
if (!r.success && r.error) console.log(` ${r.error}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
console.log(`\n Tip: Run with --health-check to verify remaining migration work.\n`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
main().catch(err => {
|
|
854
|
+
console.error(err);
|
|
855
|
+
process.exit(1);
|
|
856
|
+
});
|