@forwardimpact/basecamp 0.1.0 → 0.3.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/README.md CHANGED
@@ -54,14 +54,11 @@ To uninstall, run `/usr/local/share/basecamp/uninstall.sh`.
54
54
 
55
55
  ```bash
56
56
  cd apps/basecamp
57
- ./scripts/install.sh
57
+ ./scripts/init.sh
58
58
 
59
59
  # Configure your identity
60
60
  vi ~/Documents/Personal/USER.md
61
61
 
62
- # Start the daemon
63
- npx fit-basecamp --install-launchd
64
-
65
62
  # Open your KB interactively
66
63
  cd ~/Documents/Personal && claude
67
64
  ```
@@ -180,13 +177,11 @@ fit-basecamp Run due tasks once and exit
180
177
  fit-basecamp --daemon Run continuously (poll every 60s)
181
178
  fit-basecamp --run <task> Run a specific task immediately
182
179
  fit-basecamp --init <path> Initialize a new knowledge base
183
- fit-basecamp --install-launchd Install macOS LaunchAgent for auto-start
184
- fit-basecamp --uninstall-launchd Remove macOS LaunchAgent
185
180
  fit-basecamp --status Show knowledge bases and task status
186
181
  fit-basecamp --help Show this help
187
182
  ```
188
183
 
189
- When running from source, use `node scheduler.js` or `npx fit-basecamp` instead
184
+ When running from source, use `node basecamp.js` or `npx fit-basecamp` instead
190
185
  of `fit-basecamp`.
191
186
 
192
187
  ## How It Works
@@ -220,7 +215,7 @@ ships with these skills:
220
215
  ## Requirements
221
216
 
222
217
  - Claude CLI (`claude`) installed and authenticated
223
- - macOS (for Apple Mail/Calendar sync and launchd)
218
+ - macOS (for Apple Mail/Calendar sync)
224
219
  - Node.js >= 18 (for running from source) or the standalone binary
225
220
  - Deno >= 2.x (for building the standalone binary)
226
221
 
@@ -1,17 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Basecamp Scheduler Runs scheduled tasks across multiple knowledge bases.
3
+ // Basecamp — CLI and scheduler for personal knowledge bases.
4
4
  //
5
5
  // Usage:
6
- // node scheduler.js Run due tasks once and exit
7
- // node scheduler.js --daemon Run continuously (poll every 60s)
8
- // node scheduler.js --run <task> Run a specific task immediately
9
- // node scheduler.js --init <path> Initialize a new knowledge base
10
- // node scheduler.js --install-launchd Install macOS LaunchAgent
11
- // node scheduler.js --uninstall-launchd Remove macOS LaunchAgent
12
- // node scheduler.js --validate Validate agents and skills exist
13
- // node scheduler.js --status Show task status
14
- // node scheduler.js --help Show this help
6
+ // node basecamp.js Run due tasks once and exit
7
+ // node basecamp.js --daemon Run continuously (poll every 60s)
8
+ // node basecamp.js --run <task> Run a specific task immediately
9
+ // node basecamp.js --init <path> Initialize a new knowledge base
10
+ // node basecamp.js --validate Validate agents and skills exist
11
+ // node basecamp.js --status Show task status
12
+ // node basecamp.js --help Show this help
15
13
 
16
14
  import {
17
15
  readFileSync,
@@ -19,26 +17,27 @@ import {
19
17
  existsSync,
20
18
  mkdirSync,
21
19
  readdirSync,
20
+ unlinkSync,
21
+ chmodSync,
22
22
  } from "node:fs";
23
23
  import { execSync } from "node:child_process";
24
+ import { spawn } from "node:child_process";
24
25
  import { join, dirname, resolve } from "node:path";
25
26
  import { homedir } from "node:os";
26
27
  import { fileURLToPath } from "node:url";
28
+ import { createServer } from "node:net";
27
29
 
28
30
  const HOME = homedir();
29
31
  const BASECAMP_HOME = join(HOME, ".fit", "basecamp");
30
32
  const CONFIG_PATH = join(BASECAMP_HOME, "scheduler.json");
31
33
  const STATE_PATH = join(BASECAMP_HOME, "state.json");
32
34
  const LOG_DIR = join(BASECAMP_HOME, "logs");
33
- const PLIST_NAME = "com.fit-basecamp.scheduler";
34
- const PLIST_PATH = join(HOME, "Library", "LaunchAgents", `${PLIST_NAME}.plist`);
35
35
  const __dirname =
36
36
  import.meta.dirname || dirname(fileURLToPath(import.meta.url));
37
37
  const KB_TEMPLATE_DIR = join(__dirname, "template");
38
- const IS_COMPILED =
39
- typeof Deno !== "undefined" &&
40
- Deno.execPath &&
41
- !Deno.execPath().endsWith("deno");
38
+ const SOCKET_PATH = join(BASECAMP_HOME, "basecamp.sock");
39
+
40
+ let daemonStartedAt = null;
42
41
 
43
42
  // --- Helpers ----------------------------------------------------------------
44
43
 
@@ -152,6 +151,7 @@ function floorToMinute(d) {
152
151
 
153
152
  function shouldRun(task, taskState, now) {
154
153
  if (task.enabled === false) return false;
154
+ if (taskState.status === "running") return false;
155
155
  const { schedule } = task;
156
156
  if (!schedule) return false;
157
157
  const lastRun = taskState.lastRunAt ? new Date(taskState.lastRunAt) : null;
@@ -197,63 +197,247 @@ function runTask(taskName, task, _config, state) {
197
197
  ts.startedAt = new Date().toISOString();
198
198
  saveState(state);
199
199
 
200
- try {
201
- const args = ["--print"];
202
- if (task.agent) args.push("--agent", task.agent);
203
- args.push("-p", prompt);
204
-
205
- const result = execSync(
206
- `${claude} ${args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`,
207
- {
208
- cwd: kbPath,
209
- encoding: "utf8",
210
- timeout: 30 * 60_000,
211
- stdio: ["pipe", "pipe", "pipe"],
212
- },
213
- );
200
+ const spawnArgs = ["--print"];
201
+ if (task.agent) spawnArgs.push("--agent", task.agent);
202
+ spawnArgs.push("-p", prompt);
214
203
 
215
- log(`Task ${taskName} completed. Output: ${result.slice(0, 200)}...`);
216
- Object.assign(ts, {
217
- status: "finished",
218
- lastRunAt: new Date().toISOString(),
219
- lastError: null,
220
- runCount: (ts.runCount || 0) + 1,
204
+ return new Promise((resolve) => {
205
+ const child = spawn(claude, spawnArgs, {
206
+ cwd: kbPath,
207
+ stdio: ["pipe", "pipe", "pipe"],
208
+ timeout: 30 * 60_000,
221
209
  });
222
- } catch (err) {
223
- const errMsg = err.stderr || err.stdout || err.message || String(err);
224
- log(`Task ${taskName} failed: ${errMsg.slice(0, 300)}`);
225
- Object.assign(ts, {
226
- status: "failed",
227
- lastRunAt: new Date().toISOString(),
228
- lastError: errMsg.slice(0, 500),
210
+
211
+ let stdout = "";
212
+ let stderr = "";
213
+ child.stdout.on("data", (d) => (stdout += d));
214
+ child.stderr.on("data", (d) => (stderr += d));
215
+
216
+ child.on("close", (code) => {
217
+ if (code === 0) {
218
+ log(`Task ${taskName} completed. Output: ${stdout.slice(0, 200)}...`);
219
+ Object.assign(ts, {
220
+ status: "finished",
221
+ startedAt: null,
222
+ lastRunAt: new Date().toISOString(),
223
+ lastError: null,
224
+ runCount: (ts.runCount || 0) + 1,
225
+ });
226
+ } else {
227
+ const errMsg = stderr || stdout || `Exit code ${code}`;
228
+ log(`Task ${taskName} failed: ${errMsg.slice(0, 300)}`);
229
+ Object.assign(ts, {
230
+ status: "failed",
231
+ startedAt: null,
232
+ lastRunAt: new Date().toISOString(),
233
+ lastError: errMsg.slice(0, 500),
234
+ });
235
+ }
236
+ saveState(state);
237
+ resolve();
229
238
  });
230
- }
231
- saveState(state);
239
+
240
+ child.on("error", (err) => {
241
+ log(`Task ${taskName} failed: ${err.message}`);
242
+ Object.assign(ts, {
243
+ status: "failed",
244
+ startedAt: null,
245
+ lastRunAt: new Date().toISOString(),
246
+ lastError: err.message.slice(0, 500),
247
+ });
248
+ saveState(state);
249
+ resolve();
250
+ });
251
+ });
232
252
  }
233
253
 
234
- function runDueTasks() {
254
+ async function runDueTasks() {
235
255
  const config = loadConfig(),
236
256
  state = loadState(),
237
257
  now = new Date();
238
258
  let ranAny = false;
239
259
  for (const [name, task] of Object.entries(config.tasks)) {
240
260
  if (shouldRun(task, state.tasks[name] || {}, now)) {
241
- runTask(name, task, config, state);
261
+ await runTask(name, task, config, state);
242
262
  ranAny = true;
243
263
  }
244
264
  }
245
265
  if (!ranAny) log("No tasks due.");
246
266
  }
247
267
 
268
+ // --- Next-run computation ---------------------------------------------------
269
+
270
+ /** @param {object} task @param {object} taskState @param {Date} now */
271
+ function computeNextRunAt(task, taskState, now) {
272
+ if (task.enabled === false) return null;
273
+ const { schedule } = task;
274
+ if (!schedule) return null;
275
+
276
+ if (schedule.type === "interval") {
277
+ const ms = (schedule.minutes || 5) * 60_000;
278
+ const lastRun = taskState.lastRunAt ? new Date(taskState.lastRunAt) : null;
279
+ if (!lastRun) return now.toISOString();
280
+ return new Date(lastRun.getTime() + ms).toISOString();
281
+ }
282
+
283
+ if (schedule.type === "cron") {
284
+ const limit = 24 * 60;
285
+ const start = new Date(floorToMinute(now) + 60_000);
286
+ for (let i = 0; i < limit; i++) {
287
+ const candidate = new Date(start.getTime() + i * 60_000);
288
+ if (cronMatches(schedule.expression, candidate)) {
289
+ return candidate.toISOString();
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ if (schedule.type === "once") {
296
+ if (taskState.lastRunAt) return null;
297
+ return schedule.runAt;
298
+ }
299
+
300
+ return null;
301
+ }
302
+
303
+ // --- Socket server ----------------------------------------------------------
304
+
305
+ /** @param {import('node:net').Socket} socket @param {object} data */
306
+ function send(socket, data) {
307
+ try {
308
+ socket.write(JSON.stringify(data) + "\n");
309
+ } catch {}
310
+ }
311
+
312
+ function handleStatusRequest(socket) {
313
+ const config = loadConfig();
314
+ const state = loadState();
315
+ const now = new Date();
316
+ const tasks = {};
317
+
318
+ for (const [name, task] of Object.entries(config.tasks)) {
319
+ const ts = state.tasks[name] || {};
320
+ tasks[name] = {
321
+ enabled: task.enabled !== false,
322
+ status: ts.status || "never-run",
323
+ lastRunAt: ts.lastRunAt || null,
324
+ nextRunAt: computeNextRunAt(task, ts, now),
325
+ runCount: ts.runCount || 0,
326
+ lastError: ts.lastError || null,
327
+ };
328
+ if (ts.startedAt) tasks[name].startedAt = ts.startedAt;
329
+ }
330
+
331
+ send(socket, {
332
+ type: "status",
333
+ uptime: daemonStartedAt
334
+ ? Math.floor((Date.now() - daemonStartedAt) / 1000)
335
+ : 0,
336
+ tasks,
337
+ });
338
+ }
339
+
340
+ function handleRestartRequest(socket) {
341
+ send(socket, { type: "ack", command: "restart" });
342
+ setTimeout(() => process.exit(0), 100);
343
+ }
344
+
345
+ function handleRunRequest(socket, taskName) {
346
+ if (!taskName) {
347
+ send(socket, { type: "error", message: "Missing task name" });
348
+ return;
349
+ }
350
+ const config = loadConfig();
351
+ const task = config.tasks[taskName];
352
+ if (!task) {
353
+ send(socket, { type: "error", message: `Task not found: ${taskName}` });
354
+ return;
355
+ }
356
+ send(socket, { type: "ack", command: "run", task: taskName });
357
+ const state = loadState();
358
+ runTask(taskName, task, config, state).catch((err) => {
359
+ console.error(`[socket] runTask error for ${taskName}:`, err.message);
360
+ });
361
+ }
362
+
363
+ function handleMessage(socket, line) {
364
+ let request;
365
+ try {
366
+ request = JSON.parse(line);
367
+ } catch {
368
+ send(socket, { type: "error", message: "Invalid JSON" });
369
+ return;
370
+ }
371
+
372
+ const handlers = {
373
+ status: () => handleStatusRequest(socket),
374
+ restart: () => handleRestartRequest(socket),
375
+ run: () => handleRunRequest(socket, request.task),
376
+ };
377
+
378
+ const handler = handlers[request.type];
379
+ if (handler) {
380
+ handler();
381
+ } else {
382
+ send(socket, {
383
+ type: "error",
384
+ message: `Unknown request type: ${request.type}`,
385
+ });
386
+ }
387
+ }
388
+
389
+ function startSocketServer() {
390
+ try {
391
+ unlinkSync(SOCKET_PATH);
392
+ } catch {}
393
+
394
+ const server = createServer((socket) => {
395
+ let buffer = "";
396
+ socket.on("data", (data) => {
397
+ buffer += data.toString();
398
+ let idx;
399
+ while ((idx = buffer.indexOf("\n")) !== -1) {
400
+ const line = buffer.slice(0, idx).trim();
401
+ buffer = buffer.slice(idx + 1);
402
+ if (line) handleMessage(socket, line);
403
+ }
404
+ });
405
+ socket.on("error", () => {});
406
+ });
407
+
408
+ server.listen(SOCKET_PATH, () => {
409
+ chmodSync(SOCKET_PATH, 0o600);
410
+ log(`Socket server listening on ${SOCKET_PATH}`);
411
+ });
412
+
413
+ server.on("error", (err) => {
414
+ log(`Socket server error: ${err.message}`);
415
+ });
416
+
417
+ const cleanup = () => {
418
+ server.close();
419
+ try {
420
+ unlinkSync(SOCKET_PATH);
421
+ } catch {}
422
+ process.exit(0);
423
+ };
424
+ process.on("SIGTERM", cleanup);
425
+ process.on("SIGINT", cleanup);
426
+
427
+ return server;
428
+ }
429
+
248
430
  // --- Daemon -----------------------------------------------------------------
249
431
 
250
432
  function daemon() {
433
+ daemonStartedAt = Date.now();
251
434
  log("Scheduler daemon started. Polling every 60 seconds.");
252
435
  log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
436
+ startSocketServer();
253
437
  runDueTasks();
254
- setInterval(() => {
438
+ setInterval(async () => {
255
439
  try {
256
- runDueTasks();
440
+ await runDueTasks();
257
441
  } catch (err) {
258
442
  log(`Error: ${err.message}`);
259
443
  }
@@ -299,69 +483,6 @@ function initKB(targetPath) {
299
483
  );
300
484
  }
301
485
 
302
- // --- LaunchAgent ------------------------------------------------------------
303
-
304
- function installLaunchd() {
305
- const execPath =
306
- typeof Deno !== "undefined" ? Deno.execPath() : process.execPath;
307
- const isCompiled = IS_COMPILED || !execPath.includes("node");
308
- const progArgs = isCompiled
309
- ? ` <string>${execPath}</string>\n <string>--daemon</string>`
310
- : ` <string>${execPath}</string>\n <string>${join(__dirname, "scheduler.js")}</string>\n <string>--daemon</string>`;
311
-
312
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
313
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
314
- <plist version="1.0">
315
- <dict>
316
- <key>Label</key>
317
- <string>${PLIST_NAME}</string>
318
- <key>ProgramArguments</key>
319
- <array>
320
- ${progArgs}
321
- </array>
322
- <key>RunAtLoad</key>
323
- <true/>
324
- <key>KeepAlive</key>
325
- <true/>
326
- <key>StandardOutPath</key>
327
- <string>${join(LOG_DIR, "launchd-stdout.log")}</string>
328
- <key>StandardErrorPath</key>
329
- <string>${join(LOG_DIR, "launchd-stderr.log")}</string>
330
- <key>WorkingDirectory</key>
331
- <string>${BASECAMP_HOME}</string>
332
- <key>EnvironmentVariables</key>
333
- <dict>
334
- <key>PATH</key>
335
- <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${join(HOME, ".local", "bin")}</string>
336
- </dict>
337
- </dict>
338
- </plist>`;
339
-
340
- ensureDir(dirname(PLIST_PATH));
341
- ensureDir(LOG_DIR);
342
- writeFileSync(PLIST_PATH, plist);
343
-
344
- try {
345
- execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, {
346
- stdio: "ignore",
347
- });
348
- } catch {}
349
- execSync(`launchctl load "${PLIST_PATH}"`);
350
- console.log(
351
- `LaunchAgent installed and loaded.\n Plist: ${PLIST_PATH}\n Logs: ${LOG_DIR}/\n Config: ${CONFIG_PATH}`,
352
- );
353
- }
354
-
355
- function uninstallLaunchd() {
356
- try {
357
- execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
358
- } catch {}
359
- try {
360
- execSync(`rm -f "${PLIST_PATH}"`);
361
- } catch {}
362
- console.log("LaunchAgent uninstalled.");
363
- }
364
-
365
486
  // --- Status -----------------------------------------------------------------
