@gadmin2n/cli 0.0.90 → 0.0.92

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.
Files changed (46) hide show
  1. package/.circleci/config.yml +62 -0
  2. package/.github/ISSUE_TEMPLATE/Bug_report.yml +106 -0
  3. package/.github/ISSUE_TEMPLATE/Feature_request.yml +52 -0
  4. package/.github/ISSUE_TEMPLATE/Regression.yml +78 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +7 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +41 -0
  7. package/actions/index.d.ts +1 -1
  8. package/actions/index.js +1 -1
  9. package/actions/self-update.action.d.ts +5 -0
  10. package/actions/self-update.action.js +93 -0
  11. package/actions/update.action.d.ts +2 -0
  12. package/actions/update.action.js +945 -6
  13. package/commands/command.loader.js +2 -2
  14. package/commands/{template.command.d.ts → self-update.command.d.ts} +1 -1
  15. package/commands/self-update.command.js +35 -0
  16. package/commands/update.command.js +25 -8
  17. package/lib/install-manager.d.ts +48 -0
  18. package/lib/install-manager.js +125 -0
  19. package/lib/template-merge/config-loader.d.ts +15 -0
  20. package/lib/template-merge/config-loader.js +38 -0
  21. package/lib/template-merge/env-merger.d.ts +5 -0
  22. package/lib/template-merge/env-merger.js +57 -0
  23. package/lib/template-merge/glob.d.ts +5 -0
  24. package/lib/template-merge/glob.js +78 -0
  25. package/lib/template-merge/index.d.ts +8 -0
  26. package/lib/template-merge/index.js +24 -0
  27. package/lib/template-merge/json-merger.d.ts +5 -0
  28. package/lib/template-merge/json-merger.js +252 -0
  29. package/lib/template-merge/merger.d.ts +30 -0
  30. package/lib/template-merge/merger.js +2 -0
  31. package/lib/template-merge/prisma-merger.d.ts +5 -0
  32. package/lib/template-merge/prisma-merger.js +112 -0
  33. package/lib/template-merge/registry.d.ts +25 -0
  34. package/lib/template-merge/registry.js +42 -0
  35. package/lib/template-merge/ts-module-merger.d.ts +16 -0
  36. package/lib/template-merge/ts-module-merger.js +193 -0
  37. package/lib/version-check.d.ts +39 -0
  38. package/lib/version-check.js +157 -0
  39. package/package.json +2 -2
  40. package/test/lib/compiler/hooks/__snapshots__/tsconfig-paths.hook.spec.ts.snap +68 -0
  41. package/test/lib/compiler/hooks/fixtures/aliased-imports/src/bar.tsx +1 -0
  42. package/test/lib/compiler/hooks/fixtures/aliased-imports/src/baz.js +1 -0
  43. package/test/lib/compiler/hooks/fixtures/aliased-imports/src/qux.jsx +1 -0
  44. package/actions/template.action.d.ts +0 -7
  45. package/actions/template.action.js +0 -570
  46. package/commands/template.command.js +0 -42
@@ -10,16 +10,955 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.UpdateAction = void 0;
13
- const dependency_managers_1 = require("../lib/dependency-managers");
14
- const package_managers_1 = require("../lib/package-managers");
13
+ const chalk = require("chalk");
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+ const inquirer = require("inquirer");
18
+ const child_process_1 = require("child_process");
15
19
  const abstract_action_1 = require("./abstract.action");
