@growthub/cli 0.3.55 → 0.3.57
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 +56 -156
- package/assets/worker-kits/growthub-zernio-social-v1/QUICKSTART.md +209 -0
- package/assets/worker-kits/growthub-zernio-social-v1/brands/NEW-CLIENT.md +74 -0
- package/assets/worker-kits/growthub-zernio-social-v1/brands/_template/brand-kit.md +131 -0
- package/assets/worker-kits/growthub-zernio-social-v1/brands/growthub/brand-kit.md +141 -0
- package/assets/worker-kits/growthub-zernio-social-v1/bundles/growthub-zernio-social-v1.json +55 -0
- package/assets/worker-kits/growthub-zernio-social-v1/docs/ai-caption-layer.md +132 -0
- package/assets/worker-kits/growthub-zernio-social-v1/docs/local-adapters.md +123 -0
- package/assets/worker-kits/growthub-zernio-social-v1/docs/platform-coverage.md +112 -0
- package/assets/worker-kits/growthub-zernio-social-v1/docs/postiz-ui-shell-integration.md +166 -0
- package/assets/worker-kits/growthub-zernio-social-v1/docs/posts-and-queues-layer.md +208 -0
- package/assets/worker-kits/growthub-zernio-social-v1/docs/zernio-api-integration.md +265 -0
- package/assets/worker-kits/growthub-zernio-social-v1/examples/analytics-brief-sample.md +97 -0
- package/assets/worker-kits/growthub-zernio-social-v1/examples/client-proposal-sample.md +106 -0
- package/assets/worker-kits/growthub-zernio-social-v1/examples/content-calendar-sample.md +74 -0
- package/assets/worker-kits/growthub-zernio-social-v1/examples/social-campaign-sample.md +105 -0
- package/assets/worker-kits/growthub-zernio-social-v1/growthub-meta/README.md +146 -0
- package/assets/worker-kits/growthub-zernio-social-v1/growthub-meta/kit-standard.md +120 -0
- package/assets/worker-kits/growthub-zernio-social-v1/kit.json +104 -0
- package/assets/worker-kits/growthub-zernio-social-v1/output/README.md +63 -0
- package/assets/worker-kits/growthub-zernio-social-v1/output-standards.md +132 -0
- package/assets/worker-kits/growthub-zernio-social-v1/runtime-assumptions.md +170 -0
- package/assets/worker-kits/growthub-zernio-social-v1/setup/check-deps.mjs +117 -0
- package/assets/worker-kits/growthub-zernio-social-v1/setup/check-deps.sh +86 -0
- package/assets/worker-kits/growthub-zernio-social-v1/setup/install-mcp.mjs +177 -0
- package/assets/worker-kits/growthub-zernio-social-v1/setup/setup.mjs +247 -0
- package/assets/worker-kits/growthub-zernio-social-v1/setup/verify-env.mjs +138 -0
- package/assets/worker-kits/growthub-zernio-social-v1/skills.md +332 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/analytics-brief.md +101 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/caption-copy-deck.md +105 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/client-proposal.md +98 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/content-calendar.md +70 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/platform-publishing-plan.md +86 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/scheduling-manifest.md +92 -0
- package/assets/worker-kits/growthub-zernio-social-v1/templates/social-campaign-brief.md +102 -0
- package/assets/worker-kits/growthub-zernio-social-v1/validation-checklist.md +85 -0
- package/assets/worker-kits/growthub-zernio-social-v1/workers/zernio-social-operator/CLAUDE.md +307 -0
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// install-mcp.mjs — print the per-IDE Zernio MCP server config JSON
|
|
3
|
+
//
|
|
4
|
+
// This is a print-only helper. It does NOT run `pip install zernio-sdk[mcp]`
|
|
5
|
+
// for you, and it does NOT mutate any config file. It prints the exact
|
|
6
|
+
// copy-paste JSON blocks you need to wire Zernio's official MCP server into
|
|
7
|
+
// each MCP-compatible IDE.
|
|
8
|
+
//
|
|
9
|
+
// Cross-platform (macOS, Linux, Windows). Pure Node stdlib — no deps.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node setup/install-mcp.mjs # print all IDE configs
|
|
13
|
+
// node setup/install-mcp.mjs claude-desktop # one IDE only
|
|
14
|
+
// node setup/install-mcp.mjs cursor
|
|
15
|
+
// node setup/install-mcp.mjs claude-code
|
|
16
|
+
// node setup/install-mcp.mjs generic
|
|
17
|
+
|
|
18
|
+
import { platform } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
|
|
22
|
+
const BOLD = "\x1b[1m";
|
|
23
|
+
const DIM = "\x1b[2m";
|
|
24
|
+
const GREEN = "\x1b[32m";
|
|
25
|
+
const YELLOW = "\x1b[33m";
|
|
26
|
+
const BLUE = "\x1b[34m";
|
|
27
|
+
const RESET = "\x1b[0m";
|
|
28
|
+
|
|
29
|
+
const arg = (process.argv[2] ?? "all").toLowerCase();
|
|
30
|
+
const osPlatform = platform();
|
|
31
|
+
|
|
32
|
+
// ---- Config path hints (cross-platform) ----
|
|
33
|
+
function claudeDesktopConfigPath() {
|
|
34
|
+
const home = homedir();
|
|
35
|
+
if (osPlatform === "darwin") return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
36
|
+
if (osPlatform === "win32") return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
37
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function claudeCodeConfigPath() {
|
|
41
|
+
return join(homedir(), ".claude", "mcp.json");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cursorConfigPath() {
|
|
45
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- MCP server definition ----
|
|
49
|
+
// Zernio's official MCP server ships inside the zernio-python SDK:
|
|
50
|
+
// pip install zernio-sdk[mcp]
|
|
51
|
+
// The server entrypoint is the Python module. Auth is via ZERNIO_API_KEY env var.
|
|
52
|
+
const ZERNIO_MCP_BLOCK = {
|
|
53
|
+
command: "python",
|
|
54
|
+
args: ["-m", "zernio.mcp"],
|
|
55
|
+
env: {
|
|
56
|
+
ZERNIO_API_KEY: "${ZERNIO_API_KEY}",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function header(label) {
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log(`${BOLD}${BLUE}${label}${RESET}`);
|
|
63
|
+
console.log("-".repeat(Math.max(24, label.length)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function note(msg) {
|
|
67
|
+
console.log(`${DIM}${msg}${RESET}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function codeBlock(obj) {
|
|
71
|
+
console.log("```json");
|
|
72
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
73
|
+
console.log("```");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printPreamble() {
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(`${BOLD}Zernio MCP Server — per-IDE config${RESET}`);
|
|
79
|
+
note(`host: ${osPlatform} · pre-req: pip install zernio-sdk[mcp] · env: ZERNIO_API_KEY`);
|
|
80
|
+
console.log("");
|
|
81
|
+
console.log(`${GREEN}1.${RESET} Install Zernio's MCP server (one-time):`);
|
|
82
|
+
console.log(` ${BOLD}pip install zernio-sdk[mcp]${RESET}`);
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log(`${GREEN}2.${RESET} Ensure ZERNIO_API_KEY is set in your shell profile (or pass it via the IDE):`);
|
|
85
|
+
if (osPlatform === "win32") {
|
|
86
|
+
console.log(` ${DIM}setx ZERNIO_API_KEY "sk_..."${RESET} ${DIM}# PowerShell, persistent${RESET}`);
|
|
87
|
+
} else {
|
|
88
|
+
console.log(` ${DIM}echo 'export ZERNIO_API_KEY="sk_..."' >> ~/.zshrc${RESET} ${DIM}# or ~/.bashrc${RESET}`);
|
|
89
|
+
}
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(`${GREEN}3.${RESET} Paste one of the JSON blocks below into the matching IDE's MCP config.`);
|
|
92
|
+
console.log("");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function printClaudeDesktop() {
|
|
96
|
+
header("Claude Desktop");
|
|
97
|
+
note(`Config file: ${claudeDesktopConfigPath()}`);
|
|
98
|
+
console.log(`Add the ${BOLD}zernio${RESET} server under the top-level ${BOLD}mcpServers${RESET} object:`);
|
|
99
|
+
console.log("");
|
|
100
|
+
codeBlock({
|
|
101
|
+
mcpServers: {
|
|
102
|
+
zernio: ZERNIO_MCP_BLOCK,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
console.log(`${YELLOW}Restart Claude Desktop${RESET} after editing the config.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printClaudeCode() {
|
|
109
|
+
header("Claude Code");
|
|
110
|
+
note(`Config file: ${claudeCodeConfigPath()}`);
|
|
111
|
+
console.log(`Add to ${BOLD}.claude/mcp.json${RESET} (or use ${BOLD}claude mcp add${RESET}):`);
|
|
112
|
+
console.log("");
|
|
113
|
+
codeBlock({
|
|
114
|
+
mcpServers: {
|
|
115
|
+
zernio: ZERNIO_MCP_BLOCK,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
console.log(`Alternative one-liner:`);
|
|
119
|
+
console.log(` ${BOLD}claude mcp add zernio -- python -m zernio.mcp${RESET}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printCursor() {
|
|
123
|
+
header("Cursor");
|
|
124
|
+
note(`Config file: ${cursorConfigPath()}`);
|
|
125
|
+
console.log(`Add the ${BOLD}zernio${RESET} entry under the top-level ${BOLD}mcpServers${RESET} object:`);
|
|
126
|
+
console.log("");
|
|
127
|
+
codeBlock({
|
|
128
|
+
mcpServers: {
|
|
129
|
+
zernio: ZERNIO_MCP_BLOCK,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
console.log(`${YELLOW}Restart Cursor${RESET} after editing the config.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printGeneric() {
|
|
136
|
+
header("Generic MCP-compatible IDE");
|
|
137
|
+
note("Use this shape for any IDE that speaks MCP (Codex-mcp, n8n MCP nodes, custom clients).");
|
|
138
|
+
console.log("");
|
|
139
|
+
codeBlock({
|
|
140
|
+
mcpServers: {
|
|
141
|
+
zernio: ZERNIO_MCP_BLOCK,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
console.log("Adjust the top-level key if your IDE uses a different name (e.g. `servers` vs `mcpServers`).");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printFooter() {
|
|
148
|
+
console.log("");
|
|
149
|
+
header("What this enables");
|
|
150
|
+
console.log("- The agent can call Zernio endpoints as typed MCP tools instead of raw HTTP.");
|
|
151
|
+
console.log("- Works alongside this kit's Working Directory pattern — strictly additive.");
|
|
152
|
+
console.log("- All writes still use Idempotency-Key = clientPostId for safe re-submission.");
|
|
153
|
+
console.log("- See docs/local-adapters.md for the full matrix of supported IDEs.");
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(`${DIM}Zernio's MCP server lives in the zernio-python SDK:${RESET}`);
|
|
156
|
+
console.log(` ${DIM}https://github.com/zernio-dev/zernio-python${RESET}`);
|
|
157
|
+
console.log("");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- Dispatch ----
|
|
161
|
+
printPreamble();
|
|
162
|
+
|
|
163
|
+
switch (arg) {
|
|
164
|
+
case "claude-desktop": printClaudeDesktop(); break;
|
|
165
|
+
case "claude-code": printClaudeCode(); break;
|
|
166
|
+
case "cursor": printCursor(); break;
|
|
167
|
+
case "generic": printGeneric(); break;
|
|
168
|
+
case "all":
|
|
169
|
+
default:
|
|
170
|
+
printClaudeDesktop();
|
|
171
|
+
printClaudeCode();
|
|
172
|
+
printCursor();
|
|
173
|
+
printGeneric();
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
printFooter();
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// setup.mjs — one-command cross-platform bootstrap for the Zernio Social Media Studio
|
|
3
|
+
//
|
|
4
|
+
// Works identically on macOS, Linux, and Windows (PowerShell, cmd, WSL, git-bash).
|
|
5
|
+
// Usage:
|
|
6
|
+
// node setup/setup.mjs # full bootstrap (deps + env + verify)
|
|
7
|
+
// node setup/setup.mjs --skip-deps # skip dependency check
|
|
8
|
+
// node setup/setup.mjs --skip-verify # skip live API reachability check
|
|
9
|
+
// node setup/setup.mjs --yes # no-prompt mode (CI-safe)
|
|
10
|
+
//
|
|
11
|
+
// What it does (in order):
|
|
12
|
+
// 1. Prints the kit banner and detects the host OS.
|
|
13
|
+
// 2. Runs the cross-platform dependency check (Node 18+, curl optional, git optional).
|
|
14
|
+
// 3. If .env is missing, copies .env.example to .env verbatim.
|
|
15
|
+
// 4. Runs verify-env.mjs to confirm ZERNIO_API_KEY format + reachability.
|
|
16
|
+
// 5. Prints the exact next step for the user's OS.
|
|
17
|
+
//
|
|
18
|
+
// No external dependencies. Pure Node stdlib only.
|
|
19
|
+
|
|
20
|
+
import { existsSync, copyFileSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { join, dirname, resolve } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
import { platform, release } from "node:os";
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = dirname(__filename);
|
|
28
|
+
const KIT_ROOT = resolve(__dirname, "..");
|
|
29
|
+
|
|
30
|
+
const argv = new Set(process.argv.slice(2));
|
|
31
|
+
const SKIP_DEPS = argv.has("--skip-deps");
|
|
32
|
+
const SKIP_VERIFY = argv.has("--skip-verify");
|
|
33
|
+
const YES = argv.has("--yes");
|
|
34
|
+
|
|
35
|
+
const RED = "\x1b[31m";
|
|
36
|
+
const GREEN = "\x1b[32m";
|
|
37
|
+
const YELLOW = "\x1b[33m";
|
|
38
|
+
const BLUE = "\x1b[34m";
|
|
39
|
+
const BOLD = "\x1b[1m";
|
|
40
|
+
const DIM = "\x1b[2m";
|
|
41
|
+
const RESET = "\x1b[0m";
|
|
42
|
+
|
|
43
|
+
function log(msg) { console.log(msg); }
|
|
44
|
+
function step(n, label) { log(""); log(`${BOLD}${BLUE}[${n}/4]${RESET} ${BOLD}${label}${RESET}`); }
|
|
45
|
+
function ok(msg) { log(` ${GREEN}\u2713${RESET} ${msg}`); }
|
|
46
|
+
function warn(msg) { log(` ${YELLOW}!${RESET} ${msg}`); }
|
|
47
|
+
function fail(msg) { log(` ${RED}\u2717${RESET} ${msg}`); }
|
|
48
|
+
|
|
49
|
+
function detectOs() {
|
|
50
|
+
const p = platform();
|
|
51
|
+
if (p === "darwin") return { id: "mac", pretty: "macOS" };
|
|
52
|
+
if (p === "win32") return { id: "windows", pretty: "Windows" };
|
|
53
|
+
if (p === "linux") return { id: "linux", pretty: "Linux" };
|
|
54
|
+
return { id: p, pretty: p };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function run(cmd, args, opts = {}) {
|
|
58
|
+
return new Promise((resolvePromise) => {
|
|
59
|
+
const child = spawn(cmd, args, { stdio: "inherit", shell: false, ...opts });
|
|
60
|
+
child.on("close", (code) => resolvePromise(code ?? 1));
|
|
61
|
+
child.on("error", () => resolvePromise(1));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printBanner(osInfo) {
|
|
66
|
+
log("");
|
|
67
|
+
log(`${BOLD}Growthub Zernio Social Media Studio${RESET} \u2014 one-command setup`);
|
|
68
|
+
log(`${DIM}kit: growthub-zernio-social-v1 \u00b7 host: ${osInfo.pretty} (${platform()} ${release()}) \u00b7 node: ${process.version}${RESET}`);
|
|
69
|
+
log("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Step 1 \u2014 Dependency check (cross-platform, node-native)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function checkNodeVersion() {
|
|
77
|
+
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
78
|
+
if (!Number.isFinite(major) || major < 18) {
|
|
79
|
+
fail(`Node.js ${process.version} detected \u2014 Node 18+ required. Install from https://nodejs.org`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
ok(`Node.js ${process.version} (>= 18 required)`);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function which(binary) {
|
|
87
|
+
// Cross-platform "which": spawn a lookup with proper extension handling.
|
|
88
|
+
return new Promise((resolvePromise) => {
|
|
89
|
+
const isWin = platform() === "win32";
|
|
90
|
+
const lookup = isWin ? "where" : "which";
|
|
91
|
+
const child = spawn(lookup, [binary], { shell: false });
|
|
92
|
+
let out = "";
|
|
93
|
+
child.stdout?.on("data", (d) => (out += String(d)));
|
|
94
|
+
child.on("close", (code) => resolvePromise(code === 0 ? out.trim().split(/\r?\n/)[0] : null));
|
|
95
|
+
child.on("error", () => resolvePromise(null));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function runDepsCheck() {
|
|
100
|
+
step(1, "Checking dependencies");
|
|
101
|
+
if (SKIP_DEPS) {
|
|
102
|
+
warn("--skip-deps passed \u2014 skipping dependency check");
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
let allGood = true;
|
|
106
|
+
if (!checkNodeVersion()) allGood = false;
|
|
107
|
+
|
|
108
|
+
const curlPath = await which("curl");
|
|
109
|
+
if (curlPath) ok(`curl at ${curlPath}`);
|
|
110
|
+
else warn("curl not found \u2014 not required (Node's built-in fetch is used), but recommended for manual API testing");
|
|
111
|
+
|
|
112
|
+
const gitPath = await which("git");
|
|
113
|
+
if (gitPath) ok(`git at ${gitPath}`);
|
|
114
|
+
else warn("git not found \u2014 not required for the kit, but recommended for your own project versioning");
|
|
115
|
+
|
|
116
|
+
return allGood;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Step 2 \u2014 Ensure .env exists
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function ensureEnvFile() {
|
|
124
|
+
step(2, "Ensuring .env is in place");
|
|
125
|
+
const envPath = join(KIT_ROOT, ".env");
|
|
126
|
+
const examplePath = join(KIT_ROOT, ".env.example");
|
|
127
|
+
|
|
128
|
+
if (existsSync(envPath)) {
|
|
129
|
+
const size = statSync(envPath).size;
|
|
130
|
+
ok(`.env already exists (${size} bytes) \u2014 not overwriting`);
|
|
131
|
+
return { created: false, envPath };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (existsSync(examplePath)) {
|
|
135
|
+
copyFileSync(examplePath, envPath);
|
|
136
|
+
ok(`Copied .env.example \u2192 .env`);
|
|
137
|
+
warn(`Open .env and fill in ZERNIO_API_KEY + ZERNIO_PROFILE_ID before running the operator`);
|
|
138
|
+
return { created: true, envPath };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// No .env.example \u2014 guide the user to create .env directly.
|
|
142
|
+
warn(".env not found \u2014 create it with your Zernio credentials:");
|
|
143
|
+
log(` ${DIM}ZERNIO_API_KEY=sk_<your-64-hex-key>${RESET}`);
|
|
144
|
+
log(` ${DIM}ZERNIO_API_URL=https://zernio.com/api/v1${RESET}`);
|
|
145
|
+
log(` ${DIM}ZERNIO_PROFILE_ID=<your-profile-id>${RESET}`);
|
|
146
|
+
log(` ${DIM}ZERNIO_TIMEZONE=America/New_York # optional${RESET}`);
|
|
147
|
+
log(` ${DIM}ANTHROPIC_API_KEY=sk-ant-... # optional, for hybrid caption mode${RESET}`);
|
|
148
|
+
log("");
|
|
149
|
+
log(` Skip env entirely for agent-only mode: ${BOLD}node setup/setup.mjs --skip-verify${RESET}`);
|
|
150
|
+
return { created: false, envPath: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Step 3 \u2014 Run verify-env.mjs (same file we already ship)
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
async function runVerifyEnv() {
|
|
158
|
+
step(3, "Verifying environment (format + reachability)");
|
|
159
|
+
if (SKIP_VERIFY) {
|
|
160
|
+
warn("--skip-verify passed \u2014 skipping environment verification");
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
loadDotEnvInto(process.env);
|
|
164
|
+
const verifyPath = join(__dirname, "verify-env.mjs");
|
|
165
|
+
if (!existsSync(verifyPath)) {
|
|
166
|
+
fail("setup/verify-env.mjs missing \u2014 kit payload is incomplete");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const code = await run(process.execPath, [verifyPath]);
|
|
170
|
+
// verify-env.mjs returns 0 even on warnings; non-zero only if it crashes
|
|
171
|
+
return code === 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Minimal inline .env loader \u2014 no dotenv dependency.
|
|
175
|
+
function loadDotEnvInto(target) {
|
|
176
|
+
const envPath = join(KIT_ROOT, ".env");
|
|
177
|
+
if (!existsSync(envPath)) return;
|
|
178
|
+
const raw = readFileSync(envPath, "utf8");
|
|
179
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
180
|
+
const line = rawLine.trim();
|
|
181
|
+
if (!line || line.startsWith("#")) continue;
|
|
182
|
+
const eq = line.indexOf("=");
|
|
183
|
+
if (eq < 0) continue;
|
|
184
|
+
const key = line.slice(0, eq).trim();
|
|
185
|
+
let value = line.slice(eq + 1).trim();
|
|
186
|
+
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
187
|
+
value = value.slice(1, -1);
|
|
188
|
+
}
|
|
189
|
+
if (target[key] === undefined) target[key] = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Step 4 \u2014 Print OS-specific next steps
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function printNextSteps(osInfo, envCreated) {
|
|
198
|
+
step(4, "Next steps");
|
|
199
|
+
const open = osInfo.id === "mac" ? "open" : osInfo.id === "windows" ? "start" : "xdg-open";
|
|
200
|
+
if (envCreated) {
|
|
201
|
+
log(` 1. Fill in your Zernio key in .env:`);
|
|
202
|
+
if (osInfo.id === "windows") {
|
|
203
|
+
log(` ${DIM}notepad .env${RESET} (or any editor)`);
|
|
204
|
+
log(` ${DIM}code .env${RESET} (VS Code)`);
|
|
205
|
+
} else {
|
|
206
|
+
log(` ${DIM}${open} .env${RESET} (system default editor)`);
|
|
207
|
+
log(` ${DIM}nano .env${RESET} (terminal editor)`);
|
|
208
|
+
log(` ${DIM}code .env${RESET} (VS Code)`);
|
|
209
|
+
}
|
|
210
|
+
log(` 2. Re-run: ${BOLD}node setup/setup.mjs${RESET} to re-verify.`);
|
|
211
|
+
log(` 3. Then open Claude Code / Codex / Cursor at this folder as the Working Directory.`);
|
|
212
|
+
} else {
|
|
213
|
+
log(` 1. Open Claude Code / Codex / Cursor.`);
|
|
214
|
+
log(` 2. Point the Working Directory at:`);
|
|
215
|
+
log(` ${BOLD}${KIT_ROOT}${RESET}`);
|
|
216
|
+
log(` 3. The ${BOLD}zernio-social-operator${RESET} agent entrypoint is:`);
|
|
217
|
+
log(` ${DIM}workers/zernio-social-operator/CLAUDE.md${RESET}`);
|
|
218
|
+
log(` 4. Try: ${BOLD}"/zernio campaign"${RESET} to run the full 10-step workflow.`);
|
|
219
|
+
}
|
|
220
|
+
log("");
|
|
221
|
+
log(`${DIM}For full setup guidance see QUICKSTART.md. Agent-only mode is always valid \u2014 Zernio reachability does not block planning.${RESET}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Main
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
async function main() {
|
|
229
|
+
const osInfo = detectOs();
|
|
230
|
+
printBanner(osInfo);
|
|
231
|
+
|
|
232
|
+
const depsOk = await runDepsCheck();
|
|
233
|
+
const envResult = ensureEnvFile();
|
|
234
|
+
await runVerifyEnv();
|
|
235
|
+
printNextSteps(osInfo, envResult.created);
|
|
236
|
+
|
|
237
|
+
if (!depsOk) {
|
|
238
|
+
log("");
|
|
239
|
+
log(`${RED}${BOLD}One or more dependency checks failed. Fix the issues above and re-run.${RESET}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main().catch((err) => {
|
|
245
|
+
console.error(err);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// verify-env.mjs — Verify Zernio API key + profile + reachability for the kit
|
|
3
|
+
// Usage: node setup/verify-env.mjs
|
|
4
|
+
|
|
5
|
+
const RED = "\x1b[31m";
|
|
6
|
+
const GREEN = "\x1b[32m";
|
|
7
|
+
const YELLOW = "\x1b[33m";
|
|
8
|
+
const RESET = "\x1b[0m";
|
|
9
|
+
|
|
10
|
+
let allPassed = true;
|
|
11
|
+
const results = [];
|
|
12
|
+
|
|
13
|
+
function check(label, passed, detail = "") {
|
|
14
|
+
const icon = passed ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
15
|
+
results.push({ label, passed, detail, icon });
|
|
16
|
+
if (!passed) allPassed = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function warn(label, detail = "") {
|
|
20
|
+
results.push({ label, passed: null, detail, icon: `${YELLOW}⚠${RESET}` });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log("Growthub Zernio Social Media Studio — Environment Verification");
|
|
24
|
+
console.log("=".repeat(64));
|
|
25
|
+
console.log("");
|
|
26
|
+
|
|
27
|
+
// --- Check 1: ZERNIO_API_KEY format ---
|
|
28
|
+
const ZERNIO_KEY_RE = /^sk_[0-9a-fA-F]{64}$/;
|
|
29
|
+
const zernioKey = process.env.ZERNIO_API_KEY;
|
|
30
|
+
let zernioKeyValid = false;
|
|
31
|
+
if (!zernioKey || zernioKey === "your_zernio_api_key_here") {
|
|
32
|
+
warn("ZERNIO_API_KEY not set", "Set it in .env. Agent-only mode is still available.");
|
|
33
|
+
} else if (!ZERNIO_KEY_RE.test(zernioKey)) {
|
|
34
|
+
check("ZERNIO_API_KEY format is valid", false, "Key must match sk_ + 64 hex characters");
|
|
35
|
+
} else {
|
|
36
|
+
check("ZERNIO_API_KEY format is valid", true, zernioKey.slice(0, 8) + "...");
|
|
37
|
+
zernioKeyValid = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Check 2: ZERNIO_API_URL present ---
|
|
41
|
+
const zernioApiUrl = process.env.ZERNIO_API_URL ?? "https://zernio.com/api/v1";
|
|
42
|
+
check("ZERNIO_API_URL is set", Boolean(zernioApiUrl), zernioApiUrl);
|
|
43
|
+
|
|
44
|
+
// --- Check 3: Zernio API reachable (only if a plausible key exists) ---
|
|
45
|
+
let apiReachable = false;
|
|
46
|
+
if (zernioKeyValid) {
|
|
47
|
+
try {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
50
|
+
const res = await fetch(`${zernioApiUrl}/profiles`, {
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
headers: { Authorization: `Bearer ${zernioKey}` },
|
|
53
|
+
});
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
apiReachable = res.ok;
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
check("Zernio API accepts the key", false, `HTTP ${res.status} from GET ${zernioApiUrl}/profiles`);
|
|
58
|
+
} else {
|
|
59
|
+
check("Zernio API accepts the key", true, `HTTP 200 from GET ${zernioApiUrl}/profiles`);
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
check("Zernio API is reachable", false, `Could not reach ${zernioApiUrl} — ${String(err?.message ?? err).slice(0, 120)}`);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
warn("Zernio API reachability skipped", "No valid ZERNIO_API_KEY — agent-only mode only");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Check 4: ZERNIO_PROFILE_ID ---
|
|
69
|
+
const profileId = process.env.ZERNIO_PROFILE_ID;
|
|
70
|
+
if (!profileId || profileId === "your_zernio_profile_id_here") {
|
|
71
|
+
warn("ZERNIO_PROFILE_ID not set", "Required for scheduling — find it in the Zernio dashboard or via GET /api/v1/profiles");
|
|
72
|
+
} else if (zernioKeyValid && apiReachable) {
|
|
73
|
+
try {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
76
|
+
const res = await fetch(`${zernioApiUrl}/profiles/${encodeURIComponent(profileId)}`, {
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
headers: { Authorization: `Bearer ${zernioKey}` },
|
|
79
|
+
});
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
if (res.ok) {
|
|
82
|
+
check("ZERNIO_PROFILE_ID exists on the account", true, profileId.slice(0, 16) + "...");
|
|
83
|
+
} else if (res.status === 404) {
|
|
84
|
+
check("ZERNIO_PROFILE_ID exists on the account", false, "Profile not found for this API key");
|
|
85
|
+
} else {
|
|
86
|
+
warn("ZERNIO_PROFILE_ID lookup returned non-2xx", `HTTP ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
warn("ZERNIO_PROFILE_ID lookup failed", String(err?.message ?? err).slice(0, 120));
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
check("ZERNIO_PROFILE_ID is set", true, "Format check only — API not reached");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Check 5: ZERNIO_TIMEZONE plausibility (optional) ---
|
|
96
|
+
const tz = process.env.ZERNIO_TIMEZONE;
|
|
97
|
+
if (tz) {
|
|
98
|
+
// minimum sanity: looks like "Area/City" or a UTC offset tag
|
|
99
|
+
const plausible = /^[A-Za-z]+\/[A-Za-z_]+/.test(tz) || /^UTC[+-]?\d*$/i.test(tz);
|
|
100
|
+
if (plausible) {
|
|
101
|
+
check("ZERNIO_TIMEZONE format is plausible", true, tz);
|
|
102
|
+
} else {
|
|
103
|
+
check("ZERNIO_TIMEZONE format is plausible", false, `Unexpected value: ${tz}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Check 6: Anthropic API key (optional) ---
|
|
108
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
109
|
+
if (!anthropicKey || anthropicKey === "your_anthropic_key_here") {
|
|
110
|
+
warn("ANTHROPIC_API_KEY not set", "Optional — used only for enhanced caption drafting in hybrid mode");
|
|
111
|
+
} else if (anthropicKey.startsWith("sk-ant-")) {
|
|
112
|
+
check("ANTHROPIC_API_KEY format is valid", true);
|
|
113
|
+
} else {
|
|
114
|
+
check("ANTHROPIC_API_KEY format is valid", false, "Key does not start with sk-ant- — check your Anthropic console");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Print results ---
|
|
118
|
+
console.log("Results:");
|
|
119
|
+
for (const { icon, label, detail } of results) {
|
|
120
|
+
const detailStr = detail ? ` → ${detail}` : "";
|
|
121
|
+
console.log(` ${icon} ${label}${detailStr}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log("");
|
|
125
|
+
|
|
126
|
+
if (allPassed) {
|
|
127
|
+
console.log(`${GREEN}All checks passed. Ready for api-live or hybrid mode.${RESET}`);
|
|
128
|
+
} else {
|
|
129
|
+
const warnings = results.filter((r) => r.passed === null);
|
|
130
|
+
const failures = results.filter((r) => r.passed === false);
|
|
131
|
+
if (failures.length > 0) {
|
|
132
|
+
console.log(`${RED}${failures.length} check(s) failed. See details above.${RESET}`);
|
|
133
|
+
console.log(`\n Agent-only mode remains available for campaign planning and dry-run manifests.`);
|
|
134
|
+
}
|
|
135
|
+
if (warnings.length > 0 && failures.length === 0) {
|
|
136
|
+
console.log(`${YELLOW}Checks passed with warnings. Agent-only mode is available.${RESET}`);
|
|
137
|
+
}
|
|
138
|
+
}
|