Skip to content

Commit 255b63c

Browse files
committed
feat: implement Phase 4 shared utilities
- Add ConfigUtils for configuration management and validation - Add FileUtils for advanced file operations and searching - Add ProjectUtils for project structure detection and analysis - Add TemplateUtils for template generation and rendering - Add LoggingUtils for consistent logging and output formatting - Create main index file with unified exports - Add comprehensive test script for all utilities - Update package.json with optional dependencies - All utilities work with graceful dependency fallbacks
1 parent fc0f13b commit 255b63c

8 files changed

Lines changed: 3541 additions & 0 deletions

File tree

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
"package.json"
5656
],
5757
"dependencies": {
58+
"chalk": "^4.1.2",
59+
"boxen": "^5.1.2",
60+
"ora": "^5.4.1",
61+
"cli-table3": "^0.6.3",
62+
"progress": "^2.0.3",
63+
"yaml": "^2.3.0",
5864
"chart.js": "^4.0.0",
5965
"codemirror": "^5.65.2",
6066
"express": "^4.18.2",

scripts/lib/config-utils.js

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Shared Configuration Utilities
4+
*
5+
* Common configuration patterns for language tools
6+
*/
7+
8+
const path = require("path");
9+
const fs = require("fs");
10+
const { ensureDir, readFile, writeFile } = require("./utils");
11+
12+
class ConfigUtils {
13+
/**
14+
* Get configuration for a specific language
15+
*/
16+
static getLanguageConfig(projectPath, language) {
17+
const configManager = require("../interactive/config-manager");
18+
const manager = new configManager(projectPath);
19+
const config = manager.loadConfig();
20+
21+
if (!config) {
22+
throw new Error(`Project not configured. Run /${language}-setup first.`);
23+
}
24+
25+
const languageConfig = config[language];
26+
if (!languageConfig) {
27+
throw new Error(
28+
`${language} configuration not found. Run /${language}-setup first.`,
29+
);
30+
}
31+
32+
return {
33+
config,
34+
languageConfig,
35+
manager,
36+
};
37+
}
38+
39+
/**
40+
* Check if a tool is installed in language configuration
41+
*/
42+
static checkToolInstalled(languageConfig, toolName, required = true) {
43+
const toolInfo = languageConfig.tools?.[toolName];
44+
45+
if (!toolInfo || !toolInfo.installed) {
46+
if (required) {
47+
throw new Error(
48+
`Required ${languageConfig.name || "language"} tool '${toolName}' is not installed. ` +
49+
`Run /${languageConfig.name || "language"}-setup to install it.`,
50+
);
51+
}
52+
return false;
53+
}
54+
55+
return true;
56+
}
57+
58+
/**
59+
* Get tool path from configuration or system
60+
*/
61+
static getToolPath(
62+
toolName,
63+
languageConfig,
64+
platformDetector,
65+
customLocations = [],
66+
) {
67+
// Check if tool has custom path in config
68+
const toolInfo = languageConfig.tools?.[toolName];
69+
if (toolInfo?.path && fs.existsSync(toolInfo.path)) {
70+
return toolInfo.path;
71+
}
72+
73+
// Use platform detector to find tool
74+
if (
75+
platformDetector &&
76+
typeof platformDetector.getToolPath === "function"
77+
) {
78+
return platformDetector.getToolPath(toolName, {
79+
required: true,
80+
customLocations,
81+
});
82+
}
83+
84+
// Fallback to checking PATH
85+
const { commandExists } = require("./utils");
86+
if (commandExists(toolName)) {
87+
return toolName;
88+
}
89+
90+
throw new Error(
91+
`Tool '${toolName}' not found. Install it or check your PATH.`,
92+
);
93+
}
94+
95+
/**
96+
* Create default language configuration
97+
*/
98+
static createDefaultLanguageConfig(language, options = {}) {
99+
const defaults = {
100+
name: language,
101+
version: "1.0.0",
102+
tools: {},
103+
configuredAt: new Date().toISOString(),
104+
...options,
105+
};
106+
107+
return defaults;
108+
}
109+
110+
/**
111+
* Merge configurations with proper deep merging
112+
*/
113+
static mergeConfigs(baseConfig, newConfig) {
114+
const result = { ...baseConfig };
115+
116+
for (const key in newConfig) {
117+
if (newConfig.hasOwnProperty(key)) {
118+
if (
119+
typeof newConfig[key] === "object" &&
120+
newConfig[key] !== null &&
121+
typeof result[key] === "object" &&
122+
result[key] !== null &&
123+
!Array.isArray(newConfig[key]) &&
124+
!Array.isArray(result[key])
125+
) {
126+
// Recursively merge objects
127+
result[key] = this.mergeConfigs(result[key], newConfig[key]);
128+
} else {
129+
// Replace primitives or arrays
130+
result[key] = newConfig[key];
131+
}
132+
}
133+
}
134+
135+
return result;
136+
}
137+
138+
/**
139+
* Validate configuration against schema
140+
*/
141+
static validateConfig(config, schema = {}) {
142+
const errors = [];
143+
144+
// Check required fields
145+
if (schema.required) {
146+
for (const field of schema.required) {
147+
if (config[field] === undefined) {
148+
errors.push(`Missing required field: ${field}`);
149+
}
150+
}
151+
}
152+
153+
// Check field types
154+
if (schema.properties) {
155+
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
156+
if (config[field] !== undefined) {
157+
const value = config[field];
158+
const expectedType = fieldSchema.type;
159+
160+
if (expectedType === "string" && typeof value !== "string") {
161+
errors.push(
162+
`Field '${field}' should be string, got ${typeof value}`,
163+
);
164+
} else if (expectedType === "number" && typeof value !== "number") {
165+
errors.push(
166+
`Field '${field}' should be number, got ${typeof value}`,
167+
);
168+
} else if (expectedType === "boolean" && typeof value !== "boolean") {
169+
errors.push(
170+
`Field '${field}' should be boolean, got ${typeof value}`,
171+
);
172+
} else if (expectedType === "array" && !Array.isArray(value)) {
173+
errors.push(
174+
`Field '${field}' should be array, got ${typeof value}`,
175+
);
176+
} else if (
177+
expectedType === "object" &&
178+
(typeof value !== "object" ||
179+
value === null ||
180+
Array.isArray(value))
181+
) {
182+
errors.push(
183+
`Field '${field}' should be object, got ${typeof value}`,
184+
);
185+
}
186+
}
187+
}
188+
}
189+
190+
return {
191+
valid: errors.length === 0,
192+
errors,
193+
};
194+
}
195+
196+
/**
197+
* Save configuration with backup
198+
*/
199+
static saveConfigWithBackup(configPath, config) {
200+
const dir = path.dirname(configPath);
201+
ensureDir(dir);
202+
203+
// Create backup if config exists
204+
if (fs.existsSync(configPath)) {
205+
const backupPath = `${configPath}.backup.${Date.now()}`;
206+
fs.copyFileSync(configPath, backupPath);
207+
}
208+
209+
// Save new config
210+
writeFile(configPath, JSON.stringify(config, null, 2));
211+
212+
return true;
213+
}
214+
215+
/**
216+
* Load configuration with defaults
217+
*/
218+
static loadConfigWithDefaults(configPath, defaults = {}) {
219+
try {
220+
if (fs.existsSync(configPath)) {
221+
const content = readFile(configPath);
222+
const config = JSON.parse(content);
223+
return this.mergeConfigs(defaults, config);
224+
}
225+
} catch (error) {
226+
console.error(`Error loading config from ${configPath}:`, error.message);
227+
}
228+
229+
return defaults;
230+
}
231+
232+
/**
233+
* Get environment-specific configuration
234+
*/
235+
static getEnvironmentConfig(
236+
config,
237+
environment = process.env.NODE_ENV || "development",
238+
) {
239+
const envConfig = config.environments?.[environment] || {};
240+
return this.mergeConfigs(config, envConfig);
241+
}
242+
243+
/**
244+
* Create configuration schema for a language
245+
*/
246+
static createLanguageSchema(language, toolDefinitions = []) {
247+
const schema = {
248+
type: "object",
249+
required: ["name", "tools"],
250+
properties: {
251+
name: { type: "string" },
252+
version: { type: "string" },
253+
tools: {
254+
type: "object",
255+
additionalProperties: {
256+
type: "object",
257+
properties: {
258+
installed: { type: "boolean" },
259+
version: { type: "string" },
260+
path: { type: "string" },
261+
options: { type: "object" },
262+
},
263+
},
264+
},
265+
environments: {
266+
type: "object",
267+
additionalProperties: { type: "object" },
268+
},
269+
},
270+
};
271+
272+
// Add tool-specific properties
273+
for (const tool of toolDefinitions) {
274+
if (schema.properties.tools.properties[tool.name]) {
275+
schema.properties.tools.properties[tool.name] = {
276+
type: "object",
277+
properties: {
278+
installed: { type: "boolean" },
279+
version: { type: "string" },
280+
path: { type: "string" },
281+
...tool.schema,
282+
},
283+
};
284+
}
285+
}
286+
287+
return schema;
288+
}
289+
}
290+
291+
module.exports = ConfigUtils;

0 commit comments

Comments
 (0)