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.
@@ -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')} --create Set up OpenClaw workspace for your project
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 init --force
44
- ${dim('$')} serpentstack skills persistent --start
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
- console.log();
27
- info(`Downloading SerpentStack skills into ${bold(process.cwd())}`);
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('Downloading files from GitHub...');
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(` ${bold('IDE Agent Skills')}`);
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(` ${bold('Persistent Agent (OpenClaw)')}`);
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(` ${bold('Documentation')}`);
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
- printBox('Next steps', [
78
- `${bold('1.')} Open your IDE agent (Claude Code, Cursor, Copilot, etc.)`,
79
- ` and give it the prompt below to generate project-specific skills.`,
80
- '',
81
- `${bold('2.')} Start the persistent background agent:`,
82
- ` ${dim('$')} ${bold('serpentstack skills persistent --start')}`,
83
- '',
84
- `${bold('3.')} Customize your persistent agent identity:`,
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
- async function persistentCreate() {
22
- if (!checkOpenClawWorkspace()) {
23
- error('No .openclaw/ workspace found. Run `serpentstack skills init` first.');
24
- process.exit(1);
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
- info('Customizing OpenClaw workspace for your project...');
28
- console.log();
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.question(` Project name: `);
33
- const lang = await rl.question(` Primary language ${dim('(e.g., Python, TypeScript)')}: `);
34
- const framework = await rl.question(` Framework ${dim('(e.g., FastAPI, Next.js, Django)')}: `);
35
- const conventions = await rl.question(` Key conventions ${dim('(brief, e.g., "services flush, routes commit")')}: `);
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 ${bold(name)} project context`);
67
- info(`Review and customize .openclaw/ files, then run: ${bold('serpentstack skills persistent --start')}`);
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 persistentStart() {
75
- if (!checkOpenClawWorkspace()) {
76
- error('No .openclaw/ workspace found. Run `serpentstack skills init` first.');
77
- process.exit(1);
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 hasOpenClaw = await which('openclaw');
81
- if (!hasOpenClaw) {
82
- warn('OpenClaw is not installed.');
83
- const install = await confirm('Install OpenClaw now? (npm install -g openclaw@latest)');
84
- if (!install) {
85
- info(`Install manually: ${bold('npm install -g openclaw@latest')}`);
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 persistentStop() {
117
- const hasOpenClaw = await which('openclaw');
118
- if (!hasOpenClaw) {
119
- error('OpenClaw is not installed. Nothing to stop.');
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
- info('Stopping persistent agent...');
124
- await new Promise((resolve, reject) => {
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
- error('Specify an action: --create, --start, or --stop');
185
+ // Step 1: Check for .openclaw/ workspace
186
+ console.log(` ${bold('Step 1/3')} ${dim('\u2014 Check workspace')}`);
142
187
  console.log();
143
- console.log(` ${bold('serpentstack skills persistent --create')} Set up the workspace`);
144
- console.log(` ${bold('serpentstack skills persistent --start')} Start the background agent`);
145
- console.log(` ${bold('serpentstack skills persistent --stop')} Stop the background agent`);
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
- process.exit(1);
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
- console.log();
23
- info(`Updating SerpentStack skills in ${bold(process.cwd())}`);
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('Checking for updates...');
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
- const hasGit = await checkGit();
39
- if (!hasGit) {
40
- error('git is not installed. Install git first: https://git-scm.com/downloads');
41
- process.exit(1);
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
- // Clone
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
- // What was created
116
+ // Step 4: Summary
75
117
  console.log();
76
- console.log(` ${dim('Includes:')}`);
77
- console.log(` ${green('\u2713')} FastAPI backend with async SQLAlchemy + JWT auth`);
78
- console.log(` ${green('\u2713')} React frontend with TypeScript + shadcn/ui`);
79
- console.log(` ${green('\u2713')} PostgreSQL + Redis via Docker Compose`);
80
- console.log(` ${green('\u2713')} Terraform infrastructure (AWS App Runner)`);
81
- console.log(` ${green('\u2713')} 10 project-specific Agent Skills in .skills/`);
82
- console.log(` ${green('\u2713')} OpenClaw persistent agent configs in .openclaw/`);
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('# interactive project setup')}`,
88
- `${dim('$')} ${bold('make setup')} ${dim('# install dependencies')}`,
89
- `${dim('$')} ${bold('make dev')} ${dim('# start dev server')}`,
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
- `Then open your IDE agent and start building.`,
92
- `The agent reads ${bold('.skills/')} automatically.`,
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. Run this from a directory created with `serpentstack stack new`.');
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
- console.log();
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('Fetching latest template files...');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serpentstack",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI for SerpentStack — AI-driven development standards with project-specific skills and persistent agents",
5
5
  "type": "module",
6
6
  "bin": {