clckernel 1.2.19 → 2.0.1
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/.agents/skills/clckernel_cli/SKILL.md +23 -6
- package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
- package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
- package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
- package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
- package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
- package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
- package/.agents/skills/clckernel_cli/reference/start.md +14 -4
- package/.agents/skills/clckernel_cli/reference/test.md +1 -1
- package/.agents/skills/clckernel_cli/reference/update.md +19 -19
- package/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.es.md +280 -51
- package/README.md +274 -50
- package/bin/deprecated.js +13 -0
- package/package.json +14 -4
- package/src/adapters/agent-os.js +161 -84
- package/src/adapters/astro.js +8 -6
- package/src/adapters/base.js +23 -38
- package/src/adapters/django.js +14 -13
- package/src/adapters/fastapi.js +16 -15
- package/src/adapters/go.js +11 -9
- package/src/adapters/laravel.js +12 -10
- package/src/adapters/nextjs.js +7 -5
- package/src/adapters/rails.js +13 -11
- package/src/adapters/rust.js +11 -9
- package/src/catalog.js +58 -49
- package/src/config.js +38 -240
- package/src/detector.js +243 -44
- package/src/doctor.js +18 -9
- package/src/generator.js +734 -302
- package/src/gitignore.js +60 -14
- package/src/index.js +257 -18
- package/src/io.js +409 -0
- package/src/known-hashes.json +309 -0
- package/src/onboarding.js +15 -0
- package/src/technologies/detector.js +1 -1
- package/src/technologies/guards/_shared.js +1 -1
- package/src/ui.js +233 -40
- package/src/updater.js +271 -38
- package/tools/_changes.js +279 -0
- package/tools/_changes.py +233 -0
- package/tools/_config.js +294 -0
- package/tools/_config.py +271 -0
- package/tools/_yaml.py +489 -0
- package/tools/audit.js +511 -111
- package/tools/audit.py +506 -128
- package/tools/celery_guard.py +13 -35
- package/tools/check_a11y.js +8 -28
- package/tools/check_api_contracts.js +13 -39
- package/tools/check_architecture.js +35 -44
- package/tools/check_architecture.py +172 -105
- package/tools/check_custom.js +108 -61
- package/tools/check_custom.py +108 -271
- package/tools/check_data_files.js +88 -0
- package/tools/check_db_efficiency.py +80 -65
- package/tools/check_hitl.js +112 -69
- package/tools/check_links.js +138 -0
- package/tools/check_migrations.py +143 -94
- package/tools/check_performance.js +35 -44
- package/tools/check_placeholders.js +104 -0
- package/tools/check_responsive.js +8 -28
- package/tools/check_scope.py +40 -31
- package/tools/check_seo.js +8 -28
- package/tools/check_skills.js +158 -0
- package/tools/check_storybook.js +8 -21
- package/tools/check_symlinks.js +40 -22
- package/tools/check_ui_reuse.js +17 -29
- package/tools/docker_guard.py +19 -41
- package/tools/postgres_guard.py +15 -36
- package/tools/redis_guard.py +13 -35
- package/tools/scan_secrets.js +134 -67
- package/tools/scan_secrets.py +163 -72
- package/tools/verify_tdd.js +867 -0
- package/tools/verify_tdd.py +407 -165
- package/tools/yaml.js +404 -132
- package/tools/check_domain_data.js +0 -100
- package/tools/check_test_calendar_sync.js +0 -111
package/src/gitignore.js
CHANGED
|
@@ -4,9 +4,42 @@
|
|
|
4
4
|
* are safely ignored in consumer repositories to preserve AGENTS.md as the SSOT.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const fs = require('fs');
|
|
8
7
|
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const { currentIO } = require('./io');
|
|
9
10
|
|
|
11
|
+
// Repository-locating variables a hook may export; they would point git at
|
|
12
|
+
// another repository than the target's.
|
|
13
|
+
const GIT_LOCAL_ENV = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_PREFIX', 'GIT_COMMON_DIR'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Entries whose path (or, for a directory entry, anything under it) is
|
|
17
|
+
* tracked by git in targetDir (sdds/update-no-additions, decision 3). Only
|
|
18
|
+
* literal paths are checked: negations and glob patterns are kept as they
|
|
19
|
+
* are. Outside a git repository nothing is tracked. One `git ls-files` call.
|
|
20
|
+
* @param {string} targetDir - Project root
|
|
21
|
+
* @param {string[]} entries - Candidate .gitignore entries
|
|
22
|
+
* @returns {Set<string>}
|
|
23
|
+
*/
|
|
24
|
+
function trackedEntries(targetDir, entries) {
|
|
25
|
+
const literal = entries.filter((e) => !e.startsWith('!') && !/[*?[\\]/.test(e));
|
|
26
|
+
if (literal.length === 0) return new Set();
|
|
27
|
+
const env = { ...process.env };
|
|
28
|
+
for (const key of GIT_LOCAL_ENV) delete env[key];
|
|
29
|
+
const bare = (e) => e.replace(/\/+$/, '');
|
|
30
|
+
const res = spawnSync('git', ['ls-files', '-z', '--', ...literal.map((e) => `:(literal)${bare(e)}`)], {
|
|
31
|
+
cwd: targetDir, env, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
});
|
|
33
|
+
if (res.error || res.status !== 0) return new Set();
|
|
34
|
+
const files = res.stdout.split('\0').filter(Boolean);
|
|
35
|
+
return new Set(literal.filter((e) => files.some((f) => f === bare(e) || f.startsWith(`${bare(e)}/`))));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Managed entries per section. An `{ entry, ifExists: true }` item names an
|
|
40
|
+
* artifact of an older release (sdds/onboarding): it is added only when that
|
|
41
|
+
* path exists in the project, so a fresh install names no retired artifact.
|
|
42
|
+
*/
|
|
10
43
|
const MANAGED_GITIGNORE_SECTIONS = [
|
|
11
44
|
{
|
|
12
45
|
header: '# CLC Kernel: Local AI IDE Mirror Symlinks (SSOT: AGENTS.md)',
|
|
@@ -24,7 +57,7 @@ const MANAGED_GITIGNORE_SECTIONS = [
|
|
|
24
57
|
header: '# CLC Kernel: CLI Agent Skills & Playbooks',
|
|
25
58
|
entries: [
|
|
26
59
|
'.agents/skills/clckernel_cli/',
|
|
27
|
-
'.agents/skills/clc_forge_cli/',
|
|
60
|
+
{ entry: '.agents/skills/clc_forge_cli/', ifExists: true },
|
|
28
61
|
],
|
|
29
62
|
},
|
|
30
63
|
{
|
|
@@ -34,10 +67,11 @@ const MANAGED_GITIGNORE_SECTIONS = [
|
|
|
34
67
|
'!sdds/.gitkeep',
|
|
35
68
|
'openspec/*',
|
|
36
69
|
'!openspec/.gitkeep',
|
|
37
|
-
'.clckernel.resolved.json',
|
|
38
|
-
'.clc-forge.resolved.json',
|
|
70
|
+
{ entry: '.clckernel.resolved.json', ifExists: true },
|
|
71
|
+
{ entry: '.clc-forge.resolved.json', ifExists: true },
|
|
39
72
|
'.husky/_/',
|
|
40
73
|
'.ruff_cache/',
|
|
74
|
+
'*.clckernel-new',
|
|
41
75
|
],
|
|
42
76
|
},
|
|
43
77
|
];
|
|
@@ -45,19 +79,18 @@ const MANAGED_GITIGNORE_SECTIONS = [
|
|
|
45
79
|
/**
|
|
46
80
|
* Idempotently appends missing CLC Kernel entries to target project's .gitignore.
|
|
47
81
|
*
|
|
82
|
+
* Writes through the active io session (sdds/safe-update), so a dry run
|
|
83
|
+
* only records the entries it would add. An entry whose path git tracks is
|
|
84
|
+
* not added (sdds/update-no-additions, decision 3).
|
|
85
|
+
*
|
|
48
86
|
* @param {string} targetDir - Target project root
|
|
49
87
|
* @returns {boolean} True if .gitignore was created or modified, false if already complete.
|
|
50
88
|
*/
|
|
51
89
|
function ensureGitignoreEntries(targetDir) {
|
|
90
|
+
const io = currentIO(targetDir);
|
|
52
91
|
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
currentContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
57
|
-
} catch (e) {
|
|
58
|
-
currentContent = '';
|
|
59
|
-
}
|
|
60
|
-
}
|
|
92
|
+
const existing = io.read(gitignorePath);
|
|
93
|
+
const currentContent = existing ? existing.toString('utf-8') : '';
|
|
61
94
|
|
|
62
95
|
const existingLines = new Set(
|
|
63
96
|
currentContent
|
|
@@ -67,11 +100,24 @@ function ensureGitignoreEntries(targetDir) {
|
|
|
67
100
|
);
|
|
68
101
|
|
|
69
102
|
const sectionsToAppend = [];
|
|
103
|
+
const added = [];
|
|
104
|
+
|
|
105
|
+
const entriesOf = (section) => section.entries
|
|
106
|
+
.filter(item => typeof item === 'string' || io.exists(path.join(targetDir, item.entry)))
|
|
107
|
+
.map(item => (typeof item === 'string' ? item : item.entry));
|
|
108
|
+
|
|
109
|
+
// A path the project tracks is never ignored behind its back: the entry is
|
|
110
|
+
// left out and reported instead.
|
|
111
|
+
const candidates = MANAGED_GITIGNORE_SECTIONS.flatMap(entriesOf)
|
|
112
|
+
.filter(entry => !existingLines.has(entry.trim()));
|
|
113
|
+
const tracked = trackedEntries(targetDir, candidates);
|
|
114
|
+
for (const entry of tracked) io.noteTrackedNotIgnored(entry);
|
|
70
115
|
|
|
71
116
|
for (const section of MANAGED_GITIGNORE_SECTIONS) {
|
|
72
|
-
const missingEntries = section.
|
|
117
|
+
const missingEntries = entriesOf(section).filter(entry => !existingLines.has(entry.trim()) && !tracked.has(entry));
|
|
73
118
|
if (missingEntries.length > 0) {
|
|
74
119
|
sectionsToAppend.push(`${section.header}\n${missingEntries.join('\n')}`);
|
|
120
|
+
added.push(...missingEntries);
|
|
75
121
|
}
|
|
76
122
|
}
|
|
77
123
|
|
|
@@ -88,7 +134,7 @@ function ensureGitignoreEntries(targetDir) {
|
|
|
88
134
|
}
|
|
89
135
|
newContent += sectionsToAppend.join('\n\n') + '\n';
|
|
90
136
|
|
|
91
|
-
|
|
137
|
+
io.appendGitignore(gitignorePath, newContent, added);
|
|
92
138
|
return true;
|
|
93
139
|
}
|
|
94
140
|
|
package/src/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Coordinates auto-discovery, UI rendering, generator engine, and doctor checks.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
7
9
|
const { autoDetectStack } = require('./detector');
|
|
8
|
-
const { printBanner, printBox, promptOptions, printSuccess, colors } = require('./ui');
|
|
10
|
+
const { printBanner, printBox, promptOptions, printSuccess, partLines, colors } = require('./ui');
|
|
9
11
|
const { generateHarness } = require('./generator');
|
|
10
12
|
const { runDoctor } = require('./doctor');
|
|
11
13
|
|
|
@@ -18,28 +20,102 @@ const READ_ONLY_COMMANDS = new Set(['help', '--help', '-h', 'version', '--versio
|
|
|
18
20
|
*/
|
|
19
21
|
const INSTALL_FLAGS = new Set(['--yes', '-y']);
|
|
20
22
|
|
|
23
|
+
/** Prints the plan instead of writing (sdds/safe-update, decision 5). Not an opt-in by itself. */
|
|
24
|
+
const DRY_RUN_FLAG = '--dry-run';
|
|
25
|
+
|
|
26
|
+
/** Opts guards the project does not have into an update (sdds/update-no-additions). */
|
|
27
|
+
const ADD_GUARDS_FLAG = '--add-guards';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses the arguments after `update`: at most one target dir, --dry-run,
|
|
31
|
+
* and `--add-guards <a,b|all>` (or `--add-guards=<...>`). The names are
|
|
32
|
+
* checked against the stack's manifest by the updater, before any write.
|
|
33
|
+
* @param {string[]} rest - Arguments without the `update` word
|
|
34
|
+
* @returns {{ positionals: string[], dryRun: boolean, addGuards?: string[]|'all' }|null}
|
|
35
|
+
* null when the input is not understood
|
|
36
|
+
*/
|
|
37
|
+
function parseUpdateArgs(rest) {
|
|
38
|
+
const positionals = [];
|
|
39
|
+
let dryRun = false;
|
|
40
|
+
let addGuards;
|
|
41
|
+
for (let i = 0; i < rest.length; i++) {
|
|
42
|
+
const arg = rest[i];
|
|
43
|
+
let value = null;
|
|
44
|
+
if (arg === DRY_RUN_FLAG) {
|
|
45
|
+
dryRun = true;
|
|
46
|
+
continue;
|
|
47
|
+
} else if (arg === ADD_GUARDS_FLAG) {
|
|
48
|
+
value = rest[i + 1];
|
|
49
|
+
if (value === undefined || value.startsWith('-')) return null;
|
|
50
|
+
i++;
|
|
51
|
+
} else if (arg.startsWith(`${ADD_GUARDS_FLAG}=`)) {
|
|
52
|
+
value = arg.slice(ADD_GUARDS_FLAG.length + 1);
|
|
53
|
+
} else if (arg.startsWith('-')) {
|
|
54
|
+
return null;
|
|
55
|
+
} else {
|
|
56
|
+
positionals.push(arg);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (addGuards !== undefined) return null;
|
|
60
|
+
const names = value.split(',').map((n) => n.trim()).filter(Boolean);
|
|
61
|
+
if (names.length === 0) return null;
|
|
62
|
+
addGuards = names.includes('all') ? 'all' : names;
|
|
63
|
+
}
|
|
64
|
+
if (positionals.length > 1) return null;
|
|
65
|
+
return { positionals, dryRun, addGuards };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Words AGENTS.md documents as agent commands (`clckernel test`, ...). They are
|
|
70
|
+
* executed by the AI agent, not by this CLI; without this list they would be
|
|
71
|
+
* taken as an install target directory.
|
|
72
|
+
*/
|
|
73
|
+
const AGENT_COMMANDS = new Set(['test', 'start', 'feature', 'fix', 'commit', 'guard']);
|
|
74
|
+
|
|
21
75
|
const HELP_TEXT = `
|
|
22
76
|
${'clckernel'} — CLC Kernel: The AI Agent Governance Engine
|
|
23
77
|
|
|
24
78
|
USAGE
|
|
25
79
|
clckernel <command> [targetDir]
|
|
26
80
|
|
|
27
|
-
READ-ONLY COMMANDS (
|
|
81
|
+
READ-ONLY COMMANDS (do not modify your project's code or config)
|
|
28
82
|
help, --help, -h Show this help
|
|
29
83
|
version, --version, -v Print the installed version
|
|
30
84
|
doctor [targetDir] Run diagnostics on a project
|
|
31
|
-
|
|
85
|
+
audit [targetDir] [--change <name>]
|
|
86
|
+
Run the installed audit orchestrators (tools/audit.js
|
|
87
|
+
and/or tools/audit.py) and print the summary. Writes
|
|
88
|
+
nothing, unless --change <name>: then the report and
|
|
89
|
+
receipt go to sdds/<name>/ (only there)
|
|
90
|
+
Checks staged changes, or the last commit if none
|
|
32
91
|
|
|
33
|
-
|
|
92
|
+
WRITES TO THE TARGET PROJECT
|
|
93
|
+
update [targetDir] Refresh tools, hooks, playbooks, IDE symlinks,
|
|
94
|
+
.gitignore and AGENTS.md. A kernel file you
|
|
95
|
+
edited is kept; the new version is written
|
|
96
|
+
next to it as <file>.clckernel-new. Only the
|
|
97
|
+
guards and mirrors you have are refreshed;
|
|
98
|
+
none are added
|
|
99
|
+
--add-guards <a,b|all> With update: also install these guards
|
|
34
100
|
clckernel [targetDir] --yes Provision the governance harness
|
|
101
|
+
--dry-run With update or --yes: print the plan, write nothing
|
|
102
|
+
|
|
103
|
+
Installation requires an explicit opt-in and an existing target directory.
|
|
104
|
+
Bare \`clckernel\` runs the interactive confirmation flow; with no TTY it
|
|
105
|
+
refuses to install.
|
|
35
106
|
|
|
36
|
-
|
|
37
|
-
|
|
107
|
+
AGENT COMMANDS
|
|
108
|
+
test, start, feature, fix, commit, guard are defined in AGENTS.md and are
|
|
109
|
+
run by your AI agent, not by this CLI.
|
|
38
110
|
|
|
39
111
|
EXAMPLES
|
|
40
112
|
clckernel --help
|
|
41
113
|
clckernel doctor .
|
|
114
|
+
clckernel audit .
|
|
115
|
+
clckernel audit . --change my-feature
|
|
42
116
|
clckernel update .
|
|
117
|
+
clckernel update . --dry-run
|
|
118
|
+
clckernel update . --add-guards check_skills
|
|
43
119
|
clckernel . --yes
|
|
44
120
|
`;
|
|
45
121
|
|
|
@@ -59,6 +135,109 @@ function rejectUnknownInput(args) {
|
|
|
59
135
|
return 1;
|
|
60
136
|
}
|
|
61
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Explains that an AGENTS.md agent command is not a CLI command.
|
|
140
|
+
* @param {string} word - The reserved word the user typed
|
|
141
|
+
* @returns {number} Process exit code
|
|
142
|
+
*/
|
|
143
|
+
function rejectAgentCommand(word) {
|
|
144
|
+
process.stderr.write(
|
|
145
|
+
`\n❌ \`${word}\` is an agent command defined in AGENTS.md, not a CLI command.\n` +
|
|
146
|
+
' Ask your AI agent to run it, or run `clckernel --help` to see CLI commands.\n\n'
|
|
147
|
+
);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolves a user-supplied directory and checks it exists and is a directory.
|
|
153
|
+
* Prints the error itself so callers only decide the exit code.
|
|
154
|
+
* @param {string|undefined} input - Raw path argument (cwd when absent)
|
|
155
|
+
* @returns {string|null} Absolute path, or null when unusable
|
|
156
|
+
*/
|
|
157
|
+
function resolveExistingDir(input) {
|
|
158
|
+
const dir = input ? path.resolve(input) : process.cwd();
|
|
159
|
+
let stat;
|
|
160
|
+
try {
|
|
161
|
+
stat = fs.statSync(dir);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
process.stderr.write(`\n❌ Target directory does not exist: ${dir}\n Nothing was written.\n\n`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
if (!stat.isDirectory()) {
|
|
167
|
+
process.stderr.write(`\n❌ Target is not a directory: ${dir}\n Nothing was written.\n\n`);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return dir;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `clckernel audit [dir] [--change <name>]`: runs every orchestrator installed
|
|
175
|
+
* in dir/tools (audit.js, then audit.py) with cwd = dir. A mixed stack has
|
|
176
|
+
* both, and each only discovers guards of its own language, so all of them
|
|
177
|
+
* must run. --change is passed through (the orchestrators validate the name).
|
|
178
|
+
* @param {string|undefined} input - Target directory argument
|
|
179
|
+
* @param {string|null} [change] - SDD change name for the report, or null
|
|
180
|
+
* @returns {number} Exit code: the first non-zero orchestrator status, else 0
|
|
181
|
+
*/
|
|
182
|
+
function runAudit(input, change = null) {
|
|
183
|
+
const dir = resolveExistingDir(input);
|
|
184
|
+
if (!dir) return 1;
|
|
185
|
+
|
|
186
|
+
const candidates = [
|
|
187
|
+
{ file: 'audit.js', cmd: process.execPath },
|
|
188
|
+
{ file: 'audit.py', cmd: 'python3' },
|
|
189
|
+
];
|
|
190
|
+
const orchestrators = candidates.filter((c) => fs.existsSync(path.join(dir, 'tools', c.file)));
|
|
191
|
+
if (orchestrators.length === 0) {
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
`\n❌ No audit orchestrator found in ${path.join(dir, 'tools')} (audit.js or audit.py).\n` +
|
|
194
|
+
` Install the harness first: clckernel ${input || '.'} --yes\n\n`
|
|
195
|
+
);
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let exitCode = 0;
|
|
200
|
+
for (const orchestrator of orchestrators) {
|
|
201
|
+
const orchestratorArgs = [path.join('tools', orchestrator.file), ...(change ? ['--change', change] : [])];
|
|
202
|
+
const res = spawnSync(orchestrator.cmd, orchestratorArgs, {
|
|
203
|
+
cwd: dir,
|
|
204
|
+
stdio: 'inherit',
|
|
205
|
+
});
|
|
206
|
+
let status;
|
|
207
|
+
if (res.error) {
|
|
208
|
+
process.stderr.write(`\n❌ Could not run tools/${orchestrator.file}: ${res.error.message}\n\n`);
|
|
209
|
+
status = 1;
|
|
210
|
+
} else {
|
|
211
|
+
status = res.status === null ? 1 : res.status;
|
|
212
|
+
}
|
|
213
|
+
if (status !== 0 && exitCode === 0) exitCode = status;
|
|
214
|
+
}
|
|
215
|
+
return exitCode;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `audit` arguments: at most one target dir and one `--change <name>`.
|
|
220
|
+
* @param {string[]} rest - Arguments after `audit`
|
|
221
|
+
* @returns {{ targetDir?: string, change: string|null } | null} null on unknown input
|
|
222
|
+
*/
|
|
223
|
+
function parseAuditArgs(rest) {
|
|
224
|
+
const out = { targetDir: undefined, change: null };
|
|
225
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
226
|
+
const arg = rest[i];
|
|
227
|
+
if (arg === '--change') {
|
|
228
|
+
const name = rest[i + 1];
|
|
229
|
+
if (out.change !== null || name === undefined || name.startsWith('-')) return null;
|
|
230
|
+
out.change = name;
|
|
231
|
+
i += 1;
|
|
232
|
+
} else if (arg.startsWith('-') || out.targetDir !== undefined) {
|
|
233
|
+
return null;
|
|
234
|
+
} else {
|
|
235
|
+
out.targetDir = arg;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
62
241
|
async function runCli() {
|
|
63
242
|
const args = process.argv.slice(2);
|
|
64
243
|
const command = args[0];
|
|
@@ -78,16 +257,39 @@ async function runCli() {
|
|
|
78
257
|
return;
|
|
79
258
|
}
|
|
80
259
|
|
|
260
|
+
if (AGENT_COMMANDS.has(command)) {
|
|
261
|
+
process.exitCode = rejectAgentCommand(command);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === 'audit') {
|
|
266
|
+
const parsed = parseAuditArgs(args.slice(1));
|
|
267
|
+
if (!parsed) {
|
|
268
|
+
process.exitCode = rejectUnknownInput(args);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
process.exitCode = runAudit(parsed.targetDir, parsed.change);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
81
275
|
if (command === 'doctor' || args.includes('--doctor')) {
|
|
82
276
|
const target = args[1] && args[1] !== '--doctor' ? args[1] : (args[0] === 'doctor' ? null : args[0]);
|
|
83
|
-
runDoctor(target ? path.resolve(target) : process.cwd());
|
|
277
|
+
const failures = runDoctor(target ? path.resolve(target) : process.cwd());
|
|
278
|
+
if (failures > 0) process.exitCode = 1;
|
|
84
279
|
return;
|
|
85
280
|
}
|
|
86
281
|
|
|
87
282
|
if (command === 'update' || args.includes('--update')) {
|
|
88
|
-
const
|
|
283
|
+
const rest = [...args];
|
|
284
|
+
rest.splice(rest.indexOf(command === 'update' ? 'update' : '--update'), 1);
|
|
285
|
+
const parsed = parseUpdateArgs(rest);
|
|
286
|
+
if (!parsed) {
|
|
287
|
+
process.exitCode = rejectUnknownInput(args);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
89
290
|
const { runUpdate } = require('./updater');
|
|
90
|
-
|
|
291
|
+
const target = parsed.positionals[0];
|
|
292
|
+
await runUpdate(target ? path.resolve(target) : process.cwd(), { dryRun: parsed.dryRun, addGuards: parsed.addGuards });
|
|
91
293
|
return;
|
|
92
294
|
}
|
|
93
295
|
|
|
@@ -95,15 +297,26 @@ async function runCli() {
|
|
|
95
297
|
// or an explicit install flag.
|
|
96
298
|
const flags = args.filter((a) => a.startsWith('-'));
|
|
97
299
|
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
98
|
-
const unknownFlags = flags.filter((f) => !INSTALL_FLAGS.has(f));
|
|
300
|
+
const unknownFlags = flags.filter((f) => !INSTALL_FLAGS.has(f) && f !== DRY_RUN_FLAG);
|
|
301
|
+
const dryRun = flags.includes(DRY_RUN_FLAG);
|
|
99
302
|
|
|
100
303
|
if (unknownFlags.length > 0 || positionals.length > 1) {
|
|
101
304
|
process.exitCode = rejectUnknownInput(args);
|
|
102
305
|
return;
|
|
103
306
|
}
|
|
104
307
|
|
|
105
|
-
|
|
106
|
-
|
|
308
|
+
if (positionals.length === 1 && AGENT_COMMANDS.has(positionals[0])) {
|
|
309
|
+
process.exitCode = rejectAgentCommand(positionals[0]);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A mistyped path must never become a freshly created directory.
|
|
314
|
+
const targetDir = resolveExistingDir(positionals[0]);
|
|
315
|
+
if (!targetDir) {
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const explicitInstall = flags.some((f) => INSTALL_FLAGS.has(f));
|
|
107
320
|
const isInteractive = Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
108
321
|
|
|
109
322
|
if (!explicitInstall && !isInteractive) {
|
|
@@ -116,6 +329,20 @@ async function runCli() {
|
|
|
116
329
|
return;
|
|
117
330
|
}
|
|
118
331
|
|
|
332
|
+
// The project config is read before the prompt and before any write: an
|
|
333
|
+
// invalid one stops the install here (sdds/config-foundation).
|
|
334
|
+
const { readProjectConfig } = require('./config');
|
|
335
|
+
let projectConfig;
|
|
336
|
+
try {
|
|
337
|
+
projectConfig = readProjectConfig(targetDir);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err.name !== 'ConfigError') throw err;
|
|
340
|
+
process.stderr.write(`\n❌ ${err.message}\n Nothing was written.\n\n`);
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
for (const warning of projectConfig.warnings) process.stderr.write(`⚠ ${warning}\n`);
|
|
345
|
+
|
|
119
346
|
// 1. Print Pro ASCII Banner
|
|
120
347
|
printBanner();
|
|
121
348
|
|
|
@@ -124,15 +351,18 @@ async function runCli() {
|
|
|
124
351
|
|
|
125
352
|
// 3. Render Formatted Configuration Box
|
|
126
353
|
printBox('🔍 CONFIGURACIÓN DETECTADA AUTOMÁTICAMENTE', [
|
|
354
|
+
{ label: '📁 Target', value: targetDir, badgeColor: colors.cyan },
|
|
127
355
|
{ label: '📦 Tipo de Proyecto', value: detected.projectType.toUpperCase(), badgeColor: colors.amber },
|
|
128
356
|
{ label: '🚀 Framework', value: detected.framework, badgeColor: colors.emerald },
|
|
129
357
|
{ label: '🎨 Estilos & UI', value: `${detected.styling} (${detected.uiLibrary})`, badgeColor: colors.violet },
|
|
130
358
|
{ label: '🧪 Test Runner', value: detected.testRunner, badgeColor: colors.cyan },
|
|
131
|
-
{ label: '🔒 Git Hooks', value: detected.gitHooks, badgeColor: colors.blue }
|
|
359
|
+
{ label: '🔒 Git Hooks', value: detected.gitHooks, badgeColor: colors.blue },
|
|
360
|
+
// Multi-part repos (sdds/fullstack-monorepo): one row per part.
|
|
361
|
+
...partLines(detected).map((line) => ({ label: '🧩 Parte', value: line, badgeColor: colors.emerald }))
|
|
132
362
|
]);
|
|
133
363
|
|
|
134
364
|
// 4. Prompt User for Confirmation or Override
|
|
135
|
-
const finalConfig = await promptOptions(detected);
|
|
365
|
+
const finalConfig = await promptOptions(detected, { targetDir });
|
|
136
366
|
|
|
137
367
|
// 5. Generate Harness & Safeguards
|
|
138
368
|
let s;
|
|
@@ -142,14 +372,23 @@ async function runCli() {
|
|
|
142
372
|
s.start('Configurando salvaguardas y motor de gobernanza...');
|
|
143
373
|
}
|
|
144
374
|
|
|
145
|
-
generateHarness(targetDir, finalConfig);
|
|
375
|
+
const { installedGuards, hook, plan } = generateHarness(targetDir, finalConfig, { dryRun, projectConfig }) || {};
|
|
146
376
|
|
|
147
377
|
if (s) {
|
|
148
|
-
s.stop('Salvaguardas y motor de gobernanza configurados.');
|
|
378
|
+
s.stop(dryRun ? 'Plan listo (dry run).' : 'Salvaguardas y motor de gobernanza configurados.');
|
|
149
379
|
}
|
|
150
380
|
|
|
151
|
-
// 6. Render Pro Success Card
|
|
152
|
-
|
|
381
|
+
// 6. Render Pro Success Card (or, for a dry run, only the plan)
|
|
382
|
+
const { printPlan } = require('./ui');
|
|
383
|
+
if (dryRun) {
|
|
384
|
+
if (plan) printPlan(plan, { dryRun: true });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
printSuccess(targetDir, hook, finalConfig, installedGuards);
|
|
388
|
+
if (plan) printPlan(plan);
|
|
389
|
+
// sdds/onboarding: what to do next, true to this install.
|
|
390
|
+
const { nextStepsLines } = require('./ui');
|
|
391
|
+
console.log(`\n${nextStepsLines(hook, plan, finalConfig).join('\n')}\n`);
|
|
153
392
|
}
|
|
154
393
|
|
|
155
394
|
module.exports = { runCli };
|