serpentstack 0.2.5 → 0.2.9

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/utils/ui.js CHANGED
@@ -17,10 +17,17 @@ const MAGENTA = c(35);
17
17
  const CYAN = c(36);
18
18
  const BG_DIM = c(100);
19
19
 
20
- export const info = (msg) => console.log(`${CYAN}\u2022${RESET} ${msg}`);
21
- export const success = (msg) => console.log(`${GREEN}\u2713${RESET} ${msg}`);
22
- export const warn = (msg) => console.log(`${YELLOW}\u25B3${RESET} ${msg}`);
23
- export const error = (msg) => console.error(`${RED}\u2717${RESET} ${msg}`);
20
+ // ─── Brand ───────────────────────────────────────────────────
21
+
22
+ const SNAKE = '🐍';
23
+ const BRAND_COLOR = GREEN;
24
+
25
+ // ─── Text formatters ─────────────────────────────────────────
26
+
27
+ export const info = (msg) => console.log(` ${CYAN}•${RESET} ${msg}`);
28
+ export const success = (msg) => console.log(` ${GREEN}✓${RESET} ${msg}`);
29
+ export const warn = (msg) => console.log(` ${YELLOW}△${RESET} ${msg}`);
30
+ export const error = (msg) => console.error(` ${RED}✗${RESET} ${msg}`);
24
31
  export const dim = (msg) => `${DIM}${msg}${RESET}`;
25
32
  export const bold = (msg) => `${BOLD}${msg}${RESET}`;
26
33
  export const green = (msg) => `${GREEN}${msg}${RESET}`;
@@ -30,18 +37,22 @@ export const cyan = (msg) => `${CYAN}${msg}${RESET}`;
30
37
  export const magenta = (msg) => `${MAGENTA}${msg}${RESET}`;
31
38
  export const blue = (msg) => `${BLUE}${msg}${RESET}`;
32
39
 
33
- const SPINNER_FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
40
+ // ─── Spinner ─────────────────────────────────────────────────
41
+
42
+ const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
34
43
 
