serpentstack 0.2.9 → 0.2.11

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.
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  readConfig,
20
20
  writeConfig,
21
+ detectProjectDefaults,
21
22
  detectTemplateDefaults,
22
23
  getEffectiveModel,
23
24
  isAgentEnabled,
@@ -352,18 +353,19 @@ export async function persistent({ stop = false, reconfigure = false } = {}) {
352
353
 
353
354
  try {
354
355
  // ── Project configuration ──
355
- const templateDefaults = detectTemplateDefaults(projectDir);
356
+ const detected = detectProjectDefaults(projectDir);
357
+ const template = detectTemplateDefaults(projectDir);
356
358
  const existing = config.project || {};
357
359
  const defaults = {
358
- name: existing.name || templateDefaults?.name || '',
359
- language: existing.language || templateDefaults?.language || '',
360
- framework: existing.framework || templateDefaults?.framework || '',
361
- devCmd: existing.devCmd || templateDefaults?.devCmd || '',
362
- testCmd: existing.testCmd || templateDefaults?.testCmd || '',
363
- conventions: existing.conventions || templateDefaults?.conventions || '',
360
+ name: existing.name || template?.name || detected.name,
361
+ language: existing.language || template?.language || detected.language,
362
+ framework: existing.framework || template?.framework || detected.framework,
363
+ devCmd: existing.devCmd || template?.devCmd || detected.devCmd,
364
+ testCmd: existing.testCmd || template?.testCmd || detected.testCmd,
365
+ conventions: existing.conventions || template?.conventions || detected.conventions,
364
366
  };
365
367
 
366
- if (templateDefaults && !existing.name) {
368
+ if (template && !existing.name) {
367
369
  info('Detected SerpentStack template — defaults pre-filled');
368
370
  console.log();
369
371
  }
@@ -2,7 +2,7 @@ import { join } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { downloadFile } from '../utils/github.js';
4
4
  import { safeWrite } from '../utils/fs-helpers.js';
5
- import { readConfig, writeConfig, detectTemplateDefaults, defaultAgentConfig } from '../utils/config.js';
5
+ import { readConfig, writeConfig, detectProjectDefaults, detectTemplateDefaults, defaultAgentConfig } from '../utils/config.js';
6
6
  import { parseAgentMd, discoverAgents } from '../utils/agent-utils.js';
7
7
  import { info, success, warn, error, spinner, bold, dim, green, cyan, divider, printBox, printPrompt, printHeader, fileStatus } from '../utils/ui.js';
8
8
 
@@ -87,15 +87,18 @@ export async function skillsInit({ force = false } = {}) {
87
87
  // Generate default config.json if it doesn't exist
88
88
  const projectDir = process.cwd();
89
89
  if (!readConfig(projectDir)) {
90
- const templateDefaults = detectTemplateDefaults(projectDir) || {};
90
+ // Auto-detect project info from filesystem
91
+ const detected = detectProjectDefaults(projectDir);
92
+ // SerpentStack template overrides (more specific)
93
+ const template = detectTemplateDefaults(projectDir);
91
94
  const config = {
92
95
  project: {
93
- name: templateDefaults.name || '',
94
- language: templateDefaults.language || '',
95
- framework: templateDefaults.framework || '',
96
- devCmd: templateDefaults.devCmd || '',
97
- testCmd: templateDefaults.testCmd || '',
98
- conventions: templateDefaults.conventions || '',
96
+ name: template?.name || detected.name,
97
+ language: template?.language || detected.language,
98
+ framework: template?.framework || detected.framework,
99
+ devCmd: template?.devCmd || detected.devCmd,
100
+ testCmd: template?.testCmd || detected.testCmd,
101
+ conventions: template?.conventions || detected.conventions,
99
102
  },
100
103
  agents: {},
101
104
  };
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
3
 
4
4
  const CONFIG_PATH = '.openclaw/config.json';
5
5
 
@@ -28,7 +28,7 @@ export function readConfig(projectDir) {
28
28
  return JSON.parse(readFileSync(configPath, 'utf8'));
29
29
  } catch (err) {
30
30
  if (err instanceof SyntaxError) {
31
- console.error(` \x1b[33m\u25B3\x1b[0m .openclaw/config.json has invalid JSON — ignoring. Fix it or delete it to regenerate.`);
31
+ console.error(` \x1b[33m△\x1b[0m .openclaw/config.json has invalid JSON — ignoring. Fix it or delete it to regenerate.`);
32
32
  }
33
33
  return null;
34
34
  }
@@ -43,9 +43,30 @@ export function writeConfig(projectDir, config) {
43
43
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
44
44
  }
45
45
 
46
+ // ─── Project Detection ───────────────────────────────────────
47
+
48
+ /**
49
+ * Detect project defaults by inspecting the filesystem.
50
+ * Works for ANY project — detects name, language, framework, commands.
51
+ * Always returns an object with every field populated (never empty strings).
52
+ * When nothing can be detected, returns sensible "fill me in" defaults
53
+ * that read naturally in the config file and prompt the user to update.
54
+ */
55
+ export function detectProjectDefaults(projectDir) {
56
+ return {
57
+ name: detectProjectName(projectDir),
58
+ language: detectLanguage(projectDir),
59
+ framework: detectFramework(projectDir),
60
+ devCmd: detectDevCmd(projectDir),
61
+ testCmd: detectTestCmd(projectDir),
62
+ conventions: detectConventions(projectDir),
63
+ };
64
+ }
65
+
46
66
  /**
47
- * Detect if this is a SerpentStack template project and return defaults.
48
- * Returns null if not a template project.
67
+ * Detect if this is a SerpentStack template project and return overrides.
68
+ * Returns null if not a template project. When non-null, these values
69
+ * take priority over the generic detection.
49
70
  */
50
71
  export function detectTemplateDefaults(projectDir) {
51
72
  const makefile = join(projectDir, 'Makefile');
@@ -57,7 +78,7 @@ export function detectTemplateDefaults(projectDir) {
57
78
  if (!/^verify:/m.test(content)) return null;
58
79
  if (!content.includes('uv run')) return null;
59
80
 
60
- // It's a SerpentStack template project — return smart defaults
81
+ // It's a SerpentStack template project — return specific defaults
61
82
  const defaults = {
62
83
  language: 'Python + TypeScript',
63
84
  framework: 'FastAPI + React',
@@ -66,7 +87,7 @@ export function detectTemplateDefaults(projectDir) {
66
87
  conventions: 'Services flush, routes commit. Domain returns, not exceptions. Real Postgres in tests.',
67
88
  };
68
89
 
69
- // Try to detect project name from scripts/init.py or package.json
90
+ // Try to detect project name from frontend/package.json
70
91
  const pkgPath = join(projectDir, 'frontend', 'package.json');
71
92
  if (existsSync(pkgPath)) {
72
93
  try {
@@ -81,6 +102,209 @@ export function detectTemplateDefaults(projectDir) {
81
102
  }
82
103
  }
83
104
 
105
+ // ─── Detection helpers ───────────────────────────────────────
106
+
107
+ function detectProjectName(projectDir) {
108
+ // 1. package.json name
109
+ const pkgPath = join(projectDir, 'package.json');
110
+ if (existsSync(pkgPath)) {
111
+ try {
112
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
113
+ if (pkg.name && pkg.name !== 'unnamed') return pkg.name;
114
+ } catch { /* ignore */ }
115
+ }
116
+
117
+ // 2. pyproject.toml name
118
+ const pyprojectPath = join(projectDir, 'pyproject.toml');
119
+ if (existsSync(pyprojectPath)) {
120
+ try {
121
+ const content = readFileSync(pyprojectPath, 'utf8');
122
+ const match = content.match(/^name\s*=\s*"([^"]+)"/m);
123
+ if (match) return match[1];
124
+ } catch { /* ignore */ }
125
+ }
126
+
127
+ // 3. Cargo.toml name
128
+ const cargoPath = join(projectDir, 'Cargo.toml');
129
+ if (existsSync(cargoPath)) {
130
+ try {
131
+ const content = readFileSync(cargoPath, 'utf8');
132
+ const match = content.match(/^name\s*=\s*"([^"]+)"/m);
133
+ if (match) return match[1];
134
+ } catch { /* ignore */ }
135
+ }
136
+
137
+ // 4. Fall back to directory name
138
+ return basename(projectDir);
139
+ }
140
+
141
+ function detectLanguage(projectDir) {
142
+ const langs = [];
143
+ if (existsSync(join(projectDir, 'pyproject.toml')) || existsSync(join(projectDir, 'requirements.txt')) || existsSync(join(projectDir, 'setup.py'))) {
144
+ langs.push('Python');
145
+ }
146
+ if (existsSync(join(projectDir, 'tsconfig.json'))) {
147
+ langs.push('TypeScript');
148
+ } else if (existsSync(join(projectDir, 'package.json'))) {
149
+ langs.push('JavaScript');
150
+ }
151
+ if (existsSync(join(projectDir, 'go.mod'))) {
152
+ langs.push('Go');
153
+ }
154
+ if (existsSync(join(projectDir, 'Cargo.toml'))) {
155
+ langs.push('Rust');
156
+ }
157
+ if (existsSync(join(projectDir, 'pom.xml')) || existsSync(join(projectDir, 'build.gradle'))) {
158
+ langs.push('Java');
159
+ }
160
+ if (existsSync(join(projectDir, 'Gemfile'))) {
161
+ langs.push('Ruby');
162
+ }
163
+ // If nothing detected, use SerpentStack template default since
164
+ // that's the most common use case (user just ran `stack new`)
165
+ return langs.length > 0 ? langs.join(' + ') : 'Python + TypeScript';
166
+ }
167
+
168
+ function detectFramework(projectDir) {
169
+ const frameworks = [];
170
+
171
+ // Python frameworks
172
+ const pyFiles = [join(projectDir, 'pyproject.toml'), join(projectDir, 'requirements.txt')];
173
+ for (const f of pyFiles) {
174
+ if (!existsSync(f)) continue;
175
+ try {
176
+ const content = readFileSync(f, 'utf8');
177
+ if (content.includes('fastapi')) frameworks.push('FastAPI');
178
+ else if (content.includes('django')) frameworks.push('Django');
179
+ else if (content.includes('flask')) frameworks.push('Flask');
180
+ break;
181
+ } catch { /* ignore */ }
182
+ }
183
+
184
+ // JS/TS frameworks
185
+ const pkgPath = join(projectDir, 'package.json');
186
+ if (existsSync(pkgPath)) {
187
+ try {
188
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
189
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
190
+ if (allDeps['next']) frameworks.push('Next.js');
191
+ else if (allDeps['react']) frameworks.push('React');
192
+ else if (allDeps['vue']) frameworks.push('Vue');
193
+ else if (allDeps['svelte'] || allDeps['@sveltejs/kit']) frameworks.push('Svelte');
194
+ else if (allDeps['express']) frameworks.push('Express');
195
+ } catch { /* ignore */ }
196
+ }
197
+
198
+ // Go frameworks
199
+ const goModPath = join(projectDir, 'go.mod');
200
+ if (existsSync(goModPath)) {
201
+ try {
202
+ const content = readFileSync(goModPath, 'utf8');
203
+ if (content.includes('gin-gonic')) frameworks.push('Gin');
204
+ else if (content.includes('labstack/echo')) frameworks.push('Echo');
205
+ else if (content.includes('gofiber')) frameworks.push('Fiber');
206
+ else frameworks.push('Go');
207
+ } catch { /* ignore */ }
208
+ }
209
+
210
+ // Ruby
211
+ const gemfilePath = join(projectDir, 'Gemfile');
212
+ if (existsSync(gemfilePath)) {
213
+ try {
214
+ const content = readFileSync(gemfilePath, 'utf8');
215
+ if (content.includes('rails')) frameworks.push('Rails');
216
+ else if (content.includes('sinatra')) frameworks.push('Sinatra');
217
+ } catch { /* ignore */ }
218
+ }
219
+
220
+ return frameworks.length > 0 ? frameworks.join(' + ') : 'FastAPI + React';
221
+ }
222
+
223
+ function detectDevCmd(projectDir) {
224
+ // Makefile targets
225
+ const makefile = join(projectDir, 'Makefile');
226
+ if (existsSync(makefile)) {
227
+ try {
228
+ const content = readFileSync(makefile, 'utf8');
229
+ if (/^dev:/m.test(content)) return 'make dev';
230
+ if (/^start:/m.test(content)) return 'make start';
231
+ if (/^run:/m.test(content)) return 'make run';
232
+ if (/^serve:/m.test(content)) return 'make serve';
233
+ } catch { /* ignore */ }
234
+ }
235
+
236
+ // package.json scripts
237
+ const pkgPath = join(projectDir, 'package.json');
238
+ if (existsSync(pkgPath)) {
239
+ try {
240
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
241
+ const scripts = pkg.scripts || {};
242
+ if (scripts.dev) return 'npm run dev';
243
+ if (scripts.start) return 'npm start';
244
+ if (scripts.serve) return 'npm run serve';
245
+ } catch { /* ignore */ }
246
+ }
247
+
248
+ // Python
249
+ if (existsSync(join(projectDir, 'manage.py'))) return 'python manage.py runserver';
250
+ if (existsSync(join(projectDir, 'pyproject.toml'))) return 'uv run python -m app';
251
+
252
+ return 'make dev';
253
+ }
254
+
255
+ function detectTestCmd(projectDir) {
256
+ // Makefile targets
257
+ const makefile = join(projectDir, 'Makefile');
258
+ if (existsSync(makefile)) {
259
+ try {
260
+ const content = readFileSync(makefile, 'utf8');
261
+ if (/^verify:/m.test(content)) return 'make verify';
262
+ if (/^test:/m.test(content)) return 'make test';
263
+ if (/^check:/m.test(content)) return 'make check';
264
+ } catch { /* ignore */ }
265
+ }
266
+
267
+ // package.json scripts
268
+ const pkgPath = join(projectDir, 'package.json');
269
+ if (existsSync(pkgPath)) {
270
+ try {
271
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
272
+ const scripts = pkg.scripts || {};
273
+ if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') {
274
+ return 'npm test';
275
+ }
276
+ } catch { /* ignore */ }
277
+ }
278
+
279
+ // Python
280
+ if (existsSync(join(projectDir, 'pyproject.toml')) || existsSync(join(projectDir, 'setup.py'))) {
281
+ return 'pytest';
282
+ }
283
+
284
+ // Go
285
+ if (existsSync(join(projectDir, 'go.mod'))) return 'go test ./...';
286
+
287
+ // Rust
288
+ if (existsSync(join(projectDir, 'Cargo.toml'))) return 'cargo test';
289
+
290
+ return 'make verify';
291
+ }
292
+
293
+ function detectConventions(projectDir) {
294
+ // Check if this looks like a SerpentStack template (has the standard Makefile)
295
+ const makefile = join(projectDir, 'Makefile');
296
+ if (existsSync(makefile)) {
297
+ try {
298
+ const content = readFileSync(makefile, 'utf8');
299
+ if (/^verify:/m.test(content) && content.includes('uv run')) {
300
+ return 'Services flush, routes commit. Domain returns, not exceptions. Real Postgres in tests.';
301
+ }
302
+ } catch { /* ignore */ }
303
+ }
304
+
305
+ return 'Follow existing patterns. Match the style of surrounding code.';
306
+ }
307
+
84
308
  /**
85
309
  * Build a default agent config entry from an AGENT.md's parsed meta.
86
310
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serpentstack",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "CLI for SerpentStack — AI-driven development standards with project-specific skills and persistent agents",
5
5
  "type": "module",
6
6
  "bin": {