universal-dev-standards 3.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.
@@ -0,0 +1,453 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+
4
+ /**
5
+ * All available rule categories for integration files
6
+ */
7
+ const RULE_CATEGORIES = {
8
+ 'anti-hallucination': {
9
+ name: 'Anti-Hallucination Protocol',
10
+ nameZh: '反幻覺協議',
11
+ description: 'Evidence-based analysis and source attribution',
12
+ default: true
13
+ },
14
+ 'commit-standards': {
15
+ name: 'Commit Message Standards',
16
+ nameZh: '提交訊息標準',
17
+ description: 'Conventional commits format and guidelines',
18
+ default: true
19
+ },
20
+ 'code-review': {
21
+ name: 'Code Review Checklist',
22
+ nameZh: '程式碼審查清單',
23
+ description: 'Pre-commit quality verification',
24
+ default: true
25
+ },
26
+ 'testing': {
27
+ name: 'Testing Standards',
28
+ nameZh: '測試標準',
29
+ description: 'Test pyramid and coverage requirements',
30
+ default: false
31
+ },
32
+ 'documentation': {
33
+ name: 'Documentation Standards',
34
+ nameZh: '文件標準',
35
+ description: 'README and API documentation guidelines',
36
+ default: false
37
+ },
38
+ 'git-workflow': {
39
+ name: 'Git Workflow',
40
+ nameZh: 'Git 工作流程',
41
+ description: 'Branch naming and merge strategies',
42
+ default: false
43
+ },
44
+ 'error-handling': {
45
+ name: 'Error Handling',
46
+ nameZh: '錯誤處理',
47
+ description: 'Error codes and logging standards',
48
+ default: false
49
+ },
50
+ 'project-structure': {
51
+ name: 'Project Structure',
52
+ nameZh: '專案結構',
53
+ description: 'Directory conventions and organization',
54
+ default: false
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Language-specific rule options
60
+ */
61
+ const LANGUAGE_RULES = {
62
+ javascript: {
63
+ name: 'JavaScript/TypeScript',
64
+ rules: ['ES6+ syntax', 'Async/await patterns', 'Type safety (TS)']
65
+ },
66
+ python: {
67
+ name: 'Python',
68
+ rules: ['PEP 8 style', 'Type hints', 'Docstrings']
69
+ },
70
+ csharp: {
71
+ name: 'C#',
72
+ rules: ['.NET naming conventions', 'LINQ usage', 'Async patterns']
73
+ },
74
+ php: {
75
+ name: 'PHP',
76
+ rules: ['PSR-12 style', 'Type declarations', 'Namespace usage']
77
+ },
78
+ go: {
79
+ name: 'Go',
80
+ rules: ['Go idioms', 'Error handling', 'Package structure']
81
+ },
82
+ java: {
83
+ name: 'Java',
84
+ rules: ['Java conventions', 'Stream API', 'Dependency injection']
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Prompt for integration customization mode
90
+ * @returns {Promise<string>} 'default', 'custom', or 'merge'
91
+ */
92
+ export async function promptIntegrationMode() {
93
+ console.log();
94
+ console.log(chalk.cyan('Integration Configuration:'));
95
+ console.log(chalk.gray(' Configure how AI tool rules are generated'));
96
+ console.log();
97
+
98
+ const { mode } = await inquirer.prompt([
99
+ {
100
+ type: 'list',
101
+ name: 'mode',
102
+ message: 'How would you like to configure integration files?',
103
+ choices: [
104
+ {
105
+ name: `${chalk.green('Default')} ${chalk.gray('(推薦)')} - Use standard rule set`,
106
+ value: 'default'
107
+ },
108
+ {
109
+ name: `${chalk.blue('Custom')} - Select specific rules to include`,
110
+ value: 'custom'
111
+ },
112
+ {
113
+ name: `${chalk.yellow('Merge')} - Merge with existing rules file`,
114
+ value: 'merge'
115
+ }
116
+ ],
117
+ default: 'default'
118
+ }
119
+ ]);
120
+
121
+ return mode;
122
+ }
123
+
124
+ /**
125
+ * Prompt for selecting rule categories
126
+ * @param {Object} _detected - Detected project characteristics (for future use)
127
+ * @returns {Promise<string[]>} Selected rule category IDs
128
+ */
129
+ // eslint-disable-next-line no-unused-vars
130
+ export async function promptRuleCategories(_detected = {}) {
131
+ console.log();
132
+ console.log(chalk.cyan('Rule Categories:'));
133
+ console.log(chalk.gray(' Select which standards to include in integration files'));
134
+ console.log();
135
+
136
+ const choices = Object.entries(RULE_CATEGORIES).map(([id, cat]) => ({
137
+ name: `${cat.name} ${chalk.gray(`(${cat.nameZh})`)} - ${cat.description}`,
138
+ value: id,
139
+ checked: cat.default
140
+ }));
141
+
142
+ const { categories } = await inquirer.prompt([
143
+ {
144
+ type: 'checkbox',
145
+ name: 'categories',
146
+ message: 'Select rule categories:',
147
+ choices,
148
+ validate: (answer) => {
149
+ if (answer.length === 0) {
150
+ return 'Please select at least one category';
151
+ }
152
+ return true;
153
+ }
154
+ }
155
+ ]);
156
+
157
+ return categories;
158
+ }
159
+
160
+ /**
161
+ * Prompt for language-specific rules
162
+ * @param {Object} detected - Detected languages
163
+ * @returns {Promise<string[]>} Selected language IDs
164
+ */
165
+ export async function promptLanguageRules(detected = {}) {
166
+ const detectedLanguages = Object.entries(detected)
167
+ .filter(([, v]) => v)
168
+ .map(([k]) => k);
169
+
170
+ if (detectedLanguages.length === 0) {
171
+ return [];
172
+ }
173
+
174
+ console.log();
175
+ console.log(chalk.cyan('Language-Specific Rules:'));
176
+ console.log(chalk.gray(' Include language-specific coding standards'));
177
+ console.log();
178
+
179
+ const choices = detectedLanguages
180
+ .filter(lang => LANGUAGE_RULES[lang])
181
+ .map(lang => ({
182
+ name: `${LANGUAGE_RULES[lang].name} - ${LANGUAGE_RULES[lang].rules.join(', ')}`,
183
+ value: lang,
184
+ checked: true
185
+ }));
186
+
187
+ if (choices.length === 0) {
188
+ return [];
189
+ }
190
+
191
+ const { languages } = await inquirer.prompt([
192
+ {
193
+ type: 'checkbox',
194
+ name: 'languages',
195
+ message: 'Include language-specific rules:',
196
+ choices
197
+ }
198
+ ]);
199
+
200
+ return languages;
201
+ }
202
+
203
+ /**
204
+ * Prompt for custom exclusion rules
205
+ * @returns {Promise<string[]>} List of patterns/rules to exclude
206
+ */
207
+ export async function promptExclusions() {
208
+ console.log();
209
+ console.log(chalk.cyan('Custom Exclusions:'));
210
+ console.log(chalk.gray(' Specify patterns or rules to exclude from enforcement'));
211
+ console.log();
212
+
213
+ const { hasExclusions } = await inquirer.prompt([
214
+ {
215
+ type: 'confirm',
216
+ name: 'hasExclusions',
217
+ message: 'Do you want to add custom exclusions?',
218
+ default: false
219
+ }
220
+ ]);
221
+
222
+ if (!hasExclusions) {
223
+ return [];
224
+ }
225
+
226
+ const { exclusions } = await inquirer.prompt([
227
+ {
228
+ type: 'input',
229
+ name: 'exclusions',
230
+ message: 'Enter exclusion patterns (comma-separated):',
231
+ filter: (input) => input.split(',').map(s => s.trim()).filter(s => s.length > 0)
232
+ }
233
+ ]);
234
+
235
+ return exclusions;
236
+ }
237
+
238
+ /**
239
+ * Prompt for project-specific custom rules
240
+ * @returns {Promise<string[]>} List of custom rules to add
241
+ */
242
+ export async function promptCustomRules() {
243
+ console.log();
244
+ console.log(chalk.cyan('Project-Specific Rules:'));
245
+ console.log(chalk.gray(' Add custom rules specific to your project'));
246
+ console.log();
247
+
248
+ const { hasCustomRules } = await inquirer.prompt([
249
+ {
250
+ type: 'confirm',
251
+ name: 'hasCustomRules',
252
+ message: 'Do you want to add project-specific custom rules?',
253
+ default: false
254
+ }
255
+ ]);
256
+
257
+ if (!hasCustomRules) {
258
+ return [];
259
+ }
260
+
261
+ const customRules = [];
262
+ let addMore = true;
263
+
264
+ while (addMore) {
265
+ const { rule } = await inquirer.prompt([
266
+ {
267
+ type: 'input',
268
+ name: 'rule',
269
+ message: 'Enter custom rule (or empty to finish):',
270
+ }
271
+ ]);
272
+
273
+ if (rule.trim()) {
274
+ customRules.push(rule.trim());
275
+ } else {
276
+ addMore = false;
277
+ }
278
+ }
279
+
280
+ return customRules;
281
+ }
282
+
283
+ /**
284
+ * Prompt for merge conflict resolution strategy
285
+ * @param {string} toolName - Name of the AI tool
286
+ * @returns {Promise<string>} 'keep', 'overwrite', or 'append'
287
+ */
288
+ export async function promptMergeStrategy(toolName) {
289
+ console.log();
290
+ console.log(chalk.cyan(`Existing ${toolName} Rules Detected:`));
291
+ console.log(chalk.gray(' Choose how to handle existing rules'));
292
+ console.log();
293
+
294
+ const { strategy } = await inquirer.prompt([
295
+ {
296
+ type: 'list',
297
+ name: 'strategy',
298
+ message: `How should we handle the existing ${toolName} rules?`,
299
+ choices: [
300
+ {
301
+ name: `${chalk.green('Append')} ${chalk.gray('(推薦)')} - Add new rules after existing ones`,
302
+ value: 'append'
303
+ },
304
+ {
305
+ name: `${chalk.blue('Merge')} - Intelligently merge (avoid duplicates)`,
306
+ value: 'merge'
307
+ },
308
+ {
309
+ name: `${chalk.yellow('Overwrite')} - Replace with new rules`,
310
+ value: 'overwrite'
311
+ },
312
+ {
313
+ name: `${chalk.gray('Keep')} - Keep existing, skip installation`,
314
+ value: 'keep'
315
+ }
316
+ ],
317
+ default: 'append'
318
+ }
319
+ ]);
320
+
321
+ return strategy;
322
+ }
323
+
324
+ /**
325
+ * Prompt for rule detail level
326
+ * @returns {Promise<string>} 'minimal', 'standard', or 'comprehensive'
327
+ */
328
+ export async function promptDetailLevel() {
329
+ console.log();
330
+ console.log(chalk.cyan('Rule Detail Level:'));
331
+ console.log(chalk.gray(' Choose how detailed the generated rules should be'));
332
+ console.log();
333
+
334
+ const { level } = await inquirer.prompt([
335
+ {
336
+ type: 'list',
337
+ name: 'level',
338
+ message: 'Select rule detail level:',
339
+ choices: [
340
+ {
341
+ name: `${chalk.green('Minimal')} - Essential rules only (~50 lines)`,
342
+ value: 'minimal'
343
+ },
344
+ {
345
+ name: `${chalk.blue('Standard')} ${chalk.gray('(推薦)')} - Balanced coverage (~150 lines)`,
346
+ value: 'standard'
347
+ },
348
+ {
349
+ name: `${chalk.yellow('Comprehensive')} - Full documentation (~300+ lines)`,
350
+ value: 'comprehensive'
351
+ }
352
+ ],
353
+ default: 'standard'
354
+ }
355
+ ]);
356
+
357
+ return level;
358
+ }
359
+
360
+ /**
361
+ * Prompt for output language preference
362
+ * @returns {Promise<string>} 'en', 'zh-tw', or 'bilingual'
363
+ */
364
+ export async function promptRuleLanguage() {
365
+ const { language } = await inquirer.prompt([
366
+ {
367
+ type: 'list',
368
+ name: 'language',
369
+ message: 'Select rule documentation language:',
370
+ choices: [
371
+ {
372
+ name: `${chalk.green('English')} ${chalk.gray('(推薦)')}`,
373
+ value: 'en'
374
+ },
375
+ {
376
+ name: `${chalk.blue('繁體中文')} (Traditional Chinese)`,
377
+ value: 'zh-tw'
378
+ },
379
+ {
380
+ name: `${chalk.yellow('Bilingual')} (雙語)`,
381
+ value: 'bilingual'
382
+ }
383
+ ],
384
+ default: 'en'
385
+ }
386
+ ]);
387
+
388
+ return language;
389
+ }
390
+
391
+ /**
392
+ * Get all rule categories
393
+ * @returns {Object} Rule categories configuration
394
+ */
395
+ export function getRuleCategories() {
396
+ return RULE_CATEGORIES;
397
+ }
398
+
399
+ /**
400
+ * Get all language rules
401
+ * @returns {Object} Language rules configuration
402
+ */
403
+ export function getLanguageRules() {
404
+ return LANGUAGE_RULES;
405
+ }
406
+
407
+ /**
408
+ * Prompt for complete integration configuration
409
+ * @param {string} tool - AI tool name
410
+ * @param {Object} detected - Detected project characteristics
411
+ * @param {boolean} existingRulesFound - Whether existing rules file was found
412
+ * @returns {Promise<Object>} Complete integration configuration
413
+ */
414
+ export async function promptIntegrationConfig(tool, detected, existingRulesFound = false) {
415
+ const config = {
416
+ tool,
417
+ mode: 'default',
418
+ categories: Object.keys(RULE_CATEGORIES).filter(k => RULE_CATEGORIES[k].default),
419
+ languages: [],
420
+ exclusions: [],
421
+ customRules: [],
422
+ mergeStrategy: null,
423
+ detailLevel: 'standard',
424
+ language: 'en'
425
+ };
426
+
427
+ // If existing rules found, ask about merge strategy first
428
+ if (existingRulesFound) {
429
+ config.mergeStrategy = await promptMergeStrategy(tool);
430
+ if (config.mergeStrategy === 'keep') {
431
+ return config;
432
+ }
433
+ }
434
+
435
+ // Ask for configuration mode
436
+ config.mode = await promptIntegrationMode();
437
+
438
+ if (config.mode === 'custom') {
439
+ // Custom mode: detailed configuration
440
+ config.categories = await promptRuleCategories(detected);
441
+ config.languages = await promptLanguageRules(detected.languages || {});
442
+ config.detailLevel = await promptDetailLevel();
443
+ config.language = await promptRuleLanguage();
444
+ config.exclusions = await promptExclusions();
445
+ config.customRules = await promptCustomRules();
446
+ } else if (config.mode === 'merge' && !existingRulesFound) {
447
+ // Merge mode selected but no existing file
448
+ console.log(chalk.yellow(' No existing rules file found. Using default mode.'));
449
+ config.mode = 'default';
450
+ }
451
+
452
+ return config;
453
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from 'fs';
2
+ import { dirname, join, basename } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { downloadStandard, downloadIntegration } from './github.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // Root of the universal-dev-standards repository (for local development)
10
+ const REPO_ROOT = join(__dirname, '../../..');
11
+
12
+ /**
13
+ * Copy a standard file to the target project
14
+ * Falls back to downloading from GitHub if local file not found
15
+ * @param {string} sourcePath - Relative path from repo root (e.g., 'core/anti-hallucination.md')
16
+ * @param {string} targetDir - Target directory (usually '.standards')
17
+ * @param {string} projectPath - Project root path
18
+ * @returns {Promise<Object>} Result with success status and copied path
19
+ */
20
+ export async function copyStandard(sourcePath, targetDir, projectPath) {
21
+ const source = join(REPO_ROOT, sourcePath);
22
+ const targetFolder = join(projectPath, targetDir);
23
+ const targetFile = join(targetFolder, basename(sourcePath));
24
+
25
+ // Ensure target directory exists
26
+ if (!existsSync(targetFolder)) {
27
+ mkdirSync(targetFolder, { recursive: true });
28
+ }
29
+
30
+ // Try local copy first
31
+ if (existsSync(source)) {
32
+ try {
33
+ copyFileSync(source, targetFile);
34
+ return {
35
+ success: true,
36
+ error: null,
37
+ path: targetFile
38
+ };
39
+ } catch (error) {
40
+ return {
41
+ success: false,
42
+ error: error.message,
43
+ path: null
44
+ };
45
+ }
46
+ }
47
+
48
+ // Fall back to downloading from GitHub
49
+ return downloadStandard(sourcePath, targetDir, projectPath);
50
+ }
51
+
52
+ /**
53
+ * Copy an integration file to its target location
54
+ * Falls back to downloading from GitHub if local file not found
55
+ * @param {string} sourcePath - Source path relative to repo root
56
+ * @param {string} targetPath - Target path relative to project root
57
+ * @param {string} projectPath - Project root path
58
+ * @returns {Promise<Object>} Result
59
+ */
60
+ export async function copyIntegration(sourcePath, targetPath, projectPath) {
61
+ const source = join(REPO_ROOT, sourcePath);
62
+ const target = join(projectPath, targetPath);
63
+
64
+ // Ensure target directory exists
65
+ const targetDir = dirname(target);
66
+ if (!existsSync(targetDir)) {
67
+ mkdirSync(targetDir, { recursive: true });
68
+ }
69
+
70
+ // Try local copy first
71
+ if (existsSync(source)) {
72
+ try {
73
+ copyFileSync(source, target);
74
+ return {
75
+ success: true,
76
+ error: null,
77
+ path: target
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ success: false,
82
+ error: error.message,
83
+ path: null
84
+ };
85
+ }
86
+ }
87
+
88
+ // Fall back to downloading from GitHub
89
+ return downloadIntegration(sourcePath, targetPath, projectPath);
90
+ }
91
+
92
+ /**
93
+ * Create or update the manifest file
94
+ * @param {Object} manifest - Manifest data
95
+ * @param {string} projectPath - Project root path
96
+ */
97
+ export function writeManifest(manifest, projectPath) {
98
+ const manifestPath = join(projectPath, '.standards', 'manifest.json');
99
+ const manifestDir = dirname(manifestPath);
100
+
101
+ if (!existsSync(manifestDir)) {
102
+ mkdirSync(manifestDir, { recursive: true });
103
+ }
104
+
105
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
106
+ return manifestPath;
107
+ }
108
+
109
+ /**
110
+ * Read the manifest file
111
+ * @param {string} projectPath - Project root path
112
+ * @returns {Object|null} Manifest data or null if not found
113
+ */
114
+ export function readManifest(projectPath) {
115
+ const manifestPath = join(projectPath, '.standards', 'manifest.json');
116
+
117
+ if (!existsSync(manifestPath)) {
118
+ return null;
119
+ }
120
+
121
+ try {
122
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if standards are already initialized
130
+ * @param {string} projectPath - Project root path
131
+ * @returns {boolean} True if initialized
132
+ */
133
+ export function isInitialized(projectPath) {
134
+ return existsSync(join(projectPath, '.standards', 'manifest.json'));
135
+ }
136
+
137
+ /**
138
+ * Get the repository root path
139
+ * @returns {string} Repository root path
140
+ */
141
+ export function getRepoRoot() {
142
+ return REPO_ROOT;
143
+ }