serpentstack 0.1.0 → 0.2.1

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { error, bold, getVersion, printHeader } from '../lib/utils/ui.js';
3
+ import { error, bold, dim, green, cyan, getVersion, printHeader } from '../lib/utils/ui.js';
4
4
 
5
5
  function parseFlags(args) {
6
6
  const flags = {};
@@ -20,22 +20,30 @@ function showHelp() {
20
20
  printHeader();
21
21
  console.log(` ${bold('Usage:')} serpentstack <command> [options]
22
22
 
23
- ${bold('Stack commands')} (new projects):
24
- stack new <name> Scaffold a new project from the template
25
- stack update Update template-level files to latest
23
+ ${bold(green('Stack commands'))} ${dim('(new projects)')}
24
+ ${cyan('stack new')} <name> Scaffold a new project from the template
25
+ ${cyan('stack update')} Update template-level files to latest
26
26
 
27
- ${bold('Skills commands')} (any project):
28
- skills init Download base skills + persistent agent configs
29
- skills update Update base skills to latest versions
30
- skills persistent --create Set up OpenClaw workspace for your project
31
- skills persistent --start Install OpenClaw (if needed) and start agent
32
- skills persistent --stop Stop the background agent
27
+ ${bold(green('Skills commands'))} ${dim('(any project)')}
28
+ ${cyan('skills init')} Download base skills + persistent agent configs
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
32
+ ${cyan('skills persistent')} --stop Stop the background agent
33
33
 
34
34
  ${bold('Options:')}
35
35
  --force Overwrite existing files
36
36
  --all Include new files in updates (skills update)
37
37
  --version Show version
38
38
  --help Show this help
39
+
40
+ ${dim('Examples:')}
41
+ ${dim('$')} serpentstack stack new my-saas-app
42
+ ${dim('$')} serpentstack skills init
43
+ ${dim('$')} serpentstack skills init --force
44
+ ${dim('$')} serpentstack skills persistent --start
45
+
46
+ ${dim('Docs: https://github.com/Benja-Pauls/SerpentStack')}
39
47
  `);
40
48
  }
41
49
 
@@ -85,8 +93,8 @@ async function main() {
85
93
  process.exit(1);
86
94
  }
87
95
  } else {
88
- error(`Unknown command: ${noun}`);
89
- showHelp();
96
+ error(`Unknown command: ${bold(noun)}`);
97
+ console.log(`\n Run ${bold('serpentstack --help')} to see available commands.\n`);
90
98
  process.exit(1);
91
99
  }
92
100
  }
@@ -1,24 +1,34 @@
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 } from '../utils/ui.js';
4
+ import { info, success, warn, error, spinner, bold, dim, green, printBox, printPrompt, fileStatus } from '../utils/ui.js';
5
5
 
