sdg-agents 1.0.6 → 1.0.9
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/README.md +1 -1
- package/package.json +6 -18
- package/src/engine/bin/auto-bump.mjs +110 -0
- package/src/engine/bin/build-bundle.mjs +259 -0
- package/src/engine/bin/check-sync.mjs +116 -0
- package/src/engine/bin/clear-bundle.mjs +116 -0
- package/src/engine/bin/review-bundle.mjs +123 -0
- package/src/engine/bin/sync-rulesets.mjs +147 -0
- package/src/engine/bin/update-versions.mjs +71 -0
- package/src/engine/lib/auto-bump.test.mjs +56 -0
- package/src/engine/lib/cli-parser.test.mjs +89 -0
- package/src/engine/lib/display-utils.test.mjs +31 -0
- package/src/engine/lib/fs-utils.test.mjs +255 -0
- package/src/engine/lib/instruction-assembler.test.mjs +256 -0
- package/src/engine/lib/manifest-utils.test.mjs +105 -0
- package/src/engine/lib/result-utils.test.mjs +63 -0
- package/src/engine/lib/ruleset-injector.test.mjs +234 -0
- package/src/engine/lib/wizard.test.mjs +102 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
A CLI that installs a structured instruction set for AI agents into your project.<br>
|
|
6
6
|
<a href="docs/README.pt-BR.md">Versão em Português (Brasil)</a>
|
|
7
7
|
</p>
|
|
8
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%
|
|
8
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D24-brightgreen?style=flat-square&logo=nodedotjs" alt="Node" /></a>
|
|
9
9
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-ISC-blue?style=flat-square" alt="License: ISC" /></a>
|
|
10
10
|
</div>
|
|
11
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdg-agents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Structured working protocol and engineering rules for AI agents. Works with Claude Code, Antigravity, Codex, and others.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/engine/bin/index.mjs",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"sdg-agents": "./src/engine/bin/index.mjs"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=
|
|
11
|
+
"node": ">=24.0.0"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "node src/engine/bin/index.mjs",
|
|
@@ -43,29 +43,17 @@
|
|
|
43
43
|
"author": "thiagocajadev",
|
|
44
44
|
"license": "ISC",
|
|
45
45
|
"files": [
|
|
46
|
-
"src/engine/
|
|
47
|
-
"src/engine/bin/add-idiom.mjs",
|
|
48
|
-
"src/engine/config/",
|
|
49
|
-
"src/engine/lib/cli-parser.mjs",
|
|
50
|
-
"src/engine/lib/display-utils.mjs",
|
|
51
|
-
"src/engine/lib/fs-utils.mjs",
|
|
52
|
-
"src/engine/lib/instruction-assembler.mjs",
|
|
53
|
-
"src/engine/lib/manifest-utils.mjs",
|
|
54
|
-
"src/engine/lib/prompt-utils.mjs",
|
|
55
|
-
"src/engine/lib/result-utils.mjs",
|
|
56
|
-
"src/engine/lib/ruleset-injector.mjs",
|
|
57
|
-
"src/engine/lib/ui-utils.mjs",
|
|
58
|
-
"src/engine/lib/wizard.mjs",
|
|
46
|
+
"src/engine/",
|
|
59
47
|
"src/assets/"
|
|
60
48
|
],
|
|
61
49
|
"repository": {
|
|
62
50
|
"type": "git",
|
|
63
|
-
"url": "git+https://github.com/thiagocajadev/
|
|
51
|
+
"url": "git+https://github.com/thiagocajadev/sdg-agents-cli.git"
|
|
64
52
|
},
|
|
65
53
|
"bugs": {
|
|
66
|
-
"url": "https://github.com/thiagocajadev/
|
|
54
|
+
"url": "https://github.com/thiagocajadev/sdg-agents-cli/issues"
|
|
67
55
|
},
|
|
68
|
-
"homepage": "https://github.com/thiagocajadev/
|
|
56
|
+
"homepage": "https://github.com/thiagocajadev/sdg-agents-cli#readme",
|
|
69
57
|
"dependencies": {
|
|
70
58
|
"@inquirer/prompts": "^8.3.2",
|
|
71
59
|
"dedent": "^1.7.2"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { ResultUtils } from '../lib/result-utils.mjs';
|
|
7
|
+
import { FsUtils } from '../lib/fs-utils.mjs';
|
|
8
|
+
|
|
9
|
+
const { success } = ResultUtils;
|
|
10
|
+
const { runIfDirect } = FsUtils;
|
|
11
|
+
|
|
12
|
+
// bin/ → engine/ → src/ → root
|
|
13
|
+
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../');
|
|
14
|
+
const PACKAGE_PATHS = [path.join(ROOT_DIR, 'package.json')];
|
|
15
|
+
|
|
16
|
+
// --- Orchestrator ---
|
|
17
|
+
|
|
18
|
+
async function run() {
|
|
19
|
+
const commitMsg = readLastCommitMessage();
|
|
20
|
+
|
|
21
|
+
const bumpType = detectBumpType(commitMsg);
|
|
22
|
+
if (bumpType === 'skip') {
|
|
23
|
+
console.log(' auto-bump: skipped (version commit detected)');
|
|
24
|
+
return success({ from: null, to: null, bump: 'skip' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rootPkg = readPackageJson(resolveRootPackagePath());
|
|
28
|
+
const nextVersion = bumpVersion(rootPkg.version, bumpType);
|
|
29
|
+
|
|
30
|
+
syncAllPackages(nextVersion);
|
|
31
|
+
stageAndCommit(nextVersion);
|
|
32
|
+
|
|
33
|
+
console.log(` auto-bump: ${rootPkg.version} → ${nextVersion} (${bumpType})`);
|
|
34
|
+
return success({ from: rootPkg.version, to: nextVersion, bump: bumpType });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Core Logic ---
|
|
38
|
+
|
|
39
|
+
function detectBumpType(commitMsg) {
|
|
40
|
+
const firstLine = commitMsg.split('\n')[0].trim();
|
|
41
|
+
const footer = commitMsg.split('\n').slice(1).join('\n');
|
|
42
|
+
|
|
43
|
+
if (/^chore:\s*bump version/i.test(firstLine)) return 'skip';
|
|
44
|
+
if (/^[a-z]+!:/.test(firstLine)) return 'major';
|
|
45
|
+
if (/BREAKING CHANGE:/m.test(footer)) return 'major';
|
|
46
|
+
if (/^feat:/.test(firstLine)) return 'minor';
|
|
47
|
+
return 'patch';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bumpVersion(current, bumpType) {
|
|
51
|
+
const [major, minor, patch] = current.split('.').map(Number);
|
|
52
|
+
|
|
53
|
+
switch (bumpType) {
|
|
54
|
+
case 'major':
|
|
55
|
+
return `${major + 1}.0.0`;
|
|
56
|
+
case 'minor':
|
|
57
|
+
return `${major}.${minor + 1}.0`;
|
|
58
|
+
case 'patch':
|
|
59
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Sync & Commit ---
|
|
64
|
+
|
|
65
|
+
function syncAllPackages(nextVersion) {
|
|
66
|
+
const paths = resolvePackagePaths();
|
|
67
|
+
for (const pkgPath of paths) {
|
|
68
|
+
writeVersion(pkgPath, nextVersion);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stageAndCommit(nextVersion) {
|
|
73
|
+
const paths = resolvePackagePaths();
|
|
74
|
+
const files = paths.filter((p) => fs.existsSync(p)).join(' ');
|
|
75
|
+
execSync(`git add ${files}`, { stdio: 'inherit' });
|
|
76
|
+
execSync(`git commit -m "chore: bump version to ${nextVersion}"`, { stdio: 'inherit' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- File Helpers ---
|
|
80
|
+
|
|
81
|
+
function readLastCommitMessage() {
|
|
82
|
+
return execSync('git log -1 --pretty=%B').toString().trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveRootPackagePath() {
|
|
86
|
+
return PACKAGE_PATHS[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolvePackagePaths() {
|
|
90
|
+
return PACKAGE_PATHS.filter((p) => fs.existsSync(p));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readPackageJson(pkgPath) {
|
|
94
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeVersion(pkgPath, version) {
|
|
98
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
99
|
+
const pkg = readPackageJson(pkgPath);
|
|
100
|
+
pkg.version = version;
|
|
101
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const AutoBump = {
|
|
105
|
+
run,
|
|
106
|
+
detectBumpType,
|
|
107
|
+
bumpVersion,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
runIfDirect(import.meta.url, run);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Bundle — Orchestrates wizard, injection, and assembly.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { WizardUtils } from '../lib/wizard.mjs';
|
|
9
|
+
import { RulesetInjector } from '../lib/ruleset-injector.mjs';
|
|
10
|
+
import { InstructionAssembler } from '../lib/instruction-assembler.mjs';
|
|
11
|
+
import { ResultUtils } from '../lib/result-utils.mjs';
|
|
12
|
+
import { FsUtils } from '../lib/fs-utils.mjs';
|
|
13
|
+
import { BundleUI } from '../lib/ui-utils.mjs';
|
|
14
|
+
|
|
15
|
+
const { gatherUserSelections, validateSelections, autoSelectVersions } = WizardUtils;
|
|
16
|
+
const { prepareProjectStructure, injectRulesets, injectPrompts } = RulesetInjector;
|
|
17
|
+
const {
|
|
18
|
+
buildMasterInstructions,
|
|
19
|
+
buildPromptModeStub,
|
|
20
|
+
writeAgentConfig,
|
|
21
|
+
writeBacklogFiles,
|
|
22
|
+
writeGitignore,
|
|
23
|
+
writeManifest,
|
|
24
|
+
} = InstructionAssembler;
|
|
25
|
+
const { success } = ResultUtils;
|
|
26
|
+
const { runIfDirect } = FsUtils;
|
|
27
|
+
const {
|
|
28
|
+
printWelcome,
|
|
29
|
+
printStep,
|
|
30
|
+
printAborted,
|
|
31
|
+
printQuickSetupStart,
|
|
32
|
+
printProjectRoot,
|
|
33
|
+
printSuccessAgents,
|
|
34
|
+
printSuccessPrompts,
|
|
35
|
+
printQuickSuccess,
|
|
36
|
+
printQuickDryRun,
|
|
37
|
+
printDryRunPreview,
|
|
38
|
+
printBuildSummary,
|
|
39
|
+
} = BundleUI;
|
|
40
|
+
|
|
41
|
+
const require = createRequire(import.meta.url);
|
|
42
|
+
const packageJson = require('../../../package.json');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Entry point for both interactive (wizard) and non-interactive (flags) modes.
|
|
46
|
+
*/
|
|
47
|
+
async function run(targetDir = process.cwd(), options = {}) {
|
|
48
|
+
printWelcome();
|
|
49
|
+
|
|
50
|
+
if (options.selections) {
|
|
51
|
+
return runNonInteractive(targetDir, options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return runInteractive(targetDir, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runNonInteractive(targetDir, options) {
|
|
58
|
+
const { dryRun = false, noDevGuides = false, selections } = options;
|
|
59
|
+
|
|
60
|
+
const validationResult = validateSelections(selections);
|
|
61
|
+
if (validationResult.isFailure) {
|
|
62
|
+
console.log(`\n ⚠️ ${validationResult.error.message}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
autoSelectVersions(selections);
|
|
67
|
+
|
|
68
|
+
const state = { step: 'execute', userSelections: selections };
|
|
69
|
+
const result = await handleFinalExecutionPhase(state, targetDir, {
|
|
70
|
+
dryRun,
|
|
71
|
+
noDevGuides,
|
|
72
|
+
skipConfirm: true,
|
|
73
|
+
});
|
|
74
|
+
if (result.isFailure) {
|
|
75
|
+
console.log(`\n ⚠️ ${result.error.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runInteractive(targetDir, options) {
|
|
81
|
+
const state = { step: 'selections', userSelections: null };
|
|
82
|
+
|
|
83
|
+
while (state.step !== 'done') {
|
|
84
|
+
const result = await handleExecutionStep(state, targetDir, options);
|
|
85
|
+
if (result.isFailure) {
|
|
86
|
+
if (result.error.code !== 'USER_BACK') {
|
|
87
|
+
console.log(`\n ⚠️ ${result.error.message}`);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Phase Handlers (Storytelling) ---
|
|
95
|
+
|
|
96
|
+
async function handleExecutionStep(state, targetDir, options) {
|
|
97
|
+
switch (state.step) {
|
|
98
|
+
case 'selections':
|
|
99
|
+
return handleSelectionPhase(state, targetDir);
|
|
100
|
+
case 'execute':
|
|
101
|
+
return handleFinalExecutionPhase(state, targetDir, options);
|
|
102
|
+
default:
|
|
103
|
+
return success();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleSelectionPhase(state, targetDir) {
|
|
108
|
+
const result = await gatherUserSelections(targetDir);
|
|
109
|
+
if (result.isFailure) return result;
|
|
110
|
+
|
|
111
|
+
state.userSelections = result.value;
|
|
112
|
+
state.step = 'execute';
|
|
113
|
+
return success();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleFinalExecutionPhase(state, targetDir, options = {}) {
|
|
117
|
+
const { dryRun = false, noDevGuides = false, skipConfirm = false } = options;
|
|
118
|
+
const selections = state.userSelections;
|
|
119
|
+
|
|
120
|
+
if (selections.mode === 'quick') {
|
|
121
|
+
return runQuickMode(state, targetDir, { dryRun, noDevGuides });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
printDryRunPreview(selections, targetDir);
|
|
126
|
+
state.step = 'done';
|
|
127
|
+
return success();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (selections.mode === 'prompts') {
|
|
131
|
+
return runPromptsMode(state, targetDir, selections, { skipConfirm });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return runAgentsMode(state, targetDir, selections, { skipConfirm, noDevGuides });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Mode Runners (Narrative Orchestration) ---
|
|
138
|
+
|
|
139
|
+
async function runQuickMode(state, targetDir, { dryRun, noDevGuides = false }) {
|
|
140
|
+
if (dryRun) return abortForDryRun(state, targetDir, printQuickDryRun);
|
|
141
|
+
|
|
142
|
+
printQuickSetupStart();
|
|
143
|
+
executeQuickPipeline(targetDir, state.userSelections, { noDevGuides });
|
|
144
|
+
|
|
145
|
+
printQuickSuccess(targetDir);
|
|
146
|
+
state.step = 'done';
|
|
147
|
+
return success();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function runPromptsMode(state, targetDir, selections, { skipConfirm = false } = {}) {
|
|
151
|
+
const confirmed = skipConfirm || (await printBuildSummary(selections));
|
|
152
|
+
if (!confirmed) return abortExecution(state);
|
|
153
|
+
|
|
154
|
+
printProjectRoot(targetDir);
|
|
155
|
+
executePromptsPipeline(targetDir, selections);
|
|
156
|
+
|
|
157
|
+
printSuccessPrompts(targetDir);
|
|
158
|
+
state.step = 'done';
|
|
159
|
+
return success();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runAgentsMode(
|
|
163
|
+
state,
|
|
164
|
+
targetDir,
|
|
165
|
+
selections,
|
|
166
|
+
{ skipConfirm = false, noDevGuides = false } = {}
|
|
167
|
+
) {
|
|
168
|
+
const confirmed = skipConfirm || (await printBuildSummary(selections));
|
|
169
|
+
if (!confirmed) return abortExecution(state);
|
|
170
|
+
|
|
171
|
+
printProjectRoot(targetDir);
|
|
172
|
+
executeAgentsPipeline(targetDir, selections, { noDevGuides });
|
|
173
|
+
|
|
174
|
+
printSuccessAgents(targetDir);
|
|
175
|
+
state.step = 'done';
|
|
176
|
+
return success();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Implementation Details ---
|
|
180
|
+
|
|
181
|
+
function abortForDryRun(state, targetDir, printer) {
|
|
182
|
+
printer(targetDir);
|
|
183
|
+
state.step = 'done';
|
|
184
|
+
return success();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function abortExecution(state) {
|
|
188
|
+
printAborted();
|
|
189
|
+
state.step = 'done';
|
|
190
|
+
return success();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function executeQuickPipeline(targetDir, selections, { noDevGuides = false } = {}) {
|
|
194
|
+
printStep(1, 5, 'Preparing .ai/ structure...');
|
|
195
|
+
prepareProjectStructure(targetDir);
|
|
196
|
+
|
|
197
|
+
printStep(2, 5, 'Injecting rules and dev-guides...');
|
|
198
|
+
injectRulesets(targetDir, selections, { noDevGuides });
|
|
199
|
+
|
|
200
|
+
printStep(3, 5, 'Assembling AGENTS.md...');
|
|
201
|
+
const content = buildMasterInstructions(selections);
|
|
202
|
+
|
|
203
|
+
printStep(4, 5, 'Writing agent config and backlog...');
|
|
204
|
+
writeAgentConfig(targetDir, content, getActiveAgents(selections));
|
|
205
|
+
writeBacklogFiles(targetDir, selections);
|
|
206
|
+
writeGitignore(targetDir);
|
|
207
|
+
|
|
208
|
+
printStep(5, 5, 'Injecting spec templates...');
|
|
209
|
+
injectPrompts(targetDir, selections.track);
|
|
210
|
+
writeManifest(targetDir, selections, packageJson.version);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function executePromptsPipeline(targetDir, selections) {
|
|
214
|
+
printStep(1, 3, 'Preparing .ai/ structure...');
|
|
215
|
+
prepareProjectStructure(targetDir);
|
|
216
|
+
|
|
217
|
+
printStep(2, 3, `Injecting specification track: ${selections.track}...`);
|
|
218
|
+
injectPrompts(targetDir, selections.track);
|
|
219
|
+
|
|
220
|
+
printStep(3, 3, 'Writing prompts-only fallback config...');
|
|
221
|
+
const stubContent = buildPromptModeStub();
|
|
222
|
+
writeAgentConfig(targetDir, stubContent, getActiveAgents(selections));
|
|
223
|
+
writeGitignore(targetDir);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function executeAgentsPipeline(targetDir, selections, { noDevGuides = false } = {}) {
|
|
227
|
+
printStep(1, 5, 'Preparing .ai/ structure...');
|
|
228
|
+
prepareProjectStructure(targetDir);
|
|
229
|
+
|
|
230
|
+
printStep(2, 5, 'Injecting rules and dev-guides...');
|
|
231
|
+
injectRulesets(targetDir, selections, { noDevGuides });
|
|
232
|
+
|
|
233
|
+
printStep(3, 5, 'Assembling AGENTS.md...');
|
|
234
|
+
const content = buildMasterInstructions(selections);
|
|
235
|
+
|
|
236
|
+
printStep(4, 5, 'Writing agent config and backlog...');
|
|
237
|
+
writeAgentConfig(targetDir, content, getActiveAgents(selections));
|
|
238
|
+
writeBacklogFiles(targetDir, selections);
|
|
239
|
+
writeGitignore(targetDir);
|
|
240
|
+
|
|
241
|
+
printStep(5, 5, 'Finalizing manifest...');
|
|
242
|
+
writeManifest(targetDir, selections, packageJson.version);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getActiveAgents(selections) {
|
|
246
|
+
const agentCandidates = [...(selections.agents || []), selections.ide];
|
|
247
|
+
const activeAgents = agentCandidates.filter((agent) => agent !== null && agent !== undefined);
|
|
248
|
+
return activeAgents;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const SDG = {
|
|
252
|
+
run,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
runIfDirect(import.meta.url, () => {
|
|
256
|
+
const targetDir = process.argv[2] ?? process.cwd();
|
|
257
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
258
|
+
return run(path.resolve(targetDir), { dryRun });
|
|
259
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check Sync — Detects drift between the source-of-truth assets and the live .ai/ directory.
|
|
3
|
+
* Compares only the mirrored directories — not flavor/ which is generated with a rename.
|
|
4
|
+
* The .ai/ directory is a curated subset of src/assets/ — not a full mirror.
|
|
5
|
+
*
|
|
6
|
+
* FOLLOWS: Narrative Cascade & Lexical Scoping (nested helpers)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
import { FsUtils } from '../lib/fs-utils.mjs';
|
|
14
|
+
import { ResultUtils } from '../lib/result-utils.mjs';
|
|
15
|
+
|
|
16
|
+
const { getDirname, runIfDirect } = FsUtils;
|
|
17
|
+
const { success, fail } = ResultUtils;
|
|
18
|
+
|
|
19
|
+
const __dirname = getDirname(import.meta.url);
|
|
20
|
+
const MONOREPO_ROOT = path.join(__dirname, '..', '..', '..', '..', '..');
|
|
21
|
+
const ASSETS_DIR = path.join(MONOREPO_ROOT, 'packages', 'cli', 'src', 'assets', 'instructions');
|
|
22
|
+
const AI_DIR = path.join(MONOREPO_ROOT, 'packages', 'cli', '.ai', 'instructions');
|
|
23
|
+
|
|
24
|
+
// why: flavor/ in .ai/ is populated by renaming flavors/{id}/ — no direct mirror exists in src/assets/flavor/
|
|
25
|
+
const MIRRORED_DIRS = ['core', 'idioms', 'templates', 'competencies'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compares files present in .ai/ mirrored directories against src/assets/.
|
|
29
|
+
* Reports any files that exist in .ai/ but differ from their src/assets/ counterpart.
|
|
30
|
+
*/
|
|
31
|
+
function run() {
|
|
32
|
+
const driftedFiles = [];
|
|
33
|
+
|
|
34
|
+
for (const mirroredDir of MIRRORED_DIRS) {
|
|
35
|
+
const liveDir = path.join(AI_DIR, mirroredDir);
|
|
36
|
+
const sourceDir = path.join(ASSETS_DIR, mirroredDir);
|
|
37
|
+
const dirDrifts = collectDriftedFiles(liveDir, sourceDir, mirroredDir);
|
|
38
|
+
driftedFiles.push(...dirDrifts);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const runResult = reportResult(driftedFiles);
|
|
42
|
+
return runResult;
|
|
43
|
+
|
|
44
|
+
// --- Scoped Internal Helpers (The Stepdown Rule & Lexical Scoping) ---
|
|
45
|
+
|
|
46
|
+
function collectDriftedFiles(liveDir, sourceDir, relPrefix) {
|
|
47
|
+
if (!fs.existsSync(liveDir)) return [];
|
|
48
|
+
|
|
49
|
+
const entries = fs.readdirSync(liveDir, { withFileTypes: true });
|
|
50
|
+
const localDrifts = [];
|
|
51
|
+
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const relPath = path.join(relPrefix, entry.name);
|
|
54
|
+
const livePath = path.join(liveDir, entry.name);
|
|
55
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
56
|
+
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
const nestedDrifts = collectDriftedFiles(livePath, sourcePath, relPath);
|
|
59
|
+
localDrifts.push(...nestedDrifts);
|
|
60
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
61
|
+
const fileDrift = checkFileDrift(livePath, sourcePath, relPath);
|
|
62
|
+
if (fileDrift !== null) localDrifts.push(fileDrift);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return localDrifts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function checkFileDrift(livePath, sourcePath, relPath) {
|
|
70
|
+
if (!fs.existsSync(sourcePath)) {
|
|
71
|
+
const missingDrift = { relPath, reason: 'missing in src/assets/' };
|
|
72
|
+
return missingDrift;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const liveHash = hashFile(livePath);
|
|
76
|
+
const sourceHash = hashFile(sourcePath);
|
|
77
|
+
|
|
78
|
+
if (liveHash !== sourceHash) {
|
|
79
|
+
const contentDrift = { relPath, reason: 'content differs' };
|
|
80
|
+
return contentDrift;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
|
|
85
|
+
function hashFile(filePath) {
|
|
86
|
+
const content = fs.readFileSync(filePath);
|
|
87
|
+
const fileHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
88
|
+
return fileHash;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function reportResult(drifts) {
|
|
93
|
+
if (drifts.length === 0) {
|
|
94
|
+
console.log('\n ✅ .ai/instructions/ is in sync with src/assets/instructions/\n');
|
|
95
|
+
const syncOk = success();
|
|
96
|
+
return syncOk;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.error('\n ❌ Drift detected — files in .ai/ differ from src/assets/:\n');
|
|
100
|
+
for (const drift of drifts) {
|
|
101
|
+
console.error(` ${drift.relPath} (${drift.reason})`);
|
|
102
|
+
}
|
|
103
|
+
console.error(
|
|
104
|
+
'\n Fix: apply the same edits to both copies, or re-run `npx sdg-agents init` to regenerate.\n'
|
|
105
|
+
);
|
|
106
|
+
const driftFailure = fail({ message: 'SYNC_DRIFT', count: drifts.length });
|
|
107
|
+
return driftFailure;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const SyncChecker = { run };
|
|
112
|
+
|
|
113
|
+
runIfDirect(import.meta.url, async () => {
|
|
114
|
+
const result = run();
|
|
115
|
+
if (result.isFailure) process.exit(1);
|
|
116
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { confirm } from '@inquirer/prompts';
|
|
4
|
+
import { ResultUtils } from '../lib/result-utils.mjs';
|
|
5
|
+
import { FsUtils } from '../lib/fs-utils.mjs';
|
|
6
|
+
|
|
7
|
+
const { success } = ResultUtils;
|
|
8
|
+
const { runIfDirect } = FsUtils;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Spec Driven Guide — Reset/Clear Utility
|
|
12
|
+
* Deletes all generated SDG files and the .ia directory.
|
|
13
|
+
*/
|
|
14
|
+
async function run(targetDir = process.cwd(), options = {}) {
|
|
15
|
+
const dryRun = options.dryRun || process.argv.includes('--dry-run');
|
|
16
|
+
|
|
17
|
+
console.log('\n Spec Driven Guide — Clear Generated Content');
|
|
18
|
+
console.log(' ' + '─'.repeat(50));
|
|
19
|
+
|
|
20
|
+
const itemsToRemove = ['.ia', '.ai', '.sdg-prompts'];
|
|
21
|
+
let existing = [];
|
|
22
|
+
|
|
23
|
+
// Check current directory
|
|
24
|
+
for (const item of itemsToRemove) {
|
|
25
|
+
const fullPath = path.join(targetDir, item);
|
|
26
|
+
if (fs.existsSync(fullPath)) {
|
|
27
|
+
existing.push({ name: item, fullPath });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check packages/* if we are in a monorepo
|
|
32
|
+
const packagesDir = path.join(targetDir, 'packages');
|
|
33
|
+
if (fs.existsSync(packagesDir)) {
|
|
34
|
+
const subPackages = fs.readdirSync(packagesDir);
|
|
35
|
+
for (const pkg of subPackages) {
|
|
36
|
+
for (const item of itemsToRemove) {
|
|
37
|
+
const fullPath = path.join(packagesDir, pkg, item);
|
|
38
|
+
if (fs.existsSync(fullPath)) {
|
|
39
|
+
existing.push({ name: `packages/${pkg}/${item}`, fullPath });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (existing.length === 0) {
|
|
46
|
+
console.log('\n ✅ No Spec Driven Guide content found to clear.\n');
|
|
47
|
+
return success();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
console.log('\n [DRY RUN] The following items would be removed:');
|
|
52
|
+
for (const item of existing) {
|
|
53
|
+
console.log(` - ${item.name}`);
|
|
54
|
+
}
|
|
55
|
+
console.log('\n No files were deleted (dry-run mode).\n');
|
|
56
|
+
return success();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
printWarning();
|
|
60
|
+
printItemsToRemove(existing);
|
|
61
|
+
|
|
62
|
+
const userConfirmed = await confirm({
|
|
63
|
+
message: '\n Are you sure you want to proceed?',
|
|
64
|
+
default: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!userConfirmed) {
|
|
68
|
+
console.log('\n Aborted. No files were deleted.\n');
|
|
69
|
+
return success();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
executeCleanup(existing);
|
|
73
|
+
|
|
74
|
+
console.log('\n ✨ Project cleared successfully!\n');
|
|
75
|
+
return success();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printWarning() {
|
|
79
|
+
console.log('\n');
|
|
80
|
+
console.log(' ┌──────────────────────────────────────────────────────┐ ');
|
|
81
|
+
console.log(' │ ⚠️ WARNING: PERMANENT DELETION DETECTED │ ');
|
|
82
|
+
console.log(' │ This action will IRREVERSIBLY remove all SDG rules, │ ');
|
|
83
|
+
console.log(' │ AI instructions, and project manifests. │ ');
|
|
84
|
+
console.log(' └──────────────────────────────────────────────────────┘ ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function printItemsToRemove(items) {
|
|
88
|
+
console.log('\n The following items will be REMOVED:');
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
console.log(` - ${item.name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function executeCleanup(items) {
|
|
95
|
+
console.log('\n Cleaning up...');
|
|
96
|
+
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
const { name, fullPath } = item;
|
|
99
|
+
try {
|
|
100
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
101
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
102
|
+
} else {
|
|
103
|
+
fs.unlinkSync(fullPath);
|
|
104
|
+
}
|
|
105
|
+
console.log(` [OK] Removed ${name}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.log(` [FAIL] Could not remove ${name}: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const Cleaner = {
|
|
113
|
+
run,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
runIfDirect(import.meta.url, run);
|