@zeyue0329/xiaoma-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.releaserc.json +18 -0
  2. package/.vscode/settings.json +44 -0
  3. package/CONTRIBUTING.md +209 -0
  4. package/LICENSE +21 -0
  5. package/QUICK-START.md +173 -0
  6. package/README.md +532 -0
  7. package/common/tasks/create-doc.md +101 -0
  8. package/common/tasks/execute-checklist.md +93 -0
  9. package/common/utils/bmad-doc-template.md +325 -0
  10. package/common/utils/workflow-management.md +69 -0
  11. package/dist/agents/analyst.txt +2882 -0
  12. package/dist/agents/architect.txt +3543 -0
  13. package/dist/agents/dev.txt +428 -0
  14. package/dist/agents/pm.txt +2229 -0
  15. package/dist/agents/po.txt +1364 -0
  16. package/dist/agents/qa.txt +386 -0
  17. package/dist/agents/sm.txt +668 -0
  18. package/dist/agents/ux-expert.txt +701 -0
  19. package/dist/agents/xiaoma-master.txt +8756 -0
  20. package/dist/agents/xiaoma-orchestrator.txt +1490 -0
  21. package/dist/teams/team-all.txt +11062 -0
  22. package/dist/teams/team-fullstack.txt +10392 -0
  23. package/dist/teams/team-ide-minimal.txt +3507 -0
  24. package/dist/teams/team-no-ui.txt +8951 -0
  25. package/docs/GUIDING-PRINCIPLES.md +91 -0
  26. package/docs/core-architecture.md +219 -0
  27. package/docs/expansion-packs.md +280 -0
  28. package/docs/versioning-and-releases.md +77 -0
  29. package/docs/versions.md +48 -0
  30. package/expansion-packs/README.md +3 -0
  31. package/package.json +80 -0
  32. package/tools/bmad-npx-wrapper.js +39 -0
  33. package/tools/builders/web-builder.js +681 -0
  34. package/tools/bump-all-versions.js +106 -0
  35. package/tools/bump-expansion-version.js +83 -0
  36. package/tools/cli.js +152 -0
  37. package/tools/flattener/main.js +570 -0
  38. package/tools/installer/README.md +8 -0
  39. package/tools/installer/bin/xiaoma.js +326 -0
  40. package/tools/installer/config/ide-agent-config.yaml +58 -0
  41. package/tools/installer/config/install.config.yaml +113 -0
  42. package/tools/installer/lib/config-loader.js +253 -0
  43. package/tools/installer/lib/file-manager.js +411 -0
  44. package/tools/installer/lib/ide-base-setup.js +227 -0
  45. package/tools/installer/lib/ide-setup.js +1302 -0
  46. package/tools/installer/lib/installer.js +1772 -0
  47. package/tools/installer/lib/memory-profiler.js +224 -0
  48. package/tools/installer/lib/module-manager.js +110 -0
  49. package/tools/installer/lib/resource-locator.js +310 -0
  50. package/tools/installer/package-lock.json +906 -0
  51. package/tools/installer/package.json +43 -0
  52. package/tools/lib/dependency-resolver.js +179 -0
  53. package/tools/lib/yaml-utils.js +29 -0
  54. package/tools/md-assets/web-agent-startup-instructions.md +39 -0
  55. package/tools/semantic-release-sync-installer.js +30 -0
  56. package/tools/sync-installer-version.js +34 -0
  57. package/tools/update-expansion-version.js +54 -0
  58. package/tools/upgraders/v3-to-v4-upgrader.js +763 -0
  59. package/tools/version-bump.js +79 -0
  60. package/tools/xiaoma-npx-wrapper.js +39 -0
  61. package/tools/yaml-format.js +240 -0
  62. package/xiaoma-core/agent-teams/team-all.yaml +14 -0
  63. package/xiaoma-core/agent-teams/team-fullstack.yaml +18 -0
  64. package/xiaoma-core/agent-teams/team-ide-minimal.yaml +10 -0
  65. package/xiaoma-core/agent-teams/team-no-ui.yaml +13 -0
  66. package/xiaoma-core/agents/analyst.md +81 -0
  67. package/xiaoma-core/agents/architect.md +84 -0
  68. package/xiaoma-core/agents/dev.md +76 -0
  69. package/xiaoma-core/agents/pm.md +81 -0
  70. package/xiaoma-core/agents/po.md +76 -0
  71. package/xiaoma-core/agents/qa.md +69 -0
  72. package/xiaoma-core/agents/sm.md +62 -0
  73. package/xiaoma-core/agents/ux-expert.md +66 -0
  74. package/xiaoma-core/agents/xiaoma-master.md +108 -0
  75. package/xiaoma-core/agents/xiaoma-orchestrator.md +150 -0
  76. package/xiaoma-core/bmad-core/user-guide.md +0 -0
  77. package/xiaoma-core/checklists/architect-checklist.md +443 -0
  78. package/xiaoma-core/checklists/change-checklist.md +182 -0
  79. package/xiaoma-core/checklists/pm-checklist.md +375 -0
  80. package/xiaoma-core/checklists/po-master-checklist.md +441 -0
  81. package/xiaoma-core/checklists/story-dod-checklist.md +101 -0
  82. package/xiaoma-core/checklists/story-draft-checklist.md +156 -0
  83. package/xiaoma-core/core-config.yaml +20 -0
  84. package/xiaoma-core/data/brainstorming-techniques.md +36 -0
  85. package/xiaoma-core/data/elicitation-methods.md +134 -0
  86. package/xiaoma-core/data/technical-preferences.md +3 -0
  87. package/xiaoma-core/data/xiaoma-kb.md +803 -0
  88. package/xiaoma-core/enhanced-ide-development-workflow.md +43 -0
  89. package/xiaoma-core/tasks/advanced-elicitation.md +117 -0
  90. package/xiaoma-core/tasks/brownfield-create-epic.md +160 -0
  91. package/xiaoma-core/tasks/brownfield-create-story.md +147 -0
  92. package/xiaoma-core/tasks/correct-course.md +70 -0
  93. package/xiaoma-core/tasks/create-brownfield-story.md +304 -0
  94. package/xiaoma-core/tasks/create-deep-research-prompt.md +289 -0
  95. package/xiaoma-core/tasks/create-next-story.md +112 -0
  96. package/xiaoma-core/tasks/document-project.md +341 -0
  97. package/xiaoma-core/tasks/facilitate-brainstorming-session.md +136 -0
  98. package/xiaoma-core/tasks/generate-ai-frontend-prompt.md +51 -0
  99. package/xiaoma-core/tasks/index-docs.md +179 -0
  100. package/xiaoma-core/tasks/kb-mode-interaction.md +75 -0
  101. package/xiaoma-core/tasks/review-story.md +145 -0
  102. package/xiaoma-core/tasks/shard-doc.md +187 -0
  103. package/xiaoma-core/tasks/validate-next-story.md +134 -0
  104. package/xiaoma-core/templates/architecture-tmpl.yaml +650 -0
  105. package/xiaoma-core/templates/brainstorming-output-tmpl.yaml +156 -0
  106. package/xiaoma-core/templates/brownfield-architecture-tmpl.yaml +476 -0
  107. package/xiaoma-core/templates/brownfield-prd-tmpl.yaml +280 -0
  108. package/xiaoma-core/templates/competitor-analysis-tmpl.yaml +293 -0
  109. package/xiaoma-core/templates/front-end-architecture-tmpl.yaml +206 -0
  110. package/xiaoma-core/templates/front-end-spec-tmpl.yaml +349 -0
  111. package/xiaoma-core/templates/fullstack-architecture-tmpl.yaml +805 -0
  112. package/xiaoma-core/templates/market-research-tmpl.yaml +252 -0
  113. package/xiaoma-core/templates/prd-tmpl.yaml +202 -0
  114. package/xiaoma-core/templates/project-brief-tmpl.yaml +221 -0
  115. package/xiaoma-core/templates/story-tmpl.yaml +137 -0
  116. package/xiaoma-core/user-guide.md +251 -0
  117. package/xiaoma-core/workflows/brownfield-fullstack.yaml +297 -0
  118. package/xiaoma-core/workflows/brownfield-service.yaml +187 -0
  119. package/xiaoma-core/workflows/brownfield-ui.yaml +197 -0
  120. package/xiaoma-core/workflows/greenfield-fullstack.yaml +240 -0
  121. package/xiaoma-core/workflows/greenfield-service.yaml +206 -0
  122. package/xiaoma-core/workflows/greenfield-ui.yaml +235 -0
  123. package/xiaoma-core/working-in-the-brownfield.md +364 -0