366
487
 
367
488
  function showStatus() {
@@ -392,15 +513,6 @@ function showStatus() {
392
513
  if (s.lastError) lines.push(` Error: ${s.lastError.slice(0, 80)}`);
393
514
  console.log(lines.join("\n"));
394
515
  }
395
-
396
- try {
397
- execSync(`launchctl list 2>/dev/null | grep ${PLIST_NAME}`, {
398
- encoding: "utf8",
399
- });
400
- console.log("\nLaunchAgent: loaded");
401
- } catch {
402
- console.log("\nLaunchAgent: not loaded (run --install-launchd to start)");
403
- }
404
516
  }
405
517
 
406
518
  // --- Validate ---------------------------------------------------------------
@@ -481,8 +593,6 @@ Usage:
481
593
  ${bin} --daemon Run continuously (poll every 60s)
482
594
  ${bin} --run <task> Run a specific task immediately
483
595
  ${bin} --init <path> Initialize a new knowledge base
484
- ${bin} --install-launchd Install macOS LaunchAgent for auto-start
485
- ${bin} --uninstall-launchd Remove macOS LaunchAgent
486
596
  ${bin} --validate Validate agents and skills exist
487
597
  ${bin} --status Show task status
488
598
  ${bin} --help Show this help
@@ -520,20 +630,18 @@ const commands = {
520
630
  "--help": showHelp,
521
631
  "-h": showHelp,
522
632
  "--daemon": daemon,
523
- "--install-launchd": installLaunchd,
524
- "--uninstall-launchd": uninstallLaunchd,
525
633
  "--validate": validate,
526
634
  "--status": showStatus,
527
635
  "--init": () => {
528
636
  if (!args[1]) {
529
- console.error("Usage: node scheduler.js --init <path>");
637
+ console.error("Usage: node basecamp.js --init <path>");
530
638
  process.exit(1);
531
639
  }
532
640
  initKB(args[1]);
533
641
  },
534
- "--run": () => {
642
+ "--run": async () => {
535
643
  if (!args[1]) {
536
- console.error("Usage: node scheduler.js --run <task-name>");
644
+ console.error("Usage: node basecamp.js --run <task-name>");
537
645
  process.exit(1);
538
646
  }
539
647
  const config = loadConfig(),
@@ -545,8 +653,8 @@ const commands = {
545
653
  );
546
654
  process.exit(1);
547
655
  }
548
- runTask(args[1], task, config, state);
656
+ await runTask(args[1], task, config, state);
549
657
  },
550
658
  };