6
- const MANIFEST = [
6
+ const SKILLS_FILES = [
7
7
  '.skills/find-skills/SKILL.md',
8
8
  '.skills/generate-skills/SKILL.md',
9
9
  '.skills/git-workflow/SKILL.md',
10
10
  '.skills/model-routing/SKILL.md',
11
+ ];
12
+
13
+ const OPENCLAW_FILES = [
11
14
  '.openclaw/SOUL.md',
12
15
  '.openclaw/HEARTBEAT.md',
13
16
  '.openclaw/AGENTS.md',
17
+ ];
18
+
19
+ const DOCS_FILES = [
14
20
  'SKILL-AUTHORING.md',
15
21
  ];
16
22
 
23
+ const MANIFEST = [...SKILLS_FILES, ...OPENCLAW_FILES, ...DOCS_FILES];
24
+
17
25
  export async function skillsInit({ force = false } = {}) {
26
+ console.log();
18
27
  info(`Downloading SerpentStack skills into ${bold(process.cwd())}`);
19
28
  console.log();
20
29
 
21
30
  const results = { created: 0, skipped: 0, overwritten: 0, failed: 0 };
31
+ const logs = [];
22
32
  const spin = spinner('Downloading files from GitHub...');
23
33
 
24
34
  try {
@@ -28,41 +38,61 @@ export async function skillsInit({ force = false } = {}) {
28
38
  const content = await downloadFile(repoPath);
29
39
  const status = safeWrite(destPath, content, { force });
30
40
  results[status]++;
31
-
32
- const icon = status === 'created' ? '\u2713'
33
- : status === 'overwritten' ? '\u21BB'
34
- : '\u2022';
35
- const label = status === 'skipped' ? dim(`${repoPath} (exists, skipped)`)
36
- : status === 'overwritten' ? `${repoPath} (updated)`
37
- : repoPath;
38
-
39
- // Buffer output — will show after spinner stops
40
- results[`_log_${repoPath}`] = ` ${icon} ${label}`;
41
+ logs.push(fileStatus(repoPath, status, 'use --force to overwrite'));
41
42
  } catch (err) {
42
43
  results.failed++;
43
- results[`_log_${repoPath}`] = ` \u2717 ${repoPath} — ${err.message}`;
44
+ logs.push(fileStatus(repoPath, 'failed', err.message));
44
45
  }
45
46
  }
46
47
  } finally {
47
48
  spin.stop();
48
49
  }
49
50
 
50
- // Print file results
51
- for (const repoPath of MANIFEST) {
52
- console.log(results[`_log_${repoPath}`]);
53
- }
51
+ // Group output by section
52
+ console.log(` ${bold('IDE Agent Skills')}`);
53
+ for (let i = 0; i < SKILLS_FILES.length; i++) logs[i] && console.log(logs[i]);
54
54
  console.log();
55
-
56
- // Summary
57
- if (results.created > 0) success(`${results.created} file(s) created`);
58
- if (results.overwritten > 0) success(`${results.overwritten} file(s) updated`);
59
- if (results.skipped > 0) warn(`${results.skipped} file(s) skipped (already exist — use ${bold('--force')} to overwrite)`);
60
- if (results.failed > 0) error(`${results.failed} file(s) failed to download`);
61
-
62
- // Next steps
55
+ console.log(` ${bold('Persistent Agent (OpenClaw)')}`);
56
+ for (let i = SKILLS_FILES.length; i < SKILLS_FILES.length + OPENCLAW_FILES.length; i++) logs[i] && console.log(logs[i]);
63
57
  console.log();
64
- info(bold('Next steps:'));
65
- console.log(` 1. Open your IDE agent and say: ${bold('"generate skills for my project"')}`);
66
- console.log(` 2. Start the persistent agent: ${bold('serpentstack skills persistent --start')}`);
58
+ console.log(` ${bold('Documentation')}`);
59
+ for (let i = SKILLS_FILES.length + OPENCLAW_FILES.length; i < logs.length; i++) logs[i] && console.log(logs[i]);
60
+ console.log();
61
+
62
+ // Summary line
63
+ const parts = [];
64
+ if (results.created > 0) parts.push(green(`${results.created} created`));
65
+ if (results.overwritten > 0) parts.push(`${results.overwritten} updated`);
66
+ if (results.skipped > 0) parts.push(`${results.skipped} skipped`);
67
+ if (results.failed > 0) parts.push(`${results.failed} failed`);
68
+ console.log(` ${parts.join(dim(' \u2022 '))}`);
67
69
  console.log();
70
+
71
+ // Next steps with copyable prompt
72
+ if (results.failed === MANIFEST.length) {
73
+ error('All downloads failed. Check your internet connection and try again.');
74
+ return;
75
+ }
76
+
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
+ ]);
87
+
88
+ printPrompt([
89
+ `Read .skills/generate-skills/SKILL.md and follow its instructions`,
90
+ `to generate project-specific skills for this codebase. Interview me`,
91
+ `about my architecture decisions — how I handle transactions, auth,`,
92
+ `error patterns, testing strategy, and deployment. Ask about the`,
93
+ `business domain too: what this app does, key user flows, and where`,
94
+ `agents are most likely to make mistakes. Write each skill as a`,
95
+ `SKILL.md with complete templates an agent can copy, not vague`,
96
+ `descriptions. Reference SKILL-AUTHORING.md for the format.`,
97
+ ]);
68
98
  }
@@ -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 } from '../utils/ui.js';
5
+ import { info, success, warn, error, spinner, bold, dim, green, fileStatus } from '../utils/ui.js';
6
6
 
