@hobui/viui-cli 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -139
- package/dist/adapters/adapter-registry.d.ts +12 -0
- package/dist/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/adapters/adapter-registry.js +49 -0
- package/dist/adapters/adapter-types.d.ts +20 -0
- package/dist/adapters/adapter-types.d.ts.map +1 -0
- package/dist/adapters/adapter-types.js +1 -0
- package/dist/adapters/aider-adapter.d.ts +3 -0
- package/dist/adapters/aider-adapter.d.ts.map +1 -0
- package/dist/adapters/aider-adapter.js +8 -0
- package/dist/adapters/claude-adapter.d.ts +3 -0
- package/dist/adapters/claude-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-adapter.js +15 -0
- package/dist/adapters/cline-adapter.d.ts +3 -0
- package/dist/adapters/cline-adapter.d.ts.map +1 -0
- package/dist/adapters/cline-adapter.js +8 -0
- package/dist/adapters/copilot-adapter.d.ts +5 -0
- package/dist/adapters/copilot-adapter.d.ts.map +1 -0
- package/dist/adapters/copilot-adapter.js +20 -0
- package/dist/adapters/cursor-adapter.d.ts +3 -0
- package/dist/adapters/cursor-adapter.d.ts.map +1 -0
- package/dist/adapters/cursor-adapter.js +18 -0
- package/dist/adapters/external/bolt-adapter.d.ts +3 -0
- package/dist/adapters/external/bolt-adapter.d.ts.map +1 -0
- package/dist/adapters/external/bolt-adapter.js +15 -0
- package/dist/adapters/external/chatgpt-adapter.d.ts +3 -0
- package/dist/adapters/external/chatgpt-adapter.d.ts.map +1 -0
- package/dist/adapters/external/chatgpt-adapter.js +14 -0
- package/dist/adapters/external/external-adapter-base.d.ts +15 -0
- package/dist/adapters/external/external-adapter-base.d.ts.map +1 -0
- package/dist/adapters/external/external-adapter-base.js +92 -0
- package/dist/adapters/external/gemini-adapter.d.ts +3 -0
- package/dist/adapters/external/gemini-adapter.d.ts.map +1 -0
- package/dist/adapters/external/gemini-adapter.js +14 -0
- package/dist/adapters/external/lovable-adapter.d.ts +3 -0
- package/dist/adapters/external/lovable-adapter.d.ts.map +1 -0
- package/dist/adapters/external/lovable-adapter.js +14 -0
- package/dist/adapters/external/v0-adapter.d.ts +3 -0
- package/dist/adapters/external/v0-adapter.d.ts.map +1 -0
- package/dist/adapters/external/v0-adapter.js +15 -0
- package/dist/adapters/windsurf-adapter.d.ts +3 -0
- package/dist/adapters/windsurf-adapter.d.ts.map +1 -0
- package/dist/adapters/windsurf-adapter.js +23 -0
- package/dist/assets/plugins/viui-conf/apply-theme-body.ts +23 -4
- package/dist/assets/plugins/viui-conf/defaults/README.md +2 -0
- package/dist/assets/plugins/viui-conf/defaults/app-bar.ts +1 -1
- package/dist/assets/plugins/viui-conf/defaults/buttons.ts +1 -1
- package/dist/assets/plugins/viui-conf/defaults/by-theme/minimalist-2.ts +1 -1
- package/dist/assets/plugins/viui-conf/defaults/cards.ts +1 -1
- package/dist/assets/plugins/viui-conf/defaults/expansion-panels.ts +16 -0
- package/dist/assets/plugins/viui-conf/defaults/index.ts +3 -0
- package/dist/assets/plugins/viui-conf/defaults/inputs.ts +11 -1
- package/dist/assets/plugins/viui-conf/design-tokens.ts +135 -0
- package/dist/assets/plugins/viui-conf/theme-base.ts +1 -1
- package/dist/assets/plugins/viui-conf/v-dark.ts +3 -5
- package/dist/assets/plugins/viui-conf/v-light.ts +3 -5
- package/dist/assets/plugins/vuetify.ts +36 -0
- package/dist/assets/prompt-data/components.json +106 -0
- package/dist/assets/prompt-data/tokens.json +83 -0
- package/dist/assets/themes/_bento-grid.scss +8 -0
- package/dist/assets/themes/_glassmorphism.scss +8 -0
- package/dist/assets/themes/_material.scss +8 -0
- package/dist/assets/themes/_minimalist-2.scss +375 -0
- package/dist/assets/themes/_minimalist.scss +9 -0
- package/dist/assets/themes/_neo-brutalism.scss +199 -0
- package/dist/assets/themes/bento-grid.scss +4 -0
- package/dist/assets/themes/glassmorphism.scss +4 -0
- package/dist/assets/themes/index.scss +11 -0
- package/dist/assets/themes/material.scss +4 -0
- package/dist/assets/themes/minimalist-2.scss +5 -0
- package/dist/assets/themes/minimalist.scss +4 -0
- package/dist/assets/themes/neo-brutalism.scss +5 -0
- package/dist/assets/viui-themes/_neo-brutalism.scss +70 -152
- package/dist/cli-paths.d.ts +7 -0
- package/dist/cli-paths.d.ts.map +1 -0
- package/dist/cli-paths.js +19 -0
- package/dist/cli.js +28 -450
- package/dist/cli.legacy.d.ts +3 -0
- package/dist/cli.legacy.d.ts.map +1 -0
- package/dist/cli.legacy.js +597 -0
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +152 -0
- package/dist/commands/config/config-export.d.ts +6 -0
- package/dist/commands/config/config-export.d.ts.map +1 -0
- package/dist/commands/config/config-export.js +23 -0
- package/dist/commands/config/config-health.d.ts +6 -0
- package/dist/commands/config/config-health.d.ts.map +1 -0
- package/dist/commands/config/config-health.js +42 -0
- package/dist/commands/config/config-import.d.ts +6 -0
- package/dist/commands/config/config-import.d.ts.map +1 -0
- package/dist/commands/config/config-import.js +63 -0
- package/dist/commands/config/config-rollback.d.ts +6 -0
- package/dist/commands/config/config-rollback.d.ts.map +1 -0
- package/dist/commands/config/config-rollback.js +47 -0
- package/dist/commands/config/config-setup.d.ts +6 -0
- package/dist/commands/config/config-setup.d.ts.map +1 -0
- package/dist/commands/config/config-setup.js +103 -0
- package/dist/commands/config/config-status.d.ts +6 -0
- package/dist/commands/config/config-status.d.ts.map +1 -0
- package/dist/commands/config/config-status.js +42 -0
- package/dist/commands/config/config-uninstall.d.ts +6 -0
- package/dist/commands/config/config-uninstall.d.ts.map +1 -0
- package/dist/commands/config/config-uninstall.js +74 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +19 -0
- package/dist/commands/docs.d.ts +3 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +17 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +93 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +183 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +73 -0
- package/dist/commands/theme.d.ts +3 -0
- package/dist/commands/theme.d.ts.map +1 -0
- package/dist/commands/theme.js +86 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +97 -0
- package/dist/prompts/prompt-builder.d.ts +4 -0
- package/dist/prompts/prompt-builder.d.ts.map +1 -0
- package/dist/prompts/prompt-builder.js +18 -0
- package/dist/prompts/prompt-data-loader.d.ts +11 -0
- package/dist/prompts/prompt-data-loader.d.ts.map +1 -0
- package/dist/prompts/prompt-data-loader.js +15 -0
- package/dist/prompts/prompt-sections/section-code-examples.d.ts +2 -0
- package/dist/prompts/prompt-sections/section-code-examples.d.ts.map +1 -0
- package/dist/prompts/prompt-sections/section-code-examples.js +36 -0
- package/dist/prompts/prompt-sections/section-color-tokens.d.ts +2 -0
- package/dist/prompts/prompt-sections/section-color-tokens.d.ts.map +1 -0
- package/dist/prompts/prompt-sections/section-color-tokens.js +19 -0
- package/dist/prompts/prompt-sections/section-component-map.d.ts +3 -0
- package/dist/prompts/prompt-sections/section-component-map.d.ts.map +1 -0
- package/dist/prompts/prompt-sections/section-component-map.js +12 -0
- package/dist/prompts/prompt-sections/section-typography-spacing.d.ts +2 -0
- package/dist/prompts/prompt-sections/section-typography-spacing.d.ts.map +1 -0
- package/dist/prompts/prompt-sections/section-typography-spacing.js +29 -0
- package/dist/services/backup-service.d.ts +7 -0
- package/dist/services/backup-service.d.ts.map +1 -0
- package/dist/services/backup-service.js +54 -0
- package/dist/services/config-service.d.ts +17 -0
- package/dist/services/config-service.d.ts.map +1 -0
- package/dist/services/config-service.js +64 -0
- package/dist/services/diff-engine.d.ts +13 -0
- package/dist/services/diff-engine.d.ts.map +1 -0
- package/dist/services/diff-engine.js +59 -0
- package/dist/services/ide-detector.d.ts +9 -0
- package/dist/services/ide-detector.d.ts.map +1 -0
- package/dist/services/ide-detector.js +113 -0
- package/dist/services/ide-detector.spec.d.ts +2 -0
- package/dist/services/ide-detector.spec.d.ts.map +1 -0
- package/dist/services/ide-detector.spec.js +108 -0
- package/dist/services/lock-file-service.d.ts +15 -0
- package/dist/services/lock-file-service.d.ts.map +1 -0
- package/dist/services/lock-file-service.js +74 -0
- package/dist/services/mcp-config-reader.d.ts +11 -0
- package/dist/services/mcp-config-reader.d.ts.map +1 -0
- package/dist/services/mcp-config-reader.js +40 -0
- package/dist/services/mcp-config-reader.spec.d.ts +2 -0
- package/dist/services/mcp-config-reader.spec.d.ts.map +1 -0
- package/dist/services/mcp-config-reader.spec.js +125 -0
- package/dist/services/mcp-config-writer.d.ts +11 -0
- package/dist/services/mcp-config-writer.d.ts.map +1 -0
- package/dist/services/mcp-config-writer.js +98 -0
- package/dist/services/mcp-config-writer.spec.d.ts +2 -0
- package/dist/services/mcp-config-writer.spec.d.ts.map +1 -0
- package/dist/services/mcp-config-writer.spec.js +162 -0
- package/dist/services/merge-engine.d.ts +12 -0
- package/dist/services/merge-engine.d.ts.map +1 -0
- package/dist/services/merge-engine.js +54 -0
- package/dist/services/vuetify-scaffold-service.d.ts +5 -0
- package/dist/services/vuetify-scaffold-service.d.ts.map +1 -0
- package/dist/services/vuetify-scaffold-service.js +67 -0
- package/dist/templates/vuetify-plugin.d.ts +90 -0
- package/dist/templates/vuetify-plugin.d.ts.map +1 -0
- package/dist/templates/vuetify-plugin.js +33 -0
- package/dist/types/command-types.d.ts +15 -0
- package/dist/types/command-types.d.ts.map +1 -0
- package/dist/types/command-types.js +2 -0
- package/dist/types/config-types.d.ts +29 -0
- package/dist/types/config-types.d.ts.map +1 -0
- package/dist/types/config-types.js +10 -0
- package/dist/types/ide-types.d.ts +29 -0
- package/dist/types/ide-types.d.ts.map +1 -0
- package/dist/types/ide-types.js +4 -0
- package/dist/types/lock-file-types.d.ts +27 -0
- package/dist/types/lock-file-types.d.ts.map +1 -0
- package/dist/types/lock-file-types.js +2 -0
- package/dist/utils/diff-display.d.ts +18 -0
- package/dist/utils/diff-display.d.ts.map +1 -0
- package/dist/utils/diff-display.js +61 -0
- package/dist/utils/fs-safe.d.ts +9 -0
- package/dist/utils/fs-safe.d.ts.map +1 -0
- package/dist/utils/fs-safe.js +44 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +28 -0
- package/dist/utils/open-browser.d.ts +3 -0
- package/dist/utils/open-browser.d.ts.map +1 -0
- package/dist/utils/open-browser.js +13 -0
- package/package.json +11 -6
- package/dist/assets/cursor/.design-system-version +0 -1
- package/dist/assets/cursor/commands/audit-accessibility.md +0 -25
- package/dist/assets/cursor/commands/audit-ui.md +0 -35
- package/dist/assets/cursor/commands/component.md +0 -18
- package/dist/assets/cursor/commands/fix-storybook.md +0 -24
- package/dist/assets/cursor/commands/generate-component-from-figma.md +0 -26
- package/dist/assets/cursor/commands/generate-page-from-figma.md +0 -26
- package/dist/assets/cursor/plans/DESIGN_SYSTEM_PLAN.md +0 -177
- package/dist/assets/cursor/plans/PLANS_INDEX.md +0 -35
- package/dist/assets/cursor/rules/accessibility-contrast.mdc +0 -38
- package/dist/assets/cursor/rules/bem-class-style.mdc +0 -107
- package/dist/assets/cursor/rules/component-naming.mdc +0 -57
- package/dist/assets/cursor/rules/design-system-component-library.mdc +0 -59
- package/dist/assets/cursor/rules/design-system-workflow.mdc +0 -48
- package/dist/assets/cursor/rules/figma-mapping.mdc +0 -37
- package/dist/assets/cursor/rules/icons.mdc +0 -42
- package/dist/assets/cursor/rules/project-structure.mdc +0 -137
- package/dist/assets/cursor/rules/storybook-component-template.mdc +0 -103
- package/dist/assets/cursor/rules/storybook.mdc +0 -68
- package/dist/assets/cursor/rules/tokens.mdc +0 -32
- package/dist/assets/cursor/rules/viui-themes.mdc +0 -53
- package/dist/assets/cursor/rules/vuetify-layout.mdc +0 -52
- package/dist/assets/cursor/skills/accessibility.md +0 -75
- package/dist/assets/cursor/skills/design-system-thinking.md +0 -40
- package/dist/assets/cursor/skills/figma-interpretation.md +0 -38
- package/dist/assets/cursor/skills/vue-vuetify-design-system-architect.md +0 -60
- package/dist/assets/cursor/sync-manifest.json +0 -6
- package/dist/assets/viui-themes/bento-grid-global.scss +0 -5
- package/dist/assets/viui-themes/glassmorphism-global.scss +0 -5
- package/dist/assets/viui-themes/material-global.scss +0 -5
- package/dist/assets/viui-themes/minimalist-2-global.scss +0 -5
- package/dist/assets/viui-themes/minimalist-global.scss +0 -5
- package/dist/assets/viui-themes/neo-brutalism-global.scss +0 -5
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LockFile } from '../types/lock-file-types.js';
|
|
2
|
+
/** Get lock file path for a project */
|
|
3
|
+
export declare function getLockFilePath(cwd: string): string;
|
|
4
|
+
/** Compute SHA-512 integrity hash for a file */
|
|
5
|
+
export declare function computeIntegrity(filePath: string): string;
|
|
6
|
+
/** Read and parse .viui-lock.json */
|
|
7
|
+
export declare function readLockFile(cwd: string): LockFile | null;
|
|
8
|
+
/** Write .viui-lock.json with current timestamp */
|
|
9
|
+
export declare function writeLockFile(cwd: string, lockFile: LockFile): void;
|
|
10
|
+
/** Create a fresh lock file with default values */
|
|
11
|
+
export declare function createLockFile(cliVersion: string, theme: string, styleImport: 'scss' | 'css'): LockFile;
|
|
12
|
+
/** Verify integrity of installed files against lock file checksums.
|
|
13
|
+
* v1: check file existence only. Roadmap: hash verification via computeIntegrity(). */
|
|
14
|
+
export declare function verifyIntegrity(cwd: string, lockFile: LockFile): boolean;
|
|
15
|
+
//# sourceMappingURL=lock-file-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-file-service.d.ts","sourceRoot":"","sources":["../../src/services/lock-file-service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAO3D,uCAAuC;AACvC,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,gDAAgD;AAChD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED,qCAAqC;AACrC,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAGzD;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAKnE;AAED,mDAAmD;AACnD,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,KAAK,GAC1B,QAAQ,CAqBV;AAED;wFACwF;AACxF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAsBxE"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readJsonSafe, writeFileSafe } from '../utils/fs-safe.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
const LOCK_FILENAME = '.viui-lock.json';
|
|
7
|
+
const LOCKFILE_VERSION = 1;
|
|
8
|
+
/** Get lock file path for a project */
|
|
9
|
+
export function getLockFilePath(cwd) {
|
|
10
|
+
return path.join(cwd, LOCK_FILENAME);
|
|
11
|
+
}
|
|
12
|
+
/** Compute SHA-512 integrity hash for a file */
|
|
13
|
+
export function computeIntegrity(filePath) {
|
|
14
|
+
if (!fs.existsSync(filePath))
|
|
15
|
+
return '';
|
|
16
|
+
const content = fs.readFileSync(filePath);
|
|
17
|
+
const hash = crypto.createHash('sha512').update(content).digest('base64');
|
|
18
|
+
return `sha512-${hash}`;
|
|
19
|
+
}
|
|
20
|
+
/** Read and parse .viui-lock.json */
|
|
21
|
+
export function readLockFile(cwd) {
|
|
22
|
+
const lockPath = getLockFilePath(cwd);
|
|
23
|
+
return readJsonSafe(lockPath);
|
|
24
|
+
}
|
|
25
|
+
/** Write .viui-lock.json with current timestamp */
|
|
26
|
+
export function writeLockFile(cwd, lockFile) {
|
|
27
|
+
lockFile.metadata.generatedAt = new Date().toISOString();
|
|
28
|
+
const content = JSON.stringify(lockFile, null, 2) + '\n';
|
|
29
|
+
writeFileSafe(getLockFilePath(cwd), content);
|
|
30
|
+
logger.success(`Updated ${LOCK_FILENAME}`);
|
|
31
|
+
}
|
|
32
|
+
/** Create a fresh lock file with default values */
|
|
33
|
+
export function createLockFile(cliVersion, theme, styleImport) {
|
|
34
|
+
return {
|
|
35
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
36
|
+
tokens: {
|
|
37
|
+
version: '0.1.0',
|
|
38
|
+
integrity: '',
|
|
39
|
+
baseVersion: '0.1.0',
|
|
40
|
+
},
|
|
41
|
+
themes: {
|
|
42
|
+
[theme]: {
|
|
43
|
+
version: '0.0.1',
|
|
44
|
+
integrity: '',
|
|
45
|
+
baseVersion: '0.0.1',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
config: { theme, styleImport },
|
|
49
|
+
metadata: {
|
|
50
|
+
cliVersion,
|
|
51
|
+
generatedAt: new Date().toISOString(),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Verify integrity of installed files against lock file checksums.
|
|
56
|
+
* v1: check file existence only. Roadmap: hash verification via computeIntegrity(). */
|
|
57
|
+
export function verifyIntegrity(cwd, lockFile) {
|
|
58
|
+
logger.dim(`Lock file version: ${lockFile.lockfileVersion}`);
|
|
59
|
+
const theme = lockFile.config.theme;
|
|
60
|
+
const missing = [];
|
|
61
|
+
// Check token files exist
|
|
62
|
+
if (lockFile.tokens.integrity && !fs.existsSync(path.join(cwd, 'node_modules', '@hobui', 'tokens'))) {
|
|
63
|
+
missing.push('@hobui/tokens');
|
|
64
|
+
}
|
|
65
|
+
// Check theme entry exists in lock
|
|
66
|
+
if (theme && !lockFile.themes[theme]) {
|
|
67
|
+
missing.push(`theme "${theme}" not in lock file`);
|
|
68
|
+
}
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
missing.forEach((m) => logger.warn(`Missing: ${m}`));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IdeInfo } from '../types/ide-types.js';
|
|
2
|
+
export interface McpConfigEntry {
|
|
3
|
+
serverName: string;
|
|
4
|
+
serverUrl: string;
|
|
5
|
+
transport?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Đọc MCP config từ project hoặc global path của IDE */
|
|
8
|
+
export declare function readMcpConfig(ide: IdeInfo, useGlobal?: boolean): McpConfigEntry[];
|
|
9
|
+
/** Kiểm tra IDE đã có viui MCP config chưa */
|
|
10
|
+
export declare function hasViuiConfig(ide: IdeInfo, useGlobal?: boolean): boolean;
|
|
11
|
+
//# sourceMappingURL=mcp-config-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-config-reader.d.ts","sourceRoot":"","sources":["../../src/services/mcp-config-reader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAEpD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwBD,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,UAAQ,GAAG,cAAc,EAAE,CAK/E;AAED,8CAA8C;AAC9C,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,UAAQ,GAAG,OAAO,CAGtE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-config-reader.ts — Đọc và parse MCP config từ IDE config files
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
/** Đọc file JSON an toàn, trả {} nếu không tồn tại hoặc parse lỗi */
|
|
6
|
+
function safeReadJson(filePath) {
|
|
7
|
+
if (!fs.existsSync(filePath))
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Trích xuất MCP server entries từ config object theo format IDE */
|
|
17
|
+
function extractServers(data, ide) {
|
|
18
|
+
const topKey = ide.configFormat === 'vscode' ? 'servers' : 'mcpServers';
|
|
19
|
+
const servers = data[topKey];
|
|
20
|
+
if (!servers || typeof servers !== 'object')
|
|
21
|
+
return [];
|
|
22
|
+
return Object.entries(servers).map(([name, cfg]) => ({
|
|
23
|
+
serverName: name,
|
|
24
|
+
serverUrl: cfg.serverUrl || cfg.url || '',
|
|
25
|
+
transport: cfg.type,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/** Đọc MCP config từ project hoặc global path của IDE */
|
|
29
|
+
export function readMcpConfig(ide, useGlobal = false) {
|
|
30
|
+
const configPath = useGlobal ? ide.globalConfigPath : ide.configPath;
|
|
31
|
+
if (!configPath || configPath === '')
|
|
32
|
+
return [];
|
|
33
|
+
const data = safeReadJson(configPath);
|
|
34
|
+
return extractServers(data, ide);
|
|
35
|
+
}
|
|
36
|
+
/** Kiểm tra IDE đã có viui MCP config chưa */
|
|
37
|
+
export function hasViuiConfig(ide, useGlobal = false) {
|
|
38
|
+
const entries = readMcpConfig(ide, useGlobal);
|
|
39
|
+
return entries.some(e => e.serverName === 'viui' || e.serverName === 'inet-viui');
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-config-reader.spec.d.ts","sourceRoot":"","sources":["../../src/services/mcp-config-reader.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readMcpConfig, hasViuiConfig } from './mcp-config-reader.js';
|
|
6
|
+
// ── Helpers ──
|
|
7
|
+
const tmpDir = path.join(os.tmpdir(), '.tmp-test-reader');
|
|
8
|
+
function makeIde(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id: 'cursor',
|
|
11
|
+
name: 'Cursor',
|
|
12
|
+
detected: true,
|
|
13
|
+
configPath: path.join(tmpDir, 'mcp.json'),
|
|
14
|
+
globalConfigPath: null,
|
|
15
|
+
configFormat: 'standard',
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function writeJson(filePath, data) {
|
|
20
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
21
|
+
fs.writeFileSync(filePath, JSON.stringify(data), 'utf8');
|
|
22
|
+
}
|
|
23
|
+
// ── Tests ──
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
describe('readMcpConfig', () => {
|
|
31
|
+
it('returns [] when file does not exist', () => {
|
|
32
|
+
const ide = makeIde({ configPath: path.join(tmpDir, 'nonexistent.json') });
|
|
33
|
+
expect(readMcpConfig(ide)).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it('returns [] when file has invalid JSON', () => {
|
|
36
|
+
const filePath = path.join(tmpDir, 'bad.json');
|
|
37
|
+
fs.writeFileSync(filePath, '{not valid json', 'utf8');
|
|
38
|
+
const ide = makeIde({ configPath: filePath });
|
|
39
|
+
expect(readMcpConfig(ide)).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
it('returns [] when configPath is empty', () => {
|
|
42
|
+
const ide = makeIde({ configPath: '' });
|
|
43
|
+
expect(readMcpConfig(ide)).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
it('parses standard format (mcpServers key)', () => {
|
|
46
|
+
const data = {
|
|
47
|
+
mcpServers: {
|
|
48
|
+
'inet-viui': { url: 'https://viui.inet.vn/mcp', type: 'http' },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
writeJson(path.join(tmpDir, 'mcp.json'), data);
|
|
52
|
+
const entries = readMcpConfig(makeIde());
|
|
53
|
+
expect(entries).toHaveLength(1);
|
|
54
|
+
expect(entries[0].serverName).toBe('inet-viui');
|
|
55
|
+
expect(entries[0].serverUrl).toBe('https://viui.inet.vn/mcp');
|
|
56
|
+
expect(entries[0].transport).toBe('http');
|
|
57
|
+
});
|
|
58
|
+
it('parses vscode format (servers key)', () => {
|
|
59
|
+
const filePath = path.join(tmpDir, 'vscode-mcp.json');
|
|
60
|
+
const data = {
|
|
61
|
+
servers: {
|
|
62
|
+
viui: { url: 'https://viui.inet.vn/mcp', type: 'http' },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
writeJson(filePath, data);
|
|
66
|
+
const ide = makeIde({ configPath: filePath, configFormat: 'vscode' });
|
|
67
|
+
const entries = readMcpConfig(ide);
|
|
68
|
+
expect(entries).toHaveLength(1);
|
|
69
|
+
expect(entries[0].serverName).toBe('viui');
|
|
70
|
+
expect(entries[0].serverUrl).toBe('https://viui.inet.vn/mcp');
|
|
71
|
+
});
|
|
72
|
+
it('extracts serverUrl for gemini format', () => {
|
|
73
|
+
const filePath = path.join(tmpDir, 'gemini.json');
|
|
74
|
+
const data = {
|
|
75
|
+
mcpServers: {
|
|
76
|
+
'inet-viui': { serverUrl: 'https://viui.inet.vn/mcp' },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
writeJson(filePath, data);
|
|
80
|
+
const ide = makeIde({ configPath: filePath, configFormat: 'gemini' });
|
|
81
|
+
const entries = readMcpConfig(ide);
|
|
82
|
+
expect(entries).toHaveLength(1);
|
|
83
|
+
expect(entries[0].serverUrl).toBe('https://viui.inet.vn/mcp');
|
|
84
|
+
});
|
|
85
|
+
it('parses multiple server entries', () => {
|
|
86
|
+
const data = {
|
|
87
|
+
mcpServers: {
|
|
88
|
+
'inet-viui': { url: 'https://viui.inet.vn/mcp' },
|
|
89
|
+
'other-server': { url: 'http://other.com/mcp' },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
writeJson(path.join(tmpDir, 'mcp.json'), data);
|
|
93
|
+
const entries = readMcpConfig(makeIde());
|
|
94
|
+
expect(entries).toHaveLength(2);
|
|
95
|
+
});
|
|
96
|
+
it('reads global config when useGlobal=true', () => {
|
|
97
|
+
const globalPath = path.join(tmpDir, 'global-mcp.json');
|
|
98
|
+
const data = { mcpServers: { 'inet-viui': { url: 'https://viui.inet.vn/mcp' } } };
|
|
99
|
+
writeJson(globalPath, data);
|
|
100
|
+
const ide = makeIde({ globalConfigPath: globalPath });
|
|
101
|
+
const entries = readMcpConfig(ide, true);
|
|
102
|
+
expect(entries).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('hasViuiConfig', () => {
|
|
106
|
+
it('returns true when "inet-viui" entry exists', () => {
|
|
107
|
+
const data = { mcpServers: { 'inet-viui': { url: 'https://viui.inet.vn/mcp' } } };
|
|
108
|
+
writeJson(path.join(tmpDir, 'mcp.json'), data);
|
|
109
|
+
expect(hasViuiConfig(makeIde())).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('returns true when "viui" entry exists', () => {
|
|
112
|
+
const data = { mcpServers: { viui: { url: 'https://viui.inet.vn/mcp' } } };
|
|
113
|
+
writeJson(path.join(tmpDir, 'mcp.json'), data);
|
|
114
|
+
expect(hasViuiConfig(makeIde())).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('returns false when no viui entry exists', () => {
|
|
117
|
+
const data = { mcpServers: { 'other-server': { url: 'http://other.com' } } };
|
|
118
|
+
writeJson(path.join(tmpDir, 'mcp.json'), data);
|
|
119
|
+
expect(hasViuiConfig(makeIde())).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
it('returns false when file does not exist', () => {
|
|
122
|
+
const ide = makeIde({ configPath: path.join(tmpDir, 'nope.json') });
|
|
123
|
+
expect(hasViuiConfig(ide)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IdeInfo, McpConfig } from '../types/ide-types.js';
|
|
2
|
+
/** Tạo đối tượng config MCP cho IDE cụ thể */
|
|
3
|
+
export declare function generateMcpConfig(ide: IdeInfo, useRemote: boolean): McpConfig;
|
|
4
|
+
/** Tạo nội dung JSON theo định dạng của từng IDE */
|
|
5
|
+
export declare function buildConfigObject(ide: IdeInfo, mcpConfig: McpConfig): Record<string, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Ghi cấu hình MCP vào file config của IDE.
|
|
8
|
+
* Tự động backup file cũ và merge thay vì ghi đè hoàn toàn.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeConfig(ide: IdeInfo, mcpConfig: McpConfig, useGlobal?: boolean): void;
|
|
11
|
+
//# sourceMappingURL=mcp-config-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-config-writer.d.ts","sourceRoot":"","sources":["../../src/services/mcp-config-writer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAK/D,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,SAAS,CAM7E;AAED,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgC7F;AA0BD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,UAAQ,GAAG,IAAI,CA0BvF"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-config-writer.ts — Tạo và ghi cấu hình MCP server vào từng IDE
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const REMOTE_URL = 'https://viui.inet.vn/mcp';
|
|
7
|
+
const LOCAL_URL = 'http://localhost:3200/mcp';
|
|
8
|
+
/** Tạo đối tượng config MCP cho IDE cụ thể */
|
|
9
|
+
export function generateMcpConfig(ide, useRemote) {
|
|
10
|
+
return {
|
|
11
|
+
serverName: ide.id === 'vscode' ? 'viui' : 'inet-viui',
|
|
12
|
+
serverUrl: useRemote ? REMOTE_URL : LOCAL_URL,
|
|
13
|
+
transport: 'http',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Tạo nội dung JSON theo định dạng của từng IDE */
|
|
17
|
+
export function buildConfigObject(ide, mcpConfig) {
|
|
18
|
+
switch (ide.configFormat) {
|
|
19
|
+
case 'vscode':
|
|
20
|
+
// .vscode/mcp.json: { servers: { name: { url, type } } }
|
|
21
|
+
return {
|
|
22
|
+
servers: {
|
|
23
|
+
[mcpConfig.serverName]: {
|
|
24
|
+
url: mcpConfig.serverUrl,
|
|
25
|
+
type: mcpConfig.transport,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
case 'gemini':
|
|
30
|
+
// Gemini: { mcpServers: { name: { serverUrl } } }
|
|
31
|
+
return {
|
|
32
|
+
mcpServers: {
|
|
33
|
+
[mcpConfig.serverName]: {
|
|
34
|
+
serverUrl: mcpConfig.serverUrl,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
default:
|
|
39
|
+
// Standard: Cursor, Claude, Windsurf, Trae
|
|
40
|
+
return {
|
|
41
|
+
mcpServers: {
|
|
42
|
+
[mcpConfig.serverName]: {
|
|
43
|
+
url: mcpConfig.serverUrl,
|
|
44
|
+
type: mcpConfig.transport,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Deep merge hai object */
|
|
51
|
+
function deepMerge(base, override) {
|
|
52
|
+
const result = { ...base };
|
|
53
|
+
for (const key of Object.keys(override)) {
|
|
54
|
+
const baseVal = result[key];
|
|
55
|
+
const overVal = override[key];
|
|
56
|
+
if (baseVal !== null && typeof baseVal === 'object' && !Array.isArray(baseVal) &&
|
|
57
|
+
overVal !== null && typeof overVal === 'object' && !Array.isArray(overVal)) {
|
|
58
|
+
result[key] = deepMerge(baseVal, overVal);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
result[key] = overVal;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** Backup file hiện có nếu tồn tại */
|
|
67
|
+
function backupExisting(filePath) {
|
|
68
|
+
if (!fs.existsSync(filePath))
|
|
69
|
+
return;
|
|
70
|
+
fs.copyFileSync(filePath, `${filePath}.bak`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Ghi cấu hình MCP vào file config của IDE.
|
|
74
|
+
* Tự động backup file cũ và merge thay vì ghi đè hoàn toàn.
|
|
75
|
+
*/
|
|
76
|
+
export function writeConfig(ide, mcpConfig, useGlobal = false) {
|
|
77
|
+
const configPath = useGlobal ? ide.globalConfigPath : ide.configPath;
|
|
78
|
+
if (!configPath) {
|
|
79
|
+
throw new Error(`${ide.name} không hỗ trợ ${useGlobal ? 'global' : 'project'} config`);
|
|
80
|
+
}
|
|
81
|
+
const configDir = path.dirname(configPath);
|
|
82
|
+
if (!fs.existsSync(configDir)) {
|
|
83
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
backupExisting(configPath);
|
|
86
|
+
let existing = {};
|
|
87
|
+
if (fs.existsSync(configPath)) {
|
|
88
|
+
try {
|
|
89
|
+
existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// File hỏng — bắt đầu mới, backup đã được tạo
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const newConfig = buildConfigObject(ide, mcpConfig);
|
|
96
|
+
const merged = deepMerge(existing, newConfig);
|
|
97
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-config-writer.spec.d.ts","sourceRoot":"","sources":["../../src/services/mcp-config-writer.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { generateMcpConfig, buildConfigObject, writeConfig } from './mcp-config-writer.js';
|
|
6
|
+
// ── Helpers ──
|
|
7
|
+
function makeIde(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'cursor',
|
|
10
|
+
name: 'Cursor',
|
|
11
|
+
detected: true,
|
|
12
|
+
configPath: '/project/.cursor/mcp.json',
|
|
13
|
+
globalConfigPath: null,
|
|
14
|
+
configFormat: 'standard',
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function makeMcpConfig(overrides = {}) {
|
|
19
|
+
return {
|
|
20
|
+
serverName: 'inet-viui',
|
|
21
|
+
serverUrl: 'https://viui.inet.vn/mcp',
|
|
22
|
+
transport: 'http',
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// ── Tests ──
|
|
27
|
+
describe('generateMcpConfig', () => {
|
|
28
|
+
it('returns "viui" serverName for vscode', () => {
|
|
29
|
+
const ide = makeIde({ id: 'vscode', configFormat: 'vscode' });
|
|
30
|
+
const config = generateMcpConfig(ide, true);
|
|
31
|
+
expect(config.serverName).toBe('viui');
|
|
32
|
+
});
|
|
33
|
+
it('returns "inet-viui" serverName for non-vscode IDEs', () => {
|
|
34
|
+
for (const id of ['cursor', 'claude', 'windsurf', 'trae', 'gemini']) {
|
|
35
|
+
const ide = makeIde({ id });
|
|
36
|
+
const config = generateMcpConfig(ide, true);
|
|
37
|
+
expect(config.serverName).toBe('inet-viui');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
it('uses remote URL when useRemote=true', () => {
|
|
41
|
+
const config = generateMcpConfig(makeIde(), true);
|
|
42
|
+
expect(config.serverUrl).toBe('https://viui.inet.vn/mcp');
|
|
43
|
+
});
|
|
44
|
+
it('uses local URL when useRemote=false', () => {
|
|
45
|
+
const config = generateMcpConfig(makeIde(), false);
|
|
46
|
+
expect(config.serverUrl).toBe('http://localhost:3200/mcp');
|
|
47
|
+
});
|
|
48
|
+
it('always sets transport to "http"', () => {
|
|
49
|
+
const config = generateMcpConfig(makeIde(), true);
|
|
50
|
+
expect(config.transport).toBe('http');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('buildConfigObject', () => {
|
|
54
|
+
it('builds vscode format with "servers" key', () => {
|
|
55
|
+
const ide = makeIde({ configFormat: 'vscode' });
|
|
56
|
+
const mcpConfig = makeMcpConfig({ serverName: 'viui' });
|
|
57
|
+
const result = buildConfigObject(ide, mcpConfig);
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
servers: {
|
|
60
|
+
viui: {
|
|
61
|
+
url: 'https://viui.inet.vn/mcp',
|
|
62
|
+
type: 'http',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('builds gemini format with "serverUrl" field', () => {
|
|
68
|
+
const ide = makeIde({ id: 'gemini', configFormat: 'gemini' });
|
|
69
|
+
const mcpConfig = makeMcpConfig();
|
|
70
|
+
const result = buildConfigObject(ide, mcpConfig);
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
mcpServers: {
|
|
73
|
+
'inet-viui': {
|
|
74
|
+
serverUrl: 'https://viui.inet.vn/mcp',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('builds standard format for cursor/claude/windsurf/trae', () => {
|
|
80
|
+
const ide = makeIde({ configFormat: 'standard' });
|
|
81
|
+
const mcpConfig = makeMcpConfig();
|
|
82
|
+
const result = buildConfigObject(ide, mcpConfig);
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
mcpServers: {
|
|
85
|
+
'inet-viui': {
|
|
86
|
+
url: 'https://viui.inet.vn/mcp',
|
|
87
|
+
type: 'http',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it('gemini format does NOT include type field', () => {
|
|
93
|
+
const ide = makeIde({ configFormat: 'gemini' });
|
|
94
|
+
const result = buildConfigObject(ide, makeMcpConfig());
|
|
95
|
+
const server = result.mcpServers['inet-viui'];
|
|
96
|
+
expect(server.type).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('writeConfig', () => {
|
|
100
|
+
const tmpDir = path.join(os.tmpdir(), '.tmp-test-writeconfig');
|
|
101
|
+
const configPath = path.join(tmpDir, '.cursor', 'mcp.json');
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
104
|
+
});
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
it('creates directory and writes config when file does not exist', () => {
|
|
109
|
+
const newConfigPath = path.join(tmpDir, 'new-dir', 'mcp.json');
|
|
110
|
+
const ide = makeIde({ configPath: newConfigPath });
|
|
111
|
+
const mcpConfig = makeMcpConfig();
|
|
112
|
+
writeConfig(ide, mcpConfig);
|
|
113
|
+
expect(fs.existsSync(newConfigPath)).toBe(true);
|
|
114
|
+
const written = JSON.parse(fs.readFileSync(newConfigPath, 'utf8'));
|
|
115
|
+
expect(written.mcpServers['inet-viui'].url).toBe('https://viui.inet.vn/mcp');
|
|
116
|
+
});
|
|
117
|
+
it('backs up existing file before writing', () => {
|
|
118
|
+
fs.writeFileSync(configPath, '{"existing": true}', 'utf8');
|
|
119
|
+
const ide = makeIde({ configPath });
|
|
120
|
+
writeConfig(ide, makeMcpConfig());
|
|
121
|
+
expect(fs.existsSync(`${configPath}.bak`)).toBe(true);
|
|
122
|
+
const backup = JSON.parse(fs.readFileSync(`${configPath}.bak`, 'utf8'));
|
|
123
|
+
expect(backup.existing).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('merges with existing config without losing data', () => {
|
|
126
|
+
const existing = { mcpServers: { 'other-server': { url: 'http://other' } }, custom: 'keep' };
|
|
127
|
+
fs.writeFileSync(configPath, JSON.stringify(existing), 'utf8');
|
|
128
|
+
const ide = makeIde({ configPath });
|
|
129
|
+
writeConfig(ide, makeMcpConfig());
|
|
130
|
+
const result = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
131
|
+
expect(result.custom).toBe('keep');
|
|
132
|
+
expect(result.mcpServers['other-server']).toBeDefined();
|
|
133
|
+
expect(result.mcpServers['inet-viui']).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
it('writes JSON with 2-space indent and trailing newline', () => {
|
|
136
|
+
const ide = makeIde({ configPath });
|
|
137
|
+
writeConfig(ide, makeMcpConfig());
|
|
138
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
139
|
+
expect(raw).toContain(' ');
|
|
140
|
+
expect(raw.endsWith('\n')).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it('throws when configPath is not available', () => {
|
|
143
|
+
const ide = makeIde({ configPath: '' });
|
|
144
|
+
expect(() => writeConfig(ide, makeMcpConfig())).toThrow();
|
|
145
|
+
});
|
|
146
|
+
it('uses globalConfigPath when useGlobal=true', () => {
|
|
147
|
+
const globalPath = path.join(tmpDir, 'global', 'mcp.json');
|
|
148
|
+
const ide = makeIde({ globalConfigPath: globalPath });
|
|
149
|
+
writeConfig(ide, makeMcpConfig(), true);
|
|
150
|
+
expect(fs.existsSync(globalPath)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
it('handles corrupted existing JSON gracefully', () => {
|
|
153
|
+
fs.writeFileSync(configPath, '{broken json!!!', 'utf8');
|
|
154
|
+
const ide = makeIde({ configPath });
|
|
155
|
+
writeConfig(ide, makeMcpConfig());
|
|
156
|
+
// Backup should still be created
|
|
157
|
+
expect(fs.existsSync(`${configPath}.bak`)).toBe(true);
|
|
158
|
+
// New config written successfully (corrupted data discarded)
|
|
159
|
+
const result = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
160
|
+
expect(result.mcpServers['inet-viui']).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
export type MergeChoice = 'ours' | 'theirs' | 'skip';
|
|
3
|
+
/** v1 merge: diff + accept/reject per file (ours/theirs/skip) */
|
|
4
|
+
export declare function mergeFile(filePath: string, sourceContent: string, rl: readline.Interface): Promise<MergeChoice>;
|
|
5
|
+
/** Apply merge choice to a file. baseDir constrains writes to prevent traversal. */
|
|
6
|
+
export declare function applyMergeChoice(filePath: string, sourceContent: string, choice: MergeChoice, baseDir?: string): void;
|
|
7
|
+
/** Non-interactive merge: apply all source files (used with --apply flag) */
|
|
8
|
+
export declare function mergeApplyAll(files: Array<{
|
|
9
|
+
filePath: string;
|
|
10
|
+
sourceContent: string;
|
|
11
|
+
}>): void;
|
|
12
|
+
//# sourceMappingURL=merge-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-engine.d.ts","sourceRoot":"","sources":["../../src/services/merge-engine.ts"],"names":[],"mappings":"AAEA,OAAO,QAAQ,MAAM,wBAAwB,CAAA;AAK7C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEpD,iEAAiE;AACjE,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,EAAE,EAAE,QAAQ,CAAC,SAAS,GACrB,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CAwBN;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,GACxD,IAAI,CAIN"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readFileOrEmpty } from './diff-engine.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
/** v1 merge: diff + accept/reject per file (ours/theirs/skip) */
|
|
7
|
+
export async function mergeFile(filePath, sourceContent, rl) {
|
|
8
|
+
const localContent = readFileOrEmpty(filePath);
|
|
9
|
+
if (localContent === sourceContent) {
|
|
10
|
+
return 'skip'; // No changes needed
|
|
11
|
+
}
|
|
12
|
+
logger.br();
|
|
13
|
+
logger.header(`Conflict: ${path.basename(filePath)}`);
|
|
14
|
+
console.log(chalk.dim(' Local (ours) differs from source (theirs)'));
|
|
15
|
+
logger.br();
|
|
16
|
+
const answer = await rl.question(` Choose: ${chalk.cyan('(o)urs')} keep local / ${chalk.green('(t)heirs')} use source / ${chalk.yellow('(s)kip')} ignore: `);
|
|
17
|
+
const choice = answer.trim().toLowerCase();
|
|
18
|
+
if (choice === 't' || choice === 'theirs')
|
|
19
|
+
return 'theirs';
|
|
20
|
+
if (choice === 'o' || choice === 'ours')
|
|
21
|
+
return 'ours';
|
|
22
|
+
return 'skip';
|
|
23
|
+
}
|
|
24
|
+
/** Apply merge choice to a file. baseDir constrains writes to prevent traversal. */
|
|
25
|
+
export function applyMergeChoice(filePath, sourceContent, choice, baseDir) {
|
|
26
|
+
const resolved = path.resolve(filePath);
|
|
27
|
+
// Validate path stays within base directory to prevent path traversal
|
|
28
|
+
if (baseDir) {
|
|
29
|
+
const resolvedBase = path.resolve(baseDir);
|
|
30
|
+
if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) {
|
|
31
|
+
logger.error(`Path traversal rejected: ${filePath} is outside ${baseDir}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
switch (choice) {
|
|
36
|
+
case 'theirs':
|
|
37
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
38
|
+
fs.writeFileSync(resolved, sourceContent, 'utf8');
|
|
39
|
+
logger.success(`Applied source version: ${path.basename(filePath)}`);
|
|
40
|
+
break;
|
|
41
|
+
case 'ours':
|
|
42
|
+
logger.dim(`Kept local version: ${path.basename(filePath)}`);
|
|
43
|
+
break;
|
|
44
|
+
case 'skip':
|
|
45
|
+
logger.dim(`Skipped: ${path.basename(filePath)}`);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Non-interactive merge: apply all source files (used with --apply flag) */
|
|
50
|
+
export function mergeApplyAll(files) {
|
|
51
|
+
for (const { filePath, sourceContent } of files) {
|
|
52
|
+
applyMergeChoice(filePath, sourceContent, 'theirs');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Scaffold src/plugins/vuetify.ts from template (wired to viui-conf) */
|
|
2
|
+
export declare function scaffoldVuetifyPlugin(cwd: string, assetsDir: string): void;
|
|
3
|
+
/** Patch main.ts to import and use the scaffolded vuetify plugin */
|
|
4
|
+
export declare function patchMainEntry(cwd: string): void;
|
|
5
|
+
//# sourceMappingURL=vuetify-scaffold-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vuetify-scaffold-service.d.ts","sourceRoot":"","sources":["../../src/services/vuetify-scaffold-service.ts"],"names":[],"mappings":"AAKA,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAgB1E;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkDhD"}
|