551
659
 
552
- (commands[command] || runDueTasks)();
660
+ await (commands[command] || runDueTasks)();
package/build.js CHANGED
@@ -1,17 +1,12 @@
1
1
  #!/usr/bin/env -S deno run --allow-all
2
2
 
3
- // Build script for Basecamp.
3
+ // Build script for Basecamp (arm64 macOS).
4
4
  //
5
5
  // Usage:
6
- // deno run --allow-all build.js Build standalone executable (current arch)
7
- // deno run --allow-all build.js --pkg Build executable + macOS .pkg installer
8
- // deno run --allow-all build.js --all Build for both arm64 and x86_64 + .pkg
9
- //
10
- // Or via deno tasks:
11
- // deno task build
12
- // deno task build:pkg
6
+ // deno run --allow-all build.js Build standalone executable
7
+ // deno run --allow-all build.js --pkg Build executable + macOS .pkg installer
13
8
 
14
- import { existsSync, mkdirSync } from "node:fs";
9
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
15
10
  import { join, dirname } from "node:path";
16
11
  import { execSync } from "node:child_process";
17
12
 
@@ -19,7 +14,11 @@ const __dirname =
19
14
  import.meta.dirname || dirname(new URL(import.meta.url).pathname);
20
15
  const DIST_DIR = join(__dirname, "dist");
