slashdev 1.0.0 → 1.2.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/CLAUDE.md +5 -4
- package/GETTING-STARTED.md +68 -0
- package/claude-skills/salesforce-development/SKILL.md +341 -0
- package/claude-skills/salesforce-development/references/apex-patterns.md +388 -0
- package/claude-skills/salesforce-development/references/lwc-patterns.md +374 -0
- package/claude-skills/salesforce-development/references/testing-patterns.md +438 -0
- package/claude-skills/secure-deploy/SKILL.md +219 -0
- package/hooks/pre-install-guard.sh +88 -0
- package/package.json +1 -1
- package/src/commands/setup-internals.js +84 -0
- package/src/commands/setup.js +8 -2
- package/src/commands/update.js +5 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pre-install-guard.sh — PreToolUse hook for Bash tool calls.
|
|
3
|
+
#
|
|
4
|
+
# Blocks bare dependency install commands and forces them to be re-run with
|
|
5
|
+
# Socket Firewall (`sfw`) prefix to protect against supply-chain attacks.
|
|
6
|
+
#
|
|
7
|
+
# Reads tool-call JSON from stdin, returns:
|
|
8
|
+
# {} → allow
|
|
9
|
+
# {"permissionDecision":"deny","message":"..."} → block
|
|
10
|
+
#
|
|
11
|
+
# Patterns matched (anchored at the start of the actual command, after any
|
|
12
|
+
# leading `cd ... &&` prefix):
|
|
13
|
+
# - npm/pnpm/yarn/bun (install|i|ci|add|update|upgrade)
|
|
14
|
+
# - composer (install|require|update)
|
|
15
|
+
# - pip/pip3/uv (install|sync)
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
|
|
20
|
+
# Extract tool_input.command from the JSON. Try grep/sed first for speed,
|
|
21
|
+
# fall back to python3 for properly-escaped JSON.
|
|
22
|
+
COMMAND=$(printf '%s' "$INPUT" \
|
|
23
|
+
| grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
24
|
+
| head -1 \
|
|
25
|
+
| sed 's/.*:[[:space:]]*"//;s/"$//' \
|
|
26
|
+
|| true)
|
|
27
|
+
|
|
28
|
+
if [ -z "$COMMAND" ]; then
|
|
29
|
+
COMMAND=$(printf '%s' "$INPUT" \
|
|
30
|
+
| python3 -c 'import sys,json; print(json.loads(sys.stdin.read()).get("tool_input",{}).get("command",""))' \
|
|
31
|
+
2>/dev/null || true)
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# No command extracted → allow (don't block on parse failure).
|
|
35
|
+
if [ -z "$COMMAND" ]; then
|
|
36
|
+
echo '{}'
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Strip leading whitespace.
|
|
41
|
+
TRIMMED=$(printf '%s' "$COMMAND" | sed 's/^[[:space:]]*//')
|
|
42
|
+
|
|
43
|
+
# Strip a leading `cd <dir> && ` prefix (or `cd <dir>; `) so we examine the
|
|
44
|
+
# actual command being run, not the directory change.
|
|
45
|
+
ACTUAL_CMD="$TRIMMED"
|
|
46
|
+
while printf '%s' "$ACTUAL_CMD" | grep -qE '^cd[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*'; do
|
|
47
|
+
ACTUAL_CMD=$(printf '%s' "$ACTUAL_CMD" \
|
|
48
|
+
| sed -E 's/^cd[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*//')
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# Strip leading env-var assignments (e.g. `NODE_ENV=production npm ci`).
|
|
52
|
+
while printf '%s' "$ACTUAL_CMD" | grep -qE '^[A-Z_][A-Z0-9_]*=[^[:space:]]+[[:space:]]+'; do
|
|
53
|
+
ACTUAL_CMD=$(printf '%s' "$ACTUAL_CMD" \
|
|
54
|
+
| sed -E 's/^[A-Z_][A-Z0-9_]*=[^[:space:]]+[[:space:]]+//')
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
# Already wrapped with sfw → allow immediately.
|
|
58
|
+
if printf '%s' "$ACTUAL_CMD" | grep -qE '^sfw([[:space:]]|$)'; then
|
|
59
|
+
echo '{}'
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Detect install commands. Patterns are conservative — must be the first token,
|
|
64
|
+
# followed by an install-style subcommand.
|
|
65
|
+
IS_INSTALL=0
|
|
66
|
+
case "$ACTUAL_CMD" in
|
|
67
|
+
npm[[:space:]]install*|npm[[:space:]]i[[:space:]]*|"npm i"|npm[[:space:]]ci*|npm[[:space:]]add*|npm[[:space:]]update*|npm[[:space:]]upgrade*) IS_INSTALL=1 ;;
|
|
68
|
+
pnpm[[:space:]]install*|pnpm[[:space:]]i[[:space:]]*|"pnpm i"|pnpm[[:space:]]add*|pnpm[[:space:]]update*|pnpm[[:space:]]upgrade*) IS_INSTALL=1 ;;
|
|
69
|
+
yarn[[:space:]]install*|yarn[[:space:]]add*|yarn[[:space:]]upgrade*|"yarn") IS_INSTALL=1 ;;
|
|
70
|
+
bun[[:space:]]install*|bun[[:space:]]i[[:space:]]*|"bun i"|bun[[:space:]]add*|bun[[:space:]]update*) IS_INSTALL=1 ;;
|
|
71
|
+
composer[[:space:]]install*|composer[[:space:]]require*|composer[[:space:]]update*|composer[[:space:]]upgrade*) IS_INSTALL=1 ;;
|
|
72
|
+
pip[[:space:]]install*|pip3[[:space:]]install*|uv[[:space:]]install*|uv[[:space:]]sync*|uv[[:space:]]add*) IS_INSTALL=1 ;;
|
|
73
|
+
esac
|
|
74
|
+
|
|
75
|
+
if [ "$IS_INSTALL" -eq 0 ]; then
|
|
76
|
+
echo '{}'
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# It's an install command without sfw — block it.
|
|
81
|
+
# Escape the original command for safe inclusion in JSON.
|
|
82
|
+
ESCAPED_CMD=$(printf '%s' "$ACTUAL_CMD" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read())[1:-1])' 2>/dev/null || printf '%s' "$ACTUAL_CMD" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
83
|
+
|
|
84
|
+
if command -v sfw >/dev/null 2>&1; then
|
|
85
|
+
printf '{"permissionDecision":"deny","message":"[slashdev] Bare install command blocked. Re-run with the sfw prefix:\\n\\n sfw %s\\n\\nSocket Firewall scans every package against Socket'"'"'s vulnerability/malware database before installing it. This protects against supply-chain attacks."}\n' "$ESCAPED_CMD"
|
|
86
|
+
else
|
|
87
|
+
printf '{"permissionDecision":"deny","message":"[slashdev] Socket Firewall (sfw) is required for dependency installs but is not installed on this machine. Install it once:\\n\\n npm install -g @socketsecurity/cli\\n\\nThen retry the install with:\\n\\n sfw %s"}\n' "$ESCAPED_CMD"
|
|
88
|
+
fi
|
package/package.json
CHANGED
|
@@ -20,6 +20,38 @@ const CLAUDE_DIR = join(homedir(), '.claude');
|
|
|
20
20
|
const CLAUDE_HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
21
21
|
const CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, 'skills');
|
|
22
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}`;
|
|
23
55
|
|
|
24
56
|
function ensureDir(dir) {
|
|
25
57
|
if (!existsSync(dir)) {
|
|
@@ -87,12 +119,64 @@ export function installHooks() {
|
|
|
87
119
|
settings.hooks.Stop = stopHooks;
|
|
88
120
|
}
|
|
89
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
|
+
|
|
90
148
|
writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
|
|
91
149
|
|
|
92
150
|
const hookFiles = readdirSync(hooksSource).filter((f) => f.endsWith('.sh'));
|
|
93
151
|
return { installed: hookFiles.length };
|
|
94
152
|
}
|
|
95
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
|
+
|
|
96
180
|
export function installSkills() {
|
|
97
181
|
const skillsSource = join(PACKAGE_ROOT, 'claude-skills');
|
|
98
182
|
if (!existsSync(skillsSource)) {
|
package/src/commands/setup.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { getConfig, setConfig, getSlashdevDir } from '../core/config.js';
|
|
9
|
-
import { installHooks, installSkills, installGstack } from './setup-internals.js';
|
|
9
|
+
import { installHooks, installSkills, installGstack, writeGlobalClaudeMd } from './setup-internals.js';
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = dirname(__filename);
|
|
@@ -82,6 +82,10 @@ export async function setup() {
|
|
|
82
82
|
spinner.text = 'Installing gstack skills...';
|
|
83
83
|
const gstackResult = installGstack();
|
|
84
84
|
|
|
85
|
+
// Write global Claude rules (Socket Firewall + workspace policy)
|
|
86
|
+
spinner.text = 'Writing global Claude rules...';
|
|
87
|
+
const rulesResult = writeGlobalClaudeMd();
|
|
88
|
+
|
|
85
89
|
spinner.succeed('Slashdev Claude tools installed');
|
|
86
90
|
|
|
87
91
|
console.log('');
|
|
@@ -92,9 +96,11 @@ export async function setup() {
|
|
|
92
96
|
if (gstackResult.warning) {
|
|
93
97
|
console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
|
|
94
98
|
}
|
|
99
|
+
console.log(` ${chalk.green('✓')} Rules: ${rulesResult.path}`);
|
|
95
100
|
console.log(` ${chalk.green('✓')} Config: ${join(getSlashdevDir(), 'config.json')}`);
|
|
96
101
|
console.log('');
|
|
97
|
-
console.log(chalk.dim(' Hooks and
|
|
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.'));
|
|
98
104
|
console.log(chalk.dim(' Prompt logs will be sent to the Slashdev API for quality review.\n'));
|
|
99
105
|
} catch (err) {
|
|
100
106
|
spinner.fail('Setup failed');
|
package/src/commands/update.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function update() {
|
|
|
27
27
|
try {
|
|
28
28
|
// Re-import setup internals by calling setup's install functions
|
|
29
29
|
// We import dynamically to avoid circular deps
|
|
30
|
-
const { installHooks, installSkills, installGstack } = await import('./setup-internals.js');
|
|
30
|
+
const { installHooks, installSkills, installGstack, writeGlobalClaudeMd } = await import('./setup-internals.js');
|
|
31
31
|
|
|
32
32
|
const hookResult = installHooks();
|
|
33
33
|
|
|
@@ -37,6 +37,9 @@ export async function update() {
|
|
|
37
37
|
spinner.text = 'Updating gstack skills...';
|
|
38
38
|
const gstackResult = installGstack();
|
|
39
39
|
|
|
40
|
+
spinner.text = 'Updating global Claude rules...';
|
|
41
|
+
const rulesResult = writeGlobalClaudeMd();
|
|
42
|
+
|
|
40
43
|
// Update version in config
|
|
41
44
|
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
42
45
|
setConfig('packageVersion', pkg.version);
|
|
@@ -50,6 +53,7 @@ export async function update() {
|
|
|
50
53
|
if (gstackResult.warning) {
|
|
51
54
|
console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
|
|
52
55
|
}
|
|
56
|
+
console.log(` ${chalk.green('✓')} Rules: ${rulesResult.path}`);
|
|
53
57
|
console.log(` ${chalk.green('✓')} Config: version set to ${pkg.version}`);
|
|
54
58
|
console.log('');
|
|
55
59
|
} catch (err) {
|