@forwardimpact/basecamp 0.1.0 → 0.2.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 +1 -1
- package/{scheduler.js → basecamp.js} +248 -49
- package/build.js +48 -50
- package/package.json +8 -8
- package/scripts/build-pkg.sh +21 -23
- package/scripts/compile.sh +9 -10
- package/scripts/install.sh +4 -4
- package/scripts/postinstall +38 -0
- package/scripts/uninstall.sh +17 -0
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ fit-basecamp --status Show knowledge bases and task status
|
|
|
186
186
|
fit-basecamp --help Show this help
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
When running from source, use `node
|
|
189
|
+
When running from source, use `node basecamp.js` or `npx fit-basecamp` instead
|
|
190
190
|
of `fit-basecamp`.
|
|
191
191
|
|
|
192
192
|
## How It Works
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Basecamp
|
|
3
|
+
// Basecamp — CLI and scheduler for personal knowledge bases.
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// node
|
|
7
|
-
// node
|
|
8
|
-
// node
|
|
9
|
-
// node
|
|
10
|
-
// node
|
|
11
|
-
// node
|
|
12
|
-
// node
|
|
13
|
-
// node
|
|
14
|
-
// node
|
|
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 --install-launchd Install macOS LaunchAgent
|
|
11
|
+
// node basecamp.js --uninstall-launchd Remove macOS LaunchAgent
|
|
12
|
+
// node basecamp.js --validate Validate agents and skills exist
|
|
13
|
+
// node basecamp.js --status Show task status
|
|
14
|
+
// node basecamp.js --help Show this help
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
readFileSync,
|
|
@@ -19,11 +19,15 @@ import {
|
|
|
19
19
|
existsSync,
|
|
20
20
|
mkdirSync,
|
|
21
21
|
readdirSync,
|
|
22
|
+
unlinkSync,
|
|
23
|
+
chmodSync,
|
|
22
24
|
} from "node:fs";
|
|
23
25
|
import { execSync } from "node:child_process";
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
24
27
|
import { join, dirname, resolve } from "node:path";
|
|
25
28
|
import { homedir } from "node:os";
|
|
26
29
|
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { createServer } from "node:net";
|
|
27
31
|
|
|
28
32
|
const HOME = homedir();
|
|
29
33
|
const BASECAMP_HOME = join(HOME, ".fit", "basecamp");
|
|
@@ -35,11 +39,14 @@ const PLIST_PATH = join(HOME, "Library", "LaunchAgents", `${PLIST_NAME}.plist`);
|
|
|
35
39
|
const __dirname =
|
|
36
40
|
import.meta.dirname || dirname(fileURLToPath(import.meta.url));
|
|
37
41
|
const KB_TEMPLATE_DIR = join(__dirname, "template");
|
|
42
|
+
const SOCKET_PATH = join(BASECAMP_HOME, "basecamp.sock");
|
|
38
43
|
const IS_COMPILED =
|
|
39
44
|
typeof Deno !== "undefined" &&
|
|
40
45
|
Deno.execPath &&
|
|
41
46
|
!Deno.execPath().endsWith("deno");
|
|
42
47
|
|
|
48
|
+
let daemonStartedAt = null;
|
|
49
|
+
|
|
43
50
|
// --- Helpers ----------------------------------------------------------------
|
|
44
51
|
|
|
45
52
|
function ensureDir(dir) {
|
|
@@ -152,6 +159,7 @@ function floorToMinute(d) {
|
|
|
152
159
|
|
|
153
160
|
function shouldRun(task, taskState, now) {
|
|
154
161
|
if (task.enabled === false) return false;
|
|
162
|
+
if (taskState.status === "running") return false;
|
|
155
163
|
const { schedule } = task;
|
|
156
164
|
if (!schedule) return false;
|
|
157
165
|
const lastRun = taskState.lastRunAt ? new Date(taskState.lastRunAt) : null;
|
|
@@ -197,63 +205,254 @@ function runTask(taskName, task, _config, state) {
|
|
|
197
205
|
ts.startedAt = new Date().toISOString();
|
|
198
206
|
saveState(state);
|
|
199
207
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
);
|
|
208
|
+
const spawnArgs = ["--print"];
|
|
209
|
+
if (task.agent) spawnArgs.push("--agent", task.agent);
|
|
210
|
+
spawnArgs.push("-p", prompt);
|
|
214
211
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
runCount: (ts.runCount || 0) + 1,
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
const child = spawn(claude, spawnArgs, {
|
|
214
|
+
cwd: kbPath,
|
|
215
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
216
|
+
timeout: 30 * 60_000,
|
|
221
217
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
|
|
219
|
+
let stdout = "";
|
|
220
|
+
let stderr = "";
|
|
221
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
222
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
223
|
+
|
|
224
|
+
child.on("close", (code) => {
|
|
225
|
+
if (code === 0) {
|
|
226
|
+
log(`Task ${taskName} completed. Output: ${stdout.slice(0, 200)}...`);
|
|
227
|
+
Object.assign(ts, {
|
|
228
|
+
status: "finished",
|
|
229
|
+
startedAt: null,
|
|
230
|
+
lastRunAt: new Date().toISOString(),
|
|
231
|
+
lastError: null,
|
|
232
|
+
runCount: (ts.runCount || 0) + 1,
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
const errMsg = stderr || stdout || `Exit code ${code}`;
|
|
236
|
+
log(`Task ${taskName} failed: ${errMsg.slice(0, 300)}`);
|
|
237
|
+
Object.assign(ts, {
|
|
238
|
+
status: "failed",
|
|
239
|
+
startedAt: null,
|
|
240
|
+
lastRunAt: new Date().toISOString(),
|
|
241
|
+
lastError: errMsg.slice(0, 500),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
saveState(state);
|
|
245
|
+
resolve();
|
|
229
246
|
});
|
|
230
|
-
|
|
231
|
-
|
|
247
|
+
|
|
248
|
+
child.on("error", (err) => {
|
|
249
|
+
log(`Task ${taskName} failed: ${err.message}`);
|
|
250
|
+
Object.assign(ts, {
|
|
251
|
+
status: "failed",
|
|
252
|
+
startedAt: null,
|
|
253
|
+
lastRunAt: new Date().toISOString(),
|
|
254
|
+
lastError: err.message.slice(0, 500),
|
|
255
|
+
});
|
|
256
|
+
saveState(state);
|
|
257
|
+
resolve();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
232
260
|
}
|
|
233
261
|
|
|
234
|
-
function runDueTasks() {
|
|
262
|
+
async function runDueTasks() {
|
|
235
263
|
const config = loadConfig(),
|
|
236
264
|
state = loadState(),
|
|
237
265
|
now = new Date();
|
|
238
266
|
let ranAny = false;
|
|
239
267
|
for (const [name, task] of Object.entries(config.tasks)) {
|
|
240
268
|
if (shouldRun(task, state.tasks[name] || {}, now)) {
|
|
241
|
-
runTask(name, task, config, state);
|
|
269
|
+
await runTask(name, task, config, state);
|
|
242
270
|
ranAny = true;
|
|
243
271
|
}
|
|
244
272
|
}
|
|
245
273
|
if (!ranAny) log("No tasks due.");
|
|
246
274
|
}
|
|
247
275
|
|
|
276
|
+
// --- Next-run computation ---------------------------------------------------
|
|
277
|
+
|
|
278
|
+
/** @param {object} task @param {object} taskState @param {Date} now */
|
|
279
|
+
function computeNextRunAt(task, taskState, now) {
|
|
280
|
+
if (task.enabled === false) return null;
|
|
281
|
+
const { schedule } = task;
|
|
282
|
+
if (!schedule) return null;
|
|
283
|
+
|
|
284
|
+
if (schedule.type === "interval") {
|
|
285
|
+
const ms = (schedule.minutes || 5) * 60_000;
|
|
286
|
+
const lastRun = taskState.lastRunAt ? new Date(taskState.lastRunAt) : null;
|
|
287
|
+
if (!lastRun) return now.toISOString();
|
|
288
|
+
return new Date(lastRun.getTime() + ms).toISOString();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (schedule.type === "cron") {
|
|
292
|
+
const limit = 24 * 60;
|
|
293
|
+
const start = new Date(floorToMinute(now) + 60_000);
|
|
294
|
+
for (let i = 0; i < limit; i++) {
|
|
295
|
+
const candidate = new Date(start.getTime() + i * 60_000);
|
|
296
|
+
if (cronMatches(schedule.expression, candidate)) {
|
|
297
|
+
return candidate.toISOString();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (schedule.type === "once") {
|
|
304
|
+
if (taskState.lastRunAt) return null;
|
|
305
|
+
return schedule.runAt;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Socket server ----------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/** @param {import('node:net').Socket} socket @param {object} data */
|
|
314
|
+
function send(socket, data) {
|
|
315
|
+
try {
|
|
316
|
+
socket.write(JSON.stringify(data) + "\n");
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleStatusRequest(socket) {
|
|
321
|
+
const config = loadConfig();
|
|
322
|
+
const state = loadState();
|
|
323
|
+
const now = new Date();
|
|
324
|
+
const tasks = {};
|
|
325
|
+
|
|
326
|
+
for (const [name, task] of Object.entries(config.tasks)) {
|
|
327
|
+
const ts = state.tasks[name] || {};
|
|
328
|
+
tasks[name] = {
|
|
329
|
+
enabled: task.enabled !== false,
|
|
330
|
+
status: ts.status || "never-run",
|
|
331
|
+
lastRunAt: ts.lastRunAt || null,
|
|
332
|
+
nextRunAt: computeNextRunAt(task, ts, now),
|
|
333
|
+
runCount: ts.runCount || 0,
|
|
334
|
+
lastError: ts.lastError || null,
|
|
335
|
+
};
|
|
336
|
+
if (ts.startedAt) tasks[name].startedAt = ts.startedAt;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
send(socket, {
|
|
340
|
+
type: "status",
|
|
341
|
+
uptime: daemonStartedAt
|
|
342
|
+
? Math.floor((Date.now() - daemonStartedAt) / 1000)
|
|
343
|
+
: 0,
|
|
344
|
+
tasks,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function handleRestartRequest(socket) {
|
|
349
|
+
send(socket, { type: "ack", command: "restart" });
|
|
350
|
+
const uid = execSync("id -u", { encoding: "utf8" }).trim();
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
try {
|
|
353
|
+
execSync(`launchctl kickstart -k gui/${uid}/${PLIST_NAME}`);
|
|
354
|
+
} catch {
|
|
355
|
+
process.exit(0);
|
|
356
|
+
}
|
|
357
|
+
}, 100);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleRunRequest(socket, taskName) {
|
|
361
|
+
if (!taskName) {
|
|
362
|
+
send(socket, { type: "error", message: "Missing task name" });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const config = loadConfig();
|
|
366
|
+
const task = config.tasks[taskName];
|
|
367
|
+
if (!task) {
|
|
368
|
+
send(socket, { type: "error", message: `Task not found: ${taskName}` });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
send(socket, { type: "ack", command: "run", task: taskName });
|
|
372
|
+
const state = loadState();
|
|
373
|
+
runTask(taskName, task, config, state).catch((err) => {
|
|
374
|
+
console.error(`[socket] runTask error for ${taskName}:`, err.message);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function handleMessage(socket, line) {
|
|
379
|
+
let request;
|
|
380
|
+
try {
|
|
381
|
+
request = JSON.parse(line);
|
|
382
|
+
} catch {
|
|
383
|
+
send(socket, { type: "error", message: "Invalid JSON" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const handlers = {
|
|
388
|
+
status: () => handleStatusRequest(socket),
|
|
389
|
+
restart: () => handleRestartRequest(socket),
|
|
390
|
+
run: () => handleRunRequest(socket, request.task),
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const handler = handlers[request.type];
|
|
394
|
+
if (handler) {
|
|
395
|
+
handler();
|
|
396
|
+
} else {
|
|
397
|
+
send(socket, {
|
|
398
|
+
type: "error",
|
|
399
|
+
message: `Unknown request type: ${request.type}`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function startSocketServer() {
|
|
405
|
+
try {
|
|
406
|
+
unlinkSync(SOCKET_PATH);
|
|
407
|
+
} catch {}
|
|
408
|
+
|
|
409
|
+
const server = createServer((socket) => {
|
|
410
|
+
let buffer = "";
|
|
411
|
+
socket.on("data", (data) => {
|
|
412
|
+
buffer += data.toString();
|
|
413
|
+
let idx;
|
|
414
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
415
|
+
const line = buffer.slice(0, idx).trim();
|
|
416
|
+
buffer = buffer.slice(idx + 1);
|
|
417
|
+
if (line) handleMessage(socket, line);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
socket.on("error", () => {});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
server.listen(SOCKET_PATH, () => {
|
|
424
|
+
chmodSync(SOCKET_PATH, 0o600);
|
|
425
|
+
log(`Socket server listening on ${SOCKET_PATH}`);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
server.on("error", (err) => {
|
|
429
|
+
log(`Socket server error: ${err.message}`);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const cleanup = () => {
|
|
433
|
+
server.close();
|
|
434
|
+
try {
|
|
435
|
+
unlinkSync(SOCKET_PATH);
|
|
436
|
+
} catch {}
|
|
437
|
+
process.exit(0);
|
|
438
|
+
};
|
|
439
|
+
process.on("SIGTERM", cleanup);
|
|
440
|
+
process.on("SIGINT", cleanup);
|
|
441
|
+
|
|
442
|
+
return server;
|
|
443
|
+
}
|
|
444
|
+
|
|
248
445
|
// --- Daemon -----------------------------------------------------------------
|
|
249
446
|
|
|
250
447
|
function daemon() {
|
|
448
|
+
daemonStartedAt = Date.now();
|
|
251
449
|
log("Scheduler daemon started. Polling every 60 seconds.");
|
|
252
450
|
log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
|
|
451
|
+
startSocketServer();
|
|
253
452
|
runDueTasks();
|
|
254
|
-
setInterval(() => {
|
|
453
|
+
setInterval(async () => {
|
|
255
454
|
try {
|
|
256
|
-
runDueTasks();
|
|
455
|
+
await runDueTasks();
|
|
257
456
|
} catch (err) {
|
|
258
457
|
log(`Error: ${err.message}`);
|
|
259
458
|
}
|
|
@@ -307,7 +506,7 @@ function installLaunchd() {
|
|
|
307
506
|
const isCompiled = IS_COMPILED || !execPath.includes("node");
|
|
308
507
|
const progArgs = isCompiled
|
|
309
508
|
? ` <string>${execPath}</string>\n <string>--daemon</string>`
|
|
310
|
-
: ` <string>${execPath}</string>\n <string>${join(__dirname, "
|
|
509
|
+
: ` <string>${execPath}</string>\n <string>${join(__dirname, "basecamp.js")}</string>\n <string>--daemon</string>`;
|
|
311
510
|
|
|
312
511
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
313
512
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -526,14 +725,14 @@ const commands = {
|
|
|
526
725
|
"--status": showStatus,
|
|
527
726
|
"--init": () => {
|
|
528
727
|
if (!args[1]) {
|
|
529
|
-
console.error("Usage: node
|
|
728
|
+
console.error("Usage: node basecamp.js --init <path>");
|
|
530
729
|
process.exit(1);
|
|
531
730
|
}
|
|
532
731
|
initKB(args[1]);
|
|
533
732
|
},
|
|
534
|
-
"--run": () => {
|
|
733
|
+
"--run": async () => {
|
|
535
734
|
if (!args[1]) {
|
|
536
|
-
console.error("Usage: node
|
|
735
|
+
console.error("Usage: node basecamp.js --run <task-name>");
|
|
537
736
|
process.exit(1);
|
|
538
737
|
}
|
|
539
738
|
const config = loadConfig(),
|
|
@@ -545,8 +744,8 @@ const commands = {
|
|
|
545
744
|
);
|
|
546
745
|
process.exit(1);
|
|
547
746
|
}
|
|
548
|
-
runTask(args[1], task, config, state);
|
|
747
|
+
await runTask(args[1], task, config, state);
|
|
549
748
|
},
|
|
550
749
|
};
|
|
551
750
|
|
|
552
|
-
(commands[command] || runDueTasks)();
|
|
751
|
+
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
|
|
7
|
-
// deno run --allow-all build.js --pkg
|
|
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
|
|
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(
|
|
49
|
-
const
|
|
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}
|
|
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
|
-
|
|
49
|
+
"--no-check",
|
|
60
50
|
`--output "${outputPath}"`,
|
|
61
51
|
"--include template/",
|
|
62
|
-
"
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
82
|
-
const
|
|
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
93
|
const buildPkg = join(__dirname, "scripts", "build-pkg.sh");
|
|
88
|
-
run(
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Claude Code-native personal knowledge system with scheduled tasks",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -10,22 +10,22 @@
|
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://www.forwardimpact.team/basecamp",
|
|
12
12
|
"type": "module",
|
|
13
|
-
"main": "
|
|
13
|
+
"main": "basecamp.js",
|
|
14
14
|
"bin": {
|
|
15
|
-
"fit-basecamp": "./
|
|
15
|
+
"fit-basecamp": "./basecamp.js"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"start": "node
|
|
19
|
-
"status": "node
|
|
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
23
|
"install:basecamp": "./scripts/install.sh",
|
|
24
|
-
"scheduler:install": "node
|
|
25
|
-
"scheduler:uninstall": "node
|
|
24
|
+
"scheduler:install": "node basecamp.js --install-launchd",
|
|
25
|
+
"scheduler:uninstall": "node basecamp.js --uninstall-launchd"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"
|
|
28
|
+
"basecamp.js",
|
|
29
29
|
"build.js",
|
|
30
30
|
"config/",
|
|
31
31
|
"scripts/",
|
package/scripts/build-pkg.sh
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
-
# Build a macOS installer package (.pkg) for Basecamp.
|
|
4
|
+
# Build a macOS installer package (.pkg) for Basecamp (arm64).
|
|
5
5
|
#
|
|
6
6
|
# Uses pkgbuild (component) + productbuild (distribution) to create a .pkg
|
|
7
7
|
# that installs the binary to /usr/local/bin/ and runs a postinstall script
|
|
8
8
|
# to set up the LaunchAgent, config, and default knowledge base.
|
|
9
9
|
#
|
|
10
|
-
# Usage: build-pkg.sh <dist_dir> <app_name> <version> <
|
|
11
|
-
# e.g. build-pkg.sh dist basecamp 1.0.0
|
|
10
|
+
# Usage: build-pkg.sh <dist_dir> <app_name> <version> <status_menu_binary>
|
|
11
|
+
# e.g. build-pkg.sh dist fit-basecamp 1.0.0 dist/BasecampStatus
|
|
12
12
|
|
|
13
|
-
DIST_DIR="${1:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <
|
|
14
|
-
APP_NAME="${2:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <
|
|
15
|
-
VERSION="${3:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <
|
|
16
|
-
|
|
13
|
+
DIST_DIR="${1:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <status_menu_binary>}"
|
|
14
|
+
APP_NAME="${2:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <status_menu_binary>}"
|
|
15
|
+
VERSION="${3:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <status_menu_binary>}"
|
|
16
|
+
STATUS_MENU_BINARY="${4:?Usage: build-pkg.sh <dist_dir> <app_name> <version> <status_menu_binary>}"
|
|
17
17
|
|
|
18
18
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
19
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
20
|
-
BINARY_PATH="$DIST_DIR/$APP_NAME
|
|
20
|
+
BINARY_PATH="$DIST_DIR/$APP_NAME"
|
|
21
21
|
IDENTIFIER="com.fit-basecamp.scheduler"
|
|
22
22
|
|
|
23
23
|
if [ ! -f "$BINARY_PATH" ]; then
|
|
@@ -26,18 +26,12 @@ if [ ! -f "$BINARY_PATH" ]; then
|
|
|
26
26
|
exit 1
|
|
27
27
|
fi
|
|
28
28
|
|
|
29
|
-
|
|
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"
|
|
29
|
+
PKG_NAME="$APP_NAME-$VERSION.pkg"
|
|
36
30
|
PKG_PATH="$DIST_DIR/$PKG_NAME"
|
|
37
|
-
PAYLOAD_DIR="$DIST_DIR/pkg-payload
|
|
38
|
-
SCRIPTS_DIR="$DIST_DIR/pkg-scripts
|
|
39
|
-
RESOURCES_DIR="$DIST_DIR/pkg-resources
|
|
40
|
-
COMPONENT_PKG="$DIST_DIR/pkg-component
|
|
31
|
+
PAYLOAD_DIR="$DIST_DIR/pkg-payload"
|
|
32
|
+
SCRIPTS_DIR="$DIST_DIR/pkg-scripts"
|
|
33
|
+
RESOURCES_DIR="$DIST_DIR/pkg-resources"
|
|
34
|
+
COMPONENT_PKG="$DIST_DIR/pkg-component.pkg"
|
|
41
35
|
|
|
42
36
|
echo ""
|
|
43
37
|
echo "Building pkg: $PKG_NAME..."
|
|
@@ -59,6 +53,10 @@ cp "$PROJECT_DIR/config/scheduler.json" "$PAYLOAD_DIR/usr/local/share/fit-baseca
|
|
|
59
53
|
cp "$SCRIPT_DIR/uninstall.sh" "$PAYLOAD_DIR/usr/local/share/fit-basecamp/uninstall.sh"
|
|
60
54
|
chmod +x "$PAYLOAD_DIR/usr/local/share/fit-basecamp/uninstall.sh"
|
|
61
55
|
|
|
56
|
+
# Status menu binary
|
|
57
|
+
cp "$STATUS_MENU_BINARY" "$PAYLOAD_DIR/usr/local/bin/BasecampStatus"
|
|
58
|
+
chmod +x "$PAYLOAD_DIR/usr/local/bin/BasecampStatus"
|
|
59
|
+
|
|
62
60
|
# --- Create scripts directory ------------------------------------------------
|
|
63
61
|
|
|
64
62
|
mkdir -p "$SCRIPTS_DIR"
|
|
@@ -83,16 +81,16 @@ cp "$SCRIPT_DIR/pkg-resources/conclusion.html" "$RESOURCES_DIR/conclusion.html"
|
|
|
83
81
|
|
|
84
82
|
# --- Create distribution.xml ------------------------------------------------
|
|
85
83
|
|
|
86
|
-
DIST_XML="$DIST_DIR/distribution
|
|
84
|
+
DIST_XML="$DIST_DIR/distribution.xml"
|
|
87
85
|
cat > "$DIST_XML" <<EOF
|
|
88
86
|
<?xml version="1.0" encoding="utf-8"?>
|
|
89
87
|
<installer-gui-script minSpecVersion="2">
|
|
90
88
|
<title>Basecamp ${VERSION}</title>
|
|
91
89
|
<welcome file="welcome.html" mime-type="text/html" />
|
|
92
90
|
<conclusion file="conclusion.html" mime-type="text/html" />
|
|
93
|
-
<options customize="never" require-scripts="false" hostArchitectures="
|
|
91
|
+
<options customize="never" require-scripts="false" hostArchitectures="arm64" />
|
|
94
92
|
<domains enable_localSystem="true" />
|
|
95
|
-
<pkg-ref id="$IDENTIFIER" version="$VERSION">pkg-component
|
|
93
|
+
<pkg-ref id="$IDENTIFIER" version="$VERSION">pkg-component.pkg</pkg-ref>
|
|
96
94
|
<choices-outline>
|
|
97
95
|
<line choice="$IDENTIFIER" />
|
|
98
96
|
</choices-outline>
|
|
@@ -114,4 +112,4 @@ productbuild \
|
|
|
114
112
|
|
|
115
113
|
rm -rf "$PAYLOAD_DIR" "$SCRIPTS_DIR" "$RESOURCES_DIR" "$COMPONENT_PKG" "$DIST_XML"
|
|
116
114
|
|
|
117
|
-
echo " -> $
|
|
115
|
+
echo " -> $PKG_NAME"
|
package/scripts/compile.sh
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
-
# Compile Basecamp into a standalone Deno binary.
|
|
4
|
+
# Compile Basecamp into a standalone Deno binary (arm64 macOS).
|
|
5
5
|
#
|
|
6
|
-
# Usage: compile.sh <dist_dir> <app_name>
|
|
7
|
-
# e.g. compile.sh dist basecamp
|
|
6
|
+
# Usage: compile.sh <dist_dir> <app_name>
|
|
7
|
+
# e.g. compile.sh dist fit-basecamp
|
|
8
8
|
|
|
9
|
-
DIST_DIR="${1:?Usage: compile.sh <dist_dir> <app_name>
|
|
10
|
-
APP_NAME="${2:?Usage: compile.sh <dist_dir> <app_name>
|
|
11
|
-
TARGET="${3:?Usage: compile.sh <dist_dir> <app_name> <target>}"
|
|
9
|
+
DIST_DIR="${1:?Usage: compile.sh <dist_dir> <app_name>}"
|
|
10
|
+
APP_NAME="${2:?Usage: compile.sh <dist_dir> <app_name>}"
|
|
12
11
|
|
|
13
|
-
OUTPUT="$DIST_DIR/$APP_NAME
|
|
12
|
+
OUTPUT="$DIST_DIR/$APP_NAME"
|
|
14
13
|
|
|
15
14
|
echo ""
|
|
16
|
-
echo "Compiling $APP_NAME
|
|
15
|
+
echo "Compiling $APP_NAME..."
|
|
17
16
|
mkdir -p "$DIST_DIR"
|
|
18
17
|
|
|
19
18
|
deno compile \
|
|
20
19
|
--allow-all \
|
|
21
|
-
--
|
|
20
|
+
--no-check \
|
|
22
21
|
--output "$OUTPUT" \
|
|
23
22
|
--include template/ \
|
|
24
|
-
|
|
23
|
+
basecamp.js
|
|
25
24
|
|
|
26
25
|
echo " -> $OUTPUT"
|
package/scripts/install.sh
CHANGED
|
@@ -61,7 +61,7 @@ fi
|
|
|
61
61
|
echo ""
|
|
62
62
|
if [ ! -d "$DEFAULT_KB" ]; then
|
|
63
63
|
echo "Initializing default knowledge base at $DEFAULT_KB ..."
|
|
64
|
-
SCHEDULER="$SCRIPT_DIR/../
|
|
64
|
+
SCHEDULER="$SCRIPT_DIR/../basecamp.js"
|
|
65
65
|
if command -v deno &>/dev/null && [ -f "$SCHEDULER" ]; then
|
|
66
66
|
deno run --allow-all "$SCHEDULER" --init "$DEFAULT_KB"
|
|
67
67
|
elif command -v node &>/dev/null && [ -f "$SCHEDULER" ]; then
|
|
@@ -79,7 +79,7 @@ fi
|
|
|
79
79
|
|
|
80
80
|
echo ""
|
|
81
81
|
echo "Installing background scheduler (LaunchAgent)..."
|
|
82
|
-
SCHEDULER="$SCRIPT_DIR/../
|
|
82
|
+
SCHEDULER="$SCRIPT_DIR/../basecamp.js"
|
|
83
83
|
if command -v deno &>/dev/null && [ -f "$SCHEDULER" ]; then
|
|
84
84
|
deno run --allow-all "$SCHEDULER" --install-launchd
|
|
85
85
|
elif command -v node &>/dev/null && [ -f "$SCHEDULER" ]; then
|
|
@@ -102,7 +102,7 @@ echo ""
|
|
|
102
102
|
echo "Next steps:"
|
|
103
103
|
echo " 1. Edit $DEFAULT_KB/USER.md with your name, email, and domain"
|
|
104
104
|
echo " 2. Edit $BASECAMP_HOME/scheduler.json to configure tasks"
|
|
105
|
-
echo " 3. Run the scheduler: deno run --allow-all
|
|
106
|
-
echo " 4. Start the daemon: deno run --allow-all
|
|
105
|
+
echo " 3. Run the scheduler: deno run --allow-all basecamp.js --status"
|
|
106
|
+
echo " 4. Start the daemon: deno run --allow-all basecamp.js --install-launchd"
|
|
107
107
|
echo " 5. Open your KB: cd $DEFAULT_KB && claude"
|
|
108
108
|
echo ""
|
package/scripts/postinstall
CHANGED
|
@@ -12,6 +12,7 @@ BASECAMP_HOME="$REAL_HOME/.fit/basecamp"
|
|
|
12
12
|
DEFAULT_KB="$REAL_HOME/Documents/Personal"
|
|
13
13
|
SHARE_DIR="/usr/local/share/fit-basecamp"
|
|
14
14
|
BINARY="/usr/local/bin/fit-basecamp"
|
|
15
|
+
STATUS_MENU_BINARY="/usr/local/bin/BasecampStatus"
|
|
15
16
|
|
|
16
17
|
# --- Config home -------------------------------------------------------------
|
|
17
18
|
|
|
@@ -43,4 +44,41 @@ else
|
|
|
43
44
|
/usr/bin/sudo -u "$REAL_USER" "$BINARY" --install-launchd 2>/dev/null || true
|
|
44
45
|
fi
|
|
45
46
|
|
|
47
|
+
# --- Status menu LaunchAgent (runs as user, not root) -----------------------
|
|
48
|
+
|
|
49
|
+
STATUS_PLIST="$REAL_HOME/Library/LaunchAgents/com.fit-basecamp.status-menu.plist"
|
|
50
|
+
|
|
51
|
+
if [ -f "$STATUS_MENU_BINARY" ]; then
|
|
52
|
+
# Kill old status menu if running (picks up new binary on relaunch)
|
|
53
|
+
/usr/bin/killall BasecampStatus 2>/dev/null || true
|
|
54
|
+
|
|
55
|
+
# Write LaunchAgent plist
|
|
56
|
+
/usr/bin/sudo -u "$REAL_USER" /usr/bin/tee "$STATUS_PLIST" > /dev/null <<PLIST
|
|
57
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
58
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
59
|
+
<plist version="1.0">
|
|
60
|
+
<dict>
|
|
61
|
+
<key>Label</key>
|
|
62
|
+
<string>com.fit-basecamp.status-menu</string>
|
|
63
|
+
<key>ProgramArguments</key>
|
|
64
|
+
<array>
|
|
65
|
+
<string>/usr/local/bin/BasecampStatus</string>
|
|
66
|
+
</array>
|
|
67
|
+
<key>RunAtLoad</key>
|
|
68
|
+
<true/>
|
|
69
|
+
<key>KeepAlive</key>
|
|
70
|
+
<true/>
|
|
71
|
+
<key>StandardOutPath</key>
|
|
72
|
+
<string>$BASECAMP_HOME/logs/status-menu-stdout.log</string>
|
|
73
|
+
<key>StandardErrorPath</key>
|
|
74
|
+
<string>$BASECAMP_HOME/logs/status-menu-stderr.log</string>
|
|
75
|
+
</dict>
|
|
76
|
+
</plist>
|
|
77
|
+
PLIST
|
|
78
|
+
|
|
79
|
+
# Load the LaunchAgent (unload first for upgrades)
|
|
80
|
+
/usr/bin/sudo -u "$REAL_USER" /usr/bin/launchctl unload "$STATUS_PLIST" 2>/dev/null || true
|
|
81
|
+
/usr/bin/sudo -u "$REAL_USER" /usr/bin/launchctl load "$STATUS_PLIST"
|
|
82
|
+
fi
|
|
83
|
+
|
|
46
84
|
exit 0
|
package/scripts/uninstall.sh
CHANGED
|
@@ -24,6 +24,23 @@ else
|
|
|
24
24
|
echo " LaunchAgent not found, skipping."
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
+
# Remove status menu LaunchAgent
|
|
28
|
+
STATUS_PLIST="$HOME/Library/LaunchAgents/com.fit-basecamp.status-menu.plist"
|
|
29
|
+
if [ -f "$STATUS_PLIST" ]; then
|
|
30
|
+
launchctl unload "$STATUS_PLIST" 2>/dev/null || true
|
|
31
|
+
rm -f "$STATUS_PLIST"
|
|
32
|
+
echo " Removed status menu LaunchAgent"
|
|
33
|
+
fi
|
|
34
|
+
killall BasecampStatus 2>/dev/null || true
|
|
35
|
+
if [ -f "/usr/local/bin/BasecampStatus" ]; then
|
|
36
|
+
sudo rm -f "/usr/local/bin/BasecampStatus"
|
|
37
|
+
echo " Removed /usr/local/bin/BasecampStatus"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Remove stale socket file
|
|
41
|
+
# Socket path: ~/.fit/basecamp/basecamp.sock (must match SOCKET_PATH in basecamp.js)
|
|
42
|
+
rm -f "$HOME/.fit/basecamp/basecamp.sock"
|
|
43
|
+
|
|
27
44
|
# Remove binary
|
|
28
45
|
if [ -f "/usr/local/bin/$APP_NAME" ]; then
|
|
29
46
|
sudo rm -f "/usr/local/bin/$APP_NAME"
|