7
7
  const MANIFEST = [
8
8
  '.skills/find-skills/SKILL.md',
@@ -19,6 +19,7 @@ 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();
22
23
  info(`Updating SerpentStack skills in ${bold(process.cwd())}`);
23
24
  console.log();
24
25
 
@@ -32,7 +33,7 @@ export async function skillsUpdate({ force = false, all = false } = {}) {
32
33
 
33
34
  // By default, only update files that already exist locally
34
35
  if (!existsSync(destPath) && !all) {
35
- logs.push(` \u2022 ${dim(`${repoPath} (not installed use ${bold('--all')} to add)`)}`);
36
+ logs.push(fileStatus(repoPath, 'skipped', `not installed \u2014 use ${bold('--all')} to add`));
36
37
  results.skipped++;
37
38
  continue;
38
39
  }
@@ -44,25 +45,25 @@ export async function skillsUpdate({ force = false, all = false } = {}) {
44
45
  if (existsSync(destPath)) {
45
46
  const local = readFileSync(destPath, 'utf8');
46
47
  if (local === content) {
47
- logs.push(` \u2022 ${dim(`${repoPath} (up to date)`)}`);
48
+ logs.push(fileStatus(repoPath, 'unchanged'));
48
49
  results.unchanged++;
49
50
  continue;
50
51
  }
51
52
 
52
53
  // Warn about customizable files
53
54
  if (CUSTOMIZABLE.has(repoPath) && !force) {
54
- logs.push(` ! ${repoPath} (has local changes use ${bold('--force')} to overwrite)`);
55
+ logs.push(fileStatus(repoPath, 'skipped', `local changes \u2014 use ${bold('--force')} to overwrite`));
55
56
  results.skipped++;
56
57
  continue;
57
58
  }
58
59
  }
59
60
 
60
61
  const status = safeWrite(destPath, content, { force: true });
61
- logs.push(` \u21BB ${repoPath} (${status})`);
62
+ logs.push(fileStatus(repoPath, status === 'created' ? 'created' : 'overwritten'));
62
63
  results.updated++;
63
64
  } catch (err) {
64
65
  results.failed++;
65
- logs.push(` \u2717 ${repoPath} ${err.message}`);
66
+ logs.push(fileStatus(repoPath, 'failed', err.message));
66
67
  }
67
68
  }
68
69
  } finally {
@@ -72,9 +73,12 @@ export async function skillsUpdate({ force = false, all = false } = {}) {
72
73
  for (const log of logs) console.log(log);
73
74
  console.log();
74
75
 
75
- if (results.updated > 0) success(`${results.updated} file(s) updated`);
76
- if (results.unchanged > 0) info(`${results.unchanged} file(s) already up to date`);
77
- if (results.skipped > 0) warn(`${results.skipped} file(s) skipped`);
78
- if (results.failed > 0) error(`${results.failed} file(s) failed`);
76
+ // Summary line
77
+ const parts = [];
78
+ if (results.updated > 0) parts.push(green(`${results.updated} updated`));
79
+ if (results.unchanged > 0) parts.push(`${results.unchanged} up to date`);
80
+ if (results.skipped > 0) parts.push(`${results.skipped} skipped`);
81
+ if (results.failed > 0) parts.push(`${results.failed} failed`);
82
+ console.log(` ${parts.join(dim(' \u2022 '))}`);
79
83
  console.log();
80
84
  }
@@ -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 } from '../utils/ui.js';
6
+ import { info, success, error, spinner, bold, dim, green, printBox, printPrompt } from '../utils/ui.js';
7
7
 
8
8
  const CLEANUP_PATHS = [
9
9
  'cli',
@@ -41,11 +41,13 @@ export async function stackNew(name) {
41
41
  process.exit(1);
42
42
  }
43
43
 
44
+ console.log();
45
+
44
46
  // Clone
45
47
  const spin = spinner(`Cloning SerpentStack template into ${name}/...`);
46
48
  try {
47
49
  await cloneRepo(dest);
48
- spin.stop(success(`Template cloned into ${bold(name)}/`));
50
+ spin.stop(success(`Template cloned into ${green(name)}/`));
49
51
  } catch (err) {
50
52
  spin.stop();
51
53
  error(err.message);
@@ -69,12 +71,31 @@ export async function stackNew(name) {
69
71
  });
70
72
  success('Initialized fresh git repository');
71
73
 
72
- // Next steps
74
+ // What was created
73
75
  console.log();
74
- info(bold('Next steps:'));
75
- console.log(` cd ${name}`);
76
- console.log(` make init ${bold('#')} interactive project setup`);
77
- console.log(` make setup ${bold('#')} install dependencies`);
78
- console.log(` make dev ${bold('#')} start dev server`);
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/`);
79
83
  console.log();
84
+
85
+ printBox('Get started', [
86
+ `${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')}`,
90
+ '',
91
+ `Then open your IDE agent and start building.`,
92
+ `The agent reads ${bold('.skills/')} automatically.`,
93
+ ]);
94
+
95
+ printPrompt([
96
+ `Read .skills/scaffold/SKILL.md and add a Projects resource with`,
97
+ `full CRUD, JWT auth, and ownership enforcement. Follow the`,
98
+ `service/route/test/frontend patterns exactly as the skill`,
99
+ `describes. Run make verify when done to confirm everything passes.`,
100
+ ], { hint: 'Try this prompt to verify your skills are working' });
80
101
  }
