soloship 0.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/dist/hooks.js ADDED
@@ -0,0 +1,477 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export async function installHooks(root, project) {
4
+ const results = [];
5
+ const claudeDir = join(root, ".claude");
6
+ if (!existsSync(claudeDir)) {
7
+ mkdirSync(claudeDir, { recursive: true });
8
+ }
9
+ const settingsPath = join(claudeDir, "settings.local.json");
10
+ // Read existing settings or start fresh
11
+ let settings = {};
12
+ if (existsSync(settingsPath)) {
13
+ try {
14
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
15
+ }
16
+ catch {
17
+ // Invalid JSON, start fresh
18
+ }
19
+ }
20
+ // Build hooks config
21
+ const hooks = {};
22
+ // PreToolUse: Block dangerous commands + phone-a-friend warnings
23
+ hooks.PreToolUse = [
24
+ {
25
+ matcher: "Bash",
26
+ hooks: [
27
+ {
28
+ type: "command",
29
+ command: buildPreToolUseScript(),
30
+ timeout: 5000,
31
+ },
32
+ ],
33
+ },
34
+ {
35
+ matcher: "Bash",
36
+ hooks: [
37
+ {
38
+ type: "command",
39
+ command: buildPhoneAFriendScript(),
40
+ timeout: 10000,
41
+ },
42
+ ],
43
+ },
44
+ {
45
+ matcher: "Bash",
46
+ hooks: [
47
+ {
48
+ type: "command",
49
+ command: buildSecurityScanScript(),
50
+ timeout: 30000,
51
+ },
52
+ ],
53
+ },
54
+ ];
55
+ results.push("PreToolUse: block dangerous commands (rm -rf ~, .env edits, force push to main)");
56
+ results.push("PreToolUse: phone-a-friend warnings on commits (6 heuristic patterns)");
57
+ results.push("PreToolUse: security scan on commits (Semgrep, blocks critical findings)");
58
+ // PostToolUse: Auto-lint after file edits + CHANGELOG check after commits
59
+ const postToolUseHooks = [];
60
+ if (project.stack.hasLinter) {
61
+ postToolUseHooks.push({
62
+ matcher: "Edit|Write",
63
+ hooks: [
64
+ {
65
+ type: "command",
66
+ command: buildPostToolUseLintScript(project),
67
+ timeout: 10000,
68
+ },
69
+ ],
70
+ });
71
+ results.push("PostToolUse: auto-lint after file edits");
72
+ }
73
+ // CHANGELOG check: warn if feat/fix/refactor commit lacks CHANGELOG entry
74
+ postToolUseHooks.push({
75
+ matcher: "Bash",
76
+ hooks: [
77
+ {
78
+ type: "command",
79
+ command: buildChangelogCheckScript(),
80
+ timeout: 5000,
81
+ },
82
+ ],
83
+ });
84
+ results.push("PostToolUse: CHANGELOG check for feat/fix/refactor commits");
85
+ if (postToolUseHooks.length > 0) {
86
+ hooks.PostToolUse = postToolUseHooks;
87
+ }
88
+ // Stop: Plan validation + dependency graph + workflow navigator + handoff reminder
89
+ hooks.Stop = [
90
+ {
91
+ matcher: "",
92
+ hooks: [
93
+ {
94
+ type: "command",
95
+ command: buildStopScript(project),
96
+ timeout: 15000,
97
+ },
98
+ ],
99
+ },
100
+ ];
101
+ results.push("Stop: plan validation + workflow navigator + handoff reminder");
102
+ // SessionStart: Checkpoint commit + context injection
103
+ hooks.SessionStart = [
104
+ {
105
+ matcher: "",
106
+ hooks: [
107
+ {
108
+ type: "command",
109
+ command: buildCheckpointScript(),
110
+ timeout: 15000,
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ matcher: "",
116
+ hooks: [
117
+ {
118
+ type: "command",
119
+ command: buildSessionStartScript(),
120
+ timeout: 10000,
121
+ },
122
+ ],
123
+ },
124
+ ];
125
+ results.push("SessionStart: checkpoint commit before agent session");
126
+ results.push("SessionStart: context injection");
127
+ // Merge hooks into settings (don't overwrite other settings)
128
+ settings.hooks = hooks;
129
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
130
+ results.push(`Written to .claude/settings.local.json`);
131
+ return results;
132
+ }
133
+ function buildPreToolUseScript() {
134
+ // Exit code 2 blocks the action
135
+ return `bash -c '
136
+ COMMAND="$HOOK_TOOL_INPUT"
137
+
138
+ # Block rm -rf with home directory
139
+ if echo "$COMMAND" | grep -qE "rm\\s+-rf\\s+(~|/Users|/home|\\$HOME)"; then
140
+ echo "BLOCKED: Dangerous rm -rf targeting home directory" >&2
141
+ exit 2
142
+ fi
143
+
144
+ # Block .env edits
145
+ if echo "$COMMAND" | grep -qE "(cat|echo|printf|>).*\\.env($|\\s)"; then
146
+ echo "BLOCKED: Direct .env file modification" >&2
147
+ exit 2
148
+ fi
149
+
150
+ # Block force push to main/master
151
+ if echo "$COMMAND" | grep -qE "git\\s+push.*--force.*(main|master)"; then
152
+ echo "BLOCKED: Force push to main/master" >&2
153
+ exit 2
154
+ fi
155
+
156
+ # Block hardcoded API keys
157
+ if echo "$COMMAND" | grep -qE "(ANTHROPIC|OPENAI|STRIPE|FIREBASE)_.*_KEY.*=.*[a-zA-Z0-9]{20}"; then
158
+ echo "BLOCKED: Possible hardcoded API key" >&2
159
+ exit 2
160
+ fi
161
+
162
+ exit 0
163
+ '`;
164
+ }
165
+ function buildPostToolUseLintScript(project) {
166
+ const lintCmd = project.stack.hasLinter
167
+ ? project.stack.packageManager === "bun"
168
+ ? "bunx eslint --fix"
169
+ : "npx eslint --fix"
170
+ : "true";
171
+ return `bash -c '
172
+ # Only lint if a source file was modified
173
+ FILE="$HOOK_MODIFIED_FILE"
174
+ if [ -n "$FILE" ] && echo "$FILE" | grep -qE "\\.(ts|tsx|js|jsx)$"; then
175
+ ${lintCmd} "$FILE" 2>/dev/null || true
176
+ fi
177
+ '`;
178
+ }
179
+ function buildChangelogCheckScript() {
180
+ return `bash -c '
181
+ # Only check if the last command was a git commit
182
+ COMMAND="$HOOK_TOOL_INPUT"
183
+ if echo "$COMMAND" | grep -qE "git\\s+commit"; then
184
+ # Get the most recent commit message
185
+ MSG=$(git log -1 --pretty=%s 2>/dev/null)
186
+ # Only warn for feat/fix/refactor commits
187
+ if echo "$MSG" | grep -qE "^(feat|fix|refactor):"; then
188
+ # Check if CHANGELOG.md was modified in this commit
189
+ if ! git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -q "CHANGELOG.md"; then
190
+ echo "{\\"systemMessage\\": \\"Warning: commit \\\\\"$MSG\\\\\" has no CHANGELOG.md entry. Consider adding one to the [Unreleased] section.\\"}"
191
+ fi
192
+ fi
193
+ fi
194
+ exit 0
195
+ '`;
196
+ }
197
+ function buildStopScript(project) {
198
+ return `bash -c '
199
+ MESSAGES=""
200
+
201
+ # Plan validation: check for Key Decisions and Why lines
202
+ for plan in docs/plans/$(date +%Y)*.md; do
203
+ if [ -f "$plan" ]; then
204
+ if ! grep -q "Key Decisions" "$plan" 2>/dev/null; then
205
+ MESSAGES="$MESSAGES Plan file $plan is missing a Key Decisions section."
206
+ break
207
+ fi
208
+ fi
209
+ done
210
+
211
+ # Dependency graph generation removed.
212
+
213
+ # Workflow navigator: detect what just happened and suggest next step
214
+ LAST_COMMIT=$(git log -1 --pretty=%s 2>/dev/null || true)
215
+ RECENT_PLANS=$(find docs/plans -maxdepth 1 -name "*.md" -newer docs/plans/archive -type f 2>/dev/null | head -1)
216
+ HAS_STAGED=$(git diff --cached --name-only 2>/dev/null | head -1)
217
+ HAS_UNSTAGED=$(git diff --name-only 2>/dev/null | head -1)
218
+
219
+ # If a plan was just written, suggest next step
220
+ if [ -n "$RECENT_PLANS" ] && [ -f "$RECENT_PLANS" ]; then
221
+ PLAN_AGE=$(( $(date +%s) - $(stat -f %m "$RECENT_PLANS" 2>/dev/null || stat -c %Y "$RECENT_PLANS" 2>/dev/null || echo 0) ))
222
+ if [ "$PLAN_AGE" -lt 120 ]; then
223
+ MESSAGES="$MESSAGES Plan written. Design what it looks like, then run /soloship-implement to execute."
224
+ fi
225
+ fi
226
+
227
+ # If code was just committed, suggest ship or learn
228
+ if echo "$LAST_COMMIT" | grep -qE "^(feat|fix|refactor):" 2>/dev/null; then
229
+ COMMIT_AGE=$(( $(date +%s) - $(git log -1 --format=%ct 2>/dev/null || echo 0) ))
230
+ if [ "$COMMIT_AGE" -lt 120 ]; then
231
+ MESSAGES="$MESSAGES Code committed. Run /soloship-shipfast to deploy or /soloship-shipthorough for full review."
232
+ fi
233
+ fi
234
+
235
+ # Handoff reminder: if session has been active 30+ min, nudge for state capture
236
+ SESSION_FILE=".ai/.session-start"
237
+ if [ ! -f "$SESSION_FILE" ]; then
238
+ mkdir -p .ai
239
+ date +%s > "$SESSION_FILE"
240
+ fi
241
+ SESSION_START=$(cat "$SESSION_FILE" 2>/dev/null || echo 0)
242
+ NOW=$(date +%s)
243
+ ELAPSED=$(( NOW - SESSION_START ))
244
+ HANDOFF_FILE=".ai/.last-handoff"
245
+ LAST_HANDOFF=$(cat "$HANDOFF_FILE" 2>/dev/null || echo 0)
246
+ SINCE_HANDOFF=$(( NOW - LAST_HANDOFF ))
247
+
248
+ # Remind every 30 minutes
249
+ if [ "$ELAPSED" -gt 1800 ] && [ "$SINCE_HANDOFF" -gt 1800 ]; then
250
+ if [ -n "$HAS_STAGED" ] || [ -n "$HAS_UNSTAGED" ]; then
251
+ MESSAGES="$MESSAGES Session active 30+ min with uncommitted work. Consider writing a handoff note: state of work + next tiny action."
252
+ echo "$NOW" > "$HANDOFF_FILE"
253
+ fi
254
+ fi
255
+
256
+ # Output combined message if any
257
+ if [ -n "$MESSAGES" ]; then
258
+ # Escape for JSON
259
+ ESCAPED=$(echo "$MESSAGES" | sed "s/\"/\\\\\\\\\"/g")
260
+ echo "{\\"systemMessage\\": \\"$ESCAPED\\"}"
261
+ fi
262
+ '`;
263
+ }
264
+ function buildCheckpointScript() {
265
+ // Creates a checkpoint at session start so the user can rollback.
266
+ // Saves HEAD commit SHA and, if uncommitted changes exist, a stash snapshot SHA.
267
+ // Uses git stash create — creates a snapshot commit object WITHOUT modifying
268
+ // the working directory or the stash list. Zero side effects.
269
+ return `bash -c '
270
+ # Only run in a git repo
271
+ if ! git rev-parse --is-inside-work-tree &>/dev/null; then
272
+ exit 0
273
+ fi
274
+
275
+ # Check if there are any commits yet
276
+ if ! git rev-parse HEAD &>/dev/null; then
277
+ exit 0
278
+ fi
279
+
280
+ CHECKPOINT_DIR=".ai"
281
+ mkdir -p "$CHECKPOINT_DIR"
282
+
283
+ # Save current HEAD as the checkpoint
284
+ git rev-parse HEAD > "$CHECKPOINT_DIR/.last-checkpoint" 2>/dev/null
285
+
286
+ # If there are uncommitted changes, snapshot them without modifying working tree.
287
+ # git stash create returns a SHA but does NOT push to stash list or touch files.
288
+ STASH_SHA=$(git stash create 2>/dev/null)
289
+ if [ -n "$STASH_SHA" ]; then
290
+ echo "$STASH_SHA" > "$CHECKPOINT_DIR/.last-checkpoint-stash"
291
+ echo "{\\"systemMessage\\": \\"Safety snapshot saved. Your current work is preserved. If anything goes wrong, run: npx soloship rollback\\"}"
292
+ else
293
+ rm -f "$CHECKPOINT_DIR/.last-checkpoint-stash"
294
+ echo "{\\"systemMessage\\": \\"Safety snapshot saved. If anything goes wrong, run: npx soloship rollback\\"}"
295
+ fi
296
+ '`;
297
+ }
298
+ function buildSessionStartScript() {
299
+ return `bash -c '
300
+ # Dependency graph injection removed.
301
+ '`;
302
+ }
303
+ function buildPhoneAFriendScript() {
304
+ // Phone-a-friend: warn on git commit/push when staged changes match risk heuristics.
305
+ // All 6 checks use git diff and the filesystem only — no conversation parsing, no AI judgment.
306
+ // Exit 0 always (warn, never block). Warnings via systemMessage JSON.
307
+ return `bash -c '
308
+ COMMAND="$HOOK_TOOL_INPUT"
309
+
310
+ # Only check git commit commands (push has no staged changes to check)
311
+ if ! echo "$COMMAND" | grep -qE "git\\s+commit"; then
312
+ exit 0
313
+ fi
314
+
315
+ WARNINGS=""
316
+
317
+ STAGED=$(git diff --cached --name-only 2>/dev/null)
318
+ if [ -z "$STAGED" ]; then
319
+ exit 0
320
+ fi
321
+
322
+ # --- Heuristic 1: Files outside declared source directories ---
323
+ # Detect source dirs from filesystem at runtime
324
+ SRC_PATTERN=""
325
+ for d in src lib app pages components routes services models views controllers public static assets; do
326
+ if [ -d "$d" ]; then
327
+ SRC_PATTERN="$SRC_PATTERN|$d"
328
+ fi
329
+ done
330
+ SRC_PATTERN="\${SRC_PATTERN#|}"
331
+
332
+ if [ -n "$SRC_PATTERN" ]; then
333
+ KNOWN_DIRS="$SRC_PATTERN|tests?|__tests__|spec|__arch__|docs|doc|bin|scripts|dist|build|node_modules|\\.github|\\.claude"
334
+ KNOWN_ROOT="^(package\\.json|tsconfig.*\\.json|README.*|CLAUDE\\.md|AGENTS\\.md|CHANGELOG\\.md|\\.gitignore|\\.eslintrc.*|eslint\\.config.*|prettier.*|vite\\.config.*|next\\.config.*|jest\\.config.*|vitest\\.config.*)$"
335
+
336
+ while IFS= read -r file; do
337
+ [ -z "$file" ] && continue
338
+ DIR_PART=$(echo "$file" | cut -d/ -f1)
339
+ # Skip files in known directories
340
+ if echo "$DIR_PART" | grep -qE "^($KNOWN_DIRS)$"; then
341
+ continue
342
+ fi
343
+ # Skip known root-level files
344
+ BASENAME=$(basename "$file")
345
+ if echo "$BASENAME" | grep -qE "$KNOWN_ROOT"; then
346
+ continue
347
+ fi
348
+ WARNINGS="$WARNINGS - File outside source directories: $file\\n"
349
+ done <<< "$STAGED"
350
+ fi
351
+
352
+ # --- Heuristic 2: Configuration file changes ---
353
+ while IFS= read -r file; do
354
+ [ -z "$file" ] && continue
355
+ if echo "$file" | grep -qiE "\\.(env|env\\..+)$|\\.env$"; then
356
+ WARNINGS="$WARNINGS - Environment file changed: $file\\n"
357
+ elif echo "$file" | grep -qiE "(^|/)(\\.github/|Dockerfile|docker-compose|wrangler\\.toml|vercel\\.json|firebase\\.json|netlify\\.toml|fly\\.toml|\\.circleci/)"; then
358
+ WARNINGS="$WARNINGS - CI/deploy config changed: $file\\n"
359
+ elif echo "$file" | grep -qiE "(package-lock\\.json|yarn\\.lock|pnpm-lock\\.yaml|bun\\.lockb|bun\\.lock|Gemfile\\.lock|Pipfile\\.lock|poetry\\.lock)$"; then
360
+ WARNINGS="$WARNINGS - Lock file changed: $file\\n"
361
+ fi
362
+ done <<< "$STAGED"
363
+
364
+ # --- Heuristic 3: New dependencies added ---
365
+ while IFS= read -r file; do
366
+ [ -z "$file" ] && continue
367
+ if echo "$file" | grep -qE "(package\\.json|requirements\\.txt|Gemfile|Pipfile|pyproject\\.toml|go\\.mod|Cargo\\.toml|pom\\.xml|build\\.gradle)$"; then
368
+ # Check if dependency sections have additions (+ lines in the diff)
369
+ ADDITIONS=$(git diff --cached -- "$file" 2>/dev/null | grep -cE "^\\+.*(dependencies|require|gem |install_requires)" || true)
370
+ if [ "$ADDITIONS" -gt 0 ]; then
371
+ WARNINGS="$WARNINGS - New dependency added (check $file)\\n"
372
+ fi
373
+ fi
374
+ done <<< "$STAGED"
375
+
376
+ # --- Heuristic 4: Auth/migration/env/secret file patterns ---
377
+ while IFS= read -r file; do
378
+ [ -z "$file" ] && continue
379
+ if echo "$file" | grep -qiE "(auth|migration|migrate|secret|credential|security|permission|token|password|session|oauth|jwt|api.?key)"; then
380
+ WARNINGS="$WARNINGS - Security-sensitive file changed: $file\\n"
381
+ fi
382
+ done <<< "$STAGED"
383
+
384
+ # --- Heuristic 5: Large diffs (>300 lines added+removed) ---
385
+ DIFF_STAT=$(git diff --cached --numstat 2>/dev/null | awk "{ added += \\$1; removed += \\$2 } END { print added + removed }")
386
+ if [ -n "$DIFF_STAT" ] && [ "$DIFF_STAT" -gt 300 ] 2>/dev/null; then
387
+ WARNINGS="$WARNINGS - Large change: $DIFF_STAT lines added+removed (threshold: 300)\\n"
388
+ fi
389
+
390
+ # --- Heuristic 6: Removal of validation/sanitization patterns ---
391
+ REMOVED_VALIDATION=$(git diff --cached 2>/dev/null | grep -cE "^-.*(sanitize|validate|escape|parameteriz|prepared.?statement|htmlspecialchars|encodeURI|DOMPurify|csrf|xss|sql.?inject|input.?valid)" || true)
392
+ if [ "$REMOVED_VALIDATION" -gt 0 ]; then
393
+ WARNINGS="$WARNINGS - Validation/sanitization code removed ($REMOVED_VALIDATION lines)\\n"
394
+ fi
395
+
396
+ # --- Output warnings ---
397
+ if [ -n "$WARNINGS" ]; then
398
+ MSG="PHONE A FRIEND — Get a second opinion on these changes before shipping:\\n\\n$WARNINGS\\nAsk a developer you trust, post in a coding community (Reddit, Discord, forum), or use a code review service. Non-obvious changes are where bugs hide."
399
+ ESCAPED=$(printf "%b" "$MSG" | sed "s/\"/\\\\\\\\\"/g" | tr "\\n" " ")
400
+ echo "{\\"systemMessage\\": \\"$ESCAPED\\"}"
401
+ fi
402
+
403
+ exit 0
404
+ '`;
405
+ }
406
+ function buildSecurityScanScript() {
407
+ // Automated security scanning: runs Semgrep on staged files before git commit.
408
+ // Deterministic tool-based scanning, not AI-based — the fox doesn't guard the henhouse.
409
+ // Blocks on critical findings (exit 2), warns on medium (exit 0 + systemMessage).
410
+ // Gracefully skips if semgrep is not installed (with install instructions).
411
+ return `bash -c '
412
+ COMMAND="$HOOK_TOOL_INPUT"
413
+
414
+ # Only check git commit commands
415
+ if ! echo "$COMMAND" | grep -qE "git\\s+commit"; then
416
+ exit 0
417
+ fi
418
+
419
+ STAGED=$(git diff --cached --name-only 2>/dev/null)
420
+ if [ -z "$STAGED" ]; then
421
+ exit 0
422
+ fi
423
+
424
+ # Check if semgrep is available
425
+ if ! command -v semgrep &>/dev/null; then
426
+ if [ -f ".semgrep.yml" ] || [ -d ".semgrep" ]; then
427
+ echo "{\\"systemMessage\\": \\"Security scan skipped: semgrep not installed. Install with: pip install semgrep (or pipx install semgrep)\\"}"
428
+ fi
429
+ exit 0
430
+ fi
431
+
432
+ # Only scan source files that are staged
433
+ SCAN_FILES=""
434
+ while IFS= read -r file; do
435
+ [ -z "$file" ] && continue
436
+ if echo "$file" | grep -qE "\\.(ts|tsx|js|jsx|py|rb|go|java|php|rs)$"; then
437
+ if [ -f "$file" ]; then
438
+ SCAN_FILES="$SCAN_FILES $file"
439
+ fi
440
+ fi
441
+ done <<< "$STAGED"
442
+
443
+ if [ -z "$SCAN_FILES" ]; then
444
+ exit 0
445
+ fi
446
+
447
+ # Use project config if available, otherwise OWASP rules
448
+ SEMGREP_CONFIG=".semgrep.yml"
449
+ if [ ! -f "$SEMGREP_CONFIG" ] && [ ! -d ".semgrep" ]; then
450
+ SEMGREP_CONFIG="p/owasp-top-ten"
451
+ fi
452
+
453
+ # Run scan, capture output
454
+ RESULTS=$(semgrep --config "$SEMGREP_CONFIG" --json $SCAN_FILES 2>/dev/null || true)
455
+
456
+ if [ -z "$RESULTS" ]; then
457
+ exit 0
458
+ fi
459
+
460
+ # Count findings by severity using grep (no python3 dependency)
461
+ CRITICAL=$(echo "$RESULTS" | grep -cE "\\"severity\\"[[:space:]]*:[[:space:]]*\\"ERROR\\"" 2>/dev/null || echo "0")
462
+ MEDIUM=$(echo "$RESULTS" | grep -cE "\\"severity\\"[[:space:]]*:[[:space:]]*\\"WARNING\\"" 2>/dev/null || echo "0")
463
+
464
+ # Block on critical findings
465
+ if [ "$CRITICAL" -gt 0 ]; then
466
+ echo "BLOCKED: Semgrep found $CRITICAL critical security finding(s). Run: semgrep --config $SEMGREP_CONFIG to see details." >&2
467
+ exit 2
468
+ fi
469
+
470
+ # Warn on medium findings
471
+ if [ "$MEDIUM" -gt 0 ]; then
472
+ echo "{\\"systemMessage\\": \\"Security scan: $MEDIUM medium-severity finding(s). Run semgrep for details. Consider fixing before shipping.\\"}"
473
+ fi
474
+
475
+ exit 0
476
+ '`;
477
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ interface InitOptions {
2
+ skipPrompts?: boolean;
3
+ }
4
+ export declare function runInit(options: InitOptions): Promise<void>;
5
+ export {};
package/dist/init.js ADDED
@@ -0,0 +1,94 @@
1
+ import { input } from "@inquirer/prompts";
2
+ import { execSync } from "node:child_process";
3
+ import chalk from "chalk";
4
+ import { detectProject } from "./detect.js";
5
+ import { scaffoldDocs } from "./scaffold.js";
6
+ import { installHooks } from "./hooks.js";
7
+ import { installRules } from "./rules.js";
8
+ import { installCi } from "./ci.js";
9
+ export async function runInit(options) {
10
+ const root = process.cwd();
11
+ // Step 1: Detect project
12
+ console.log(chalk.blue("Detecting project..."));
13
+ const detected = detectProject(root);
14
+ const stack = detected.stack;
15
+ const existingDocs = detected.existingDocs;
16
+ if (stack.language !== "unknown") {
17
+ console.log(` Stack: ${chalk.cyan(stack.language)}` +
18
+ (stack.framework ? ` + ${chalk.cyan(stack.framework)}` : ""));
19
+ console.log(` Package manager: ${chalk.cyan(stack.packageManager)}`);
20
+ }
21
+ if (existingDocs.hasClaudeMd) {
22
+ console.log(` ${chalk.yellow("CLAUDE.md already exists")} — will not overwrite`);
23
+ }
24
+ console.log("");
25
+ // Step 2: Gather project info
26
+ let projectName = detected.name;
27
+ let projectDescription;
28
+ if (!options.skipPrompts) {
29
+ if (!projectName) {
30
+ projectName = await input({
31
+ message: "Project name:",
32
+ default: root.split("/").pop(),
33
+ });
34
+ }
35
+ else {
36
+ console.log(` Project: ${chalk.bold(projectName)}`);
37
+ }
38
+ projectDescription = await input({
39
+ message: "One sentence — what does this project do?",
40
+ });
41
+ }
42
+ const projectInfo = {
43
+ name: projectName || root.split("/").pop() || "my-project",
44
+ description: projectDescription || "",
45
+ stack,
46
+ hasGit: detected.hasGit || false,
47
+ hasClaude: detected.hasClaude || false,
48
+ existingDocs,
49
+ };
50
+ // Step 3: Scaffold documentation infrastructure
51
+ console.log("");
52
+ console.log(chalk.blue("Creating documentation infrastructure..."));
53
+ const scaffoldResults = await scaffoldDocs(root, projectInfo);
54
+ for (const result of scaffoldResults) {
55
+ const icon = result.action === "created" ? chalk.green("+") : chalk.yellow("~");
56
+ console.log(` ${icon} ${result.path} ${chalk.dim(`(${result.action})`)}`);
57
+ }
58
+ // Step 4: Install Claude Code hooks
59
+ console.log("");
60
+ console.log(chalk.blue("Configuring Claude Code hooks..."));
61
+ const hookResults = await installHooks(root, projectInfo);
62
+ for (const result of hookResults) {
63
+ console.log(` ${chalk.green("+")} ${result}`);
64
+ }
65
+ // Step 5: Install rules
66
+ console.log("");
67
+ console.log(chalk.blue("Installing workflow rules..."));
68
+ const ruleResults = await installRules(root);
69
+ for (const result of ruleResults) {
70
+ console.log(` ${chalk.green("+")} ${result}`);
71
+ }
72
+ // Step 6: Install CI + architecture fitness functions
73
+ console.log("");
74
+ console.log(chalk.blue("Setting up CI..."));
75
+ const ciResults = await installCi(root, projectInfo);
76
+ for (const result of ciResults) {
77
+ console.log(` ${chalk.green("+")} ${result}`);
78
+ }
79
+ // Post-install notes
80
+ const notes = [];
81
+ try {
82
+ execSync("command -v semgrep", { stdio: "ignore" });
83
+ }
84
+ catch {
85
+ notes.push(`Semgrep not found. Security scanning hooks will skip until installed: ${chalk.cyan("pipx install semgrep")}`);
86
+ }
87
+ if (notes.length > 0) {
88
+ console.log("");
89
+ console.log(chalk.yellow("Notes:"));
90
+ for (const note of notes) {
91
+ console.log(` ${chalk.dim("→")} ${note}`);
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Soloship dependency manifest.
3
+ *
4
+ * Declares the companion plugins, MCP servers, user-scope skills, and global
5
+ * hooks that Soloship either REQUIRES (its skills will break without them) or
6
+ * RECOMMENDS (non-coder workflow is materially improved by having them).
7
+ *
8
+ * The `doctor` command audits this list against the user's actual
9
+ * `~/.claude/` environment and reports what's missing with install commands.
10
+ *
11
+ * Edit this file when Soloship adds or drops companion dependencies. The
12
+ * manifest is the single source of truth for dependency information — do not
13
+ * hard-code checks elsewhere.
14
+ */
15
+ export type Severity = "required" | "recommended";
16
+ export interface PluginDep {
17
+ /** Plugin identifier as it appears in ~/.claude/settings.json enabledPlugins */
18
+ id: string;
19
+ /** Marketplace the plugin ships from (the suffix after @ in enabledPlugins) */
20
+ source: string;
21
+ severity: Severity;
22
+ /** One-sentence purpose for the doctor output */
23
+ purpose: string;
24
+ /** Which Soloship skills route to this plugin */
25
+ usedBy: string[];
26
+ /** Install guidance shown when missing */
27
+ install: string;
28
+ }
29
+ export interface McpServerDep {
30
+ /** Server name as it appears in `claude mcp list` */
31
+ name: string;
32
+ severity: Severity;
33
+ purpose: string;
34
+ /** Install command (or template — use <VAULT_PATH>, <PROJECT_ROOT>, etc.) */
35
+ install: string;
36
+ /** Notes shown alongside install command */
37
+ notes?: string;
38
+ }
39
+ export interface SkillDep {
40
+ /** Directory name under ~/.claude/skills/ */
41
+ name: string;
42
+ severity: Severity;
43
+ purpose: string;
44
+ install: string;
45
+ }
46
+ export interface HookDep {
47
+ /** Hook event name (SessionStart, PreToolUse, Stop, etc.) */
48
+ event: string;
49
+ /** Matcher value, or null for matcher-less hooks */
50
+ matcher: string | null;
51
+ /** Substring that identifies this hook's command in settings.json */
52
+ commandContains: string;
53
+ severity: Severity;
54
+ purpose: string;
55
+ install: string;
56
+ }
57
+ export interface DependencyManifest {
58
+ plugins: PluginDep[];
59
+ mcpServers: McpServerDep[];
60
+ skills: SkillDep[];
61
+ hooks: HookDep[];
62
+ }
63
+ export declare const SOLOSHIP_MANIFEST: DependencyManifest;