sticky-note-cli 2.6.16 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -4
- package/bin/cli.js +589 -0
- package/bin/mcp-server.js +788 -0
- package/package.json +3 -2
- package/templates/CLAUDE.md +37 -16
- package/templates/copilot-instructions.md +69 -35
- package/templates/environment/manifest.json +37 -0
- package/templates/hooks/inject-context.js +41 -22
- package/templates/hooks/pre-tool-use.js +4 -56
- package/templates/hooks/session-end.js +3 -3
- package/templates/hooks/session-start.js +341 -25
- package/templates/hooks.json +12 -12
- package/templates/settings.json +7 -7
package/bin/cli.js
CHANGED
|
@@ -116,6 +116,7 @@ function installPreCommitHook() {
|
|
|
116
116
|
' [ -f "$_sn_root/.sticky-note/merge-driver.js" ] && git add "$_sn_root/.sticky-note/merge-driver.js" 2>/dev/null || true',
|
|
117
117
|
' [ -d "$_sn_root/.sticky-note/audit" ] && git add "$_sn_root/.sticky-note/audit/" 2>/dev/null || true',
|
|
118
118
|
' [ -d "$_sn_root/.sticky-note/presence" ] && git add "$_sn_root/.sticky-note/presence/" 2>/dev/null || true',
|
|
119
|
+
' [ -d "$_sn_root/.sticky-note/environment" ] && git add "$_sn_root/.sticky-note/environment/" 2>/dev/null || true',
|
|
119
120
|
'fi',
|
|
120
121
|
STICKY_PRE_COMMIT_END,
|
|
121
122
|
].join("\n");
|
|
@@ -761,6 +762,52 @@ async function cmdInit() {
|
|
|
761
762
|
print(" [OK] .git/hooks/post-commit (auto-sync .sticky-note/ after commit)");
|
|
762
763
|
}
|
|
763
764
|
|
|
765
|
+
// Create environment directory structure
|
|
766
|
+
const envDir = path.join(stickyNoteDir, "environment");
|
|
767
|
+
const envManifestDest = path.join(envDir, "manifest.json");
|
|
768
|
+
mkdirSafe(path.join(envDir, "skills"));
|
|
769
|
+
mkdirSafe(path.join(envDir, "agents"));
|
|
770
|
+
mkdirSafe(path.join(envDir, "commands"));
|
|
771
|
+
if (!fs.existsSync(envManifestDest)) {
|
|
772
|
+
// Build manifest from detected MCP servers and skills
|
|
773
|
+
const manifestData = { version: "1", mcp_servers: {}, permissions: [], env_vars: {} };
|
|
774
|
+
for (const server of mcpServers) {
|
|
775
|
+
const entry = { type: server.type || "stdio", description: server.name, required: false };
|
|
776
|
+
if (server.command) entry.command = server.command;
|
|
777
|
+
if (server.args) entry.args = server.args;
|
|
778
|
+
if (server.env) {
|
|
779
|
+
entry.env = {};
|
|
780
|
+
for (const [k, v] of Object.entries(server.env)) {
|
|
781
|
+
entry.env[k] = `\${${k}}`;
|
|
782
|
+
manifestData.env_vars[k] = { description: k, required: true };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
manifestData.mcp_servers[server.name] = entry;
|
|
786
|
+
}
|
|
787
|
+
fs.writeFileSync(envManifestDest, JSON.stringify(manifestData, null, 2) + "\n");
|
|
788
|
+
if (mcpServers.length > 0) {
|
|
789
|
+
print(` [OK] Manifest populated with ${mcpServers.length} detected MCP server(s)`);
|
|
790
|
+
}
|
|
791
|
+
// Copy detected skills into environment/skills/
|
|
792
|
+
for (const skillName of skills) {
|
|
793
|
+
const claudeSkillPath = path.join(process.cwd(), ".claude", "plugins");
|
|
794
|
+
const possibleSkillPaths = [
|
|
795
|
+
path.join(claudeSkillPath, skillName, "SKILL.md"),
|
|
796
|
+
path.join(claudeSkillPath, skillName, "skills", skillName, "SKILL.md"),
|
|
797
|
+
];
|
|
798
|
+
for (const sp of possibleSkillPaths) {
|
|
799
|
+
if (fs.existsSync(sp)) {
|
|
800
|
+
fs.copyFileSync(sp, path.join(envDir, "skills", skillName + ".md"));
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (skills.length > 0) {
|
|
806
|
+
print(` [OK] Copied ${skills.length} detected skill(s) to environment/skills/`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
print(" [OK] Created environment directory (.sticky-note/environment/)");
|
|
810
|
+
|
|
764
811
|
// Force-add .claude/hooks/ and .claude/settings.json so git tracks them
|
|
765
812
|
// (needed if .claude/ was previously in .gitignore)
|
|
766
813
|
try {
|
|
@@ -2430,6 +2477,536 @@ function cmdSync() {
|
|
|
2430
2477
|
print("");
|
|
2431
2478
|
}
|
|
2432
2479
|
|
|
2480
|
+
// MCP-SERVER command — launch the stdio MCP server
|
|
2481
|
+
function cmdMcpServer() {
|
|
2482
|
+
const mcpServerPath = path.join(__dirname, "mcp-server.js");
|
|
2483
|
+
if (!fs.existsSync(mcpServerPath)) {
|
|
2484
|
+
print(" [ERR] mcp-server.js not found");
|
|
2485
|
+
process.exit(1);
|
|
2486
|
+
}
|
|
2487
|
+
// Forward to the MCP server script, passing remaining args + --project
|
|
2488
|
+
const args = ["--project", process.cwd(), ...process.argv.slice(3)];
|
|
2489
|
+
const { execFileSync } = require("child_process");
|
|
2490
|
+
try {
|
|
2491
|
+
execFileSync(process.execPath, [mcpServerPath, ...args], {
|
|
2492
|
+
stdio: "inherit",
|
|
2493
|
+
});
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
if (err.status) process.exit(err.status);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// ──────────────────────────────────────────────
|
|
2500
|
+
// bootstrap — interactive MCP server provisioning with secret resolution
|
|
2501
|
+
// ──────────────────────────────────────────────
|
|
2502
|
+
|
|
2503
|
+
async function cmdBootstrap() {
|
|
2504
|
+
const cwd = process.cwd();
|
|
2505
|
+
const manifestPath = path.join(cwd, ".sticky-note", "environment", "manifest.json");
|
|
2506
|
+
const mcpJsonPath = path.join(cwd, ".mcp.json");
|
|
2507
|
+
const dotEnvPath = path.join(cwd, ".env");
|
|
2508
|
+
const dotEnvExamplePath = path.join(cwd, ".env.example");
|
|
2509
|
+
|
|
2510
|
+
printBanner();
|
|
2511
|
+
print(" 🔧 Bootstrap — MCP Server Provisioning\n");
|
|
2512
|
+
|
|
2513
|
+
// 1. Read manifest
|
|
2514
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2515
|
+
print(" No environment manifest found. Run `npx sticky-note init` first.");
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
2520
|
+
if (!manifest) {
|
|
2521
|
+
print(" [ERR] Failed to parse manifest.json");
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
const mcpServers = manifest.mcp_servers || {};
|
|
2526
|
+
const envVarsMeta = manifest.env_vars || {};
|
|
2527
|
+
const serverEntries = Object.entries(mcpServers);
|
|
2528
|
+
|
|
2529
|
+
if (serverEntries.length === 0) {
|
|
2530
|
+
print(" No MCP servers defined in manifest. Nothing to provision.");
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// 2. Find all ${ENV_VAR} placeholders across server configs
|
|
2535
|
+
const placeholders = new Set();
|
|
2536
|
+
for (const [, config] of serverEntries) {
|
|
2537
|
+
const envEntries = config.env || {};
|
|
2538
|
+
for (const val of Object.values(envEntries)) {
|
|
2539
|
+
if (typeof val === "string" && val.startsWith("${") && val.endsWith("}")) {
|
|
2540
|
+
placeholders.add(val.slice(2, -1));
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// 3. Parse .env file
|
|
2546
|
+
function parseDotEnv(filePath) {
|
|
2547
|
+
const vars = {};
|
|
2548
|
+
try {
|
|
2549
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
2550
|
+
for (const line of content.split("\n")) {
|
|
2551
|
+
const trimmed = line.trim();
|
|
2552
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2553
|
+
const eqIdx = trimmed.indexOf("=");
|
|
2554
|
+
if (eqIdx === -1) continue;
|
|
2555
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
2556
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
2557
|
+
// Strip surrounding quotes
|
|
2558
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
2559
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
2560
|
+
value = value.slice(1, -1);
|
|
2561
|
+
}
|
|
2562
|
+
vars[key] = value;
|
|
2563
|
+
}
|
|
2564
|
+
} catch {
|
|
2565
|
+
// .env doesn't exist — that's fine
|
|
2566
|
+
}
|
|
2567
|
+
return vars;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
const dotEnvVars = parseDotEnv(dotEnvPath);
|
|
2571
|
+
|
|
2572
|
+
// 4. Resolve each placeholder
|
|
2573
|
+
const resolved = {};
|
|
2574
|
+
const promptedVars = {};
|
|
2575
|
+
|
|
2576
|
+
if (placeholders.size > 0) {
|
|
2577
|
+
print(` Found ${placeholders.size} environment variable(s) to resolve:\n`);
|
|
2578
|
+
|
|
2579
|
+
const rl = readline.createInterface({
|
|
2580
|
+
input: process.stdin,
|
|
2581
|
+
output: process.stdout,
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
for (const varName of placeholders) {
|
|
2585
|
+
const meta = envVarsMeta[varName] || {};
|
|
2586
|
+
const desc = meta.description || varName;
|
|
2587
|
+
const docsUrl = meta.docs_url || null;
|
|
2588
|
+
|
|
2589
|
+
// Priority: process.env → .env file → interactive prompt
|
|
2590
|
+
if (process.env[varName]) {
|
|
2591
|
+
resolved[varName] = process.env[varName];
|
|
2592
|
+
print(` ✅ ${varName}: found in shell environment`);
|
|
2593
|
+
} else if (dotEnvVars[varName]) {
|
|
2594
|
+
resolved[varName] = dotEnvVars[varName];
|
|
2595
|
+
print(` ✅ ${varName}: found in .env file`);
|
|
2596
|
+
} else {
|
|
2597
|
+
// Interactive prompt
|
|
2598
|
+
if (docsUrl) {
|
|
2599
|
+
print(` 📖 ${desc}`);
|
|
2600
|
+
print(` Docs: ${docsUrl}`);
|
|
2601
|
+
}
|
|
2602
|
+
const value = await ask(rl, `Enter value for ${varName}`, "");
|
|
2603
|
+
if (value) {
|
|
2604
|
+
resolved[varName] = value;
|
|
2605
|
+
promptedVars[varName] = value;
|
|
2606
|
+
print(` ✅ ${varName}: set via prompt`);
|
|
2607
|
+
} else {
|
|
2608
|
+
print(` ⚠️ ${varName}: skipped (no value provided)`);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// 5. Offer to save prompted values to .env
|
|
2614
|
+
if (Object.keys(promptedVars).length > 0) {
|
|
2615
|
+
print("");
|
|
2616
|
+
const saveChoice = await ask(rl, "Save missing values to .env?", "yes");
|
|
2617
|
+
if (saveChoice.toLowerCase() === "yes" || saveChoice.toLowerCase() === "y") {
|
|
2618
|
+
let envContent = "";
|
|
2619
|
+
try {
|
|
2620
|
+
envContent = fs.readFileSync(dotEnvPath, "utf-8");
|
|
2621
|
+
if (!envContent.endsWith("\n")) envContent += "\n";
|
|
2622
|
+
} catch {
|
|
2623
|
+
// .env doesn't exist yet
|
|
2624
|
+
}
|
|
2625
|
+
for (const [key, val] of Object.entries(promptedVars)) {
|
|
2626
|
+
envContent += `${key}=${val}\n`;
|
|
2627
|
+
}
|
|
2628
|
+
fs.writeFileSync(dotEnvPath, envContent, "utf-8");
|
|
2629
|
+
print(` ✅ Saved ${Object.keys(promptedVars).length} variable(s) to .env`);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
rl.close();
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// 6. Provision MCP servers to .mcp.json
|
|
2637
|
+
let mcpJson = { mcpServers: {} };
|
|
2638
|
+
try {
|
|
2639
|
+
mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
|
|
2640
|
+
mcpJson.mcpServers = mcpJson.mcpServers || {};
|
|
2641
|
+
} catch {
|
|
2642
|
+
// .mcp.json doesn't exist or is invalid — start fresh
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
let provisioned = 0;
|
|
2646
|
+
let skipped = 0;
|
|
2647
|
+
|
|
2648
|
+
for (const [name, config] of serverEntries) {
|
|
2649
|
+
// Build the server entry with resolved env vars
|
|
2650
|
+
const serverEntry = {
|
|
2651
|
+
type: config.type || "stdio",
|
|
2652
|
+
command: config.command,
|
|
2653
|
+
args: config.args || [],
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
if (config.env) {
|
|
2657
|
+
const resolvedEnv = {};
|
|
2658
|
+
let hasUnresolved = false;
|
|
2659
|
+
for (const [envKey, envVal] of Object.entries(config.env)) {
|
|
2660
|
+
if (typeof envVal === "string" && envVal.startsWith("${") && envVal.endsWith("}")) {
|
|
2661
|
+
const varName = envVal.slice(2, -1);
|
|
2662
|
+
if (resolved[varName]) {
|
|
2663
|
+
resolvedEnv[envKey] = resolved[varName];
|
|
2664
|
+
} else {
|
|
2665
|
+
hasUnresolved = true;
|
|
2666
|
+
}
|
|
2667
|
+
} else {
|
|
2668
|
+
resolvedEnv[envKey] = envVal;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (hasUnresolved) {
|
|
2672
|
+
print(` ⚠️ ${name}: skipped (unresolved secrets)`);
|
|
2673
|
+
skipped++;
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
serverEntry.env = resolvedEnv;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
mcpJson.mcpServers[name] = serverEntry;
|
|
2680
|
+
provisioned++;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
2684
|
+
|
|
2685
|
+
// 7. Generate .env.example from manifest env_vars
|
|
2686
|
+
if (Object.keys(envVarsMeta).length > 0) {
|
|
2687
|
+
const lines = [
|
|
2688
|
+
"# Environment variables for this project",
|
|
2689
|
+
"# Generated by sticky-note bootstrap",
|
|
2690
|
+
"#",
|
|
2691
|
+
"# Copy this file to .env and fill in your values.",
|
|
2692
|
+
"",
|
|
2693
|
+
];
|
|
2694
|
+
for (const [varName, meta] of Object.entries(envVarsMeta)) {
|
|
2695
|
+
if (meta.description) lines.push(`# ${meta.description}`);
|
|
2696
|
+
if (meta.docs_url) lines.push(`# Docs: ${meta.docs_url}`);
|
|
2697
|
+
if (meta.required === false) {
|
|
2698
|
+
lines.push(`# ${varName}=`);
|
|
2699
|
+
} else {
|
|
2700
|
+
lines.push(`${varName}=`);
|
|
2701
|
+
}
|
|
2702
|
+
lines.push("");
|
|
2703
|
+
}
|
|
2704
|
+
fs.writeFileSync(dotEnvExamplePath, lines.join("\n"), "utf-8");
|
|
2705
|
+
print(` ✅ Generated .env.example`);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// 8. Print summary
|
|
2709
|
+
print("");
|
|
2710
|
+
print(" ── Summary ──");
|
|
2711
|
+
print(` Provisioned: ${provisioned} server(s)`);
|
|
2712
|
+
if (skipped > 0) print(` Skipped: ${skipped} server(s) (missing secrets)`);
|
|
2713
|
+
print(` Config: ${mcpJsonPath}`);
|
|
2714
|
+
print("");
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// ──────────────────────────────────────────────
|
|
2718
|
+
// env — environment management subcommands
|
|
2719
|
+
// ──────────────────────────────────────────────
|
|
2720
|
+
|
|
2721
|
+
async function cmdEnv() {
|
|
2722
|
+
const args = process.argv.slice(3);
|
|
2723
|
+
const subcommand = args[0];
|
|
2724
|
+
|
|
2725
|
+
switch (subcommand) {
|
|
2726
|
+
case "status":
|
|
2727
|
+
cmdEnvStatus();
|
|
2728
|
+
break;
|
|
2729
|
+
case "add-server":
|
|
2730
|
+
await cmdEnvAddServer();
|
|
2731
|
+
break;
|
|
2732
|
+
case "add-plugin":
|
|
2733
|
+
await cmdEnvAddPlugin();
|
|
2734
|
+
break;
|
|
2735
|
+
default:
|
|
2736
|
+
printBanner();
|
|
2737
|
+
print(" Usage: npx sticky-note env <subcommand>\n");
|
|
2738
|
+
print(" Subcommands:");
|
|
2739
|
+
print(" status Show what's provisioned vs missing vs needs-secrets");
|
|
2740
|
+
print(" add-server Interactively add an MCP server to the manifest");
|
|
2741
|
+
print(" add-plugin Interactively add a plugin to the manifest");
|
|
2742
|
+
print("");
|
|
2743
|
+
break;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function cmdEnvStatus() {
|
|
2748
|
+
const cwd = process.cwd();
|
|
2749
|
+
const envDir = path.join(cwd, ".sticky-note", "environment");
|
|
2750
|
+
const manifestPath = path.join(envDir, "manifest.json");
|
|
2751
|
+
const mcpJsonPath = path.join(cwd, ".mcp.json");
|
|
2752
|
+
|
|
2753
|
+
printBanner();
|
|
2754
|
+
print(" 📊 Environment Status\n");
|
|
2755
|
+
|
|
2756
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2757
|
+
print(" No environment manifest found. Run `npx sticky-note init` first.");
|
|
2758
|
+
print("");
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
const manifest = readJsonSafe(manifestPath, {});
|
|
2763
|
+
const mcpConfig = readJsonSafe(mcpJsonPath, { mcpServers: {} });
|
|
2764
|
+
const registeredServers = Object.keys(mcpConfig.mcpServers || {});
|
|
2765
|
+
|
|
2766
|
+
// MCP servers status
|
|
2767
|
+
const mcpServers = manifest.mcp_servers || {};
|
|
2768
|
+
const serverNames = Object.keys(mcpServers);
|
|
2769
|
+
|
|
2770
|
+
if (serverNames.length === 0) {
|
|
2771
|
+
print(" MCP Servers: (none defined)");
|
|
2772
|
+
} else {
|
|
2773
|
+
print(" MCP Servers:");
|
|
2774
|
+
for (const name of serverNames) {
|
|
2775
|
+
const config = mcpServers[name];
|
|
2776
|
+
const isProvisioned = registeredServers.includes(name);
|
|
2777
|
+
|
|
2778
|
+
if (isProvisioned) {
|
|
2779
|
+
print(` ✅ ${name} — provisioned`);
|
|
2780
|
+
} else {
|
|
2781
|
+
// Check if it needs secrets
|
|
2782
|
+
const envEntries = config.env || {};
|
|
2783
|
+
const needsSecrets = Object.values(envEntries).some(
|
|
2784
|
+
(val) => typeof val === "string" && val.startsWith("${") && val.endsWith("}")
|
|
2785
|
+
);
|
|
2786
|
+
if (needsSecrets) {
|
|
2787
|
+
print(` 🔑 ${name} — needs secrets (run \`npx sticky-note bootstrap\`)`);
|
|
2788
|
+
} else {
|
|
2789
|
+
print(` ❌ ${name} — not provisioned`);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// Skills, Agents, Commands counts
|
|
2796
|
+
const skillsDir = path.join(envDir, "skills");
|
|
2797
|
+
const agentsDir = path.join(envDir, "agents");
|
|
2798
|
+
const commandsDir = path.join(envDir, "commands");
|
|
2799
|
+
|
|
2800
|
+
function countMdFiles(dir) {
|
|
2801
|
+
try {
|
|
2802
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
|
|
2803
|
+
} catch {
|
|
2804
|
+
return 0;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
const skillsCount = countMdFiles(skillsDir);
|
|
2809
|
+
const agentsCount = countMdFiles(agentsDir);
|
|
2810
|
+
const commandsCount = countMdFiles(commandsDir);
|
|
2811
|
+
|
|
2812
|
+
print("");
|
|
2813
|
+
print(" Resources:");
|
|
2814
|
+
print(` Skills: ${skillsCount}`);
|
|
2815
|
+
print(` Agents: ${agentsCount}`);
|
|
2816
|
+
print(` Commands: ${commandsCount}`);
|
|
2817
|
+
|
|
2818
|
+
// Env vars status
|
|
2819
|
+
const envVars = manifest.env_vars || {};
|
|
2820
|
+
const envVarNames = Object.keys(envVars);
|
|
2821
|
+
|
|
2822
|
+
if (envVarNames.length > 0) {
|
|
2823
|
+
print("");
|
|
2824
|
+
print(" Environment Variables:");
|
|
2825
|
+
|
|
2826
|
+
// Parse .env file for checking
|
|
2827
|
+
const dotEnvPath = path.join(cwd, ".env");
|
|
2828
|
+
const dotEnvVars = {};
|
|
2829
|
+
try {
|
|
2830
|
+
const content = fs.readFileSync(dotEnvPath, "utf-8");
|
|
2831
|
+
for (const line of content.split("\n")) {
|
|
2832
|
+
const trimmed = line.trim();
|
|
2833
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2834
|
+
const eqIdx = trimmed.indexOf("=");
|
|
2835
|
+
if (eqIdx === -1) continue;
|
|
2836
|
+
dotEnvVars[trimmed.slice(0, eqIdx).trim()] = true;
|
|
2837
|
+
}
|
|
2838
|
+
} catch {
|
|
2839
|
+
// no .env
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
for (const varName of envVarNames) {
|
|
2843
|
+
const meta = envVars[varName];
|
|
2844
|
+
const inEnv = !!process.env[varName];
|
|
2845
|
+
const inDotEnv = !!dotEnvVars[varName];
|
|
2846
|
+
const reqLabel = meta.required === false ? " (optional)" : "";
|
|
2847
|
+
|
|
2848
|
+
if (inEnv) {
|
|
2849
|
+
print(` ✅ ${varName} — set in environment${reqLabel}`);
|
|
2850
|
+
} else if (inDotEnv) {
|
|
2851
|
+
print(` ✅ ${varName} — set in .env${reqLabel}`);
|
|
2852
|
+
} else {
|
|
2853
|
+
print(` ❌ ${varName} — not found${reqLabel}`);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
print("");
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
async function cmdEnvAddServer() {
|
|
2862
|
+
const cwd = process.cwd();
|
|
2863
|
+
const manifestPath = path.join(cwd, ".sticky-note", "environment", "manifest.json");
|
|
2864
|
+
|
|
2865
|
+
printBanner();
|
|
2866
|
+
print(" ➕ Add MCP Server to Manifest\n");
|
|
2867
|
+
|
|
2868
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2869
|
+
print(" No environment manifest found. Run `npx sticky-note init` first.");
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
2874
|
+
if (!manifest) {
|
|
2875
|
+
print(" [ERR] Failed to parse manifest.json");
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const rl = readline.createInterface({
|
|
2880
|
+
input: process.stdin,
|
|
2881
|
+
output: process.stdout,
|
|
2882
|
+
});
|
|
2883
|
+
|
|
2884
|
+
const name = await ask(rl, "Server name (e.g. github, context7)", "");
|
|
2885
|
+
if (!name) {
|
|
2886
|
+
print(" [ERR] Server name is required.");
|
|
2887
|
+
rl.close();
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
const command = await ask(rl, "Command (e.g. npx, node)", "npx");
|
|
2892
|
+
const argsRaw = await ask(rl, "Args (comma-separated)", "");
|
|
2893
|
+
const description = await ask(rl, "Description", "");
|
|
2894
|
+
const requiredRaw = await ask(rl, "Required?", "yes");
|
|
2895
|
+
const required = requiredRaw.toLowerCase() === "yes" || requiredRaw.toLowerCase() === "y";
|
|
2896
|
+
|
|
2897
|
+
const args = argsRaw
|
|
2898
|
+
? argsRaw.split(",").map((a) => a.trim()).filter(Boolean)
|
|
2899
|
+
: [];
|
|
2900
|
+
|
|
2901
|
+
const serverConfig = {
|
|
2902
|
+
type: "stdio",
|
|
2903
|
+
command,
|
|
2904
|
+
args,
|
|
2905
|
+
description,
|
|
2906
|
+
required,
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// Ask about env vars
|
|
2910
|
+
const needsEnv = await ask(rl, "Does this server need environment variables?", "no");
|
|
2911
|
+
if (needsEnv.toLowerCase() === "yes" || needsEnv.toLowerCase() === "y") {
|
|
2912
|
+
serverConfig.env = {};
|
|
2913
|
+
let addMore = true;
|
|
2914
|
+
while (addMore) {
|
|
2915
|
+
const envName = await ask(rl, " Env var name (e.g. GITHUB_TOKEN)", "");
|
|
2916
|
+
if (!envName) break;
|
|
2917
|
+
|
|
2918
|
+
const envDesc = await ask(rl, " Description", "");
|
|
2919
|
+
const envDocs = await ask(rl, " Docs URL (optional)", "");
|
|
2920
|
+
const envReqRaw = await ask(rl, " Required?", "yes");
|
|
2921
|
+
const envRequired = envReqRaw.toLowerCase() === "yes" || envReqRaw.toLowerCase() === "y";
|
|
2922
|
+
|
|
2923
|
+
// Add placeholder to server env
|
|
2924
|
+
serverConfig.env[envName] = `\${${envName}}`;
|
|
2925
|
+
|
|
2926
|
+
// Add to env_vars metadata
|
|
2927
|
+
manifest.env_vars = manifest.env_vars || {};
|
|
2928
|
+
manifest.env_vars[envName] = {
|
|
2929
|
+
description: envDesc || envName,
|
|
2930
|
+
required: envRequired,
|
|
2931
|
+
};
|
|
2932
|
+
if (envDocs) manifest.env_vars[envName].docs_url = envDocs;
|
|
2933
|
+
|
|
2934
|
+
const moreRaw = await ask(rl, " Add another env var?", "no");
|
|
2935
|
+
addMore = moreRaw.toLowerCase() === "yes" || moreRaw.toLowerCase() === "y";
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
rl.close();
|
|
2940
|
+
|
|
2941
|
+
// Write to manifest
|
|
2942
|
+
manifest.mcp_servers = manifest.mcp_servers || {};
|
|
2943
|
+
manifest.mcp_servers[name] = serverConfig;
|
|
2944
|
+
|
|
2945
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
2946
|
+
|
|
2947
|
+
print("");
|
|
2948
|
+
print(` ✅ Added server "${name}" to manifest.json`);
|
|
2949
|
+
print(` Run \`npx sticky-note bootstrap\` to provision it.`);
|
|
2950
|
+
print("");
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
async function cmdEnvAddPlugin() {
|
|
2954
|
+
const cwd = process.cwd();
|
|
2955
|
+
const manifestPath = path.join(cwd, ".sticky-note", "environment", "manifest.json");
|
|
2956
|
+
|
|
2957
|
+
printBanner();
|
|
2958
|
+
print(" ➕ Add Plugin to Manifest\n");
|
|
2959
|
+
|
|
2960
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2961
|
+
print(" No environment manifest found. Run `npx sticky-note init` first.");
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
2966
|
+
if (!manifest) {
|
|
2967
|
+
print(" [ERR] Failed to parse manifest.json");
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
const rl = readline.createInterface({
|
|
2972
|
+
input: process.stdin,
|
|
2973
|
+
output: process.stdout,
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
const name = await ask(rl, "Plugin name (e.g. docker, eslint-helper)", "");
|
|
2977
|
+
if (!name) {
|
|
2978
|
+
print(" [ERR] Plugin name is required.");
|
|
2979
|
+
rl.close();
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
const source = await ask(rl, "Source (e.g. registry:docker-copilot-ext, github:user/repo)", "");
|
|
2984
|
+
if (!source) {
|
|
2985
|
+
print(" [ERR] Plugin source is required.");
|
|
2986
|
+
rl.close();
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
const description = await ask(rl, "Description", "");
|
|
2991
|
+
const requiredRaw = await ask(rl, "Required?", "no");
|
|
2992
|
+
const required = requiredRaw.toLowerCase() === "yes" || requiredRaw.toLowerCase() === "y";
|
|
2993
|
+
|
|
2994
|
+
rl.close();
|
|
2995
|
+
|
|
2996
|
+
manifest.plugins = manifest.plugins || {};
|
|
2997
|
+
manifest.plugins[name] = {
|
|
2998
|
+
source,
|
|
2999
|
+
description,
|
|
3000
|
+
required,
|
|
3001
|
+
};
|
|
3002
|
+
|
|
3003
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
3004
|
+
|
|
3005
|
+
print("");
|
|
3006
|
+
print(` ✅ Added plugin "${name}" to manifest.json`);
|
|
3007
|
+
print("");
|
|
3008
|
+
}
|
|
3009
|
+
|
|
2433
3010
|
// ──────────────────────────────────────────────
|
|
2434
3011
|
// Main
|
|
2435
3012
|
// ──────────────────────────────────────────────
|
|
@@ -2487,6 +3064,15 @@ async function main() {
|
|
|
2487
3064
|
case "sync":
|
|
2488
3065
|
cmdSync();
|
|
2489
3066
|
break;
|
|
3067
|
+
case "bootstrap":
|
|
3068
|
+
await cmdBootstrap();
|
|
3069
|
+
break;
|
|
3070
|
+
case "env":
|
|
3071
|
+
await cmdEnv();
|
|
3072
|
+
break;
|
|
3073
|
+
case "mcp-server":
|
|
3074
|
+
cmdMcpServer();
|
|
3075
|
+
break;
|
|
2490
3076
|
case "--version":
|
|
2491
3077
|
case "-v":
|
|
2492
3078
|
print(`sticky-note v${VERSION}`);
|
|
@@ -2513,6 +3099,9 @@ async function main() {
|
|
|
2513
3099
|
print(" overlap Detect file overlaps with open/stuck threads");
|
|
2514
3100
|
print(" claim Claim ownership of files you're working on (--list, --clear)");
|
|
2515
3101
|
print(" sync Commit .sticky-note/ changes to git (--push to also push)");
|
|
3102
|
+
print(" bootstrap Provision MCP servers with secrets from manifest");
|
|
3103
|
+
print(" env Environment management (status, add-server, add-plugin)");
|
|
3104
|
+
print(" mcp-server Launch the sticky-note MCP server (stdio JSON-RPC)");
|
|
2516
3105
|
print("");
|
|
2517
3106
|
print(" Options:");
|
|
2518
3107
|
print(" --version Show version");
|