35
44
  export function spinner(msg) {
36
45
  if (NO_COLOR || !stderr.isTTY) {
37
46
  stderr.write(` ${msg}\n`);
38
- return { stop(final) { if (final) stderr.write(` ${final}\n`); } };
47
+ return { stop(final) { if (final) stderr.write(` ${final}\n`); }, update() {} };
39
48
  }
40
49
  let i = 0;
50
+ let currentMsg = msg;
41
51
  const id = setInterval(() => {
42
- stderr.write(`\r${CYAN}${SPINNER_FRAMES[i++ % SPINNER_FRAMES.length]}${RESET} ${msg}`);
43
- }, 80);
52
+ stderr.write(`\r ${GREEN}${SPINNER_FRAMES[i++ % SPINNER_FRAMES.length]}${RESET} ${currentMsg}`);
53
+ }, 100);
44
54
  return {
55
+ update(newMsg) { currentMsg = newMsg; },
45
56
  stop(final) {
46
57
  clearInterval(id);
47
58
  stderr.write(`\r\x1b[K`);
@@ -50,16 +61,20 @@ export function spinner(msg) {
50
61
  };
51
62
  }
52
63
 
64
+ // ─── Confirm ─────────────────────────────────────────────────
65
+
53
66
  export async function confirm(msg) {
54
67
  const rl = createInterface({ input: stdin, output: stdout });
55
68
  try {
56
- const answer = await rl.question(`${YELLOW}?${RESET} ${msg} ${dim('(y/N)')} `);
69
+ const answer = await rl.question(`${YELLOW}?${RESET} ${msg} ${DIM}(y/N)${RESET} `);
57
70
  return answer.trim().toLowerCase() === 'y';
58
71
  } finally {
59
72
  rl.close();
60
73
  }
61
74
  }
62
75
 
76
+ // ─── Version ─────────────────────────────────────────────────
77
+
63
78
  export function getVersion() {
64
79
  try {
65
80
  const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
@@ -69,36 +84,44 @@ export function getVersion() {
69
84
  }
70
85
  }
71
86
 
87
+ // ─── Headers & Dividers ──────────────────────────────────────
88
+
72
89
  export function printHeader() {
73
- console.log(`\n ${BOLD}${GREEN}\u2728 SerpentStack${RESET} ${DIM}v${getVersion()}${RESET}\n`);
90
+ console.log();
91
+ console.log(` ${SNAKE} ${BOLD}${GREEN}SerpentStack${RESET} ${DIM}v${getVersion()}${RESET}`);
92
+ console.log();
74
93
  }
75
94
 
95
+ export function divider(label) {
96
+ if (label) {
97
+ console.log(` ${DIM}── ${RESET}${BOLD}${label}${RESET} ${DIM}${'─'.repeat(Math.max(0, 50 - stripAnsi(label).length))}${RESET}`);
98
+ } else {
99
+ console.log(` ${DIM}${'─'.repeat(54)}${RESET}`);
100
+ }
101
+ }
102
+
103
+ // ─── Boxes ───────────────────────────────────────────────────
104
+
76
105
  /**
77
106
  * Print a boxed section with a title and content lines.
78
- * Used for "Next steps" and prompt blocks.
79
107
  */
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
- );
108
+ export function printBox(title, lines, { color = GREEN, icon = '' } = {}) {
109
+ const allText = [title, ...lines];
110
+ const maxLen = Math.max(...allText.map(l => stripAnsi(l).length + 4));
85
111
  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
112
 
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}`);
113
+ console.log(` ${DIM}┌${'─'.repeat(width)}┐${RESET}`);
114
+ console.log(` ${DIM}│${RESET} ${BOLD}${icon} ${title}${RESET}${' '.repeat(Math.max(0, width - stripAnsi(title).length - 3))}${DIM}│${RESET}`);
115
+ console.log(` ${DIM}│${' '.repeat(width)}│${RESET}`);
92
116
  for (const line of lines) {
93
117
  const pad = Math.max(0, width - stripAnsi(line).length - 2);
94
- console.log(`${color}\u2502${RESET} ${line}${' '.repeat(pad)}${color}\u2502${RESET}`);
118
+ console.log(` ${DIM}│${RESET} ${line}${' '.repeat(pad)}${DIM}│${RESET}`);
95
119
  }
96
- console.log(bot);
120
+ console.log(` ${DIM}└${'─'.repeat(width)}┘${RESET}`);
97
121
  }
98
122
 
99
123
  /**
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.
124
+ * Print a copyable prompt block.
102
125
  */
103
126
  export function printPrompt(promptLines, { hint = 'Copy this prompt and paste it into your IDE agent' } = {}) {
104
127
  const lines = Array.isArray(promptLines) ? promptLines : [promptLines];
@@ -107,45 +130,43 @@ export function printPrompt(promptLines, { hint = 'Copy this prompt and paste it
107
130
  const innerWidth = width - 2;
108
131
 
109
132
  console.log();
110
- console.log(` ${DIM}\u250C${'─'.repeat(innerWidth)}\u2510${RESET}`);
133
+ console.log(` ${DIM}┌${'─'.repeat(innerWidth)}┐${RESET}`);
111
134
  for (const line of lines) {
112
135
  const pad = Math.max(0, innerWidth - stripAnsi(line).length - 2);
113
- console.log(` ${DIM}\u2502${RESET} ${CYAN}${line}${RESET}${' '.repeat(pad)}${DIM}\u2502${RESET}`);
136
+ console.log(` ${DIM}│${RESET} ${CYAN}${line}${RESET}${' '.repeat(pad)}${DIM}│${RESET}`);
114
137
  }
115
- console.log(` ${DIM}\u2514${'─'.repeat(innerWidth)}\u2518${RESET}`);
116
- console.log(` ${DIM}\u2191 ${hint}${RESET}`);
138
+ console.log(` ${DIM}└${'─'.repeat(innerWidth)}┘${RESET}`);
139
+ console.log(` ${DIM} ${hint}${RESET}`);
117
140
  console.log();
118
141
  }
119
142
 
120
- /**
121
- * File status icon with color.
122
- */
143
+ // ─── File Status ─────────────────────────────────────────────
144
+
123
145
  export function fileIcon(status) {
124
146
  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}`;
147
+ case 'created': return `${GREEN}✓${RESET}`;
148
+ case 'overwritten': return `${CYAN}↻${RESET}`;
149
+ case 'skipped': return `${YELLOW}•${RESET}`;
150
+ case 'failed': return `${RED}✗${RESET}`;
151
+ case 'unchanged': return `${DIM}•${RESET}`;
152
+ default: return `${DIM}•${RESET}`;
131
153
  }
132
154
  }
133
155
 
134
- /**
135
- * Format a file path with its status for display.
136
- */
137
156
  export function fileStatus(path, status, detail) {
138
157
  const icon = fileIcon(status);
139
158
  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}`;
159
+ case 'created': return ` ${icon} ${path}`;
160
+ case 'overwritten': return ` ${icon} ${cyan(path)} ${dim('(updated)')}`;
161
+ case 'skipped': return ` ${icon} ${dim(`${path} (${detail || 'exists, skipped'})`)}`;
162
+ case 'failed': return ` ${icon} ${red(path)} ${dim(`— ${detail}`)}`;
163
+ case 'unchanged': return ` ${icon} ${dim(`${path} (up to date)`)}`;
164
+ default: return ` ${icon} ${path}`;
146
165
  }
147
166
  }
148
167
 
149
- function stripAnsi(str) {
150
- return str.replace(/\x1b\[\d+m/g, '');
168
+ // ─── Utilities ───────────────────────────────────────────────
169
+
170
+ export function stripAnsi(str) {
171
+ return str.replace(/\x1b\[[\d;]*m/g, '');
151
172
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "serpentstack",
3
- "version": "0.2.5",
3
+ "version": "0.2.9",
4
4
  "description": "CLI for SerpentStack — AI-driven development standards with project-specific skills and persistent agents",
5
5
  "type": "module",
6
6
  "bin": {
7
- "serpentstack": "./bin/serpentstack.js"
7
+ "serpentstack": "bin/serpentstack.js"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=22"
@@ -27,7 +27,7 @@
27
27
  "license": "MIT",
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "https://github.com/Benja-Pauls/SerpentStack"
30
+ "url": "git+https://github.com/Benja-Pauls/SerpentStack.git"
31
31
  },
32
32
  "homepage": "https://github.com/Benja-Pauls/SerpentStack#readme",
33
33
  "author": "Ben Paulson"