@aitne-sh/aitne 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/agent-assets/agent-profiles/_safety.md +26 -0
- package/agent-assets/agent-profiles/conversational.md +33 -0
- package/agent-assets/agent-profiles/docs-qa.md +24 -0
- package/agent-assets/agent-profiles/observer.md +28 -0
- package/agent-assets/agent-profiles/profile-importer.md +63 -0
- package/agent-assets/agent-profiles/proxy.md +28 -0
- package/agent-assets/agent-profiles/routine.md +16 -0
- package/agent-assets/agent-profiles/task.md +18 -0
- package/agent-assets/docs/concepts/agent-day.md +88 -0
- package/agent-assets/docs/concepts/auth-health.md +75 -0
- package/agent-assets/docs/concepts/backends-and-tiers.md +126 -0
- package/agent-assets/docs/concepts/costs-and-quotas.md +103 -0
- package/agent-assets/docs/concepts/delegated-mode.md +223 -0
- package/agent-assets/docs/concepts/memory-model.md +118 -0
- package/agent-assets/docs/concepts/observations.md +80 -0
- package/agent-assets/docs/concepts/process-keys.md +89 -0
- package/agent-assets/docs/concepts/routines.md +108 -0
- package/agent-assets/docs/concepts/safety-and-execution.md +109 -0
- package/agent-assets/docs/concepts/safety-model.md +279 -0
- package/agent-assets/docs/concepts/skills.md +100 -0
- package/agent-assets/docs/features/integrations/calendar.md +92 -0
- package/agent-assets/docs/features/integrations/git.md +95 -0
- package/agent-assets/docs/features/integrations/github.md +170 -0
- package/agent-assets/docs/features/integrations/mail.md +106 -0
- package/agent-assets/docs/features/integrations/notion.md +69 -0
- package/agent-assets/docs/features/integrations/obsidian.md +71 -0
- package/agent-assets/docs/features/lifestyle/git.md +178 -0
- package/agent-assets/docs/features/lifestyle/reading.md +93 -0
- package/agent-assets/docs/features/lifestyle/receipts.md +71 -0
- package/agent-assets/docs/features/lifestyle/travel-bookings.md +44 -0
- package/agent-assets/docs/features/lifestyle/travel-time.md +52 -0
- package/agent-assets/docs/features/memory-files/agent-journal.md +105 -0
- package/agent-assets/docs/features/memory-files/projects.md +56 -0
- package/agent-assets/docs/features/memory-files/roadmap.md +61 -0
- package/agent-assets/docs/features/memory-files/schedule.md +112 -0
- package/agent-assets/docs/features/memory-files/today.md +73 -0
- package/agent-assets/docs/features/memory-files/user-profile.md +81 -0
- package/agent-assets/docs/features/messaging/dashboard-chat.md +93 -0
- package/agent-assets/docs/features/messaging/discord.md +50 -0
- package/agent-assets/docs/features/messaging/overview.md +111 -0
- package/agent-assets/docs/features/messaging/pairing-and-magic-phrase.md +69 -0
- package/agent-assets/docs/features/messaging/slack.md +51 -0
- package/agent-assets/docs/features/messaging/telegram.md +63 -0
- package/agent-assets/docs/features/messaging/whatsapp.md +48 -0
- package/agent-assets/docs/features/operations/activity-and-conversations.md +105 -0
- package/agent-assets/docs/features/operations/approvals.md +58 -0
- package/agent-assets/docs/features/operations/backend-routing.md +62 -0
- package/agent-assets/docs/features/operations/cost-tracking.md +59 -0
- package/agent-assets/docs/features/operations/notifications.md +69 -0
- package/agent-assets/docs/features/operations/quiet-hours.md +106 -0
- package/agent-assets/docs/features/operations/schedule-approaching.md +60 -0
- package/agent-assets/docs/features/routines/custom-routines.md +101 -0
- package/agent-assets/docs/features/routines/evening-review.md +81 -0
- package/agent-assets/docs/features/routines/hourly-check.md +85 -0
- package/agent-assets/docs/features/routines/monthly-review.md +65 -0
- package/agent-assets/docs/features/routines/morning-routine.md +123 -0
- package/agent-assets/docs/features/routines/weekly-review.md +70 -0
- package/agent-assets/docs/getting-started/01-what-is-this.md +192 -0
- package/agent-assets/docs/getting-started/02-first-steps.md +80 -0
- package/agent-assets/docs/getting-started/03-what-can-this-do.md +110 -0
- package/agent-assets/docs/getting-started/04-first-day.md +287 -0
- package/agent-assets/docs/glossary.md +116 -0
- package/agent-assets/docs/guides/add-a-custom-routine.md +71 -0
- package/agent-assets/docs/guides/backup-and-restore.md +54 -0
- package/agent-assets/docs/guides/change-which-model-handles-x.md +47 -0
- package/agent-assets/docs/guides/connect-a-new-mail-account.md +59 -0
- package/agent-assets/docs/guides/import-knowledge-file.md +275 -0
- package/agent-assets/docs/guides/install-and-run.md +72 -0
- package/agent-assets/docs/guides/migrate-machines.md +52 -0
- package/agent-assets/docs/guides/pause-the-agent.md +65 -0
- package/agent-assets/docs/guides/reinstall-cleanly.md +52 -0
- package/agent-assets/docs/guides/setup-wizard.md +107 -0
- package/agent-assets/docs/guides/switch-default-backend.md +60 -0
- package/agent-assets/docs/reference/api.md +51 -0
- package/agent-assets/docs/reference/cli-commands.md +121 -0
- package/agent-assets/docs/reference/config.md +74 -0
- package/agent-assets/docs/reference/disallowed-tools.md +76 -0
- package/agent-assets/docs/reference/keyboard-shortcuts.md +39 -0
- package/agent-assets/docs/reference/process-keys.md +59 -0
- package/agent-assets/docs/reference/skills.md +50 -0
- package/agent-assets/docs/troubleshooting/auth-failed.md +57 -0
- package/agent-assets/docs/troubleshooting/dashboard-shows-degraded.md +55 -0
- package/agent-assets/docs/troubleshooting/fallback-keeps-firing.md +54 -0
- package/agent-assets/docs/troubleshooting/messaging-not-pairing.md +53 -0
- package/agent-assets/docs/troubleshooting/morning-routine-didnt-run.md +75 -0
- package/agent-assets/docs/troubleshooting/observation-not-detected.md +57 -0
- package/agent-assets/docs/troubleshooting/quota-exhausted.md +57 -0
- package/agent-assets/optimizer-skills/drift-analysis/SKILL.md +75 -0
- package/agent-assets/optimizer-skills/knowledge-map/SKILL.md +71 -0
- package/agent-assets/optimizer-skills/skill-curation/SKILL.md +108 -0
- package/agent-assets/project-doc-templates/git-repo.md +21 -0
- package/agent-assets/project-doc-templates/project.md +38 -0
- package/agent-assets/skills/attach/SKILL.md +104 -0
- package/agent-assets/skills/context/SKILL.md +257 -0
- package/agent-assets/skills/context/curation.json +37 -0
- package/agent-assets/skills/context/seeds/file-responsibilities.seed.json +13 -0
- package/agent-assets/skills/context/seeds/frontmatter-requirements.seed.json +40 -0
- package/agent-assets/skills/docs-search/SKILL.md +176 -0
- package/agent-assets/skills/external-services/SKILL.delegated.claude.md +369 -0
- package/agent-assets/skills/external-services/SKILL.delegated.codex.md +349 -0
- package/agent-assets/skills/external-services/SKILL.delegated.gemini.md +347 -0
- package/agent-assets/skills/external-services/SKILL.md +371 -0
- package/agent-assets/skills/mail/SKILL.delegated.claude.md +284 -0
- package/agent-assets/skills/mail/SKILL.delegated.codex.md +261 -0
- package/agent-assets/skills/mail/SKILL.delegated.gemini.md +255 -0
- package/agent-assets/skills/mail/SKILL.md +313 -0
- package/agent-assets/skills/mail/references/errors.md +17 -0
- package/agent-assets/skills/mail/references/providers.md +40 -0
- package/agent-assets/skills/mail/references/query-grammar.md +24 -0
- package/agent-assets/skills/management-policy/SKILL.md +307 -0
- package/agent-assets/skills/management-policy/curation.json +13 -0
- package/agent-assets/skills/management-policy/seeds/policy-file-shape.seed.json +16 -0
- package/agent-assets/skills/management-task-modify/SKILL.md +202 -0
- package/agent-assets/skills/management-task-register/SKILL.md +330 -0
- package/agent-assets/skills/management-task-stop/SKILL.md +166 -0
- package/agent-assets/skills/notify/SKILL.md +196 -0
- package/agent-assets/skills/notion/SKILL.delegated.claude.md +254 -0
- package/agent-assets/skills/notion/SKILL.delegated.codex.md +195 -0
- package/agent-assets/skills/notion/SKILL.delegated.gemini.md +194 -0
- package/agent-assets/skills/notion/SKILL.md +86 -0
- package/agent-assets/skills/observations/SKILL.md +234 -0
- package/agent-assets/skills/observations/curation.json +13 -0
- package/agent-assets/skills/observations/seeds/source-namespacing.seed.json +20 -0
- package/agent-assets/skills/project-doc/SKILL.md +86 -0
- package/agent-assets/skills/project-doc/curation.json +21 -0
- package/agent-assets/skills/project-doc/seeds/project-shape.seed.json +25 -0
- package/agent-assets/skills/project-doc/seeds/slug-grammar.seed.json +20 -0
- package/agent-assets/skills/reading/SKILL.md +198 -0
- package/agent-assets/skills/reading/references/reading-taste.md +197 -0
- package/agent-assets/skills/receipts/SKILL.md +134 -0
- package/agent-assets/skills/roadmap/SKILL.md +276 -0
- package/agent-assets/skills/roadmap/curation.json +13 -0
- package/agent-assets/skills/roadmap/references/horizon-tags.md +40 -0
- package/agent-assets/skills/roadmap/references/preparation-timeline.md +47 -0
- package/agent-assets/skills/roadmap/seeds/entry-types.seed.json +16 -0
- package/agent-assets/skills/schedule/SKILL.md +228 -0
- package/agent-assets/skills/scheduled-managed-task/SKILL.md +392 -0
- package/agent-assets/skills/today/SKILL.md +198 -0
- package/agent-assets/skills/today/curation.json +21 -0
- package/agent-assets/skills/today/seeds/agent-notes-flavors.seed.json +17 -0
- package/agent-assets/skills/today/seeds/section-shape.seed.json +17 -0
- package/agent-assets/skills/travel/SKILL.md +132 -0
- package/agent-assets/skills/travel-time/SKILL.md +149 -0
- package/agent-assets/skills/user-interview/SKILL.md +323 -0
- package/agent-assets/skills/user-interview/references/sweep-and-fallback.md +94 -0
- package/agent-assets/skills/user-profile/SKILL.md +210 -0
- package/agent-assets/skills/user-profile/curation.json +29 -0
- package/agent-assets/skills/user-profile/seeds/learned-context-format.seed.json +14 -0
- package/agent-assets/skills/user-profile/seeds/routing-table.seed.json +53 -0
- package/agent-assets/skills/user-profile/seeds/topic-files.seed.json +27 -0
- package/agent-assets/task-flows/dashboard.docs_qa.md +43 -0
- package/agent-assets/task-flows/default.md +11 -0
- package/agent-assets/task-flows/git.branch.created.md +25 -0
- package/agent-assets/task-flows/git.lifecycle.poll.md +52 -0
- package/agent-assets/task-flows/git.local_ahead.stale.md +34 -0
- package/agent-assets/task-flows/git.merge_to_default.md +30 -0
- package/agent-assets/task-flows/git.project.refresh_architecture.md +100 -0
- package/agent-assets/task-flows/git.project.retemplate.md +73 -0
- package/agent-assets/task-flows/git.push.detected.md +32 -0
- package/agent-assets/task-flows/git.push.force_pushed.md +36 -0
- package/agent-assets/task-flows/git.tag.created.md +24 -0
- package/agent-assets/task-flows/github.assigned.md +43 -0
- package/agent-assets/task-flows/github.pull_request.review_requested.md +57 -0
- package/agent-assets/task-flows/github.security_alert.md +45 -0
- package/agent-assets/task-flows/github.workflow_run.failed.md +57 -0
- package/agent-assets/task-flows/knowledge.import.md +161 -0
- package/agent-assets/task-flows/message.received.dm.md +142 -0
- package/agent-assets/task-flows/message.received.dm_first.md +117 -0
- package/agent-assets/task-flows/message.received.md +14 -0
- package/agent-assets/task-flows/routine.custom.md +38 -0
- package/agent-assets/task-flows/routine.evening_review.md +323 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.claude.md +405 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.codex.md +400 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.gemini.md +404 -0
- package/agent-assets/task-flows/routine.hourly_check.md +184 -0
- package/agent-assets/task-flows/routine.hourly_check.triage.md +93 -0
- package/agent-assets/task-flows/routine.monthly_review.md +250 -0
- package/agent-assets/task-flows/routine.morning_routine.md +300 -0
- package/agent-assets/task-flows/routine.morning_routine_initial.md +184 -0
- package/agent-assets/task-flows/routine.roadmap_refresh.md +275 -0
- package/agent-assets/task-flows/routine.today_refresh.md +172 -0
- package/agent-assets/task-flows/routine.user_profile_sweep.md +242 -0
- package/agent-assets/task-flows/routine.weekly_review.md +247 -0
- package/agent-assets/task-flows/schedule.approaching.md +124 -0
- package/agent-assets/task-flows/scheduled.dm.md +391 -0
- package/agent-assets/task-flows/scheduled.task.md +141 -0
- package/agent-assets/task-flows/setup.initial.md +277 -0
- package/agent-assets/task-flows/setup.update.md +53 -0
- package/agent-assets/templates/README.md +85 -0
- package/agent-assets/templates/_index.md +39 -0
- package/agent-assets/templates/_manifest.json +103 -0
- package/agent-assets/templates/agent/journal.md +10 -0
- package/agent-assets/templates/agent/profile-questions.md +74 -0
- package/agent-assets/templates/context-index.md +42 -0
- package/agent-assets/templates/dossiers/_index.md +22 -0
- package/agent-assets/templates/dossiers/evening.md +23 -0
- package/agent-assets/templates/dossiers/hourly.md +23 -0
- package/agent-assets/templates/dossiers/monthly.md +23 -0
- package/agent-assets/templates/dossiers/morning.md +23 -0
- package/agent-assets/templates/dossiers/roadmap.md +23 -0
- package/agent-assets/templates/dossiers/weekly.md +23 -0
- package/agent-assets/templates/projects/_active.base +14 -0
- package/agent-assets/templates/projects/_index.md +29 -0
- package/agent-assets/templates/roadmap.md +15 -0
- package/agent-assets/templates/routines/_index.md +20 -0
- package/agent-assets/templates/routines/evening.md +22 -0
- package/agent-assets/templates/routines/hourly.md +30 -0
- package/agent-assets/templates/routines/monthly.md +25 -0
- package/agent-assets/templates/routines/morning.md +26 -0
- package/agent-assets/templates/routines/weekly.md +23 -0
- package/agent-assets/templates/rules/_index.md +19 -0
- package/agent-assets/templates/rules/journal-export.md +41 -0
- package/agent-assets/templates/rules/journal-format.md +61 -0
- package/agent-assets/templates/rules/management.md +48 -0
- package/agent-assets/templates/rules/mcp.md +40 -0
- package/agent-assets/templates/rules/policies/_index.md +22 -0
- package/agent-assets/templates/rules/redaction.md +30 -0
- package/agent-assets/templates/today.md +13 -0
- package/agent-assets/templates/user/_index.md +16 -0
- package/agent-assets/templates/user/expertise.md +7 -0
- package/agent-assets/templates/user/goals.md +7 -0
- package/agent-assets/templates/user/people.md +7 -0
- package/agent-assets/templates/user/personal.md +7 -0
- package/agent-assets/templates/user/profile.md +28 -0
- package/agent-assets/templates/user/work.md +7 -0
- package/bin/aitne.mjs +1096 -0
- package/package.json +78 -0
- package/personal-agent.mjs +39 -0
- package/scripts/browser.mjs +99 -0
- package/scripts/check-redaction-coverage.mjs +109 -0
- package/scripts/commands/audit.mjs +309 -0
- package/scripts/commands/doctor.mjs +437 -0
- package/scripts/commands/open.mjs +40 -0
- package/scripts/commands/setup.mjs +21 -0
- package/scripts/commands/uninstall.mjs +114 -0
- package/scripts/commands/update.mjs +96 -0
- package/scripts/commands/version.mjs +62 -0
- package/scripts/commands.md +0 -0
- package/scripts/lib/sqlite-loader.mjs +49 -0
- package/scripts/message-discipline-digest.mjs +535 -0
- package/scripts/poc/google-connector-inheritance/REPORT.md +197 -0
- package/scripts/poc/google-connector-inheritance/claude-sdk-probe.mjs +79 -0
- package/scripts/remint-roadmap-ids.mjs +257 -0
- package/scripts/rm-paths.mjs +22 -0
- package/scripts/run-node.mjs +223 -0
- package/scripts/smoke-obsidian-api.mjs +166 -0
- package/scripts/start.mjs +160 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aitne doctor` — diagnose install-time problems.
|
|
3
|
+
*
|
|
4
|
+
* Eight independent checks. Each returns a status (pass / warn / fail), a
|
|
5
|
+
* short detail line, and an optional hint that tells the user what to do.
|
|
6
|
+
* The single most common failure mode for new users is "I installed it but
|
|
7
|
+
* something is wrong" — doctor narrows that to "row N failed: do X."
|
|
8
|
+
*
|
|
9
|
+
* Doctor is read-only and offline. No daemon is started, no DB is mutated,
|
|
10
|
+
* no network call is made. Safe to run at any time.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import net from "node:net";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export async function run(args, ctx) {
|
|
18
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
19
|
+
console.log(`Usage: aitne doctor [--json]
|
|
20
|
+
|
|
21
|
+
Run a series of install-health checks and report pass/warn/fail. Useful as
|
|
22
|
+
a first step when triaging "it doesn't work" — most first-install failures
|
|
23
|
+
are exactly one of these checks.
|
|
24
|
+
|
|
25
|
+
Flags:
|
|
26
|
+
--json Machine-readable output. Implies no terminal formatting.
|
|
27
|
+
|
|
28
|
+
Exit code:
|
|
29
|
+
0 All checks pass (warnings are tolerated).
|
|
30
|
+
1 At least one check failed.`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const checks = [
|
|
35
|
+
await checkNodeVersion(),
|
|
36
|
+
await checkPort("Daemon port", ctx.DAEMON_PORT, ctx.DAEMON_PID_FILE, ctx.helpers.getRunningPid),
|
|
37
|
+
await checkPort("Dashboard port", ctx.DASHBOARD_PORT, ctx.DASHBOARD_PID_FILE, ctx.helpers.getRunningPid),
|
|
38
|
+
await checkSecretStore(ctx),
|
|
39
|
+
await checkBackendCli(),
|
|
40
|
+
await checkDataDirWritable(ctx.DATA_DIR),
|
|
41
|
+
await checkBetterSqlite3(ctx.PROJECT_ROOT),
|
|
42
|
+
await checkAgentAssets(ctx.PROJECT_ROOT),
|
|
43
|
+
...(await checkRepositoryGithubLinkDrift(ctx.DATA_DIR)),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const passed = checks.filter((c) => c.status === "pass").length;
|
|
47
|
+
const warned = checks.filter((c) => c.status === "warn").length;
|
|
48
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
49
|
+
|
|
50
|
+
if (args.includes("--json")) {
|
|
51
|
+
process.stdout.write(JSON.stringify({ checks, passed, warned, failed }, null, 2) + "\n");
|
|
52
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`${ctx.APP_NAME} v${ctx.VERSION} doctor — ${checks.length} checks`);
|
|
56
|
+
console.log("");
|
|
57
|
+
|
|
58
|
+
// Right-pad label to widest so the detail column lines up.
|
|
59
|
+
const labelWidth = Math.max(...checks.map((c) => c.label.length));
|
|
60
|
+
for (const c of checks) {
|
|
61
|
+
const mark = c.status === "pass" ? "ok " : c.status === "warn" ? "warn" : "FAIL";
|
|
62
|
+
const label = c.label.padEnd(labelWidth);
|
|
63
|
+
console.log(` [${mark}] ${label} ${c.detail}`);
|
|
64
|
+
if (c.hint && c.status !== "pass") {
|
|
65
|
+
console.log(` ${" ".repeat(labelWidth)} hint: ${c.hint}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(`${passed} ok · ${warned} warn · ${failed} fail`);
|
|
70
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// Individual checks. Each is an async function returning
|
|
75
|
+
// { status, label, detail, hint? }. Independent — never throw.
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
async function checkNodeVersion() {
|
|
79
|
+
const v = process.versions.node; // e.g. "22.10.0"
|
|
80
|
+
const major = parseInt(v.split(".")[0], 10) || 0;
|
|
81
|
+
if (major >= 22) {
|
|
82
|
+
return { status: "pass", label: "Node version", detail: `v${v} (>= 22)` };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
status: "fail",
|
|
86
|
+
label: "Node version",
|
|
87
|
+
detail: `v${v} — too old (need >= 22)`,
|
|
88
|
+
hint: "Install Node 22 LTS, then re-run. `corepack enable` is bundled with 22+.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check whether a port is either (a) bindable, or (b) already held by our own
|
|
94
|
+
* PID file. If neither, we surface a fail — something else is on the port and
|
|
95
|
+
* the user needs to know.
|
|
96
|
+
*
|
|
97
|
+
* `getRunningPid` is passed in (rather than re-implemented inline) so the
|
|
98
|
+
* stale-PID handling stays consistent with `aitne start`'s view of "is our
|
|
99
|
+
* daemon up?". Inlining it would drift over time.
|
|
100
|
+
*/
|
|
101
|
+
async function checkPort(label, port, pidFile, getRunningPid) {
|
|
102
|
+
// (b) — our own daemon already running.
|
|
103
|
+
const pid = getRunningPid(pidFile);
|
|
104
|
+
if (pid != null) {
|
|
105
|
+
return {
|
|
106
|
+
status: "pass",
|
|
107
|
+
label,
|
|
108
|
+
detail: `${port} held by ${label.split(" ")[0].toLowerCase()} PID ${pid}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// (a) — try to bind. If success, port is free.
|
|
113
|
+
const free = await new Promise((resolve) => {
|
|
114
|
+
const sock = net.createServer();
|
|
115
|
+
sock.unref();
|
|
116
|
+
sock.once("error", () => { resolve(false); });
|
|
117
|
+
sock.listen(port, "127.0.0.1", () => {
|
|
118
|
+
sock.close(() => resolve(true));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (free) return { status: "pass", label, detail: `${port} free` };
|
|
123
|
+
return {
|
|
124
|
+
status: "fail",
|
|
125
|
+
label,
|
|
126
|
+
detail: `${port} in use by another process`,
|
|
127
|
+
hint: label.startsWith("Daemon")
|
|
128
|
+
? `Set PA_API_PORT to an open port (e.g. PA_API_PORT=8322 aitne start), or stop the conflicting process.`
|
|
129
|
+
: `Set PA_DASHBOARD_PORT to an open port (e.g. PA_DASHBOARD_PORT=3001 aitne start), or stop the conflicting process.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* OS-specific secret-store probe. Per README §Platform support:
|
|
135
|
+
* - macOS: `security` CLI is in-box; just verify it executes.
|
|
136
|
+
* - Linux: `secret-tool` is preferred; the file fallback needs
|
|
137
|
+
* PA_MASTER_PASSWORD or a keyfile present.
|
|
138
|
+
* - Windows: PowerShell DPAPI is in-box; prefer `powershell.exe` and
|
|
139
|
+
* fall back to `pwsh.exe`.
|
|
140
|
+
*
|
|
141
|
+
* Failure here is "warn" (not "fail") because the daemon's
|
|
142
|
+
* file-store fallback is a documented graceful path on Linux.
|
|
143
|
+
*/
|
|
144
|
+
async function checkSecretStore(ctx) {
|
|
145
|
+
const platform = process.platform;
|
|
146
|
+
if (platform === "darwin") {
|
|
147
|
+
try {
|
|
148
|
+
execFileSync("security", ["list-keychains", "-d", "user"], { stdio: "pipe", timeout: 3000 });
|
|
149
|
+
return { status: "pass", label: "Secret store", detail: "macOS Keychain reachable" };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return {
|
|
152
|
+
status: "fail",
|
|
153
|
+
label: "Secret store",
|
|
154
|
+
detail: `macOS \`security\` CLI failed: ${err?.message ?? "unknown"}`,
|
|
155
|
+
hint: "macOS ships `security`; this should never fail. Check $PATH.",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (platform === "linux") {
|
|
160
|
+
try {
|
|
161
|
+
execFileSync("secret-tool", ["--version"], { stdio: "pipe", timeout: 3000 });
|
|
162
|
+
return { status: "pass", label: "Secret store", detail: "libsecret (`secret-tool`) reachable" };
|
|
163
|
+
} catch {
|
|
164
|
+
const hasMaster = !!process.env.PA_MASTER_PASSWORD;
|
|
165
|
+
const keyfile = path.join(ctx.DATA_DIR, "secrets", ".master-key");
|
|
166
|
+
const hasKeyfile = fs.existsSync(keyfile);
|
|
167
|
+
if (hasMaster || hasKeyfile) {
|
|
168
|
+
return {
|
|
169
|
+
status: "pass",
|
|
170
|
+
label: "Secret store",
|
|
171
|
+
detail: hasMaster ? "file store with PA_MASTER_PASSWORD" : "file store with keyfile",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
status: "warn",
|
|
176
|
+
label: "Secret store",
|
|
177
|
+
detail: "no `secret-tool`, no PA_MASTER_PASSWORD, no keyfile",
|
|
178
|
+
hint: "apt install libsecret-tools · or set PA_MASTER_PASSWORD before first run (README §Linux setup)",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (platform === "win32") {
|
|
183
|
+
const psBinary = whichSync("powershell.exe") ? "powershell.exe" : "pwsh.exe";
|
|
184
|
+
try {
|
|
185
|
+
execFileSync(psBinary, [
|
|
186
|
+
"-NoProfile", "-Command",
|
|
187
|
+
"[System.Security.Cryptography.ProtectedData] | Out-Null; exit 0",
|
|
188
|
+
], { stdio: "pipe", timeout: 5000 });
|
|
189
|
+
return { status: "pass", label: "Secret store", detail: `Windows DPAPI via ${psBinary} reachable` };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
status: "fail",
|
|
193
|
+
label: "Secret store",
|
|
194
|
+
detail: `PowerShell DPAPI probe failed: ${err?.message ?? "unknown"}`,
|
|
195
|
+
hint: "PowerShell ships with Windows; check $PATH or install PowerShell 7+ (`pwsh.exe`).",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
status: "warn",
|
|
201
|
+
label: "Secret store",
|
|
202
|
+
detail: `unknown platform ${platform}`,
|
|
203
|
+
hint: "Supported: darwin, linux, win32. File a bug if your platform is reasonable.",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function checkBackendCli() {
|
|
208
|
+
const candidates = ["claude", "codex", "gemini"];
|
|
209
|
+
const found = [];
|
|
210
|
+
for (const name of candidates) {
|
|
211
|
+
if (whichSync(name)) found.push(name);
|
|
212
|
+
}
|
|
213
|
+
if (found.length > 0) {
|
|
214
|
+
return { status: "pass", label: "Backend CLI", detail: `${found.join(", ")} on PATH` };
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
status: "warn",
|
|
218
|
+
label: "Backend CLI",
|
|
219
|
+
detail: "none of claude/codex/gemini found on PATH",
|
|
220
|
+
hint: "Install at least one (Claude Code recommended). The setup wizard can guide you.",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function checkDataDirWritable(dataDir) {
|
|
225
|
+
try {
|
|
226
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
227
|
+
const probe = path.join(dataDir, `.doctor-probe-${process.pid}`);
|
|
228
|
+
fs.writeFileSync(probe, "ok");
|
|
229
|
+
fs.unlinkSync(probe);
|
|
230
|
+
return { status: "pass", label: "Data dir writable", detail: dataDir };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
return {
|
|
233
|
+
status: "fail",
|
|
234
|
+
label: "Data dir writable",
|
|
235
|
+
detail: `${dataDir}: ${err?.message ?? "unknown"}`,
|
|
236
|
+
hint: "Check permissions on the parent directory, or set PA_DATA_DIR to a writable path.",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function checkBetterSqlite3(projectRoot) {
|
|
242
|
+
try {
|
|
243
|
+
const { loadBetterSqlite3 } = await import("../lib/sqlite-loader.mjs");
|
|
244
|
+
const Database = await loadBetterSqlite3(projectRoot);
|
|
245
|
+
// Open an in-memory DB to exercise the native binding fully.
|
|
246
|
+
const db = new Database(":memory:");
|
|
247
|
+
db.close();
|
|
248
|
+
return { status: "pass", label: "better-sqlite3", detail: "native binding loads" };
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
status: "fail",
|
|
252
|
+
label: "better-sqlite3",
|
|
253
|
+
detail: `failed to load: ${err?.message ?? "unknown"}`,
|
|
254
|
+
hint: "Reinstall the package — your platform may have downloaded a corrupt prebuild. `pnpm rebuild better-sqlite3` from the dev repo.",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function checkAgentAssets(projectRoot) {
|
|
260
|
+
const skillsDir = path.join(projectRoot, "agent-assets", "skills");
|
|
261
|
+
if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
|
|
262
|
+
let entries = 0;
|
|
263
|
+
try { entries = fs.readdirSync(skillsDir).length; } catch { /* permission edge */ }
|
|
264
|
+
return { status: "pass", label: "agent-assets", detail: `${entries} skill(s) at ${skillsDir}` };
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
status: "fail",
|
|
268
|
+
label: "agent-assets",
|
|
269
|
+
detail: `missing: ${skillsDir}`,
|
|
270
|
+
hint: "The package looks corrupt. Reinstall: `npm install -g aitne@latest`.",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Per-row drift check for unified repositories rows that pair a GitHub
|
|
276
|
+
* remote with a local clone. Resolves `git -C <local_path> remote get-url
|
|
277
|
+
* origin` and compares to the registered `<owner>/<repo>`. See
|
|
278
|
+
* `docs/design/appendices/unified-repositories.md` §11.1 for the lock.
|
|
279
|
+
*
|
|
280
|
+
* Returns one check row per drifted (or origin-less) repository, plus a
|
|
281
|
+
* single summary row when nothing drifts. The doctor is informational —
|
|
282
|
+
* fixes happen via /api/repositories or the dashboard.
|
|
283
|
+
*/
|
|
284
|
+
async function checkRepositoryGithubLinkDrift(dataDir) {
|
|
285
|
+
const dbPath = path.join(dataDir, "data", "personal_agent.db");
|
|
286
|
+
if (!fs.existsSync(dbPath)) {
|
|
287
|
+
return [
|
|
288
|
+
{
|
|
289
|
+
status: "pass",
|
|
290
|
+
label: "Repository drift",
|
|
291
|
+
detail: "no DB yet (skipped — run aitne start first)",
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Lazy-load better-sqlite3 from the daemon package so doctor stays
|
|
297
|
+
// dependency-light; if the native binding is missing we fall through
|
|
298
|
+
// to a pass with a hint (rather than fail the whole doctor pass).
|
|
299
|
+
let Database;
|
|
300
|
+
try {
|
|
301
|
+
const candidates = [
|
|
302
|
+
path.join(process.cwd(), "node_modules", "better-sqlite3", "lib", "index.js"),
|
|
303
|
+
path.join(process.cwd(), "packages", "daemon", "node_modules", "better-sqlite3", "lib", "index.js"),
|
|
304
|
+
];
|
|
305
|
+
const found = candidates.find((p) => fs.existsSync(p));
|
|
306
|
+
if (!found) {
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
status: "warn",
|
|
310
|
+
label: "Repository drift",
|
|
311
|
+
detail: "better-sqlite3 not resolvable from doctor — skipped",
|
|
312
|
+
hint: "Run `pnpm install` in the repo, then re-run aitne doctor.",
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
Database = (await import(found)).default ?? (await import(found));
|
|
317
|
+
} catch (err) {
|
|
318
|
+
return [
|
|
319
|
+
{
|
|
320
|
+
status: "warn",
|
|
321
|
+
label: "Repository drift",
|
|
322
|
+
detail: `better-sqlite3 load failed: ${err?.message ?? "unknown"}`,
|
|
323
|
+
hint: "Re-run `pnpm install` to rebuild the native binding.",
|
|
324
|
+
},
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let rows;
|
|
329
|
+
try {
|
|
330
|
+
const db = new Database(dbPath, { readonly: true });
|
|
331
|
+
rows = db
|
|
332
|
+
.prepare(
|
|
333
|
+
`SELECT id, github_owner, github_repo, local_path, display_name
|
|
334
|
+
FROM repositories
|
|
335
|
+
WHERE github_owner IS NOT NULL
|
|
336
|
+
AND github_repo IS NOT NULL
|
|
337
|
+
AND local_path IS NOT NULL
|
|
338
|
+
AND local_only = 0`,
|
|
339
|
+
)
|
|
340
|
+
.all();
|
|
341
|
+
db.close();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return [
|
|
344
|
+
{
|
|
345
|
+
status: "warn",
|
|
346
|
+
label: "Repository drift",
|
|
347
|
+
detail: `DB read failed: ${err?.message ?? "unknown"}`,
|
|
348
|
+
hint: "Stop the daemon (aitne stop) and re-run, or check DB integrity.",
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (rows.length === 0) {
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
status: "pass",
|
|
357
|
+
label: "Repository drift",
|
|
358
|
+
detail: "no GitHub-paired rows with a local clone",
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const drifted = [];
|
|
364
|
+
for (const row of rows) {
|
|
365
|
+
const expected = `${row.github_owner}/${row.github_repo}`;
|
|
366
|
+
let originUrl = "";
|
|
367
|
+
try {
|
|
368
|
+
const out = execFileSync("git", ["-C", row.local_path, "remote", "get-url", "origin"], {
|
|
369
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
370
|
+
timeout: 3000,
|
|
371
|
+
});
|
|
372
|
+
originUrl = out.toString().trim();
|
|
373
|
+
} catch {
|
|
374
|
+
drifted.push({ row, expected, actual: null });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const actual = parseGithubOwnerRepo(originUrl);
|
|
378
|
+
if (!actual || actual.toLowerCase() !== expected.toLowerCase()) {
|
|
379
|
+
drifted.push({ row, expected, actual });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (drifted.length === 0) {
|
|
384
|
+
return [
|
|
385
|
+
{
|
|
386
|
+
status: "pass",
|
|
387
|
+
label: "Repository drift",
|
|
388
|
+
detail: `${rows.length} paired row(s) — origin matches`,
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return drifted.map(({ row, expected, actual }) => ({
|
|
394
|
+
status: "warn",
|
|
395
|
+
label: `Repository drift`,
|
|
396
|
+
detail: `'${row.display_name ?? expected}' — registered ${expected}, origin ${actual ?? "(none)"}`,
|
|
397
|
+
hint: `clone: ${row.local_path} — re-link to actual / mark local-only / unlink local clone via the dashboard`,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Parse `<owner>/<repo>` out of a GitHub remote URL. Supports
|
|
403
|
+
* - https://github.com/owner/repo(.git)?
|
|
404
|
+
* - git@github.com:owner/repo(.git)?
|
|
405
|
+
* - ssh://git@github.com/owner/repo(.git)?
|
|
406
|
+
* Returns null for non-GitHub remotes — the row's GitHub side is then
|
|
407
|
+
* treated as unknown for drift purposes.
|
|
408
|
+
*/
|
|
409
|
+
function parseGithubOwnerRepo(url) {
|
|
410
|
+
if (!url) return null;
|
|
411
|
+
const trimmed = url.trim();
|
|
412
|
+
const patterns = [
|
|
413
|
+
/^https?:\/\/(?:[^/]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
414
|
+
/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
415
|
+
/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
416
|
+
];
|
|
417
|
+
for (const re of patterns) {
|
|
418
|
+
const m = trimmed.match(re);
|
|
419
|
+
if (m) return `${m[1]}/${m[2]}`;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── helpers ──
|
|
425
|
+
|
|
426
|
+
/** Cross-platform `which` returning the resolved path or null. */
|
|
427
|
+
function whichSync(cmd) {
|
|
428
|
+
const isWin = process.platform === "win32";
|
|
429
|
+
const tool = isWin ? "where" : "which";
|
|
430
|
+
try {
|
|
431
|
+
const out = execFileSync(tool, [cmd], { stdio: ["ignore", "pipe", "ignore"], timeout: 2000 });
|
|
432
|
+
const first = out.toString().split(/\r?\n/)[0]?.trim();
|
|
433
|
+
return first || null;
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aitne open` — open the dashboard in the user's browser.
|
|
3
|
+
*
|
|
4
|
+
* If the daemon isn't running, auto-starts it (so a single command takes the
|
|
5
|
+
* user from "nothing running" to "dashboard tab open"). The auto-start path
|
|
6
|
+
* deliberately suppresses the browser-open step inside `cmdStart` (`--no-open`)
|
|
7
|
+
* because we open at the end of *this* command — otherwise the URL gets
|
|
8
|
+
* opened twice on cold-start.
|
|
9
|
+
*/
|
|
10
|
+
export async function run(args, ctx) {
|
|
11
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
12
|
+
console.log(`Usage: aitne open [--setup]
|
|
13
|
+
|
|
14
|
+
Open the dashboard in the default browser. Auto-starts the daemon if
|
|
15
|
+
it isn't running.
|
|
16
|
+
|
|
17
|
+
Flags:
|
|
18
|
+
--setup Open the /setup wizard route instead of the root page.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const goSetup = args.includes("--setup");
|
|
23
|
+
const url = goSetup
|
|
24
|
+
? `http://localhost:${ctx.DASHBOARD_PORT}/setup`
|
|
25
|
+
: `http://localhost:${ctx.DASHBOARD_PORT}/`;
|
|
26
|
+
|
|
27
|
+
const daemonPid = ctx.helpers.getRunningPid(ctx.DAEMON_PID_FILE);
|
|
28
|
+
const dashPid = ctx.helpers.getRunningPid(ctx.DASHBOARD_PID_FILE);
|
|
29
|
+
|
|
30
|
+
if (!daemonPid || !dashPid) {
|
|
31
|
+
console.log(`${ctx.APP_NAME} is not running — starting now…`);
|
|
32
|
+
await ctx.helpers.cmdStart(["--no-open"]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`Opening ${url}`);
|
|
36
|
+
const opened = await ctx.helpers.openBrowser(url);
|
|
37
|
+
if (!opened) {
|
|
38
|
+
console.log("(could not auto-open browser — paste the URL above manually)");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aitne setup` — re-run the setup wizard.
|
|
3
|
+
*
|
|
4
|
+
* Equivalent to `aitne open --setup`, but exists as its own verb so users
|
|
5
|
+
* who just want to (re)configure don't have to know about the `--setup`
|
|
6
|
+
* flag. Both paths converge on /setup once the daemon is up.
|
|
7
|
+
*/
|
|
8
|
+
export async function run(args, ctx) {
|
|
9
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
10
|
+
console.log(`Usage: aitne setup
|
|
11
|
+
|
|
12
|
+
Open the dashboard /setup wizard, auto-starting ${ctx.APP_NAME} first if
|
|
13
|
+
needed. Use this to (re)configure backends, integrations, plans, or
|
|
14
|
+
execution mode after the initial install.`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Delegate to open.mjs with --setup so the auto-start logic stays in one
|
|
18
|
+
// place.
|
|
19
|
+
const open = await import("./open.mjs");
|
|
20
|
+
return open.run(["--setup", ...args.filter((a) => a !== "--setup")], ctx);
|
|
21
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aitne uninstall` — stop services and print the npm uninstall command.
|
|
3
|
+
*
|
|
4
|
+
* We do not run `npm uninstall -g aitne` ourselves: doing so from a process
|
|
5
|
+
* the user is currently executing (the bin lives inside the package being
|
|
6
|
+
* removed) is asking for partial writes and confusing error states. Instead,
|
|
7
|
+
* stop cleanly, optionally wipe the data dir on explicit confirmation, then
|
|
8
|
+
* print the command for the user to copy-paste.
|
|
9
|
+
*
|
|
10
|
+
* The data-dir wipe is gated behind a literal "WIPE" confirmation so a fat
|
|
11
|
+
* finger doesn't lose months of context/. Mirrors the safety pattern used by
|
|
12
|
+
* `aitne restart --clean-context`.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export async function run(args, ctx) {
|
|
18
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
19
|
+
console.log(`Usage: aitne uninstall [--keep-data] [--wipe-data]
|
|
20
|
+
|
|
21
|
+
Stops the daemon and dashboard, then prints the npm command to remove the
|
|
22
|
+
binary. Offers to wipe the data directory.
|
|
23
|
+
|
|
24
|
+
Flags:
|
|
25
|
+
--keep-data Skip the data-wipe prompt; leave ~/.personal-agent intact.
|
|
26
|
+
--wipe-data Skip the confirmation prompt; wipe the data dir non-interactively.
|
|
27
|
+
(Both flags together is an error.)`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const keepData = args.includes("--keep-data");
|
|
32
|
+
const wipeData = args.includes("--wipe-data");
|
|
33
|
+
if (keepData && wipeData) {
|
|
34
|
+
console.error("--keep-data and --wipe-data are mutually exclusive.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Stop first — calling cmdStop here keeps the lifecycle logic in one place.
|
|
39
|
+
const daemonPid = ctx.helpers.getRunningPid(ctx.DAEMON_PID_FILE);
|
|
40
|
+
const dashPid = ctx.helpers.getRunningPid(ctx.DASHBOARD_PID_FILE);
|
|
41
|
+
if (daemonPid || dashPid) {
|
|
42
|
+
console.log(`Stopping ${ctx.APP_NAME}…`);
|
|
43
|
+
await ctx.helpers.cmdStop();
|
|
44
|
+
} else {
|
|
45
|
+
console.log(`${ctx.APP_NAME} is not running.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Prompt (or skip) for data wipe.
|
|
49
|
+
const dataInfo = describeDataDir(ctx.DATA_DIR);
|
|
50
|
+
let willWipe = false;
|
|
51
|
+
if (!keepData) {
|
|
52
|
+
if (wipeData) {
|
|
53
|
+
willWipe = true;
|
|
54
|
+
} else if (dataInfo.exists) {
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log(`Data directory: ${ctx.DATA_DIR} (${dataInfo.fileCount} files, ${dataInfo.sizeMb.toFixed(1)} MB)`);
|
|
57
|
+
console.log("Type WIPE to also delete the data directory, anything else to keep it:");
|
|
58
|
+
const reply = await readLineFromStdin();
|
|
59
|
+
willWipe = reply.trim() === "WIPE";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (willWipe) {
|
|
64
|
+
if (dataInfo.exists) {
|
|
65
|
+
fs.rmSync(ctx.DATA_DIR, { recursive: true, force: true });
|
|
66
|
+
console.log(`Wiped → ${ctx.DATA_DIR}`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log("Data directory does not exist; nothing to wipe.");
|
|
69
|
+
}
|
|
70
|
+
} else if (!keepData && dataInfo.exists) {
|
|
71
|
+
console.log(`Kept ${ctx.DATA_DIR} (${dataInfo.sizeMb.toFixed(1)} MB).`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log("To remove the binary, run:");
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log(" npm uninstall -g aitne");
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log("(We don't run this for you — npm handles uninstalling a global");
|
|
80
|
+
console.log("package more reliably than a process executing from inside it.)");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function describeDataDir(dir) {
|
|
84
|
+
if (!fs.existsSync(dir)) return { exists: false, fileCount: 0, sizeMb: 0 };
|
|
85
|
+
let fileCount = 0;
|
|
86
|
+
let totalBytes = 0;
|
|
87
|
+
const walk = (d) => {
|
|
88
|
+
let entries;
|
|
89
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
90
|
+
for (const e of entries) {
|
|
91
|
+
const full = path.join(d, e.name);
|
|
92
|
+
if (e.isDirectory()) walk(full);
|
|
93
|
+
else if (e.isFile()) {
|
|
94
|
+
fileCount++;
|
|
95
|
+
try { totalBytes += fs.statSync(full).size; } catch { /* race — ignore */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
walk(dir);
|
|
100
|
+
return { exists: true, fileCount, sizeMb: totalBytes / (1024 * 1024) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readLineFromStdin() {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
process.stdin.resume();
|
|
106
|
+
process.stdin.setEncoding("utf-8");
|
|
107
|
+
const onData = (chunk) => {
|
|
108
|
+
process.stdin.pause();
|
|
109
|
+
process.stdin.removeListener("data", onData);
|
|
110
|
+
resolve(chunk);
|
|
111
|
+
};
|
|
112
|
+
process.stdin.on("data", onData);
|
|
113
|
+
});
|
|
114
|
+
}
|