generate-ui-cli 2.1.7 → 2.3.0
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/README.md +216 -27
- package/dist/commands/angular.js +535 -40
- package/dist/commands/generate.js +350 -20
- package/dist/commands/login.js +131 -42
- package/dist/commands/merge.js +201 -0
- package/dist/generate-ui/routes.gen.js +4 -0
- package/dist/generators/angular/feature.generator.js +3183 -583
- package/dist/generators/angular/menu.generator.js +165 -0
- package/dist/generators/angular/routes.generator.js +8 -3
- package/dist/generators/screen.generator.js +100 -5
- package/dist/generators/screen.merge.js +70 -15
- package/dist/index.js +88 -12
- package/dist/license/guard.js +2 -1
- package/dist/license/permissions.js +63 -9
- package/dist/license/token.js +38 -3
- package/dist/postinstall.js +47 -0
- package/dist/runtime/config.js +4 -0
- package/dist/runtime/logger.js +29 -0
- package/dist/runtime/project-config.js +64 -0
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -6,25 +6,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.login = login;
|
|
7
7
|
const http_1 = __importDefault(require("http"));
|
|
8
8
|
const url_1 = require("url");
|
|
9
|
-
const
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
10
|
const config_1 = require("../runtime/config");
|
|
11
11
|
const user_config_1 = require("../runtime/user-config");
|
|
12
12
|
const open_browser_1 = require("../runtime/open-browser");
|
|
13
13
|
const token_1 = require("../license/token");
|
|
14
14
|
const permissions_1 = require("../license/permissions");
|
|
15
15
|
const telemetry_1 = require("../telemetry");
|
|
16
|
+
const logger_1 = require("../runtime/logger");
|
|
16
17
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
17
18
|
async function login(options) {
|
|
18
19
|
void (0, telemetry_1.trackCommand)('login', options.telemetryEnabled);
|
|
20
|
+
(0, logger_1.logStep)('Starting login flow');
|
|
19
21
|
const token = await waitForLogin();
|
|
20
22
|
(0, token_1.saveToken)(token);
|
|
23
|
+
(0, logger_1.logDebug)('Token saved');
|
|
24
|
+
let permissionsLoaded = false;
|
|
25
|
+
let subscriptionReason = '';
|
|
21
26
|
try {
|
|
22
|
-
await (0, permissions_1.fetchPermissions)();
|
|
27
|
+
const permissions = await (0, permissions_1.fetchPermissions)();
|
|
28
|
+
permissionsLoaded = true;
|
|
29
|
+
subscriptionReason = String(permissions.subscription.reason ?? '').trim();
|
|
23
30
|
}
|
|
24
31
|
catch {
|
|
25
|
-
|
|
32
|
+
console.warn('⚠ Não foi possível validar a licença agora. Verifique sua conexão e rode o comando novamente se necessário.');
|
|
26
33
|
}
|
|
27
|
-
const email =
|
|
34
|
+
const email = resolveLoginEmail();
|
|
28
35
|
if (email) {
|
|
29
36
|
(0, user_config_1.updateUserConfig)((config) => ({
|
|
30
37
|
...config,
|
|
@@ -32,27 +39,36 @@ async function login(options) {
|
|
|
32
39
|
}));
|
|
33
40
|
}
|
|
34
41
|
await (0, telemetry_1.trackLogin)(email, options.telemetryEnabled);
|
|
35
|
-
console.log(
|
|
42
|
+
console.log(permissionsLoaded
|
|
43
|
+
? '✔ Login completo'
|
|
44
|
+
: '✔ Login completo (verificação pendente)');
|
|
45
|
+
if (permissionsLoaded && subscriptionReason) {
|
|
46
|
+
console.log(`ℹ Subscription: ${subscriptionReason}`);
|
|
47
|
+
}
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
49
|
+
function resolveLoginEmail() {
|
|
50
|
+
const envEmail = process.env.GIT_AUTHOR_EMAIL ||
|
|
51
|
+
process.env.GIT_COMMITTER_EMAIL ||
|
|
52
|
+
process.env.EMAIL;
|
|
53
|
+
if (envEmail && envEmail.trim().length) {
|
|
54
|
+
return envEmail.trim();
|
|
55
|
+
}
|
|
44
56
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
const output = (0, child_process_1.execSync)('git config --get user.email', {
|
|
58
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
59
|
+
})
|
|
60
|
+
.toString()
|
|
61
|
+
.trim();
|
|
62
|
+
return output.length ? output : null;
|
|
48
63
|
}
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
68
|
async function waitForLogin() {
|
|
54
69
|
return new Promise((resolve, reject) => {
|
|
55
70
|
let loginUrl = '';
|
|
71
|
+
let settled = false;
|
|
56
72
|
const server = http_1.default.createServer((req, res) => {
|
|
57
73
|
const requestUrl = req.url || '/';
|
|
58
74
|
if (!requestUrl.startsWith('/callback')) {
|
|
@@ -68,7 +84,7 @@ async function waitForLogin() {
|
|
|
68
84
|
res.end('Missing access token');
|
|
69
85
|
return;
|
|
70
86
|
}
|
|
71
|
-
const expiresAt = expiresAtParam ||
|
|
87
|
+
const expiresAt = normalizeExpiresAt(expiresAtParam) ||
|
|
72
88
|
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
73
89
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
74
90
|
res.end(`<!doctype html>
|
|
@@ -79,72 +95,128 @@ async function waitForLogin() {
|
|
|
79
95
|
<title>GenerateUI</title>
|
|
80
96
|
<style>
|
|
81
97
|
:root {
|
|
82
|
-
--bg: #
|
|
98
|
+
--bg-1: #f9f5ea;
|
|
99
|
+
--bg-2: #eef7f4;
|
|
83
100
|
--card: #ffffff;
|
|
84
|
-
--text: #
|
|
85
|
-
--muted: #
|
|
86
|
-
--
|
|
87
|
-
--
|
|
101
|
+
--text: #39455f;
|
|
102
|
+
--muted: #76819a;
|
|
103
|
+
--accent: #6fd3c0;
|
|
104
|
+
--accent-2: #9fd8ff;
|
|
105
|
+
--border: #e1e7f2;
|
|
106
|
+
--shadow: rgba(76, 88, 120, 0.14);
|
|
88
107
|
}
|
|
89
108
|
* {
|
|
90
109
|
box-sizing: border-box;
|
|
91
|
-
font-family: "
|
|
110
|
+
font-family: "Manrope", "Segoe UI", sans-serif;
|
|
92
111
|
}
|
|
93
112
|
body {
|
|
94
113
|
margin: 0;
|
|
95
114
|
min-height: 100vh;
|
|
96
115
|
display: grid;
|
|
97
116
|
place-items: center;
|
|
98
|
-
background:
|
|
117
|
+
background:
|
|
118
|
+
radial-gradient(circle at 15% 0%, #fff3c8 0%, var(--bg-1) 36%, transparent 60%),
|
|
119
|
+
radial-gradient(circle at 85% 0%, #e6f7ff 0%, var(--bg-2) 40%, transparent 70%),
|
|
120
|
+
linear-gradient(135deg, #fdfbf6, #f3f7fb);
|
|
99
121
|
color: var(--text);
|
|
100
122
|
}
|
|
101
123
|
main {
|
|
102
|
-
background:
|
|
103
|
-
padding:
|
|
104
|
-
border-radius:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.94), rgba(255,255,255,0.98));
|
|
125
|
+
padding: 44px;
|
|
126
|
+
border-radius: 28px;
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
box-shadow: 0 24px 60px var(--shadow);
|
|
129
|
+
width: min(520px, 92vw);
|
|
130
|
+
text-align: left;
|
|
131
|
+
position: relative;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
}
|
|
134
|
+
main::before {
|
|
135
|
+
content: "";
|
|
136
|
+
position: absolute;
|
|
137
|
+
inset: -40% 25% auto auto;
|
|
138
|
+
width: 280px;
|
|
139
|
+
height: 280px;
|
|
140
|
+
background: radial-gradient(circle, rgba(111,211,192,0.35), transparent 70%);
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
}
|
|
143
|
+
.label {
|
|
144
|
+
letter-spacing: 0.22em;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
color: var(--muted);
|
|
147
|
+
text-transform: uppercase;
|
|
108
148
|
}
|
|
109
149
|
h1 {
|
|
110
|
-
margin:
|
|
150
|
+
margin: 10px 0 12px;
|
|
111
151
|
font-size: 30px;
|
|
152
|
+
letter-spacing: 0.02em;
|
|
112
153
|
}
|
|
113
154
|
p {
|
|
114
155
|
margin: 0 0 24px;
|
|
115
156
|
color: var(--muted);
|
|
116
|
-
|
|
157
|
+
line-height: 1.5;
|
|
117
158
|
}
|
|
118
159
|
.pill {
|
|
119
|
-
|
|
120
|
-
background: #f5e9ff;
|
|
121
|
-
color: var(--primary);
|
|
122
|
-
padding: 8px 14px;
|
|
160
|
+
padding: 4px 10px;
|
|
123
161
|
border-radius: 999px;
|
|
124
|
-
|
|
125
|
-
|
|
162
|
+
background: rgba(255,255,255,0.65);
|
|
163
|
+
border: 1px solid var(--border);
|
|
164
|
+
font-size: 12px;
|
|
165
|
+
color: var(--muted);
|
|
166
|
+
}
|
|
167
|
+
.footer {
|
|
168
|
+
margin-top: 12px;
|
|
169
|
+
font-size: 13px;
|
|
170
|
+
color: var(--muted);
|
|
171
|
+
}
|
|
172
|
+
@media (max-width: 520px) {
|
|
173
|
+
main {
|
|
174
|
+
padding: 32px 24px;
|
|
175
|
+
text-align: center;
|
|
176
|
+
}
|
|
126
177
|
}
|
|
127
178
|
</style>
|
|
128
179
|
</head>
|
|
129
180
|
<body>
|
|
130
181
|
<main>
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
<
|
|
182
|
+
<div class="label">Generated UI</div>
|
|
183
|
+
<h1>Login completed</h1>
|
|
184
|
+
<p>You can now return to the terminal.</p>
|
|
185
|
+
<div class="footer">
|
|
186
|
+
<span class="pill">You can close this window</span>
|
|
187
|
+
</div>
|
|
134
188
|
</main>
|
|
135
189
|
</body>
|
|
136
190
|
</html>`);
|
|
137
191
|
clearTimeout(timeout);
|
|
138
192
|
server.close();
|
|
193
|
+
settled = true;
|
|
194
|
+
process.off('SIGINT', handleSigint);
|
|
195
|
+
process.off('SIGTERM', handleSigint);
|
|
139
196
|
resolve({ accessToken, expiresAt });
|
|
140
197
|
});
|
|
198
|
+
const handleSigint = () => {
|
|
199
|
+
if (settled)
|
|
200
|
+
return;
|
|
201
|
+
settled = true;
|
|
202
|
+
clearTimeout(timeout);
|
|
203
|
+
server.close();
|
|
204
|
+
reject(new Error('Login canceled by user (SIGINT).'));
|
|
205
|
+
};
|
|
141
206
|
const timeout = setTimeout(() => {
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
209
|
+
settled = true;
|
|
142
210
|
server.close();
|
|
211
|
+
process.off('SIGINT', handleSigint);
|
|
212
|
+
process.off('SIGTERM', handleSigint);
|
|
143
213
|
const help = loginUrl
|
|
144
214
|
? ` Ensure the login page is reachable and try again: ${loginUrl}`
|
|
145
215
|
: ` Ensure ${(0, config_1.getWebAuthUrl)()} and ${(0, config_1.getApiBaseUrl)()} are reachable.`;
|
|
146
216
|
reject(new Error(`Login timed out.${help}`));
|
|
147
217
|
}, LOGIN_TIMEOUT_MS);
|
|
218
|
+
process.on('SIGINT', handleSigint);
|
|
219
|
+
process.on('SIGTERM', handleSigint);
|
|
148
220
|
server.listen(0, () => {
|
|
149
221
|
const address = server.address();
|
|
150
222
|
if (!address || typeof address === 'string') {
|
|
@@ -158,7 +230,24 @@ async function waitForLogin() {
|
|
|
158
230
|
url.searchParams.set('api_base', (0, config_1.getApiBaseUrl)());
|
|
159
231
|
loginUrl = url.toString();
|
|
160
232
|
console.log(`Open this URL to finish login: ${loginUrl}`);
|
|
233
|
+
(0, logger_1.logDebug)(`Login callback listening on ${redirectUri}`);
|
|
161
234
|
(0, open_browser_1.openBrowser)(loginUrl);
|
|
162
235
|
});
|
|
163
236
|
});
|
|
164
237
|
}
|
|
238
|
+
function normalizeExpiresAt(value) {
|
|
239
|
+
if (!value)
|
|
240
|
+
return null;
|
|
241
|
+
const trimmed = value.trim();
|
|
242
|
+
if (!trimmed.length)
|
|
243
|
+
return null;
|
|
244
|
+
const asNumber = Number(trimmed);
|
|
245
|
+
if (Number.isFinite(asNumber)) {
|
|
246
|
+
const ms = asNumber < 1e12 ? asNumber * 1000 : asNumber;
|
|
247
|
+
return new Date(ms).toISOString();
|
|
248
|
+
}
|
|
249
|
+
const parsed = new Date(trimmed);
|
|
250
|
+
if (Number.isNaN(parsed.getTime()))
|
|
251
|
+
return null;
|
|
252
|
+
return parsed.toISOString();
|
|
253
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.merge = merge;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const logger_1 = require("../runtime/logger");
|
|
12
|
+
const project_config_1 = require("../runtime/project-config");
|
|
13
|
+
async function merge(options) {
|
|
14
|
+
const projectConfig = (0, project_config_1.findProjectConfig)(process.cwd());
|
|
15
|
+
const configuredFeatures = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'features');
|
|
16
|
+
const featuresRoot = resolveFeaturesRoot(options.featuresPath, configuredFeatures, projectConfig.configPath);
|
|
17
|
+
const generatedRoot = path_1.default.join(featuresRoot, 'generated');
|
|
18
|
+
const overridesRoot = path_1.default.join(featuresRoot, 'overrides');
|
|
19
|
+
if (!options.feature) {
|
|
20
|
+
throw new Error('Missing --feature. Example: generate-ui merge --feature ProductsAdmin');
|
|
21
|
+
}
|
|
22
|
+
const folder = resolveFeatureFolder(options.feature, generatedRoot, overridesRoot);
|
|
23
|
+
const files = resolveFiles(folder, options.file);
|
|
24
|
+
if (files.length === 0) {
|
|
25
|
+
throw new Error('No files selected to compare.');
|
|
26
|
+
}
|
|
27
|
+
const selectedTool = options.tool || 'code';
|
|
28
|
+
(0, logger_1.logStep)(`Generated: ${generatedRoot}`);
|
|
29
|
+
(0, logger_1.logStep)(`Overrides: ${overridesRoot}`);
|
|
30
|
+
(0, logger_1.logStep)(`Merge tool: ${selectedTool}${selectedTool === 'code'
|
|
31
|
+
? ' (merge editor, recommended)'
|
|
32
|
+
: selectedTool === 'code-diff'
|
|
33
|
+
? ' (red/green diff)'
|
|
34
|
+
: ''}`);
|
|
35
|
+
let compared = 0;
|
|
36
|
+
for (const fileName of files) {
|
|
37
|
+
const generatedPath = path_1.default.join(generatedRoot, folder, fileName);
|
|
38
|
+
const overridePath = path_1.default.join(overridesRoot, folder, fileName);
|
|
39
|
+
if (!fs_1.default.existsSync(generatedPath)) {
|
|
40
|
+
console.warn(`⚠ Missing generated file: ${generatedPath}`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!fs_1.default.existsSync(overridePath)) {
|
|
44
|
+
fs_1.default.mkdirSync(path_1.default.dirname(overridePath), { recursive: true });
|
|
45
|
+
fs_1.default.copyFileSync(generatedPath, overridePath);
|
|
46
|
+
console.log(`ℹ Created override file: ${overridePath}`);
|
|
47
|
+
}
|
|
48
|
+
openMergeTool(selectedTool, generatedPath, overridePath);
|
|
49
|
+
compared += 1;
|
|
50
|
+
}
|
|
51
|
+
if (compared === 0) {
|
|
52
|
+
throw new Error(`No comparable files found for feature "${folder}".`);
|
|
53
|
+
}
|
|
54
|
+
(0, logger_1.logTip)('Save changes in the overrides file (right side) to keep custom edits.');
|
|
55
|
+
}
|
|
56
|
+
function resolveFeaturesRoot(value, configured, configPath) {
|
|
57
|
+
const fromConfig = (0, project_config_1.resolveOptionalPath)(value, configured, configPath);
|
|
58
|
+
if (fromConfig) {
|
|
59
|
+
return normalizeFeaturesRoot(fromConfig);
|
|
60
|
+
}
|
|
61
|
+
const srcAppRoot = path_1.default.resolve(process.cwd(), 'src', 'app');
|
|
62
|
+
if (fs_1.default.existsSync(srcAppRoot)) {
|
|
63
|
+
return path_1.default.join(srcAppRoot, 'features');
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Default features path not found.\n' +
|
|
66
|
+
'Use --features <path> or set "features" (or "paths.features") in generateui-config.json.');
|
|
67
|
+
}
|
|
68
|
+
function normalizeFeaturesRoot(value) {
|
|
69
|
+
const isSrcApp = path_1.default.basename(value) === 'app' &&
|
|
70
|
+
path_1.default.basename(path_1.default.dirname(value)) === 'src';
|
|
71
|
+
if (isSrcApp)
|
|
72
|
+
return path_1.default.join(value, 'features');
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function resolveFeatureFolder(value, generatedRoot, overridesRoot) {
|
|
76
|
+
const folders = listFeatureFolders(generatedRoot, overridesRoot);
|
|
77
|
+
if (folders.length === 0) {
|
|
78
|
+
throw new Error(`No feature folders found under ${generatedRoot} or ${overridesRoot}.`);
|
|
79
|
+
}
|
|
80
|
+
const direct = normalizeFeatureKey(value);
|
|
81
|
+
const matches = folders.filter(folder => normalizeFeatureKey(folder) === direct);
|
|
82
|
+
if (matches.length === 1)
|
|
83
|
+
return matches[0];
|
|
84
|
+
const contains = folders.filter(folder => normalizeFeatureKey(folder).includes(direct));
|
|
85
|
+
if (contains.length === 1)
|
|
86
|
+
return contains[0];
|
|
87
|
+
if (contains.length > 1) {
|
|
88
|
+
throw new Error(`Feature "${value}" is ambiguous. Matches: ${contains.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
const sample = folders.slice(0, 12).join(', ');
|
|
91
|
+
throw new Error(`Feature not found: ${value}.\n` +
|
|
92
|
+
`Available features: ${sample}${folders.length > 12 ? ', ...' : ''}`);
|
|
93
|
+
}
|
|
94
|
+
function listFeatureFolders(generatedRoot, overridesRoot) {
|
|
95
|
+
const set = new Set();
|
|
96
|
+
for (const root of [generatedRoot, overridesRoot]) {
|
|
97
|
+
if (!fs_1.default.existsSync(root))
|
|
98
|
+
continue;
|
|
99
|
+
for (const entry of fs_1.default.readdirSync(root, { withFileTypes: true })) {
|
|
100
|
+
if (!entry.isDirectory())
|
|
101
|
+
continue;
|
|
102
|
+
set.add(entry.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...set].sort((a, b) => a.localeCompare(b));
|
|
106
|
+
}
|
|
107
|
+
function normalizeFeatureKey(value) {
|
|
108
|
+
return String(value)
|
|
109
|
+
.trim()
|
|
110
|
+
.replace(/Component$/i, '')
|
|
111
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
112
|
+
.toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
function resolveFiles(folder, raw) {
|
|
115
|
+
const key = String(raw || 'component.ts').trim();
|
|
116
|
+
if (key === 'all') {
|
|
117
|
+
return [
|
|
118
|
+
`${folder}.component.ts`,
|
|
119
|
+
`${folder}.component.html`,
|
|
120
|
+
`${folder}.component.scss`
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
const normalized = key.startsWith('.')
|
|
124
|
+
? key.slice(1)
|
|
125
|
+
: key;
|
|
126
|
+
const suffix = normalized.startsWith('component')
|
|
127
|
+
? normalized
|
|
128
|
+
: `component.${normalized}`;
|
|
129
|
+
return [`${folder}.${suffix}`];
|
|
130
|
+
}
|
|
131
|
+
function openMergeTool(tool, generatedPath, overridePath) {
|
|
132
|
+
const run = (cmd, args) => {
|
|
133
|
+
try {
|
|
134
|
+
(0, child_process_1.execFileSync)(cmd, args, { stdio: 'inherit' });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const message = String(error?.message || '');
|
|
138
|
+
if (error?.code === 'ENOENT') {
|
|
139
|
+
throw new Error(`Merge tool "${cmd}" not found in PATH.\n` +
|
|
140
|
+
'Recommended: install VS Code command in PATH and use --tool code.\n' +
|
|
141
|
+
"In VS Code run: 'Shell Command: Install code command in PATH'.\n" +
|
|
142
|
+
'Or try one of these:\n' +
|
|
143
|
+
' --tool code\n' +
|
|
144
|
+
' --tool code-diff\n' +
|
|
145
|
+
' --tool vimdiff\n' +
|
|
146
|
+
' --tool diff');
|
|
147
|
+
}
|
|
148
|
+
if (cmd === 'opendiff' &&
|
|
149
|
+
message.includes('requires Xcode')) {
|
|
150
|
+
throw new Error('Tool "opendiff" requires full Xcode.\n' +
|
|
151
|
+
'Use --tool vimdiff or --tool diff, or install full Xcode.');
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
if (tool === 'code') {
|
|
157
|
+
openCodeMerge(run, generatedPath, overridePath);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (tool === 'code-diff') {
|
|
161
|
+
// Diff view: left = incoming generated, right = current overrides.
|
|
162
|
+
run('code', ['--wait', '--diff', generatedPath, overridePath]);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (tool === 'meld' || tool === 'kdiff3' || tool === 'bc') {
|
|
166
|
+
run('git', ['difftool', '--no-index', `--tool=${tool}`, generatedPath, overridePath]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
run(tool, [generatedPath, overridePath]);
|
|
170
|
+
}
|
|
171
|
+
function openCodeMerge(run, generatedPath, overridePath) {
|
|
172
|
+
const ext = path_1.default.extname(overridePath) || '.txt';
|
|
173
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'generate-ui-merge-'));
|
|
174
|
+
const currentPath = path_1.default.join(tempDir, `current${ext}`);
|
|
175
|
+
const incomingPath = path_1.default.join(tempDir, `incoming${ext}`);
|
|
176
|
+
const basePath = path_1.default.join(tempDir, `base${ext}`);
|
|
177
|
+
const resultPath = path_1.default.join(tempDir, `result${ext}`);
|
|
178
|
+
try {
|
|
179
|
+
fs_1.default.copyFileSync(overridePath, currentPath);
|
|
180
|
+
fs_1.default.copyFileSync(generatedPath, incomingPath);
|
|
181
|
+
fs_1.default.copyFileSync(generatedPath, basePath);
|
|
182
|
+
fs_1.default.copyFileSync(overridePath, resultPath);
|
|
183
|
+
// VS Code merge editor:
|
|
184
|
+
// - current: user's override
|
|
185
|
+
// - incoming: regenerated output
|
|
186
|
+
// - base: fallback ancestor (best effort, regenerated)
|
|
187
|
+
// - result: file that will be saved back to overrides
|
|
188
|
+
run('code', [
|
|
189
|
+
'--wait',
|
|
190
|
+
'--merge',
|
|
191
|
+
currentPath,
|
|
192
|
+
incomingPath,
|
|
193
|
+
basePath,
|
|
194
|
+
resultPath
|
|
195
|
+
]);
|
|
196
|
+
fs_1.default.copyFileSync(resultPath, overridePath);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
}
|