@@ -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 } from '../utils/ui.js';
5
+ import { info, success, warn, error, spinner, bold, dim, green, fileStatus } from '../utils/ui.js';
6
6
 
7
7
  // Template-level files that may be updated upstream
8
8
  const TEMPLATE_FILES = [
@@ -20,6 +20,7 @@ export async function stackUpdate({ force = false } = {}) {
20
20
  process.exit(1);
21
21
  }
22
22
 
23
+ console.log();
23
24
  info(`Checking for template updates in ${bold(process.cwd())}`);
24
25
  console.log();
25
26
 
@@ -36,18 +37,18 @@ export async function stackUpdate({ force = false } = {}) {
36
37
  if (existsSync(destPath)) {
37
38
  const local = readFileSync(destPath, 'utf8');
38
39
  if (local === content) {
39
- logs.push(` \u2022 ${dim(`${repoPath} (up to date)`)}`);
40
+ logs.push(fileStatus(repoPath, 'unchanged'));
40
41
  results.unchanged++;
41
42
  continue;
42
43
  }
43
44
  }
44
45
 
45
46
  const status = safeWrite(destPath, content, { force: true });
46
- logs.push(` \u21BB ${repoPath} (${status})`);
47
+ logs.push(fileStatus(repoPath, status === 'created' ? 'created' : 'overwritten'));
47
48
  results.updated++;
48
49
  } catch (err) {
49
50
  results.failed++;
50
- logs.push(` \u2717 ${repoPath} ${err.message}`);
51
+ logs.push(fileStatus(repoPath, 'failed', err.message));
51
52
  }
52
53
  }
