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.
- package/bin/serpentstack.js +20 -12
- package/lib/commands/skills-init.js +58 -28
- package/lib/commands/skills-update.js +14 -10
- package/lib/commands/stack-new.js +29 -8
- package/lib/commands/stack-update.js +11 -7
- package/lib/utils/ui.js +90 -3
- package/package.json +1 -1
package/bin/serpentstack.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
44
|
+
logs.push(fileStatus(repoPath, 'failed', err.message));
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
} finally {
|
|
47
48
|
spin.stop();
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
62
|
+
logs.push(fileStatus(repoPath, status === 'created' ? 'created' : 'overwritten'));
|
|
62
63
|
results.updated++;
|
|
63
64
|
} catch (err) {
|
|
64
65
|
results.failed++;
|
|
65
|
-
logs.push(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
if (results.
|
|
78
|
-
if (results.
|
|
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 ${
|
|
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
|
-
//
|
|
74
|
+
// What was created
|
|
73
75
|
console.log();
|
|
74
|
-
|
|
75
|
-
console.log(`
|
|
76
|
-
console.log(`
|
|
77
|
-
console.log(`
|
|
78
|
-
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/`);
|
|
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(
|
|
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(
|
|
47
|
+
logs.push(fileStatus(repoPath, status === 'created' ? 'created' : 'overwritten'));
|
|
47
48
|
results.updated++;
|
|
48
49
|
} catch (err) {
|
|
49
50
|
results.failed++;
|
|
50
|
-
logs.push(
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
if (results.
|
|
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}
|
|
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}
|
|
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}${
|
|
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
|
}
|