clementine-agent 1.1.28 → 1.1.30
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/dist/cli/index.js
CHANGED
|
@@ -155,7 +155,7 @@ function ensureDataHome() {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
// ── Commands ─────────────────────────────────────────────────────────
|
|
158
|
-
function cmdLaunch(options) {
|
|
158
|
+
async function cmdLaunch(options) {
|
|
159
159
|
if (options.uninstall) {
|
|
160
160
|
if (process.platform === 'darwin') {
|
|
161
161
|
const plistPath = getLaunchdPlistPath();
|
|
@@ -197,6 +197,22 @@ function cmdLaunch(options) {
|
|
|
197
197
|
}
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
|
+
// Ensure data home exists so the wizard's sentinel write lands somewhere,
|
|
201
|
+
// then run the one-time keychain ACL repair wizard if applicable. This
|
|
202
|
+
// happens before --install so the launchd-spawned daemon (no TTY) inherits
|
|
203
|
+
// already-repaired entries; before --foreground so the user sees the
|
|
204
|
+
// prompt; and before the daemon spawn for the same reason.
|
|
205
|
+
ensureDataHome();
|
|
206
|
+
if (!options.skipWizard) {
|
|
207
|
+
try {
|
|
208
|
+
const { runFirstRunKeychainWizardIfNeeded } = await import('../config/keychain-first-run-wizard.js');
|
|
209
|
+
await runFirstRunKeychainWizardIfNeeded(BASE_DIR);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// Wizard failure must never block launch — log and continue.
|
|
213
|
+
console.error(` ${'\x1b[0;90m'}keychain wizard skipped: ${err.message}${'\x1b[0m'}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
200
216
|
if (options.install) {
|
|
201
217
|
if (process.platform === 'darwin') {
|
|
202
218
|
const plistPath = getLaunchdPlistPath();
|
|
@@ -414,7 +430,7 @@ async function cmdRestart(options) {
|
|
|
414
430
|
}
|
|
415
431
|
}
|
|
416
432
|
catch { /* dashboard module may not be available */ }
|
|
417
|
-
cmdLaunch({ foreground: options.foreground });
|
|
433
|
+
await cmdLaunch({ foreground: options.foreground });
|
|
418
434
|
if (dashboardWasRunning) {
|
|
419
435
|
try {
|
|
420
436
|
const { spawn: spawnProc } = await import('node:child_process');
|
|
@@ -1317,6 +1333,7 @@ async function cmdConfigHardenPermissions(opts) {
|
|
|
1317
1333
|
// ── Config keychain-fix-acl ─────────────────────────────────────────
|
|
1318
1334
|
async function cmdConfigKeychainFixAcl(opts) {
|
|
1319
1335
|
const { listClementineKeychainEntries, fixAllClementineEntries } = await import('../config/keychain-fix-acl.js');
|
|
1336
|
+
const { markKeychainWizardDone } = await import('../config/keychain-first-run-wizard.js');
|
|
1320
1337
|
const DIM = '\x1b[0;90m';
|
|
1321
1338
|
const BOLD = '\x1b[1m';
|
|
1322
1339
|
const GREEN = '\x1b[0;32m';
|
|
@@ -1332,6 +1349,8 @@ async function cmdConfigKeychainFixAcl(opts) {
|
|
|
1332
1349
|
if (entries.length === 0) {
|
|
1333
1350
|
console.log(` ${GREEN}Nothing to fix.${RESET}`);
|
|
1334
1351
|
console.log();
|
|
1352
|
+
// No entries means the launch wizard has nothing to do either.
|
|
1353
|
+
markKeychainWizardDone(BASE_DIR);
|
|
1335
1354
|
return;
|
|
1336
1355
|
}
|
|
1337
1356
|
if (opts.list) {
|
|
@@ -1365,6 +1384,9 @@ async function cmdConfigKeychainFixAcl(opts) {
|
|
|
1365
1384
|
console.log(` ${DIM}Failed entries can be fixed manually in Keychain Access.app:${RESET}`);
|
|
1366
1385
|
console.log(` ${DIM} search "clementine-agent" → double-click → Access Control → Allow all applications.${RESET}`);
|
|
1367
1386
|
}
|
|
1387
|
+
// Always mark the launch wizard satisfied — user has explicitly decided
|
|
1388
|
+
// to deal with this via the manual command.
|
|
1389
|
+
markKeychainWizardDone(BASE_DIR);
|
|
1368
1390
|
console.log();
|
|
1369
1391
|
}
|
|
1370
1392
|
// ── Analytics ────────────────────────────────────────────────────────
|
|
@@ -3144,13 +3166,15 @@ async function cmdUpdate(options) {
|
|
|
3144
3166
|
console.log(` ${GREEN}OK${RESET} Daemon stopped`);
|
|
3145
3167
|
}
|
|
3146
3168
|
}
|
|
3147
|
-
// Helper: if update fails after stopping daemon, relaunch before exiting
|
|
3148
|
-
|
|
3169
|
+
// Helper: if update fails after stopping daemon, relaunch before exiting.
|
|
3170
|
+
// Runs cmdLaunch with skipWizard so it doesn't try to prompt mid-recovery —
|
|
3171
|
+
// the wizard, if needed, fires on the user's next intentional `clementine launch`.
|
|
3172
|
+
async function failAndRestart(backupDir) {
|
|
3149
3173
|
if (wasRunning) {
|
|
3150
3174
|
console.log();
|
|
3151
3175
|
console.log(` Restarting daemon (was running before update)...`);
|
|
3152
3176
|
try {
|
|
3153
|
-
cmdLaunch({});
|
|
3177
|
+
await cmdLaunch({ skipWizard: true });
|
|
3154
3178
|
console.log(` ${GREEN}OK${RESET} Daemon restarted`);
|
|
3155
3179
|
}
|
|
3156
3180
|
catch {
|
|
@@ -3242,7 +3266,7 @@ async function cmdUpdate(options) {
|
|
|
3242
3266
|
}
|
|
3243
3267
|
catch { /* best effort */ }
|
|
3244
3268
|
}
|
|
3245
|
-
failAndRestart(backupDir);
|
|
3269
|
+
await failAndRestart(backupDir);
|
|
3246
3270
|
}
|
|
3247
3271
|
// 6. npm install
|
|
3248
3272
|
console.log(` ${S()} Installing dependencies...`);
|
|
@@ -3255,7 +3279,7 @@ async function cmdUpdate(options) {
|
|
|
3255
3279
|
}
|
|
3256
3280
|
catch (err) {
|
|
3257
3281
|
console.error(` ${RED}FAIL${RESET} npm install failed: ${String(err).slice(0, 200)}`);
|
|
3258
|
-
failAndRestart(backupDir);
|
|
3282
|
+
await failAndRestart(backupDir);
|
|
3259
3283
|
}
|
|
3260
3284
|
// 6b. Rebuild native modules (better-sqlite3) for current Node version
|
|
3261
3285
|
try {
|
|
@@ -3347,7 +3371,7 @@ async function cmdUpdate(options) {
|
|
|
3347
3371
|
}
|
|
3348
3372
|
catch (retryErr) {
|
|
3349
3373
|
console.error(` ${RED}FAIL${RESET} Build failed after update: ${String(retryErr).slice(0, 200)}`);
|
|
3350
|
-
failAndRestart(backupDir);
|
|
3374
|
+
await failAndRestart(backupDir);
|
|
3351
3375
|
}
|
|
3352
3376
|
}
|
|
3353
3377
|
// 7b. Verify build output is fresh
|
|
@@ -3363,7 +3387,7 @@ async function cmdUpdate(options) {
|
|
|
3363
3387
|
}
|
|
3364
3388
|
catch (err) {
|
|
3365
3389
|
console.error(` ${RED}FAIL${RESET} Clean rebuild failed: ${String(err).slice(0, 200)}`);
|
|
3366
|
-
failAndRestart(backupDir);
|
|
3390
|
+
await failAndRestart(backupDir);
|
|
3367
3391
|
}
|
|
3368
3392
|
}
|
|
3369
3393
|
}
|
|
@@ -3615,7 +3639,7 @@ async function cmdUpdate(options) {
|
|
|
3615
3639
|
// Ensure build output is fully flushed before spawning new process
|
|
3616
3640
|
execSync('sync', { stdio: 'pipe' });
|
|
3617
3641
|
console.log(` ${S()} Restarting daemon...`);
|
|
3618
|
-
cmdLaunch({});
|
|
3642
|
+
await cmdLaunch({ skipWizard: true });
|
|
3619
3643
|
}
|
|
3620
3644
|
// 13. Post-restart health check — verify daemon started and channels connected
|
|
3621
3645
|
if (options.restart || wasRunning) {
|
|
@@ -3862,20 +3886,175 @@ const siCmd = program
|
|
|
3862
3886
|
.description('Manage Clementine self-improvement');
|
|
3863
3887
|
siCmd
|
|
3864
3888
|
.command('status')
|
|
3865
|
-
.description('Show self-improvement
|
|
3866
|
-
.
|
|
3889
|
+
.description('Show self-improvement health — last cycle, infra errors, per-agent runs, recent activity')
|
|
3890
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
3891
|
+
.action(async (opts) => {
|
|
3892
|
+
const BOLD = '\x1b[1m';
|
|
3893
|
+
const DIM = '\x1b[0;90m';
|
|
3894
|
+
const GREEN = '\x1b[0;32m';
|
|
3895
|
+
const YELLOW = '\x1b[1;33m';
|
|
3896
|
+
const RED = '\x1b[0;31m';
|
|
3897
|
+
const CYAN = '\x1b[0;36m';
|
|
3898
|
+
const RESET = '\x1b[0m';
|
|
3867
3899
|
try {
|
|
3900
|
+
process.env.CLEMENTINE_HOME = BASE_DIR;
|
|
3868
3901
|
const { SelfImproveLoop } = await import('../agent/self-improve.js');
|
|
3869
3902
|
const { PersonalAssistant } = await import('../agent/assistant.js');
|
|
3870
3903
|
const assistant = new PersonalAssistant();
|
|
3871
3904
|
const loop = new SelfImproveLoop(assistant);
|
|
3872
3905
|
const state = loop.loadState();
|
|
3873
|
-
const
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3906
|
+
const log = loop.loadExperimentLog();
|
|
3907
|
+
const pending = loop.getPendingChanges();
|
|
3908
|
+
// Compute "last successful cycle" — most recent log entry that wasn't
|
|
3909
|
+
// a plateau record or pure infra failure. Different from lastRunAt
|
|
3910
|
+
// (which moves on every attempt, even crashed ones).
|
|
3911
|
+
const lastSuccessful = [...log].reverse().find(e => e.area !== 'soul' || e.hypothesis !== 'No new hypothesis — diversity constraint exhausted');
|
|
3912
|
+
const nowMs = Date.now();
|
|
3913
|
+
const formatAge = (iso) => {
|
|
3914
|
+
if (!iso)
|
|
3915
|
+
return 'never';
|
|
3916
|
+
const ms = nowMs - Date.parse(iso);
|
|
3917
|
+
const h = Math.floor(ms / 3_600_000);
|
|
3918
|
+
if (h < 1)
|
|
3919
|
+
return `${Math.floor(ms / 60_000)}m ago`;
|
|
3920
|
+
if (h < 48)
|
|
3921
|
+
return `${h}h ago`;
|
|
3922
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
3923
|
+
};
|
|
3924
|
+
// Auto-applied count over the last 7 days = experiments with status 'approved'
|
|
3925
|
+
const since7d = nowMs - 7 * 86_400_000;
|
|
3926
|
+
const autoAppliedRecent = log.filter(e => e.approvalStatus === 'approved' && Date.parse(e.startedAt) >= since7d).length;
|
|
3927
|
+
// Per-agent SI runs from heartbeat state file.
|
|
3928
|
+
const hbStateFile = path.join(BASE_DIR, '.heartbeat_state.json');
|
|
3929
|
+
let perAgentRuns = {};
|
|
3930
|
+
try {
|
|
3931
|
+
if (existsSync(hbStateFile)) {
|
|
3932
|
+
const hb = JSON.parse(readFileSync(hbStateFile, 'utf-8'));
|
|
3933
|
+
perAgentRuns = (hb.lastAgentSiRuns ?? {});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
catch { /* non-fatal */ }
|
|
3937
|
+
if (opts.json) {
|
|
3938
|
+
console.log(JSON.stringify({
|
|
3939
|
+
state,
|
|
3940
|
+
lastSuccessfulAt: lastSuccessful?.startedAt ?? null,
|
|
3941
|
+
autoAppliedLast7d: autoAppliedRecent,
|
|
3942
|
+
pendingCount: pending.length,
|
|
3943
|
+
perAgentRuns,
|
|
3944
|
+
recent: log.slice(-5).reverse(),
|
|
3945
|
+
}, null, 2));
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
// ── Header ─────────────────────────────────────────────────────
|
|
3949
|
+
console.log();
|
|
3950
|
+
console.log(` ${BOLD}Self-improve loop${RESET}`);
|
|
3951
|
+
console.log(` ${DIM}Status: ${RESET}${state.status}`);
|
|
3952
|
+
console.log(` ${DIM}Last attempted: ${RESET}${state.lastRunAt || 'never'} ${DIM}(${formatAge(state.lastRunAt)})${RESET}`);
|
|
3953
|
+
// Stalled-loop warning: if we have a lastRunAt but no successful cycle
|
|
3954
|
+
// in 36+ hours, surface red. That's the visibility gap from pillar #4.
|
|
3955
|
+
const lastSuccAt = lastSuccessful?.startedAt;
|
|
3956
|
+
const hoursSinceSuccess = lastSuccAt ? (nowMs - Date.parse(lastSuccAt)) / 3_600_000 : null;
|
|
3957
|
+
if (lastSuccAt) {
|
|
3958
|
+
const stallTag = hoursSinceSuccess !== null && hoursSinceSuccess > 36 ? ` ${YELLOW}⚠ stalled${RESET}` : ` ${GREEN}✓${RESET}`;
|
|
3959
|
+
console.log(` ${DIM}Last successful: ${RESET}${lastSuccAt} ${DIM}(${formatAge(lastSuccAt)})${RESET}${stallTag}`);
|
|
3960
|
+
}
|
|
3961
|
+
else {
|
|
3962
|
+
console.log(` ${DIM}Last successful: ${RESET}never ${YELLOW}⚠ no cycles yet${RESET}`);
|
|
3963
|
+
}
|
|
3964
|
+
console.log(` ${DIM}Total experiments:${RESET} ${state.totalExperiments}`);
|
|
3965
|
+
console.log(` ${DIM}Auto-applied (7d):${RESET} ${autoAppliedRecent}`);
|
|
3966
|
+
console.log(` ${DIM}Pending review: ${RESET}${pending.length > 0 ? `${YELLOW}${pending.length}${RESET}` : '0'}`);
|
|
3967
|
+
if (state.infraError) {
|
|
3968
|
+
console.log();
|
|
3969
|
+
console.log(` ${RED}⚠ Infra error blocking the loop:${RESET}`);
|
|
3970
|
+
console.log(` Category: ${state.infraError.category}`);
|
|
3971
|
+
console.log(` Diagnostic: ${state.infraError.diagnostic.slice(0, 200)}`);
|
|
3972
|
+
}
|
|
3973
|
+
else {
|
|
3974
|
+
console.log(` ${DIM}Infra errors: ${RESET}${GREEN}none${RESET}`);
|
|
3975
|
+
}
|
|
3976
|
+
// ── Per-agent cycles ───────────────────────────────────────────
|
|
3977
|
+
const agentEntries = Object.entries(perAgentRuns);
|
|
3978
|
+
if (agentEntries.length > 0) {
|
|
3979
|
+
console.log();
|
|
3980
|
+
console.log(` ${BOLD}Per-agent cycles${RESET} ${DIM}(weekly cadence, 2 AM)${RESET}`);
|
|
3981
|
+
for (const [slug, iso] of agentEntries) {
|
|
3982
|
+
console.log(` ${CYAN}${slug.padEnd(28)}${RESET}${DIM}last run ${formatAge(iso)}${RESET}`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
// ── Recent activity ────────────────────────────────────────────
|
|
3986
|
+
const recent = log.slice(-5).reverse();
|
|
3987
|
+
if (recent.length > 0) {
|
|
3988
|
+
console.log();
|
|
3989
|
+
console.log(` ${BOLD}Recent activity${RESET}`);
|
|
3990
|
+
for (const e of recent) {
|
|
3991
|
+
const score = (e.score * 10).toFixed(1);
|
|
3992
|
+
let icon = '❌';
|
|
3993
|
+
if (e.approvalStatus === 'approved')
|
|
3994
|
+
icon = '✅';
|
|
3995
|
+
else if (e.approvalStatus === 'pending')
|
|
3996
|
+
icon = '⏳';
|
|
3997
|
+
else if (e.approvalStatus === 'unsurfaced')
|
|
3998
|
+
icon = '⛔';
|
|
3999
|
+
const what = e.hypothesis.slice(0, 60);
|
|
4000
|
+
console.log(` ${icon} ${DIM}#${String(e.iteration).padEnd(3)}${RESET} ${e.area.padEnd(16)} ${score.padStart(4)}/10 "${what}"`);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
if (pending.length > 0) {
|
|
4004
|
+
console.log();
|
|
4005
|
+
console.log(` ${YELLOW}${pending.length} change(s) pending your review${RESET}`);
|
|
4006
|
+
console.log(` ${BOLD}clementine self-improve pending${RESET} ${DIM}— see what they propose${RESET}`);
|
|
4007
|
+
console.log(` ${BOLD}clementine self-improve apply <id>${RESET} ${DIM}— approve and apply one${RESET}`);
|
|
4008
|
+
}
|
|
4009
|
+
console.log();
|
|
4010
|
+
}
|
|
4011
|
+
catch (err) {
|
|
4012
|
+
console.error('Error:', err);
|
|
4013
|
+
process.exit(1);
|
|
4014
|
+
}
|
|
4015
|
+
});
|
|
4016
|
+
siCmd
|
|
4017
|
+
.command('pending')
|
|
4018
|
+
.description('List pending self-improve changes — what needs your review')
|
|
4019
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
4020
|
+
.action(async (opts) => {
|
|
4021
|
+
const BOLD = '\x1b[1m';
|
|
4022
|
+
const DIM = '\x1b[0;90m';
|
|
4023
|
+
const YELLOW = '\x1b[1;33m';
|
|
4024
|
+
const CYAN = '\x1b[0;36m';
|
|
4025
|
+
const RESET = '\x1b[0m';
|
|
4026
|
+
try {
|
|
4027
|
+
process.env.CLEMENTINE_HOME = BASE_DIR;
|
|
4028
|
+
const { SelfImproveLoop } = await import('../agent/self-improve.js');
|
|
4029
|
+
const { PersonalAssistant } = await import('../agent/assistant.js');
|
|
4030
|
+
const assistant = new PersonalAssistant();
|
|
4031
|
+
const loop = new SelfImproveLoop(assistant);
|
|
4032
|
+
const pending = loop.getPendingChanges();
|
|
4033
|
+
if (opts.json) {
|
|
4034
|
+
console.log(JSON.stringify(pending.map(p => ({
|
|
4035
|
+
id: p.id, area: p.area, target: p.target,
|
|
4036
|
+
score: p.score, hypothesis: p.hypothesis, reason: p.reason,
|
|
4037
|
+
})), null, 2));
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4040
|
+
if (pending.length === 0) {
|
|
4041
|
+
console.log();
|
|
4042
|
+
console.log(` ${DIM}No changes pending review.${RESET}`);
|
|
4043
|
+
console.log();
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
console.log();
|
|
4047
|
+
console.log(` ${YELLOW}${pending.length} change${pending.length === 1 ? '' : 's'} pending${RESET}`);
|
|
4048
|
+
console.log();
|
|
4049
|
+
for (const p of pending) {
|
|
4050
|
+
const score = (p.score * 10).toFixed(1);
|
|
4051
|
+
console.log(` ${BOLD}#${p.id}${RESET} ${CYAN}${p.area}${RESET} ${DIM}→${RESET} ${p.target} ${DIM}(score ${score}/10)${RESET}`);
|
|
4052
|
+
console.log(` ${p.hypothesis}`);
|
|
4053
|
+
console.log(` ${DIM}${p.reason}${RESET}`);
|
|
4054
|
+
console.log();
|
|
4055
|
+
}
|
|
4056
|
+
console.log(` Apply: ${BOLD}clementine self-improve apply <id>${RESET}`);
|
|
4057
|
+
console.log();
|
|
3879
4058
|
}
|
|
3880
4059
|
catch (err) {
|
|
3881
4060
|
console.error('Error:', err);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time interactive wizard that repairs ACLs on legacy clementine-agent
|
|
3
|
+
* keychain entries during `clementine launch`.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: entries written before commit 88cfd99 used
|
|
6
|
+
* `add-generic-password -T ''` (no apps pre-approved), so every Clementine
|
|
7
|
+
* read would trigger a per-app approval dialog. Newer writes use
|
|
8
|
+
* `-T /usr/bin/security` and read silently. Existing users have legacy
|
|
9
|
+
* entries that need a one-time partition-list repair to stop the prompt
|
|
10
|
+
* cascade.
|
|
11
|
+
*
|
|
12
|
+
* The manual fix is `clementine config keychain-fix-acl`. This wizard runs
|
|
13
|
+
* the same fix automatically on the next `clementine launch` (where we
|
|
14
|
+
* know we have a TTY for the macOS login-keychain password prompt), then
|
|
15
|
+
* writes a sentinel so we never prompt again on this machine.
|
|
16
|
+
*
|
|
17
|
+
* Skipped when:
|
|
18
|
+
* - non-darwin platform (no keychain),
|
|
19
|
+
* - non-TTY stdin (launchd, systemd, CI — no way to prompt),
|
|
20
|
+
* - sentinel already exists (already offered + decided),
|
|
21
|
+
* - no clementine-agent entries exist (nothing to repair).
|
|
22
|
+
*/
|
|
23
|
+
/** Write the sentinel so the wizard skips on subsequent launches. */
|
|
24
|
+
export declare function markKeychainWizardDone(baseDir: string): void;
|
|
25
|
+
export declare function runFirstRunKeychainWizardIfNeeded(baseDir: string): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=keychain-first-run-wizard.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time interactive wizard that repairs ACLs on legacy clementine-agent
|
|
3
|
+
* keychain entries during `clementine launch`.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: entries written before commit 88cfd99 used
|
|
6
|
+
* `add-generic-password -T ''` (no apps pre-approved), so every Clementine
|
|
7
|
+
* read would trigger a per-app approval dialog. Newer writes use
|
|
8
|
+
* `-T /usr/bin/security` and read silently. Existing users have legacy
|
|
9
|
+
* entries that need a one-time partition-list repair to stop the prompt
|
|
10
|
+
* cascade.
|
|
11
|
+
*
|
|
12
|
+
* The manual fix is `clementine config keychain-fix-acl`. This wizard runs
|
|
13
|
+
* the same fix automatically on the next `clementine launch` (where we
|
|
14
|
+
* know we have a TTY for the macOS login-keychain password prompt), then
|
|
15
|
+
* writes a sentinel so we never prompt again on this machine.
|
|
16
|
+
*
|
|
17
|
+
* Skipped when:
|
|
18
|
+
* - non-darwin platform (no keychain),
|
|
19
|
+
* - non-TTY stdin (launchd, systemd, CI — no way to prompt),
|
|
20
|
+
* - sentinel already exists (already offered + decided),
|
|
21
|
+
* - no clementine-agent entries exist (nothing to repair).
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import readline from 'node:readline/promises';
|
|
26
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
27
|
+
const SENTINEL_FILE = '.keychain-acl-wizard-done';
|
|
28
|
+
function sentinelPath(baseDir) {
|
|
29
|
+
return path.join(baseDir, SENTINEL_FILE);
|
|
30
|
+
}
|
|
31
|
+
/** Write the sentinel so the wizard skips on subsequent launches. */
|
|
32
|
+
export function markKeychainWizardDone(baseDir) {
|
|
33
|
+
try {
|
|
34
|
+
writeFileSync(sentinelPath(baseDir), new Date().toISOString() + '\n');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Best-effort. If we can't write the sentinel the user gets re-prompted
|
|
38
|
+
// next launch — annoying but not broken.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
42
|
+
if (process.platform !== 'darwin')
|
|
43
|
+
return;
|
|
44
|
+
if (!input.isTTY)
|
|
45
|
+
return;
|
|
46
|
+
if (existsSync(sentinelPath(baseDir)))
|
|
47
|
+
return;
|
|
48
|
+
const { listClementineKeychainEntries, fixAllClementineEntries } = await import('./keychain-fix-acl.js');
|
|
49
|
+
const entries = listClementineKeychainEntries().filter((e) => e.isClementine);
|
|
50
|
+
if (entries.length === 0) {
|
|
51
|
+
// Nothing to fix — write sentinel so we don't re-scan every launch.
|
|
52
|
+
markKeychainWizardDone(baseDir);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const DIM = '\x1b[0;90m';
|
|
56
|
+
const BOLD = '\x1b[1m';
|
|
57
|
+
const YELLOW = '\x1b[0;33m';
|
|
58
|
+
const GREEN = '\x1b[0;32m';
|
|
59
|
+
const RED = '\x1b[0;31m';
|
|
60
|
+
const RESET = '\x1b[0m';
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(` ${BOLD}One-time keychain setup${RESET}`);
|
|
63
|
+
console.log(` ${DIM}${entries.length} keychain entr${entries.length === 1 ? 'y' : 'ies'} from a previous version need an${RESET}`);
|
|
64
|
+
console.log(` ${DIM}access-control update so Clementine can read them${RESET}`);
|
|
65
|
+
console.log(` ${DIM}silently — otherwise macOS will prompt on every read.${RESET}`);
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(` ${DIM}macOS will ask once for your login-keychain password.${RESET}`);
|
|
68
|
+
console.log(` ${DIM}After that, no more prompts. We won't ask again.${RESET}`);
|
|
69
|
+
console.log();
|
|
70
|
+
const rl = readline.createInterface({ input, output });
|
|
71
|
+
let answer;
|
|
72
|
+
try {
|
|
73
|
+
answer = (await rl.question(` Repair now? ${DIM}[Y/n]${RESET} `)).trim().toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rl.close();
|
|
77
|
+
}
|
|
78
|
+
if (answer === 'n' || answer === 'no') {
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(` ${YELLOW}Skipped.${RESET} ${DIM}Run later with: clementine config keychain-fix-acl${RESET}`);
|
|
81
|
+
console.log();
|
|
82
|
+
markKeychainWizardDone(baseDir);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(` ${BOLD}Repairing ACLs...${RESET}`);
|
|
87
|
+
console.log();
|
|
88
|
+
const results = fixAllClementineEntries();
|
|
89
|
+
let okCount = 0;
|
|
90
|
+
let failCount = 0;
|
|
91
|
+
for (const r of results) {
|
|
92
|
+
if (r.status === 'fixed') {
|
|
93
|
+
console.log(` ${GREEN}✓${RESET} ${r.account}`);
|
|
94
|
+
okCount++;
|
|
95
|
+
}
|
|
96
|
+
else if (r.status === 'failed') {
|
|
97
|
+
console.log(` ${RED}✗${RESET} ${r.account} ${DIM}— ${r.error ?? 'unknown'}${RESET}`);
|
|
98
|
+
failCount++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
if (failCount === 0) {
|
|
103
|
+
console.log(` ${GREEN}Done — ${okCount} entr${okCount === 1 ? 'y' : 'ies'} repaired.${RESET} ${DIM}Future reads silent.${RESET}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(` ${YELLOW}${okCount} fixed, ${failCount} failed.${RESET}`);
|
|
107
|
+
console.log(` ${DIM}Failed entries can be fixed manually in Keychain Access.app:${RESET}`);
|
|
108
|
+
console.log(` ${DIM} search "clementine-agent" → right-click → Get Info → Access Control.${RESET}`);
|
|
109
|
+
}
|
|
110
|
+
console.log();
|
|
111
|
+
// Always mark done — even on partial failure we don't want to re-prompt
|
|
112
|
+
// every launch. The user can re-run the manual command if they want.
|
|
113
|
+
markKeychainWizardDone(baseDir);
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=keychain-first-run-wizard.js.map
|