@gadmin2n/cli 0.0.105 → 0.0.107
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/CHANGELOG.md +17 -0
- package/actions/new.action.js +140 -94
- package/actions/update.action.d.ts +1 -0
- package/actions/update.action.js +564 -630
- package/commands/command.input.d.ts +1 -6
- package/commands/command.input.js +1 -7
- package/commands/command.loader.js +0 -2
- package/commands/new.command.js +8 -3
- package/commands/update.command.js +27 -37
- package/lib/template-merge/base-store.d.ts +16 -0
- package/lib/template-merge/base-store.js +127 -0
- package/lib/template-merge/binary-detect.d.ts +7 -0
- package/lib/template-merge/binary-detect.js +36 -0
- package/lib/template-merge/classify.d.ts +10 -0
- package/lib/template-merge/classify.js +61 -0
- package/lib/template-merge/config-validate.d.ts +5 -0
- package/lib/template-merge/config-validate.js +33 -0
- package/lib/template-merge/excludes.d.ts +7 -0
- package/lib/template-merge/excludes.js +109 -0
- package/lib/template-merge/index.d.ts +9 -8
- package/lib/template-merge/index.js +10 -8
- package/lib/template-merge/materialize.d.ts +18 -0
- package/lib/template-merge/materialize.js +129 -0
- package/lib/template-merge/merge3way.d.ts +15 -0
- package/lib/template-merge/merge3way.js +54 -0
- package/lib/template-merge/residual-scan.d.ts +10 -0
- package/lib/template-merge/residual-scan.js +31 -0
- package/lib/template-merge/types.d.ts +27 -0
- package/lib/template-merge/types.js +5 -0
- package/lib/ui/messages.d.ts +0 -2
- package/lib/ui/messages.js +0 -2
- package/package.json +2 -2
- package/commands/build.command.d.ts +0 -5
- package/commands/build.command.js +0 -50
- package/lib/template-merge/config-loader.d.ts +0 -15
- package/lib/template-merge/config-loader.js +0 -38
- package/lib/template-merge/env-merger.d.ts +0 -5
- package/lib/template-merge/env-merger.js +0 -57
- package/lib/template-merge/glob.d.ts +0 -5
- package/lib/template-merge/glob.js +0 -78
- package/lib/template-merge/json-merger.d.ts +0 -5
- package/lib/template-merge/json-merger.js +0 -269
- package/lib/template-merge/merger.d.ts +0 -30
- package/lib/template-merge/merger.js +0 -2
- package/lib/template-merge/prisma-merger.d.ts +0 -5
- package/lib/template-merge/prisma-merger.js +0 -112
- package/lib/template-merge/registry.d.ts +0 -25
- package/lib/template-merge/registry.js +0 -42
- package/lib/template-merge/ts-module-merger.d.ts +0 -16
- package/lib/template-merge/ts-module-merger.js +0 -193
package/actions/update.action.js
CHANGED
|
@@ -17,157 +17,37 @@ const path = require("path");
|
|
|
17
17
|
const inquirer = require("inquirer");
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
19
|
const abstract_action_1 = require("./abstract.action");
|
|
20
|
-
const template_merge_1 = require("../lib/template-merge");
|
|
21
20
|
const version_check_1 = require("../lib/version-check");
|
|
21
|
+
const merge3way_1 = require("../lib/template-merge/merge3way");
|
|
22
|
+
const binary_detect_1 = require("../lib/template-merge/binary-detect");
|
|
23
|
+
const residual_scan_1 = require("../lib/template-merge/residual-scan");
|
|
24
|
+
const classify_1 = require("../lib/template-merge/classify");
|
|
25
|
+
const excludes_1 = require("../lib/template-merge/excludes");
|
|
26
|
+
const base_store_1 = require("../lib/template-merge/base-store");
|
|
27
|
+
const materialize_1 = require("../lib/template-merge/materialize");
|
|
28
|
+
const config_validate_1 = require("../lib/template-merge/config-validate");
|
|
29
|
+
const types_1 = require("../lib/template-merge/types");
|
|
22
30
|
/* ------------------------------------------------------------------ *
|
|
23
31
|
* Config / constants
|
|
24
32
|
* ------------------------------------------------------------------ */
|
|
25
|
-
const DEFAULT_EXCLUDES = [
|
|
26
|
-
'node_modules',
|
|
27
|
-
'.git',
|
|
28
|
-
'dist',
|
|
29
|
-
'generated',
|
|
30
|
-
'.DS_Store',
|
|
31
|
-
'.env.local',
|
|
32
|
-
'readme.md',
|
|
33
|
-
// package manager lock files & PnP artifacts —
|
|
34
|
-
// regenerated by the auto-install stage based on the merged package.json.
|
|
35
|
-
// Letting template versions overwrite instance lock files is semantically
|
|
36
|
-
// wrong (lock should reflect actually-installed deps) and clutters diff output.
|
|
37
|
-
'yarn.lock',
|
|
38
|
-
'package-lock.json',
|
|
39
|
-
'pnpm-lock.yaml',
|
|
40
|
-
'.yarn',
|
|
41
|
-
'.pnp.cjs',
|
|
42
|
-
'.pnp.loader.mjs',
|
|
43
|
-
];
|
|
44
33
|
const CLAUDE_CONTEXT_REPO_HTTPS = 'https://git.tencent.com/OIT-OMC/erp-ai-coding-context.git';
|
|
45
34
|
const CLAUDE_CONTEXT_REPO_SSH = 'git@git.tencent.com:OIT-OMC/erp-ai-coding-context.git';
|
|
46
35
|
const CLAUDE_CONTEXT_BRANCH = 'master';
|
|
47
36
|
const CLAUDE_SOURCE_SUBDIR = '.claude';
|
|
48
37
|
const CLAUDE_TARGET_DIRS = ['.claude-internal', '.agent'];
|
|
49
|
-
/* ------------------------------------------------------------------ *
|
|
50
|
-
* Glob / pattern utilities
|
|
51
|
-
* ------------------------------------------------------------------ */
|
|
52
38
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
39
|
+
* Canonical formatting for "failed to materialize the base" errors.
|
|
40
|
+
* All three handlers (handleDiff/handleStatus/handleUpdate) share this so the
|
|
41
|
+
* user sees the same wording regardless of entry point.
|
|
55
42
|
*/
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const c = pattern[i];
|
|
61
|
-
if (c === '*') {
|
|
62
|
-
if (pattern[i + 1] === '*') {
|
|
63
|
-
if (pattern[i + 2] === '/') {
|
|
64
|
-
regexStr += '(?:.+/)?';
|
|
65
|
-
i += 3;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
regexStr += '.*';
|
|
69
|
-
i += 2;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
regexStr += '[^/]*';
|
|
74
|
-
i++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else if (c === '?') {
|
|
78
|
-
regexStr += '[^/]';
|
|
79
|
-
i++;
|
|
80
|
-
}
|
|
81
|
-
else if (c === '.' ||
|
|
82
|
-
c === '(' ||
|
|
83
|
-
c === ')' ||
|
|
84
|
-
c === '[' ||
|
|
85
|
-
c === ']' ||
|
|
86
|
-
c === '{' ||
|
|
87
|
-
c === '}' ||
|
|
88
|
-
c === '+' ||
|
|
89
|
-
c === '^' ||
|
|
90
|
-
c === '$' ||
|
|
91
|
-
c === '|' ||
|
|
92
|
-
c === '\\') {
|
|
93
|
-
regexStr += '\\' + c;
|
|
94
|
-
i++;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
regexStr += c;
|
|
98
|
-
i++;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
regexStr += '$';
|
|
102
|
-
return new RegExp(regexStr);
|
|
103
|
-
}
|
|
104
|
-
function matchesPattern(filePath, pattern) {
|
|
105
|
-
if (filePath === pattern)
|
|
106
|
-
return true;
|
|
107
|
-
if (filePath.startsWith(pattern + '/'))
|
|
108
|
-
return true;
|
|
109
|
-
const parts = filePath.split('/');
|
|
110
|
-
if (!pattern.includes('/') &&
|
|
111
|
-
!pattern.includes('*') &&
|
|
112
|
-
!pattern.includes('?')) {
|
|
113
|
-
if (parts.includes(pattern))
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
if (pattern.includes('*') || pattern.includes('?')) {
|
|
117
|
-
const regex = globToRegex(pattern);
|
|
118
|
-
return regex.test(filePath);
|
|
119
|
-
}
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
function shouldExclude(filePath, excludes) {
|
|
123
|
-
for (const pattern of excludes) {
|
|
124
|
-
if (matchesPattern(filePath, pattern))
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
function collectFiles(dir, excludes, base = '') {
|
|
130
|
-
const results = [];
|
|
131
|
-
const entries = fs.readdirSync(path.join(dir, base), { withFileTypes: true });
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
134
|
-
if (shouldExclude(rel, excludes))
|
|
135
|
-
continue;
|
|
136
|
-
if (entry.isDirectory()) {
|
|
137
|
-
results.push(...collectFiles(dir, excludes, rel));
|
|
138
|
-
}
|
|
139
|
-
else if (entry.isFile()) {
|
|
140
|
-
results.push(rel);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return results.sort();
|
|
144
|
-
}
|
|
145
|
-
function loadTemplateConfig() {
|
|
146
|
-
const configPaths = [
|
|
147
|
-
path.join(process.cwd(), 'server/gadmin-cli.json'),
|
|
148
|
-
path.join(process.cwd(), 'gadmin-cli.json'),
|
|
149
|
-
];
|
|
150
|
-
let templateName;
|
|
151
|
-
let userExcludes = [];
|
|
152
|
-
for (const configPath of configPaths) {
|
|
153
|
-
if (fs.existsSync(configPath)) {
|
|
154
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
155
|
-
templateName = config.template;
|
|
156
|
-
if (Array.isArray(config.templateExcludes)) {
|
|
157
|
-
userExcludes = config.templateExcludes;
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (!templateName) {
|
|
163
|
-
throw new Error('Cannot find "template" field in gadmin-cli.json. ' +
|
|
164
|
-
'Make sure you are in a gadmin2 project root directory.');
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
templateName,
|
|
168
|
-
excludes: [...DEFAULT_EXCLUDES, ...userExcludes],
|
|
169
|
-
};
|
|
43
|
+
function formatMaterializeError(schematicsVersion, err) {
|
|
44
|
+
var _a;
|
|
45
|
+
const m = (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err);
|
|
46
|
+
return `\n❌ Failed to materialize base @ schematics ${schematicsVersion}: ${m}\n`;
|
|
170
47
|
}
|
|
48
|
+
/* ------------------------------------------------------------------ *
|
|
49
|
+
* Template config / path resolution
|
|
50
|
+
* ------------------------------------------------------------------ */
|
|
171
51
|
function resolveTemplatePath(templateName) {
|
|
172
52
|
let schematicsRoot;
|
|
173
53
|
const searchPaths = [
|
|
@@ -208,101 +88,6 @@ function resolveTemplatePath(templateName) {
|
|
|
208
88
|
}
|
|
209
89
|
throw new Error(`Template directory not found for "${templateName}" in ${schematicsRoot}`);
|
|
210
90
|
}
|
|
211
|
-
/* ------------------------------------------------------------------ *
|
|
212
|
-
* Diff / merge utilities
|
|
213
|
-
* ------------------------------------------------------------------ */
|
|
214
|
-
function createUnifiedDiff(filePath, instanceContent, templateContent) {
|
|
215
|
-
const instanceLines = instanceContent.split('\n');
|
|
216
|
-
const templateLines = templateContent.split('\n');
|
|
217
|
-
const lines = [];
|
|
218
|
-
lines.push(chalk.bold(`--- a/${filePath} (instance)`));
|
|
219
|
-
lines.push(chalk.bold(`+++ b/${filePath} (template)`));
|
|
220
|
-
const maxLen = Math.max(instanceLines.length, templateLines.length);
|
|
221
|
-
const hunks = [];
|
|
222
|
-
let currentHunk = null;
|
|
223
|
-
for (let i = 0; i < maxLen; i++) {
|
|
224
|
-
const a = i < instanceLines.length ? instanceLines[i] : undefined;
|
|
225
|
-
const b = i < templateLines.length ? templateLines[i] : undefined;
|
|
226
|
-
if (a !== b) {
|
|
227
|
-
if (!currentHunk) {
|
|
228
|
-
currentHunk = { start: i, instanceLines: [], templateLines: [] };
|
|
229
|
-
}
|
|
230
|
-
if (a !== undefined)
|
|
231
|
-
currentHunk.instanceLines.push(a);
|
|
232
|
-
if (b !== undefined)
|
|
233
|
-
currentHunk.templateLines.push(b);
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
if (currentHunk) {
|
|
237
|
-
hunks.push(currentHunk);
|
|
238
|
-
currentHunk = null;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (currentHunk)
|
|
243
|
-
hunks.push(currentHunk);
|
|
244
|
-
for (const hunk of hunks.slice(0, 10)) {
|
|
245
|
-
const contextStart = Math.max(0, hunk.start - 2);
|
|
246
|
-
lines.push(chalk.cyan(`@@ -${hunk.start + 1},${hunk.instanceLines.length} +${hunk.start + 1},${hunk.templateLines.length} @@`));
|
|
247
|
-
for (let i = contextStart; i < hunk.start; i++) {
|
|
248
|
-
if (i < instanceLines.length) {
|
|
249
|
-
lines.push(` ${instanceLines[i]}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
for (const line of hunk.instanceLines) {
|
|
253
|
-
lines.push(chalk.red(`-${line}`));
|
|
254
|
-
}
|
|
255
|
-
for (const line of hunk.templateLines) {
|
|
256
|
-
lines.push(chalk.green(`+${line}`));
|
|
257
|
-
}
|
|
258
|
-
const afterStart = hunk.start + Math.max(hunk.instanceLines.length, hunk.templateLines.length);
|
|
259
|
-
for (let i = afterStart; i < Math.min(afterStart + 2, instanceLines.length); i++) {
|
|
260
|
-
lines.push(` ${instanceLines[i]}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (hunks.length > 10) {
|
|
264
|
-
lines.push(chalk.yellow(` ... and ${hunks.length - 10} more hunks`));
|
|
265
|
-
}
|
|
266
|
-
return lines.join('\n');
|
|
267
|
-
}
|
|
268
|
-
function createConflictContent(instanceContent, templateContent) {
|
|
269
|
-
return [
|
|
270
|
-
'<<<<<<< INSTANCE',
|
|
271
|
-
instanceContent,
|
|
272
|
-
'=======',
|
|
273
|
-
templateContent,
|
|
274
|
-
'>>>>>>> TEMPLATE',
|
|
275
|
-
].join('\n');
|
|
276
|
-
}
|
|
277
|
-
function tryAutoMerge(instanceContent, templateContent) {
|
|
278
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gadmin-merge-'));
|
|
279
|
-
const basePath = path.join(tmpDir, 'base');
|
|
280
|
-
const oursPath = path.join(tmpDir, 'ours');
|
|
281
|
-
const theirsPath = path.join(tmpDir, 'theirs');
|
|
282
|
-
try {
|
|
283
|
-
fs.writeFileSync(basePath, '', 'utf-8');
|
|
284
|
-
fs.writeFileSync(oursPath, instanceContent, 'utf-8');
|
|
285
|
-
fs.writeFileSync(theirsPath, templateContent, 'utf-8');
|
|
286
|
-
try {
|
|
287
|
-
(0, child_process_1.execSync)(`git merge-file -L "INSTANCE" -L "BASE" -L "TEMPLATE" "${oursPath}" "${basePath}" "${theirsPath}"`, { stdio: 'pipe' });
|
|
288
|
-
const merged = fs.readFileSync(oursPath, 'utf-8');
|
|
289
|
-
return { merged, hasConflicts: false };
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
if (err.status > 0) {
|
|
293
|
-
const merged = fs.readFileSync(oursPath, 'utf-8');
|
|
294
|
-
return { merged, hasConflicts: true };
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
merged: createConflictContent(instanceContent, templateContent),
|
|
298
|
-
hasConflicts: true,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
finally {
|
|
303
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
91
|
/* ------------------------------------------------------------------ *
|
|
307
92
|
* .claude context sync (from remote git repo)
|
|
308
93
|
* ------------------------------------------------------------------ */
|
|
@@ -591,40 +376,288 @@ function maybeInstallChangedDeps(snapshots, dryRun) {
|
|
|
591
376
|
}
|
|
592
377
|
});
|
|
593
378
|
}
|
|
379
|
+
function assertGitAvailable() {
|
|
380
|
+
try {
|
|
381
|
+
(0, child_process_1.execSync)('git --version', { stdio: 'pipe' });
|
|
382
|
+
}
|
|
383
|
+
catch (_a) {
|
|
384
|
+
throw new Error('gadmin2 update requires git in PATH (used internally for 3-way merge). Install git and retry.');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function assertNotBothOursAndTheirs(opts) {
|
|
388
|
+
if (opts.ours && opts.theirs) {
|
|
389
|
+
throw new Error('--ours and --theirs are mutually exclusive');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function loadGadminCliJson(instanceDir) {
|
|
393
|
+
const candidates = [
|
|
394
|
+
path.join(instanceDir, 'server/gadmin-cli.json'),
|
|
395
|
+
path.join(instanceDir, 'gadmin-cli.json'),
|
|
396
|
+
];
|
|
397
|
+
for (const p of candidates) {
|
|
398
|
+
if (fs.existsSync(p)) {
|
|
399
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
400
|
+
if (!raw.template) {
|
|
401
|
+
throw new Error('gadmin-cli.json missing "template" field');
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
template: raw.template,
|
|
405
|
+
templateExcludes: raw.templateExcludes,
|
|
406
|
+
templateUpdate: raw.templateUpdate,
|
|
407
|
+
raw,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
throw new Error('Cannot find "template" field in gadmin-cli.json. ' +
|
|
412
|
+
'Make sure you are in a gadmin2 project root directory.');
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* 整合:必须在 instance 根 / git 可用 / 配置无废弃字段 / 锁可获取 / 没有残留冲突 / flag 不矛盾。
|
|
416
|
+
*/
|
|
417
|
+
function runPreflight(opts) {
|
|
418
|
+
var _a, _b, _c;
|
|
419
|
+
const instanceDir = process.cwd();
|
|
420
|
+
const instanceMarker = path.join(instanceDir, 'server', 'gadmin-cli.json');
|
|
421
|
+
if (!fs.existsSync(instanceMarker)) {
|
|
422
|
+
throw new Error('Not at a gadmin2 instance project root.\n' +
|
|
423
|
+
` Expected to find: server/gadmin-cli.json\n` +
|
|
424
|
+
` Current directory: ${instanceDir}\n\n` +
|
|
425
|
+
'Run this command from the project root created by `gadmin2 n <name>`.');
|
|
426
|
+
}
|
|
427
|
+
assertGitAvailable();
|
|
428
|
+
assertNotBothOursAndTheirs(opts);
|
|
429
|
+
const cfg = loadGadminCliJson(instanceDir);
|
|
430
|
+
(0, config_validate_1.assertNoDeprecatedFields)(cfg.raw);
|
|
431
|
+
const userExcludes = [
|
|
432
|
+
...((_a = cfg.templateExcludes) !== null && _a !== void 0 ? _a : []),
|
|
433
|
+
...((_c = (_b = cfg.templateUpdate) === null || _b === void 0 ? void 0 : _b.excludes) !== null && _c !== void 0 ? _c : []),
|
|
434
|
+
];
|
|
435
|
+
const releaseLock = (0, base_store_1.acquireLock)(instanceDir);
|
|
436
|
+
// Residual conflict scan (instance-side files only).
|
|
437
|
+
const allExcludes = [...excludes_1.HARDCODED_EXCLUDES, ...excludes_1.DEFAULT_EXCLUDES, ...userExcludes];
|
|
438
|
+
const instanceFiles = (0, excludes_1.collectFiles)(instanceDir, allExcludes);
|
|
439
|
+
const residual = (0, residual_scan_1.scanResidualMarkers)(instanceDir, instanceFiles);
|
|
440
|
+
if (residual.length > 0) {
|
|
441
|
+
releaseLock();
|
|
442
|
+
const lines = residual.map((r) => ` ${r.file}:${r.line}`).join('\n');
|
|
443
|
+
throw new Error(`Refusing to update: ${residual.length} marker(s) from a previous update remain.\n\n` +
|
|
444
|
+
`${lines}\n\n` +
|
|
445
|
+
` Resolve all <<<<<<< / >>>>>>> markers, then re-run.\n` +
|
|
446
|
+
` Hint: git diff --check\n` +
|
|
447
|
+
` gadmin2 update status`);
|
|
448
|
+
}
|
|
449
|
+
return { instanceDir, templateName: cfg.template, userExcludes, releaseLock };
|
|
450
|
+
}
|
|
594
451
|
/* ------------------------------------------------------------------ *
|
|
595
|
-
*
|
|
452
|
+
* Apply phase
|
|
596
453
|
* ------------------------------------------------------------------ */
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
{
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
454
|
+
function readUtf8Safe(p) {
|
|
455
|
+
return fs.readFileSync(p, 'utf-8');
|
|
456
|
+
}
|
|
457
|
+
function ensureDir(file) {
|
|
458
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
459
|
+
}
|
|
460
|
+
function handleBinary(rel, baseRoot, oursRoot, theirsRoot, opts, conflicts) {
|
|
461
|
+
const basePath = path.join(baseRoot, rel);
|
|
462
|
+
const oursPath = path.join(oursRoot, rel);
|
|
463
|
+
const theirsPath = path.join(theirsRoot, rel);
|
|
464
|
+
const baseBuf = fs.existsSync(basePath) ? fs.readFileSync(basePath) : null;
|
|
465
|
+
const oursBuf = fs.existsSync(oursPath) ? fs.readFileSync(oursPath) : null;
|
|
466
|
+
const theirsBuf = fs.existsSync(theirsPath) ? fs.readFileSync(theirsPath) : null;
|
|
467
|
+
if (oursBuf && theirsBuf && oursBuf.equals(theirsBuf))
|
|
468
|
+
return;
|
|
469
|
+
if (baseBuf && oursBuf && baseBuf.equals(oursBuf) && theirsBuf) {
|
|
470
|
+
ensureDir(oursPath);
|
|
471
|
+
fs.copyFileSync(theirsPath, oursPath);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (opts.theirs && theirsBuf) {
|
|
475
|
+
ensureDir(oursPath);
|
|
476
|
+
fs.copyFileSync(theirsPath, oursPath);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (opts.ours)
|
|
480
|
+
return; // keep OURS as-is
|
|
481
|
+
conflicts.push({
|
|
482
|
+
path: rel,
|
|
483
|
+
kind: 'binary',
|
|
484
|
+
detail: `binary differs three-ways. To accept template: cp "${theirsPath}" "${oursPath}"`,
|
|
485
|
+
});
|
|
486
|
+
console.warn(chalk.yellow(` ⚠️ binary conflict (cannot 3-way merge): ${rel}`));
|
|
487
|
+
console.warn(chalk.gray(` template: ${theirsPath}`));
|
|
488
|
+
console.warn(chalk.gray(` base: ${basePath}`));
|
|
489
|
+
}
|
|
490
|
+
function applyPlan(plan, baseRoot, oursRoot, theirsRoot, opts) {
|
|
491
|
+
const conflicts = [];
|
|
492
|
+
for (const rel of plan.add) {
|
|
493
|
+
const dst = path.join(oursRoot, rel);
|
|
494
|
+
ensureDir(dst);
|
|
495
|
+
fs.copyFileSync(path.join(theirsRoot, rel), dst);
|
|
496
|
+
}
|
|
497
|
+
for (const rel of plan.delete) {
|
|
498
|
+
const dst = path.join(oursRoot, rel);
|
|
499
|
+
if (fs.existsSync(dst))
|
|
500
|
+
fs.rmSync(dst);
|
|
501
|
+
}
|
|
502
|
+
for (const rel of plan.update) {
|
|
503
|
+
const basePath = path.join(baseRoot, rel);
|
|
504
|
+
const oursPath = path.join(oursRoot, rel);
|
|
505
|
+
const theirsPath = path.join(theirsRoot, rel);
|
|
506
|
+
if ((0, binary_detect_1.isAnyBinary)(basePath, oursPath, theirsPath)) {
|
|
507
|
+
handleBinary(rel, baseRoot, oursRoot, theirsRoot, opts, conflicts);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const result = (0, merge3way_1.merge3Way)(readUtf8Safe(basePath), readUtf8Safe(oursPath), readUtf8Safe(theirsPath));
|
|
511
|
+
if (result.hasConflicts && opts.ours) {
|
|
512
|
+
// keep OURS as-is — no write
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (result.hasConflicts && opts.theirs) {
|
|
516
|
+
ensureDir(oursPath);
|
|
517
|
+
fs.copyFileSync(theirsPath, oursPath);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
ensureDir(oursPath);
|
|
521
|
+
fs.writeFileSync(oursPath, result.merged, 'utf-8');
|
|
522
|
+
if (result.hasConflicts)
|
|
523
|
+
conflicts.push({ path: rel, kind: 'content' });
|
|
524
|
+
}
|
|
525
|
+
// add/add
|
|
526
|
+
for (const rel of plan.addAddConflict) {
|
|
527
|
+
const oursPath = path.join(oursRoot, rel);
|
|
528
|
+
const theirsPath = path.join(theirsRoot, rel);
|
|
529
|
+
if (opts.theirs) {
|
|
530
|
+
ensureDir(oursPath);
|
|
531
|
+
fs.copyFileSync(theirsPath, oursPath);
|
|
532
|
+
}
|
|
533
|
+
else if (!opts.ours) {
|
|
534
|
+
conflicts.push({
|
|
535
|
+
path: rel,
|
|
536
|
+
kind: 'add/add',
|
|
537
|
+
detail: 'both added different content; instance kept',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// modify/delete (template deleted, instance modified)
|
|
542
|
+
for (const rel of plan.modifyDeleteConflict) {
|
|
543
|
+
const oursPath = path.join(oursRoot, rel);
|
|
544
|
+
if (opts.theirs) {
|
|
545
|
+
if (fs.existsSync(oursPath))
|
|
546
|
+
fs.rmSync(oursPath);
|
|
547
|
+
}
|
|
548
|
+
else if (!opts.ours) {
|
|
549
|
+
conflicts.push({
|
|
550
|
+
path: rel,
|
|
551
|
+
kind: 'modify/delete',
|
|
552
|
+
detail: 'template deleted this file; instance modified — file kept',
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// delete/modify (instance deleted, template modified)
|
|
557
|
+
for (const rel of plan.deleteModifyConflict) {
|
|
558
|
+
const oursPath = path.join(oursRoot, rel);
|
|
559
|
+
const theirsPath = path.join(theirsRoot, rel);
|
|
560
|
+
if (opts.theirs) {
|
|
561
|
+
ensureDir(oursPath);
|
|
562
|
+
fs.copyFileSync(theirsPath, oursPath);
|
|
563
|
+
}
|
|
564
|
+
else if (!opts.ours) {
|
|
565
|
+
conflicts.push({
|
|
566
|
+
path: rel,
|
|
567
|
+
kind: 'delete/modify',
|
|
568
|
+
detail: 'instance deleted this file; template modified — file NOT restored',
|
|
569
|
+
});
|
|
620
570
|
}
|
|
621
571
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
572
|
+
return conflicts;
|
|
573
|
+
}
|
|
574
|
+
/* ------------------------------------------------------------------ *
|
|
575
|
+
* Print summary
|
|
576
|
+
* ------------------------------------------------------------------ */
|
|
577
|
+
function printPlanCounts(plan) {
|
|
578
|
+
console.info(` ${plan.update.length} files cleanly merged or 3-way merged\n` +
|
|
579
|
+
` ${plan.add.length} files added (template-only)\n` +
|
|
580
|
+
` ${plan.delete.length} files deleted (template removed)`);
|
|
581
|
+
if (plan.keep.length > 0) {
|
|
582
|
+
console.info(chalk.gray(` ${plan.keep.length} files kept (instance-only or identical)`));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function printSummary(plan, conflicts, templateName, schematicsVersion) {
|
|
586
|
+
console.info(chalk.bold('\n═══════════════════════════════════════════════════'));
|
|
587
|
+
console.info(chalk.bold(' Template Update Summary'));
|
|
588
|
+
console.info(chalk.bold('═══════════════════════════════════════════════════\n'));
|
|
589
|
+
printPlanCounts(plan);
|
|
590
|
+
console.info();
|
|
591
|
+
if (conflicts.length === 0) {
|
|
592
|
+
console.info(chalk.green('✅ Template update complete — no conflicts.'));
|
|
593
|
+
console.info(` Base advanced to: ${templateName} @ schematics ${schematicsVersion}\n`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
console.info(chalk.yellow(`⚠️ ${conflicts.length} conflict(s) need your attention:\n`));
|
|
597
|
+
const padKind = (k) => k.padEnd(16);
|
|
598
|
+
for (const c of conflicts) {
|
|
599
|
+
const kindLabel = `[${c.kind}]`;
|
|
600
|
+
console.info(` ${chalk.yellow(padKind(kindLabel))} ${c.path}` +
|
|
601
|
+
(c.detail ? chalk.gray(` (${c.detail})`) : ''));
|
|
602
|
+
}
|
|
603
|
+
console.info();
|
|
604
|
+
console.info(chalk.gray(' Recommended workflow:'));
|
|
605
|
+
console.info(chalk.gray(' $ git status # see all modified files'));
|
|
606
|
+
console.info(chalk.gray(' $ git diff --check # locate residual <<<<<<< / >>>>>>> markers'));
|
|
607
|
+
console.info(chalk.gray(' $ <editor> <conflicting-file>'));
|
|
608
|
+
console.info(chalk.gray(' ...'));
|
|
609
|
+
console.info(chalk.gray(' $ gadmin2 update status # verify conflicts resolved'));
|
|
610
|
+
console.info(chalk.gray(' $ git add . && git commit'));
|
|
611
|
+
console.info();
|
|
612
|
+
console.info(` Base advanced to: ${templateName} @ schematics ${schematicsVersion}\n`);
|
|
613
|
+
}
|
|
614
|
+
/* ------------------------------------------------------------------ *
|
|
615
|
+
* Per-file 3-way diff display (used by `gadmin2 update diff <file>`)
|
|
616
|
+
* ------------------------------------------------------------------ */
|
|
617
|
+
function printPerFileTriDiff(rel, baseRoot, oursRoot, theirsRoot, baseExists) {
|
|
618
|
+
const basePath = path.join(baseRoot, rel);
|
|
619
|
+
const oursPath = path.join(oursRoot, rel);
|
|
620
|
+
const theirsPath = path.join(theirsRoot, rel);
|
|
621
|
+
const has = (p) => fs.existsSync(p);
|
|
622
|
+
console.info(`File: ${chalk.bold(rel)}\n`);
|
|
623
|
+
if (baseExists && has(basePath) && has(theirsPath)) {
|
|
624
|
+
console.info(chalk.gray(' base → theirs (template increment since last update)'));
|
|
625
|
+
console.info(chalk.gray(' ──────────────────'));
|
|
626
|
+
console.info(unifiedDiffSimple(fs.readFileSync(basePath, 'utf-8'), fs.readFileSync(theirsPath, 'utf-8')));
|
|
627
|
+
console.info();
|
|
628
|
+
}
|
|
629
|
+
if (baseExists && has(basePath) && has(oursPath)) {
|
|
630
|
+
console.info(chalk.gray(' ours → base (instance changes since last update)'));
|
|
631
|
+
console.info(chalk.gray(' ──────────────────'));
|
|
632
|
+
console.info(unifiedDiffSimple(fs.readFileSync(basePath, 'utf-8'), fs.readFileSync(oursPath, 'utf-8')));
|
|
633
|
+
console.info();
|
|
634
|
+
}
|
|
635
|
+
else if (!baseExists) {
|
|
636
|
+
console.info(chalk.gray(' ours vs theirs (no base — falls back to 2-way)'));
|
|
637
|
+
console.info(chalk.gray(' ──────────────────'));
|
|
638
|
+
if (has(oursPath) && has(theirsPath)) {
|
|
639
|
+
console.info(unifiedDiffSimple(fs.readFileSync(oursPath, 'utf-8'), fs.readFileSync(theirsPath, 'utf-8')));
|
|
625
640
|
}
|
|
641
|
+
console.info();
|
|
626
642
|
}
|
|
627
|
-
|
|
643
|
+
}
|
|
644
|
+
/** Minimal unified diff output suitable for terminal. */
|
|
645
|
+
function unifiedDiffSimple(a, b) {
|
|
646
|
+
const A = a.split('\n');
|
|
647
|
+
const B = b.split('\n');
|
|
648
|
+
const out = [];
|
|
649
|
+
const max = Math.max(A.length, B.length);
|
|
650
|
+
for (let i = 0; i < max; i++) {
|
|
651
|
+
const x = i < A.length ? A[i] : undefined;
|
|
652
|
+
const y = i < B.length ? B[i] : undefined;
|
|
653
|
+
if (x === y)
|
|
654
|
+
continue;
|
|
655
|
+
if (x !== undefined)
|
|
656
|
+
out.push(chalk.red(`-${x}`));
|
|
657
|
+
if (y !== undefined)
|
|
658
|
+
out.push(chalk.green(`+${y}`));
|
|
659
|
+
}
|
|
660
|
+
return out.length === 0 ? chalk.gray(' (no textual diff)') : out.join('\n');
|
|
628
661
|
}
|
|
629
662
|
/* ------------------------------------------------------------------ *
|
|
630
663
|
* Action
|
|
@@ -634,8 +667,8 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
634
667
|
var _a, _b;
|
|
635
668
|
return __awaiter(this, void 0, void 0, function* () {
|
|
636
669
|
const subcommand = (_a = inputs.find((i) => i.name === 'subcommand')) === null || _a === void 0 ? void 0 : _a.value;
|
|
637
|
-
if (subcommand && subcommand !== 'update' && subcommand !== 'diff') {
|
|
638
|
-
console.error(chalk.red(`Unknown subcommand "${subcommand}". Expected: (none) | diff`));
|
|
670
|
+
if (subcommand && subcommand !== 'update' && subcommand !== 'diff' && subcommand !== 'status') {
|
|
671
|
+
console.error(chalk.red(`Unknown subcommand "${subcommand}". Expected: (none) | diff | status`));
|
|
639
672
|
process.exit(1);
|
|
640
673
|
}
|
|
641
674
|
// Guard: refuse to run anywhere except a gadmin2 instance project root.
|
|
@@ -643,7 +676,7 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
643
676
|
// We check this up-front so commands like `gadmin2 update --no-template`
|
|
644
677
|
// can't silently sync .claude into an unrelated directory.
|
|
645
678
|
const instanceMarker = path.join(process.cwd(), 'server', 'gadmin-cli.json');
|
|
646
|
-
if (!fs.existsSync(instanceMarker)) {
|
|
679
|
+
if (subcommand !== 'status' && !fs.existsSync(instanceMarker)) {
|
|
647
680
|
console.error(chalk.red('\nNot at a gadmin2 instance project root.\n' +
|
|
648
681
|
` Expected to find: server/gadmin-cli.json\n` +
|
|
649
682
|
` Current directory: ${process.cwd()}\n\n` +
|
|
@@ -665,6 +698,10 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
665
698
|
// never let the version check break update
|
|
666
699
|
}
|
|
667
700
|
}
|
|
701
|
+
if (subcommand === 'status') {
|
|
702
|
+
yield this.handleStatus();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
668
705
|
if (subcommand === 'diff') {
|
|
669
706
|
yield this.handleDiff(options);
|
|
670
707
|
return;
|
|
@@ -673,88 +710,169 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
673
710
|
});
|
|
674
711
|
}
|
|
675
712
|
handleDiff(options) {
|
|
676
|
-
var _a;
|
|
713
|
+
var _a, _b, _c, _d, _e;
|
|
677
714
|
return __awaiter(this, void 0, void 0, function* () {
|
|
678
|
-
const
|
|
679
|
-
let templateDir;
|
|
680
|
-
let excludes;
|
|
681
|
-
try {
|
|
682
|
-
const config = loadTemplateConfig();
|
|
683
|
-
templateDir = resolveTemplatePath(config.templateName);
|
|
684
|
-
excludes = config.excludes;
|
|
685
|
-
}
|
|
686
|
-
catch (err) {
|
|
687
|
-
console.error(chalk.red(err.message));
|
|
688
|
-
process.exit(1);
|
|
689
|
-
}
|
|
715
|
+
const filterFile = (_a = options.find((o) => o.name === 'subcommand-arg')) === null || _a === void 0 ? void 0 : _a.value;
|
|
690
716
|
const instanceDir = process.cwd();
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
717
|
+
const cfg = loadGadminCliJson(instanceDir);
|
|
718
|
+
const userExcludes = [
|
|
719
|
+
...((_b = cfg.templateExcludes) !== null && _b !== void 0 ? _b : []),
|
|
720
|
+
...((_d = (_c = cfg.templateUpdate) === null || _c === void 0 ? void 0 : _c.excludes) !== null && _d !== void 0 ? _d : []),
|
|
721
|
+
];
|
|
722
|
+
const theirsRoot = resolveTemplatePath(cfg.template);
|
|
723
|
+
const metaExists = fs.existsSync((0, base_store_1.metaPath)(instanceDir));
|
|
724
|
+
let baseHandle = null;
|
|
725
|
+
let baseRoot;
|
|
726
|
+
let meta = null;
|
|
727
|
+
if (metaExists) {
|
|
728
|
+
try {
|
|
729
|
+
meta = (0, base_store_1.readMeta)(instanceDir);
|
|
730
|
+
baseHandle = (0, materialize_1.materializeBase)(meta.schematicsVersion, cfg.template);
|
|
731
|
+
baseRoot = baseHandle.dir;
|
|
707
732
|
}
|
|
708
|
-
|
|
709
|
-
|
|
733
|
+
catch (err) {
|
|
734
|
+
console.error(chalk.red(formatMaterializeError((_e = meta === null || meta === void 0 ? void 0 : meta.schematicsVersion) !== null && _e !== void 0 ? _e : '?', err)));
|
|
735
|
+
process.exit(1);
|
|
710
736
|
}
|
|
711
737
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
738
|
+
else {
|
|
739
|
+
// No bootstrap yet — fall back to "compare instance vs theirs" by pointing
|
|
740
|
+
// base at a non-existent path so classifyAll treats every file as new.
|
|
741
|
+
baseRoot = path.join(instanceDir, '.gadmin/__nonexistent__');
|
|
716
742
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
console.info(` ${f}`);
|
|
724
|
-
}
|
|
743
|
+
try {
|
|
744
|
+
console.info(`\n${chalk.gray('Template:')} ${theirsRoot}`);
|
|
745
|
+
console.info(`${chalk.gray('Instance:')} ${instanceDir}`);
|
|
746
|
+
console.info(`${chalk.gray('Base: ')} ${metaExists
|
|
747
|
+
? `(materialized from schematics ${meta.schematicsVersion})`
|
|
748
|
+
: '(not yet established — run `gadmin2 update` once to bootstrap)'}`);
|
|
725
749
|
console.info();
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
console.info(` ${f}`);
|
|
750
|
+
const plan = (0, classify_1.classifyAll)(baseRoot, instanceDir, theirsRoot, userExcludes);
|
|
751
|
+
if (filterFile) {
|
|
752
|
+
printPerFileTriDiff(filterFile, baseRoot, instanceDir, theirsRoot, metaExists);
|
|
753
|
+
return;
|
|
731
754
|
}
|
|
732
|
-
console.info();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
console.info(` ${f}`);
|
|
755
|
+
console.info(chalk.bold('═══════════════════════════════════════════════════'));
|
|
756
|
+
console.info(chalk.bold(' Template Diff Summary (3-way)'));
|
|
757
|
+
console.info(chalk.bold('═══════════════════════════════════════════════════\n'));
|
|
758
|
+
if (plan.add.length > 0) {
|
|
759
|
+
console.info(chalk.green(`➕ To add (${plan.add.length} files)`));
|
|
760
|
+
plan.add.forEach((f) => console.info(` ${f}`));
|
|
761
|
+
console.info();
|
|
762
|
+
}
|
|
763
|
+
if (plan.delete.length > 0) {
|
|
764
|
+
console.info(chalk.red(`🗑 To delete (${plan.delete.length} files)`));
|
|
765
|
+
plan.delete.forEach((f) => console.info(` ${f}`));
|
|
766
|
+
console.info();
|
|
767
|
+
}
|
|
768
|
+
if (plan.update.length > 0) {
|
|
769
|
+
console.info(chalk.yellow(`📝 To update via 3-way merge (${plan.update.length} files)`));
|
|
770
|
+
plan.update.forEach((f) => console.info(` ${f}`));
|
|
771
|
+
console.info();
|
|
772
|
+
}
|
|
773
|
+
const conflicts = [
|
|
774
|
+
...plan.addAddConflict.map((p) => ['add/add', p]),
|
|
775
|
+
...plan.modifyDeleteConflict.map((p) => ['modify/delete', p]),
|
|
776
|
+
...plan.deleteModifyConflict.map((p) => ['delete/modify', p]),
|
|
777
|
+
];
|
|
778
|
+
if (conflicts.length > 0) {
|
|
779
|
+
console.info(chalk.magenta(`⚠️ Structural conflicts (${conflicts.length} files)`));
|
|
780
|
+
conflicts.forEach(([k, p]) => console.info(` [${k}] ${p}`));
|
|
781
|
+
console.info();
|
|
782
|
+
}
|
|
783
|
+
if (plan.add.length + plan.delete.length + plan.update.length + conflicts.length === 0) {
|
|
784
|
+
console.info(chalk.green('✅ No differences found. Instance matches template.\n'));
|
|
738
785
|
}
|
|
739
|
-
console.info();
|
|
740
786
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
console.info(chalk.green('✅ No differences found. Instance matches template.\n'));
|
|
745
|
-
return;
|
|
787
|
+
finally {
|
|
788
|
+
if (baseHandle)
|
|
789
|
+
baseHandle.cleanup();
|
|
746
790
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
handleStatus() {
|
|
794
|
+
var _a, _b, _c, _d;
|
|
795
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
796
|
+
const instanceDir = process.cwd();
|
|
797
|
+
const instanceMarker = path.join(instanceDir, 'server', 'gadmin-cli.json');
|
|
798
|
+
if (!fs.existsSync(instanceMarker)) {
|
|
799
|
+
console.error(chalk.red('Not at a gadmin2 instance project root.\n'));
|
|
800
|
+
process.exit(2);
|
|
801
|
+
}
|
|
802
|
+
const cfg = loadGadminCliJson(instanceDir);
|
|
803
|
+
const userExcludes = [
|
|
804
|
+
...((_a = cfg.templateExcludes) !== null && _a !== void 0 ? _a : []),
|
|
805
|
+
...((_c = (_b = cfg.templateUpdate) === null || _b === void 0 ? void 0 : _b.excludes) !== null && _c !== void 0 ? _c : []),
|
|
806
|
+
];
|
|
807
|
+
if (!fs.existsSync((0, base_store_1.metaPath)(instanceDir))) {
|
|
808
|
+
console.error(chalk.red('Base meta missing — run `gadmin2 update` to bootstrap.\n'));
|
|
809
|
+
process.exit(2);
|
|
810
|
+
}
|
|
811
|
+
let meta;
|
|
812
|
+
try {
|
|
813
|
+
meta = (0, base_store_1.readMeta)(instanceDir);
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
console.error(chalk.red(err.message + '\n'));
|
|
817
|
+
process.exit(2);
|
|
818
|
+
}
|
|
819
|
+
console.info(`Template: ${cfg.template} @ schematics ${meta.schematicsVersion}`);
|
|
820
|
+
const allExcludes = [...excludes_1.HARDCODED_EXCLUDES, ...excludes_1.DEFAULT_EXCLUDES, ...userExcludes];
|
|
821
|
+
const instanceFiles = (0, excludes_1.collectFiles)(instanceDir, allExcludes);
|
|
822
|
+
const residual = (0, residual_scan_1.scanResidualMarkers)(instanceDir, instanceFiles);
|
|
823
|
+
let theirsRoot;
|
|
824
|
+
try {
|
|
825
|
+
theirsRoot = resolveTemplatePath(cfg.template);
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
console.error(chalk.red(err.message + '\n'));
|
|
829
|
+
process.exit(2);
|
|
830
|
+
}
|
|
831
|
+
let baseHandle = null;
|
|
832
|
+
try {
|
|
833
|
+
try {
|
|
834
|
+
baseHandle = (0, materialize_1.materializeBase)(meta.schematicsVersion, cfg.template);
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
console.error(chalk.red(formatMaterializeError(meta.schematicsVersion, err)));
|
|
838
|
+
process.exit(2);
|
|
839
|
+
}
|
|
840
|
+
const plan = (0, classify_1.classifyAll)(baseHandle.dir, instanceDir, theirsRoot, userExcludes);
|
|
841
|
+
const structural = [
|
|
842
|
+
...plan.addAddConflict.map((p) => ({ kind: 'add/add', path: p })),
|
|
843
|
+
...plan.modifyDeleteConflict.map((p) => ({ kind: 'modify/delete', path: p })),
|
|
844
|
+
...plan.deleteModifyConflict.map((p) => ({ kind: 'delete/modify', path: p })),
|
|
845
|
+
];
|
|
846
|
+
const totalConflicts = residual.length + structural.length;
|
|
847
|
+
if (totalConflicts === 0) {
|
|
848
|
+
console.info('Instance state: ' + chalk.green('✅ clean (no unresolved conflicts)\n'));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
console.info(`Instance state: ${chalk.yellow(`${totalConflicts} unresolved conflict(s)`)}\n`);
|
|
852
|
+
if (residual.length > 0) {
|
|
853
|
+
console.info(' Content conflicts (with markers in source):');
|
|
854
|
+
const byFile = new Map();
|
|
855
|
+
for (const r of residual)
|
|
856
|
+
byFile.set(r.file, ((_d = byFile.get(r.file)) !== null && _d !== void 0 ? _d : 0) + 1);
|
|
857
|
+
for (const [f, n] of byFile) {
|
|
858
|
+
console.info(` ${f.padEnd(48)} (${n} marker${n === 1 ? '' : 's'})`);
|
|
859
|
+
}
|
|
860
|
+
console.info();
|
|
861
|
+
}
|
|
862
|
+
if (structural.length > 0) {
|
|
863
|
+
console.info(' Structural conflicts:');
|
|
864
|
+
for (const s of structural) {
|
|
865
|
+
console.info(` [${s.kind.padEnd(13)}] ${s.path}`);
|
|
866
|
+
}
|
|
756
867
|
console.info();
|
|
757
868
|
}
|
|
869
|
+
console.info(' Resolve them, then run `gadmin2 update status` again to verify.');
|
|
870
|
+
process.exitCode = 1;
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
if (baseHandle)
|
|
875
|
+
baseHandle.cleanup();
|
|
758
876
|
}
|
|
759
877
|
});
|
|
760
878
|
}
|
|
@@ -763,324 +881,140 @@ class UpdateAction extends abstract_action_1.AbstractAction {
|
|
|
763
881
|
return __awaiter(this, void 0, void 0, function* () {
|
|
764
882
|
const dryRun = (_a = options.find((o) => o.name === 'dry-run')) === null || _a === void 0 ? void 0 : _a.value;
|
|
765
883
|
const autoYes = (_b = options.find((o) => o.name === 'yes')) === null || _b === void 0 ? void 0 : _b.value;
|
|
766
|
-
const
|
|
884
|
+
const ours = (_c = options.find((o) => o.name === 'ours')) === null || _c === void 0 ? void 0 : _c.value;
|
|
885
|
+
const theirs = (_d = options.find((o) => o.name === 'theirs')) === null || _d === void 0 ? void 0 : _d.value;
|
|
767
886
|
const templateEnabled = ((_e = options.find((o) => o.name === 'template')) === null || _e === void 0 ? void 0 : _e.value) !== false;
|
|
768
|
-
const claudeEnabled = ((_f = options.find((o) => o.name === 'coding-context')) === null || _f === void 0 ? void 0 : _f.value) !==
|
|
769
|
-
false;
|
|
887
|
+
const claudeEnabled = ((_f = options.find((o) => o.name === 'coding-context')) === null || _f === void 0 ? void 0 : _f.value) !== false;
|
|
770
888
|
if (!templateEnabled && !claudeEnabled) {
|
|
771
889
|
console.error(chalk.red('\nNothing to do: both --no-template and --no-coding-context were specified.\n'));
|
|
772
890
|
process.exit(1);
|
|
773
891
|
}
|
|
774
|
-
const
|
|
775
|
-
// Hoisted so stage 3 (auto-install) can see snapshots taken in stage 1.
|
|
776
|
-
// Remains null when --no-template, which is also the signal to skip stage 3.
|
|
892
|
+
const mergeOpts = { ours, theirs };
|
|
777
893
|
let pkgSnapshots = null;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
excludes = config.excludes;
|
|
785
|
-
}
|
|
786
|
-
catch (err) {
|
|
787
|
-
console.error(chalk.red(err.message));
|
|
788
|
-
process.exit(1);
|
|
789
|
-
}
|
|
790
|
-
console.info(`\n${chalk.gray('Template:')} ${templateDir}`);
|
|
791
|
-
console.info(`${chalk.gray('Instance:')} ${instanceDir}\n`);
|
|
792
|
-
// Build smart-merger registry (or null if disabled)
|
|
793
|
-
const userTemplateUpdate = smartMergeEnabled
|
|
794
|
-
? (0, template_merge_1.loadTemplateUpdateConfig)(instanceDir)
|
|
795
|
-
: null;
|
|
796
|
-
const registry = smartMergeEnabled
|
|
797
|
-
? buildMergerRegistry(userTemplateUpdate)
|
|
798
|
-
: null;
|
|
799
|
-
const userPolicies = userTemplateUpdate === null || userTemplateUpdate === void 0 ? void 0 : userTemplateUpdate.policies;
|
|
800
|
-
// Snapshot package.json files BEFORE any writes. We watch:
|
|
801
|
-
// - <instanceDir>/package.json (root, if present — workspace / husky / etc.)
|
|
802
|
-
// - <instanceDir>/web/package.json
|
|
803
|
-
// - <instanceDir>/server/package.json
|
|
804
|
-
pkgSnapshots = [
|
|
805
|
-
snapshotPackageJson(instanceDir, '(root)'),
|
|
806
|
-
snapshotPackageJson(path.join(instanceDir, 'web'), 'web'),
|
|
807
|
-
snapshotPackageJson(path.join(instanceDir, 'server'), 'server'),
|
|
808
|
-
];
|
|
809
|
-
const templateFiles = collectFiles(templateDir, excludes);
|
|
810
|
-
const instanceFiles = collectFiles(instanceDir, excludes);
|
|
811
|
-
const instanceSet = new Set(instanceFiles);
|
|
812
|
-
const toUpdate = [];
|
|
813
|
-
const toAdd = [];
|
|
814
|
-
for (const file of templateFiles) {
|
|
815
|
-
if (instanceSet.has(file)) {
|
|
816
|
-
const tContent = fs.readFileSync(path.join(templateDir, file), 'utf-8');
|
|
817
|
-
const iContent = fs.readFileSync(path.join(instanceDir, file), 'utf-8');
|
|
818
|
-
if (tContent !== iContent) {
|
|
819
|
-
toUpdate.push(file);
|
|
820
|
-
}
|
|
894
|
+
let preflight = null;
|
|
895
|
+
let baseHandle = null;
|
|
896
|
+
try {
|
|
897
|
+
if (templateEnabled) {
|
|
898
|
+
try {
|
|
899
|
+
preflight = runPreflight(mergeOpts);
|
|
821
900
|
}
|
|
822
|
-
|
|
823
|
-
|
|
901
|
+
catch (err) {
|
|
902
|
+
console.error(chalk.red(`\n❌ ${err.message}\n`));
|
|
903
|
+
process.exit(1);
|
|
824
904
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
console.info(chalk.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
905
|
+
const { instanceDir, templateName, userExcludes } = preflight;
|
|
906
|
+
const theirsRoot = resolveTemplatePath(templateName);
|
|
907
|
+
console.info(`\n${chalk.gray('Template:')} ${theirsRoot}`);
|
|
908
|
+
console.info(`${chalk.gray('Instance:')} ${instanceDir}\n`);
|
|
909
|
+
// Snapshot package.json before any writes (for stage 3 install)
|
|
910
|
+
pkgSnapshots = [
|
|
911
|
+
snapshotPackageJson(instanceDir, '(root)'),
|
|
912
|
+
snapshotPackageJson(path.join(instanceDir, 'web'), 'web'),
|
|
913
|
+
snapshotPackageJson(path.join(instanceDir, 'server'), 'server'),
|
|
914
|
+
];
|
|
915
|
+
const schematicsPkg = require(require.resolve('@gadmin2n/schematics/package.json', { paths: [instanceDir, path.join(instanceDir, 'server')] }));
|
|
916
|
+
const schematicsVersion = schematicsPkg.version || '0.0.0';
|
|
917
|
+
// First-time bootstrap: meta absent → write meta + README, exit.
|
|
918
|
+
if (!fs.existsSync((0, base_store_1.metaPath)(instanceDir))) {
|
|
919
|
+
if (dryRun) {
|
|
920
|
+
console.info(chalk.gray('(dry-run: would write .gadmin/.meta.json pinned to current schematics version, then exit)\n'));
|
|
837
921
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
922
|
+
else {
|
|
923
|
+
(0, base_store_1.writeMeta)(instanceDir, {
|
|
924
|
+
schematicsVersion,
|
|
925
|
+
schemaVersion: types_1.CURRENT_META_SCHEMA_VERSION,
|
|
926
|
+
});
|
|
927
|
+
(0, base_store_1.writeReadme)(instanceDir);
|
|
928
|
+
console.info(chalk.green('✅ Baseline established at .gadmin/.meta.json'));
|
|
929
|
+
console.info(chalk.gray(` Pinned to schematics ${schematicsVersion}.`));
|
|
930
|
+
console.info(chalk.gray(' The next `gadmin2 update` will perform a real 3-way merge.'));
|
|
844
931
|
}
|
|
845
|
-
|
|
932
|
+
// Skip the rest of stage 1 — nothing to merge yet.
|
|
846
933
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
default: false,
|
|
858
|
-
},
|
|
859
|
-
]);
|
|
860
|
-
if (!confirm) {
|
|
861
|
-
console.info('Cancelled.\n');
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
934
|
+
else {
|
|
935
|
+
// Validate meta consistency.
|
|
936
|
+
let meta;
|
|
937
|
+
try {
|
|
938
|
+
meta = (0, base_store_1.validateMetaConsistency)(instanceDir);
|
|
939
|
+
}
|
|
940
|
+
catch (err) {
|
|
941
|
+
console.error(chalk.red(`\n❌ ${err.message}\n`));
|
|
942
|
+
process.exit(1);
|
|
943
|
+
return; // unreachable; satisfies TS that meta is definitely assigned
|
|
864
944
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
let conflictCount = 0;
|
|
869
|
-
let smartCount = 0;
|
|
870
|
-
let applyAll = null;
|
|
871
|
-
for (let i = 0; i < toUpdate.length; i++) {
|
|
872
|
-
const file = toUpdate[i];
|
|
873
|
-
const tContent = fs.readFileSync(path.join(templateDir, file), 'utf-8');
|
|
874
|
-
const iContent = fs.readFileSync(path.join(instanceDir, file), 'utf-8');
|
|
875
|
-
// 1. Try smart merger if registered for this file
|
|
876
|
-
const smartMerger = registry
|
|
877
|
-
? registry.resolve(file)
|
|
878
|
-
: null;
|
|
879
|
-
let smartResult = null;
|
|
880
|
-
if (smartMerger) {
|
|
881
|
-
try {
|
|
882
|
-
smartResult = yield smartMerger.merge({
|
|
883
|
-
filePath: file,
|
|
884
|
-
instancePath: path.join(instanceDir, file),
|
|
885
|
-
templatePath: path.join(templateDir, file),
|
|
886
|
-
instanceContent: iContent,
|
|
887
|
-
templateContent: tContent,
|
|
888
|
-
policies: userPolicies,
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
catch (err) {
|
|
892
|
-
smartResult = {
|
|
893
|
-
ok: false,
|
|
894
|
-
reason: (err === null || err === void 0 ? void 0 : err.message) || String(err),
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
let strategy = applyAll;
|
|
899
|
-
if (!strategy) {
|
|
900
|
-
if (autoYes) {
|
|
901
|
-
// autoYes: prefer smart merge when available, else conflict markers
|
|
902
|
-
strategy =
|
|
903
|
-
smartResult && smartResult.ok ? 'smart' : 'conflict';
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
const diff = createUnifiedDiff(file, iContent, tContent);
|
|
907
|
-
console.info(`\n${chalk.bold(`[${i + 1}/${toUpdate.length}]`)} ${chalk.yellow(file)}`);
|
|
908
|
-
console.info(diff);
|
|
909
|
-
console.info();
|
|
910
|
-
const choices = [];
|
|
911
|
-
if (smartResult && smartResult.ok) {
|
|
912
|
-
const summary = smartResult.notes.slice(0, 3).join('; ');
|
|
913
|
-
const more = smartResult.notes.length > 3 ? '...' : '';
|
|
914
|
-
choices.push({
|
|
915
|
-
name: `Smart merge — ${smartMerger.name} (recommended): ${summary}${more}`,
|
|
916
|
-
value: 'smart',
|
|
917
|
-
short: 'smart',
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
else if (smartMerger) {
|
|
921
|
-
const reason = smartResult && !smartResult.ok
|
|
922
|
-
? smartResult.reason
|
|
923
|
-
: 'no result';
|
|
924
|
-
choices.push({
|
|
925
|
-
name: `Smart merge — ${smartMerger.name} (UNAVAILABLE: ${reason})`,
|
|
926
|
-
value: 'smart-unavailable',
|
|
927
|
-
short: 'smart-unavailable',
|
|
928
|
-
disabled: reason,
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
choices.push({
|
|
932
|
-
name: 'Skip — keep instance as-is',
|
|
933
|
-
value: 'skip',
|
|
934
|
-
short: 'skip',
|
|
935
|
-
}, {
|
|
936
|
-
name: 'Overwrite — use template version',
|
|
937
|
-
value: 'overwrite',
|
|
938
|
-
short: 'overwrite',
|
|
939
|
-
}, {
|
|
940
|
-
name: 'Auto merge — attempt git merge',
|
|
941
|
-
value: 'merge',
|
|
942
|
-
short: 'merge',
|
|
943
|
-
}, {
|
|
944
|
-
name: 'Conflict markers — write both versions for manual resolve',
|
|
945
|
-
value: 'conflict',
|
|
946
|
-
short: 'conflict',
|
|
947
|
-
}, new inquirer.Separator());
|
|
948
|
-
if (smartResult && smartResult.ok) {
|
|
949
|
-
choices.push({
|
|
950
|
-
name: 'Smart-merge ALL remaining (where supported)',
|
|
951
|
-
value: 'smart-all',
|
|
952
|
-
short: 'smart all',
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
choices.push({
|
|
956
|
-
name: 'Skip ALL remaining',
|
|
957
|
-
value: 'skip-all',
|
|
958
|
-
short: 'skip all',
|
|
959
|
-
}, {
|
|
960
|
-
name: 'Overwrite ALL remaining',
|
|
961
|
-
value: 'overwrite-all',
|
|
962
|
-
short: 'overwrite all',
|
|
963
|
-
}, {
|
|
964
|
-
name: 'Conflict markers ALL remaining',
|
|
965
|
-
value: 'conflict-all',
|
|
966
|
-
short: 'conflict all',
|
|
967
|
-
});
|
|
968
|
-
const { action } = yield inquirer.prompt([
|
|
969
|
-
{
|
|
970
|
-
type: 'list',
|
|
971
|
-
name: 'action',
|
|
972
|
-
message: `How to handle ${file}?`,
|
|
973
|
-
default: smartResult && smartResult.ok ? 'smart' : 'skip',
|
|
974
|
-
choices,
|
|
975
|
-
},
|
|
976
|
-
]);
|
|
977
|
-
if (action.endsWith('-all')) {
|
|
978
|
-
applyAll = action.replace('-all', '');
|
|
979
|
-
strategy = applyAll;
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
strategy = action;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
const destPath = path.join(instanceDir, file);
|
|
987
|
-
switch (strategy) {
|
|
988
|
-
case 'smart':
|
|
989
|
-
if (smartResult && smartResult.ok) {
|
|
990
|
-
fs.writeFileSync(destPath, smartResult.merged, 'utf-8');
|
|
991
|
-
smartCount++;
|
|
992
|
-
console.info(chalk.green(` ✓ ${file} — smart merge (${smartMerger.name})`));
|
|
993
|
-
for (const note of smartResult.notes) {
|
|
994
|
-
console.info(chalk.gray(` • ${note}`));
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
else {
|
|
998
|
-
// No smart merger usable: fallback to conflict markers
|
|
999
|
-
fs.writeFileSync(destPath, createConflictContent(iContent, tContent), 'utf-8');
|
|
1000
|
-
conflictCount++;
|
|
1001
|
-
console.info(chalk.yellow(` ⚠️ ${file} — no smart merger available, wrote conflict markers`));
|
|
1002
|
-
}
|
|
1003
|
-
break;
|
|
1004
|
-
case 'skip':
|
|
1005
|
-
skippedCount++;
|
|
1006
|
-
break;
|
|
1007
|
-
case 'overwrite':
|
|
1008
|
-
fs.writeFileSync(destPath, tContent, 'utf-8');
|
|
1009
|
-
overwrittenCount++;
|
|
1010
|
-
break;
|
|
1011
|
-
case 'merge': {
|
|
1012
|
-
const result = tryAutoMerge(iContent, tContent);
|
|
1013
|
-
fs.writeFileSync(destPath, result.merged, 'utf-8');
|
|
1014
|
-
if (result.hasConflicts) {
|
|
1015
|
-
conflictCount++;
|
|
1016
|
-
console.info(chalk.yellow(` ⚠️ ${file} — merge has conflicts`));
|
|
1017
|
-
}
|
|
1018
|
-
else {
|
|
1019
|
-
mergedCount++;
|
|
1020
|
-
console.info(chalk.green(` ✓ ${file} — merged cleanly`));
|
|
1021
|
-
}
|
|
1022
|
-
break;
|
|
1023
|
-
}
|
|
1024
|
-
case 'conflict':
|
|
1025
|
-
default:
|
|
1026
|
-
fs.writeFileSync(destPath, createConflictContent(iContent, tContent), 'utf-8');
|
|
1027
|
-
conflictCount++;
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
945
|
+
// Materialize base from npm version recorded in meta.
|
|
946
|
+
try {
|
|
947
|
+
baseHandle = (0, materialize_1.materializeBase)(meta.schematicsVersion, templateName);
|
|
1030
948
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
fs.copyFileSync(path.join(templateDir, file), destPath);
|
|
949
|
+
catch (err) {
|
|
950
|
+
console.error(chalk.red(formatMaterializeError(meta.schematicsVersion, err)));
|
|
951
|
+
process.exit(1);
|
|
1035
952
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
`
|
|
953
|
+
const baseRoot = baseHandle.dir;
|
|
954
|
+
// Classify three sides.
|
|
955
|
+
const plan = (0, classify_1.classifyAll)(baseRoot, instanceDir, theirsRoot, userExcludes);
|
|
956
|
+
const totalChanges = plan.add.length + plan.delete.length + plan.update.length +
|
|
957
|
+
plan.addAddConflict.length + plan.modifyDeleteConflict.length + plan.deleteModifyConflict.length;
|
|
958
|
+
if (totalChanges === 0) {
|
|
959
|
+
console.info(chalk.green('✅ Instance is up to date with template.\n'));
|
|
960
|
+
}
|
|
961
|
+
else if (dryRun) {
|
|
962
|
+
console.info(chalk.bold('═══════════════════════════════════════════════════'));
|
|
963
|
+
console.info(chalk.bold(' Template Update Plan (dry-run)'));
|
|
964
|
+
console.info(chalk.bold('═══════════════════════════════════════════════════\n'));
|
|
965
|
+
for (const f of plan.add)
|
|
966
|
+
console.info(chalk.green(` add ${f}`));
|
|
967
|
+
for (const f of plan.delete)
|
|
968
|
+
console.info(chalk.red(` delete ${f}`));
|
|
969
|
+
for (const f of plan.update)
|
|
970
|
+
console.info(chalk.yellow(` update ${f}`));
|
|
971
|
+
for (const f of plan.addAddConflict)
|
|
972
|
+
console.info(chalk.magenta(` add/add ${f}`));
|
|
973
|
+
for (const f of plan.modifyDeleteConflict)
|
|
974
|
+
console.info(chalk.magenta(` modify/delete ${f}`));
|
|
975
|
+
for (const f of plan.deleteModifyConflict)
|
|
976
|
+
console.info(chalk.magenta(` delete/modify ${f}`));
|
|
977
|
+
console.info();
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
const conflicts = applyPlan(plan, baseRoot, instanceDir, theirsRoot, mergeOpts);
|
|
981
|
+
// Always advance meta to new version, even when conflicts present (invariant #2).
|
|
982
|
+
(0, base_store_1.writeMeta)(instanceDir, {
|
|
983
|
+
schematicsVersion,
|
|
984
|
+
schemaVersion: types_1.CURRENT_META_SCHEMA_VERSION,
|
|
985
|
+
});
|
|
986
|
+
printSummary(plan, conflicts, templateName, schematicsVersion);
|
|
1054
987
|
}
|
|
1055
988
|
}
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
console.info(chalk.gray('\nSkipping template update (--no-template).\n'));
|
|
992
|
+
}
|
|
993
|
+
// Stage 2: .claude context sync — UNCHANGED.
|
|
994
|
+
if (claudeEnabled) {
|
|
995
|
+
const cliRepo = ((_g = options.find((o) => o.name === 'coding-context-repo')) === null || _g === void 0 ? void 0 : _g.value) || '';
|
|
996
|
+
const cliProtocol = ((_h = options.find((o) => o.name === 'coding-context-protocol')) === null || _h === void 0 ? void 0 : _h.value) || '';
|
|
997
|
+
const claudeSource = yield resolveClaudeContextSource(cliRepo, cliProtocol, autoYes);
|
|
998
|
+
if (claudeSource) {
|
|
999
|
+
yield syncClaudeContext(process.cwd(), dryRun, claudeSource);
|
|
1000
|
+
}
|
|
1056
1001
|
else {
|
|
1057
|
-
console.info(chalk.gray('(
|
|
1002
|
+
console.info(chalk.gray('\nSkipping .claude context sync (per user choice / --no-coding-context).\n'));
|
|
1058
1003
|
}
|
|
1059
1004
|
}
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
console.info(chalk.gray('\nSkipping template update (--no-template).\n'));
|
|
1063
|
-
}
|
|
1064
|
-
// 2. Sync .claude context (regardless of whether template had changes)
|
|
1065
|
-
if (claudeEnabled) {
|
|
1066
|
-
const cliRepo = ((_g = options.find((o) => o.name === 'coding-context-repo')) === null || _g === void 0 ? void 0 : _g.value) ||
|
|
1067
|
-
'';
|
|
1068
|
-
const cliProtocol = ((_h = options.find((o) => o.name === 'coding-context-protocol')) === null || _h === void 0 ? void 0 : _h.value) || '';
|
|
1069
|
-
const claudeSource = yield resolveClaudeContextSource(cliRepo, cliProtocol, autoYes);
|
|
1070
|
-
if (claudeSource) {
|
|
1071
|
-
yield syncClaudeContext(instanceDir, dryRun, claudeSource);
|
|
1072
|
-
}
|
|
1073
1005
|
else {
|
|
1074
|
-
console.info(chalk.gray('\nSkipping .claude context sync (
|
|
1006
|
+
console.info(chalk.gray('\nSkipping .claude context sync (--no-coding-context).\n'));
|
|
1007
|
+
}
|
|
1008
|
+
// Stage 3: Install changed deps — UNCHANGED.
|
|
1009
|
+
if (pkgSnapshots) {
|
|
1010
|
+
yield maybeInstallChangedDeps(pkgSnapshots, dryRun);
|
|
1075
1011
|
}
|
|
1076
1012
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (pkgSnapshots) {
|
|
1083
|
-
yield maybeInstallChangedDeps(pkgSnapshots, dryRun);
|
|
1013
|
+
finally {
|
|
1014
|
+
if (baseHandle)
|
|
1015
|
+
baseHandle.cleanup();
|
|
1016
|
+
if (preflight)
|
|
1017
|
+
preflight.releaseLock();
|
|
1084
1018
|
}
|
|
1085
1019
|
});
|
|
1086
1020
|
}
|