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/bin/serpentstack.js +76 -29
- package/lib/commands/persistent.js +318 -286
- package/lib/commands/skills-init.js +61 -23
- package/lib/commands/skills-update.js +10 -8
- package/lib/commands/stack-new.js +56 -21
- package/lib/utils/agent-utils.js +1 -1
- package/lib/utils/config.js +14 -7
- package/lib/utils/fs-helpers.js +1 -1
- package/lib/utils/models.js +181 -0
- package/lib/utils/ui.js +70 -49
- package/package.json +3 -3
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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${
|
|
43
|
-
},
|
|
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} ${
|
|
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(
|
|
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 = '
|
|
81
|
-
const
|
|
82
|
-
|
|
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(
|
|
90
|
-
console.log(
|
|
91
|
-
console.log(
|
|
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(
|
|
118
|
+
console.log(` ${DIM}│${RESET} ${line}${' '.repeat(pad)}${DIM}│${RESET}`);
|
|
95
119
|
}
|
|
96
|
-
console.log(
|
|
120
|
+
console.log(` ${DIM}└${'─'.repeat(width)}┘${RESET}`);
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
/**
|
|
100
|
-
* Print a copyable prompt block
|
|
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}
|
|
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}
|
|
136
|
+
console.log(` ${DIM}│${RESET} ${CYAN}${line}${RESET}${' '.repeat(pad)}${DIM}│${RESET}`);
|
|
114
137
|
}
|
|
115
|
-
console.log(` ${DIM}
|
|
116
|
-
console.log(` ${DIM}
|
|
138
|
+
console.log(` ${DIM}└${'─'.repeat(innerWidth)}┘${RESET}`);
|
|
139
|
+
console.log(` ${DIM}↑ ${hint}${RESET}`);
|
|
117
140
|
console.log();
|
|
118
141
|
}
|
|
119
142
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*/
|
|
143
|
+
// ─── File Status ─────────────────────────────────────────────
|
|
144
|
+
|
|
123
145
|
export function fileIcon(status) {
|
|
124
146
|
switch (status) {
|
|
125
|
-
case 'created': return `${GREEN}
|
|
126
|
-
case 'overwritten': return `${CYAN}
|
|
127
|
-
case 'skipped': return `${YELLOW}
|
|
128
|
-
case 'failed': return `${RED}
|
|
129
|
-
case 'unchanged': return `${DIM}
|
|
130
|
-
default: return `${DIM}
|
|
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 `
|
|
141
|
-
case 'overwritten': return `
|
|
142
|
-
case 'skipped': return `
|
|
143
|
-
case 'failed': return `
|
|
144
|
-
case 'unchanged': return `
|
|
145
|
-
default: return `
|
|
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
|
-
|
|
150
|
-
|
|
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.
|
|
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": "
|
|
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"
|