aios-core 4.2.6 → 4.2.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/.aios-core/core/orchestration/context-manager.js +333 -5
- package/.aios-core/core/orchestration/dashboard-integration.js +17 -1
- package/.aios-core/core/orchestration/execution-profile-resolver.js +107 -0
- package/.aios-core/core/orchestration/index.js +3 -0
- package/.aios-core/core/orchestration/skill-dispatcher.js +2 -0
- package/.aios-core/core/orchestration/subagent-prompt-builder.js +2 -0
- package/.aios-core/core/orchestration/workflow-orchestrator.js +113 -5
- package/.aios-core/data/entity-registry.yaml +1114 -1336
- package/.aios-core/development/agents/ux-design-expert.md +1 -1
- package/.aios-core/development/checklists/brownfield-compatibility-checklist.md +114 -0
- package/.aios-core/development/scripts/workflow-state-manager.js +128 -1
- package/.aios-core/development/tasks/next.md +36 -5
- package/.aios-core/hooks/ids-post-commit.js +29 -1
- package/.aios-core/hooks/ids-pre-push.js +29 -1
- package/.aios-core/infrastructure/contracts/compatibility/aios-4.0.4.yaml +44 -0
- package/.aios-core/infrastructure/scripts/validate-parity.js +238 -2
- package/.aios-core/install-manifest.yaml +43 -27
- package/.aios-core/product/templates/brownfield-risk-report-tmpl.yaml +277 -0
- package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +114 -5
- package/LICENSE +13 -1
- package/README.md +39 -9
- package/package.json +8 -6
- package/packages/installer/src/wizard/ide-config-generator.js +0 -117
- package/packages/installer/src/wizard/index.js +2 -118
- package/packages/installer/src/wizard/pro-setup.js +50 -5
- package/scripts/semantic-lint.js +190 -0
|
@@ -539,12 +539,6 @@ async function generateIDEConfigs(selectedIDEs, wizardState, options = {}) {
|
|
|
539
539
|
} else {
|
|
540
540
|
spinner.info('Skipped settings.local.json (no hooks to register)');
|
|
541
541
|
}
|
|
542
|
-
|
|
543
|
-
// Silent statusline setup (graceful skip if user already has one)
|
|
544
|
-
const statuslineResult = await setupGlobalStatusline();
|
|
545
|
-
if (statuslineResult.installed) {
|
|
546
|
-
createdFiles.push(...statuslineResult.files);
|
|
547
|
-
}
|
|
548
542
|
}
|
|
549
543
|
|
|
550
544
|
// Gemini parity with Claude Code: copy hooks and configure settings
|
|
@@ -764,7 +758,6 @@ async function createClaudeSettingsLocal(projectRoot) {
|
|
|
764
758
|
}
|
|
765
759
|
|
|
766
760
|
/**
|
|
767
|
-
<<<<<<< HEAD
|
|
768
761
|
* Copy .aios-core/hooks/gemini folder into .gemini/hooks during installation
|
|
769
762
|
* @param {string} projectRoot - Project root directory
|
|
770
763
|
* @returns {Promise<string[]>} List of copied files
|
|
@@ -982,115 +975,6 @@ async function linkGeminiExtension(projectRoot) {
|
|
|
982
975
|
return { status: 'skipped', reason: 'link-failed' };
|
|
983
976
|
}
|
|
984
977
|
|
|
985
|
-
/**
|
|
986
|
-
* Setup global statusline for Claude Code
|
|
987
|
-
*
|
|
988
|
-
* Copies statusline-script.js and track-agent.sh to ~/.claude/
|
|
989
|
-
* and configures ~/.claude/settings.json with statusLine + hook entries.
|
|
990
|
-
*
|
|
991
|
-
* GRACEFUL SKIP: If user already has a statusLine configured, this function
|
|
992
|
-
* returns silently without any output — the user never knows it was checked.
|
|
993
|
-
*
|
|
994
|
-
* @returns {Promise<{installed: boolean, files: string[]}>}
|
|
995
|
-
*/
|
|
996
|
-
async function setupGlobalStatusline() {
|
|
997
|
-
const homeDir = require('os').homedir();
|
|
998
|
-
const globalSettingsPath = path.join(homeDir, '.claude', 'settings.json');
|
|
999
|
-
const result = { installed: false, files: [] };
|
|
1000
|
-
|
|
1001
|
-
// Read existing global settings
|
|
1002
|
-
let settings = {};
|
|
1003
|
-
try {
|
|
1004
|
-
if (await fs.pathExists(globalSettingsPath)) {
|
|
1005
|
-
const content = await fs.readFile(globalSettingsPath, 'utf8');
|
|
1006
|
-
settings = JSON.parse(content);
|
|
1007
|
-
}
|
|
1008
|
-
} catch {
|
|
1009
|
-
// Corrupted or unreadable — treat as empty
|
|
1010
|
-
settings = {};
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// GRACEFUL SKIP: User already has a statusLine configured
|
|
1014
|
-
if (settings.statusLine) {
|
|
1015
|
-
return result;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Source templates
|
|
1019
|
-
const templatesDir = path.join(__dirname, '..', '..', '..', '..', '.aios-core', 'product', 'templates', 'statusline');
|
|
1020
|
-
|
|
1021
|
-
const scriptSource = path.join(templatesDir, 'statusline-script.js');
|
|
1022
|
-
const hookSource = path.join(templatesDir, 'track-agent.sh');
|
|
1023
|
-
|
|
1024
|
-
// Verify templates exist
|
|
1025
|
-
if (!await fs.pathExists(scriptSource) || !await fs.pathExists(hookSource)) {
|
|
1026
|
-
return result;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Target paths
|
|
1030
|
-
const scriptTarget = path.join(homeDir, '.claude', 'statusline-script.js');
|
|
1031
|
-
const hookTarget = path.join(homeDir, '.claude', 'hooks', 'track-agent.sh');
|
|
1032
|
-
const cacheDir = path.join(homeDir, '.claude', 'session-cache');
|
|
1033
|
-
|
|
1034
|
-
// Copy files
|
|
1035
|
-
try {
|
|
1036
|
-
await fs.ensureDir(path.join(homeDir, '.claude', 'hooks'));
|
|
1037
|
-
await fs.ensureDir(cacheDir);
|
|
1038
|
-
await fs.copy(scriptSource, scriptTarget);
|
|
1039
|
-
await fs.copy(hookSource, hookTarget);
|
|
1040
|
-
result.files.push(scriptTarget, hookTarget);
|
|
1041
|
-
} catch {
|
|
1042
|
-
return result;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Build the statusLine command with platform-appropriate path
|
|
1046
|
-
const scriptPathEscaped = scriptTarget.replace(/\\/g, '\\\\');
|
|
1047
|
-
|
|
1048
|
-
// Add statusLine to settings
|
|
1049
|
-
settings.statusLine = {
|
|
1050
|
-
type: 'command',
|
|
1051
|
-
command: `node "${scriptPathEscaped}"`,
|
|
1052
|
-
};
|
|
1053
|
-
|
|
1054
|
-
// Add track-agent hook to UserPromptSubmit (if not already present)
|
|
1055
|
-
if (!settings.hooks) {
|
|
1056
|
-
settings.hooks = {};
|
|
1057
|
-
}
|
|
1058
|
-
if (!Array.isArray(settings.hooks.UserPromptSubmit)) {
|
|
1059
|
-
settings.hooks.UserPromptSubmit = [];
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const hookPathEscaped = hookTarget.replace(/\\/g, '\\\\');
|
|
1063
|
-
const alreadyHasTrackAgent = settings.hooks.UserPromptSubmit.some(entry => {
|
|
1064
|
-
if (Array.isArray(entry.hooks)) {
|
|
1065
|
-
return entry.hooks.some(h => h.command && h.command.includes('track-agent'));
|
|
1066
|
-
}
|
|
1067
|
-
return entry.command && entry.command.includes('track-agent');
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
if (!alreadyHasTrackAgent) {
|
|
1071
|
-
settings.hooks.UserPromptSubmit.push({
|
|
1072
|
-
matcher: '',
|
|
1073
|
-
hooks: [
|
|
1074
|
-
{
|
|
1075
|
-
type: 'command',
|
|
1076
|
-
command: `bash "${hookPathEscaped}"`,
|
|
1077
|
-
},
|
|
1078
|
-
],
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Write settings back
|
|
1083
|
-
try {
|
|
1084
|
-
await fs.ensureDir(path.dirname(globalSettingsPath));
|
|
1085
|
-
await fs.writeFile(globalSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
1086
|
-
result.installed = true;
|
|
1087
|
-
} catch {
|
|
1088
|
-
return result;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
return result;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
978
|
module.exports = {
|
|
1095
979
|
generateIDEConfigs,
|
|
1096
980
|
showSuccessSummary,
|
|
@@ -1104,5 +988,4 @@ module.exports = {
|
|
|
1104
988
|
copyGeminiHooksFolder,
|
|
1105
989
|
createGeminiSettings,
|
|
1106
990
|
linkGeminiExtension,
|
|
1107
|
-
setupGlobalStatusline,
|
|
1108
991
|
};
|
|
@@ -461,67 +461,7 @@ async function runWizard(options = {}) {
|
|
|
461
461
|
answers.techPresetResult = { preset: 'none', success: true };
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
//
|
|
465
|
-
// Install Squads if selected
|
|
466
|
-
// if (answers.selectedExpansionPacks && answers.selectedExpansionPacks.length > 0) {
|
|
467
|
-
// console.log('\n🎁 Installing Squads...');
|
|
468
|
-
//
|
|
469
|
-
// // Detect source squads directory (npm package location)
|
|
470
|
-
// const possibleSourceDirs = [
|
|
471
|
-
// path.join(__dirname, '..', '..', 'squads'),
|
|
472
|
-
// path.join(__dirname, '..', '..', '..', 'squads'),
|
|
473
|
-
// path.join(process.cwd(), 'node_modules', '@synkra/aios-core', 'squads'),
|
|
474
|
-
// ];
|
|
475
|
-
//
|
|
476
|
-
// let sourceExpansionDir = null;
|
|
477
|
-
// for (const dir of possibleSourceDirs) {
|
|
478
|
-
// if (fse.existsSync(dir)) {
|
|
479
|
-
// sourceExpansionDir = dir;
|
|
480
|
-
// break;
|
|
481
|
-
// }
|
|
482
|
-
// }
|
|
483
|
-
//
|
|
484
|
-
// if (sourceExpansionDir) {
|
|
485
|
-
// const targetExpansionDir = path.join(process.cwd(), 'squads');
|
|
486
|
-
// await fse.ensureDir(targetExpansionDir);
|
|
487
|
-
//
|
|
488
|
-
// const installedPacks = [];
|
|
489
|
-
// const failedPacks = [];
|
|
490
|
-
//
|
|
491
|
-
// for (const pack of answers.selectedExpansionPacks) {
|
|
492
|
-
// const sourcePack = path.join(sourceExpansionDir, pack);
|
|
493
|
-
// const targetPack = path.join(targetExpansionDir, pack);
|
|
494
|
-
//
|
|
495
|
-
// try {
|
|
496
|
-
// if (fse.existsSync(sourcePack)) {
|
|
497
|
-
// await fse.copy(sourcePack, targetPack);
|
|
498
|
-
// installedPacks.push(pack);
|
|
499
|
-
// console.log(` ✅ ${pack}`);
|
|
500
|
-
// } else {
|
|
501
|
-
// failedPacks.push({ pack, reason: 'not found' });
|
|
502
|
-
// console.log(` ⚠️ ${pack} - not found in source`);
|
|
503
|
-
// }
|
|
504
|
-
// } catch (packError) {
|
|
505
|
-
// failedPacks.push({ pack, reason: packError.message });
|
|
506
|
-
// console.log(` ⚠️ ${pack} - ${packError.message}`);
|
|
507
|
-
// }
|
|
508
|
-
// }
|
|
509
|
-
//
|
|
510
|
-
// answers.expansionPacksInstalled = installedPacks.length > 0;
|
|
511
|
-
// answers.expansionPacksResult = {
|
|
512
|
-
// installed: installedPacks,
|
|
513
|
-
// failed: failedPacks,
|
|
514
|
-
// targetDir: targetExpansionDir,
|
|
515
|
-
// };
|
|
516
|
-
//
|
|
517
|
-
// if (installedPacks.length > 0) {
|
|
518
|
-
// console.log(`\n✅ Squads installed (${installedPacks.length}/${answers.selectedExpansionPacks.length})`);
|
|
519
|
-
// }
|
|
520
|
-
// } else {
|
|
521
|
-
// console.log(' ⚠️ Squads source directory not found');
|
|
522
|
-
// answers.expansionPacksInstalled = false;
|
|
523
|
-
// }
|
|
524
|
-
// }
|
|
464
|
+
// Legacy squad installation path removed; unified squads flow is now the only supported path.
|
|
525
465
|
|
|
526
466
|
// Story 1.4: Generate IDE configs if IDEs were selected
|
|
527
467
|
let ideConfigResult = null;
|
|
@@ -545,63 +485,7 @@ async function runWizard(options = {}) {
|
|
|
545
485
|
}
|
|
546
486
|
}
|
|
547
487
|
|
|
548
|
-
//
|
|
549
|
-
// Install squad agents to each selected IDE
|
|
550
|
-
// if (answers.expansionPacksResult && answers.expansionPacksResult.installed.length > 0) {
|
|
551
|
-
// console.log('\n📦 Installing squad agents to IDEs...');
|
|
552
|
-
//
|
|
553
|
-
// for (const packName of answers.expansionPacksResult.installed) {
|
|
554
|
-
// const packAgentsDir = path.join(answers.expansionPacksResult.targetDir, packName, 'agents');
|
|
555
|
-
//
|
|
556
|
-
// if (await fse.pathExists(packAgentsDir)) {
|
|
557
|
-
// const agentFiles = (await fse.readdir(packAgentsDir)).filter(f => f.endsWith('.md'));
|
|
558
|
-
//
|
|
559
|
-
// if (agentFiles.length > 0) {
|
|
560
|
-
// for (const ideKey of answers.selectedIDEs) {
|
|
561
|
-
// const ideConfig = getIDEConfig(ideKey);
|
|
562
|
-
// if (!ideConfig || !ideConfig.agentFolder) continue;
|
|
563
|
-
//
|
|
564
|
-
// const isAntiGravity = ideConfig.specialConfig && ideConfig.specialConfig.type === 'antigravity';
|
|
565
|
-
//
|
|
566
|
-
// // Determine target folder for this squad
|
|
567
|
-
// let targetFolder;
|
|
568
|
-
// if (isAntiGravity) {
|
|
569
|
-
// // AntiGravity: workflows go to .agent/workflows/{packName}/
|
|
570
|
-
// targetFolder = path.join(process.cwd(), ideConfig.agentFolder, packName);
|
|
571
|
-
// // Also need to copy actual agents to .antigravity/agents/{packName}/
|
|
572
|
-
// const agentsTargetFolder = path.join(process.cwd(), ideConfig.specialConfig.agentsFolder, packName);
|
|
573
|
-
// await fse.ensureDir(agentsTargetFolder);
|
|
574
|
-
//
|
|
575
|
-
// for (const agentFile of agentFiles) {
|
|
576
|
-
// const sourcePath = path.join(packAgentsDir, agentFile);
|
|
577
|
-
// const agentName = agentFile.replace('.md', '');
|
|
578
|
-
//
|
|
579
|
-
// // Create workflow file
|
|
580
|
-
// const workflowContent = generateExpansionPackWorkflow(agentName, packName);
|
|
581
|
-
// await fse.ensureDir(targetFolder);
|
|
582
|
-
// await fse.writeFile(path.join(targetFolder, agentFile), workflowContent, 'utf8');
|
|
583
|
-
//
|
|
584
|
-
// // Copy actual agent
|
|
585
|
-
// await fse.copy(sourcePath, path.join(agentsTargetFolder, agentFile));
|
|
586
|
-
// }
|
|
587
|
-
// } else {
|
|
588
|
-
// // Other IDEs: copy directly to agentFolder/{packName}/
|
|
589
|
-
// targetFolder = path.join(process.cwd(), ideConfig.agentFolder, packName);
|
|
590
|
-
// await fse.ensureDir(targetFolder);
|
|
591
|
-
//
|
|
592
|
-
// for (const agentFile of agentFiles) {
|
|
593
|
-
// await fse.copy(
|
|
594
|
-
// path.join(packAgentsDir, agentFile),
|
|
595
|
-
// path.join(targetFolder, agentFile),
|
|
596
|
-
// );
|
|
597
|
-
// }
|
|
598
|
-
// }
|
|
599
|
-
// }
|
|
600
|
-
// console.log(` ✅ ${packName}: ${agentFiles.length} agents installed to ${answers.selectedIDEs.length} IDE(s)`);
|
|
601
|
-
// }
|
|
602
|
-
// }
|
|
603
|
-
// }
|
|
604
|
-
// }
|
|
488
|
+
// Legacy per-squad IDE copy path removed; sync pipeline handles IDE propagation.
|
|
605
489
|
}
|
|
606
490
|
|
|
607
491
|
// Story 1.6: Environment Configuration
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { createSpinner, showSuccess, showError, showWarning, showInfo } = require('./feedback');
|
|
19
|
-
const { colors,
|
|
19
|
+
const { colors, status } = require('../utils/aios-colors');
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Gold color for Pro branding.
|
|
@@ -947,6 +947,55 @@ async function validateKeyWithApi(key) {
|
|
|
947
947
|
async function stepInstallScaffold(targetDir, options = {}) {
|
|
948
948
|
showStep(2, 3, 'Pro Content Installation');
|
|
949
949
|
|
|
950
|
+
const path = require('path');
|
|
951
|
+
const fs = require('fs');
|
|
952
|
+
const { execSync } = require('child_process');
|
|
953
|
+
|
|
954
|
+
const proSourceDir = path.join(targetDir, 'node_modules', '@aios-fullstack', 'pro');
|
|
955
|
+
|
|
956
|
+
// Step 2a: Ensure package.json exists (greenfield projects)
|
|
957
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
958
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
959
|
+
const initSpinner = createSpinner('Initializing package.json...');
|
|
960
|
+
initSpinner.start();
|
|
961
|
+
try {
|
|
962
|
+
execSync('npm init -y', { cwd: targetDir, stdio: 'pipe' });
|
|
963
|
+
initSpinner.succeed('package.json created');
|
|
964
|
+
} catch (err) {
|
|
965
|
+
initSpinner.fail('Failed to create package.json');
|
|
966
|
+
return { success: false, error: `npm init failed: ${err.message}` };
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Step 2b: Install @aios-fullstack/pro if not present
|
|
971
|
+
if (!fs.existsSync(proSourceDir)) {
|
|
972
|
+
const installSpinner = createSpinner('Installing @aios-fullstack/pro...');
|
|
973
|
+
installSpinner.start();
|
|
974
|
+
try {
|
|
975
|
+
execSync('npm install @aios-fullstack/pro', {
|
|
976
|
+
cwd: targetDir,
|
|
977
|
+
stdio: 'pipe',
|
|
978
|
+
timeout: 120000,
|
|
979
|
+
});
|
|
980
|
+
installSpinner.succeed('Pro package installed');
|
|
981
|
+
} catch (err) {
|
|
982
|
+
installSpinner.fail('Failed to install Pro package');
|
|
983
|
+
return {
|
|
984
|
+
success: false,
|
|
985
|
+
error: `npm install @aios-fullstack/pro failed: ${err.message}. Try manually: npm install @aios-fullstack/pro`,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Validate installation
|
|
990
|
+
if (!fs.existsSync(proSourceDir)) {
|
|
991
|
+
return {
|
|
992
|
+
success: false,
|
|
993
|
+
error: 'Pro package not found after npm install. Check npm output.',
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Step 2c: Scaffold pro content
|
|
950
999
|
const scaffolderModule = loadProScaffolder();
|
|
951
1000
|
|
|
952
1001
|
if (!scaffolderModule) {
|
|
@@ -955,10 +1004,6 @@ async function stepInstallScaffold(targetDir, options = {}) {
|
|
|
955
1004
|
}
|
|
956
1005
|
|
|
957
1006
|
const { scaffoldProContent } = scaffolderModule;
|
|
958
|
-
const path = require('path');
|
|
959
|
-
|
|
960
|
-
// Determine pro source directory
|
|
961
|
-
const proSourceDir = path.join(targetDir, 'node_modules', '@aios-fullstack', 'pro');
|
|
962
1007
|
|
|
963
1008
|
const spinner = createSpinner('Scaffolding pro content...');
|
|
964
1009
|
spinner.start();
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TARGETS = [
|
|
8
|
+
'README.md',
|
|
9
|
+
'docs/getting-started.md',
|
|
10
|
+
'docs/roadmap.md',
|
|
11
|
+
'docs/strategy',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const SEMANTIC_RULESET_VERSION = '1.0.0';
|
|
15
|
+
|
|
16
|
+
const RULES = [
|
|
17
|
+
{
|
|
18
|
+
id: 'deprecated-expansion-pack',
|
|
19
|
+
severity: 'error',
|
|
20
|
+
pattern: /\bexpansion pack(s)?\b/gi,
|
|
21
|
+
replacement: 'squad',
|
|
22
|
+
reason: 'Use AIOS-first taxonomy for domain agent sets.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'deprecated-permission-mode',
|
|
26
|
+
severity: 'error',
|
|
27
|
+
pattern: /\bpermission mode(s)?\b/gi,
|
|
28
|
+
replacement: 'execution profile',
|
|
29
|
+
reason: 'Use risk-oriented autonomy terminology.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'legacy-workflow-state-term',
|
|
33
|
+
severity: 'warn',
|
|
34
|
+
pattern: /\bworkflow state\b/gi,
|
|
35
|
+
replacement: 'flow-state',
|
|
36
|
+
reason: 'Prefer flow-state in product-facing differentiation messaging.',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
41
|
+
const args = new Set(argv.filter((arg) => arg.startsWith('--')));
|
|
42
|
+
const files = argv.filter((arg) => !arg.startsWith('--'));
|
|
43
|
+
return {
|
|
44
|
+
staged: args.has('--staged'),
|
|
45
|
+
json: args.has('--json'),
|
|
46
|
+
files,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectFiles(inputPaths, projectRoot = process.cwd()) {
|
|
51
|
+
const selected = inputPaths && inputPaths.length > 0 ? inputPaths : DEFAULT_TARGETS;
|
|
52
|
+
const files = [];
|
|
53
|
+
|
|
54
|
+
for (const input of selected) {
|
|
55
|
+
const resolved = path.resolve(projectRoot, input);
|
|
56
|
+
if (!fs.existsSync(resolved)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const stat = fs.statSync(resolved);
|
|
61
|
+
if (stat.isFile()) {
|
|
62
|
+
files.push(resolved);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (stat.isDirectory()) {
|
|
67
|
+
walkDirectory(resolved, files);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function walkDirectory(dir, files) {
|
|
75
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const full = path.join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
walkDirectory(full, files);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (/\.(md|js|yaml|yml)$/i.test(entry.name)) {
|
|
83
|
+
files.push(full);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function lintContent(content, relativePath) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
|
|
91
|
+
for (const rule of RULES) {
|
|
92
|
+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = regex.exec(content)) !== null) {
|
|
95
|
+
const line = 1 + content.slice(0, match.index).split('\n').length - 1;
|
|
96
|
+
findings.push({
|
|
97
|
+
ruleId: rule.id,
|
|
98
|
+
severity: rule.severity,
|
|
99
|
+
term: match[0],
|
|
100
|
+
replacement: rule.replacement,
|
|
101
|
+
file: relativePath,
|
|
102
|
+
line,
|
|
103
|
+
reason: rule.reason,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return findings;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runSemanticLint(options = {}, deps = {}) {
|
|
112
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
113
|
+
const targets = options.targets || [];
|
|
114
|
+
const fileCollector = deps.collectFiles || collectFiles;
|
|
115
|
+
const fileReader = deps.readFile || ((filePath) => fs.readFileSync(filePath, 'utf8'));
|
|
116
|
+
const files = fileCollector(targets, projectRoot);
|
|
117
|
+
|
|
118
|
+
const findings = [];
|
|
119
|
+
for (const filePath of files) {
|
|
120
|
+
const content = fileReader(filePath);
|
|
121
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
122
|
+
findings.push(...lintContent(content, relativePath));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const errors = findings.filter((f) => f.severity === 'error');
|
|
126
|
+
const warnings = findings.filter((f) => f.severity === 'warn');
|
|
127
|
+
return {
|
|
128
|
+
ok: errors.length === 0,
|
|
129
|
+
version: SEMANTIC_RULESET_VERSION,
|
|
130
|
+
filesScanned: files.length,
|
|
131
|
+
findings,
|
|
132
|
+
errors,
|
|
133
|
+
warnings,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatHuman(result) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push(`Semantic Lint v${result.version}`);
|
|
140
|
+
lines.push(`Files scanned: ${result.filesScanned}`);
|
|
141
|
+
|
|
142
|
+
if (result.findings.length === 0) {
|
|
143
|
+
lines.push('✅ No semantic term regressions found');
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const finding of result.findings) {
|
|
148
|
+
lines.push(
|
|
149
|
+
`${finding.severity === 'error' ? '❌' : '⚠️'} ${finding.file}:${finding.line} ${finding.ruleId} -> "${finding.term}" (use "${finding.replacement}")`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(
|
|
155
|
+
result.ok
|
|
156
|
+
? `✅ Completed with ${result.warnings.length} warning(s)`
|
|
157
|
+
: `❌ Failed with ${result.errors.length} error(s) and ${result.warnings.length} warning(s)`,
|
|
158
|
+
);
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function main() {
|
|
163
|
+
const args = parseArgs();
|
|
164
|
+
const targets = args.files.length > 0
|
|
165
|
+
? args.files
|
|
166
|
+
: (args.staged ? [] : DEFAULT_TARGETS);
|
|
167
|
+
const result = runSemanticLint({ targets });
|
|
168
|
+
|
|
169
|
+
if (args.json) {
|
|
170
|
+
console.log(JSON.stringify(result, null, 2));
|
|
171
|
+
} else {
|
|
172
|
+
console.log(formatHuman(result));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (require.main === module) {
|
|
181
|
+
main();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
parseArgs,
|
|
186
|
+
collectFiles,
|
|
187
|
+
lintContent,
|
|
188
|
+
runSemanticLint,
|
|
189
|
+
RULES,
|
|
190
|
+
};
|