@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 +3 -8
- package/{scheduler.js → basecamp.js} +238 -130
- package/build.js +49 -51
- package/package.json +7 -10
- package/scripts/build-pkg.sh +0 -117
- package/scripts/compile.sh +0 -26
- package/scripts/install.sh +0 -108
- package/scripts/pkg-resources/conclusion.html +0 -62
- package/scripts/pkg-resources/welcome.html +0 -64
- package/scripts/postinstall +0 -46
- package/scripts/uninstall.sh +0 -56
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/
|
|
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
|
|
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
|
|
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
|
|
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 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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
200
|
+
const spawnArgs = ["--print"];
|
|
201
|
+
if (task.agent) spawnArgs.push("--agent", task.agent);
|
|
202
|
+
spawnArgs.push("-p", prompt);
|
|
214
203
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
const buildPkg = join(__dirname, "
|
|
88
|
-
run(
|
|
89
|
-
|
|
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
|
-
|
|
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.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": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
26
|
+
"basecamp.js",
|
|
29
27
|
"build.js",
|
|
30
28
|
"config/",
|
|
31
|
-
"scripts/",
|
|
32
29
|
"template/"
|
|
33
30
|
],
|
|
34
31
|
"engines": {
|
package/scripts/build-pkg.sh
DELETED
|
@@ -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"
|
package/scripts/compile.sh
DELETED
|
@@ -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"
|
package/scripts/install.sh
DELETED
|
@@ -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 && 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>
|
package/scripts/postinstall
DELETED
|
@@ -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
|
package/scripts/uninstall.sh
DELETED
|
@@ -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 ""
|