53
54
  } finally {
@@ -57,8 +58,11 @@ export async function stackUpdate({ force = false } = {}) {
57
58
  for (const log of logs) console.log(log);
58
59
  console.log();
59
60
 
60
- if (results.updated > 0) success(`${results.updated} file(s) updated`);
61
- if (results.unchanged > 0) info(`${results.unchanged} file(s) already up to date`);
62
- if (results.failed > 0) error(`${results.failed} file(s) failed`);
61
+ // Summary line
62
+ const parts = [];
63
+ if (results.updated > 0) parts.push(green(`${results.updated} updated`));
64
+ if (results.unchanged > 0) parts.push(`${results.unchanged} up to date`);
65
+ if (results.failed > 0) parts.push(`${results.failed} failed`);
66
+ console.log(` ${parts.join(dim(' \u2022 '))}`);
63
67
  console.log();
64
68
  }
package/lib/utils/ui.js CHANGED
@@ -8,17 +8,27 @@ const c = (code) => NO_COLOR ? '' : `\x1b[${code}m`;
8
8
  const RESET = c(0);
9
9
  const BOLD = c(1);
10
10
  const DIM = c(2);
11
+ const ITALIC = c(3);
11
12
  const RED = c(31);
12
13
  const GREEN = c(32);
13
14
  const YELLOW = c(33);
15
+ const BLUE = c(34);
16
+ const MAGENTA = c(35);
14
17
  const CYAN = c(36);
18
+ const BG_DIM = c(100);
15
19
 
16
- export const info = (msg) => console.log(`${CYAN}i${RESET} ${msg}`);
20
+ export const info = (msg) => console.log(`${CYAN}\u2022${RESET} ${msg}`);
17
21
  export const success = (msg) => console.log(`${GREEN}\u2713${RESET} ${msg}`);
18
- export const warn = (msg) => console.log(`${YELLOW}!${RESET} ${msg}`);
22
+ export const warn = (msg) => console.log(`${YELLOW}\u25B3${RESET} ${msg}`);
19
23
  export const error = (msg) => console.error(`${RED}\u2717${RESET} ${msg}`);
20
24
  export const dim = (msg) => `${DIM}${msg}${RESET}`;
21
25
  export const bold = (msg) => `${BOLD}${msg}${RESET}`;
26
+ export const green = (msg) => `${GREEN}${msg}${RESET}`;
27
+ export const yellow = (msg) => `${YELLOW}${msg}${RESET}`;
28
+ export const red = (msg) => `${RED}${msg}${RESET}`;
29
+ export const cyan = (msg) => `${CYAN}${msg}${RESET}`;
30
+ export const magenta = (msg) => `${MAGENTA}${msg}${RESET}`;
31
+ export const blue = (msg) => `${BLUE}${msg}${RESET}`;
22
32
 
23
33
  const SPINNER_FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
24
34
 
@@ -60,5 +70,82 @@ export function getVersion() {
60
70
  }
61
71
 
62
72
  export function printHeader() {
63
- console.log(`\n ${BOLD}${CYAN}\u2728 SerpentStack${RESET} ${DIM}v${getVersion()}${RESET}\n`);
73
+ console.log(`\n ${BOLD}${GREEN}\u2728 SerpentStack${RESET} ${DIM}v${getVersion()}${RESET}\n`);
74
+ }
75
+
76
+ /**
77
+ * Print a boxed section with a title and content lines.
78
+ * Used for "Next steps" and prompt blocks.
79
+ */
80
+ export function printBox(title, lines, { color = GREEN, icon = '\u25B6' } = {}) {
81
+ const maxLen = Math.max(
82
+ title.length + 4,
83
+ ...lines.map(l => stripAnsi(l).length + 4)
84
+ );
85
+ const width = Math.min(Math.max(maxLen, 50), 80);
86
+ const top = `${color}\u250C${'─'.repeat(width)}\u2510${RESET}`;
87
+ const bot = `${color}\u2514${'─'.repeat(width)}\u2518${RESET}`;
88
+
89
+ console.log(top);
90
+ console.log(`${color}\u2502${RESET} ${BOLD}${icon} ${title}${RESET}${' '.repeat(Math.max(0, width - stripAnsi(title).length - 3))}${color}\u2502${RESET}`);
91
+ console.log(`${color}\u2502${' '.repeat(width)}${color}\u2502${RESET}`);
92
+ for (const line of lines) {
93
+ const pad = Math.max(0, width - stripAnsi(line).length - 2);
94
+ console.log(`${color}\u2502${RESET} ${line}${' '.repeat(pad)}${color}\u2502${RESET}`);
95
+ }
96
+ console.log(bot);
97
+ }
98
+
99
+ /**
100
+ * Print a copyable prompt block that the user can paste into their IDE agent.
101
+ * Accepts a single string or an array of lines for multi-line prompts.
102
+ */
103
+ export function printPrompt(promptLines, { hint = 'Copy this prompt and paste it into your IDE agent' } = {}) {
104
+ const lines = Array.isArray(promptLines) ? promptLines : [promptLines];
105
+ const maxLineLen = Math.max(...lines.map(l => stripAnsi(l).length));
106
+ const width = Math.min(Math.max(maxLineLen + 6, 50), 90);
107
+ const innerWidth = width - 2;
108
+
109
+ console.log();
110
+ console.log(` ${DIM}\u250C${'─'.repeat(innerWidth)}\u2510${RESET}`);
111
+ for (const line of lines) {
112
+ const pad = Math.max(0, innerWidth - stripAnsi(line).length - 2);
113
+ console.log(` ${DIM}\u2502${RESET} ${CYAN}${line}${RESET}${' '.repeat(pad)}${DIM}\u2502${RESET}`);
114
+ }
115
+ console.log(` ${DIM}\u2514${'─'.repeat(innerWidth)}\u2518${RESET}`);
116
+ console.log(` ${DIM}\u2191 ${hint}${RESET}`);
117
+ console.log();
118
+ }
119
+
120
+ /**
121
+ * File status icon with color.
122
+ */
123
+ export function fileIcon(status) {
124
+ switch (status) {
125
+ case 'created': return `${GREEN}\u2713${RESET}`;
126
+ case 'overwritten': return `${CYAN}\u21BB${RESET}`;
127
+ case 'skipped': return `${YELLOW}\u2022${RESET}`;
128
+ case 'failed': return `${RED}\u2717${RESET}`;
129
+ case 'unchanged': return `${DIM}\u2022${RESET}`;
130
+ default: return `${DIM}\u2022${RESET}`;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Format a file path with its status for display.
136
+ */
137
+ export function fileStatus(path, status, detail) {
138
+ const icon = fileIcon(status);
139
+ switch (status) {
140
+ case 'created': return ` ${icon} ${green(path)}`;
141
+ case 'overwritten': return ` ${icon} ${cyan(path)} ${dim('(updated)')}`;
142
+ case 'skipped': return ` ${icon} ${dim(`${path} (${detail || 'exists, skipped'})`)}`;
143
+ case 'failed': return ` ${icon} ${red(path)} ${dim(`\u2014 ${detail}`)}`;
144
+ case 'unchanged': return ` ${icon} ${dim(`${path} (up to date)`)}`;
145
+ default: return ` ${icon} ${path}`;
146
+ }
147
+ }
148
+
149
+ function stripAnsi(str) {
150
+ return str.replace(/\x1b\[\d+m/g, '');
64
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serpentstack",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for SerpentStack — AI-driven development standards with project-specific skills and persistent agents",
5
5
  "type": "module",
6
6
  "bin": {