serpentstack 0.2.9 → 0.2.10
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/lib/commands/persistent.js +10 -8
- package/lib/commands/skills-init.js +11 -8
- package/lib/utils/config.js +231 -7
- package/package.json +1 -1
|
@@ -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
|
|
356
|
+
const detected = detectProjectDefaults(projectDir);
|
|
357
|
+
const template = detectTemplateDefaults(projectDir);
|
|
356
358
|
const existing = config.project || {};
|
|
357
359
|
const defaults = {
|
|
358
|
-
name: existing.name ||
|
|
359
|
-
language: existing.language ||
|
|
360
|
-
framework: existing.framework ||
|
|
361
|
-
devCmd: existing.devCmd ||
|
|
362
|
-
testCmd: existing.testCmd ||
|
|
363
|
-
conventions: existing.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 (
|
|
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
|
-
|
|
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:
|
|
94
|
-
language:
|
|
95
|
-
framework:
|
|
96
|
-
devCmd:
|
|
97
|
-
testCmd:
|
|
98
|
-
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
|
};
|
package/lib/utils/config.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
*/
|