serpentstack 0.2.1 → 0.2.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/bin/serpentstack.js
CHANGED
|
@@ -27,8 +27,7 @@ function showHelp() {
|
|
|
27
27
|
${bold(green('Skills commands'))} ${dim('(any project)')}
|
|
28
28
|
${cyan('skills init')} Download base skills + persistent agent configs
|
|
29
29
|
${cyan('skills update')} Update base skills to latest versions
|
|
30
|
-
${cyan('skills persistent')}
|
|
31
|
-
${cyan('skills persistent')} --start Install OpenClaw (if needed) and start agent
|
|
30
|
+
${cyan('skills persistent')} Guided setup: configure + install + start agent
|
|
32
31
|
${cyan('skills persistent')} --stop Stop the background agent
|
|
33
32
|
|
|
34
33
|
${bold('Options:')}
|
|
@@ -40,8 +39,8 @@ function showHelp() {
|
|
|
40
39
|
${dim('Examples:')}
|
|
41
40
|
${dim('$')} serpentstack stack new my-saas-app
|
|
42
41
|
${dim('$')} serpentstack skills init
|
|
43
|
-
${dim('$')} serpentstack skills
|
|
44
|
-
${dim('$')} serpentstack skills persistent --
|
|
42
|
+
${dim('$')} serpentstack skills persistent
|
|
43
|
+
${dim('$')} serpentstack skills persistent --stop
|
|
45
44
|
|
|
46
45
|
${dim('Docs: https://github.com/Benja-Pauls/SerpentStack')}
|
|
47
46
|
`);
|
|
@@ -82,11 +81,7 @@ async function main() {
|
|
|
82
81
|
await skillsUpdate({ force: !!flags.force, all: !!flags.all });
|
|
83
82
|
} else if (verb === 'persistent') {
|
|
84
83
|
const { skillsPersistent } = await import('../lib/commands/skills-persistent.js');
|
|
85
|
-
await skillsPersistent({
|
|
86
|
-
create: !!flags.create,
|
|
87
|
-
start: !!flags.start,
|
|
88
|
-
stop: !!flags.stop,
|
|
89
|
-
});
|
|
84
|
+
await skillsPersistent({ stop: !!flags.stop });
|
|
90
85
|
} else {
|
|
91
86
|
error(`Unknown skills command: ${verb}`);
|
|
92
87
|
console.log(`\n Available: ${bold('skills init')}, ${bold('skills update')}, ${bold('skills persistent')}\n`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { downloadFile } from '../utils/github.js';
|
|
3
3
|
import { safeWrite } from '../utils/fs-helpers.js';
|
|
4
|
-
import { info, success, warn, error, spinner, bold, dim, green, printBox, printPrompt, fileStatus } from '../utils/ui.js';
|
|
4
|
+
import { info, success, warn, error, spinner, bold, dim, green, printBox, printPrompt, printHeader, fileStatus } from '../utils/ui.js';
|
|
5
5
|
|
|
6
6
|
const SKILLS_FILES = [
|
|
7
7
|
'.skills/find-skills/SKILL.md',
|
|
@@ -23,13 +23,15 @@ const DOCS_FILES = [
|
|
|
23
23
|
const MANIFEST = [...SKILLS_FILES, ...OPENCLAW_FILES, ...DOCS_FILES];
|
|
24
24
|
|
|
25
25
|
export async function skillsInit({ force = false } = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
printHeader();
|
|
27
|
+
|
|
28
|
+
// Step 1: Download files
|
|
29
|
+
console.log(` ${bold('Step 1/2')} ${dim('\u2014 Downloading skills + persistent agent configs')}`);
|
|
28
30
|
console.log();
|
|
29
31
|
|
|
30
32
|
const results = { created: 0, skipped: 0, overwritten: 0, failed: 0 };
|
|
31
33
|
const logs = [];
|
|
32
|
-
const spin = spinner('
|
|
34
|
+
const spin = spinner('Fetching from GitHub...');
|
|
33
35
|
|
|
34
36
|
try {
|
|
35
37
|
for (const repoPath of MANIFEST) {
|
|
@@ -49,13 +51,13 @@ export async function skillsInit({ force = false } = {}) {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// Group output by section
|
|
52
|
-
console.log(` ${
|
|
54
|
+
console.log(` ${dim('IDE Agent Skills')}`);
|
|
53
55
|
for (let i = 0; i < SKILLS_FILES.length; i++) logs[i] && console.log(logs[i]);
|
|
54
56
|
console.log();
|
|
55
|
-
console.log(` ${
|
|
57
|
+
console.log(` ${dim('Persistent Agent (OpenClaw)')}`);
|
|
56
58
|
for (let i = SKILLS_FILES.length; i < SKILLS_FILES.length + OPENCLAW_FILES.length; i++) logs[i] && console.log(logs[i]);
|
|
57
59
|
console.log();
|
|
58
|
-
console.log(` ${
|
|
60
|
+
console.log(` ${dim('Documentation')}`);
|
|
59
61
|
for (let i = SKILLS_FILES.length + OPENCLAW_FILES.length; i < logs.length; i++) logs[i] && console.log(logs[i]);
|
|
60
62
|
console.log();
|
|
61
63
|
|
|
@@ -66,24 +68,21 @@ export async function skillsInit({ force = false } = {}) {
|
|
|
66
68
|
if (results.skipped > 0) parts.push(`${results.skipped} skipped`);
|
|
67
69
|
if (results.failed > 0) parts.push(`${results.failed} failed`);
|
|
68
70
|
console.log(` ${parts.join(dim(' \u2022 '))}`);
|
|
69
|
-
console.log();
|
|
70
71
|
|
|
71
|
-
// Next steps with copyable prompt
|
|
72
72
|
if (results.failed === MANIFEST.length) {
|
|
73
|
+
console.log();
|
|
73
74
|
error('All downloads failed. Check your internet connection and try again.');
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
` ${dim('$')} ${bold('serpentstack skills persistent --create')}`,
|
|
86
|
-
]);
|
|
78
|
+
// Step 2: What to do next
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(` ${bold('Step 2/2')} ${dim('\u2014 Generate project-specific skills')}`);
|
|
81
|
+
console.log();
|
|
82
|
+
console.log(` ${dim('Open your IDE agent (Claude Code, Cursor, Copilot, etc.)')}`);
|
|
83
|
+
console.log(` ${dim('and paste the prompt below. The agent will read your codebase,')}`);
|
|
84
|
+
console.log(` ${dim('interview you about your conventions, and produce a full')}`);
|
|
85
|
+
console.log(` ${dim('.skills/ directory tailored to your project.')}`);
|
|
87
86
|
|
|
88
87
|
printPrompt([
|
|
89
88
|
`Read .skills/generate-skills/SKILL.md and follow its instructions`,
|
|
@@ -95,4 +94,12 @@ export async function skillsInit({ force = false } = {}) {
|
|
|
95
94
|
`SKILL.md with complete templates an agent can copy, not vague`,
|
|
96
95
|
`descriptions. Reference SKILL-AUTHORING.md for the format.`,
|
|
97
96
|
]);
|
|
97
|
+
|
|
98
|
+
printBox('After generating skills, try setting up a persistent agent too', [
|
|
99
|
+
`${dim('$')} ${bold('serpentstack skills persistent')} ${dim('# guided setup + start')}`,
|
|
100
|
+
'',
|
|
101
|
+
`${dim('A background agent that watches your dev server, catches')}`,
|
|
102
|
+
`${dim('errors, runs tests, and keeps your skills up to date.')}`,
|
|
103
|
+
`${dim('Customize its behavior by editing the files in .openclaw/.')}`,
|
|
104
|
+
]);
|
|
98
105
|
}
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { execFile, spawn } from 'node:child_process';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { stdin, stdout } from 'node:process';
|
|
6
|
-
import { info, success, warn, error, confirm, bold, dim } from '../utils/ui.js';
|
|
6
|
+
import { info, success, warn, error, confirm, bold, dim, green, cyan, printBox, printHeader } from '../utils/ui.js';
|
|
7
7
|
|
|
8
8
|
function which(cmd) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
@@ -18,21 +18,40 @@ function checkOpenClawWorkspace() {
|
|
|
18
18
|
return required.every((f) => existsSync(join(dir, f)));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
function checkSoulCustomized() {
|
|
22
|
+
const soulPath = join(process.cwd(), '.openclaw/SOUL.md');
|
|
23
|
+
if (!existsSync(soulPath)) return false;
|
|
24
|
+
const { readFileSync } = require_fs();
|
|
25
|
+
const content = readFileSync(soulPath, 'utf8');
|
|
26
|
+
// If it still has the template placeholder, it's not customized
|
|
27
|
+
return !content.includes('{{PROJECT_NAME}}') && !content.startsWith('# SerpentStack');
|
|
28
|
+
}
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
function require_fs() {
|
|
31
|
+
// Lazy import to avoid top-level dynamic import
|
|
32
|
+
return { readFileSync: existsSync ? (await_import()).readFileSync : null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function askQuestion(rl, label, hint) {
|
|
36
|
+
const answer = await rl.question(` ${green('?')} ${bold(label)}${hint ? ` ${dim(hint)}` : ''}: `);
|
|
37
|
+
return answer.trim();
|
|
38
|
+
}
|
|
29
39
|
|
|
40
|
+
async function customizeWorkspace() {
|
|
30
41
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(` ${bold('Configure your persistent agent')}`);
|
|
45
|
+
console.log(` ${dim("Answer a few questions so the agent understands your project.")}`);
|
|
46
|
+
console.log();
|
|
47
|
+
|
|
31
48
|
try {
|
|
32
|
-
const name = await rl
|
|
33
|
-
const lang = await rl
|
|
34
|
-
const framework = await rl
|
|
35
|
-
const
|
|
49
|
+
const name = await askQuestion(rl, 'Project name', '(e.g., Acme API)');
|
|
50
|
+
const lang = await askQuestion(rl, 'Primary language', '(e.g., Python, TypeScript)');
|
|
51
|
+
const framework = await askQuestion(rl, 'Framework', '(e.g., FastAPI, Next.js, Django)');
|
|
52
|
+
const devCmd = await askQuestion(rl, 'Dev server command', '(e.g., make dev, npm run dev)');
|
|
53
|
+
const testCmd = await askQuestion(rl, 'Test command', '(e.g., make test, pytest, npm test)');
|
|
54
|
+
const conventions = await askQuestion(rl, 'Key conventions', '(brief, e.g., "services flush, routes commit")');
|
|
36
55
|
|
|
37
56
|
console.log();
|
|
38
57
|
|
|
@@ -41,20 +60,20 @@ async function persistentCreate() {
|
|
|
41
60
|
const soulPath = join(process.cwd(), '.openclaw/SOUL.md');
|
|
42
61
|
let soul = readFileSync(soulPath, 'utf8');
|
|
43
62
|
|
|
44
|
-
// Prepend project-specific context
|
|
45
63
|
const projectContext = [
|
|
46
64
|
`# ${name} — Persistent Development Agent`,
|
|
47
65
|
'',
|
|
48
66
|
`**Project:** ${name}`,
|
|
49
67
|
`**Language:** ${lang}`,
|
|
50
68
|
`**Framework:** ${framework}`,
|
|
69
|
+
`**Dev server:** \`${devCmd}\``,
|
|
70
|
+
`**Tests:** \`${testCmd}\``,
|
|
51
71
|
`**Conventions:** ${conventions}`,
|
|
52
72
|
'',
|
|
53
73
|
'---',
|
|
54
74
|
'',
|
|
55
75
|
].join('\n');
|
|
56
76
|
|
|
57
|
-
// Replace the first heading and everything before the first ---
|
|
58
77
|
const dashIndex = soul.indexOf('---');
|
|
59
78
|
if (dashIndex !== -1) {
|
|
60
79
|
soul = projectContext + soul.slice(dashIndex + 3).trimStart();
|
|
@@ -63,38 +82,65 @@ async function persistentCreate() {
|
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
writeFileSync(soulPath, soul, 'utf8');
|
|
66
|
-
success(`Updated .openclaw/SOUL.md with ${
|
|
67
|
-
|
|
85
|
+
success(`Updated ${bold('.openclaw/SOUL.md')} with ${green(name)} project context`);
|
|
86
|
+
|
|
87
|
+
// Update HEARTBEAT.md with the actual dev/test commands
|
|
88
|
+
const heartbeatPath = join(process.cwd(), '.openclaw/HEARTBEAT.md');
|
|
89
|
+
if (existsSync(heartbeatPath)) {
|
|
90
|
+
let heartbeat = readFileSync(heartbeatPath, 'utf8');
|
|
91
|
+
// Replace placeholder commands if they exist
|
|
92
|
+
heartbeat = heartbeat.replace(/make dev/g, devCmd);
|
|
93
|
+
heartbeat = heartbeat.replace(/make test/g, testCmd);
|
|
94
|
+
writeFileSync(heartbeatPath, heartbeat, 'utf8');
|
|
95
|
+
success(`Updated ${bold('.openclaw/HEARTBEAT.md')} with your dev/test commands`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true;
|
|
68
99
|
} finally {
|
|
69
100
|
rl.close();
|
|
70
101
|
}
|
|
71
|
-
console.log();
|
|
72
102
|
}
|
|
73
103
|
|
|
74
|
-
async function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
104
|
+
async function installOpenClaw() {
|
|
105
|
+
console.log();
|
|
106
|
+
warn('OpenClaw is not installed.');
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(` ${dim('OpenClaw is the persistent agent runtime. It runs in the background,')}`);
|
|
109
|
+
console.log(` ${dim('watching your dev server and running health checks on a schedule.')}`);
|
|
110
|
+
console.log();
|
|
79
111
|
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
info('Installing OpenClaw...');
|
|
89
|
-
await new Promise((resolve, reject) => {
|
|
90
|
-
const child = spawn('npm', ['install', '-g', 'openclaw@latest'], { stdio: 'inherit' });
|
|
91
|
-
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`)));
|
|
92
|
-
});
|
|
93
|
-
success('OpenClaw installed');
|
|
112
|
+
const install = await confirm('Install OpenClaw now? (npm install -g openclaw@latest)');
|
|
113
|
+
if (!install) {
|
|
114
|
+
console.log();
|
|
115
|
+
info(`Install manually when ready:`);
|
|
116
|
+
console.log(` ${dim('$')} ${bold('npm install -g openclaw@latest')}`);
|
|
117
|
+
console.log(` ${dim('$')} ${bold('serpentstack skills persistent')}`);
|
|
94
118
|
console.log();
|
|
119
|
+
return false;
|
|
95
120
|
}
|
|
96
121
|
|
|
122
|
+
console.log();
|
|
123
|
+
info('Installing OpenClaw...');
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
const child = spawn('npm', ['install', '-g', 'openclaw@latest'], { stdio: 'inherit' });
|
|
126
|
+
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`)));
|
|
127
|
+
});
|
|
128
|
+
console.log();
|
|
129
|
+
success('OpenClaw installed');
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function startAgent() {
|
|
134
|
+
console.log();
|
|
97
135
|
info('Starting persistent agent...');
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(` ${dim('The agent will:')}`);
|
|
138
|
+
console.log(` ${dim('\u2022 Watch your dev server for errors')}`);
|
|
139
|
+
console.log(` ${dim('\u2022 Run tests on a schedule')}`);
|
|
140
|
+
console.log(` ${dim('\u2022 Flag when skills go stale')}`);
|
|
141
|
+
console.log(` ${dim('\u2022 Propose fixes with full context')}`);
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
98
144
|
const child = spawn('openclaw', ['start', '--workspace', '.openclaw/'], {
|
|
99
145
|
stdio: 'inherit',
|
|
100
146
|
cwd: process.cwd(),
|
|
@@ -113,36 +159,91 @@ async function persistentStart() {
|
|
|
113
159
|
});
|
|
114
160
|
}
|
|
115
161
|
|
|
116
|
-
async function
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
162
|
+
export async function skillsPersistent({ stop = false } = {}) {
|
|
163
|
+
// --stop is the only flag — everything else is a guided flow
|
|
164
|
+
if (stop) {
|
|
165
|
+
const hasOpenClaw = await which('openclaw');
|
|
166
|
+
if (!hasOpenClaw) {
|
|
167
|
+
error('OpenClaw is not installed. Nothing to stop.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
info('Stopping persistent agent...');
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
execFile('openclaw', ['stop'], (err, _stdout, stderr) => {
|
|
173
|
+
if (err) reject(new Error(stderr || err.message));
|
|
174
|
+
else resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
success('Persistent agent stopped');
|
|
178
|
+
console.log();
|
|
120
179
|
return;
|
|
121
180
|
}
|
|
122
181
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
execFile('openclaw', ['stop'], (err, _stdout, stderr) => {
|
|
126
|
-
if (err) {
|
|
127
|
-
reject(new Error(stderr || err.message));
|
|
128
|
-
} else {
|
|
129
|
-
resolve();
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
success('Persistent agent stopped');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function skillsPersistent({ create = false, start = false, stop = false } = {}) {
|
|
137
|
-
if (create) return persistentCreate();
|
|
138
|
-
if (start) return persistentStart();
|
|
139
|
-
if (stop) return persistentStop();
|
|
182
|
+
// === Guided setup flow ===
|
|
183
|
+
printHeader();
|
|
140
184
|
|
|
141
|
-
|
|
185
|
+
// Step 1: Check for .openclaw/ workspace
|
|
186
|
+
console.log(` ${bold('Step 1/3')} ${dim('\u2014 Check workspace')}`);
|
|
142
187
|
console.log();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
188
|
+
|
|
189
|
+
if (!checkOpenClawWorkspace()) {
|
|
190
|
+
error('No .openclaw/ workspace found.');
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(` Run ${bold('serpentstack skills init')} first to download the workspace files.`);
|
|
193
|
+
console.log();
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
success('.openclaw/ workspace found');
|
|
146
198
|
console.log();
|
|
147
|
-
|
|
199
|
+
|
|
200
|
+
// Step 2: Customize if needed
|
|
201
|
+
console.log(` ${bold('Step 2/3')} ${dim('\u2014 Configure for your project')}`);
|
|
202
|
+
|
|
203
|
+
// Check if SOUL.md looks like it has been customized
|
|
204
|
+
const { readFileSync } = await import('node:fs');
|
|
205
|
+
const soulPath = join(process.cwd(), '.openclaw/SOUL.md');
|
|
206
|
+
const soulContent = readFileSync(soulPath, 'utf8');
|
|
207
|
+
const isCustomized = !soulContent.startsWith('# SerpentStack') && !soulContent.includes('{{PROJECT_NAME}}');
|
|
208
|
+
|
|
209
|
+
if (isCustomized) {
|
|
210
|
+
console.log();
|
|
211
|
+
success('Workspace already customized');
|
|
212
|
+
console.log();
|
|
213
|
+
|
|
214
|
+
const reconfigure = await confirm('Reconfigure? (will overwrite current settings)');
|
|
215
|
+
if (reconfigure) {
|
|
216
|
+
await customizeWorkspace();
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
} else {
|
|
220
|
+
await customizeWorkspace();
|
|
221
|
+
console.log();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Step 3: Install OpenClaw + start
|
|
225
|
+
console.log(` ${bold('Step 3/3')} ${dim('\u2014 Start the agent')}`);
|
|
226
|
+
|
|
227
|
+
const hasOpenClaw = await which('openclaw');
|
|
228
|
+
if (!hasOpenClaw) {
|
|
229
|
+
const installed = await installOpenClaw();
|
|
230
|
+
if (!installed) return;
|
|
231
|
+
} else {
|
|
232
|
+
success('OpenClaw is installed');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const shouldStart = await confirm('Start the persistent agent now?');
|
|
236
|
+
if (!shouldStart) {
|
|
237
|
+
console.log();
|
|
238
|
+
printBox('Start later', [
|
|
239
|
+
`${dim('$')} ${bold('serpentstack skills persistent')} ${dim('# run setup again')}`,
|
|
240
|
+
`${dim('$')} ${bold('openclaw start --workspace .openclaw/')} ${dim('# start directly')}`,
|
|
241
|
+
'',
|
|
242
|
+
`${dim('To stop:')}`,
|
|
243
|
+
`${dim('$')} ${bold('serpentstack skills persistent --stop')}`,
|
|
244
|
+
]);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await startAgent();
|
|
148
249
|
}
|
|
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { downloadFile } from '../utils/github.js';
|
|
4
4
|
import { safeWrite } from '../utils/fs-helpers.js';
|
|
5
|
-
import { info, success, warn, error, spinner, bold, dim, green, fileStatus } from '../utils/ui.js';
|
|
5
|
+
import { info, success, warn, error, spinner, bold, dim, green, printHeader, fileStatus } from '../utils/ui.js';
|
|
6
6
|
|
|
7
7
|
const MANIFEST = [
|
|
8
8
|
'.skills/find-skills/SKILL.md',
|
|
@@ -19,19 +19,19 @@ const MANIFEST = [
|
|
|
19
19
|
const CUSTOMIZABLE = new Set(['.openclaw/SOUL.md', '.openclaw/HEARTBEAT.md', '.openclaw/AGENTS.md']);
|
|
20
20
|
|
|
21
21
|
export async function skillsUpdate({ force = false, all = false } = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
printHeader();
|
|
23
|
+
|
|
24
|
+
info(`Checking for updates in ${bold(process.cwd())}`);
|
|
24
25
|
console.log();
|
|
25
26
|
|
|
26
27
|
const results = { updated: 0, skipped: 0, unchanged: 0, failed: 0 };
|
|
27
|
-
const spin = spinner('
|
|
28
|
+
const spin = spinner('Comparing with latest versions...');
|
|
28
29
|
const logs = [];
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
for (const repoPath of MANIFEST) {
|
|
32
33
|
const destPath = join(process.cwd(), repoPath);
|
|
33
34
|
|
|
34
|
-
// By default, only update files that already exist locally
|
|
35
35
|
if (!existsSync(destPath) && !all) {
|
|
36
36
|
logs.push(fileStatus(repoPath, 'skipped', `not installed \u2014 use ${bold('--all')} to add`));
|
|
37
37
|
results.skipped++;
|
|
@@ -41,7 +41,6 @@ export async function skillsUpdate({ force = false, all = false } = {}) {
|
|
|
41
41
|
try {
|
|
42
42
|
const content = await downloadFile(repoPath);
|
|
43
43
|
|
|
44
|
-
// Check if local content is identical
|
|
45
44
|
if (existsSync(destPath)) {
|
|
46
45
|
const local = readFileSync(destPath, 'utf8');
|
|
47
46
|
if (local === content) {
|
|
@@ -50,7 +49,6 @@ export async function skillsUpdate({ force = false, all = false } = {}) {
|
|
|
50
49
|
continue;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
// Warn about customizable files
|
|
54
52
|
if (CUSTOMIZABLE.has(repoPath) && !force) {
|
|
55
53
|
logs.push(fileStatus(repoPath, 'skipped', `local changes \u2014 use ${bold('--force')} to overwrite`));
|
|
56
54
|
results.skipped++;
|
|
@@ -80,5 +78,11 @@ export async function skillsUpdate({ force = false, all = false } = {}) {
|
|
|
80
78
|
if (results.skipped > 0) parts.push(`${results.skipped} skipped`);
|
|
81
79
|
if (results.failed > 0) parts.push(`${results.failed} failed`);
|
|
82
80
|
console.log(` ${parts.join(dim(' \u2022 '))}`);
|
|
81
|
+
|
|
82
|
+
if (results.updated > 0) {
|
|
83
|
+
console.log();
|
|
84
|
+
info(`${dim('Your project-specific skills (generated by your IDE agent) are not affected.')}`);
|
|
85
|
+
info(`${dim('Only the base SerpentStack files were updated.')}`);
|
|
86
|
+
}
|
|
83
87
|
console.log();
|
|
84
88
|
}
|
|
@@ -3,7 +3,7 @@ import { rm } from 'node:fs/promises';
|
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { cloneRepo, checkGit } from '../utils/github.js';
|
|
6
|
-
import { info, success, error, spinner, bold, dim, green, printBox, printPrompt } from '../utils/ui.js';
|
|
6
|
+
import { info, success, warn, error, spinner, bold, dim, green, cyan, printBox, printPrompt, printHeader } from '../utils/ui.js';
|
|
7
7
|
|
|
8
8
|
const CLEANUP_PATHS = [
|
|
9
9
|
'cli',
|
|
@@ -20,6 +20,12 @@ function validateName(name) {
|
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function checkCommand(cmd) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
execFile('which', [cmd], (err) => resolve(!err));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export async function stackNew(name) {
|
|
24
30
|
const nameError = validateName(name);
|
|
25
31
|
if (nameError) {
|
|
@@ -35,16 +41,48 @@ export async function stackNew(name) {
|
|
|
35
41
|
process.exit(1);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
printHeader();
|
|
45
|
+
|
|
46
|
+
// Step 1: Check prerequisites
|
|
47
|
+
console.log(` ${bold('Step 1/4')} ${dim('\u2014 Checking prerequisites')}`);
|
|
48
|
+
console.log();
|
|
49
|
+
|
|
50
|
+
const checks = [
|
|
51
|
+
{ cmd: 'git', label: 'git', url: 'https://git-scm.com/downloads' },
|
|
52
|
+
{ cmd: 'python3', label: 'Python 3.12+', url: 'https://python.org/downloads' },
|
|
53
|
+
{ cmd: 'node', label: 'Node 22+', url: 'https://nodejs.org' },
|
|
54
|
+
{ cmd: 'docker', label: 'Docker', url: 'https://docker.com/get-started' },
|
|
55
|
+
{ cmd: 'uv', label: 'uv (Python package manager)', url: 'https://docs.astral.sh/uv' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
let missing = [];
|
|
59
|
+
for (const { cmd, label, url } of checks) {
|
|
60
|
+
const found = await checkCommand(cmd);
|
|
61
|
+
if (found) {
|
|
62
|
+
console.log(` ${green('\u2713')} ${label}`);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(` ${dim('\u2022')} ${dim(label)} ${dim(`\u2014 install: ${url}`)}`);
|
|
65
|
+
missing.push(label);
|
|
66
|
+
}
|
|
42
67
|
}
|
|
68
|
+
console.log();
|
|
43
69
|
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
// git is required to clone — everything else is needed later
|
|
72
|
+
const gitMissing = !(await checkCommand('git'));
|
|
73
|
+
if (gitMissing) {
|
|
74
|
+
error(`git is required to scaffold the project. Install it first: https://git-scm.com/downloads`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
warn(`Missing: ${bold(missing.join(', '))}. You can install these before running ${bold('make setup')}.`);
|
|
78
|
+
console.log();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Step 2: Clone template
|
|
82
|
+
console.log(` ${bold('Step 2/4')} ${dim('\u2014 Cloning template')}`);
|
|
44
83
|
console.log();
|
|
45
84
|
|
|
46
|
-
|
|
47
|
-
const spin = spinner(`Cloning SerpentStack template into ${name}/...`);
|
|
85
|
+
const spin = spinner(`Downloading SerpentStack template...`);
|
|
48
86
|
try {
|
|
49
87
|
await cloneRepo(dest);
|
|
50
88
|
spin.stop(success(`Template cloned into ${green(name)}/`));
|
|
@@ -54,15 +92,19 @@ export async function stackNew(name) {
|
|
|
54
92
|
process.exit(1);
|
|
55
93
|
}
|
|
56
94
|
|
|
57
|
-
// Clean up repo-specific files
|
|
95
|
+
// Step 3: Clean up repo-specific files
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(` ${bold('Step 3/4')} ${dim('\u2014 Preparing project')}`);
|
|
98
|
+
console.log();
|
|
99
|
+
|
|
58
100
|
for (const p of CLEANUP_PATHS) {
|
|
59
101
|
const full = join(dest, p);
|
|
60
102
|
if (existsSync(full)) {
|
|
61
103
|
await rm(full, { recursive: true, force: true });
|
|
62
104
|
}
|
|
63
105
|
}
|
|
106
|
+
success('Removed SerpentStack repo files');
|
|
64
107
|
|
|
65
|
-
// Initialize fresh git repo
|
|
66
108
|
await new Promise((resolve, reject) => {
|
|
67
109
|
execFile('git', ['init'], { cwd: dest }, (err) => {
|
|
68
110
|
if (err) reject(new Error(`git init failed: ${err.message}`));
|
|
@@ -71,25 +113,28 @@ export async function stackNew(name) {
|
|
|
71
113
|
});
|
|
72
114
|
success('Initialized fresh git repository');
|
|
73
115
|
|
|
74
|
-
//
|
|
116
|
+
// Step 4: Summary
|
|
75
117
|
console.log();
|
|
76
|
-
console.log(` ${dim('
|
|
77
|
-
console.log(
|
|
78
|
-
|
|
79
|
-
console.log(` ${
|
|
80
|
-
console.log(` ${green('\u2713')}
|
|
81
|
-
console.log(` ${green('\u2713')}
|
|
82
|
-
console.log(` ${green('\u2713')}
|
|
118
|
+
console.log(` ${bold('Step 4/4')} ${dim('\u2014 Done!')}`);
|
|
119
|
+
console.log();
|
|
120
|
+
|
|
121
|
+
console.log(` ${dim('Your project includes:')}`);
|
|
122
|
+
console.log(` ${green('\u2713')} FastAPI backend ${dim('(async SQLAlchemy, JWT auth, ownership enforcement)')}`);
|
|
123
|
+
console.log(` ${green('\u2713')} React frontend ${dim('(TypeScript, Vite, shadcn/ui)')}`);
|
|
124
|
+
console.log(` ${green('\u2713')} PostgreSQL + Redis ${dim('(Docker Compose)')}`);
|
|
125
|
+
console.log(` ${green('\u2713')} Terraform infrastructure ${dim('(AWS App Runner, RDS, ECR)')}`);
|
|
126
|
+
console.log(` ${green('\u2713')} 10 project-specific Agent Skills ${dim('(.skills/)')}`);
|
|
127
|
+
console.log(` ${green('\u2713')} Persistent agent configs ${dim('(.openclaw/)')}`);
|
|
83
128
|
console.log();
|
|
84
129
|
|
|
85
130
|
printBox('Get started', [
|
|
86
131
|
`${dim('$')} ${bold(`cd ${name}`)}`,
|
|
87
|
-
`${dim('$')} ${bold('make init')} ${dim('#
|
|
88
|
-
`${dim('$')} ${bold('make setup')} ${dim('# install dependencies')}`,
|
|
89
|
-
`${dim('$')} ${bold('make dev')} ${dim('# start
|
|
132
|
+
`${dim('$')} ${bold('make init')} ${dim('# set project name, DB config')}`,
|
|
133
|
+
`${dim('$')} ${bold('make setup')} ${dim('# install Python + Node dependencies')}`,
|
|
134
|
+
`${dim('$')} ${bold('make dev')} ${dim('# start Postgres + Redis + backend + frontend')}`,
|
|
90
135
|
'',
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
`${dim('Then open your IDE agent and try the prompt below.')}`,
|
|
137
|
+
`${dim('The agent discovers .skills/ automatically.')}`,
|
|
93
138
|
]);
|
|
94
139
|
|
|
95
140
|
printPrompt([
|
|
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { downloadFile } from '../utils/github.js';
|
|
4
4
|
import { safeWrite } from '../utils/fs-helpers.js';
|
|
5
|
-
import { info, success, warn, error, spinner, bold, dim, green, fileStatus } from '../utils/ui.js';
|
|
5
|
+
import { info, success, warn, error, spinner, bold, dim, green, printHeader, fileStatus } from '../utils/ui.js';
|
|
6
6
|
|
|
7
7
|
// Template-level files that may be updated upstream
|
|
8
8
|
const TEMPLATE_FILES = [
|
|
@@ -16,16 +16,18 @@ const TEMPLATE_FILES = [
|
|
|
16
16
|
export async function stackUpdate({ force = false } = {}) {
|
|
17
17
|
// Verify we're in a SerpentStack project
|
|
18
18
|
if (!existsSync(join(process.cwd(), 'Makefile')) || !existsSync(join(process.cwd(), 'backend'))) {
|
|
19
|
-
error('Not a SerpentStack project.
|
|
19
|
+
error('Not a SerpentStack project.');
|
|
20
|
+
console.log(`\n Run this from a directory created with ${bold('serpentstack stack new')}.\n`);
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
printHeader();
|
|
25
|
+
|
|
24
26
|
info(`Checking for template updates in ${bold(process.cwd())}`);
|
|
25
27
|
console.log();
|
|
26
28
|
|
|
27
29
|
const results = { updated: 0, unchanged: 0, failed: 0 };
|
|
28
|
-
const spin = spinner('
|
|
30
|
+
const spin = spinner('Comparing with latest template...');
|
|
29
31
|
const logs = [];
|
|
30
32
|
|
|
31
33
|
try {
|
|
@@ -64,5 +66,14 @@ export async function stackUpdate({ force = false } = {}) {
|
|
|
64
66
|
if (results.unchanged > 0) parts.push(`${results.unchanged} up to date`);
|
|
65
67
|
if (results.failed > 0) parts.push(`${results.failed} failed`);
|
|
66
68
|
console.log(` ${parts.join(dim(' \u2022 '))}`);
|
|
69
|
+
|
|
70
|
+
if (results.updated > 0) {
|
|
71
|
+
console.log();
|
|
72
|
+
info(`${dim('Review changes with')} ${bold('git diff')} ${dim('before committing.')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Also offer skills update
|
|
76
|
+
console.log();
|
|
77
|
+
info(`${dim('To also update skills:')} ${bold('serpentstack skills update')}`);
|
|
67
78
|
console.log();
|
|
68
79
|
}
|