@@ -0,0 +1,253 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const { extractYamlFromAgent } = require('../../lib/yaml-utils');
5
+
6
+ class ConfigLoader {
7
+ constructor() {
8
+ this.configPath = path.join(__dirname, '..', 'config', 'install.config.yaml');
9
+ this.config = null;
10
+ }
11
+
12
+ async load() {
13
+ if (this.config) return this.config;
14
+
15
+ try {
16
+ const configContent = await fs.readFile(this.configPath, 'utf8');
17
+ this.config = yaml.load(configContent);
18
+ return this.config;
19
+ } catch (error) {
20
+ throw new Error(`Failed to load configuration: ${error.message}`);
21
+ }
22
+ }
23
+
24
+ async getInstallationOptions() {
25
+ const config = await this.load();
26
+ return config['installation-options'] || {};
27
+ }
28
+
29
+ async getAvailableAgents() {
30
+ const agentsDir = path.join(this.getBmadCorePath(), 'agents');
31
+
32
+ try {
33
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
34
+ const agents = [];
35
+
36
+ for (const entry of entries) {
37
+ if (entry.isFile() && entry.name.endsWith('.md')) {
38
+ const agentPath = path.join(agentsDir, entry.name);
39
+ const agentId = path.basename(entry.name, '.md');
40
+
41
+ try {
42
+ const agentContent = await fs.readFile(agentPath, 'utf8');
43
+
44
+ // Extract YAML block from agent file
45
+ const yamlContentText = extractYamlFromAgent(agentContent);
46
+ if (yamlContentText) {
47
+ const yamlContent = yaml.load(yamlContentText);
48
+ const agentConfig = yamlContent.agent || {};
49
+
50
+ agents.push({
51
+ id: agentId,
52
+ name: agentConfig.title || agentConfig.name || agentId,
53
+ file: `xiaoma-core/agents/${entry.name}`,
54
+ description: agentConfig.whenToUse || 'No description available'
55
+ });
56
+ }
57
+ } catch (error) {
58
+ console.warn(`Failed to read agent ${entry.name}: ${error.message}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Sort agents by name for consistent display
64
+ agents.sort((a, b) => a.name.localeCompare(b.name));
65
+
66
+ return agents;
67
+ } catch (error) {
68
+ console.warn(`Failed to read agents directory: ${error.message}`);
69
+ return [];
70
+ }
71
+ }
72
+
73
+ async getAvailableExpansionPacks() {
74
+ const expansionPacksDir = path.join(this.getBmadCorePath(), '..', 'expansion-packs');
75
+
76
+ try {
77
+ const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
78
+ const expansionPacks = [];
79
+
80
+ for (const entry of entries) {
81
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
82
+ const packPath = path.join(expansionPacksDir, entry.name);
83
+ const configPath = path.join(packPath, 'config.yaml');
84
+
85
+ try {
86
+ // Read config.yaml
87
+ const configContent = await fs.readFile(configPath, 'utf8');
88
+ const config = yaml.load(configContent);
89
+
90
+ expansionPacks.push({
91
+ id: entry.name,
92
+ name: config.name || entry.name,
93
+ description: config['short-title'] || config.description || 'No description available',
94
+ fullDescription: config.description || config['short-title'] || 'No description available',
95
+ version: config.version || '1.0.0',
96
+ author: config.author || 'XiaoMa Team',
97
+ packPath: packPath,
98
+ dependencies: config.dependencies?.agents || []
99
+ });
100
+ } catch (error) {
101
+ // Fallback if config.yaml doesn't exist or can't be read
102
+ console.warn(`Failed to read config for expansion pack ${entry.name}: ${error.message}`);
103
+
104
+ // Try to derive info from directory name as fallback
105
+ const name = entry.name
106
+ .split('-')
107
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
108
+ .join(' ');
109
+
110
+ expansionPacks.push({
111
+ id: entry.name,
112
+ name: name,
113
+ description: 'No description available',
114
+ fullDescription: 'No description available',
115
+ version: '1.0.0',
116
+ author: 'XiaoMa Team',
117
+ packPath: packPath,
118
+ dependencies: []
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ return expansionPacks;
125
+ } catch (error) {
126
+ console.warn(`Failed to read expansion packs directory: ${error.message}`);
127
+ return [];
128
+ }
129
+ }
130
+
131
+ async getAgentDependencies(agentId) {
132
+ // Use DependencyResolver to dynamically parse agent dependencies
133
+ const DependencyResolver = require('../../lib/dependency-resolver');
134
+ const resolver = new DependencyResolver(path.join(__dirname, '..', '..', '..'));
135
+
136
+ const agentDeps = await resolver.resolveAgentDependencies(agentId);
137
+
138
+ // Convert to flat list of file paths
139
+ const depPaths = [];
140
+
141
+ // Core files and utilities are included automatically by DependencyResolver
142
+
143
+ // Add agent file itself is already handled by installer
144
+
145
+ // Add all resolved resources
146
+ for (const resource of agentDeps.resources) {
147
+ const filePath = `.xiaoma-core/${resource.type}/${resource.id}.md`;
148
+ if (!depPaths.includes(filePath)) {
149
+ depPaths.push(filePath);
150
+ }
151
+ }
152
+
153
+ return depPaths;
154
+ }
155
+
156
+ async getIdeConfiguration(ide) {
157
+ const config = await this.load();
158
+ const ideConfigs = config['ide-configurations'] || {};
159
+ return ideConfigs[ide] || null;
160
+ }
161
+
162
+ getBmadCorePath() {
163
+ // Get the path to xiaoma-core relative to the installer (now under tools)
164
+ return path.join(__dirname, '..', '..', '..', 'xiaoma-core');
165
+ }
166
+
167
+ getDistPath() {
168
+ // Get the path to dist directory relative to the installer
169
+ return path.join(__dirname, '..', '..', '..', 'dist');
170
+ }
171
+
172
+ getAgentPath(agentId) {
173
+ return path.join(this.getBmadCorePath(), 'agents', `${agentId}.md`);
174
+ }
175
+
176
+ async getAvailableTeams() {
177
+ const teamsDir = path.join(this.getBmadCorePath(), 'agent-teams');
178
+
179
+ try {
180
+ const entries = await fs.readdir(teamsDir, { withFileTypes: true });
181
+ const teams = [];
182
+
183
+ for (const entry of entries) {
184
+ if (entry.isFile() && entry.name.endsWith('.yaml')) {
185
+ const teamPath = path.join(teamsDir, entry.name);
186
+
187
+ try {
188
+ const teamContent = await fs.readFile(teamPath, 'utf8');
189
+ const teamConfig = yaml.load(teamContent);
190
+
191
+ if (teamConfig.bundle) {
192
+ teams.push({
193
+ id: path.basename(entry.name, '.yaml'),
194
+ name: teamConfig.bundle.name || entry.name,
195
+ description: teamConfig.bundle.description || 'Team configuration',
196
+ icon: teamConfig.bundle.icon || '📋'
197
+ });
198
+ }
199
+ } catch (error) {
200
+ console.warn(`Warning: Could not load team config ${entry.name}: ${error.message}`);
201
+ }
202
+ }
203
+ }
204
+
205
+ return teams;
206
+ } catch (error) {
207
+ console.warn(`Warning: Could not scan teams directory: ${error.message}`);
208
+ return [];
209
+ }
210
+ }
211
+
212
+ getTeamPath(teamId) {
213
+ return path.join(this.getBmadCorePath(), 'agent-teams', `${teamId}.yaml`);
214
+ }
215
+
216
+ async getTeamDependencies(teamId) {
217
+ // Use DependencyResolver to dynamically parse team dependencies
218
+ const DependencyResolver = require('../../lib/dependency-resolver');
219
+ const resolver = new DependencyResolver(path.join(__dirname, '..', '..', '..'));
220
+
221
+ try {
222
+ const teamDeps = await resolver.resolveTeamDependencies(teamId);
223
+
224
+ // Convert to flat list of file paths
225
+ const depPaths = [];
226
+
227
+ // Add team config file
228
+ depPaths.push(`.xiaoma-core/agent-teams/${teamId}.yaml`);
229
+
230
+ // Add all agents
231
+ for (const agent of teamDeps.agents) {
232
+ const filePath = `.xiaoma-core/agents/${agent.id}.md`;
233
+ if (!depPaths.includes(filePath)) {
234
+ depPaths.push(filePath);
235
+ }
236
+ }
237
+
238
+ // Add all resolved resources
239
+ for (const resource of teamDeps.resources) {
240
+ const filePath = `.xiaoma-core/${resource.type}/${resource.id}.${resource.type === 'workflows' ? 'yaml' : 'md'}`;
241
+ if (!depPaths.includes(filePath)) {
242
+ depPaths.push(filePath);
243
+ }
244
+ }
245
+
246
+ return depPaths;
247
+ } catch (error) {
248
+ throw new Error(`Failed to resolve team dependencies for ${teamId}: ${error.message}`);
249
+ }
250
+ }
251
+ }
252
+
253
+ module.exports = new ConfigLoader();
@@ -0,0 +1,411 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const crypto = require("crypto");
4
+ const yaml = require("js-yaml");
5
+ const chalk = require("chalk");
6
+ const { createReadStream, createWriteStream, promises: fsPromises } = require('fs');
7
+ const { pipeline } = require('stream/promises');
8
+ const resourceLocator = require('./resource-locator');
9
+
10
+ class FileManager {
11
+ constructor() {
12
+ this.manifestDir = ".xiaoma-core";
13
+ this.manifestFile = "install-manifest.yaml";
14
+ }
15
+
16
+ async copyFile(source, destination) {
17
+ try {
18
+ await fs.ensureDir(path.dirname(destination));
19
+
20
+ // Use streaming for large files (> 10MB)
21
+ const stats = await fs.stat(source);
22
+ if (stats.size > 10 * 1024 * 1024) {
23
+ await pipeline(
24
+ createReadStream(source),
25
+ createWriteStream(destination)
26
+ );
27
+ } else {
28
+ await fs.copy(source, destination);
29
+ }
30
+ return true;
31
+ } catch (error) {
32
+ console.error(chalk.red(`Failed to copy ${source}:`), error.message);
33
+ return false;
34
+ }
35
+ }
36
+
37
+ async copyDirectory(source, destination) {
38
+ try {
39
+ await fs.ensureDir(destination);
40
+
41
+ // Use streaming copy for large directories
42
+ const files = await resourceLocator.findFiles('**/*', {
43
+ cwd: source,
44
+ nodir: true
45
+ });
46
+
47
+ // Process files in batches to avoid memory issues
48
+ const batchSize = 50;
49
+ for (let i = 0; i < files.length; i += batchSize) {
50
+ const batch = files.slice(i, i + batchSize);
51
+ await Promise.all(
52
+ batch.map(file =>
53
+ this.copyFile(
54
+ path.join(source, file),
55
+ path.join(destination, file)
56
+ )
57
+ )
58
+ );
59
+ }
60
+ return true;
61
+ } catch (error) {
62
+ console.error(
63
+ chalk.red(`Failed to copy directory ${source}:`),
64
+ error.message
65
+ );
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async copyGlobPattern(pattern, sourceDir, destDir, rootValue = null) {
71
+ const files = await resourceLocator.findFiles(pattern, { cwd: sourceDir });
72
+ const copied = [];
73
+
74
+ for (const file of files) {
75
+ const sourcePath = path.join(sourceDir, file);
76
+ const destPath = path.join(destDir, file);
77
+
78
+ // Use root replacement if rootValue is provided and file needs it
79
+ const needsRootReplacement = rootValue && (file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml'));
80
+
81
+ let success = false;
82
+ if (needsRootReplacement) {
83
+ success = await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue);
84
+ } else {
85
+ success = await this.copyFile(sourcePath, destPath);
86
+ }
87
+
88
+ if (success) {
89
+ copied.push(file);
90
+ }
91
+ }
92
+
93
+ return copied;
94
+ }
95
+
96
+ async calculateFileHash(filePath) {
97
+ try {
98
+ // Use streaming for hash calculation to reduce memory usage
99
+ const stream = createReadStream(filePath);
100
+ const hash = crypto.createHash("sha256");
101
+
102
+ for await (const chunk of stream) {
103
+ hash.update(chunk);
104
+ }
105
+
106
+ return hash.digest("hex").slice(0, 16);
107
+ } catch (error) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ async createManifest(installDir, config, files) {
113
+ const manifestPath = path.join(
114
+ installDir,
115
+ this.manifestDir,
116
+ this.manifestFile
117
+ );
118
+
119
+ // Read version from package.json
120
+ let coreVersion = "unknown";
121
+ try {
122
+ const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
123
+ const packageJson = require(packagePath);
124
+ coreVersion = packageJson.version;
125
+ } catch (error) {
126
+ console.warn("Could not read version from package.json, using 'unknown'");
127
+ }
128
+
129
+ const manifest = {
130
+ version: coreVersion,
131
+ installed_at: new Date().toISOString(),
132
+ install_type: config.installType,
133
+ agent: config.agent || null,
134
+ ides_setup: config.ides || [],
135
+ expansion_packs: config.expansionPacks || [],
136
+ files: [],
137
+ };
138
+
139
+ // Add file information
140
+ for (const file of files) {
141
+ const filePath = path.join(installDir, file);
142
+ const hash = await this.calculateFileHash(filePath);
143
+
144
+ manifest.files.push({
145
+ path: file,
146
+ hash: hash,
147
+ modified: false,
148
+ });
149
+ }
150
+
151
+ // Write manifest
152
+ await fs.ensureDir(path.dirname(manifestPath));
153
+ await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
154
+
155
+ return manifest;
156
+ }
157
+
158
+ async readManifest(installDir) {
159
+ const manifestPath = path.join(
160
+ installDir,
161
+ this.manifestDir,
162
+ this.manifestFile
163
+ );
164
+
165
+ try {
166
+ const content = await fs.readFile(manifestPath, "utf8");
167
+ return yaml.load(content);
168
+ } catch (error) {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ async readExpansionPackManifest(installDir, packId) {
174
+ const manifestPath = path.join(
175
+ installDir,
176
+ `.${packId}`,
177
+ this.manifestFile
178
+ );
179
+
180
+ try {
181
+ const content = await fs.readFile(manifestPath, "utf8");
182
+ return yaml.load(content);
183
+ } catch (error) {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ async checkModifiedFiles(installDir, manifest) {
189
+ const modified = [];
190
+
191
+ for (const file of manifest.files) {
192
+ const filePath = path.join(installDir, file.path);
193
+ const currentHash = await this.calculateFileHash(filePath);
194
+
195
+ if (currentHash && currentHash !== file.hash) {
196
+ modified.push(file.path);
197
+ }
198
+ }
199
+
200
+ return modified;
201
+ }
202
+
203
+ async checkFileIntegrity(installDir, manifest) {
204
+ const result = {
205
+ missing: [],
206
+ modified: []
207
+ };
208
+
209
+ for (const file of manifest.files) {
210
+ const filePath = path.join(installDir, file.path);
211
+
212
+ // Skip checking the manifest file itself - it will always be different due to timestamps
213
+ if (file.path.endsWith('install-manifest.yaml')) {
214
+ continue;
215
+ }
216
+
217
+ if (!(await this.pathExists(filePath))) {
218
+ result.missing.push(file.path);
219
+ } else {
220
+ const currentHash = await this.calculateFileHash(filePath);
221
+ if (currentHash && currentHash !== file.hash) {
222
+ result.modified.push(file.path);
223
+ }
224
+ }
225
+ }
226
+
227
+ return result;
228
+ }
229
+
230
+ async backupFile(filePath) {
231
+ const backupPath = filePath + ".bak";
232
+ let counter = 1;
233
+ let finalBackupPath = backupPath;
234
+
235
+ // Find a unique backup filename
236
+ while (await fs.pathExists(finalBackupPath)) {
237
+ finalBackupPath = `${filePath}.bak${counter}`;
238
+ counter++;
239
+ }
240
+
241
+ await fs.copy(filePath, finalBackupPath);
242
+ return finalBackupPath;
243
+ }
244
+
245
+ async ensureDirectory(dirPath) {
246
+ try {
247
+ await fs.ensureDir(dirPath);
248
+ return true;
249
+ } catch (error) {
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ async pathExists(filePath) {
255
+ return fs.pathExists(filePath);
256
+ }
257
+
258
+ async readFile(filePath) {
259
+ return fs.readFile(filePath, "utf8");
260
+ }
261
+
262
+ async writeFile(filePath, content) {
263
+ await fs.ensureDir(path.dirname(filePath));
264
+ await fs.writeFile(filePath, content);
265
+ }
266
+
267
+ async removeDirectory(dirPath) {
268
+ await fs.remove(dirPath);
269
+ }
270
+
271
+ async createExpansionPackManifest(installDir, packId, config, files) {
272
+ const manifestPath = path.join(
273
+ installDir,
274
+ `.${packId}`,
275
+ this.manifestFile
276
+ );
277
+
278
+ const manifest = {
279
+ version: config.expansionPackVersion || require("../../../package.json").version,
280
+ installed_at: new Date().toISOString(),
281
+ install_type: config.installType,
282
+ expansion_pack_id: config.expansionPackId,
283
+ expansion_pack_name: config.expansionPackName,
284
+ ides_setup: config.ides || [],
285
+ files: [],
286
+ };
287
+
288
+ // Add file information
289
+ for (const file of files) {
290
+ const filePath = path.join(installDir, file);
291
+ const hash = await this.calculateFileHash(filePath);
292
+
293
+ manifest.files.push({
294
+ path: file,
295
+ hash: hash,
296
+ modified: false,
297
+ });
298
+ }
299
+
300
+ // Write manifest
301
+ await fs.ensureDir(path.dirname(manifestPath));
302
+ await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
303
+
304
+ return manifest;
305
+ }
306
+
307
+ async modifyCoreConfig(installDir, config) {
308
+ const coreConfigPath = path.join(installDir, '.xiaoma-core', 'core-config.yaml');
309
+
310
+ try {
311
+ // Read the existing core-config.yaml
312
+ const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
313
+ const coreConfig = yaml.load(coreConfigContent);
314
+
315
+ // Modify sharding settings if provided
316
+ if (config.prdSharded !== undefined) {
317
+ coreConfig.prd.prdSharded = config.prdSharded;
318
+ }
319
+
320
+ if (config.architectureSharded !== undefined) {
321
+ coreConfig.architecture.architectureSharded = config.architectureSharded;
322
+ }
323
+
324
+ // Write back the modified config
325
+ await fs.writeFile(coreConfigPath, yaml.dump(coreConfig, { indent: 2 }));
326
+
327
+ return true;
328
+ } catch (error) {
329
+ console.error(chalk.red(`Failed to modify core-config.yaml:`), error.message);
330
+ return false;
331
+ }
332
+ }
333
+
334
+ async copyFileWithRootReplacement(source, destination, rootValue) {
335
+ try {
336
+ // Check file size to determine if we should stream
337
+ const stats = await fs.stat(source);
338
+
339
+ if (stats.size > 5 * 1024 * 1024) { // 5MB threshold
340
+ // Use streaming for large files
341
+ const { Transform } = require('stream');
342
+ const replaceStream = new Transform({
343
+ transform(chunk, encoding, callback) {
344
+ const modified = chunk.toString().replace(/\{root\}/g, rootValue);
345
+ callback(null, modified);
346
+ }
347
+ });
348
+
349
+ await this.ensureDirectory(path.dirname(destination));
350
+ await pipeline(
351
+ createReadStream(source, { encoding: 'utf8' }),
352
+ replaceStream,
353
+ createWriteStream(destination, { encoding: 'utf8' })
354
+ );
355
+ } else {
356
+ // Regular approach for smaller files
357
+ const content = await fsPromises.readFile(source, 'utf8');
358
+ const updatedContent = content.replace(/\{root\}/g, rootValue);
359
+ await this.ensureDirectory(path.dirname(destination));
360
+ await fsPromises.writeFile(destination, updatedContent, 'utf8');
361
+ }
362
+
363
+ return true;
364
+ } catch (error) {
365
+ console.error(chalk.red(`Failed to copy ${source} with root replacement:`), error.message);
366
+ return false;
367
+ }
368
+ }
369
+
370
+ async copyDirectoryWithRootReplacement(source, destination, rootValue, fileExtensions = ['.md', '.yaml', '.yml']) {
371
+ try {
372
+ await this.ensureDirectory(destination);
373
+
374
+ // Get all files in source directory
375
+ const files = await resourceLocator.findFiles('**/*', {
376
+ cwd: source,
377
+ nodir: true
378
+ });
379
+
380
+ let replacedCount = 0;
381
+
382
+ for (const file of files) {
383
+ const sourcePath = path.join(source, file);
384
+ const destPath = path.join(destination, file);
385
+
386
+ // Check if this file type should have {root} replacement
387
+ const shouldReplace = fileExtensions.some(ext => file.endsWith(ext));
388
+
389
+ if (shouldReplace) {
390
+ if (await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue)) {
391
+ replacedCount++;
392
+ }
393
+ } else {
394
+ // Regular copy for files that don't need replacement
395
+ await this.copyFile(sourcePath, destPath);
396
+ }
397
+ }
398
+
399
+ if (replacedCount > 0) {
400
+ console.log(chalk.dim(` Processed ${replacedCount} files with {root} replacement`));
401
+ }
402
+
403
+ return true;
404
+ } catch (error) {
405
+ console.error(chalk.red(`Failed to copy directory ${source} with root replacement:`), error.message);
406
+ return false;
407
+ }
408
+ }
409
+ }
410
+
411
+ module.exports = new FileManager();