20
+ const template_merge_1 = require("../lib/template-merge");
21
+ const version_check_1 = require("../lib/version-check");
22
+ /* ------------------------------------------------------------------ *
23
+ * Config / constants
24
+ * ------------------------------------------------------------------ */
25
+ const DEFAULT_EXCLUDES = [
26
+ 'node_modules',
27
+ '.git',
28
+ 'dist',
29
+ 'generated',
30
+ '.DS_Store',
31
+ '.env.local',
32
+ 'readme.md',
33
+ ];
34
+ const CLAUDE_CONTEXT_REPO = 'https://git.tencent.com/OIT-OMC/erp-ai-coding-context.git';
35
+ const CLAUDE_CONTEXT_BRANCH = 'master';
36
+ const CLAUDE_SOURCE_SUBDIR = '.claude';
37
+ const CLAUDE_TARGET_DIRS = ['.claude-internal', '.agent'];
38
+ /* ------------------------------------------------------------------ *
39
+ * Glob / pattern utilities
40
+ * ------------------------------------------------------------------ */
41
+ /**
42
+ * Convert a glob pattern to a RegExp.
43
+ * Supports: * (any chars except /), ** (any path segments), ? (single char)
44
+ */
45
+ function globToRegex(pattern) {
46
+ let regexStr = '^';
47
+ let i = 0;
48
+ while (i < pattern.length) {
49
+ const c = pattern[i];
50
+ if (c === '*') {
51
+ if (pattern[i + 1] === '*') {
52
+ if (pattern[i + 2] === '/') {
53
+ regexStr += '(?:.+/)?';
54
+ i += 3;
55
+ }
56
+ else {
57
+ regexStr += '.*';
58
+ i += 2;
59
+ }
60
+ }
61
+ else {
62
+ regexStr += '[^/]*';
63
+ i++;
64
+ }
65
+ }
66
+ else if (c === '?') {
67
+ regexStr += '[^/]';
68
+ i++;
69
+ }
70
+ else if (c === '.' ||
71
+ c === '(' ||
72
+ c === ')' ||
73
+ c === '[' ||
74
+ c === ']' ||
75
+ c === '{' ||
76
+ c === '}' ||
77
+ c === '+' ||
78
+ c === '^' ||
79
+ c === '$' ||
80
+ c === '|' ||
81
+ c === '\\') {
82
+ regexStr += '\\' + c;
83
+ i++;
84
+ }
85
+ else {
86
+ regexStr += c;
87
+ i++;
88
+ }
89
+ }
90
+ regexStr += '$';
91
+ return new RegExp(regexStr);
92
+ }
93
+ function matchesPattern(filePath, pattern) {
94
+ if (filePath === pattern)
95
+ return true;
96
+ if (filePath.startsWith(pattern + '/'))
97
+ return true;
98
+ const parts = filePath.split('/');
99
+ if (!pattern.includes('/') &&
100
+ !pattern.includes('*') &&
101
+ !pattern.includes('?')) {
102
+ if (parts.includes(pattern))
103
+ return true;
104
+ }
105
+ if (pattern.includes('*') || pattern.includes('?')) {
106
+ const regex = globToRegex(pattern);
107
+ return regex.test(filePath);
108
+ }
109
+ return false;
110
+ }
111
+ function shouldExclude(filePath, excludes) {
112
+ for (const pattern of excludes) {
113
+ if (matchesPattern(filePath, pattern))
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ function collectFiles(dir, excludes, base = '') {
119
+ const results = [];
120
+ const entries = fs.readdirSync(path.join(dir, base), { withFileTypes: true });
121
+ for (const entry of entries) {
122
+ const rel = base ? `${base}/${entry.name}` : entry.name;
123
+ if (shouldExclude(rel, excludes))
124
+ continue;
125
+ if (entry.isDirectory()) {
126
+ results.push(...collectFiles(dir, excludes, rel));
127
+ }
128
+ else if (entry.isFile()) {
129
+ results.push(rel);
130
+ }
131
+ }
132
+ return results.sort();
133
+ }
134
+ function loadTemplateConfig() {
135
+ const configPaths = [
136
+ path.join(process.cwd(), 'server/gadmin-cli.json'),
137
+ path.join(process.cwd(), 'gadmin-cli.json'),
138
+ ];
139
+ let templateName;
140
+ let userExcludes = [];
141
+ for (const configPath of configPaths) {
142
+ if (fs.existsSync(configPath)) {
143
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
144
+ templateName = config.template;
145
+ if (Array.isArray(config.templateExcludes)) {
146
+ userExcludes = config.templateExcludes;
147
+ }
148
+ break;
149
+ }
150
+ }
151
+ if (!templateName) {
152
+ throw new Error('Cannot find "template" field in gadmin-cli.json. ' +
153
+ 'Make sure you are in a gadmin2 project root directory.');
154
+ }
155
+ return {
156
+ templateName,
157
+ excludes: [...DEFAULT_EXCLUDES, ...userExcludes],
158
+ };
159
+ }
160
+ function resolveTemplatePath(templateName) {
161
+ let schematicsRoot;
162
+ const searchPaths = [
163
+ process.cwd(),
164
+ path.join(process.cwd(), 'server'),
165
+ path.resolve(__dirname, '..'),
166
+ ];
167
+ for (const searchPath of searchPaths) {
168
+ try {
169
+ const pkgPath = require.resolve('@gadmin2n/schematics/package.json', {
170
+ paths: [searchPath],
171
+ });
172
+ schematicsRoot = path.dirname(pkgPath);
173
+ break;
174
+ }
175
+ catch (_a) {
176
+ // continue
177
+ }
178
+ }
179
+ if (!schematicsRoot) {
180
+ throw new Error('Cannot resolve @gadmin2n/schematics package. ' +
181
+ 'Make sure it is installed.');
182
+ }
183
+ const nameVariants = [templateName];
184
+ if (templateName.startsWith('gadmin2n-')) {
185
+ nameVariants.push(templateName.replace('gadmin2n-', 'gadmin2-'));
186
+ }
187
+ else if (templateName.startsWith('gadmin2-')) {
188
+ nameVariants.push(templateName.replace('gadmin2-', 'gadmin2n-'));
189
+ }
190
+ for (const name of nameVariants) {
191
+ const distPath = path.join(schematicsRoot, 'dist/lib/application/files', name);
192
+ const srcPath = path.join(schematicsRoot, 'src/lib/application/files', name);
193
+ if (fs.existsSync(distPath))
194
+ return distPath;
195
+ if (fs.existsSync(srcPath))
196
+ return srcPath;
197
+ }
198
+ throw new Error(`Template directory not found for "${templateName}" in ${schematicsRoot}`);
199
+ }
200
+ /* ------------------------------------------------------------------ *
201
+ * Diff / merge utilities
202
+ * ------------------------------------------------------------------ */
203
+ function createUnifiedDiff(filePath, instanceContent, templateContent) {
204
+ const instanceLines = instanceContent.split('\n');
205
+ const templateLines = templateContent.split('\n');
206
+ const lines = [];
207
+ lines.push(chalk.bold(`--- a/${filePath} (instance)`));
208
+ lines.push(chalk.bold(`+++ b/${filePath} (template)`));
209
+ const maxLen = Math.max(instanceLines.length, templateLines.length);
210
+ const hunks = [];
211
+ let currentHunk = null;
212
+ for (let i = 0; i < maxLen; i++) {
213
+ const a = i < instanceLines.length ? instanceLines[i] : undefined;
214
+ const b = i < templateLines.length ? templateLines[i] : undefined;
215
+ if (a !== b) {
216
+ if (!currentHunk) {
217
+ currentHunk = { start: i, instanceLines: [], templateLines: [] };
218
+ }
219
+ if (a !== undefined)
220
+ currentHunk.instanceLines.push(a);
221
+ if (b !== undefined)
222
+ currentHunk.templateLines.push(b);
223
+ }
224
+ else {
225
+ if (currentHunk) {
226
+ hunks.push(currentHunk);
227
+ currentHunk = null;
228
+ }
229
+ }
230
+ }
231
+ if (currentHunk)
232
+ hunks.push(currentHunk);
233
+ for (const hunk of hunks.slice(0, 10)) {
234
+ const contextStart = Math.max(0, hunk.start - 2);
235
+ lines.push(chalk.cyan(`@@ -${hunk.start + 1},${hunk.instanceLines.length} +${hunk.start + 1},${hunk.templateLines.length} @@`));
236
+ for (let i = contextStart; i < hunk.start; i++) {
237
+ if (i < instanceLines.length) {
238
+ lines.push(` ${instanceLines[i]}`);
239
+ }
240
+ }
241
+ for (const line of hunk.instanceLines) {
242
+ lines.push(chalk.red(`-${line}`));
243
+ }
244
+ for (const line of hunk.templateLines) {
245
+ lines.push(chalk.green(`+${line}`));
246
+ }
247
+ const afterStart = hunk.start + Math.max(hunk.instanceLines.length, hunk.templateLines.length);
248
+ for (let i = afterStart; i < Math.min(afterStart + 2, instanceLines.length); i++) {
249
+ lines.push(` ${instanceLines[i]}`);
250
+ }
251
+ }
252
+ if (hunks.length > 10) {
253
+ lines.push(chalk.yellow(` ... and ${hunks.length - 10} more hunks`));
254
+ }
255
+ return lines.join('\n');
256
+ }
257
+ function createConflictContent(instanceContent, templateContent) {
258
+ return [
259
+ '<<<<<<< INSTANCE',
260
+ instanceContent,
261
+ '=======',
262
+ templateContent,
263
+ '>>>>>>> TEMPLATE',
264
+ ].join('\n');
265
+ }
266
+ function tryAutoMerge(instanceContent, templateContent) {
267
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gadmin-merge-'));
268
+ const basePath = path.join(tmpDir, 'base');
269
+ const oursPath = path.join(tmpDir, 'ours');
270
+ const theirsPath = path.join(tmpDir, 'theirs');
271
+ try {
272
+ fs.writeFileSync(basePath, '', 'utf-8');
273
+ fs.writeFileSync(oursPath, instanceContent, 'utf-8');
274
+ fs.writeFileSync(theirsPath, templateContent, 'utf-8');
275
+ try {
276
+ (0, child_process_1.execSync)(`git merge-file -L "INSTANCE" -L "BASE" -L "TEMPLATE" "${oursPath}" "${basePath}" "${theirsPath}"`, { stdio: 'pipe' });
277
+ const merged = fs.readFileSync(oursPath, 'utf-8');
278
+ return { merged, hasConflicts: false };
279
+ }
280
+ catch (err) {
281
+ if (err.status > 0) {
282
+ const merged = fs.readFileSync(oursPath, 'utf-8');
283
+ return { merged, hasConflicts: true };
284
+ }
285
+ return {
286
+ merged: createConflictContent(instanceContent, templateContent),
287
+ hasConflicts: true,
288
+ };
289
+ }
290
+ }
291
+ finally {
292
+ fs.rmSync(tmpDir, { recursive: true, force: true });
293
+ }
294
+ }
295
+ /* ------------------------------------------------------------------ *
296
+ * .claude context sync (from remote git repo)
297
+ * ------------------------------------------------------------------ */
298
+ /**
299
+ * Shallow-clone the remote .claude context repo into a temp dir and return
300
+ * the absolute path to the `.claude` subfolder. Returns null on failure
301
+ * (e.g. no network, repo private, credentials missing, etc.).
302
+ */
303
+ function fetchClaudeContextRepo() {
304
+ var _a, _b;
305
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gadmin-claude-ctx-'));
306
+ try {
307
+ (0, child_process_1.execSync)(`git clone --depth 1 --branch ${CLAUDE_CONTEXT_BRANCH} ${CLAUDE_CONTEXT_REPO} "${tmpDir}"`, { stdio: 'pipe' });
308
+ const claudeSrc = path.join(tmpDir, CLAUDE_SOURCE_SUBDIR);
309
+ if (!fs.existsSync(claudeSrc)) {
310
+ // Repo cloned, but the .claude dir is missing — treat as no-op
311
+ fs.rmSync(tmpDir, { recursive: true, force: true });
312
+ return null;
313
+ }
314
+ // Caller is responsible for cleaning up the parent tmpDir.
315
+ // We return the inner path; we stash the parent on a property so caller
316
+ // can clean it. To keep things simple, register a process exit cleanup
317
+ // hook here.
318
+ process.on('exit', () => {
319
+ try {
320
+ fs.rmSync(tmpDir, { recursive: true, force: true });
321
+ }
322
+ catch (_a) {
323
+ // ignore
324
+ }
325
+ });
326
+ return claudeSrc;
327
+ }
328
+ catch (err) {
329
+ fs.rmSync(tmpDir, { recursive: true, force: true });
330
+ const msg = ((_b = (_a = err === null || err === void 0 ? void 0 : err.stderr) === null || _a === void 0 ? void 0 : _a.toString) === null || _b === void 0 ? void 0 : _b.call(_a)) || (err === null || err === void 0 ? void 0 : err.message) || String(err);
331
+ console.warn(chalk.yellow(`⚠️ Skipping .claude context sync: failed to clone ${CLAUDE_CONTEXT_REPO}\n ${msg
332
+ .trim()
333
+ .split('\n')
334
+ .join('\n ')}`));
335
+ return null;
336
+ }
337
+ }
338
+ /**
339
+ * Recursively copy every file under `srcDir` into `destDir`, preserving
340
+ * relative structure. Existing destination files are NOT overwritten.
341
+ * Returns counts.
342
+ */
343
+ function copyTreeNoOverwrite(srcDir, destDir) {
344
+ let added = 0;
345
+ let skipped = 0;
346
+ function walk(rel) {
347
+ const absSrc = path.join(srcDir, rel);
348
+ const entries = fs.readdirSync(absSrc, { withFileTypes: true });
349
+ for (const entry of entries) {
350
+ const childRel = rel ? path.join(rel, entry.name) : entry.name;
351
+ const absChildSrc = path.join(srcDir, childRel);
352
+ const absChildDest = path.join(destDir, childRel);
353
+ if (entry.isDirectory()) {
354
+ walk(childRel);
355
+ }
356
+ else if (entry.isFile()) {
357
+ if (fs.existsSync(absChildDest)) {
358
+ skipped++;
359
+ continue;
360
+ }
361
+ fs.mkdirSync(path.dirname(absChildDest), { recursive: true });
362
+ fs.copyFileSync(absChildSrc, absChildDest);
363
+ added++;
364
+ }
365
+ }
366
+ }
367
+ if (!fs.existsSync(srcDir))
368
+ return { added, skipped };
369
+ walk('');
370
+ return { added, skipped };
371
+ }
372
+ function syncClaudeContext(instanceDir, dryRun) {
373
+ return __awaiter(this, void 0, void 0, function* () {
374
+ console.info(chalk.bold('\n═══════════════════════════════════════════════════'));
375
+ console.info(chalk.bold(' Sync .claude context'));
376
+ console.info(chalk.bold('═══════════════════════════════════════════════════'));
377
+ console.info(`${chalk.gray('Source: ')} ${CLAUDE_CONTEXT_REPO} (${CLAUDE_CONTEXT_BRANCH}) :: ${CLAUDE_SOURCE_SUBDIR}/`);
378
+ console.info(`${chalk.gray('Targets:')} ${CLAUDE_TARGET_DIRS.map((d) => `${d}/`).join(', ')}\n`);
379
+ if (dryRun) {
380
+ console.info(chalk.gray('(dry-run mode — would clone the repo and copy missing files into target dirs)\n'));
381
+ return;
382
+ }
383
+ const claudeSrc = fetchClaudeContextRepo();
384
+ if (!claudeSrc) {
385
+ return;
386
+ }
387
+ for (const targetName of CLAUDE_TARGET_DIRS) {
388
+ const destDir = path.join(instanceDir, targetName);
389
+ fs.mkdirSync(destDir, { recursive: true });
390
+ const { added, skipped } = copyTreeNoOverwrite(claudeSrc, destDir);
391
+ const summary = [];
392
+ if (added > 0)
393
+ summary.push(`${added} added`);
394
+ if (skipped > 0)
395
+ summary.push(`${skipped} kept (already exists)`);
396
+ if (summary.length === 0)
397
+ summary.push('no files');
398
+ console.info(` ${chalk.cyan(targetName + '/')} ${summary.join(' | ')}`);
399
+ }
400
+ console.info();
401
+ });
402
+ }
403
+ function snapshotPackageJson(dir, label) {
404
+ const pkgPath = path.join(dir, 'package.json');
405
+ const before = fs.existsSync(pkgPath)
406
+ ? fs.readFileSync(pkgPath, 'utf-8')
407
+ : null;
408
+ return { dir, label, before };
409
+ }
410
+ /**
411
+ * Build a stable, canonical JSON string of only the dependency-related
412
+ * sections of a package.json. Sub-keys are sorted alphabetically so that
413
+ * pure formatting / key-order changes do not register as a difference.
414
+ *
415
+ * Returns null if the input cannot be parsed as JSON.
416
+ */
417
+ function canonicalDeps(content) {
418
+ try {
419
+ const json = JSON.parse(content);
420
+ const depKeys = [
421
+ 'dependencies',
422
+ 'devDependencies',
423
+ 'peerDependencies',
424
+ 'optionalDependencies',
425
+ ];
426
+ const subset = {};
427
+ for (const key of depKeys) {
428
+ const val = json[key];
429
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
430
+ const sorted = {};
431
+ for (const sk of Object.keys(val).sort()) {
432
+ sorted[sk] = val[sk];
433
+ }
434
+ subset[key] = sorted;
435
+ }
436
+ }
437
+ return JSON.stringify(subset);
438
+ }
439
+ catch (_a) {
440
+ return null;
441
+ }
442
+ }
443
+ /**
444
+ * Determine whether the dependency-related sections of package.json have
445
+ * meaningfully changed between two snapshots. Ignores formatting / key
446
+ * order / unrelated fields like name / scripts / description.
447
+ *
448
+ * - First-time existence (before === null) → considered changed (need install).
449
+ * - Identical bytes → no change (fast path).
450
+ * - Either side fails to parse → fall back to raw byte comparison (safe default).
451
+ */
452
+ function depsChanged(before, after) {
453
+ if (before === null)
454
+ return true;
455
+ if (before === after)
456
+ return false;
457
+ const cBefore = canonicalDeps(before);
458
+ const cAfter = canonicalDeps(after);
459
+ if (cBefore === null || cAfter === null) {
460
+ return before !== after;
461
+ }
462
+ return cBefore !== cAfter;
463
+ }
464
+ function detectInstallCommand(dir) {
465
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) {
466
+ return { cmd: 'yarn', manager: 'yarn' };
467
+ }
468
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) {
469
+ return { cmd: 'pnpm install', manager: 'pnpm' };
470
+ }
471
+ return { cmd: 'npm install', manager: 'npm' };
472
+ }
473
+ function maybeInstallChangedDeps(snapshots, dryRun) {
474
+ return __awaiter(this, void 0, void 0, function* () {
475
+ const changed = [];
476
+ const skippedNoDepChange = [];
477
+ for (const snap of snapshots) {
478
+ const pkgPath = path.join(snap.dir, 'package.json');
479
+ if (!fs.existsSync(pkgPath))
480
+ continue;
481
+ const after = fs.readFileSync(pkgPath, 'utf-8');
482
+ if (depsChanged(snap.before, after)) {
483
+ changed.push(snap);
484
+ }
485
+ else if (snap.before !== after) {
486
+ // package.json bytes changed but deps did not (e.g. scripts / formatting only)
487
+ skippedNoDepChange.push(snap.label);
488
+ }
489
+ }
490
+ if (changed.length === 0 && skippedNoDepChange.length === 0)
491
+ return;
492
+ console.info(chalk.bold('\n═══════════════════════════════════════════════════'));
493
+ console.info(chalk.bold(' Install dependencies for changed package.json'));
494
+ console.info(chalk.bold('═══════════════════════════════════════════════════\n'));
495
+ for (const label of skippedNoDepChange) {
496
+ console.info(` ${chalk.cyan(label + '/')} package.json changed but deps unchanged — ${chalk.gray('skip install')}`);
497
+ }
498
+ for (const snap of changed) {
499
+ const { cmd, manager } = detectInstallCommand(snap.dir);
500
+ console.info(` ${chalk.cyan(snap.label + '/')} deps changed — using ${chalk.bold(manager)}: ${chalk.gray(cmd)}`);
501
+ if (dryRun) {
502
+ console.info(chalk.gray(` (dry-run: skip running)\n`));
503
+ continue;
504
+ }
505
+ try {
506
+ (0, child_process_1.execSync)(cmd, { cwd: snap.dir, stdio: 'inherit' });
507
+ console.info(chalk.green(` ✓ ${snap.label}/ install done\n`));
508
+ }
509
+ catch (err) {
510
+ console.warn(chalk.yellow(` ⚠️ ${snap.label}/ install failed: ${(err === null || err === void 0 ? void 0 : err.message) || err}\n`));
511
+ }
512
+ }
513
+ });
514
+ }
515
+ /* ------------------------------------------------------------------ *
516
+ * Smart merger registry
517
+ * ------------------------------------------------------------------ */
518
+ const DEFAULT_MERGER_RULES = [
519
+ { pattern: 'package.json', mergerName: 'json' },
520
+ { pattern: '**/package.json', mergerName: 'json' },
521
+ { pattern: 'tsconfig*.json', mergerName: 'json' },
522
+ { pattern: '**/tsconfig*.json', mergerName: 'json' },
523
+ { pattern: '**/*.module.ts', mergerName: 'ts-module' },
524
+ { pattern: 'config/prisma/*.prisma', mergerName: 'prisma' },
525
+ { pattern: 'server/prisma/schema.prisma', mergerName: 'prisma' },
526
+ { pattern: '.env', mergerName: 'env' },
527
+ { pattern: '.env.*', mergerName: 'env' },
528
+ { pattern: '**/.env', mergerName: 'env' },
529
+ { pattern: '**/.env.*', mergerName: 'env' },
530
+ ];
531
+ function buildMergerRegistry(userConfig) {
532
+ const reg = new template_merge_1.MergerRegistry();
533
+ reg.register(new template_merge_1.JsonMerger());
534
+ reg.register(new template_merge_1.EnvMerger());
535
+ reg.register(new template_merge_1.PrismaMerger());
536
+ reg.register(new template_merge_1.TsModuleMerger());
537
+ if ((userConfig === null || userConfig === void 0 ? void 0 : userConfig.mergers) && Object.keys(userConfig.mergers).length > 0) {
538
+ // 用户配置完全替换默认规则
539
+ for (const [pattern, mergerName] of Object.entries(userConfig.mergers)) {
540
+ reg.addRule({ pattern, mergerName });
541
+ }
542
+ }
543
+ else {
544
+ for (const rule of DEFAULT_MERGER_RULES) {
545
+ reg.addRule(rule);
546
+ }
547
+ }
548
+ return reg;
549
+ }
550
+ /* ------------------------------------------------------------------ *
551
+ * Action
552
+ * ------------------------------------------------------------------ */
16
553
  class UpdateAction extends abstract_action_1.AbstractAction {
17
554
  handle(inputs, options) {
555
+ var _a, _b;
556
+ return __awaiter(this, void 0, void 0, function* () {
557
+ const subcommand = (_a = inputs.find((i) => i.name === 'subcommand')) === null || _a === void 0 ? void 0 : _a.value;
558
+ if (subcommand && subcommand !== 'update' && subcommand !== 'diff') {
559
+ console.error(chalk.red(`Unknown subcommand "${subcommand}". Expected: (none) | diff`));
560
+ process.exit(1);
561
+ }
562
+ // Pre-flight version check (B): silent network call to npm registry,
563
+ // hard 1.5s timeout, never blocks. Prints a yellow banner if outdated.
564
+ const versionCheckEnabled = ((_b = options.find((o) => o.name === 'version-check')) === null || _b === void 0 ? void 0 : _b.value) !==
565
+ false;
566
+ if (versionCheckEnabled) {
567
+ try {
568
+ const info = yield (0, version_check_1.checkVersion)(1500);
569
+ if (info && (0, version_check_1.compareVersions)(info.installed, info.latest) < 0) {
570
+ (0, version_check_1.printOutdatedWarning)(info);
571
+ }
572
+ }
573
+ catch (_c) {
574
+ // never let the version check break update
575
+ }
576
+ }
577
+ if (subcommand === 'diff') {
578
+ yield this.handleDiff(options);
579
+ return;
580
+ }
581
+ yield this.handleUpdate(options);
582
+ });
583
+ }
584
+ handleDiff(options) {
585
+ var _a;
586
+ return __awaiter(this, void 0, void 0, function* () {
587
+ const noContent = (_a = options.find((o) => o.name === 'no-content')) === null || _a === void 0 ? void 0 : _a.value;
588
+ let templateDir;
589
+ let excludes;
590
+ try {
591
+ const config = loadTemplateConfig();
592
+ templateDir = resolveTemplatePath(config.templateName);
593
+ excludes = config.excludes;
594
+ }
595
+ catch (err) {
596
+ console.error(chalk.red(err.message));
597
+ process.exit(1);
598
+ }
599
+ const instanceDir = process.cwd();
600
+ console.info(`\n${chalk.gray('Template:')} ${templateDir}`);
601
+ console.info(`${chalk.gray('Instance:')} ${instanceDir}\n`);
602
+ const templateFiles = collectFiles(templateDir, excludes);
603
+ const instanceFiles = collectFiles(instanceDir, excludes);
604
+ const templateSet = new Set(templateFiles);
605
+ const instanceSet = new Set(instanceFiles);
606
+ const modified = [];
607
+ const onlyInTemplate = [];
608
+ const onlyInInstance = [];
609
+ for (const file of templateFiles) {
610
+ if (instanceSet.has(file)) {
611
+ const tContent = fs.readFileSync(path.join(templateDir, file), 'utf-8');
612
+ const iContent = fs.readFileSync(path.join(instanceDir, file), 'utf-8');
613
+ if (tContent !== iContent) {
614
+ modified.push(file);
615
+ }
616
+ }
617
+ else {
618
+ onlyInTemplate.push(file);
619
+ }
620
+ }
621
+ for (const file of instanceFiles) {
622
+ if (!templateSet.has(file)) {
623
+ onlyInInstance.push(file);
624
+ }
625
+ }
626
+ console.info(chalk.bold('═══════════════════════════════════════════════════'));
627
+ console.info(chalk.bold(' Template Diff Summary'));
628
+ console.info(chalk.bold('═══════════════════════════════════════════════════\n'));
629
+ if (modified.length > 0) {
630
+ console.info(chalk.yellow(`📝 Modified (${modified.length} files)`));
631
+ for (const f of modified) {
632
+ console.info(` ${f}`);
633
+ }
634
+ console.info();
635
+ }
636
+ if (onlyInTemplate.length > 0) {
637
+ console.info(chalk.green(`➕ Only in template (${onlyInTemplate.length} files)`));
638
+ for (const f of onlyInTemplate) {
639
+ console.info(` ${f}`);
640
+ }
641
+ console.info();
642
+ }
643
+ if (onlyInInstance.length > 0) {
644
+ console.info(chalk.cyan(`📄 Only in instance (${onlyInInstance.length} files)`));
645
+ for (const f of onlyInInstance) {
646
+ console.info(` ${f}`);
647
+ }
648
+ console.info();
649
+ }
650
+ if (modified.length === 0 &&
651
+ onlyInTemplate.length === 0 &&
652
+ onlyInInstance.length === 0) {
653
+ console.info(chalk.green('✅ No differences found. Instance matches template.\n'));
654
+ return;
655
+ }
656
+ if (!noContent && modified.length > 0) {
657
+ console.info(chalk.bold('───────────────────────────────────────────────────'));
658
+ console.info(chalk.bold(' File Diffs (instance vs template)'));
659
+ console.info(chalk.bold('───────────────────────────────────────────────────\n'));
660
+ for (const file of modified) {
661
+ const tContent = fs.readFileSync(path.join(templateDir, file), 'utf-8');
662
+ const iContent = fs.readFileSync(path.join(instanceDir, file), 'utf-8');
663
+ const diff = createUnifiedDiff(file, iContent, tContent);
664
+ console.info(diff);
665
+ console.info();
666
+ }
667
+ }
668
+ });
669
+ }
670
+ handleUpdate(options) {
671
+ var _a, _b, _c, _d;
18
672
  return __awaiter(this, void 0, void 0, function* () {
19
- const force = options.find((option) => option.name === 'force');
20
- const tag = options.find((option) => option.name === 'tag');
21
- const manager = new dependency_managers_1.NestDependencyManager(yield package_managers_1.PackageManagerFactory.find());
22
- yield manager.update(force.value, tag.value);
673
+ const dryRun = (_a = options.find((o) => o.name === 'dry-run')) === null || _a === void 0 ? void 0 : _a.value;
674
+ const autoYes = (_b = options.find((o) => o.name === 'yes')) === null || _b === void 0 ? void 0 : _b.value;
675
+ const smartMergeEnabled = (_d = (_c = options.find((o) => o.name === 'smart-merge')) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : true;
676
+ let templateDir;
677
+ let excludes;
678
+ try {
679
+ const config = loadTemplateConfig();
680
+ templateDir = resolveTemplatePath(config.templateName);
681
+ excludes = config.excludes;
682
+ }
683
+ catch (err) {
684
+ console.error(chalk.red(err.message));
685
+ process.exit(1);
686
+ }
687
+ const instanceDir = process.cwd();
688
+ console.info(`\n${chalk.gray('Template:')} ${templateDir}`);
689
+ console.info(`${chalk.gray('Instance:')} ${instanceDir}\n`);
690
+ // Build smart-merger registry (or null if disabled)
691
+ const userTemplateUpdate = smartMergeEnabled
692
+ ? (0, template_merge_1.loadTemplateUpdateConfig)(instanceDir)
693
+ : null;
694
+ const registry = smartMergeEnabled
695
+ ? buildMergerRegistry(userTemplateUpdate)
696
+ : null;
697
+ const userPolicies = userTemplateUpdate === null || userTemplateUpdate === void 0 ? void 0 : userTemplateUpdate.policies;
698
+ // Snapshot package.json files BEFORE any writes. We watch:
699
+ // - <instanceDir>/package.json (root, if present — workspace / husky / etc.)
700
+ // - <instanceDir>/web/package.json
701
+ // - <instanceDir>/server/package.json
702
+ const pkgSnapshots = [
703
+ snapshotPackageJson(instanceDir, '(root)'),
704
+ snapshotPackageJson(path.join(instanceDir, 'web'), 'web'),
705
+ snapshotPackageJson(path.join(instanceDir, 'server'), 'server'),
706
+ ];
707
+ const templateFiles = collectFiles(templateDir, excludes);
708
+ const instanceFiles = collectFiles(instanceDir, excludes);
709
+ const instanceSet = new Set(instanceFiles);
710
+ const toUpdate = [];
711
+ const toAdd = [];
712
+ for (const file of templateFiles) {
713
+ if (instanceSet.has(file)) {
714
+ const tContent = fs.readFileSync(path.join(templateDir, file), 'utf-8');
715
+ const iContent = fs.readFileSync(path.join(instanceDir, file), 'utf-8');
716
+ if (tContent !== iContent) {
717
+ toUpdate.push(file);
718
+ }
719
+ }
720
+ else {
721
+ toAdd.push(file);
722
+ }
723
+ }
724
+ if (toUpdate.length === 0 && toAdd.length === 0) {
725
+ console.info(chalk.green('✅ Instance is up to date with template.\n'));
726
+ }
727
+ else {
728
+ console.info(chalk.bold('═══════════════════════════════════════════════════'));
729
+ console.info(chalk.bold(' Template Update Summary'));
730
+ console.info(chalk.bold('═══════════════════════════════════════════════════\n'));
731
+ if (toUpdate.length > 0) {
732
+ console.info(chalk.yellow(`📝 To update (${toUpdate.length} files)`));
733
+ for (const f of toUpdate) {
734
+ console.info(` ${f}`);
735
+ }
736
+ console.info();
737
+ }
738
+ if (toAdd.length > 0) {
739
+ console.info(chalk.green(`➕ To add (${toAdd.length} files)`));
740
+ for (const f of toAdd) {
741
+ console.info(` ${f}`);
742
+ }
743
+ console.info();
744
+ }
745
+ console.info(chalk.gray('───────────────────────────────────────────────────'));
746
+ console.info(chalk.gray(` Total: ${toUpdate.length} update | ${toAdd.length} add`));
747
+ console.info(chalk.gray('───────────────────────────────────────────────────\n'));
748
+ if (!dryRun) {
749
+ if (!autoYes) {
750
+ const { confirm } = yield inquirer.prompt([
751
+ {
752
+ type: 'confirm',
753
+ name: 'confirm',
754
+ message: 'Proceed with template update?',
755
+ default: false,
756
+ },
757
+ ]);
758
+ if (!confirm) {
759
+ console.info('Cancelled.\n');
760
+ return;
761
+ }
762
+ }
763
+ let skippedCount = 0;
764
+ let overwrittenCount = 0;
765
+ let mergedCount = 0;
766
+ let conflictCount = 0;
767
+ let smartCount = 0;
768
+ let applyAll = null;
769
+ for (let i = 0; i < toUpdate.length; i++) {
770
+ const file = toUpdate[i];
771
+ const tContent = fs.readFileSync(path.join(templateDir, file), 'utf-8');
772
+ const iContent = fs.readFileSync(path.join(instanceDir, file), 'utf-8');
773
+ // 1. Try smart merger if registered for this file
774
+ const smartMerger = registry
775
+ ? registry.resolve(file)
776
+ : null;
777
+ let smartResult = null;
778
+ if (smartMerger) {
779
+ try {
780
+ smartResult = yield smartMerger.merge({
781
+ filePath: file,
782
+ instancePath: path.join(instanceDir, file),
783
+ templatePath: path.join(templateDir, file),
784
+ instanceContent: iContent,
785
+ templateContent: tContent,
786
+ policies: userPolicies,
787
+ });
788
+ }
789
+ catch (err) {
790
+ smartResult = {
791
+ ok: false,
792
+ reason: (err === null || err === void 0 ? void 0 : err.message) || String(err),
793
+ };
794
+ }
795
+ }
796
+ let strategy = applyAll;
797
+ if (!strategy) {
798
+ if (autoYes) {
799
+ // autoYes: prefer smart merge when available, else conflict markers
800
+ strategy =
801
+ smartResult && smartResult.ok ? 'smart' : 'conflict';
802
+ }
803
+ else {
804
+ const diff = createUnifiedDiff(file, iContent, tContent);
805
+ console.info(`\n${chalk.bold(`[${i + 1}/${toUpdate.length}]`)} ${chalk.yellow(file)}`);
806
+ console.info(diff);
807
+ console.info();
808
+ const choices = [];
809
+ if (smartResult && smartResult.ok) {
810
+ const summary = smartResult.notes.slice(0, 3).join('; ');
811
+ const more = smartResult.notes.length > 3 ? '...' : '';
812
+ choices.push({
813
+ name: `Smart merge — ${smartMerger.name} (recommended): ${summary}${more}`,
814
+ value: 'smart',
815
+ short: 'smart',
816
+ });
817
+ }
818
+ else if (smartMerger) {
819
+ const reason = smartResult && !smartResult.ok
820
+ ? smartResult.reason
821
+ : 'no result';
822
+ choices.push({
823
+ name: `Smart merge — ${smartMerger.name} (UNAVAILABLE: ${reason})`,
824
+ value: 'smart-unavailable',
825
+ short: 'smart-unavailable',
826
+ disabled: reason,
827
+ });
828
+ }
829
+ choices.push({
830
+ name: 'Skip — keep instance as-is',
831
+ value: 'skip',
832
+ short: 'skip',
833
+ }, {
834
+ name: 'Overwrite — use template version',
835
+ value: 'overwrite',
836
+ short: 'overwrite',
837
+ }, {
838
+ name: 'Auto merge — attempt git merge',
839
+ value: 'merge',
840
+ short: 'merge',
841
+ }, {
842
+ name: 'Conflict markers — write both versions for manual resolve',
843
+ value: 'conflict',
844
+ short: 'conflict',
845
+ }, new inquirer.Separator());
846
+ if (smartResult && smartResult.ok) {
847
+ choices.push({
848
+ name: 'Smart-merge ALL remaining (where supported)',
849
+ value: 'smart-all',
850
+ short: 'smart all',
851
+ });
852
+ }
853
+ choices.push({
854
+ name: 'Skip ALL remaining',
855
+ value: 'skip-all',
856
+ short: 'skip all',
857
+ }, {
858
+ name: 'Overwrite ALL remaining',
859
+ value: 'overwrite-all',
860
+ short: 'overwrite all',
861
+ }, {
862
+ name: 'Conflict markers ALL remaining',
863
+ value: 'conflict-all',
864
+ short: 'conflict all',
865
+ });
866
+ const { action } = yield inquirer.prompt([
867
+ {
868
+ type: 'list',
869
+ name: 'action',
870
+ message: `How to handle ${file}?`,
871
+ default: smartResult && smartResult.ok ? 'smart' : 'skip',
872
+ choices,
873
+ },
874
+ ]);
875
+ if (action.endsWith('-all')) {
876
+ applyAll = action.replace('-all', '');
877
+ strategy = applyAll;
878
+ }
879
+ else {
880
+ strategy = action;
881
+ }
882
+ }
883
+ }
884
+ const destPath = path.join(instanceDir, file);
885
+ switch (strategy) {
886
+ case 'smart':
887
+ if (smartResult && smartResult.ok) {
888
+ fs.writeFileSync(destPath, smartResult.merged, 'utf-8');
889
+ smartCount++;
890
+ console.info(chalk.green(` ✓ ${file} — smart merge (${smartMerger.name})`));
891
+ for (const note of smartResult.notes) {
892
+ console.info(chalk.gray(` • ${note}`));
893
+ }
894
+ }
895
+ else {
896
+ // No smart merger usable: fallback to conflict markers
897
+ fs.writeFileSync(destPath, createConflictContent(iContent, tContent), 'utf-8');
898
+ conflictCount++;
899
+ console.info(chalk.yellow(` ⚠️ ${file} — no smart merger available, wrote conflict markers`));
900
+ }
901
+ break;
902
+ case 'skip':
903
+ skippedCount++;
904
+ break;
905
+ case 'overwrite':
906
+ fs.writeFileSync(destPath, tContent, 'utf-8');
907
+ overwrittenCount++;
908
+ break;
909
+ case 'merge': {
910
+ const result = tryAutoMerge(iContent, tContent);
911
+ fs.writeFileSync(destPath, result.merged, 'utf-8');
912
+ if (result.hasConflicts) {
913
+ conflictCount++;
914
+ console.info(chalk.yellow(` ⚠️ ${file} — merge has conflicts`));
915
+ }
916
+ else {
917
+ mergedCount++;
918
+ console.info(chalk.green(` ✓ ${file} — merged cleanly`));
919
+ }
920
+ break;
921
+ }
922
+ case 'conflict':
923
+ default:
924
+ fs.writeFileSync(destPath, createConflictContent(iContent, tContent), 'utf-8');
925
+ conflictCount++;
926
+ break;
927
+ }
928
+ }
929
+ for (const file of toAdd) {
930
+ const destPath = path.join(instanceDir, file);
931
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
932
+ fs.copyFileSync(path.join(templateDir, file), destPath);
933
+ }
934
+ console.info(chalk.bold('\n✅ Template update complete!\n'));
935
+ const stats = [];
936
+ if (overwrittenCount > 0)
937
+ stats.push(`${overwrittenCount} overwritten`);
938
+ if (smartCount > 0)
939
+ stats.push(`${smartCount} smart-merged`);
940
+ if (mergedCount > 0)
941
+ stats.push(`${mergedCount} merged`);
942
+ if (conflictCount > 0)
943
+ stats.push(`${conflictCount} with conflicts`);
944
+ if (skippedCount > 0)
945
+ stats.push(`${skippedCount} skipped`);
946
+ if (toAdd.length > 0)
947
+ stats.push(`${toAdd.length} added`);
948
+ console.info(` ${stats.join(' | ')}\n`);
949
+ if (conflictCount > 0) {
950
+ console.info(chalk.yellow(`⚠️ ${conflictCount} files have conflict markers. ` +
951
+ `Search for "<<<<<<< INSTANCE" to resolve them.\n`));
952
+ }
953
+ }
954
+ else {
955
+ console.info(chalk.gray('(dry-run mode, no template changes made)\n'));
956
+ }
957
+ }
958
+ // 2. Sync .claude context (regardless of whether template had changes)
959
+ yield syncClaudeContext(instanceDir, dryRun);
960
+ // 3. If web/server package.json changed during template update, install deps
961
+ yield maybeInstallChangedDeps(pkgSnapshots, dryRun);
23
962
  });
24
963
  }
25
964
  }