dhurandhar 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.
- package/.dhurandhar-session-start.md +242 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/docs/ARCHITECTURE_V2.md +249 -0
- package/docs/DECISION_REGISTRY.md +357 -0
- package/docs/IMPLEMENTATION_PERSONAS.md +406 -0
- package/docs/PLUGGABLE_STRATEGIES.md +439 -0
- package/docs/SYSTEM_OBSERVER.md +433 -0
- package/docs/TEST_FIRST_AGILE.md +359 -0
- package/docs/architecture.md +279 -0
- package/docs/engineering-first-philosophy.md +263 -0
- package/docs/getting-started.md +218 -0
- package/docs/module-development.md +323 -0
- package/docs/strategy-example.md +299 -0
- package/docs/test-first-example.md +392 -0
- package/package.json +79 -0
- package/src/core/README.md +92 -0
- package/src/core/agent-instructions/backend-developer.md +412 -0
- package/src/core/agent-instructions/devops-engineer.md +372 -0
- package/src/core/agent-instructions/dhurandhar-council.md +547 -0
- package/src/core/agent-instructions/edge-case-hunter.md +322 -0
- package/src/core/agent-instructions/frontend-developer.md +494 -0
- package/src/core/agent-instructions/lead-system-architect.md +631 -0
- package/src/core/agent-instructions/system-observer.md +319 -0
- package/src/core/agent-instructions/test-architect.md +284 -0
- package/src/core/module.yaml +54 -0
- package/src/core/schemas/design-module-schema.yaml +995 -0
- package/src/core/schemas/system-design-map-schema.yaml +324 -0
- package/src/modules/example/README.md +130 -0
- package/src/modules/example/module.yaml +252 -0
- package/tools/cli/commands/audit.js +267 -0
- package/tools/cli/commands/config.js +113 -0
- package/tools/cli/commands/context.js +170 -0
- package/tools/cli/commands/decisions.js +398 -0
- package/tools/cli/commands/entity.js +218 -0
- package/tools/cli/commands/epic.js +125 -0
- package/tools/cli/commands/install.js +172 -0
- package/tools/cli/commands/module.js +109 -0
- package/tools/cli/commands/service.js +167 -0
- package/tools/cli/commands/story.js +225 -0
- package/tools/cli/commands/strategy.js +294 -0
- package/tools/cli/commands/test.js +277 -0
- package/tools/cli/commands/validate.js +107 -0
- package/tools/cli/dhurandhar.js +212 -0
- package/tools/lib/config-manager.js +170 -0
- package/tools/lib/filesystem.js +126 -0
- package/tools/lib/module-installer.js +61 -0
- package/tools/lib/module-manager.js +149 -0
- package/tools/lib/sdm-manager.js +982 -0
- package/tools/lib/test-engine.js +255 -0
- package/tools/lib/test-templates/api-client.template.js +100 -0
- package/tools/lib/test-templates/vitest.config.template.js +37 -0
- package/tools/lib/validators/config-validator.js +113 -0
- package/tools/lib/validators/module-validator.js +137 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager
|
|
3
|
+
* Handles loading, saving, and managing YAML-based configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
9
|
+
import yaml from 'yaml';
|
|
10
|
+
|
|
11
|
+
export class ConfigManager {
|
|
12
|
+
constructor(projectRoot) {
|
|
13
|
+
this.projectRoot = projectRoot;
|
|
14
|
+
this.configDir = join(projectRoot, '.dhurandhar');
|
|
15
|
+
this.configPath = join(this.configDir, 'config.yaml');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if configuration exists
|
|
20
|
+
*/
|
|
21
|
+
exists() {
|
|
22
|
+
return existsSync(this.configPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize configuration with user values
|
|
27
|
+
*/
|
|
28
|
+
async initialize(config) {
|
|
29
|
+
// Create config directory if it doesn't exist
|
|
30
|
+
if (!existsSync(this.configDir)) {
|
|
31
|
+
await mkdir(this.configDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultConfig = {
|
|
35
|
+
version: '0.1.0',
|
|
36
|
+
projectName: config.projectName || 'dhurandhar-project',
|
|
37
|
+
userName: config.userName || 'User',
|
|
38
|
+
outputFolder: config.outputFolder || '_dhurandhar-output',
|
|
39
|
+
modules: [],
|
|
40
|
+
settings: {
|
|
41
|
+
created: new Date().toISOString(),
|
|
42
|
+
lastModified: new Date().toISOString(),
|
|
43
|
+
},
|
|
44
|
+
variables: {
|
|
45
|
+
projectRoot: this.projectRoot,
|
|
46
|
+
configDir: this.configDir,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await this.save(defaultConfig);
|
|
51
|
+
return defaultConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load configuration from file
|
|
56
|
+
*/
|
|
57
|
+
async load() {
|
|
58
|
+
if (!this.exists()) {
|
|
59
|
+
throw new Error('Configuration file not found. Run "dhurandhar install" first.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const content = await readFile(this.configPath, 'utf-8');
|
|
63
|
+
const config = yaml.parse(content);
|
|
64
|
+
|
|
65
|
+
// Perform variable substitution
|
|
66
|
+
return this.substituteVariables(config);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Save configuration to file
|
|
71
|
+
*/
|
|
72
|
+
async save(config) {
|
|
73
|
+
const content = yaml.stringify(config, {
|
|
74
|
+
indent: 2,
|
|
75
|
+
lineWidth: 0,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await writeFile(this.configPath, content, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Update configuration
|
|
83
|
+
*/
|
|
84
|
+
async update(updates) {
|
|
85
|
+
const config = await this.load();
|
|
86
|
+
const merged = {
|
|
87
|
+
...config,
|
|
88
|
+
...updates,
|
|
89
|
+
settings: {
|
|
90
|
+
...config.settings,
|
|
91
|
+
lastModified: new Date().toISOString(),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await this.save(merged);
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reset configuration to defaults
|
|
101
|
+
*/
|
|
102
|
+
async reset() {
|
|
103
|
+
return this.initialize({});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get a configuration value with dot notation
|
|
108
|
+
* Example: get('settings.created')
|
|
109
|
+
*/
|
|
110
|
+
async get(path) {
|
|
111
|
+
const config = await this.load();
|
|
112
|
+
return this.getNestedValue(config, path);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set a configuration value with dot notation
|
|
117
|
+
*/
|
|
118
|
+
async set(path, value) {
|
|
119
|
+
const config = await this.load();
|
|
120
|
+
this.setNestedValue(config, path, value);
|
|
121
|
+
await this.save(config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Substitute variables in configuration
|
|
126
|
+
* Supports {variable} syntax
|
|
127
|
+
*/
|
|
128
|
+
substituteVariables(obj) {
|
|
129
|
+
if (typeof obj === 'string') {
|
|
130
|
+
return obj.replace(/\{([^}]+)\}/g, (match, key) => {
|
|
131
|
+
const value = this.getNestedValue(obj, key);
|
|
132
|
+
return value !== undefined ? value : match;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(obj)) {
|
|
137
|
+
return obj.map(item => this.substituteVariables(item));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (obj !== null && typeof obj === 'object') {
|
|
141
|
+
const result = {};
|
|
142
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
143
|
+
result[key] = this.substituteVariables(value);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return obj;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get nested value from object using dot notation
|
|
153
|
+
*/
|
|
154
|
+
getNestedValue(obj, path) {
|
|
155
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set nested value in object using dot notation
|
|
160
|
+
*/
|
|
161
|
+
setNestedValue(obj, path, value) {
|
|
162
|
+
const keys = path.split('.');
|
|
163
|
+
const lastKey = keys.pop();
|
|
164
|
+
const target = keys.reduce((current, key) => {
|
|
165
|
+
if (!current[key]) current[key] = {};
|
|
166
|
+
return current[key];
|
|
167
|
+
}, obj);
|
|
168
|
+
target[lastKey] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSystem Utilities
|
|
3
|
+
* Helper functions for file system operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { mkdir, readdir, stat, readFile, writeFile, rm } from 'fs/promises';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
|
|
11
|
+
export class FileSystem {
|
|
12
|
+
/**
|
|
13
|
+
* Ensure required directories exist
|
|
14
|
+
*/
|
|
15
|
+
static async ensureDirectories(projectRoot) {
|
|
16
|
+
const dirs = [
|
|
17
|
+
'.dhurandhar',
|
|
18
|
+
'.dhurandhar/modules',
|
|
19
|
+
'.dhurandhar/cache',
|
|
20
|
+
'_dhurandhar-output',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const dir of dirs) {
|
|
24
|
+
const fullPath = join(projectRoot, dir);
|
|
25
|
+
if (!existsSync(fullPath)) {
|
|
26
|
+
await mkdir(fullPath, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy directory recursively
|
|
33
|
+
*/
|
|
34
|
+
static async copyDir(src, dest) {
|
|
35
|
+
await mkdir(dest, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const srcPath = join(src, entry.name);
|
|
41
|
+
const destPath = join(dest, entry.name);
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
await FileSystem.copyDir(srcPath, destPath);
|
|
45
|
+
} else {
|
|
46
|
+
const content = await readFile(srcPath);
|
|
47
|
+
await writeFile(destPath, content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove directory recursively
|
|
54
|
+
*/
|
|
55
|
+
static async removeDir(path) {
|
|
56
|
+
if (existsSync(path)) {
|
|
57
|
+
await rm(path, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find files matching pattern
|
|
63
|
+
*/
|
|
64
|
+
static async findFiles(pattern, options = {}) {
|
|
65
|
+
return glob(pattern, {
|
|
66
|
+
cwd: options.cwd || process.cwd(),
|
|
67
|
+
ignore: options.ignore || ['node_modules/**', '.git/**'],
|
|
68
|
+
absolute: options.absolute !== false,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read JSON file
|
|
74
|
+
*/
|
|
75
|
+
static async readJSON(path) {
|
|
76
|
+
const content = await readFile(path, 'utf-8');
|
|
77
|
+
return JSON.parse(content);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Write JSON file
|
|
82
|
+
*/
|
|
83
|
+
static async writeJSON(path, data, options = {}) {
|
|
84
|
+
const content = JSON.stringify(data, null, options.indent || 2);
|
|
85
|
+
await writeFile(path, content + '\n', 'utf-8');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if path is a directory
|
|
90
|
+
*/
|
|
91
|
+
static async isDirectory(path) {
|
|
92
|
+
try {
|
|
93
|
+
const stats = await stat(path);
|
|
94
|
+
return stats.isDirectory();
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if path is a file
|
|
102
|
+
*/
|
|
103
|
+
static async isFile(path) {
|
|
104
|
+
try {
|
|
105
|
+
const stats = await stat(path);
|
|
106
|
+
return stats.isFile();
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get file size
|
|
114
|
+
*/
|
|
115
|
+
static async getSize(path) {
|
|
116
|
+
const stats = await stat(path);
|
|
117
|
+
return stats.size;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if file exists
|
|
122
|
+
*/
|
|
123
|
+
static exists(path) {
|
|
124
|
+
return existsSync(path);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Installer
|
|
3
|
+
* Handles bulk module installation during framework setup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModuleManager } from './module-manager.js';
|
|
7
|
+
|
|
8
|
+
export class ModuleInstaller {
|
|
9
|
+
constructor(projectRoot) {
|
|
10
|
+
this.projectRoot = projectRoot;
|
|
11
|
+
this.moduleManager = new ModuleManager(projectRoot);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Install multiple modules
|
|
16
|
+
*/
|
|
17
|
+
async installModules(moduleCodes) {
|
|
18
|
+
const results = {
|
|
19
|
+
installed: [],
|
|
20
|
+
failed: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (const moduleCode of moduleCodes) {
|
|
24
|
+
try {
|
|
25
|
+
await this.moduleManager.install(moduleCode);
|
|
26
|
+
results.installed.push(moduleCode);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
results.failed.push({
|
|
29
|
+
module: moduleCode,
|
|
30
|
+
error: error.message,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Uninstall multiple modules
|
|
40
|
+
*/
|
|
41
|
+
async uninstallModules(moduleCodes) {
|
|
42
|
+
const results = {
|
|
43
|
+
uninstalled: [],
|
|
44
|
+
failed: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (const moduleCode of moduleCodes) {
|
|
48
|
+
try {
|
|
49
|
+
await this.moduleManager.uninstall(moduleCode);
|
|
50
|
+
results.uninstalled.push(moduleCode);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
results.failed.push({
|
|
53
|
+
module: moduleCode,
|
|
54
|
+
error: error.message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Manager
|
|
3
|
+
* Handles module discovery, installation, and management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { readFile } from 'fs/promises';
|
|
10
|
+
import yaml from 'yaml';
|
|
11
|
+
import { FileSystem } from './filesystem.js';
|
|
12
|
+
import { ConfigManager } from './config-manager.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
export class ModuleManager {
|
|
18
|
+
constructor(projectRoot) {
|
|
19
|
+
this.projectRoot = projectRoot;
|
|
20
|
+
this.configManager = new ConfigManager(projectRoot);
|
|
21
|
+
this.modulesSourceDir = join(__dirname, '../../src/modules');
|
|
22
|
+
this.modulesInstallDir = join(projectRoot, '.dhurandhar/modules');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* List available modules from source
|
|
27
|
+
*/
|
|
28
|
+
async listAvailable() {
|
|
29
|
+
const moduleDirs = await FileSystem.findFiles('*/module.yaml', {
|
|
30
|
+
cwd: this.modulesSourceDir,
|
|
31
|
+
absolute: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const modules = [];
|
|
35
|
+
|
|
36
|
+
for (const modulePath of moduleDirs) {
|
|
37
|
+
const moduleDir = dirname(modulePath);
|
|
38
|
+
const fullPath = join(this.modulesSourceDir, modulePath);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
42
|
+
const metadata = yaml.parse(content);
|
|
43
|
+
|
|
44
|
+
modules.push({
|
|
45
|
+
code: metadata.code || moduleDir,
|
|
46
|
+
name: metadata.name || moduleDir,
|
|
47
|
+
description: metadata.description || 'No description available',
|
|
48
|
+
version: metadata.version || '1.0.0',
|
|
49
|
+
dependencies: metadata.dependencies || [],
|
|
50
|
+
path: moduleDir,
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn(`Warning: Could not load module ${moduleDir}:`, error.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return modules;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List installed modules
|
|
62
|
+
*/
|
|
63
|
+
async listInstalled() {
|
|
64
|
+
if (!existsSync(this.modulesInstallDir)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const moduleDirs = await FileSystem.findFiles('*/module.yaml', {
|
|
69
|
+
cwd: this.modulesInstallDir,
|
|
70
|
+
absolute: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return moduleDirs.map(path => dirname(path));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get module information
|
|
78
|
+
*/
|
|
79
|
+
async getInfo(moduleCode) {
|
|
80
|
+
const available = await this.listAvailable();
|
|
81
|
+
return available.find(m => m.code === moduleCode);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Install a module
|
|
86
|
+
*/
|
|
87
|
+
async install(moduleCode) {
|
|
88
|
+
const moduleInfo = await this.getInfo(moduleCode);
|
|
89
|
+
|
|
90
|
+
if (!moduleInfo) {
|
|
91
|
+
throw new Error(`Module "${moduleCode}" not found`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check and install dependencies
|
|
95
|
+
if (moduleInfo.dependencies && moduleInfo.dependencies.length > 0) {
|
|
96
|
+
for (const dep of moduleInfo.dependencies) {
|
|
97
|
+
const isInstalled = (await this.listInstalled()).includes(dep);
|
|
98
|
+
if (!isInstalled) {
|
|
99
|
+
await this.install(dep);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Copy module files
|
|
105
|
+
const sourcePath = join(this.modulesSourceDir, moduleInfo.path);
|
|
106
|
+
const destPath = join(this.modulesInstallDir, moduleInfo.code);
|
|
107
|
+
|
|
108
|
+
await FileSystem.copyDir(sourcePath, destPath);
|
|
109
|
+
|
|
110
|
+
// Update configuration
|
|
111
|
+
const config = await this.configManager.load();
|
|
112
|
+
if (!config.modules.includes(moduleCode)) {
|
|
113
|
+
config.modules.push(moduleCode);
|
|
114
|
+
await this.configManager.save(config);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Uninstall a module
|
|
120
|
+
*/
|
|
121
|
+
async uninstall(moduleCode) {
|
|
122
|
+
const destPath = join(this.modulesInstallDir, moduleCode);
|
|
123
|
+
|
|
124
|
+
if (!existsSync(destPath)) {
|
|
125
|
+
throw new Error(`Module "${moduleCode}" is not installed`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if other modules depend on this
|
|
129
|
+
const installed = await this.listInstalled();
|
|
130
|
+
for (const installedModule of installed) {
|
|
131
|
+
if (installedModule === moduleCode) continue;
|
|
132
|
+
|
|
133
|
+
const info = await this.getInfo(installedModule);
|
|
134
|
+
if (info && info.dependencies && info.dependencies.includes(moduleCode)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Cannot uninstall "${moduleCode}" because "${installedModule}" depends on it`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Remove module files
|
|
142
|
+
await FileSystem.removeDir(destPath);
|
|
143
|
+
|
|
144
|
+
// Update configuration
|
|
145
|
+
const config = await this.configManager.load();
|
|
146
|
+
config.modules = config.modules.filter(m => m !== moduleCode);
|
|
147
|
+
await this.configManager.save(config);
|
|
148
|
+
}
|
|
149
|
+
}
|