slashdev 0.1.0 → 1.1.0
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/.gitmodules +3 -0
- package/CLAUDE.md +88 -0
- package/GETTING-STARTED.md +68 -0
- package/README.md +158 -21
- package/bin/check-setup.js +27 -0
- package/claude-skills/agentswarm/SKILL.md +479 -0
- package/claude-skills/bug-diagnosis/SKILL.md +34 -0
- package/claude-skills/code-review/SKILL.md +26 -0
- package/claude-skills/frontend-design/LICENSE.txt +177 -0
- package/claude-skills/frontend-design/SKILL.md +42 -0
- package/claude-skills/pr-description/SKILL.md +35 -0
- package/claude-skills/scope-estimate/SKILL.md +37 -0
- package/claude-skills/secure-deploy/SKILL.md +219 -0
- package/hooks/post-response.sh +242 -0
- package/hooks/pre-install-guard.sh +88 -0
- package/package.json +11 -3
- package/skills/front-end-design/prompts/system.md +37 -0
- package/skills/front-end-testing/prompts/system.md +66 -0
- package/skills/github-manager/prompts/system.md +79 -0
- package/skills/product-expert/prompts/system.md +52 -0
- package/skills/server-admin/prompts/system.md +39 -0
- package/src/auth/index.js +115 -0
- package/src/cli.js +188 -18
- package/src/commands/setup-internals.js +221 -0
- package/src/commands/setup.js +110 -0
- package/src/commands/update.js +64 -0
- package/src/connections/index.js +449 -0
- package/src/connections/providers/github.js +71 -0
- package/src/connections/providers/servers.js +175 -0
- package/src/connections/registry.js +21 -0
- package/src/core/claude.js +78 -0
- package/src/core/codebase.js +119 -0
- package/src/core/config.js +110 -0
- package/src/index.js +8 -1
- package/src/info.js +54 -21
- package/src/skills/index.js +252 -0
- package/src/utils/ssh-keys.js +67 -0
- package/vendor/gstack/.env.example +5 -0
- package/vendor/gstack/autoplan/SKILL.md +1116 -0
- package/vendor/gstack/browse/SKILL.md +538 -0
- package/vendor/gstack/canary/SKILL.md +587 -0
- package/vendor/gstack/careful/SKILL.md +59 -0
- package/vendor/gstack/codex/SKILL.md +862 -0
- package/vendor/gstack/connect-chrome/SKILL.md +549 -0
- package/vendor/gstack/cso/ACKNOWLEDGEMENTS.md +14 -0
- package/vendor/gstack/cso/SKILL.md +929 -0
- package/vendor/gstack/design-consultation/SKILL.md +962 -0
- package/vendor/gstack/design-review/SKILL.md +1314 -0
- package/vendor/gstack/design-shotgun/SKILL.md +730 -0
- package/vendor/gstack/document-release/SKILL.md +718 -0
- package/vendor/gstack/freeze/SKILL.md +82 -0
- package/vendor/gstack/gstack-upgrade/SKILL.md +232 -0
- package/vendor/gstack/guard/SKILL.md +82 -0
- package/vendor/gstack/investigate/SKILL.md +504 -0
- package/vendor/gstack/land-and-deploy/SKILL.md +1367 -0
- package/vendor/gstack/office-hours/SKILL.md +1317 -0
- package/vendor/gstack/plan-ceo-review/SKILL.md +1537 -0
- package/vendor/gstack/plan-design-review/SKILL.md +1227 -0
- package/vendor/gstack/plan-eng-review/SKILL.md +1120 -0
- package/vendor/gstack/qa/SKILL.md +1136 -0
- package/vendor/gstack/qa/references/issue-taxonomy.md +85 -0
- package/vendor/gstack/qa/templates/qa-report-template.md +126 -0
- package/vendor/gstack/qa-only/SKILL.md +726 -0
- package/vendor/gstack/retro/SKILL.md +1197 -0
- package/vendor/gstack/review/SKILL.md +1138 -0
- package/vendor/gstack/review/TODOS-format.md +62 -0
- package/vendor/gstack/review/checklist.md +220 -0
- package/vendor/gstack/review/design-checklist.md +132 -0
- package/vendor/gstack/review/greptile-triage.md +220 -0
- package/vendor/gstack/setup-browser-cookies/SKILL.md +348 -0
- package/vendor/gstack/setup-deploy/SKILL.md +528 -0
- package/vendor/gstack/ship/SKILL.md +1931 -0
- package/vendor/gstack/unfreeze/SKILL.md +40 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { setApiKey, setUser, getApiKey, getUser, clearApiKey, isAuthenticated } from '../core/config.js';
|
|
5
|
+
import { validateApiKey } from '../core/claude.js';
|
|
6
|
+
import { blue } from '../banner.js';
|
|
7
|
+
|
|
8
|
+
export async function login() {
|
|
9
|
+
console.log();
|
|
10
|
+
|
|
11
|
+
if (isAuthenticated()) {
|
|
12
|
+
const user = getUser();
|
|
13
|
+
console.log(chalk.hex('#4d7fff')(` You're already logged in${user ? ` as ${chalk.bold(user)}` : ''}.`));
|
|
14
|
+
console.log(chalk.dim(` Run ${chalk.hex('#215ff6')('slashdev logout')} to sign out first.`));
|
|
15
|
+
console.log();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.hex('#215ff6').bold(' Slashdev Login'));
|
|
20
|
+
console.log(chalk.hex('#4d7fff')(' ─────────────────────────────────────────'));
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.dim(' To use Slashdev skills, you need an Anthropic API key.'));
|
|
23
|
+
console.log(chalk.dim(' Get one at: ') + chalk.hex('#215ff6').underline('https://console.anthropic.com/settings/keys'));
|
|
24
|
+
console.log();
|
|
25
|
+
|
|
26
|
+
const answers = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'password',
|
|
29
|
+
name: 'apiKey',
|
|
30
|
+
message: chalk.hex('#7a9fff')('Enter your Anthropic API key:'),
|
|
31
|
+
mask: '*',
|
|
32
|
+
validate: (input) => {
|
|
33
|
+
if (!input || input.trim().length === 0) {
|
|
34
|
+
return 'API key is required';
|
|
35
|
+
}
|
|
36
|
+
if (!input.startsWith('sk-ant-')) {
|
|
37
|
+
return 'Invalid API key format. Should start with sk-ant-';
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'name',
|
|
45
|
+
message: chalk.hex('#7a9fff')('What should we call you? (optional):'),
|
|
46
|
+
default: '',
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const spinner = ora({
|
|
51
|
+
text: chalk.hex('#4d7fff')('Validating API key...'),
|
|
52
|
+
color: 'blue',
|
|
53
|
+
}).start();
|
|
54
|
+
|
|
55
|
+
const isValid = await validateApiKey(answers.apiKey.trim());
|
|
56
|
+
|
|
57
|
+
if (!isValid) {
|
|
58
|
+
spinner.fail(chalk.red('Invalid API key. Please check and try again.'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setApiKey(answers.apiKey.trim());
|
|
63
|
+
if (answers.name) {
|
|
64
|
+
setUser(answers.name.trim());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
spinner.succeed(chalk.hex('#4d7fff')('API key validated and saved!'));
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(chalk.hex('#215ff6').bold(` Welcome to Slashdev${answers.name ? `, ${answers.name}` : ''}!`));
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.dim(' Try running a skill:'));
|
|
72
|
+
console.log(` ${chalk.hex('#4d7fff')('slashdev design')} ${chalk.dim('"review my Button component"')}`);
|
|
73
|
+
console.log(` ${chalk.hex('#4d7fff')('slashdev product')} ${chalk.dim('"write a PRD for user auth"')}`);
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function logout() {
|
|
78
|
+
console.log();
|
|
79
|
+
|
|
80
|
+
if (!isAuthenticated()) {
|
|
81
|
+
console.log(chalk.hex('#7a9fff')(' You\'re not logged in.'));
|
|
82
|
+
console.log();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const user = getUser();
|
|
87
|
+
clearApiKey();
|
|
88
|
+
|
|
89
|
+
console.log(chalk.hex('#4d7fff')(` Logged out${user ? ` ${user}` : ''} successfully.`));
|
|
90
|
+
console.log(chalk.dim(` Run ${chalk.hex('#215ff6')('slashdev login')} to sign in again.`));
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function whoami() {
|
|
95
|
+
console.log();
|
|
96
|
+
|
|
97
|
+
if (!isAuthenticated()) {
|
|
98
|
+
console.log(chalk.hex('#7a9fff')(' Not logged in.'));
|
|
99
|
+
console.log(chalk.dim(` Run ${chalk.hex('#215ff6')('slashdev login')} to authenticate.`));
|
|
100
|
+
console.log();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const user = getUser();
|
|
105
|
+
const apiKey = getApiKey();
|
|
106
|
+
const maskedKey = apiKey ? `${apiKey.slice(0, 10)}...${apiKey.slice(-4)}` : 'unknown';
|
|
107
|
+
|
|
108
|
+
console.log(chalk.hex('#215ff6').bold(' Slashdev Account'));
|
|
109
|
+
console.log(chalk.hex('#4d7fff')(' ─────────────────────────────────────────'));
|
|
110
|
+
console.log();
|
|
111
|
+
console.log(` ${chalk.hex('#7a9fff')('User:')} ${user || chalk.dim('(not set)')}`);
|
|
112
|
+
console.log(` ${chalk.hex('#7a9fff')('API Key:')} ${chalk.dim(maskedKey)}`);
|
|
113
|
+
console.log(` ${chalk.hex('#7a9fff')('Status:')} ${chalk.hex('#4d7fff')('● Authenticated')}`);
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,23 +1,193 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import { displayBanner } from './banner.js';
|
|
2
3
|
import { displayInfo, displayVersion, displayHelp } from './info.js';
|
|
4
|
+
import { login, logout, whoami } from './auth/index.js';
|
|
5
|
+
import { listSkills, runSkill, getSkillByCommand } from './skills/index.js';
|
|
6
|
+
import { connect, disconnect, listConnections } from './connections/index.js';
|
|
7
|
+
import { setup } from './commands/setup.js';
|
|
8
|
+
import { update } from './commands/update.js';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name('slashdev')
|
|
21
|
+
.description('Skills & workflows for AI agents in the Slashdev ecosystem')
|
|
22
|
+
.version(pkg.version, '-v, --version', 'Show version number');
|
|
23
|
+
|
|
24
|
+
// Claude Code integration commands
|
|
25
|
+
program
|
|
26
|
+
.command('setup')
|
|
27
|
+
.description('Install Claude Code hooks and skills for your Slashdev account')
|
|
28
|
+
.action(async () => {
|
|
29
|
+
displayBanner();
|
|
30
|
+
await setup();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('update')
|
|
35
|
+
.description('Update Claude Code hooks and skills to the latest version')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
displayBanner();
|
|
38
|
+
await update();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Default action (no command) - show banner
|
|
42
|
+
program
|
|
43
|
+
.action(async () => {
|
|
44
|
+
displayBanner();
|
|
45
|
+
displayInfo();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Authentication commands
|
|
49
|
+
program
|
|
50
|
+
.command('login')
|
|
51
|
+
.description('Authenticate with your Anthropic API key')
|
|
52
|
+
.action(async () => {
|
|
53
|
+
displayBanner();
|
|
54
|
+
await login();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('logout')
|
|
59
|
+
.description('Clear stored credentials')
|
|
60
|
+
.action(async () => {
|
|
61
|
+
await logout();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command('whoami')
|
|
66
|
+
.description('Show current authentication status')
|
|
67
|
+
.action(() => {
|
|
68
|
+
whoami();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Connection commands
|
|
72
|
+
program
|
|
73
|
+
.command('connect [service]')
|
|
74
|
+
.description('Connect an external service (e.g. github)')
|
|
75
|
+
.action(async (service) => {
|
|
76
|
+
displayBanner();
|
|
77
|
+
await connect(service);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
program
|
|
81
|
+
.command('disconnect [service]')
|
|
82
|
+
.description('Remove a connected service')
|
|
83
|
+
.action(async (service) => {
|
|
84
|
+
await disconnect(service);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command('connections')
|
|
89
|
+
.description('List all connections and their status')
|
|
90
|
+
.action(() => {
|
|
91
|
+
displayBanner();
|
|
92
|
+
listConnections();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Skills command
|
|
96
|
+
program
|
|
97
|
+
.command('skills')
|
|
98
|
+
.description('List installed skills')
|
|
99
|
+
.action(() => {
|
|
100
|
+
displayBanner();
|
|
101
|
+
listSkills();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Design skill
|
|
105
|
+
program
|
|
106
|
+
.command('design [prompt...]')
|
|
107
|
+
.description('Front-end design assistance')
|
|
108
|
+
.action(async (promptParts) => {
|
|
109
|
+
const prompt = promptParts.join(' ');
|
|
110
|
+
if (!prompt) {
|
|
111
|
+
console.log('\n Usage: slashdev design "your design question"\n');
|
|
112
|
+
console.log(' Examples:');
|
|
113
|
+
console.log(' slashdev design "review my Button component for accessibility"');
|
|
114
|
+
console.log(' slashdev design "create a dark mode color palette"');
|
|
115
|
+
console.log(' slashdev design "improve the mobile layout"\n');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await runSkill('front-end-design', prompt);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Product skill
|
|
122
|
+
program
|
|
123
|
+
.command('product [prompt...]')
|
|
124
|
+
.description('Product management assistance')
|
|
125
|
+
.action(async (promptParts) => {
|
|
126
|
+
const prompt = promptParts.join(' ');
|
|
127
|
+
if (!prompt) {
|
|
128
|
+
console.log('\n Usage: slashdev product "your product question"\n');
|
|
129
|
+
console.log(' Examples:');
|
|
130
|
+
console.log(' slashdev product "write a PRD for user authentication"');
|
|
131
|
+
console.log(' slashdev product "create user stories for checkout flow"');
|
|
132
|
+
console.log(' slashdev product "draft release notes for v2.0"\n');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await runSkill('product-expert', prompt);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// GitHub skill
|
|
139
|
+
program
|
|
140
|
+
.command('github [prompt...]')
|
|
141
|
+
.alias('gh')
|
|
142
|
+
.description('GitHub workflow assistance')
|
|
143
|
+
.action(async (promptParts) => {
|
|
144
|
+
const prompt = promptParts.join(' ');
|
|
145
|
+
if (!prompt) {
|
|
146
|
+
console.log('\n Usage: slashdev github "your github question"\n');
|
|
147
|
+
console.log(' Examples:');
|
|
148
|
+
console.log(' slashdev github "write a PR description for adding auth"');
|
|
149
|
+
console.log(' slashdev github "create release notes for v1.2.0"');
|
|
150
|
+
console.log(' slashdev github "structure an issue for a login bug"\n');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await runSkill('github-manager', prompt);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Test skill
|
|
157
|
+
program
|
|
158
|
+
.command('test [prompt...]')
|
|
159
|
+
.description('Front-end testing assistance')
|
|
160
|
+
.action(async (promptParts) => {
|
|
161
|
+
const prompt = promptParts.join(' ');
|
|
162
|
+
if (!prompt) {
|
|
163
|
+
console.log('\n Usage: slashdev test "your testing question"\n');
|
|
164
|
+
console.log(' Examples:');
|
|
165
|
+
console.log(' slashdev test "write unit tests for the Button component"');
|
|
166
|
+
console.log(' slashdev test "create E2E tests for user login flow"');
|
|
167
|
+
console.log(' slashdev test "improve test coverage for the utils"\n');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await runSkill('front-end-testing', prompt);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Server admin skill
|
|
174
|
+
program
|
|
175
|
+
.command('server [prompt...]')
|
|
176
|
+
.alias('ssh')
|
|
177
|
+
.description('Server administration assistance')
|
|
178
|
+
.action(async (promptParts) => {
|
|
179
|
+
const prompt = promptParts.join(' ');
|
|
180
|
+
if (!prompt) {
|
|
181
|
+
console.log('\n Usage: slashdev server "your server question"\n');
|
|
182
|
+
console.log(' Examples:');
|
|
183
|
+
console.log(' slashdev server "harden SSH config on prod-web"');
|
|
184
|
+
console.log(' slashdev server "set up nginx reverse proxy"');
|
|
185
|
+
console.log(' slashdev server "check disk usage and clean up"\n');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await runSkill('server-admin', prompt);
|
|
189
|
+
});
|
|
3
190
|
|
|
4
191
|
export async function run(args) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// Handle --version / -v
|
|
8
|
-
if (flags.has('--version') || flags.has('-v')) {
|
|
9
|
-
displayVersion();
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Handle --help / -h
|
|
14
|
-
if (flags.has('--help') || flags.has('-h')) {
|
|
15
|
-
await displayBanner();
|
|
16
|
-
displayHelp();
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Default: show banner + info
|
|
21
|
-
await displayBanner();
|
|
22
|
-
displayInfo();
|
|
192
|
+
await program.parseAsync(['node', 'slashdev', ...args]);
|
|
23
193
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Shared install functions used by both setup and update commands
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
chmodSync,
|
|
13
|
+
} from 'fs';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
18
|
+
|
|
19
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
20
|
+
const CLAUDE_HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
21
|
+
const CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, 'skills');
|
|
22
|
+
const CLAUDE_SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
|
|
23
|
+
const CLAUDE_MEMORY_FILE = join(CLAUDE_DIR, 'CLAUDE.md');
|
|
24
|
+
|
|
25
|
+
const SLASHDEV_SECTION_START = '<!-- SLASHDEV:RULES:START -->';
|
|
26
|
+
const SLASHDEV_SECTION_END = '<!-- SLASHDEV:RULES:END -->';
|
|
27
|
+
|
|
28
|
+
const SLASHDEV_RULES_BLOCK = `${SLASHDEV_SECTION_START}
|
|
29
|
+
# Slashdev workspace rules
|
|
30
|
+
|
|
31
|
+
## Security: always use Socket Firewall for dependency installs
|
|
32
|
+
|
|
33
|
+
Claude must use Socket Firewall (\`sfw\`) for **every** dependency install command.
|
|
34
|
+
This applies to:
|
|
35
|
+
|
|
36
|
+
- **Local development** — never run \`npm install\`, \`pnpm install\`, \`yarn add\`,
|
|
37
|
+
\`bun install\`, \`composer install\`, \`pip install\`, or any equivalent without
|
|
38
|
+
prefixing \`sfw \`. A PreToolUse hook will block bare install commands and tell
|
|
39
|
+
you to retry with \`sfw\`.
|
|
40
|
+
- **Production deployment** — when generating or editing CI/CD config (GitHub Actions,
|
|
41
|
+
GitLab CI, CircleCI, Jenkins), Dockerfiles, deploy scripts (\`deploy.sh\`, \`Procfile\`),
|
|
42
|
+
or \`package.json\` scripts that install dependencies, **always** wrap the install
|
|
43
|
+
step with \`sfw\`. Examples:
|
|
44
|
+
- \`sfw npm ci\` instead of \`npm ci\`
|
|
45
|
+
- \`sfw composer install --no-dev\` instead of \`composer install --no-dev\`
|
|
46
|
+
- \`RUN npm install -g @socketsecurity/cli && sfw npm ci\` in a Dockerfile
|
|
47
|
+
- If \`sfw\` is not yet installed in a CI environment, install it first:
|
|
48
|
+
\`npm install -g @socketsecurity/cli\`. Document the install step inline in the workflow.
|
|
49
|
+
- Use the \`/secure-deploy\` skill to retrofit existing CI/CD configs.
|
|
50
|
+
|
|
51
|
+
Reason: Socket Firewall scans every package against Socket's vulnerability/malware
|
|
52
|
+
database before letting it install. This protects against supply-chain attacks
|
|
53
|
+
(compromised packages, typosquats, dependency confusion).
|
|
54
|
+
${SLASHDEV_SECTION_END}`;
|
|
55
|
+
|
|
56
|
+
function ensureDir(dir) {
|
|
57
|
+
if (!existsSync(dir)) {
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function copyDirRecursive(src, dest) {
|
|
63
|
+
ensureDir(dest);
|
|
64
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const srcPath = join(src, entry.name);
|
|
67
|
+
const destPath = join(dest, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
copyDirRecursive(srcPath, destPath);
|
|
70
|
+
} else {
|
|
71
|
+
copyFileSync(srcPath, destPath);
|
|
72
|
+
if (entry.name.endsWith('.sh')) {
|
|
73
|
+
chmodSync(destPath, 0o755);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function installHooks() {
|
|
80
|
+
const hooksSource = join(PACKAGE_ROOT, 'hooks');
|
|
81
|
+
if (!existsSync(hooksSource)) {
|
|
82
|
+
return { installed: 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ensureDir(CLAUDE_HOOKS_DIR);
|
|
86
|
+
copyDirRecursive(hooksSource, CLAUDE_HOOKS_DIR);
|
|
87
|
+
|
|
88
|
+
// Register hooks in Claude Code settings.json
|
|
89
|
+
let settings = {};
|
|
90
|
+
if (existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
91
|
+
try {
|
|
92
|
+
settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
93
|
+
} catch {
|
|
94
|
+
settings = {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!settings.hooks) {
|
|
99
|
+
settings.hooks = {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hookCommand = join(CLAUDE_HOOKS_DIR, 'post-response.sh');
|
|
103
|
+
const stopHooks = settings.hooks.Stop || [];
|
|
104
|
+
|
|
105
|
+
const alreadyRegistered = stopHooks.some(
|
|
106
|
+
(h) => h.hooks && h.hooks.some((inner) => inner.command && inner.command.includes('post-response.sh'))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!alreadyRegistered) {
|
|
110
|
+
stopHooks.push({
|
|
111
|
+
matcher: '',
|
|
112
|
+
hooks: [
|
|
113
|
+
{
|
|
114
|
+
type: 'command',
|
|
115
|
+
command: hookCommand,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
settings.hooks.Stop = stopHooks;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Register PreToolUse hook for the install guard (Bash tool only).
|
|
123
|
+
const guardCommand = join(CLAUDE_HOOKS_DIR, 'pre-install-guard.sh');
|
|
124
|
+
if (existsSync(guardCommand)) {
|
|
125
|
+
const preToolUseHooks = settings.hooks.PreToolUse || [];
|
|
126
|
+
const guardRegistered = preToolUseHooks.some(
|
|
127
|
+
(entry) =>
|
|
128
|
+
entry.matcher === 'Bash' &&
|
|
129
|
+
Array.isArray(entry.hooks) &&
|
|
130
|
+
entry.hooks.some((h) => h.command && h.command.includes('pre-install-guard.sh'))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (!guardRegistered) {
|
|
134
|
+
preToolUseHooks.push({
|
|
135
|
+
matcher: 'Bash',
|
|
136
|
+
hooks: [
|
|
137
|
+
{
|
|
138
|
+
type: 'command',
|
|
139
|
+
command: guardCommand,
|
|
140
|
+
statusMessage: 'Checking install safety…',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
settings.hooks.PreToolUse = preToolUseHooks;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
|
|
149
|
+
|
|
150
|
+
const hookFiles = readdirSync(hooksSource).filter((f) => f.endsWith('.sh'));
|
|
151
|
+
return { installed: hookFiles.length };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writeGlobalClaudeMd() {
|
|
155
|
+
ensureDir(CLAUDE_DIR);
|
|
156
|
+
|
|
157
|
+
let existing = '';
|
|
158
|
+
if (existsSync(CLAUDE_MEMORY_FILE)) {
|
|
159
|
+
existing = readFileSync(CLAUDE_MEMORY_FILE, 'utf-8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let next;
|
|
163
|
+
if (existing.includes(SLASHDEV_SECTION_START) && existing.includes(SLASHDEV_SECTION_END)) {
|
|
164
|
+
// Replace the existing managed section in place, preserving everything around it.
|
|
165
|
+
const before = existing.split(SLASHDEV_SECTION_START)[0];
|
|
166
|
+
const after = existing.split(SLASHDEV_SECTION_END)[1];
|
|
167
|
+
next = `${before}${SLASHDEV_RULES_BLOCK}${after}`;
|
|
168
|
+
} else if (existing.trim().length > 0) {
|
|
169
|
+
// Append after existing user content.
|
|
170
|
+
next = `${existing.trimEnd()}\n\n${SLASHDEV_RULES_BLOCK}\n`;
|
|
171
|
+
} else {
|
|
172
|
+
// Brand new file.
|
|
173
|
+
next = `${SLASHDEV_RULES_BLOCK}\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
writeFileSync(CLAUDE_MEMORY_FILE, next, 'utf-8');
|
|
177
|
+
return { written: true, path: CLAUDE_MEMORY_FILE };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function installSkills() {
|
|
181
|
+
const skillsSource = join(PACKAGE_ROOT, 'claude-skills');
|
|
182
|
+
if (!existsSync(skillsSource)) {
|
|
183
|
+
return { installed: 0 };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ensureDir(CLAUDE_SKILLS_DIR);
|
|
187
|
+
|
|
188
|
+
const skillDirs = readdirSync(skillsSource, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
189
|
+
|
|
190
|
+
for (const dir of skillDirs) {
|
|
191
|
+
const srcPath = join(skillsSource, dir.name);
|
|
192
|
+
const destPath = join(CLAUDE_SKILLS_DIR, dir.name);
|
|
193
|
+
copyDirRecursive(srcPath, destPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { installed: skillDirs.length };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function installGstack() {
|
|
200
|
+
const gstackSource = join(PACKAGE_ROOT, 'vendor', 'gstack');
|
|
201
|
+
if (!existsSync(gstackSource)) {
|
|
202
|
+
return { installed: 0, warning: 'gstack submodule not initialized. Run: git submodule update --init vendor/gstack' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const gstackDest = join(CLAUDE_SKILLS_DIR, 'gstack');
|
|
206
|
+
ensureDir(gstackDest);
|
|
207
|
+
|
|
208
|
+
const entries = readdirSync(gstackSource, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
209
|
+
let installed = 0;
|
|
210
|
+
|
|
211
|
+
for (const dir of entries) {
|
|
212
|
+
const skillMd = join(gstackSource, dir.name, 'SKILL.md');
|
|
213
|
+
if (existsSync(skillMd)) {
|
|
214
|
+
const destPath = join(gstackDest, dir.name);
|
|
215
|
+
copyDirRecursive(join(gstackSource, dir.name), destPath);
|
|
216
|
+
installed++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { installed };
|
|
221
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { getConfig, setConfig, getSlashdevDir } from '../core/config.js';
|
|
9
|
+
import { installHooks, installSkills, installGstack, writeGlobalClaudeMd } from './setup-internals.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
14
|
+
|
|
15
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
16
|
+
|
|
17
|
+
const BRAND = chalk.hex('#215ff6');
|
|
18
|
+
|
|
19
|
+
function validateEmail(email) {
|
|
20
|
+
if (!email || !email.endsWith('@slashdev.io')) {
|
|
21
|
+
return 'Email must be a valid @slashdev.io address';
|
|
22
|
+
}
|
|
23
|
+
if (!/^[^\s@]+@slashdev\.io$/.test(email)) {
|
|
24
|
+
return 'Invalid email format';
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function setup() {
|
|
30
|
+
console.log(BRAND('\n Slashdev Claude Tools Setup\n'));
|
|
31
|
+
|
|
32
|
+
// Check if already configured
|
|
33
|
+
const existingEngineer = getConfig('engineer');
|
|
34
|
+
if (existingEngineer) {
|
|
35
|
+
const { overwrite } = await inquirer.prompt([
|
|
36
|
+
{
|
|
37
|
+
type: 'confirm',
|
|
38
|
+
name: 'overwrite',
|
|
39
|
+
message: `Already configured for ${chalk.cyan(existingEngineer)}. Overwrite?`,
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
if (!overwrite) {
|
|
44
|
+
console.log(chalk.yellow('\n Setup cancelled.\n'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Prompt for email
|
|
50
|
+
const { email } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'email',
|
|
54
|
+
message: 'Enter your Slashdev email (e.g. you@slashdev.io):',
|
|
55
|
+
validate: validateEmail,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const spinner = ora('Installing Slashdev Claude tools...').start();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Write engineer config
|
|
63
|
+
setConfig('engineer', email);
|
|
64
|
+
|
|
65
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
66
|
+
setConfig('packageVersion', pkg.version);
|
|
67
|
+
|
|
68
|
+
// Ensure Claude directory exists
|
|
69
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
70
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Install hooks
|
|
74
|
+
spinner.text = 'Installing hooks...';
|
|
75
|
+
const hookResult = installHooks();
|
|
76
|
+
|
|
77
|
+
// Install skills
|
|
78
|
+
spinner.text = 'Installing Claude Code skills...';
|
|
79
|
+
const skillResult = installSkills();
|
|
80
|
+
|
|
81
|
+
// Install gstack skills
|
|
82
|
+
spinner.text = 'Installing gstack skills...';
|
|
83
|
+
const gstackResult = installGstack();
|
|
84
|
+
|
|
85
|
+
// Write global Claude rules (Socket Firewall + workspace policy)
|
|
86
|
+
spinner.text = 'Writing global Claude rules...';
|
|
87
|
+
const rulesResult = writeGlobalClaudeMd();
|
|
88
|
+
|
|
89
|
+
spinner.succeed('Slashdev Claude tools installed');
|
|
90
|
+
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(` ${chalk.green('✓')} Engineer: ${chalk.cyan(email)}`);
|
|
93
|
+
console.log(` ${chalk.green('✓')} Hooks: ${hookResult.installed} installed`);
|
|
94
|
+
console.log(` ${chalk.green('✓')} Skills: ${skillResult.installed} installed`);
|
|
95
|
+
console.log(` ${chalk.green('✓')} Gstack: ${gstackResult.installed} skills installed`);
|
|
96
|
+
if (gstackResult.warning) {
|
|
97
|
+
console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
|
|
98
|
+
}
|
|
99
|
+
console.log(` ${chalk.green('✓')} Rules: ${rulesResult.path}`);
|
|
100
|
+
console.log(` ${chalk.green('✓')} Config: ${join(getSlashdevDir(), 'config.json')}`);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(chalk.dim(' Hooks, skills, and global rules have been installed to ~/.claude/'));
|
|
103
|
+
console.log(chalk.dim(' Socket Firewall enforcement is active for all Bash install commands.'));
|
|
104
|
+
console.log(chalk.dim(' Prompt logs will be sent to the Slashdev API for quality review.\n'));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
spinner.fail('Setup failed');
|
|
107
|
+
console.error(chalk.red(`\n Error: ${err.message}\n`));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|