21
16
  const APP_NAME = "fit-basecamp";
22
- const VERSION = "1.0.0";
17
+ const STATUS_MENU_NAME = "BasecampStatus";
18
+ const STATUS_MENU_DIR = join(__dirname, "StatusMenu");
19
+ const VERSION = JSON.parse(
20
+ readFileSync(join(__dirname, "package.json"), "utf8"),
21
+ ).version;
23
22
 
24
23
  // ---------------------------------------------------------------------------
25
24
  // Helpers
@@ -34,32 +33,23 @@ function run(cmd, opts = {}) {
34
33
  return execSync(cmd, { encoding: "utf8", stdio: "inherit", ...opts });
35
34
  }
36
35
 
37
- function runCapture(cmd) {
38
- return execSync(cmd, {
39
- encoding: "utf8",
40
- stdio: ["pipe", "pipe", "pipe"],
41
- }).trim();
42
- }
43
-
44
36
  // ---------------------------------------------------------------------------
45
37
  // Compile standalone binary
46
38
  // ---------------------------------------------------------------------------
47
39
 
48
- function compile(target = null) {
49
- const arch = target || detectArch();
50
- const outputName = target ? `${APP_NAME}-${arch}` : APP_NAME;
51
- const outputPath = join(DIST_DIR, outputName);
40
+ function compile() {
41
+ const outputPath = join(DIST_DIR, APP_NAME);
52
42
 
53
- console.log(`\nCompiling ${APP_NAME} for ${arch}...`);
43
+ console.log(`\nCompiling ${APP_NAME}...`);
54
44
  ensureDir(DIST_DIR);
55
45
 
56
46
  const cmd = [
57
47
  "deno compile",
58
48
  "--allow-all",
59
- `--target ${arch}`,
49
+ "--no-check",
60
50
  `--output "${outputPath}"`,
61
51
  "--include template/",
62
- "scheduler.js",
52
+ "basecamp.js",
63
53
  ].join(" ");
64
54
 
65
55
  run(cmd, { cwd: __dirname });
@@ -68,26 +58,43 @@ function compile(target = null) {
68
58
  return outputPath;
69
59
  }
70
60
 
71
- function detectArch() {
72
- const arch = runCapture("uname -m");
73
- if (arch === "arm64") return "aarch64-apple-darwin";
74
- return "x86_64-apple-darwin";
61
+ // ---------------------------------------------------------------------------
62
+ // Compile Swift status menu binary
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function compileStatusMenu() {
66
+ console.log(`\nCompiling ${STATUS_MENU_NAME}...`);
67
+ ensureDir(DIST_DIR);
68
+
69
+ const buildDir = join(STATUS_MENU_DIR, ".build");
70
+ rmSync(buildDir, { recursive: true, force: true });
71
+
72
+ run("swift build -c release", { cwd: STATUS_MENU_DIR });
73
+
74
+ const binary = join(buildDir, "release", STATUS_MENU_NAME);
75
+ const outputPath = join(DIST_DIR, STATUS_MENU_NAME);
76
+ run(`cp "${binary}" "${outputPath}"`);
77
+
78
+ rmSync(buildDir, { recursive: true, force: true });
79
+
80
+ console.log(` -> ${outputPath}`);
81
+ return outputPath;
75
82
  }
76
83
 
77
84
  // ---------------------------------------------------------------------------
78
85
  // Build macOS installer package (.pkg)
79
86
  // ---------------------------------------------------------------------------
80
87
 
81
- function buildPKG(binaryPath, arch) {
82
- const archShort = arch.includes("aarch64") ? "arm64" : "x86_64";
83
- const pkgName = `${APP_NAME}-${VERSION}-${archShort}.pkg`;
88
+ function buildPKG(statusMenuBinaryPath) {
89
+ const pkgName = `${APP_NAME}-${VERSION}.pkg`;
84
90
 
85
91
  console.log(`\nBuilding pkg: ${pkgName}...`);
86
92
 
87
- const buildPkg = join(__dirname, "scripts", "build-pkg.sh");
88
- run(`"${buildPkg}" "${DIST_DIR}" "${APP_NAME}" "${VERSION}" "${arch}"`, {
89
- cwd: __dirname,
90
- });
93
+ const buildPkg = join(__dirname, "pkg", "macos", "build-pkg.sh");
94
+ run(
95
+ `"${buildPkg}" "${DIST_DIR}" "${APP_NAME}" "${VERSION}" "${statusMenuBinaryPath}"`,
96
+ { cwd: __dirname },
97
+ );
91
98
 
92
99
  console.log(` -> ${join(DIST_DIR, pkgName)}`);
93
100
  return join(DIST_DIR, pkgName);
@@ -99,26 +106,17 @@ function buildPKG(binaryPath, arch) {
99
106
 
100
107
  const args = Deno?.args || process.argv.slice(2);
101
108
  const wantPKG = args.includes("--pkg");
102
- const wantAll = args.includes("--all");
103
109
 
104
110
  console.log(`Basecamp Build (v${VERSION})`);
105
111
  console.log("==========================");
106
112
 
107
- if (wantAll) {
108
- // Build for both architectures
109
- const targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
110
- for (const target of targets) {
111
- const binary = compile(target);
112
- buildPKG(binary, target);
113
- }
114
- } else {
115
- // Build for current architecture
116
- const arch = detectArch();
117
- const binary = compile(arch);
118
-
119
- if (wantPKG) {
120
- buildPKG(binary, arch);
121
- }
113
+ // Compile Deno binary first (before status menu exists in dist/),
114
+ // so the status menu binary is not embedded in the Deno binary.
115
+ compile();
116
+ const statusMenuBinary = compileStatusMenu();
117
+
118
+ if (wantPKG) {
119
+ buildPKG(statusMenuBinary);
122
120
  }
123
121
 
124
122
  console.log("\nBuild complete! Output in dist/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/basecamp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code-native personal knowledge system with scheduled tasks",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -10,25 +10,22 @@
10
10
  },
11
11
  "homepage": "https://www.forwardimpact.team/basecamp",
12
12
  "type": "module",
13
- "main": "scheduler.js",
13
+ "main": "basecamp.js",
14
14
  "bin": {
15
- "fit-basecamp": "./scheduler.js"
15
+ "fit-basecamp": "./basecamp.js"
16
16
  },
17
17
  "scripts": {
18
- "start": "node scheduler.js",
19
- "status": "node scheduler.js --status",
18
+ "start": "node basecamp.js",
19
+ "status": "node basecamp.js --status",
20
20
  "build": "deno run --allow-all build.js",
21
21
  "build:pkg": "deno run --allow-all build.js --pkg",
22
22
  "build:all": "deno run --allow-all build.js --all",
23
- "install:basecamp": "./scripts/install.sh",
24
- "scheduler:install": "node scheduler.js --install-launchd",
25
- "scheduler:uninstall": "node scheduler.js --uninstall-launchd"
23
+ "init": "./scripts/init.sh"
26
24
  },
27
25
  "files": [
28
- "scheduler.js",
26
+ "basecamp.js",
29
27
  "build.js",
30
28
  "config/",
31
- "scripts/",
32
29
  "template/"
33
30
  ],
34
31
  "engines": {
@@ -1,117 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # Build a macOS installer package (.pkg) for Basecamp.
5
- #
6
- # Uses pkgbuild (component) + productbuild (distribution) to create a .pkg
7
- # that installs the binary to /usr/local/bin/ and runs a postinstall script
8
- # to set up the LaunchAgent, config, and default knowledge base.
9
- #
10
- # Usage: build-pkg.sh <dist_dir> <app_name> <version> <target>
11
- # e.g. build-pkg.sh dist basecamp 1.0.0 aarch64-apple-darwin
12
-
13
- DIST_DIR="${1:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <target>}"
14
- APP_NAME="${2:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <target>}"
15
- VERSION="${3:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <target>}"
16
- TARGET="${4:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <target>}"
17
-
18
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
- PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20
- BINARY_PATH="$DIST_DIR/$APP_NAME-$TARGET"
21
- IDENTIFIER="com.fit-basecamp.scheduler"
22
-
23
- if [ ! -f "$BINARY_PATH" ]; then
24
- echo "Error: binary not found at $BINARY_PATH"
25
- echo "Run compile.sh first."
26
- exit 1
27
- fi
28
-
29
- # Determine short arch name
30
- case "$TARGET" in
31
- *aarch64*) ARCH_SHORT="arm64" ;;
32
- *) ARCH_SHORT="x86_64" ;;
33
- esac
34
-
35
- PKG_NAME="$APP_NAME-$VERSION-$ARCH_SHORT.pkg"
36
- PKG_PATH="$DIST_DIR/$PKG_NAME"
37
- PAYLOAD_DIR="$DIST_DIR/pkg-payload-$ARCH_SHORT"
38
- SCRIPTS_DIR="$DIST_DIR/pkg-scripts-$ARCH_SHORT"
39
- RESOURCES_DIR="$DIST_DIR/pkg-resources-$ARCH_SHORT"
40
- COMPONENT_PKG="$DIST_DIR/pkg-component-$ARCH_SHORT.pkg"
41
-
42
- echo ""
43
- echo "Building pkg: $PKG_NAME..."
44
-
45
- # --- Clean previous artifacts ------------------------------------------------
46
-
47
- rm -rf "$PAYLOAD_DIR" "$SCRIPTS_DIR" "$RESOURCES_DIR" "$COMPONENT_PKG"
48
- rm -f "$PKG_PATH"
49
-
50
- # --- Create payload (files to install) ---------------------------------------
51
-
52
- mkdir -p "$PAYLOAD_DIR/usr/local/bin"
53
- mkdir -p "$PAYLOAD_DIR/usr/local/share/fit-basecamp/config"
54
-
55
- cp "$BINARY_PATH" "$PAYLOAD_DIR/usr/local/bin/$APP_NAME"
56
- chmod +x "$PAYLOAD_DIR/usr/local/bin/$APP_NAME"
57
-
58
- cp "$PROJECT_DIR/config/scheduler.json" "$PAYLOAD_DIR/usr/local/share/fit-basecamp/config/scheduler.json"
59
- cp "$SCRIPT_DIR/uninstall.sh" "$PAYLOAD_DIR/usr/local/share/fit-basecamp/uninstall.sh"
60
- chmod +x "$PAYLOAD_DIR/usr/local/share/fit-basecamp/uninstall.sh"
61
-
62
- # --- Create scripts directory ------------------------------------------------
63
-
64
- mkdir -p "$SCRIPTS_DIR"
65
- cp "$SCRIPT_DIR/postinstall" "$SCRIPTS_DIR/postinstall"
66
- chmod +x "$SCRIPTS_DIR/postinstall"
67
-
68
- # --- Build component package -------------------------------------------------
69
-
70
- pkgbuild \
71
- --root "$PAYLOAD_DIR" \
72
- --scripts "$SCRIPTS_DIR" \
73
- --identifier "$IDENTIFIER" \
74
- --version "$VERSION" \
75
- --install-location "/" \
76
- "$COMPONENT_PKG"
77
-
78
- # --- Create distribution resources -------------------------------------------
79
-
80
- mkdir -p "$RESOURCES_DIR"
81
- cp "$SCRIPT_DIR/pkg-resources/welcome.html" "$RESOURCES_DIR/welcome.html"
82
- cp "$SCRIPT_DIR/pkg-resources/conclusion.html" "$RESOURCES_DIR/conclusion.html"
83
-
84
- # --- Create distribution.xml ------------------------------------------------
85
-
86
- DIST_XML="$DIST_DIR/distribution-$ARCH_SHORT.xml"
87
- cat > "$DIST_XML" <<EOF
88
- <?xml version="1.0" encoding="utf-8"?>
89
- <installer-gui-script minSpecVersion="2">
90
- <title>Basecamp ${VERSION}</title>
91
- <welcome file="welcome.html" mime-type="text/html" />
92
- <conclusion file="conclusion.html" mime-type="text/html" />
93
- <options customize="never" require-scripts="false" hostArchitectures="$ARCH_SHORT" />
94
- <domains enable_localSystem="true" />
95
- <pkg-ref id="$IDENTIFIER" version="$VERSION">pkg-component-$ARCH_SHORT.pkg</pkg-ref>
96
- <choices-outline>
97
- <line choice="$IDENTIFIER" />
98
- </choices-outline>
99
- <choice id="$IDENTIFIER" visible="false">
100
- <pkg-ref id="$IDENTIFIER" />
101
- </choice>
102
- </installer-gui-script>
103
- EOF
104
-
105
- # --- Build distribution package ----------------------------------------------
106
-
107
- productbuild \
108
- --distribution "$DIST_XML" \
109
- --resources "$RESOURCES_DIR" \
110
- --package-path "$DIST_DIR" \
111
- "$PKG_PATH"
112
-
113
- # --- Clean up staging --------------------------------------------------------
114
-
115
- rm -rf "$PAYLOAD_DIR" "$SCRIPTS_DIR" "$RESOURCES_DIR" "$COMPONENT_PKG" "$DIST_XML"
116
-
117
- echo " -> $PKG_PATH"
@@ -1,26 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # Compile Basecamp into a standalone Deno binary.
5
- #
6
- # Usage: compile.sh <dist_dir> <app_name> <target>
7
- # e.g. compile.sh dist basecamp aarch64-apple-darwin
8
-
9
- DIST_DIR="${1:?Usage: compile.sh <dist_dir> <app_name> <target>}"
10
- APP_NAME="${2:?Usage: compile.sh <dist_dir> <app_name> <target>}"
11
- TARGET="${3:?Usage: compile.sh <dist_dir> <app_name> <target>}"
12
-
13
- OUTPUT="$DIST_DIR/$APP_NAME-$TARGET"
14
-
15
- echo ""
16
- echo "Compiling $APP_NAME for $TARGET..."
17
- mkdir -p "$DIST_DIR"
18
-
19
- deno compile \
20
- --allow-all \
21
- --target "$TARGET" \
22
- --output "$OUTPUT" \
23
- --include template/ \
24
- scheduler.js
25
-
26
- echo " -> $OUTPUT"
@@ -1,108 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # Basecamp Installer (development / repo context)
5
- #
6
- # Sets up scheduler config, default knowledge base, and LaunchAgent for local
7
- # development. The compiled binary is installed via the .pkg installer instead.
8
- #
9
- # This script is for engineers running from the repo with deno or node.
10
-
11
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
- APP_NAME="fit-basecamp"
13
- BASECAMP_HOME="$HOME/.fit/basecamp"
14
- DEFAULT_KB="$HOME/Documents/Personal"
15
-
16
- echo ""
17
- echo "Basecamp Installer (dev)"
18
- echo "========================"
19
- echo ""
20
-
21
- # ---------------------------------------------------------------------------
22
- # 1. Set up scheduler home
23
- # ---------------------------------------------------------------------------
24
-
25
- echo "Setting up scheduler home at $BASECAMP_HOME ..."
26
- mkdir -p "$BASECAMP_HOME/logs"
27
-
28
- # ---------------------------------------------------------------------------
29
- # 2. Copy scheduler config (single source of truth: config/scheduler.json)
30
- # ---------------------------------------------------------------------------
31
-
32
- CONFIG_SRC=""
33
- if [ -f "$SCRIPT_DIR/../config/scheduler.json" ]; then
34
- CONFIG_SRC="$SCRIPT_DIR/../config/scheduler.json"
35
- fi
36
-
37
- if [ ! -f "$BASECAMP_HOME/scheduler.json" ]; then
38
- if [ -n "$CONFIG_SRC" ]; then
39
- cp "$CONFIG_SRC" "$BASECAMP_HOME/scheduler.json"
40
- echo " Created $BASECAMP_HOME/scheduler.json"
41
- else
42
- echo " Warning: config/scheduler.json not found, skipping config setup."
43
- fi
44
- else
45
- echo " Scheduler config already exists, skipping."
46
- fi
47
-
48
- # ---------------------------------------------------------------------------
49
- # 3. Initialize state file
50
- # ---------------------------------------------------------------------------
51
-
52
- if [ ! -f "$BASECAMP_HOME/state.json" ]; then
53
- echo '{ "tasks": {} }' > "$BASECAMP_HOME/state.json"
54
- echo " Created $BASECAMP_HOME/state.json"
55
- fi
56
-
57
- # ---------------------------------------------------------------------------
58
- # 4. Initialize default knowledge base
59
- # ---------------------------------------------------------------------------
60
-
61
- echo ""
62
- if [ ! -d "$DEFAULT_KB" ]; then
63
- echo "Initializing default knowledge base at $DEFAULT_KB ..."
64
- SCHEDULER="$SCRIPT_DIR/../scheduler.js"
65
- if command -v deno &>/dev/null && [ -f "$SCHEDULER" ]; then
66
- deno run --allow-all "$SCHEDULER" --init "$DEFAULT_KB"
67
- elif command -v node &>/dev/null && [ -f "$SCHEDULER" ]; then
68
- node "$SCHEDULER" --init "$DEFAULT_KB"
69
- else
70
- echo " Neither deno nor node found, skipping KB initialization."
71
- fi
72
- else
73
- echo "Basecamp already initialized at $DEFAULT_KB/"
74
- fi
75
-
76
- # ---------------------------------------------------------------------------
77
- # 5. Install LaunchAgent
78
- # ---------------------------------------------------------------------------
79
-
80
- echo ""
81
- echo "Installing background scheduler (LaunchAgent)..."
82
- SCHEDULER="$SCRIPT_DIR/../scheduler.js"
83
- if command -v deno &>/dev/null && [ -f "$SCHEDULER" ]; then
84
- deno run --allow-all "$SCHEDULER" --install-launchd
85
- elif command -v node &>/dev/null && [ -f "$SCHEDULER" ]; then
86
- node "$SCHEDULER" --install-launchd
87
- else
88
- echo " Neither deno nor node found, skipping LaunchAgent install."
89
- fi
90
-
91
- # ---------------------------------------------------------------------------
92
- # Summary
93
- # ---------------------------------------------------------------------------
94
-
95
- echo ""
96
- echo "Done! Basecamp is installed."
97
- echo ""
98
- echo " Config: $BASECAMP_HOME/scheduler.json"
99
- echo " Knowledge: $DEFAULT_KB/"
100
- echo " Logs: $BASECAMP_HOME/logs/"
101
- echo ""
102
- echo "Next steps:"
103
- echo " 1. Edit $DEFAULT_KB/USER.md with your name, email, and domain"
104
- echo " 2. Edit $BASECAMP_HOME/scheduler.json to configure tasks"
105
- echo " 3. Run the scheduler: deno run --allow-all scheduler.js --status"
106
- echo " 4. Start the daemon: deno run --allow-all scheduler.js --install-launchd"
107
- echo " 5. Open your KB: cd $DEFAULT_KB && claude"
108
- echo ""
@@ -1,62 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <style>
5
- body {
6
- font-family:
7
- -apple-system,
8
- Helvetica Neue,
9
- sans-serif;
10
- padding: 20px;
11
- color: #1d1d1f;
12
- }
13
- h1 {
14
- font-size: 22px;
15
- font-weight: 600;
16
- margin-bottom: 16px;
17
- }
18
- p {
19
- font-size: 14px;
20
- line-height: 1.6;
21
- color: #424245;
22
- }
23
- code {
24
- font-family:
25
- SF Mono,
26
- Menlo,
27
- monospace;
28
- font-size: 13px;
29
- background: #f5f5f7;
30
- padding: 2px 6px;
31
- border-radius: 4px;
32
- }
33
- .cmd {
34
- margin: 8px 0 8px 16px;
35
- }
36
- .note {
37
- font-size: 12px;
38
- color: #86868b;
39
- margin-top: 20px;
40
- }
41
- </style>
42
- </head>
43
- <body>
44
- <h1>Basecamp is installed</h1>
45
- <p>
46
- The scheduler is running in the background and will start automatically on
47
- login.
48
- </p>
49
- <p><strong>Next steps:</strong></p>
50
- <p>1. Edit your identity file:</p>
51
- <p class="cmd"><code>vim ~/Documents/Personal/USER.md</code></p>
52
- <p>2. Configure scheduled tasks:</p>
53
- <p class="cmd"><code>vim ~/.fit/basecamp/scheduler.json</code></p>
54
- <p>3. Check scheduler status:</p>
55
- <p class="cmd"><code>fit-basecamp --status</code></p>
56
- <p>4. Open your knowledge base interactively:</p>
57
- <p class="cmd"><code>cd ~/Documents/Personal &amp;&amp; claude</code></p>
58
- <p class="note">
59
- To uninstall, run: <code>/usr/local/share/fit-basecamp/uninstall.sh</code>
60
- </p>
61
- </body>
62
- </html>
@@ -1,64 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <style>
5
- body {
6
- font-family:
7
- -apple-system,
8
- Helvetica Neue,
9
- sans-serif;
10
- padding: 20px;
11
- color: #1d1d1f;
12
- }
13
- h1 {
14
- font-size: 22px;
15
- font-weight: 600;
16
- margin-bottom: 16px;
17
- }
18
- p {
19
- font-size: 14px;
20
- line-height: 1.6;
21
- color: #424245;
22
- }
23
- ul {
24
- font-size: 14px;
25
- line-height: 1.8;
26
- color: #424245;
27
- padding-left: 20px;
28
- }
29
- .note {
30
- font-size: 12px;
31
- color: #86868b;
32
- margin-top: 20px;
33
- }
34
- </style>
35
- </head>
36
- <body>
37
- <h1>Basecamp</h1>
38
- <p>
39
- Personal knowledge system with scheduled AI tasks. No server, no database.
40
- just plain files, markdown, and Claude Code.
41
- </p>
42
- <p>This installer will:</p>
43
- <ul>
44
- <li>
45
- Install the <code>fit-basecamp</code> binary to
46
- <code>/usr/local/bin/</code>
47
- </li>
48
- <li>
49
- Set up the scheduler configuration at <code>~/.fit/basecamp/</code>
50
- </li>
51
- <li>
52
- Initialize a default knowledge base at
53
- <code>~/Documents/Personal/</code>
54
- </li>
55
- <li>
56
- Install a LaunchAgent so the scheduler runs automatically on login
57
- </li>
58
- </ul>
59
- <p class="note">
60
- Requires the Claude Code CLI (<code>claude</code>) to be installed and
61
- authenticated.
62
- </p>
63
- </body>
64
- </html>
@@ -1,46 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Basecamp postinstall script — runs as root after macOS pkg installation.
4
- #
5
- # Sets up per-user config, default knowledge base, and LaunchAgent for the
6
- # console user (the person who double-clicked the installer).
7
-
8
- REAL_USER=$(/usr/bin/stat -f "%Su" /dev/console)
9
- REAL_HOME=$(/usr/bin/dscl . -read "/Users/$REAL_USER" NFSHomeDirectory | /usr/bin/awk '{print $2}')
10
-
11
- BASECAMP_HOME="$REAL_HOME/.fit/basecamp"
12
- DEFAULT_KB="$REAL_HOME/Documents/Personal"
13
- SHARE_DIR="/usr/local/share/fit-basecamp"
14
- BINARY="/usr/local/bin/fit-basecamp"
15
-
16
- # --- Config home -------------------------------------------------------------
17
-
18
- /usr/bin/sudo -u "$REAL_USER" /bin/mkdir -p "$BASECAMP_HOME/logs"
19
-
20
- if [ ! -f "$BASECAMP_HOME/scheduler.json" ] && [ -f "$SHARE_DIR/config/scheduler.json" ]; then
21
- /usr/bin/sudo -u "$REAL_USER" /bin/cp "$SHARE_DIR/config/scheduler.json" "$BASECAMP_HOME/scheduler.json"
22
- fi
23
-
24
- if [ ! -f "$BASECAMP_HOME/state.json" ]; then
25
- echo '{ "tasks": {} }' | /usr/bin/sudo -u "$REAL_USER" /usr/bin/tee "$BASECAMP_HOME/state.json" > /dev/null
26
- fi
27
-
28
- # --- Default knowledge base --------------------------------------------------
29
-
30
- if [ ! -d "$DEFAULT_KB" ]; then
31
- /usr/bin/sudo -u "$REAL_USER" "$BINARY" --init "$DEFAULT_KB" 2>/dev/null || true
32
- fi
33
-
34
- # --- LaunchAgent (runs as user, not root) ------------------------------------
35
-
36
- PLIST="$REAL_HOME/Library/LaunchAgents/com.fit-basecamp.scheduler.plist"
37
- if [ -f "$PLIST" ]; then
38
- # Upgrade: binary already replaced by pkg payload. Kill the old daemon and
39
- # let KeepAlive restart it with the new binary. No plist rewrite needed.
40
- /usr/bin/killall fit-basecamp 2>/dev/null || true
41
- else
42
- # Fresh install: create plist and load it.
43
- /usr/bin/sudo -u "$REAL_USER" "$BINARY" --install-launchd 2>/dev/null || true
44
- fi
45
-
46
- exit 0
@@ -1,56 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # Basecamp Uninstaller
5
- #
6
- # Removes the binary, LaunchAgent, and shared data installed by the .pkg.
7
- # User data at ~/Documents/Personal/ and config at ~/.fit/basecamp/ are preserved.
8
-
9
- APP_NAME="${1:-fit-basecamp}"
10
- PLIST_NAME="${2:-com.fit-basecamp.scheduler}"
11
-
12
- echo ""
13
- echo "Basecamp Uninstaller"
14
- echo "====================="
15
- echo ""
16
-
17
- # Remove LaunchAgent
18
- PLIST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist"
19
- if [ -f "$PLIST" ]; then
20
- launchctl unload "$PLIST" 2>/dev/null || true
21
- rm -f "$PLIST"
22
- echo " Removed LaunchAgent"
23
- else
24
- echo " LaunchAgent not found, skipping."
25
- fi
26
-
27
- # Remove binary
28
- if [ -f "/usr/local/bin/$APP_NAME" ]; then
29
- sudo rm -f "/usr/local/bin/$APP_NAME"
30
- echo " Removed /usr/local/bin/$APP_NAME"
31
- else
32
- echo " Binary not found at /usr/local/bin/$APP_NAME, skipping."
33
- fi
34
-
35
- # Remove shared data (default config template, this uninstall script's installed copy)
36
- if [ -d "/usr/local/share/fit-basecamp" ]; then
37
- sudo rm -rf "/usr/local/share/fit-basecamp"
38
- echo " Removed /usr/local/share/fit-basecamp/"
39
- else
40
- echo " Shared data not found, skipping."
41
- fi
42
-
43
- # Forget pkg receipt
44
- pkgutil --pkgs 2>/dev/null | grep -q "com.fit-basecamp.scheduler" && {
45
- sudo pkgutil --forget "com.fit-basecamp.scheduler" >/dev/null 2>&1
46
- echo " Removed installer receipt"
47
- } || true
48
-
49
- echo ""
50
- echo "Basecamp uninstalled."
51
- echo "Your data at ~/Documents/Personal/ has been preserved."
52
- echo "Your config at ~/.fit/basecamp/ has been preserved."
53
- echo ""
54
- echo "To remove all data: rm -rf ~/Documents/Personal/"
55
- echo "To remove all config: rm -rf ~/.fit/basecamp/"
56
- echo ""