hearth-dash 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AIDHD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,26 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { banner, bold, dim, cyan, warn } from "../lib/ui.js";
5
+
6
+ const CONFIG_PATH = join(homedir(), ".hearth-dash", "config.json");
7
+
8
+ export default async function configCommand(args) {
9
+ banner();
10
+
11
+ if (!existsSync(CONFIG_PATH)) {
12
+ warn("No config found. Run 'hearth-dash deploy' first.");
13
+ process.exit(1);
14
+ }
15
+
16
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
17
+
18
+ console.log(bold(" Current Configuration\n"));
19
+ console.log(` ${bold("Dashboard URL:")} ${cyan(config.workerUrl || "not set")}`);
20
+ console.log(` ${bold("Partner 1:")} ${config.partner1 || "not set"}`);
21
+ console.log(` ${bold("Partner 2:")} ${config.partner2 || "not set"}`);
22
+ console.log(` ${bold("Database ID:")} ${config.dbId ? config.dbId.substring(0, 8) + "..." : "not set"}`);
23
+ console.log(` ${bold("MCP Secret:")} ${config.mcpSecret ? config.mcpSecret.substring(0, 6) + "..." : "not set"}`);
24
+ console.log(` ${bold("Deployed:")} ${config.deployedAt || "never"}`);
25
+ console.log(`\n ${dim("Config file: " + CONFIG_PATH)}\n`);
26
+ }
@@ -0,0 +1,216 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { randomBytes } from "node:crypto";
5
+ import { getPackageRoot, getNodeMajor } from "../lib/platform.js";
6
+ import { ask, confirm, password } from "../lib/prompts.js";
7
+ import {
8
+ execWrangler, parseD1CreateOutput, parseDeployOutput,
9
+ checkWranglerAuth, wranglerLogin, setSecret, executeSchema, listD1Databases,
10
+ } from "../lib/wrangler.js";
11
+ import { banner, step, bold, dim, cyan, green, yellow, red, success, fail, warn, info, spinner } from "../lib/ui.js";
12
+
13
+ const TOTAL_STEPS = 6;
14
+ const CONFIG_DIR = join(homedir(), ".hearth-dash");
15
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
16
+
17
+ function saveConfig(data) {
18
+ mkdirSync(CONFIG_DIR, { recursive: true });
19
+ let existing = {};
20
+ if (existsSync(CONFIG_PATH)) {
21
+ try { existing = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")); } catch {}
22
+ }
23
+ const merged = { ...existing, ...data };
24
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
25
+ return merged;
26
+ }
27
+
28
+ function genSecret() {
29
+ return randomBytes(16).toString("base64url");
30
+ }
31
+
32
+ async function createD1OrReuse(dbName, cwd) {
33
+ const result = await execWrangler(["d1", "create", dbName], cwd);
34
+ if (result.code === 0) return parseD1CreateOutput(result);
35
+ const combined = result.stdout + result.stderr;
36
+ if (combined.includes("already exists") || combined.includes("already a database")) {
37
+ warn(`Database "${dbName}" already exists. Looking up ID...`);
38
+ const databases = await listD1Databases(cwd);
39
+ const db = databases.find((d) => d.name === dbName);
40
+ if (db) { info(`Found: ${db.uuid}`); return db.uuid; }
41
+ fail("Could not find existing database ID.");
42
+ return null;
43
+ }
44
+ fail(`Failed to create database: ${combined}`);
45
+ return null;
46
+ }
47
+
48
+ export default async function deployCommand(args) {
49
+ banner();
50
+ console.log(bold(" Full Cloudflare Deployment\n"));
51
+
52
+ // ── Step 1: Preflight ────────────────────────────────────
53
+
54
+ step(1, TOTAL_STEPS, "Preflight checks");
55
+
56
+ const nodeMajor = getNodeMajor();
57
+ if (nodeMajor < 18) { fail(`Node.js ${nodeMajor} detected. Need >= 18.`); process.exit(1); }
58
+ success(`Node.js ${process.version}`);
59
+
60
+ const wranglerCheck = await execWrangler(["--version"], ".");
61
+ if (wranglerCheck.code !== 0) {
62
+ fail("Wrangler not found. Install: npm install -g wrangler");
63
+ process.exit(1);
64
+ }
65
+ success(`Wrangler installed`);
66
+
67
+ const authed = await checkWranglerAuth(".");
68
+ if (!authed) {
69
+ warn("Not logged into Cloudflare. Opening browser...");
70
+ const loginOk = await wranglerLogin(".");
71
+ if (!loginOk) { fail("Cloudflare login failed."); process.exit(1); }
72
+ }
73
+ success("Cloudflare authenticated");
74
+
75
+ // ── Step 2: Configuration ────────────────────────────────
76
+
77
+ step(2, TOTAL_STEPS, "Configuration");
78
+
79
+ const partner1 = await ask(" Partner 1 name", "Partner 1");
80
+ const partner2 = await ask(" Partner 2 name (or AI name)", "AI");
81
+ const dashPassword = await password(" Dashboard password");
82
+ if (!dashPassword) { fail("Password is required."); process.exit(1); }
83
+ const mcpSecret = genSecret();
84
+ success("MCP secret generated");
85
+
86
+ console.log();
87
+ info("Weather requires a free OpenWeatherMap API key.");
88
+ info("Get one at: https://openweathermap.org/api");
89
+ const weatherKey = await ask(" OpenWeatherMap API key (or skip)");
90
+ let weatherLat = "", weatherLon = "";
91
+ if (weatherKey) {
92
+ info("Find your coordinates at: https://www.latlong.net");
93
+ weatherLat = await ask(" Latitude (e.g. 51.5074)", "51.5074");
94
+ weatherLon = await ask(" Longitude (e.g. -0.1278)", "-0.1278");
95
+ }
96
+
97
+ // ── Step 3: Create D1 database ───────────────────────────
98
+
99
+ step(3, TOTAL_STEPS, "Creating D1 database");
100
+
101
+ const pkgRoot = getPackageRoot();
102
+ const deployDir = join(CONFIG_DIR, "deploy");
103
+ mkdirSync(deployDir, { recursive: true });
104
+
105
+ // Copy worker files to deploy dir
106
+ const s1 = spinner("Copying source files");
107
+ for (const file of ["worker.js", "schema.sql", "wrangler.toml"]) {
108
+ const src = join(pkgRoot, file);
109
+ if (!existsSync(src)) { s1.fail(`Missing: ${file}`); process.exit(1); }
110
+ writeFileSync(join(deployDir, file), readFileSync(src, "utf-8"), "utf-8");
111
+ }
112
+ s1.stop("Source files copied");
113
+
114
+ const s2 = spinner("Creating D1 database");
115
+ const dbId = await createD1OrReuse("hearth-dash-db", deployDir);
116
+ if (!dbId) { s2.fail("D1 creation failed"); process.exit(1); }
117
+ s2.stop(`D1 database ready: ${dbId.substring(0, 8)}...`);
118
+
119
+ // ── Step 4: Create R2 bucket ─────────────────────────────
120
+
121
+ step(4, TOTAL_STEPS, "Creating R2 bucket");
122
+
123
+ const s3 = spinner("Creating R2 bucket");
124
+ const r2Result = await execWrangler(["r2", "bucket", "create", "hearth-dash-photos"], deployDir);
125
+ if (r2Result.code !== 0) {
126
+ const combined = r2Result.stdout + r2Result.stderr;
127
+ if (combined.includes("already exists")) {
128
+ s3.stop("R2 bucket already exists");
129
+ } else {
130
+ s3.fail(`R2 creation failed: ${combined}`);
131
+ process.exit(1);
132
+ }
133
+ } else {
134
+ s3.stop("R2 bucket created");
135
+ }
136
+
137
+ // Patch wrangler.toml with real values
138
+ let toml = readFileSync(join(deployDir, "wrangler.toml"), "utf-8");
139
+ toml = toml.replace("YOUR_D1_DATABASE_ID", dbId);
140
+ toml = toml.replace('PARTNER_1 = "Partner 1"', `PARTNER_1 = "${partner1}"`);
141
+ toml = toml.replace('PARTNER_2 = "Partner 2"', `PARTNER_2 = "${partner2}"`);
142
+ if (weatherLat) {
143
+ toml = toml.replace('# WEATHER_LAT = "52.5726"', `WEATHER_LAT = "${weatherLat}"`);
144
+ toml = toml.replace('# WEATHER_LON = "-0.2405"', `WEATHER_LON = "${weatherLon}"`);
145
+ }
146
+ writeFileSync(join(deployDir, "wrangler.toml"), toml, "utf-8");
147
+
148
+ // ── Step 5: Deploy worker + set secrets ──────────────────
149
+
150
+ step(5, TOTAL_STEPS, "Deploying worker");
151
+
152
+ const s4 = spinner("Setting secrets");
153
+ await setSecret("DASHBOARD_PASSWORD", dashPassword, deployDir);
154
+ await setSecret("MCP_SECRET", mcpSecret, deployDir);
155
+ if (weatherKey) await setSecret("WEATHER_API_KEY", weatherKey, deployDir);
156
+ s4.stop("Secrets configured");
157
+
158
+ const s5 = spinner("Deploying to Cloudflare Workers");
159
+ const deployResult = await execWrangler(["deploy"], deployDir);
160
+ if (deployResult.code !== 0) {
161
+ s5.fail("Deploy failed");
162
+ console.error(deployResult.stderr || deployResult.stdout);
163
+ process.exit(1);
164
+ }
165
+ const workerUrl = parseDeployOutput(deployResult);
166
+ s5.stop(`Deployed: ${workerUrl}`);
167
+
168
+ // Init schema
169
+ const s6 = spinner("Initializing database schema");
170
+ const schemaResult = await executeSchema("hearth-dash-db", join(deployDir, "schema.sql"), deployDir);
171
+ if (!schemaResult.ok) {
172
+ s6.fail("Schema init failed: " + (schemaResult.error || "unknown error"));
173
+ warn("You can retry manually: npx wrangler d1 execute hearth-dash-db --remote --file schema.sql");
174
+ } else {
175
+ s6.stop("Database schema initialized");
176
+ }
177
+
178
+ // ── Step 6: Save config + print results ──────────────────
179
+
180
+ step(6, TOTAL_STEPS, "Finishing up");
181
+
182
+ const config = saveConfig({
183
+ workerUrl,
184
+ mcpSecret,
185
+ partner1,
186
+ partner2,
187
+ dbId,
188
+ deployedAt: new Date().toISOString(),
189
+ });
190
+
191
+ const mcpUrl = workerUrl + "/mcp/" + mcpSecret;
192
+
193
+ console.log(`
194
+ ${green(bold(" Done!"))} Your dashboard is live.
195
+
196
+ ${bold("Dashboard:")} ${cyan(workerUrl)}
197
+ ${bold("Password:")} ${dim("(the one you just set)")}
198
+
199
+ ${bold("MCP Endpoint:")}
200
+ ${dim(mcpUrl)}
201
+
202
+ ${bold("MCP Config")} (add to Claude Code ${dim("~/.claude.json")} or Claude Desktop):
203
+ ${dim(" {")}
204
+ ${dim(' "mcpServers": {')}
205
+ ${dim(' "hearth-dash": {')}
206
+ ${dim(' "url": "' + mcpUrl + '"')}
207
+ ${dim(" }")}
208
+ ${dim(" }")}
209
+ ${dim(" }")}
210
+
211
+ ${bold("Connector URL")} (for Claude.ai mobile):
212
+ ${cyan(mcpUrl)}
213
+
214
+ ${dim("Config saved to: " + CONFIG_PATH)}
215
+ `);
216
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { banner, bold, dim, cyan, warn, info } from "../lib/ui.js";
5
+
6
+ const CONFIG_PATH = join(homedir(), ".hearth-dash", "config.json");
7
+
8
+ export default async function mcpCommand(args) {
9
+ banner();
10
+
11
+ if (!existsSync(CONFIG_PATH)) {
12
+ warn("No config found. Run 'hearth-dash deploy' first.");
13
+ process.exit(1);
14
+ }
15
+
16
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
17
+ const mcpUrl = config.workerUrl + "/mcp/" + config.mcpSecret;
18
+
19
+ console.log(bold(" MCP Configuration\n"));
20
+
21
+ console.log(` ${bold("Endpoint:")} ${cyan(mcpUrl)}\n`);
22
+
23
+ console.log(` ${bold("For Claude Code")} (add to ${dim("~/.claude.json")}):\n`);
24
+ console.log(` {`);
25
+ console.log(` "mcpServers": {`);
26
+ console.log(` "hearth-dash": {`);
27
+ console.log(` "url": "${mcpUrl}"`);
28
+ console.log(` }`);
29
+ console.log(` }`);
30
+ console.log(` }\n`);
31
+
32
+ console.log(` ${bold("For Claude Desktop")} (Settings > Developer > MCP):\n`);
33
+ console.log(` Name: hearth-dash`);
34
+ console.log(` URL: ${mcpUrl}\n`);
35
+
36
+ console.log(` ${bold("Connector URL")} (for Claude.ai mobile):`);
37
+ console.log(` ${cyan(mcpUrl)}\n`);
38
+
39
+ console.log(` ${bold("Available MCP tools:")}`);
40
+ const tools = [
41
+ "hearth_status — Dashboard overview",
42
+ "hearth_mood — Get/set moods",
43
+ "hearth_note — Get/leave fridge notes",
44
+ "hearth_moment — List/add moments",
45
+ "hearth_date — Upcoming/add dates",
46
+ "hearth_shopping_list — Get shopping list",
47
+ "hearth_shopping_add — Add shopping item",
48
+ "hearth_pressure — Barometric pressure data",
49
+ "hearth_food_diary_today — Today's meals + water",
50
+ "hearth_food_diary_history — Historical food data",
51
+ "hearth_food_review — Post food review",
52
+ "hearth_water_status — Water intake status",
53
+ ];
54
+ tools.forEach(t => console.log(` ${dim(t)}`));
55
+ console.log();
56
+
57
+ if (args.includes("--install")) {
58
+ const { writeFileSync } = await import("node:fs");
59
+ const claudeConfigPath = join(homedir(), ".claude.json");
60
+ let claudeConfig = {};
61
+ if (existsSync(claudeConfigPath)) {
62
+ try { claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8")); } catch {}
63
+ }
64
+ if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
65
+ claudeConfig.mcpServers["hearth-dash"] = { url: mcpUrl };
66
+ writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2) + "\n", "utf-8");
67
+ info("MCP config written to " + claudeConfigPath);
68
+ }
69
+ }
package/cli/index.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { bold, dim, magenta, cyan } from "./lib/ui.js";
4
+
5
+ const [,, command, ...args] = process.argv;
6
+
7
+ const COMMANDS = {
8
+ deploy: () => import("./commands/deploy.js"),
9
+ mcp: () => import("./commands/mcp.js"),
10
+ config: () => import("./commands/config.js"),
11
+ };
12
+
13
+ async function main() {
14
+ if (!command || command === "help" || command === "--help" || command === "-h") {
15
+ printHelp();
16
+ process.exit(0);
17
+ }
18
+
19
+ if (command === "--version" || command === "-v") {
20
+ const { readFileSync } = await import("node:fs");
21
+ const { join } = await import("node:path");
22
+ const { getPackageRoot } = await import("./lib/platform.js");
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(join(getPackageRoot(), "package.json"), "utf-8"));
25
+ console.log(pkg.version);
26
+ } catch { console.log("unknown"); }
27
+ process.exit(0);
28
+ }
29
+
30
+ const loader = COMMANDS[command];
31
+ if (!loader) {
32
+ console.error(`Unknown command: ${command}\n`);
33
+ printHelp();
34
+ process.exit(1);
35
+ }
36
+
37
+ const mod = await loader();
38
+ await mod.default(args);
39
+ }
40
+
41
+ function printHelp() {
42
+ console.log(`
43
+ ${magenta(bold("HEARTH DASH"))} ${dim("— Personal dashboard")}
44
+
45
+ ${bold("Usage:")} hearth-dash <command> [options]
46
+
47
+ ${bold("Commands:")}
48
+ ${cyan("deploy")} Deploy to Cloudflare (Workers + D1 + R2)
49
+ ${cyan("mcp")} Print MCP config for Claude Code/Desktop
50
+ ${cyan("config")} View current configuration
51
+
52
+ ${bold("Quick start:")}
53
+ ${dim("$")} npx hearth-dash deploy
54
+ ${dim("$")} npx hearth-dash mcp
55
+
56
+ ${bold("Requirements:")}
57
+ - Node.js 18+
58
+ - Cloudflare account (free tier works)
59
+ - OpenWeatherMap API key (free at ${dim("https://openweathermap.org/api")})
60
+ `);
61
+ }
62
+
63
+ main();
@@ -0,0 +1,12 @@
1
+ import { existsSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+
4
+ export function getPackageRoot() {
5
+ const url = new URL("../..", import.meta.url);
6
+ const decoded = decodeURIComponent(url.pathname);
7
+ return decoded.replace(/^\/([A-Z]:)/, "$1").replace(/\/$/, "");
8
+ }
9
+
10
+ export function getNodeMajor() {
11
+ return parseInt(process.versions.node.split(".")[0], 10);
12
+ }
@@ -0,0 +1,44 @@
1
+ import { createInterface } from "node:readline";
2
+
3
+ function rl() { return createInterface({ input: process.stdin, output: process.stdout }); }
4
+
5
+ export function ask(question, defaultValue) {
6
+ return new Promise((resolve) => {
7
+ const r = rl();
8
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
9
+ r.question(prompt, (a) => { r.close(); resolve(a.trim() || defaultValue || ""); });
10
+ });
11
+ }
12
+
13
+ export function confirm(question) {
14
+ return new Promise((resolve) => {
15
+ const r = rl();
16
+ r.question(`${question} (y/N): `, (a) => { r.close(); resolve(a.trim().toLowerCase() === "y"); });
17
+ });
18
+ }
19
+
20
+ export function password(question) {
21
+ return new Promise((resolve) => {
22
+ const stdin = process.stdin;
23
+ process.stdout.write(`${question}: `);
24
+ if (!stdin.isTTY || !stdin.setRawMode) {
25
+ const r = rl();
26
+ r.question("", (a) => { r.close(); resolve(a.trim()); });
27
+ return;
28
+ }
29
+ const wasRaw = stdin.isRaw;
30
+ stdin.setRawMode(true);
31
+ stdin.resume();
32
+ let input = "";
33
+ const onData = (buf) => {
34
+ const ch = buf.toString("utf8");
35
+ if (ch === "\r" || ch === "\n") {
36
+ stdin.setRawMode(wasRaw || false); stdin.removeListener("data", onData); stdin.pause();
37
+ process.stdout.write("\n"); resolve(input.trim());
38
+ } else if (ch === "\u0003") { stdin.setRawMode(wasRaw || false); process.exit(1); }
39
+ else if (ch === "\u007f" || ch === "\b") { if (input.length) { input = input.slice(0, -1); process.stdout.write("\b \b"); } }
40
+ else if (ch.charCodeAt(0) >= 32) { input += ch; process.stdout.write("*"); }
41
+ };
42
+ stdin.on("data", onData);
43
+ });
44
+ }
package/cli/lib/ui.js ADDED
@@ -0,0 +1,41 @@
1
+ const isColor = process.stdout.isTTY && !process.env.NO_COLOR;
2
+ const fmt = (code) => (text) => isColor ? `\x1b[${code}m${text}\x1b[0m` : text;
3
+
4
+ export const bold = fmt("1");
5
+ export const dim = fmt("2");
6
+ export const red = fmt("31");
7
+ export const green = fmt("32");
8
+ export const yellow = fmt("33");
9
+ export const cyan = fmt("36");
10
+ export const magenta = fmt("35");
11
+
12
+ export function banner() {
13
+ console.log(magenta(bold("\n HEARTH DASH")));
14
+ console.log(dim(" Personal dashboard\n"));
15
+ }
16
+
17
+ export function step(n, total, text) {
18
+ console.log(cyan(`[${n}/${total}]`) + ` ${text}`);
19
+ }
20
+
21
+ export function success(text) { console.log(green(" \u2713 ") + text); }
22
+ export function warn(text) { console.log(yellow(" ! ") + text); }
23
+ export function fail(text) { console.log(red(" \u2717 ") + text); }
24
+ export function info(text) { console.log(dim(" \u2192 ") + text); }
25
+
26
+ const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
27
+
28
+ export function spinner(text) {
29
+ if (!isColor) {
30
+ process.stdout.write(` ${text}...`);
31
+ return { stop: (f) => console.log(` ${f || "done"}`), fail: (f) => console.log(` ${f || "failed"}`) };
32
+ }
33
+ let i = 0;
34
+ const id = setInterval(() => {
35
+ process.stdout.write(`\r ${cyan(FRAMES[i++ % FRAMES.length])} ${text}`);
36
+ }, 80);
37
+ return {
38
+ stop(f) { clearInterval(id); process.stdout.write(`\r ${green("\u2713")} ${f || text}\n`); },
39
+ fail(f) { clearInterval(id); process.stdout.write(`\r ${red("\u2717")} ${f || text}\n`); },
40
+ };
41
+ }
@@ -0,0 +1,73 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ function spawnCmd(cmd, args, opts) {
5
+ if (process.platform === "win32") {
6
+ const full = [cmd, ...args].map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
7
+ return spawn(full, [], { ...opts, shell: true });
8
+ }
9
+ return spawn(cmd, args, opts);
10
+ }
11
+
12
+ export function execWrangler(args, cwd, stdinData) {
13
+ return new Promise((resolve) => {
14
+ const proc = spawnCmd("npx", ["wrangler", ...args], {
15
+ cwd, stdio: ["pipe", "pipe", "pipe"],
16
+ env: { ...process.env, FORCE_COLOR: "0" },
17
+ });
18
+ let stdout = "", stderr = "";
19
+ proc.stdout.on("data", (d) => { stdout += d.toString(); });
20
+ proc.stderr.on("data", (d) => { stderr += d.toString(); });
21
+ if (stdinData) { proc.stdin.write(stdinData); proc.stdin.end(); }
22
+ else { proc.stdin.end(); }
23
+ proc.on("close", (code) => resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code }));
24
+ });
25
+ }
26
+
27
+ export function parseD1CreateOutput(output) {
28
+ const combined = output.stdout + "\n" + output.stderr;
29
+ const match = combined.match(/database_id\s*=\s*"([^"]+)"/);
30
+ return match ? match[1] : null;
31
+ }
32
+
33
+ export function parseDeployOutput(output) {
34
+ const combined = output.stdout + "\n" + output.stderr;
35
+ const match = combined.match(/https:\/\/[^\s)]+\.workers\.dev/);
36
+ return match ? match[0] : null;
37
+ }
38
+
39
+ export async function checkWranglerAuth(cwd) {
40
+ const result = await execWrangler(["whoami"], cwd);
41
+ if (result.code !== 0) return false;
42
+ const combined = result.stdout + result.stderr;
43
+ return !combined.includes("Not logged in") && !combined.includes("not authenticated");
44
+ }
45
+
46
+ export async function wranglerLogin(cwd) {
47
+ return new Promise((resolve) => {
48
+ const proc = spawnCmd("npx", ["wrangler", "login"], { cwd, stdio: "inherit" });
49
+ proc.on("close", (code) => resolve(code === 0));
50
+ });
51
+ }
52
+
53
+ export async function setSecret(name, value, cwd) {
54
+ return execWrangler(["secret", "put", name], cwd, value + "\n");
55
+ }
56
+
57
+ export async function executeSchema(dbName, schemaPath, cwd) {
58
+ const result = await execWrangler(["d1", "execute", dbName, "--remote", "--file=" + schemaPath], cwd);
59
+ if (result.code === 0) return { ok: true };
60
+ const sql = readFileSync(schemaPath, "utf-8");
61
+ const statements = sql.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
62
+ for (const stmt of statements) {
63
+ const r = await execWrangler(["d1", "execute", dbName, "--remote", "--command", stmt + ";"], cwd);
64
+ if (r.code !== 0) return { ok: false, error: r.stderr || r.stdout };
65
+ }
66
+ return { ok: true };
67
+ }
68
+
69
+ export async function listD1Databases(cwd) {
70
+ const result = await execWrangler(["d1", "list", "--json"], cwd);
71
+ if (result.code !== 0) return [];
72
+ try { return JSON.parse(result.stdout); } catch { return []; }
73
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "hearth-dash",
3
+ "version": "1.0.0",
4
+ "description": "Personal dashboard — moods, moments, food diary, weather, and more. Deploy to Cloudflare in one command.",
5
+ "type": "module",
6
+ "bin": {
7
+ "hearth-dash": "./cli/index.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "npx wrangler dev",
11
+ "deploy": "npx wrangler deploy",
12
+ "db:init": "npx wrangler d1 execute hearth-dash-db --file schema.sql",
13
+ "db:init:remote": "npx wrangler d1 execute hearth-dash-db --remote --file schema.sql"
14
+ },
15
+ "keywords": [
16
+ "dashboard",
17
+ "cloudflare-workers",
18
+ "mcp",
19
+ "mood-tracker",
20
+ "food-diary",
21
+ "weather",
22
+ "personal"
23
+ ],
24
+ "author": "AIDHD",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/martusha89/hearth-dash.git"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "files": [
34
+ "worker.js",
35
+ "schema.sql",
36
+ "wrangler.toml",
37
+ "cli/",
38
+ "README.md",
39
+ "LICENSE"
40
+ ]
41
+ }