claude-setup 1.1.6 → 1.1.8
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/README.md +88 -107
- package/dist/builder.d.ts +6 -0
- package/dist/builder.js +134 -78
- package/dist/commands/add.d.ts +3 -1
- package/dist/commands/add.js +5 -13
- package/dist/commands/init.js +61 -21
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +5 -9
- package/dist/commands/restore.d.ts +4 -1
- package/dist/commands/restore.js +42 -4
- package/dist/commands/sync.js +130 -103
- package/dist/index.js +39 -9
- package/dist/marketplace.js +6 -4
- package/dist/os.d.ts +33 -4
- package/dist/os.js +238 -3
- package/dist/snapshot.js +1 -0
- package/package.json +1 -1
- package/templates/add.md +33 -1
- package/templates/sync.md +15 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { createInterface } from "readline";
|
|
3
4
|
import { createRequire } from "module";
|
|
4
5
|
import { runInit } from "./commands/init.js";
|
|
5
6
|
import { runAdd } from "./commands/add.js";
|
|
@@ -10,6 +11,7 @@ import { runRemove } from "./commands/remove.js";
|
|
|
10
11
|
import { runRestore } from "./commands/restore.js";
|
|
11
12
|
import { runCompare } from "./commands/compare.js";
|
|
12
13
|
import { runExport } from "./commands/export.js";
|
|
14
|
+
import { c } from "./output.js";
|
|
13
15
|
const require = createRequire(import.meta.url);
|
|
14
16
|
const pkg = require("../package.json");
|
|
15
17
|
const program = new Command();
|
|
@@ -24,9 +26,9 @@ program
|
|
|
24
26
|
.option("--template <path>", "Apply a template instead of scanning (local path or URL)")
|
|
25
27
|
.action((opts) => runInit({ dryRun: opts.dryRun, template: opts.template }));
|
|
26
28
|
program
|
|
27
|
-
.command("add")
|
|
29
|
+
.command("add [input...]")
|
|
28
30
|
.description("Add a multi-file capability")
|
|
29
|
-
.action(runAdd);
|
|
31
|
+
.action((input) => runAdd({ input: input?.length ? input.join(" ") : undefined }));
|
|
30
32
|
program
|
|
31
33
|
.command("sync")
|
|
32
34
|
.description("Update setup after project changes")
|
|
@@ -49,23 +51,51 @@ program
|
|
|
49
51
|
testHooks: opts.testHooks,
|
|
50
52
|
}));
|
|
51
53
|
program
|
|
52
|
-
.command("remove")
|
|
54
|
+
.command("remove [input...]")
|
|
53
55
|
.description("Remove a capability cleanly")
|
|
54
|
-
.action(runRemove);
|
|
55
|
-
// Feature A: Time-travel snapshot commands
|
|
56
|
+
.action((input) => runRemove({ input: input?.length ? input.join(" ") : undefined }));
|
|
56
57
|
program
|
|
57
58
|
.command("restore")
|
|
58
59
|
.description("Jump to any snapshot node, restore files to that state")
|
|
59
|
-
.
|
|
60
|
+
.option("--list", "Show snapshot timeline without prompting")
|
|
61
|
+
.option("--id <snapshotId>", "Restore directly to a specific snapshot ID")
|
|
62
|
+
.action((opts) => runRestore({ list: opts.list, id: opts.id }));
|
|
60
63
|
program
|
|
61
64
|
.command("compare")
|
|
62
65
|
.description("Diff between any two snapshot nodes to see what changed")
|
|
63
66
|
.action(runCompare);
|
|
64
|
-
// Feature H: Config template export
|
|
65
67
|
program
|
|
66
68
|
.command("export")
|
|
67
69
|
.description("Save current project config as a reusable template")
|
|
68
70
|
.action(runExport);
|
|
69
|
-
// Default action when no command given
|
|
70
|
-
program.action(() =>
|
|
71
|
+
// Default action — interactive menu when no command given
|
|
72
|
+
program.action(async () => {
|
|
73
|
+
const choices = [
|
|
74
|
+
{ key: "1", label: "init", desc: "Full project setup", run: () => runInit({}) },
|
|
75
|
+
{ key: "2", label: "add", desc: "Add a capability", run: () => runAdd({}) },
|
|
76
|
+
{ key: "3", label: "sync", desc: "Update after changes", run: () => runSync({}) },
|
|
77
|
+
{ key: "4", label: "status", desc: "Show current state", run: () => runStatus() },
|
|
78
|
+
{ key: "5", label: "doctor", desc: "Validate environment", run: () => runDoctorCommand({}) },
|
|
79
|
+
{ key: "6", label: "restore", desc: "Time-travel to snapshot", run: () => runRestore({}) },
|
|
80
|
+
{ key: "7", label: "compare", desc: "Diff between snapshots", run: () => runCompare() },
|
|
81
|
+
{ key: "8", label: "remove", desc: "Remove a capability", run: () => runRemove({}) },
|
|
82
|
+
{ key: "9", label: "export", desc: "Save as template", run: () => runExport() },
|
|
83
|
+
];
|
|
84
|
+
console.log(`\n${c.bold("Claude Setup")} ${c.dim(`v${pkg.version}`)}\n`);
|
|
85
|
+
for (const ch of choices) {
|
|
86
|
+
console.log(` ${c.cyan(ch.key)} ${ch.label.padEnd(10)} ${c.dim(ch.desc)}`);
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
+
const answer = await new Promise(resolve => {
|
|
91
|
+
rl.question(` ${c.bold("Choose (1-9):")} `, a => { rl.close(); resolve(a.trim()); });
|
|
92
|
+
});
|
|
93
|
+
const choice = choices.find(ch => ch.key === answer || ch.label === answer.toLowerCase());
|
|
94
|
+
if (!choice) {
|
|
95
|
+
console.log(`\n Invalid choice. Run ${c.cyan("npx claude-setup <command>")} or pick 1-9.\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.log("");
|
|
99
|
+
await choice.run();
|
|
100
|
+
});
|
|
71
101
|
program.parse();
|
package/dist/marketplace.js
CHANGED
|
@@ -104,10 +104,11 @@ export function buildMarketplaceInstructions(input) {
|
|
|
104
104
|
const { categories, saasMatches } = classifyRequest(input);
|
|
105
105
|
const categoryFilter = categories[0] ?? "";
|
|
106
106
|
const lines = [];
|
|
107
|
-
lines.push(`## Marketplace — fully automated install`);
|
|
107
|
+
lines.push(`## Marketplace — fully automated install (DO NOT ASK — JUST DO IT)`);
|
|
108
108
|
lines.push(``);
|
|
109
109
|
lines.push(`A plugin marketplace has 416 community skills + 13 official Anthropic plugins.`);
|
|
110
|
-
lines.push(`Follow the steps below IN ORDER. Do not stop until a skill is installed.`);
|
|
110
|
+
lines.push(`Follow the steps below IN ORDER. Execute every command yourself. Do not stop until a skill is installed.`);
|
|
111
|
+
lines.push(`**DO NOT ask the user clarifying questions. DO NOT suggest commands for the user to run. Execute them yourself.**`);
|
|
111
112
|
lines.push(``);
|
|
112
113
|
if (categories.length > 0 || saasMatches.length > 0) {
|
|
113
114
|
lines.push(`### Matched for "${input}":`);
|
|
@@ -138,11 +139,12 @@ export function buildMarketplaceInstructions(input) {
|
|
|
138
139
|
lines.push(``);
|
|
139
140
|
lines.push(`NOTE: /plugin marketplace add FAILS for this repo (one entry has broken schema).`);
|
|
140
141
|
lines.push(`Use the DIRECT FETCH approach below instead — no marketplace add needed.`);
|
|
142
|
+
lines.push(`If the curl command fails or returns an error, skip to STEP 3 immediately — do NOT stop.`);
|
|
141
143
|
lines.push(``);
|
|
142
144
|
lines.push(`**2a. Fetch catalog and find matching plugin:**`);
|
|
143
145
|
lines.push(`\`\`\`bash`);
|
|
144
146
|
lines.push(`curl -s "${MARKETPLACE_CATALOG_URL}" \\`);
|
|
145
|
-
lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(
|
|
147
|
+
lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));const q='${categoryFilter}';const r=d.plugins.filter(p=>(q===''||p.category.includes(q))&&p.name&&p.source).slice(0,10).map(p=>({name:p.name,source:p.source,desc:p.description}));console.log(JSON.stringify(r,null,2));"`);
|
|
146
148
|
lines.push(`\`\`\``);
|
|
147
149
|
lines.push(``);
|
|
148
150
|
lines.push(`**2b. Pick the best match — get its source path (e.g. \`./plugins/productivity/my-skill\`)**`);
|
|
@@ -152,7 +154,7 @@ export function buildMarketplaceInstructions(input) {
|
|
|
152
154
|
lines.push(`# Replace PLUGIN_SOURCE_PATH with value from step 2b (e.g. plugins/productivity/my-skill)`);
|
|
153
155
|
lines.push(`PLUGIN_SOURCE_PATH="plugins/productivity/my-skill"`);
|
|
154
156
|
lines.push(`curl -s "https://api.github.com/repos/${MARKETPLACE_REPO}/contents/\${PLUGIN_SOURCE_PATH}/skills" \\`);
|
|
155
|
-
lines.push(` | node -e "const a=JSON.parse(require('fs').readFileSync(
|
|
157
|
+
lines.push(` | node -e "const a=JSON.parse(require('fs').readFileSync(0,'utf8'));console.log(a.map(x=>x.name).join('\\n'));"`);
|
|
156
158
|
lines.push(`\`\`\``);
|
|
157
159
|
lines.push(``);
|
|
158
160
|
lines.push(`**2d. For each skill listed, download and install it:**`);
|
package/dist/os.d.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
export type DetectedOS = "Windows" | "macOS" | "Linux";
|
|
1
|
+
export type DetectedOS = "Windows" | "macOS" | "Linux" | "WSL";
|
|
2
2
|
/**
|
|
3
|
-
* Detect OS once per session. Order
|
|
3
|
+
* Detect OS once per session. Order:
|
|
4
4
|
* 1. COMSPEC set → Windows
|
|
5
5
|
* 2. OS === "Windows_NT" → Windows
|
|
6
6
|
* 3. process.platform === "win32" → Windows
|
|
7
|
-
* 4.
|
|
8
|
-
* 5.
|
|
7
|
+
* 4. /proc/version contains "microsoft" or "WSL" → WSL
|
|
8
|
+
* 5. WSL_DISTRO_NAME env var set → WSL
|
|
9
|
+
* 6. uname() === "Darwin" → macOS
|
|
10
|
+
* 7. default → Linux
|
|
9
11
|
*/
|
|
10
12
|
export declare function detectOS(): DetectedOS;
|
|
13
|
+
/** Returns true if the OS uses Unix-style shell commands (bash, npx direct) */
|
|
14
|
+
export declare function isUnixLike(os: DetectedOS): boolean;
|
|
11
15
|
/**
|
|
12
16
|
* Verified MCP package names — ONLY use these.
|
|
13
17
|
* If a service is not in this map, do not guess a package name.
|
|
14
18
|
*/
|
|
15
19
|
export declare const VERIFIED_MCP_PACKAGES: Record<string, string>;
|
|
20
|
+
/** Default connection strings for local services — used when env vars are not set */
|
|
21
|
+
export declare const DEFAULT_SERVICE_CONNECTIONS: Record<string, {
|
|
22
|
+
envVar: string;
|
|
23
|
+
defaultUrl: string;
|
|
24
|
+
testCmd: Record<DetectedOS, string>;
|
|
25
|
+
}>;
|
|
16
26
|
/** MCP command format per OS — always includes -y to prevent npx install hangs */
|
|
17
27
|
export declare function mcpCommandFormat(os: DetectedOS, pkg: string): {
|
|
18
28
|
command: string;
|
|
@@ -23,3 +33,22 @@ export declare function hookShellFormat(os: DetectedOS, cmd: string): {
|
|
|
23
33
|
command: string;
|
|
24
34
|
args: string[];
|
|
25
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Detect which services are available on the local machine.
|
|
38
|
+
* Returns a map of service name → detected info.
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectLocalServices(os: DetectedOS): Record<string, {
|
|
41
|
+
found: boolean;
|
|
42
|
+
defaultUrl: string;
|
|
43
|
+
envVar: string;
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Scan project files for service evidence and return auto-discovery instructions.
|
|
47
|
+
* Reads docker-compose, .env.example, package.json to find which services are used.
|
|
48
|
+
*/
|
|
49
|
+
export declare function discoverProjectServices(cwd: string): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Build auto-discovery MCP configuration instructions for the detected OS.
|
|
52
|
+
* Returns markdown text to embed in templates.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildServiceDiscoveryInstructions(cwd: string): string;
|
package/dist/os.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
2
4
|
/**
|
|
3
|
-
* Detect OS once per session. Order
|
|
5
|
+
* Detect OS once per session. Order:
|
|
4
6
|
* 1. COMSPEC set → Windows
|
|
5
7
|
* 2. OS === "Windows_NT" → Windows
|
|
6
8
|
* 3. process.platform === "win32" → Windows
|
|
7
|
-
* 4.
|
|
8
|
-
* 5.
|
|
9
|
+
* 4. /proc/version contains "microsoft" or "WSL" → WSL
|
|
10
|
+
* 5. WSL_DISTRO_NAME env var set → WSL
|
|
11
|
+
* 6. uname() === "Darwin" → macOS
|
|
12
|
+
* 7. default → Linux
|
|
9
13
|
*/
|
|
10
14
|
export function detectOS() {
|
|
11
15
|
if (process.env.COMSPEC)
|
|
@@ -14,6 +18,15 @@ export function detectOS() {
|
|
|
14
18
|
return "Windows";
|
|
15
19
|
if (process.platform === "win32")
|
|
16
20
|
return "Windows";
|
|
21
|
+
// WSL detection — runs as Linux but under Windows kernel
|
|
22
|
+
if (process.env.WSL_DISTRO_NAME)
|
|
23
|
+
return "WSL";
|
|
24
|
+
try {
|
|
25
|
+
const procVersion = readFileSync("/proc/version", "utf8").toLowerCase();
|
|
26
|
+
if (procVersion.includes("microsoft") || procVersion.includes("wsl"))
|
|
27
|
+
return "WSL";
|
|
28
|
+
}
|
|
29
|
+
catch { /* not WSL or /proc not available */ }
|
|
17
30
|
try {
|
|
18
31
|
const uname = execSync("uname", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
19
32
|
if (uname === "Darwin")
|
|
@@ -22,6 +35,10 @@ export function detectOS() {
|
|
|
22
35
|
catch { /* not unix — unlikely to reach here */ }
|
|
23
36
|
return "Linux";
|
|
24
37
|
}
|
|
38
|
+
/** Returns true if the OS uses Unix-style shell commands (bash, npx direct) */
|
|
39
|
+
export function isUnixLike(os) {
|
|
40
|
+
return os === "Linux" || os === "macOS" || os === "WSL";
|
|
41
|
+
}
|
|
25
42
|
/**
|
|
26
43
|
* Verified MCP package names — ONLY use these.
|
|
27
44
|
* If a service is not in this map, do not guess a package name.
|
|
@@ -41,11 +58,55 @@ export const VERIFIED_MCP_PACKAGES = {
|
|
|
41
58
|
mysql: "@benborla29/mcp-server-mysql",
|
|
42
59
|
mongodb: "mcp-mongo-server",
|
|
43
60
|
};
|
|
61
|
+
/** Default connection strings for local services — used when env vars are not set */
|
|
62
|
+
export const DEFAULT_SERVICE_CONNECTIONS = {
|
|
63
|
+
postgres: {
|
|
64
|
+
envVar: "DATABASE_URL",
|
|
65
|
+
defaultUrl: "postgresql://localhost:5432/postgres",
|
|
66
|
+
testCmd: {
|
|
67
|
+
Windows: "where psql 2>nul || where pg_isready 2>nul",
|
|
68
|
+
macOS: "command -v psql || command -v pg_isready",
|
|
69
|
+
Linux: "command -v psql || command -v pg_isready",
|
|
70
|
+
WSL: "command -v psql || command -v pg_isready || /mnt/c/Program\\ Files/PostgreSQL/*/bin/psql.exe --version 2>/dev/null",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
mysql: {
|
|
74
|
+
envVar: "MYSQL_URL",
|
|
75
|
+
defaultUrl: "mysql://root@localhost:3306",
|
|
76
|
+
testCmd: {
|
|
77
|
+
Windows: "where mysql 2>nul",
|
|
78
|
+
macOS: "command -v mysql || brew list mysql 2>/dev/null",
|
|
79
|
+
Linux: "command -v mysql",
|
|
80
|
+
WSL: "command -v mysql || /mnt/c/Program\\ Files/MySQL/*/bin/mysql.exe --version 2>/dev/null",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
mongodb: {
|
|
84
|
+
envVar: "MONGODB_URI",
|
|
85
|
+
defaultUrl: "mongodb://localhost:27017",
|
|
86
|
+
testCmd: {
|
|
87
|
+
Windows: "where mongosh 2>nul || where mongo 2>nul",
|
|
88
|
+
macOS: "command -v mongosh || command -v mongo || brew list mongodb-community 2>/dev/null",
|
|
89
|
+
Linux: "command -v mongosh || command -v mongo",
|
|
90
|
+
WSL: "command -v mongosh || command -v mongo || mongosh.exe --version 2>/dev/null",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
redis: {
|
|
94
|
+
envVar: "REDIS_URL",
|
|
95
|
+
defaultUrl: "redis://localhost:6379",
|
|
96
|
+
testCmd: {
|
|
97
|
+
Windows: "where redis-cli 2>nul",
|
|
98
|
+
macOS: "command -v redis-cli || brew list redis 2>/dev/null",
|
|
99
|
+
Linux: "command -v redis-cli",
|
|
100
|
+
WSL: "command -v redis-cli || redis-cli.exe --version 2>/dev/null",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
44
104
|
/** MCP command format per OS — always includes -y to prevent npx install hangs */
|
|
45
105
|
export function mcpCommandFormat(os, pkg) {
|
|
46
106
|
if (os === "Windows") {
|
|
47
107
|
return { command: "cmd", args: ["/c", "npx", "-y", pkg] };
|
|
48
108
|
}
|
|
109
|
+
// macOS, Linux, and WSL all use npx directly
|
|
49
110
|
return { command: "npx", args: ["-y", pkg] };
|
|
50
111
|
}
|
|
51
112
|
/** Hook shell format per OS */
|
|
@@ -53,5 +114,179 @@ export function hookShellFormat(os, cmd) {
|
|
|
53
114
|
if (os === "Windows") {
|
|
54
115
|
return { command: "cmd", args: ["/c", cmd] };
|
|
55
116
|
}
|
|
117
|
+
// macOS, Linux, and WSL all use bash
|
|
56
118
|
return { command: "bash", args: ["-c", cmd] };
|
|
57
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Detect which services are available on the local machine.
|
|
122
|
+
* Returns a map of service name → detected info.
|
|
123
|
+
*/
|
|
124
|
+
export function detectLocalServices(os) {
|
|
125
|
+
const results = {};
|
|
126
|
+
for (const [service, config] of Object.entries(DEFAULT_SERVICE_CONNECTIONS)) {
|
|
127
|
+
let found = false;
|
|
128
|
+
try {
|
|
129
|
+
const cmd = config.testCmd[os];
|
|
130
|
+
execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
|
|
131
|
+
found = true;
|
|
132
|
+
}
|
|
133
|
+
catch { /* not installed */ }
|
|
134
|
+
results[service] = {
|
|
135
|
+
found,
|
|
136
|
+
defaultUrl: config.defaultUrl,
|
|
137
|
+
envVar: config.envVar,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Scan project files for service evidence and return auto-discovery instructions.
|
|
144
|
+
* Reads docker-compose, .env.example, package.json to find which services are used.
|
|
145
|
+
*/
|
|
146
|
+
export function discoverProjectServices(cwd) {
|
|
147
|
+
const discovered = [];
|
|
148
|
+
const os = detectOS();
|
|
149
|
+
// Check docker-compose.yml for service definitions
|
|
150
|
+
for (const dcFile of ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
|
|
151
|
+
const dcPath = join(cwd, dcFile);
|
|
152
|
+
if (existsSync(dcPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const content = readFileSync(dcPath, "utf8");
|
|
155
|
+
if (/postgres|pg_/i.test(content))
|
|
156
|
+
discovered.push("postgres");
|
|
157
|
+
if (/mysql|mariadb/i.test(content))
|
|
158
|
+
discovered.push("mysql");
|
|
159
|
+
if (/mongo/i.test(content))
|
|
160
|
+
discovered.push("mongodb");
|
|
161
|
+
if (/redis/i.test(content))
|
|
162
|
+
discovered.push("redis");
|
|
163
|
+
}
|
|
164
|
+
catch { /* skip */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Check .env.example or .env.sample for service-related vars
|
|
168
|
+
for (const envFile of [".env.example", ".env.sample", ".env.template"]) {
|
|
169
|
+
const envPath = join(cwd, envFile);
|
|
170
|
+
if (existsSync(envPath)) {
|
|
171
|
+
try {
|
|
172
|
+
const content = readFileSync(envPath, "utf8");
|
|
173
|
+
if (/DATABASE_URL|POSTGRES|PG_/i.test(content) && !discovered.includes("postgres"))
|
|
174
|
+
discovered.push("postgres");
|
|
175
|
+
if (/MYSQL/i.test(content) && !discovered.includes("mysql"))
|
|
176
|
+
discovered.push("mysql");
|
|
177
|
+
if (/MONGO/i.test(content) && !discovered.includes("mongodb"))
|
|
178
|
+
discovered.push("mongodb");
|
|
179
|
+
if (/REDIS/i.test(content) && !discovered.includes("redis"))
|
|
180
|
+
discovered.push("redis");
|
|
181
|
+
}
|
|
182
|
+
catch { /* skip */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Check package.json for database dependencies
|
|
186
|
+
const pkgPath = join(cwd, "package.json");
|
|
187
|
+
if (existsSync(pkgPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
190
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
191
|
+
if (allDeps.pg || allDeps.prisma || allDeps["@prisma/client"] || allDeps.knex) {
|
|
192
|
+
if (!discovered.includes("postgres"))
|
|
193
|
+
discovered.push("postgres");
|
|
194
|
+
}
|
|
195
|
+
if (allDeps.mysql2 || allDeps.mysql) {
|
|
196
|
+
if (!discovered.includes("mysql"))
|
|
197
|
+
discovered.push("mysql");
|
|
198
|
+
}
|
|
199
|
+
if (allDeps.mongoose || allDeps.mongodb) {
|
|
200
|
+
if (!discovered.includes("mongodb"))
|
|
201
|
+
discovered.push("mongodb");
|
|
202
|
+
}
|
|
203
|
+
if (allDeps.redis || allDeps.ioredis) {
|
|
204
|
+
if (!discovered.includes("redis"))
|
|
205
|
+
discovered.push("redis");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch { /* skip */ }
|
|
209
|
+
}
|
|
210
|
+
// Check requirements.txt / pyproject.toml for Python projects
|
|
211
|
+
for (const pyFile of ["requirements.txt", "pyproject.toml"]) {
|
|
212
|
+
const pyPath = join(cwd, pyFile);
|
|
213
|
+
if (existsSync(pyPath)) {
|
|
214
|
+
try {
|
|
215
|
+
const content = readFileSync(pyPath, "utf8");
|
|
216
|
+
if (/psycopg|asyncpg|sqlalchemy/i.test(content) && !discovered.includes("postgres"))
|
|
217
|
+
discovered.push("postgres");
|
|
218
|
+
if (/pymysql|mysqlclient/i.test(content) && !discovered.includes("mysql"))
|
|
219
|
+
discovered.push("mysql");
|
|
220
|
+
if (/pymongo|motor/i.test(content) && !discovered.includes("mongodb"))
|
|
221
|
+
discovered.push("mongodb");
|
|
222
|
+
if (/redis/i.test(content) && !discovered.includes("redis"))
|
|
223
|
+
discovered.push("redis");
|
|
224
|
+
}
|
|
225
|
+
catch { /* skip */ }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [...new Set(discovered)];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Build auto-discovery MCP configuration instructions for the detected OS.
|
|
232
|
+
* Returns markdown text to embed in templates.
|
|
233
|
+
*/
|
|
234
|
+
export function buildServiceDiscoveryInstructions(cwd) {
|
|
235
|
+
const os = detectOS();
|
|
236
|
+
const projectServices = discoverProjectServices(cwd);
|
|
237
|
+
const localServices = detectLocalServices(os);
|
|
238
|
+
const lines = [];
|
|
239
|
+
if (projectServices.length === 0)
|
|
240
|
+
return "";
|
|
241
|
+
lines.push(`### Auto-discovered services`);
|
|
242
|
+
lines.push(`The following services were detected in your project files:\n`);
|
|
243
|
+
for (const service of projectServices) {
|
|
244
|
+
const local = localServices[service];
|
|
245
|
+
const pkg = VERIFIED_MCP_PACKAGES[service];
|
|
246
|
+
if (!pkg || !local)
|
|
247
|
+
continue;
|
|
248
|
+
const status = local.found ? "installed locally" : "not found locally";
|
|
249
|
+
const statusIcon = local.found ? "✅" : "⚠️";
|
|
250
|
+
lines.push(`**${service}** — ${statusIcon} ${status}`);
|
|
251
|
+
if (local.found) {
|
|
252
|
+
lines.push(`- Default connection: \`${local.defaultUrl}\``);
|
|
253
|
+
lines.push(`- Env var: \`${local.envVar}\``);
|
|
254
|
+
lines.push(`- If \`${local.envVar}\` is not set in the environment, use the default: \`${local.defaultUrl}\``);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
lines.push(`- Env var: \`${local.envVar}\` — must be set before starting Claude Code`);
|
|
258
|
+
lines.push(`- Use \`\${${local.envVar}}\` in .mcp.json env block`);
|
|
259
|
+
}
|
|
260
|
+
lines.push(``);
|
|
261
|
+
}
|
|
262
|
+
lines.push(`### MCP auto-configuration strategy`);
|
|
263
|
+
lines.push(``);
|
|
264
|
+
lines.push(`For each service above, configure .mcp.json as follows:`);
|
|
265
|
+
lines.push(`1. **Check if the env var is already set** in the user's environment`);
|
|
266
|
+
lines.push(`2. **If set** → use \`\${VARNAME}\` syntax in the env block`);
|
|
267
|
+
lines.push(`3. **If not set but service is installed locally** → use the default connection URL directly in the env block AND document the var in .env.example`);
|
|
268
|
+
lines.push(`4. **If not set and not installed** → use \`\${VARNAME}\` syntax and flag: "⚠️ Set ${"{VARNAME}"} before starting Claude Code"`);
|
|
269
|
+
lines.push(``);
|
|
270
|
+
// OS-specific MCP format reminder
|
|
271
|
+
if (os === "Windows") {
|
|
272
|
+
lines.push(`### OS format (Windows detected)`);
|
|
273
|
+
lines.push(`\`\`\`json`);
|
|
274
|
+
lines.push(`{ "command": "cmd", "args": ["/c", "npx", "-y", "<package>"], "env": { ... } }`);
|
|
275
|
+
lines.push(`\`\`\``);
|
|
276
|
+
}
|
|
277
|
+
else if (os === "WSL") {
|
|
278
|
+
lines.push(`### OS format (WSL detected — uses Linux-style commands)`);
|
|
279
|
+
lines.push(`\`\`\`json`);
|
|
280
|
+
lines.push(`{ "command": "npx", "args": ["-y", "<package>"], "env": { ... } }`);
|
|
281
|
+
lines.push(`\`\`\``);
|
|
282
|
+
lines.push(`Note: WSL can also access Windows-side services on localhost. If a service runs on the Windows host, it is reachable at \`localhost\` from WSL.`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
lines.push(`### OS format (${os} detected)`);
|
|
286
|
+
lines.push(`\`\`\`json`);
|
|
287
|
+
lines.push(`{ "command": "npx", "args": ["-y", "<package>"], "env": { ... } }`);
|
|
288
|
+
lines.push(`\`\`\``);
|
|
289
|
+
}
|
|
290
|
+
lines.push(``);
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|
package/dist/snapshot.js
CHANGED
|
@@ -88,6 +88,7 @@ export function createSnapshot(cwd, command, changedFiles, opts = {}) {
|
|
|
88
88
|
fullSnapshot: true,
|
|
89
89
|
};
|
|
90
90
|
timeline.nodes.push(node);
|
|
91
|
+
delete timeline.restoredTo; // User is at latest — clear any restore marker
|
|
91
92
|
writeTimeline(cwd, timeline);
|
|
92
93
|
writeNodeData(cwd, nodeId, data);
|
|
93
94
|
return node;
|
package/package.json
CHANGED
package/templates/add.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Add to Claude Code setup: "{{USER_INPUT}}"
|
|
4
4
|
|
|
5
|
+
**CRITICAL: Marketplace-first. Do NOT ask clarifying questions. Search and install automatically.**
|
|
6
|
+
**Execute every curl/install command yourself. The user expects zero-friction automation.**
|
|
7
|
+
|
|
5
8
|
## Project context
|
|
6
9
|
{{PROJECT_CONTEXT}}
|
|
7
10
|
|
|
@@ -38,9 +41,38 @@ Parse the user's request and take ALL applicable actions:
|
|
|
38
41
|
If the request mentions an external service (database, API, browser, etc.):
|
|
39
42
|
- Check the verified MCP package list below
|
|
40
43
|
- If found: add to `.mcp.json` with OS-correct format (detected: {{DETECTED_OS}})
|
|
41
|
-
-
|
|
44
|
+
- **Smart connection strings** — follow this order:
|
|
45
|
+
1. Check if the env var is already set in the environment
|
|
46
|
+
2. If not set, detect if the service is installed locally (run check command below)
|
|
47
|
+
3. If local service found: use default localhost URL directly in env block
|
|
48
|
+
4. If nothing found: use `${VARNAME}` syntax and flag the missing var
|
|
42
49
|
- Document new env vars in `.env.example`
|
|
43
50
|
|
|
51
|
+
**Service detection commands ({{DETECTED_OS}}):**
|
|
52
|
+
{{#if IS_WINDOWS}}
|
|
53
|
+
- PostgreSQL: `where psql 2>nul` → default: `postgresql://localhost:5432/postgres`
|
|
54
|
+
- MongoDB: `where mongosh 2>nul` → default: `mongodb://localhost:27017`
|
|
55
|
+
- Redis: `where redis-cli 2>nul` → default: `redis://localhost:6379`
|
|
56
|
+
- MySQL: `where mysql 2>nul` → default: `mysql://root@localhost:3306`
|
|
57
|
+
{{else}}
|
|
58
|
+
{{#if IS_MACOS}}
|
|
59
|
+
- PostgreSQL: `command -v psql || brew list postgresql 2>/dev/null` → default: `postgresql://localhost:5432/postgres`
|
|
60
|
+
- MongoDB: `command -v mongosh || brew list mongodb-community 2>/dev/null` → default: `mongodb://localhost:27017`
|
|
61
|
+
- Redis: `command -v redis-cli || brew list redis 2>/dev/null` → default: `redis://localhost:6379`
|
|
62
|
+
- MySQL: `command -v mysql || brew list mysql 2>/dev/null` → default: `mysql://root@localhost:3306`
|
|
63
|
+
{{else}}
|
|
64
|
+
- PostgreSQL: `command -v psql` → default: `postgresql://localhost:5432/postgres`
|
|
65
|
+
- MongoDB: `command -v mongosh` → default: `mongodb://localhost:27017`
|
|
66
|
+
- Redis: `command -v redis-cli` → default: `redis://localhost:6379`
|
|
67
|
+
- MySQL: `command -v mysql` → default: `mysql://root@localhost:3306`
|
|
68
|
+
{{/if}}
|
|
69
|
+
{{#if IS_WSL}}
|
|
70
|
+
Note: WSL can access Windows-host services on localhost. If the service runs on the Windows side, it is reachable at `localhost` from WSL.
|
|
71
|
+
{{/if}}
|
|
72
|
+
{{/if}}
|
|
73
|
+
|
|
74
|
+
Run the check command. If the service IS installed locally and the env var is NOT set, use the default URL directly. This avoids the "MCP server not showing" problem where `${VARNAME}` fails silently.
|
|
75
|
+
|
|
44
76
|
Verified MCP packages — ONLY use these for MCP servers:
|
|
45
77
|
```
|
|
46
78
|
playwright → @playwright/mcp@latest
|
package/templates/sync.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<!-- claude-setup sync {{DATE}} | last: {{LAST_RUN_DATE}} -->
|
|
2
2
|
|
|
3
3
|
> **Step 1 — always do this first:** Run `npx claude-setup sync` to refresh this file with the latest diff.
|
|
4
|
-
> - If
|
|
5
|
-
> -
|
|
4
|
+
> - If the output says **"No changes"** and shows "Setup is current" → respond "No changes detected since last sync. Everything is up to date." and STOP.
|
|
5
|
+
> - If the output shows **changes** (any "+N added", "~N modified", or "-N deleted" with N > 0) → continue to Step 2.
|
|
6
|
+
> **Step 2:** Re-read `.claude/commands/stack-sync.md` — it now contains the actual diff with line-level changes.
|
|
7
|
+
> **Step 3:** Process every change listed below. Update CLAUDE.md, .mcp.json, settings.json as needed.
|
|
8
|
+
|
|
9
|
+
**IMPORTANT:** Do NOT tell the user to "run /stack-sync" — you ARE running it right now. Process the diff below.
|
|
6
10
|
|
|
7
11
|
Project changed since last setup. Update ONLY what the changes demand.
|
|
8
12
|
|
|
@@ -38,16 +42,20 @@ Skills: {{SKILLS_LIST}} | Commands: {{COMMANDS_LIST}} | Workflows: {{WORKFLOWS_L
|
|
|
38
42
|
|
|
39
43
|
## Your job
|
|
40
44
|
|
|
41
|
-
For EACH changed file
|
|
45
|
+
For EACH changed file, update the Claude Code setup:
|
|
46
|
+
|
|
47
|
+
**Source files added/removed/modified — ALWAYS update CLAUDE.md:**
|
|
48
|
+
- New source directories or modules → add to key dirs section
|
|
49
|
+
- New routes, services, controllers → document the new endpoints/patterns
|
|
50
|
+
- New dependencies or frameworks → update runtime section
|
|
51
|
+
- Renamed or restructured files → update stale paths
|
|
52
|
+
- CLAUDE.md must reflect the CURRENT project structure, not just config files
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
**Config and infrastructure changes:**
|
|
44
55
|
- New dependency → new MCP server needed? New hook justified?
|
|
45
56
|
- New docker-compose service → new MCP entry? Env vars changed?
|
|
46
|
-
- Source file added/removed → CLAUDE.md paths stale? Skill still applies?
|
|
47
57
|
- Config deleted → remove its MCP/hook reference if it was the only evidence?
|
|
48
58
|
|
|
49
|
-
Update ONLY what the change demands.
|
|
50
|
-
Do NOT update things that did not change.
|
|
51
59
|
Do NOT rewrite files — surgical edits only.
|
|
52
60
|
If unsure about a change's implication: flag it, don't guess.
|
|
53
61
|
|