stet-cli 0.1.2 → 0.1.3
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/CHANGELOG.md +4 -0
- package/lib/core.js +76 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3 — 2026-11-14
|
|
4
|
+
- Stack detection now finds projects in subfolders (e.g. backend/, frontend/, apps/web) and languages from file types, so monorepos with Python + JavaScript are detected correctly.
|
|
5
|
+
- stet init creates one test lock per sub-project (e.g. `cd backend && pytest -q`).
|
|
6
|
+
|
|
3
7
|
## 0.1.2 — 2026-11-13
|
|
4
8
|
- Published on npm as stet-cli (the command is still `stet`).
|
|
5
9
|
|
package/lib/core.js
CHANGED
|
@@ -77,37 +77,83 @@ function matches(file, globs) {
|
|
|
77
77
|
const splitList = (v) => (Array.isArray(v) ? v : String(v || '').split(',')).map((s) => s.trim()).filter(Boolean);
|
|
78
78
|
|
|
79
79
|
// ---------- stack detection ----------
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage', '__pycache__', '.venv', 'venv', 'env', 'vendor', 'target', '.stet', '.idea', '.vscode']);
|
|
81
|
+
const MANIFESTS = ['package.json', 'pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile', 'go.mod', 'Cargo.toml', 'Gemfile', 'pom.xml', 'build.gradle', 'composer.json', 'pubspec.yaml'];
|
|
82
|
+
const EXT_LANG = { py: 'Python', ts: 'TypeScript', tsx: 'TypeScript', js: 'JavaScript', jsx: 'JavaScript', go: 'Go', rs: 'Rust', rb: 'Ruby', java: 'Java', kt: 'Kotlin', swift: 'Swift', php: 'PHP', cs: 'C#', dart: 'Dart' };
|
|
83
|
+
|
|
84
|
+
// Folders (root + up to 2 levels deep) that contain a project manifest, e.g. "", "backend", "apps/web".
|
|
85
|
+
function projectDirs(root) {
|
|
86
|
+
const out = [];
|
|
87
|
+
const walk = (rel, depth) => {
|
|
88
|
+
const abs = path.join(root, rel);
|
|
89
|
+
let entries = [];
|
|
90
|
+
try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
|
|
91
|
+
if (entries.some((e) => e.isFile() && MANIFESTS.includes(e.name))) out.push(rel);
|
|
92
|
+
if (depth >= 2) return;
|
|
93
|
+
for (const e of entries) if (e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walk(rel ? rel + '/' + e.name : e.name, depth + 1);
|
|
94
|
+
};
|
|
95
|
+
walk('', 0);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function detectIn(root, rel, s) {
|
|
100
|
+
const ex = (f) => fs.existsSync(path.join(root, rel, f));
|
|
101
|
+
const rd = (f) => { try { return fs.readFileSync(path.join(root, rel, f), 'utf8'); } catch { return ''; } };
|
|
102
|
+
const where = rel ? ` (${rel}/)` : '';
|
|
103
|
+
const cd = (cmd) => (rel ? `cd ${rel} && ${cmd}` : cmd);
|
|
104
|
+
const addLang = (l) => { if (!s.langs.includes(l + where)) s.langs.push(l + where); };
|
|
105
|
+
const addFw = (f) => { if (!s.frameworks.includes(f)) s.frameworks.push(f); };
|
|
106
|
+
const addTest = (cmd) => { if (!s.tests.includes(cd(cmd))) s.tests.push(cd(cmd)); };
|
|
107
|
+
const p = (f) => (rel ? rel + '/' + f : f);
|
|
84
108
|
if (ex('package.json')) {
|
|
85
109
|
let pj = {}; try { pj = JSON.parse(rd('package.json')); } catch {}
|
|
86
110
|
const deps = { ...(pj.dependencies || {}), ...(pj.devDependencies || {}) };
|
|
87
111
|
s.deps.push(...Object.keys(pj.dependencies || {}));
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
112
|
+
const emptyWorkspaceRoot = !rel && !Object.keys(deps).length && !Object.keys(pj.scripts || {}).length;
|
|
113
|
+
if (!emptyWorkspaceRoot) addLang(deps.typescript || ex('tsconfig.json') ? 'TypeScript' : 'JavaScript');
|
|
114
|
+
for (const [k, n] of [['next', 'Next.js'], ['react', 'React'], ['vue', 'Vue'], ['svelte', 'Svelte'], ['express', 'Express'], ['@nestjs/core', 'NestJS'], ['fastify', 'Fastify'], ['hono', 'Hono'], ['vite', 'Vite'], ['prisma', 'Prisma'], ['drizzle-orm', 'Drizzle'], ['@supabase/supabase-js', 'Supabase'], ['stripe', 'Stripe'], ['tailwindcss', 'Tailwind']]) if (deps[k]) addFw(n);
|
|
115
|
+
const rootEx = (f) => ex(f) || fs.existsSync(path.join(root, f));
|
|
116
|
+
const pm = rootEx('pnpm-lock.yaml') ? 'pnpm' : rootEx('yarn.lock') ? 'yarn' : rootEx('bun.lockb') || rootEx('bun.lock') ? 'bun' : 'npm';
|
|
91
117
|
const sc = pj.scripts || {};
|
|
92
|
-
if (sc.test && !/no test specified/.test(sc.test))
|
|
93
|
-
for (const k of ['dev', 'start', 'build', 'lint', 'typecheck']) if (sc[k]) s.run.push(`${pm} run ${k}`);
|
|
94
|
-
if (pj.main) s.entry.push(pj.main);
|
|
95
|
-
if (pj.bin) s.entry.push(...(typeof pj.bin === 'string' ? [pj.bin] : Object.values(pj.bin)));
|
|
118
|
+
if (sc.test && !/no test specified/.test(sc.test)) addTest(`${pm} test`);
|
|
119
|
+
for (const k of ['dev', 'start', 'build', 'lint', 'typecheck']) if (sc[k]) s.run.push(cd(`${pm} run ${k}`));
|
|
120
|
+
if (pj.main) s.entry.push(p(pj.main));
|
|
121
|
+
if (pj.bin) s.entry.push(...(typeof pj.bin === 'string' ? [pj.bin] : Object.values(pj.bin)).map(p));
|
|
96
122
|
}
|
|
97
|
-
if (ex('pyproject.toml') || ex('requirements.txt') || ex('setup.py')) {
|
|
98
|
-
|
|
99
|
-
const txt = (rd('pyproject.toml') + '\n' + rd('requirements.txt')).toLowerCase();
|
|
100
|
-
for (const [k, n] of [['django', 'Django'], ['fastapi', 'FastAPI'], ['flask', 'Flask'], ['sqlalchemy', 'SQLAlchemy'], ['pydantic', 'Pydantic'], ['pandas', 'pandas'], ['celery', 'Celery']]) if (txt.includes(k))
|
|
101
|
-
s.deps.push(...nonEmpty(rd('requirements.txt')).filter((l) => !l.startsWith('#')).map((l) => l.split(/[=<>~! ;\[]/)[0]));
|
|
102
|
-
if (
|
|
103
|
-
if (ex('manage.py')) { s.run.push('python manage.py runserver'); s.entry.push('manage.py'); }
|
|
123
|
+
if (ex('pyproject.toml') || ex('requirements.txt') || ex('setup.py') || ex('Pipfile')) {
|
|
124
|
+
addLang('Python');
|
|
125
|
+
const txt = (rd('pyproject.toml') + '\n' + rd('requirements.txt') + '\n' + rd('Pipfile')).toLowerCase();
|
|
126
|
+
for (const [k, n] of [['django', 'Django'], ['fastapi', 'FastAPI'], ['flask', 'Flask'], ['sqlalchemy', 'SQLAlchemy'], ['pydantic', 'Pydantic'], ['pandas', 'pandas'], ['celery', 'Celery']]) if (txt.includes(k)) addFw(n);
|
|
127
|
+
s.deps.push(...nonEmpty(rd('requirements.txt')).filter((l) => !l.trim().startsWith('#') && !l.trim().startsWith('-')).map((l) => l.trim().split(/[=<>~! ;\[]/)[0]));
|
|
128
|
+
if (ex('tests') || ex('test') || ex('pytest.ini') || ex('conftest.py') || txt.includes('[tool.pytest')) addTest('pytest -q');
|
|
129
|
+
if (ex('manage.py')) { s.run.push(cd('python manage.py runserver')); s.entry.push(p('manage.py')); }
|
|
130
|
+
}
|
|
131
|
+
if (ex('go.mod')) { addLang('Go'); addTest('go test ./...'); }
|
|
132
|
+
if (ex('Cargo.toml')) { addLang('Rust'); addTest('cargo test'); }
|
|
133
|
+
if (ex('Gemfile')) addLang('Ruby');
|
|
134
|
+
if (ex('pom.xml') || ex('build.gradle')) addLang('Java');
|
|
135
|
+
if (ex('composer.json')) addLang('PHP');
|
|
136
|
+
if (ex('pubspec.yaml')) addLang('Dart');
|
|
137
|
+
if (ex('Dockerfile') || ex('docker-compose.yml') || ex('docker-compose.yaml') || ex('compose.yaml')) addFw('Docker');
|
|
138
|
+
for (const f of ['main.py', 'app.py', 'server.js', 'index.js', 'src/server.js', 'src/server.ts', 'src/app.js', 'src/index.ts', 'src/main.ts', 'src/index.js', 'src/app.ts', 'app/main.py', 'app/page.tsx', 'src/main.tsx', 'main.go', 'src/main.rs']) if (ex(f) && !s.entry.includes(p(f))) s.entry.push(p(f));
|
|
139
|
+
if (!rel && ex('Makefile')) s.run.push(...nonEmpty(rd('Makefile')).filter((l) => /^[a-zA-Z][\w-]*:/.test(l)).slice(0, 6).map((l) => 'make ' + l.split(':')[0]));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function detect(root) {
|
|
143
|
+
const s = { langs: [], frameworks: [], tests: [], test: null, run: [], deps: [], entry: [] };
|
|
144
|
+
for (const rel of projectDirs(root)) detectIn(root, rel, s);
|
|
145
|
+
if (!s.frameworks.includes('Docker') && ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yaml', 'compose.yml'].some((f) => fs.existsSync(path.join(root, f)))) s.frameworks.push('Docker');
|
|
146
|
+
// Fallback: languages with real source files but no manifest we recognise.
|
|
147
|
+
const files = names(tgit(['ls-files', '--cached', '--others', '--exclude-standard'], root));
|
|
148
|
+
const count = {};
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
if (f.split('/').some((seg) => SKIP_DIRS.has(seg))) continue;
|
|
151
|
+
const l = EXT_LANG[f.split('.').pop().toLowerCase()];
|
|
152
|
+
if (l) count[l] = (count[l] || 0) + 1;
|
|
104
153
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (ex('Dockerfile') || ex('docker-compose.yml') || ex('compose.yaml')) s.frameworks.push('Docker');
|
|
109
|
-
for (const f of ['main.py', 'app.py', 'server.js', 'index.js', 'src/server.js', 'src/server.ts', 'src/app.js', 'src/index.ts', 'src/main.ts', 'src/index.js', 'src/app.ts', 'app/page.tsx', 'src/main.tsx', 'main.go', 'src/main.rs']) if (ex(f) && !s.entry.includes(f)) s.entry.push(f);
|
|
110
|
-
if (ex('Makefile')) s.run.push(...nonEmpty(rd('Makefile')).filter((l) => /^[a-zA-Z][\w-]*:/.test(l)).slice(0, 6).map((l) => 'make ' + l.split(':')[0]));
|
|
154
|
+
for (const [l, n] of Object.entries(count)) if (n >= 3 && !s.langs.some((x) => x.split(' (')[0] === l)) s.langs.push(l);
|
|
155
|
+
s.deps = [...new Set(s.deps.filter(Boolean))];
|
|
156
|
+
s.test = s.tests.length ? s.tests.join(' && ') : null;
|
|
111
157
|
return s;
|
|
112
158
|
}
|
|
113
159
|
|
|
@@ -155,7 +201,7 @@ ${name} is ...
|
|
|
155
201
|
|
|
156
202
|
## Stack
|
|
157
203
|
- ${stack}
|
|
158
|
-
${st.
|
|
204
|
+
${st.tests.length ? `- Tests: ${st.tests.map((x) => '`' + x + '`').join(', ')}\n` : ''}${st.run.length ? `- Run: ${st.run.map((x) => '`' + x + '`').join(', ')}\n` : ''}
|
|
159
205
|
## Always
|
|
160
206
|
- Read this charter, the current scope and the locked behaviours before changing code.
|
|
161
207
|
- Make the smallest change that solves the task. Match the existing style and patterns.
|
|
@@ -253,7 +299,10 @@ function init(r, { name, mcp } = {}) {
|
|
|
253
299
|
const put = (f, s) => { if (fs.existsSync(r.p(f))) kept.push(f); else { r.write(f, s); made.push(f); } };
|
|
254
300
|
put('config.json', JSON.stringify({ name: nm, version: 1, protect: [], ignore: [], maxFileKB: 64, checkTimeoutSec: 600 }, null, 2) + '\n');
|
|
255
301
|
put('charter.md', charterTemplate(nm, st));
|
|
256
|
-
put('locks.json', JSON.stringify(st.
|
|
302
|
+
put('locks.json', JSON.stringify(st.tests.map((cmd, i) => {
|
|
303
|
+
const m = cmd.match(/^cd (\S+) && /);
|
|
304
|
+
return { id: 'L' + (i + 1), name: m ? `Test suite passes (${m[1]}/)` : 'Full test suite passes', cmd, protect: [], added: today() };
|
|
305
|
+
}), null, 2) + '\n');
|
|
257
306
|
put('scope.json', '{}\n');
|
|
258
307
|
put('decisions.md', `# Decision log — ${nm}\n\nOne entry per decision. Newest at the bottom. Managed with \`stet decide\`.\n\n## D-001 · ${today()} · Adopted Stet to keep AI agents in scope\nAI agents read AGENTS.md. Locked behaviours are checked with \`stet check\` before work is called done.\n`);
|
|
259
308
|
put('.gitignore', '.cache/\ncontext.md\n');
|
|
@@ -495,7 +544,7 @@ Generated by Stet ${VERSION} on ${today()}. Regenerate any time with \`stet hand
|
|
|
495
544
|
${charter}
|
|
496
545
|
|
|
497
546
|
## 2. Run it
|
|
498
|
-
${st.run.length || st.
|
|
547
|
+
${st.run.length || st.tests.length ? [...st.run, ...st.tests.map((t) => t + ' # tests')].map((x) => '- `' + x + '`').join('\n') : '- No run scripts detected. Add them to the charter.'}
|
|
499
548
|
${envKeys.length ? `\nEnvironment variables needed (names only, from the example env file):\n${envKeys.map((k) => '`' + k + '`').join(' · ')}` : ''}
|
|
500
549
|
|
|
501
550
|
## 3. Architecture at a glance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stet-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Keep AI coding agents from breaking what already works. Charter, locked behaviours, scope guard, decision log and git-synced context for Claude Code, Cursor, Codex and any chat.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stet": "bin/stet.js"
|