start-vibing-stacks 2.5.1 → 2.7.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.
- package/dist/detector.js +5 -2
- package/dist/index.js +16 -2
- package/dist/migrate.d.ts +27 -0
- package/dist/migrate.js +217 -0
- package/dist/scanner.js +91 -0
- package/dist/setup.js +10 -0
- package/package.json +1 -1
- package/stacks/_shared/agents/claude-md-compactor.md +1 -0
- package/stacks/_shared/agents/commit-manager.md +1 -0
- package/stacks/_shared/agents/documenter.md +1 -0
- package/stacks/_shared/agents/domain-updater.md +1 -0
- package/stacks/_shared/agents/research-web.md +1 -0
- package/stacks/_shared/agents/security-auditor.md +168 -0
- package/stacks/_shared/agents/tester.md +1 -0
- package/stacks/_shared/hooks/final-check.ts +205 -0
- package/stacks/_shared/hooks/stop-validator.ts +77 -1
- package/stacks/_shared/skills/accessibility-wcag22/SKILL.md +284 -0
- package/stacks/_shared/skills/ci-pipelines/SKILL.md +166 -0
- package/stacks/_shared/skills/codebase-knowledge/SKILL.md +5 -0
- package/stacks/_shared/skills/database-migrations/SKILL.md +256 -0
- package/stacks/_shared/skills/debugging-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docker-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docs-tracker/SKILL.md +5 -0
- package/stacks/_shared/skills/error-handling/SKILL.md +335 -0
- package/stacks/_shared/skills/final-check/SKILL.md +74 -37
- package/stacks/_shared/skills/git-workflow/SKILL.md +5 -0
- package/stacks/_shared/skills/hook-development/SKILL.md +5 -0
- package/stacks/_shared/skills/observability/SKILL.md +351 -0
- package/stacks/_shared/skills/performance-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/playwright-automation/SKILL.md +5 -0
- package/stacks/_shared/skills/quality-gate/SKILL.md +5 -0
- package/stacks/_shared/skills/research-cache/SKILL.md +5 -0
- package/stacks/_shared/skills/secrets-management/SKILL.md +245 -0
- package/stacks/_shared/skills/security-baseline/SKILL.md +202 -0
- package/stacks/_shared/skills/test-coverage/SKILL.md +5 -0
- package/stacks/_shared/skills/ui-ux-audit/SKILL.md +5 -0
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-standards/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +5 -0
- package/stacks/nodejs/skills/api-security-node/SKILL.md +275 -0
- package/stacks/nodejs/skills/bun-runtime/SKILL.md +5 -0
- package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +5 -0
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +5 -0
- package/stacks/nodejs/skills/trpc-api/SKILL.md +5 -0
- package/stacks/nodejs/skills/typescript-strict/SKILL.md +5 -0
- package/stacks/nodejs/stack.json +2 -1
- package/stacks/nodejs/workflows/ci.yml +90 -0
- package/stacks/nodejs/workflows/security.yml +45 -0
- package/stacks/php/skills/api-design/SKILL.md +5 -0
- package/stacks/php/skills/api-security/SKILL.md +5 -0
- package/stacks/php/skills/composer-workflow/SKILL.md +5 -0
- package/stacks/php/skills/external-api-patterns/SKILL.md +5 -0
- package/stacks/php/skills/inertia-react/SKILL.md +5 -0
- package/stacks/php/skills/laravel-inertia-i18n/SKILL.md +5 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +5 -0
- package/stacks/php/skills/laravel-patterns/SKILL.md +5 -0
- package/stacks/php/skills/mariadb-octane/SKILL.md +5 -0
- package/stacks/php/skills/php-patterns/SKILL.md +5 -0
- package/stacks/php/skills/phpstan-analysis/SKILL.md +5 -0
- package/stacks/php/skills/phpunit-testing/SKILL.md +5 -0
- package/stacks/php/skills/security-scan-php/SKILL.md +5 -0
- package/stacks/php/workflows/ci.yml +106 -0
- package/stacks/php/workflows/security.yml +36 -0
- package/stacks/python/skills/api-security-python/SKILL.md +312 -0
- package/stacks/python/skills/async-patterns/SKILL.md +5 -0
- package/stacks/python/skills/django-patterns/SKILL.md +5 -0
- package/stacks/python/skills/fastapi-patterns/SKILL.md +5 -0
- package/stacks/python/skills/pydantic-validation/SKILL.md +5 -0
- package/stacks/python/skills/pytest-testing/SKILL.md +5 -0
- package/stacks/python/skills/python-patterns/SKILL.md +26 -5
- package/stacks/python/skills/python-performance/SKILL.md +5 -0
- package/stacks/python/skills/scripting-automation/SKILL.md +260 -0
- package/stacks/python/stack.json +70 -35
- package/stacks/python/workflows/ci.yml +76 -0
- package/stacks/python/workflows/security.yml +56 -0
- package/templates/CLAUDE-python.md +315 -0
package/dist/detector.js
CHANGED
|
@@ -114,10 +114,8 @@ export function detectNodeFramework(projectDir) {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
export function detectPythonFramework(projectDir) {
|
|
117
|
-
// Check manage.py (Django)
|
|
118
117
|
if (existsSync(join(projectDir, 'manage.py')))
|
|
119
118
|
return 'django';
|
|
120
|
-
// Check for FastAPI in requirements or pyproject
|
|
121
119
|
for (const reqFile of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
|
|
122
120
|
const filePath = join(projectDir, reqFile);
|
|
123
121
|
if (existsSync(filePath)) {
|
|
@@ -128,5 +126,10 @@ export function detectPythonFramework(projectDir) {
|
|
|
128
126
|
return 'flask';
|
|
129
127
|
}
|
|
130
128
|
}
|
|
129
|
+
// If Python project detected but no web framework, suggest scripts
|
|
130
|
+
if (existsSync(join(projectDir, 'main.py')) &&
|
|
131
|
+
!existsSync(join(projectDir, 'app', 'main.py'))) {
|
|
132
|
+
return 'scripts';
|
|
133
|
+
}
|
|
131
134
|
return null;
|
|
132
135
|
}
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const PKG_VERSION = JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf
|
|
|
24
24
|
// CLI Arguments
|
|
25
25
|
// =============================================================================
|
|
26
26
|
const args = process.argv.slice(2);
|
|
27
|
+
const SUBCOMMAND = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
27
28
|
const FLAGS = {
|
|
28
29
|
force: args.includes('--force'),
|
|
29
30
|
noClaude: args.includes('--no-claude'),
|
|
@@ -31,6 +32,7 @@ const FLAGS = {
|
|
|
31
32
|
noInstall: args.includes('--no-install'),
|
|
32
33
|
help: args.includes('--help') || args.includes('-h'),
|
|
33
34
|
version: args.includes('--version') || args.includes('-v'),
|
|
35
|
+
apply: args.includes('--apply'),
|
|
34
36
|
};
|
|
35
37
|
if (FLAGS.version) {
|
|
36
38
|
console.log(PKG_VERSION);
|
|
@@ -40,10 +42,16 @@ if (FLAGS.help) {
|
|
|
40
42
|
console.log(ui.LOGO);
|
|
41
43
|
console.log(`
|
|
42
44
|
${chalk.bold('Usage:')}
|
|
43
|
-
npx start-vibing-stacks [options]
|
|
45
|
+
npx start-vibing-stacks [command] [options]
|
|
46
|
+
|
|
47
|
+
${chalk.bold('Commands:')}
|
|
48
|
+
(default) Setup or resume current project
|
|
49
|
+
migrate Compare installed vs bundled skill/agent versions
|
|
50
|
+
Add --apply to update outdated/missing items
|
|
44
51
|
|
|
45
52
|
${chalk.bold('Options:')}
|
|
46
|
-
--force Overwrite existing configuration
|
|
53
|
+
--force Overwrite existing configuration (default command)
|
|
54
|
+
--apply Apply updates (migrate command)
|
|
47
55
|
--no-claude Skip Claude Code installation
|
|
48
56
|
--no-mcp Skip MCP server selection
|
|
49
57
|
--no-install Skip dependency installation
|
|
@@ -57,6 +65,12 @@ if (FLAGS.help) {
|
|
|
57
65
|
`);
|
|
58
66
|
process.exit(0);
|
|
59
67
|
}
|
|
68
|
+
// Subcommand: migrate
|
|
69
|
+
if (SUBCOMMAND === 'migrate') {
|
|
70
|
+
const { runMigrate } = await import('./migrate.js');
|
|
71
|
+
await runMigrate(process.cwd(), { apply: FLAGS.apply });
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
60
74
|
const AVAILABLE_STACKS = [
|
|
61
75
|
{ id: 'php', name: 'PHP 8.3+', icon: '🐘', available: true },
|
|
62
76
|
{ id: 'nodejs', name: 'Node.js / TypeScript', icon: '📦', available: true },
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start Vibing Stacks — Migrate
|
|
3
|
+
*
|
|
4
|
+
* Compares the SKILL.md / agent / hook versions installed in .claude/
|
|
5
|
+
* against the bundled stacks/<stack>/ and stacks/_shared/ versions.
|
|
6
|
+
*
|
|
7
|
+
* Reports outdated, missing, and modified items. Optionally upgrades.
|
|
8
|
+
*
|
|
9
|
+
* Skill version contract:
|
|
10
|
+
* YAML frontmatter at top of SKILL.md must include `version: X.Y.Z`.
|
|
11
|
+
* No frontmatter = treated as "v0" / pre-versioning.
|
|
12
|
+
*/
|
|
13
|
+
export interface MigrateItem {
|
|
14
|
+
kind: 'skill' | 'agent' | 'hook';
|
|
15
|
+
name: string;
|
|
16
|
+
source: string;
|
|
17
|
+
target: string;
|
|
18
|
+
bundledVersion: string;
|
|
19
|
+
installedVersion: string | null;
|
|
20
|
+
status: 'missing' | 'outdated' | 'current' | 'ahead' | 'modified-no-version';
|
|
21
|
+
}
|
|
22
|
+
export interface MigrateOptions {
|
|
23
|
+
apply: boolean;
|
|
24
|
+
scope?: 'skills' | 'agents' | 'hooks' | 'all';
|
|
25
|
+
}
|
|
26
|
+
export declare function planMigration(projectDir: string, opts: MigrateOptions): MigrateItem[];
|
|
27
|
+
export declare function runMigrate(projectDir: string, opts: MigrateOptions): Promise<void>;
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start Vibing Stacks — Migrate
|
|
3
|
+
*
|
|
4
|
+
* Compares the SKILL.md / agent / hook versions installed in .claude/
|
|
5
|
+
* against the bundled stacks/<stack>/ and stacks/_shared/ versions.
|
|
6
|
+
*
|
|
7
|
+
* Reports outdated, missing, and modified items. Optionally upgrades.
|
|
8
|
+
*
|
|
9
|
+
* Skill version contract:
|
|
10
|
+
* YAML frontmatter at top of SKILL.md must include `version: X.Y.Z`.
|
|
11
|
+
* No frontmatter = treated as "v0" / pre-versioning.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, copyFileSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
14
|
+
import { join, relative, dirname, resolve } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import * as semver from 'semver';
|
|
17
|
+
import * as ui from './ui.js';
|
|
18
|
+
const __m_filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __m_dirname = dirname(__m_filename);
|
|
20
|
+
const CLI_ROOT = resolve(__m_dirname, '..');
|
|
21
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---/;
|
|
22
|
+
const VERSION_RE = /^version:\s*["']?([0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?)["']?\s*$/m;
|
|
23
|
+
function parseVersion(file) {
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(file, 'utf8');
|
|
28
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
29
|
+
if (!fm)
|
|
30
|
+
return null;
|
|
31
|
+
const v = VERSION_RE.exec(fm[1] ?? '');
|
|
32
|
+
return v?.[1] ?? null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function statusOf(bundled, installed) {
|
|
39
|
+
if (installed === null)
|
|
40
|
+
return 'modified-no-version';
|
|
41
|
+
const cmp = semver.compare(installed, bundled);
|
|
42
|
+
if (cmp < 0)
|
|
43
|
+
return 'outdated';
|
|
44
|
+
if (cmp > 0)
|
|
45
|
+
return 'ahead';
|
|
46
|
+
return 'current';
|
|
47
|
+
}
|
|
48
|
+
function listSubdirs(dir) {
|
|
49
|
+
if (!existsSync(dir))
|
|
50
|
+
return [];
|
|
51
|
+
try {
|
|
52
|
+
return readdirSync(dir).filter(n => {
|
|
53
|
+
try {
|
|
54
|
+
return statSync(join(dir, n)).isDirectory();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function listFiles(dir, suffix) {
|
|
66
|
+
if (!existsSync(dir))
|
|
67
|
+
return [];
|
|
68
|
+
try {
|
|
69
|
+
return readdirSync(dir).filter(n => n.endsWith(suffix));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function listSkillSources(stack, frontendSkillsDir) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const sharedDir = join(CLI_ROOT, 'stacks', '_shared', 'skills');
|
|
78
|
+
const stackDir = join(CLI_ROOT, 'stacks', stack, 'skills');
|
|
79
|
+
const frontendDir = frontendSkillsDir
|
|
80
|
+
? join(CLI_ROOT, 'stacks', 'frontend', frontendSkillsDir, 'skills')
|
|
81
|
+
: null;
|
|
82
|
+
for (const dir of [sharedDir, stackDir, frontendDir].filter(Boolean)) {
|
|
83
|
+
for (const entry of listSubdirs(dir)) {
|
|
84
|
+
const skillPath = join(dir, entry, 'SKILL.md');
|
|
85
|
+
if (existsSync(skillPath))
|
|
86
|
+
out.push({ source: skillPath, name: entry });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function listAgentSources() {
|
|
92
|
+
const dir = join(CLI_ROOT, 'stacks', '_shared', 'agents');
|
|
93
|
+
return listFiles(dir, '.md').map(n => ({ source: join(dir, n), name: n }));
|
|
94
|
+
}
|
|
95
|
+
function listHookSources() {
|
|
96
|
+
const dir = join(CLI_ROOT, 'stacks', '_shared', 'hooks');
|
|
97
|
+
return listFiles(dir, '.ts').map(n => ({ source: join(dir, n), name: n }));
|
|
98
|
+
}
|
|
99
|
+
function loadProjectConfig(projectDir) {
|
|
100
|
+
const path = join(projectDir, '.claude', 'config', 'active-project.json');
|
|
101
|
+
if (!existsSync(path))
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function planMigration(projectDir, opts) {
|
|
111
|
+
const config = loadProjectConfig(projectDir);
|
|
112
|
+
if (!config) {
|
|
113
|
+
ui.error('No .claude/config/active-project.json found. Run `npx start-vibing-stacks` first.');
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const items = [];
|
|
117
|
+
const scope = opts.scope ?? 'all';
|
|
118
|
+
if (scope === 'all' || scope === 'skills') {
|
|
119
|
+
for (const { source, name } of listSkillSources(config.stack, config.frontendSkillsDir)) {
|
|
120
|
+
const target = join(projectDir, '.claude', 'skills', name, 'SKILL.md');
|
|
121
|
+
const bundledVersion = parseVersion(source);
|
|
122
|
+
if (!bundledVersion)
|
|
123
|
+
continue;
|
|
124
|
+
const installedVersion = parseVersion(target);
|
|
125
|
+
const status = !existsSync(target)
|
|
126
|
+
? 'missing'
|
|
127
|
+
: statusOf(bundledVersion, installedVersion);
|
|
128
|
+
items.push({ kind: 'skill', name, source, target, bundledVersion, installedVersion, status });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (scope === 'all' || scope === 'agents') {
|
|
132
|
+
for (const { source, name } of listAgentSources()) {
|
|
133
|
+
const target = join(projectDir, '.claude', 'agents', name);
|
|
134
|
+
const bundledVersion = parseVersion(source);
|
|
135
|
+
if (!bundledVersion)
|
|
136
|
+
continue;
|
|
137
|
+
const installedVersion = parseVersion(target);
|
|
138
|
+
const status = !existsSync(target)
|
|
139
|
+
? 'missing'
|
|
140
|
+
: statusOf(bundledVersion, installedVersion);
|
|
141
|
+
items.push({ kind: 'agent', name, source, target, bundledVersion, installedVersion, status });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (scope === 'all' || scope === 'hooks') {
|
|
145
|
+
for (const { source, name } of listHookSources()) {
|
|
146
|
+
const target = join(projectDir, '.claude', 'hooks', name);
|
|
147
|
+
const bundledVersion = '0.0.0';
|
|
148
|
+
const installedVersion = existsSync(target) ? '0.0.0' : null;
|
|
149
|
+
const status = !existsSync(target) ? 'missing' : 'current';
|
|
150
|
+
items.push({ kind: 'hook', name, source, target, bundledVersion, installedVersion, status });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return items;
|
|
154
|
+
}
|
|
155
|
+
function applyOne(item) {
|
|
156
|
+
mkdirSync(dirname(item.target), { recursive: true });
|
|
157
|
+
copyFileSync(item.source, item.target);
|
|
158
|
+
}
|
|
159
|
+
export async function runMigrate(projectDir, opts) {
|
|
160
|
+
ui.header('🔄 Start Vibing — Migrate');
|
|
161
|
+
const items = planMigration(projectDir, opts);
|
|
162
|
+
if (items.length === 0) {
|
|
163
|
+
ui.info('Nothing to migrate.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const grouped = {
|
|
167
|
+
missing: [], outdated: [], current: [], ahead: [], 'modified-no-version': [],
|
|
168
|
+
};
|
|
169
|
+
for (const it of items)
|
|
170
|
+
grouped[it.status].push(it);
|
|
171
|
+
const summary = (label, list) => {
|
|
172
|
+
if (list.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
console.log(`\n ${label} (${list.length}):`);
|
|
175
|
+
for (const it of list.slice(0, 30)) {
|
|
176
|
+
const v = it.installedVersion ?? '–';
|
|
177
|
+
console.log(` ${it.kind.padEnd(5)} ${it.name.padEnd(32)} installed=${v.padEnd(8)} bundled=${it.bundledVersion}`);
|
|
178
|
+
}
|
|
179
|
+
if (list.length > 30)
|
|
180
|
+
console.log(` ... and ${list.length - 30} more`);
|
|
181
|
+
};
|
|
182
|
+
summary('MISSING', grouped.missing);
|
|
183
|
+
summary('OUTDATED', grouped.outdated);
|
|
184
|
+
summary('AHEAD (installed newer than bundled — kept)', grouped.ahead);
|
|
185
|
+
summary('UNVERSIONED (manual review)', grouped['modified-no-version']);
|
|
186
|
+
summary('CURRENT', grouped.current);
|
|
187
|
+
const upgradable = [...grouped.missing, ...grouped.outdated];
|
|
188
|
+
if (upgradable.length === 0) {
|
|
189
|
+
console.log('');
|
|
190
|
+
ui.success('Everything up to date.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!opts.apply) {
|
|
194
|
+
console.log('');
|
|
195
|
+
ui.info(`Run with --apply to install/update ${upgradable.length} item(s).`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log('');
|
|
199
|
+
for (const it of upgradable) {
|
|
200
|
+
try {
|
|
201
|
+
applyOne(it);
|
|
202
|
+
ui.success(`updated ${it.kind} ${it.name} (${it.installedVersion ?? '–'} → ${it.bundledVersion})`);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
ui.warn(`failed ${it.kind} ${it.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log('');
|
|
209
|
+
ui.success(`Migration complete: ${upgradable.length} item(s) updated.`);
|
|
210
|
+
if (grouped['modified-no-version'].length > 0) {
|
|
211
|
+
console.log('');
|
|
212
|
+
ui.warn(`${grouped['modified-no-version'].length} unversioned local item(s) skipped — review manually.`);
|
|
213
|
+
for (const it of grouped['modified-no-version'].slice(0, 10)) {
|
|
214
|
+
console.log(` ${relative(projectDir, it.target)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
package/dist/scanner.js
CHANGED
|
@@ -322,6 +322,69 @@ function scanEslintConfig(projectDir) {
|
|
|
322
322
|
],
|
|
323
323
|
};
|
|
324
324
|
}
|
|
325
|
+
// ─── Python Scanners ───────────────────────────────────────────────────────
|
|
326
|
+
const PYTHON_PACKAGES = {
|
|
327
|
+
'fastapi': { category: 'framework', name: 'FastAPI framework' },
|
|
328
|
+
'django': { category: 'framework', name: 'Django framework' },
|
|
329
|
+
'flask': { category: 'framework', name: 'Flask framework' },
|
|
330
|
+
'uvicorn': { category: 'server', name: 'Uvicorn ASGI server' },
|
|
331
|
+
'gunicorn': { category: 'server', name: 'Gunicorn WSGI server' },
|
|
332
|
+
'sqlalchemy': { category: 'database', name: 'SQLAlchemy ORM' },
|
|
333
|
+
'alembic': { category: 'database', name: 'Alembic migrations' },
|
|
334
|
+
'asyncpg': { category: 'database', name: 'asyncpg (PostgreSQL async)' },
|
|
335
|
+
'psycopg': { category: 'database', name: 'psycopg (PostgreSQL)' },
|
|
336
|
+
'mariadb': { category: 'database', name: 'MariaDB connector' },
|
|
337
|
+
'pymongo': { category: 'database', name: 'PyMongo (MongoDB)' },
|
|
338
|
+
'beanie': { category: 'database', name: 'Beanie ODM (MongoDB)' },
|
|
339
|
+
'motor': { category: 'database', name: 'Motor (MongoDB async)' },
|
|
340
|
+
'pydantic': { category: 'validation', name: 'Pydantic v2 validation' },
|
|
341
|
+
'pydantic-settings': { category: 'config', name: 'Pydantic Settings' },
|
|
342
|
+
'httpx': { category: 'http', name: 'httpx HTTP client' },
|
|
343
|
+
'tenacity': { category: 'reliability', name: 'tenacity retry logic' },
|
|
344
|
+
'celery': { category: 'queue', name: 'Celery task queue' },
|
|
345
|
+
'arq': { category: 'queue', name: 'ARQ async task queue' },
|
|
346
|
+
'redis': { category: 'cache', name: 'Redis client' },
|
|
347
|
+
'pytest': { category: 'testing', name: 'pytest testing' },
|
|
348
|
+
'pytest-asyncio': { category: 'testing', name: 'pytest async support' },
|
|
349
|
+
'mypy': { category: 'quality', name: 'mypy type checker' },
|
|
350
|
+
'ruff': { category: 'quality', name: 'ruff linter + formatter' },
|
|
351
|
+
'rich': { category: 'ui', name: 'rich CLI output' },
|
|
352
|
+
'typer': { category: 'cli', name: 'Typer CLI framework' },
|
|
353
|
+
'click': { category: 'cli', name: 'Click CLI framework' },
|
|
354
|
+
'stripe': { category: 'billing', name: 'Stripe payments' },
|
|
355
|
+
'google-ads': { category: 'ads', name: 'Google Ads API' },
|
|
356
|
+
'facebook-business': { category: 'ads', name: 'Facebook/Meta Ads API' },
|
|
357
|
+
'python-wordpress-xmlrpc': { category: 'cms', name: 'WordPress XML-RPC client' },
|
|
358
|
+
};
|
|
359
|
+
function scanPyprojectToml(projectDir) {
|
|
360
|
+
const content = readFileIfExists(join(projectDir, 'pyproject.toml')) ||
|
|
361
|
+
readFileIfExists(join(projectDir, 'requirements.txt'));
|
|
362
|
+
if (!content)
|
|
363
|
+
return null;
|
|
364
|
+
const patterns = [];
|
|
365
|
+
const lowerContent = content.toLowerCase();
|
|
366
|
+
for (const [pkg, meta] of Object.entries(PYTHON_PACKAGES)) {
|
|
367
|
+
if (lowerContent.includes(pkg.toLowerCase())) {
|
|
368
|
+
patterns.push({
|
|
369
|
+
category: meta.category,
|
|
370
|
+
name: meta.name,
|
|
371
|
+
confidence: 95,
|
|
372
|
+
detail: `Found: ${pkg}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const pythonVersionMatch = content.match(/requires-python\s*=\s*"([^"]+)"/i);
|
|
377
|
+
if (pythonVersionMatch) {
|
|
378
|
+
patterns.push({
|
|
379
|
+
category: 'runtime',
|
|
380
|
+
name: `Python version constraint: ${pythonVersionMatch[1]}`,
|
|
381
|
+
confidence: 100,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (patterns.length === 0)
|
|
385
|
+
return null;
|
|
386
|
+
return { source: 'pyproject.toml', patterns };
|
|
387
|
+
}
|
|
325
388
|
// ─── Shared Scanners ───────────────────────────────────────────────────────
|
|
326
389
|
function scanProjectFiles(projectDir) {
|
|
327
390
|
const patterns = [];
|
|
@@ -399,6 +462,33 @@ function scanProjectFiles(projectDir) {
|
|
|
399
462
|
else if (existsSync(join(projectDir, 'yarn.lock'))) {
|
|
400
463
|
patterns.push({ category: 'runtime', name: 'Yarn package manager in use', confidence: 100 });
|
|
401
464
|
}
|
|
465
|
+
// ── Python: Project configs ──
|
|
466
|
+
if (existsSync(join(projectDir, 'pyproject.toml'))) {
|
|
467
|
+
patterns.push({ category: 'runtime', name: 'pyproject.toml present (modern Python)', confidence: 100 });
|
|
468
|
+
}
|
|
469
|
+
if (existsSync(join(projectDir, 'uv.lock'))) {
|
|
470
|
+
patterns.push({ category: 'runtime', name: 'uv package manager in use', confidence: 100 });
|
|
471
|
+
}
|
|
472
|
+
else if (existsSync(join(projectDir, 'Pipfile.lock'))) {
|
|
473
|
+
patterns.push({ category: 'runtime', name: 'Pipenv package manager in use', confidence: 100 });
|
|
474
|
+
}
|
|
475
|
+
if (existsSync(join(projectDir, 'mypy.ini')) || existsSync(join(projectDir, '.mypy.ini'))) {
|
|
476
|
+
patterns.push({ category: 'quality', name: 'mypy config present', confidence: 100 });
|
|
477
|
+
}
|
|
478
|
+
if (existsSync(join(projectDir, 'ruff.toml')) || existsSync(join(projectDir, '.ruff.toml'))) {
|
|
479
|
+
patterns.push({ category: 'quality', name: 'ruff config present', confidence: 100 });
|
|
480
|
+
}
|
|
481
|
+
if (existsSync(join(projectDir, 'alembic.ini'))) {
|
|
482
|
+
patterns.push({ category: 'database', name: 'Alembic migrations present', confidence: 100 });
|
|
483
|
+
}
|
|
484
|
+
if (existsSync(join(projectDir, 'pytest.ini')) || existsSync(join(projectDir, 'pyproject.toml'))) {
|
|
485
|
+
if (existsSync(join(projectDir, 'tests'))) {
|
|
486
|
+
patterns.push({ category: 'testing', name: 'pytest test directory present', confidence: 90 });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (existsSync(join(projectDir, '.pre-commit-config.yaml'))) {
|
|
490
|
+
patterns.push({ category: 'quality', name: 'pre-commit hooks configured', confidence: 100 });
|
|
491
|
+
}
|
|
402
492
|
// ── Deploy targets ──
|
|
403
493
|
if (existsSync(join(projectDir, 'vercel.json'))) {
|
|
404
494
|
patterns.push({ category: 'deploy', name: 'Vercel deployment config', confidence: 100 });
|
|
@@ -485,6 +575,7 @@ export function scanProjectStandards(projectDir) {
|
|
|
485
575
|
scanPackageJson,
|
|
486
576
|
scanTsConfig,
|
|
487
577
|
scanEslintConfig,
|
|
578
|
+
scanPyprojectToml,
|
|
488
579
|
scanProjectFiles,
|
|
489
580
|
];
|
|
490
581
|
const results = [];
|
package/dist/setup.js
CHANGED
|
@@ -169,6 +169,16 @@ export async function setupProject(projectDir, config, options = {}) {
|
|
|
169
169
|
spinner.text = `Imported ${config.standardsReview.patterns.length} project standards`;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
// 11d. Copy CI workflow templates (only when target dir is empty or --force)
|
|
173
|
+
const stackWorkflowsDir = join(PACKAGE_ROOT, 'stacks', config.stack, 'workflows');
|
|
174
|
+
if (existsSync(stackWorkflowsDir)) {
|
|
175
|
+
const ghWorkflowsDir = join(projectDir, '.github', 'workflows');
|
|
176
|
+
const targetIsEmpty = !existsSync(ghWorkflowsDir);
|
|
177
|
+
if (targetIsEmpty || options.force) {
|
|
178
|
+
copyDirRecursive(stackWorkflowsDir, ghWorkflowsDir, options.force);
|
|
179
|
+
spinner.text = 'Installed CI workflow templates';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
172
182
|
// 12. Copy commands
|
|
173
183
|
const sharedCommandsDir = join(PACKAGE_ROOT, 'stacks', '_shared', 'commands');
|
|
174
184
|
if (existsSync(sharedCommandsDir)) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-md-compactor
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: "AUTOMATICALLY invoke when CLAUDE.md exceeds 40k chars OR auto memory MEMORY.md exceeds 200 lines. Compacts while preserving critical knowledge by offloading to topic files."
|
|
4
5
|
model: sonnet
|
|
5
6
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: research-web
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: "AUTOMATICALLY invoke BEFORE implementing any new feature or technology. Triggers: new feature, new technology, 'search', 'find info'. Web research specialist."
|
|
4
5
|
model: sonnet
|
|
5
6
|
tools: WebSearch, WebFetch, Read, Write
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-auditor
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: "AUTOMATICALLY invoke when code touches auth, sessions, user data, passwords, tokens, API routes, database queries, cookies, or env vars. VETO POWER — blocks insecure code. Runs AFTER tester, BEFORE quality-gate."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: Read, Grep, Glob, Bash
|
|
7
|
+
skills: security-baseline, secrets-management
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Security Auditor Agent
|
|
11
|
+
|
|
12
|
+
You audit code for security flaws. **You have VETO power** — when violations are found you block the workflow and require fixes.
|
|
13
|
+
|
|
14
|
+
## When You Run
|
|
15
|
+
|
|
16
|
+
After implementation and tester, **before** quality-gate and commit-manager. Always run when modified files include:
|
|
17
|
+
|
|
18
|
+
- Auth, session, login, register, password, token, JWT
|
|
19
|
+
- Route Handlers, Server Actions, controllers, API endpoints
|
|
20
|
+
- Database queries, ORM models
|
|
21
|
+
- Cookie / header / CORS / CSP configuration
|
|
22
|
+
- File uploads
|
|
23
|
+
- Anything reading `process.env` / `os.environ` / `$_ENV` / `env()`
|
|
24
|
+
|
|
25
|
+
## Step 1 — Read Stack Context
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cat .claude/config/active-project.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Branch logic on `stack`:
|
|
32
|
+
- `nodejs` → load `api-security-node` skill
|
|
33
|
+
- `python` → load `api-security-python` skill
|
|
34
|
+
- `php` → load `api-security` skill
|
|
35
|
+
- always → `security-baseline`, `secrets-management`
|
|
36
|
+
|
|
37
|
+
## Step 2 — Identify Modified Files
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git diff --name-only --diff-filter=AM HEAD
|
|
41
|
+
git diff --name-only --cached --diff-filter=AM
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Read each modified source file.
|
|
45
|
+
|
|
46
|
+
## Step 3 — Run the Audit Matrix
|
|
47
|
+
|
|
48
|
+
Apply each check below. **One violation = block.**
|
|
49
|
+
|
|
50
|
+
### A. Authn / Session
|
|
51
|
+
|
|
52
|
+
| Check | Pattern that fails |
|
|
53
|
+
|---|---|
|
|
54
|
+
| User ID from session, not body | `req.body.userId`, `request.json()["user_id"]`, `$request->input('user_id')` used for ownership |
|
|
55
|
+
| Auth gate before logic | Route handler with no `auth()` / `Depends(current_user)` / `auth:sanctum` middleware |
|
|
56
|
+
| Algorithm pinned on JWT | `jwt.verify(token)` without `algorithms` array |
|
|
57
|
+
| Token in HttpOnly cookie | `localStorage.setItem('token', ...)` or token rendered into HTML |
|
|
58
|
+
|
|
59
|
+
### B. Authz
|
|
60
|
+
|
|
61
|
+
| Check | Pattern that fails |
|
|
62
|
+
|---|---|
|
|
63
|
+
| Object-level scope | `Model.findById(id)` with no `where userId = session.user.id` |
|
|
64
|
+
| Role check on server | Role check only in client/UI |
|
|
65
|
+
| Mass assignment guard | `User.create(req.body)` without allowlist / Zod `.strict()` / Pydantic `extra="forbid"` / `$fillable` |
|
|
66
|
+
|
|
67
|
+
### C. Input Validation
|
|
68
|
+
|
|
69
|
+
| Check | Pattern that fails |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Schema at boundary | Route handler reads `req.body` / `request.json()` without Zod / Pydantic / FormRequest |
|
|
72
|
+
| Strict mode | Schema present but allows extra keys |
|
|
73
|
+
| Mongo operator injection | `User.findOne({ email: req.body.email })` without coercing email to string |
|
|
74
|
+
| SQL bindings | f-string / template-literal SQL: `f"... {x} ..."`, `\`SELECT ... ${x}\``, `"... $x ..."` |
|
|
75
|
+
|
|
76
|
+
### D. Secrets / Env
|
|
77
|
+
|
|
78
|
+
Run secrets scan:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Quick high-signal grep
|
|
82
|
+
git diff --cached -U0 \
|
|
83
|
+
| grep -E '(api[_-]?key|secret|token|password|bearer|aws_|private_key)' \
|
|
84
|
+
| grep -vE '\.env\.example|TEMPLATE|placeholder|example|<your|YOUR_|XXXX'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Also check:
|
|
88
|
+
- No `NEXT_PUBLIC_` containing `SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL`
|
|
89
|
+
- No hardcoded connection string with embedded password
|
|
90
|
+
- `.env` is in `.gitignore`
|
|
91
|
+
- `.env.example` exists if any env var is read
|
|
92
|
+
|
|
93
|
+
### E. Cookies
|
|
94
|
+
|
|
95
|
+
| Check | Required |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `httpOnly: true` | yes |
|
|
98
|
+
| `secure: true` in prod | yes |
|
|
99
|
+
| `sameSite` set | `lax` or `strict` |
|
|
100
|
+
|
|
101
|
+
### F. CORS / Headers
|
|
102
|
+
|
|
103
|
+
- No `origin: '*'` or `allow_origins=["*"]` combined with `credentials: true` / `allow_credentials=True`
|
|
104
|
+
- Security headers configured (Helmet / middleware): HSTS, CSP, X-Content-Type-Options, Referrer-Policy
|
|
105
|
+
|
|
106
|
+
### G. Rate Limiting
|
|
107
|
+
|
|
108
|
+
- Auth endpoints (`/login`, `/register`, `/password/reset`) have a rate limiter
|
|
109
|
+
- Webhook endpoints reject requests without verified signature
|
|
110
|
+
|
|
111
|
+
### H. Logging
|
|
112
|
+
|
|
113
|
+
- No `console.log(req.body)` / `print(request.json())` / `Log::info($request->all())`
|
|
114
|
+
- No `console.log(token)` / logging of cookies, headers, or `Authorization`
|
|
115
|
+
- No PII (email, phone, full name) in logs without redaction
|
|
116
|
+
|
|
117
|
+
### I. SSRF
|
|
118
|
+
|
|
119
|
+
- User-supplied URLs are validated against an allowlist OR private IP ranges are blocked
|
|
120
|
+
|
|
121
|
+
### J. Webhook Verification
|
|
122
|
+
|
|
123
|
+
- Stripe / GitHub / GitHub App webhooks call `constructEvent` / signature verifier **before** parsing body
|
|
124
|
+
|
|
125
|
+
## Step 4 — Report
|
|
126
|
+
|
|
127
|
+
If everything passes, output:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
✅ Security audit passed.
|
|
131
|
+
Files audited: <n>
|
|
132
|
+
Checks: A-J all green.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If violations are found, output:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
🛑 SECURITY AUDIT BLOCKED
|
|
139
|
+
|
|
140
|
+
<n> violation(s) found:
|
|
141
|
+
|
|
142
|
+
1. [HIGH] <file>:<line>
|
|
143
|
+
Issue: <one line>
|
|
144
|
+
Fix: <one line>
|
|
145
|
+
Reference: security-baseline §A01 (or whichever)
|
|
146
|
+
|
|
147
|
+
2. [MEDIUM] ...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Severity:
|
|
151
|
+
- **CRITICAL** — RCE, SQLi, missing auth, secret in commit. Block immediately.
|
|
152
|
+
- **HIGH** — Authz bypass, missing CSRF, unsafe cookie. Block.
|
|
153
|
+
- **MEDIUM** — Missing rate limit, weak headers. Block.
|
|
154
|
+
- **LOW** — Minor logging concern. Warn, allow.
|
|
155
|
+
|
|
156
|
+
## Rules
|
|
157
|
+
|
|
158
|
+
1. **VETO POWER** — `domain-updater` and `commit-manager` MUST NOT run while you have unresolved CRITICAL/HIGH/MEDIUM findings.
|
|
159
|
+
2. **READ THE CODE** — never approve based on file names alone.
|
|
160
|
+
3. **NO FALSE NEGATIVES > FALSE POSITIVES** — when in doubt, flag and explain.
|
|
161
|
+
4. **CITE THE FIX** — every finding has a one-line fix and a skill reference.
|
|
162
|
+
5. **RE-RUN AFTER FIXES** — never trust "I fixed it" without re-reading the file.
|
|
163
|
+
|
|
164
|
+
## See Also
|
|
165
|
+
|
|
166
|
+
- Skill `security-baseline` — universal OWASP rules
|
|
167
|
+
- Skill `secrets-management` — env hygiene
|
|
168
|
+
- Stack-specific skill: `api-security-node` / `api-security-python` / PHP `api-security`
|