phewsh 0.14.0 โ 0.14.2
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/bin/phewsh.js +2 -2
- package/commands/receipts.js +1 -1
- package/commands/session.js +150 -15
- package/commands/update.js +1 -1
- package/lib/projects-index.js +90 -0
- package/lib/ui.js +25 -86
- package/package.json +1 -1
package/bin/phewsh.js
CHANGED
|
@@ -7,7 +7,7 @@ const command = args[0];
|
|
|
7
7
|
const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
|
|
8
8
|
const d = (s) => `\x1b[2m${s}\x1b[0m`; // dim
|
|
9
9
|
const w = (s) => `\x1b[97m${s}\x1b[0m`; // bright white
|
|
10
|
-
const g = (s) => `\x1b[38;
|
|
10
|
+
const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`; // slate, 256-color (matches ui.js โ 24-bit breaks Apple Terminal)
|
|
11
11
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
12
12
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
13
13
|
|
|
@@ -26,7 +26,7 @@ function showBrand() {
|
|
|
26
26
|
} catch { /* no config */ }
|
|
27
27
|
|
|
28
28
|
console.log('');
|
|
29
|
-
console.log(
|
|
29
|
+
console.log(' ๐ฎ\u200d๐จ ๐คซ');
|
|
30
30
|
console.log('');
|
|
31
31
|
console.log(` ${b(w('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
32
32
|
console.log(` ${b(w('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
package/commands/receipts.js
CHANGED
|
@@ -21,7 +21,7 @@ const { gatherReceipts } = require('../lib/receipts-data');
|
|
|
21
21
|
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
22
22
|
const d = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
23
23
|
const w = (s) => `\x1b[97m${s}\x1b[0m`;
|
|
24
|
-
const g = (s) => `\x1b[38;
|
|
24
|
+
const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
|
|
25
25
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
26
26
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
27
27
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
package/commands/session.js
CHANGED
|
@@ -11,7 +11,8 @@ const ui = require('../lib/ui');
|
|
|
11
11
|
|
|
12
12
|
const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
|
|
13
13
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
14
|
-
|
|
14
|
+
// Dynamic โ the session can chdir into a project from the root bootstrap
|
|
15
|
+
const intentDir = () => path.join(process.cwd(), '.intent');
|
|
15
16
|
|
|
16
17
|
const { select, refreshSession: refreshSess } = require('../lib/supabase');
|
|
17
18
|
const { readPPS } = require('../lib/pps');
|
|
@@ -19,6 +20,7 @@ const { push, pull, ensureValidToken } = require('./sync');
|
|
|
19
20
|
const { HARNESSES, listHarnesses, runViaHarness } = require('../lib/harnesses');
|
|
20
21
|
const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
21
22
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
23
|
+
const { recordProject, listProjects, scanForProjects, fmtAgo } = require('../lib/projects-index');
|
|
22
24
|
|
|
23
25
|
// Brand palette shortcuts
|
|
24
26
|
const { b, d, w, g, green, cyan, yellow,
|
|
@@ -27,13 +29,13 @@ const { b, d, w, g, green, cyan, yellow,
|
|
|
27
29
|
// Sync awareness: compare local .intent/ timestamps with cloud updated_at
|
|
28
30
|
async function checkSyncStatus(config) {
|
|
29
31
|
if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
|
|
30
|
-
if (!fs.existsSync(
|
|
32
|
+
if (!fs.existsSync(intentDir())) return null;
|
|
31
33
|
|
|
32
34
|
try {
|
|
33
35
|
const token = await ensureValidToken(config);
|
|
34
36
|
if (!token) return null;
|
|
35
37
|
|
|
36
|
-
const pps = readPPS(
|
|
38
|
+
const pps = readPPS(intentDir());
|
|
37
39
|
const cloudId = pps?.adapters?.phewsh?.cloud_id;
|
|
38
40
|
const projectName = path.basename(process.cwd());
|
|
39
41
|
|
|
@@ -59,7 +61,7 @@ async function checkSyncStatus(config) {
|
|
|
59
61
|
const localFiles = ['vision.md', 'plan.md', 'next.md'];
|
|
60
62
|
let latestLocal = 0;
|
|
61
63
|
for (const file of localFiles) {
|
|
62
|
-
const filePath = path.join(
|
|
64
|
+
const filePath = path.join(intentDir(), file);
|
|
63
65
|
if (fs.existsSync(filePath)) {
|
|
64
66
|
const mtime = fs.statSync(filePath).mtimeMs;
|
|
65
67
|
if (mtime > latestLocal) latestLocal = mtime;
|
|
@@ -154,7 +156,7 @@ function loadIntentContext() {
|
|
|
154
156
|
const files = ['vision.md', 'plan.md', 'next.md'];
|
|
155
157
|
const loaded = [];
|
|
156
158
|
for (const file of files) {
|
|
157
|
-
const p = path.join(
|
|
159
|
+
const p = path.join(intentDir(), file);
|
|
158
160
|
if (fs.existsSync(p)) {
|
|
159
161
|
loaded.push({ file, content: fs.readFileSync(p, 'utf-8') });
|
|
160
162
|
}
|
|
@@ -255,7 +257,12 @@ async function main() {
|
|
|
255
257
|
let intentFiles = loadIntentContext();
|
|
256
258
|
let systemPrompt = buildSystemPrompt(intentFiles);
|
|
257
259
|
const messages = [];
|
|
258
|
-
|
|
260
|
+
let projectName = path.basename(process.cwd());
|
|
261
|
+
|
|
262
|
+
// Index this project so `phewsh` from anywhere can offer it as a recent
|
|
263
|
+
if (intentFiles.length > 0) {
|
|
264
|
+
try { recordProject(process.cwd()); } catch { /* index is best-effort */ }
|
|
265
|
+
}
|
|
259
266
|
let currentModel = DEFAULT_MODEL;
|
|
260
267
|
let totalPromptTokens = 0;
|
|
261
268
|
let totalCompletionTokens = 0;
|
|
@@ -267,6 +274,7 @@ async function main() {
|
|
|
267
274
|
let sessionMode = null; // INTENT_MODES id once picked
|
|
268
275
|
let awaitingOutcome = null; // decision id eligible for 1-4 labeling
|
|
269
276
|
let awaitingFallback = null; // { input, fullSystem, options } after a route failure
|
|
277
|
+
let bootstrapChoices = null; // root-bootstrap menu entries when no project here
|
|
270
278
|
let decisionsThisSession = 0;
|
|
271
279
|
|
|
272
280
|
// โโ The Exhale: animated brand reveal โโโโโโโโโโโโโโโโโโ
|
|
@@ -292,9 +300,26 @@ async function main() {
|
|
|
292
300
|
|
|
293
301
|
const row = (label, value) => console.log(` ${slate(label.padEnd(9))}${value}`);
|
|
294
302
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
// realpath both sides โ macOS /tmp and /var are symlinks into /private
|
|
304
|
+
const realPath = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
305
|
+
const tildify = (p) => {
|
|
306
|
+
const home = realPath(os.homedir());
|
|
307
|
+
const rp = realPath(p);
|
|
308
|
+
return rp.startsWith(home) ? '~' + rp.slice(home.length) : p;
|
|
309
|
+
};
|
|
310
|
+
const atHome = realPath(process.cwd()) === realPath(os.homedir());
|
|
311
|
+
const recents = intentFiles.length === 0
|
|
312
|
+
? listProjects().filter(p => realPath(p.path) !== realPath(process.cwd())).slice(0, 3)
|
|
313
|
+
: [];
|
|
314
|
+
|
|
315
|
+
if (intentFiles.length > 0) {
|
|
316
|
+
row('PROJECT', cream(projectName) + slate(' ยท ') + teal('โ')
|
|
317
|
+
+ sage(` .intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''}`));
|
|
318
|
+
} else if (atHome || recents.length > 0) {
|
|
319
|
+
row('PROJECT', slate('none here โ your projects are listed below'));
|
|
320
|
+
} else {
|
|
321
|
+
row('PROJECT', cream(projectName) + slate(' ยท no memory yet โ ') + sage('/init'));
|
|
322
|
+
}
|
|
298
323
|
|
|
299
324
|
row('ROUTE', route
|
|
300
325
|
? cream(routeLabel(route, config)) + (route.type === 'harness' ? slate(' ยท no API key needed') : '')
|
|
@@ -310,11 +335,29 @@ async function main() {
|
|
|
310
335
|
: slate('none โ install Codex or Gemini to cover usage limits'));
|
|
311
336
|
|
|
312
337
|
if (config?.supabaseUserId) {
|
|
338
|
+
// Cloud project count, best-effort with a hard 2s budget โ the cockpit
|
|
339
|
+
// never blocks on the network.
|
|
340
|
+
let cloudCount = null;
|
|
341
|
+
try {
|
|
342
|
+
const token = await Promise.race([
|
|
343
|
+
ensureValidToken(config),
|
|
344
|
+
new Promise(r => setTimeout(() => r(null), 2000)),
|
|
345
|
+
]);
|
|
346
|
+
if (token) {
|
|
347
|
+
const rows = await Promise.race([
|
|
348
|
+
select('projects', `user_id=eq.${config.supabaseUserId}&select=id`, token),
|
|
349
|
+
new Promise(r => setTimeout(() => r(null), 2000)),
|
|
350
|
+
]);
|
|
351
|
+
if (Array.isArray(rows)) cloudCount = rows.length;
|
|
352
|
+
}
|
|
353
|
+
} catch { /* offline โ cockpit still renders */ }
|
|
354
|
+
|
|
313
355
|
const syncLabel = syncState?.status === 'synced' ? teal('โ ') + sage('mirrored')
|
|
314
356
|
: syncState?.status === 'cloud-newer' ? ember('โ ') + sage(`cloud newer (${syncState.ago}) โ /pull`)
|
|
315
357
|
: syncState?.status === 'local-newer' ? ember('โ ') + sage('local ahead โ /push')
|
|
316
358
|
: sage('linked');
|
|
317
|
-
row('WEB', cream(config.email || 'logged in') + slate(' ยท ') + syncLabel
|
|
359
|
+
row('WEB', cream(config.email || 'logged in') + slate(' ยท ') + syncLabel
|
|
360
|
+
+ (cloudCount !== null ? slate(' ยท ') + sage(`${cloudCount} cloud project${cloudCount !== 1 ? 's' : ''}`) : ''));
|
|
318
361
|
} else {
|
|
319
362
|
row('WEB', sage('local-only (works fine)') + slate(' ยท /login mirrors this at phewsh.com/intent'));
|
|
320
363
|
}
|
|
@@ -329,6 +372,45 @@ async function main() {
|
|
|
329
372
|
row('RECORD', slate('empty โ decisions and outcomes accumulate as you work'));
|
|
330
373
|
}
|
|
331
374
|
|
|
375
|
+
function showModeMenu() {
|
|
376
|
+
console.log(` ${b(cream('What are you trying to do?'))}`);
|
|
377
|
+
console.log(` ${teal('1')} ${sage('Build')} ${slate('ยท')} ${teal('2')} ${sage('Research')} ${slate('ยท')} ${teal('3')} ${sage('Decide')} ${slate('ยท')} ${teal('4')} ${sage('Review')} ${slate('ยท')} ${teal('5')} ${sage('Ask another model')}`);
|
|
378
|
+
console.log(` ${slate('pick a number, or just type โ your context travels with every route')}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Open a known project from the bootstrap menu: chdir, reload memory,
|
|
382
|
+
// back to the normal flow. The session is the cockpit; projects swap in.
|
|
383
|
+
function openProjectAt(dir) {
|
|
384
|
+
try { process.chdir(dir); } catch (err) {
|
|
385
|
+
console.log(` ${ember('!')} ${sage('Could not open ' + dir + ': ' + err.message)}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
projectName = path.basename(dir);
|
|
389
|
+
intentFiles = loadIntentContext();
|
|
390
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
391
|
+
try { recordProject(dir); } catch { /* best-effort */ }
|
|
392
|
+
bootstrapChoices = null;
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(` ${teal('โ')} ${cream(projectName)} ${slate('ยท')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('ยท via ' + routeLabel(route, config))}`);
|
|
395
|
+
console.log('');
|
|
396
|
+
showModeMenu();
|
|
397
|
+
console.log('');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function showBootstrapMenu(projects) {
|
|
401
|
+
console.log(` ${b(cream('Where do you want to work?'))}`);
|
|
402
|
+
bootstrapChoices = [];
|
|
403
|
+
for (const p of projects) {
|
|
404
|
+
bootstrapChoices.push({ kind: 'open', path: p.path });
|
|
405
|
+
console.log(` ${teal(String(bootstrapChoices.length))} ${cream(p.name)} ${slate('โ ' + fmtAgo(p.lastOpened) + ' ยท ' + tildify(p.path))}`);
|
|
406
|
+
}
|
|
407
|
+
bootstrapChoices.push({ kind: 'init' });
|
|
408
|
+
const initN = bootstrapChoices.length;
|
|
409
|
+
bootstrapChoices.push({ kind: 'scan' });
|
|
410
|
+
console.log(` ${teal(String(initN))} ${sage('Start a project here')} ${slate('(/init)')} ${slate('ยท')} ${teal(String(initN + 1))} ${sage('Scan my folders for projects')}`);
|
|
411
|
+
console.log(` ${slate('pick a number, or just type to chat โ no project required')}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
332
414
|
console.log('');
|
|
333
415
|
if (!route) {
|
|
334
416
|
// Nothing to route through: no key, no agent CLIs found on this machine.
|
|
@@ -340,10 +422,10 @@ async function main() {
|
|
|
340
422
|
console.log(` ${teal('/key')} ${sage('Set an API key (10 seconds)')}`);
|
|
341
423
|
console.log(` ${teal('/tour')} ${sage('See what this does (nothing needed)')}`);
|
|
342
424
|
console.log(` ${slate('Or install Claude Code / Codex โ phewsh uses their login automatically.')}`);
|
|
425
|
+
} else if (intentFiles.length === 0 && (atHome || recents.length > 0)) {
|
|
426
|
+
showBootstrapMenu(recents);
|
|
343
427
|
} else {
|
|
344
|
-
|
|
345
|
-
console.log(` ${teal('1')} ${sage('Build')} ${slate('ยท')} ${teal('2')} ${sage('Research')} ${slate('ยท')} ${teal('3')} ${sage('Decide')} ${slate('ยท')} ${teal('4')} ${sage('Review')} ${slate('ยท')} ${teal('5')} ${sage('Ask another model')}`);
|
|
346
|
-
console.log(` ${slate('pick a number, or just type โ your context travels with every route')}`);
|
|
428
|
+
showModeMenu();
|
|
347
429
|
}
|
|
348
430
|
console.log('');
|
|
349
431
|
|
|
@@ -493,6 +575,59 @@ async function main() {
|
|
|
493
575
|
return;
|
|
494
576
|
}
|
|
495
577
|
|
|
578
|
+
// Root bootstrap: a bare number opens a project, inits, or scans
|
|
579
|
+
if (bootstrapChoices && messages.length === 0 && /^[0-9]{1,2}$/.test(input)) {
|
|
580
|
+
const choice = bootstrapChoices[parseInt(input, 10) - 1];
|
|
581
|
+
if (!choice) {
|
|
582
|
+
console.log(` ${sage('Pick 1-' + bootstrapChoices.length)}`);
|
|
583
|
+
rl.prompt();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (choice.kind === 'open') {
|
|
587
|
+
openProjectAt(choice.path);
|
|
588
|
+
rl.prompt();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (choice.kind === 'init') {
|
|
592
|
+
bootstrapChoices = null;
|
|
593
|
+
try {
|
|
594
|
+
const { execSync } = require('child_process');
|
|
595
|
+
execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
|
|
596
|
+
intentFiles = loadIntentContext();
|
|
597
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
598
|
+
if (intentFiles.length > 0) {
|
|
599
|
+
try { recordProject(process.cwd()); } catch { /* best-effort */ }
|
|
600
|
+
console.log(` ${teal('โ')} ${sage('Project started โ context loaded:')} ${cream(intentFiles.map(f => f.file).join(', '))}`);
|
|
601
|
+
}
|
|
602
|
+
} catch (err) {
|
|
603
|
+
console.error(` ${ember('!')} ${sage('Init failed:')} ${err.message}`);
|
|
604
|
+
}
|
|
605
|
+
console.log('');
|
|
606
|
+
rl.prompt();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (choice.kind === 'scan') {
|
|
610
|
+
const spin = ui.spinner('scanning your usual folders');
|
|
611
|
+
const found = scanForProjects();
|
|
612
|
+
spin.stop();
|
|
613
|
+
if (found.length === 0) {
|
|
614
|
+
bootstrapChoices = null;
|
|
615
|
+
console.log(` ${sage('No .intent/ projects found in the usual folders.')}`);
|
|
616
|
+
console.log(` ${slate('cd into a project and run phewsh, or /init to start one here.')}`);
|
|
617
|
+
} else {
|
|
618
|
+
console.log(` ${teal('โ')} ${sage(`Found ${found.length} project${found.length !== 1 ? 's' : ''}:`)}`);
|
|
619
|
+
bootstrapChoices = found.map(p => ({ kind: 'open', path: p.path }));
|
|
620
|
+
found.forEach((p, i) => {
|
|
621
|
+
console.log(` ${teal(String(i + 1))} ${cream(p.name)} ${slate('ยท ' + tildify(p.path))}`);
|
|
622
|
+
});
|
|
623
|
+
console.log(` ${slate('pick a number to open it')}`);
|
|
624
|
+
}
|
|
625
|
+
console.log('');
|
|
626
|
+
rl.prompt();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
496
631
|
// A bare 1-5 on an empty conversation picks an intent mode
|
|
497
632
|
if (messages.length === 0 && !awaitingOutcome && /^[1-5]$/.test(input)) {
|
|
498
633
|
const n = parseInt(input, 10);
|
|
@@ -669,7 +804,7 @@ async function main() {
|
|
|
669
804
|
}
|
|
670
805
|
|
|
671
806
|
if (cmd === 'init') {
|
|
672
|
-
if (fs.existsSync(path.join(
|
|
807
|
+
if (fs.existsSync(path.join(intentDir(), 'vision.md'))) {
|
|
673
808
|
console.log(`\n ${sage('.intent/ already exists in')} ${slate(process.cwd())}`);
|
|
674
809
|
console.log(` ${sage('Use /reload to refresh context')}\n`);
|
|
675
810
|
} else {
|
|
@@ -1053,7 +1188,7 @@ async function main() {
|
|
|
1053
1188
|
}
|
|
1054
1189
|
|
|
1055
1190
|
if (cmd === 'watch') {
|
|
1056
|
-
if (!fs.existsSync(
|
|
1191
|
+
if (!fs.existsSync(intentDir())) {
|
|
1057
1192
|
console.log(`\n ${ember('!')} ${sage('No .intent/ found. Run /init first.')}\n`);
|
|
1058
1193
|
rl.prompt();
|
|
1059
1194
|
return;
|
package/commands/update.js
CHANGED
|
@@ -8,7 +8,7 @@ const { spawn } = require('child_process');
|
|
|
8
8
|
|
|
9
9
|
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
10
10
|
const w = (s) => `\x1b[97m${s}\x1b[0m`;
|
|
11
|
-
const g = (s) => `\x1b[38;
|
|
11
|
+
const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
|
|
12
12
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
13
13
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
14
14
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Local project index โ how phewsh knows your projects from anywhere.
|
|
2
|
+
//
|
|
3
|
+
// Every session opened in a project (or created via /init) records it here,
|
|
4
|
+
// so running `phewsh` at machine root becomes mission-control bootstrap
|
|
5
|
+
// ("where do you want to work?") instead of "no project found, goodbye."
|
|
6
|
+
//
|
|
7
|
+
// Storage: ~/.phewsh/projects.json. Local-first; web sync layers on top.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
const INDEX_FILE = path.join(os.homedir(), '.phewsh', 'projects.json');
|
|
14
|
+
|
|
15
|
+
// Shallow-scanned roots when the user asks to find projects. One level deep,
|
|
16
|
+
// opt-in only โ deep-scanning someone's machine uninvited is invasive.
|
|
17
|
+
const SCAN_ROOTS = [
|
|
18
|
+
path.join(os.homedir(), 'Documents', 'GitHub'),
|
|
19
|
+
path.join(os.homedir(), 'Projects'),
|
|
20
|
+
path.join(os.homedir(), 'projects'),
|
|
21
|
+
path.join(os.homedir(), 'repos'),
|
|
22
|
+
path.join(os.homedir(), 'Developer'),
|
|
23
|
+
path.join(os.homedir(), 'code'),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function load() {
|
|
27
|
+
try { return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8')); } catch { return { projects: {} }; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function save(index) {
|
|
31
|
+
fs.mkdirSync(path.dirname(INDEX_FILE), { recursive: true });
|
|
32
|
+
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Upsert the project at `dir` (called whenever a session opens one). */
|
|
36
|
+
function recordProject(dir, extra = {}) {
|
|
37
|
+
const index = load();
|
|
38
|
+
const key = path.resolve(dir);
|
|
39
|
+
index.projects[key] = {
|
|
40
|
+
...(index.projects[key] || {}),
|
|
41
|
+
name: path.basename(key),
|
|
42
|
+
path: key,
|
|
43
|
+
lastOpened: new Date().toISOString(),
|
|
44
|
+
...extra,
|
|
45
|
+
};
|
|
46
|
+
save(index);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Known projects, most recently opened first. Prunes paths that vanished. */
|
|
50
|
+
function listProjects() {
|
|
51
|
+
const index = load();
|
|
52
|
+
const alive = Object.values(index.projects).filter(p => {
|
|
53
|
+
try { return fs.existsSync(path.join(p.path, '.intent')); } catch { return false; }
|
|
54
|
+
});
|
|
55
|
+
return alive.sort((a, b) => String(b.lastOpened).localeCompare(String(a.lastOpened)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Shallow scan: direct children of common roots that contain .intent/. */
|
|
59
|
+
function scanForProjects() {
|
|
60
|
+
const found = [];
|
|
61
|
+
const seen = new Set(); // realpath-dedupe โ case-insensitive FS makes ~/Projects and ~/projects one dir
|
|
62
|
+
for (const root of SCAN_ROOTS) {
|
|
63
|
+
let entries;
|
|
64
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
if (!e.isDirectory() || e.name.startsWith('.')) continue;
|
|
67
|
+
const dir = path.join(root, e.name);
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(path.join(dir, '.intent', 'vision.md'))) {
|
|
70
|
+
const real = fs.realpathSync(dir);
|
|
71
|
+
if (seen.has(real)) continue;
|
|
72
|
+
seen.add(real);
|
|
73
|
+
found.push({ name: e.name, path: real });
|
|
74
|
+
}
|
|
75
|
+
} catch { /* unreadable dir โ skip */ }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fmtAgo(ts) {
|
|
82
|
+
if (!ts) return '';
|
|
83
|
+
const mins = Math.floor((Date.now() - new Date(ts).getTime()) / 60000);
|
|
84
|
+
if (mins < 60) return `${mins}m ago`;
|
|
85
|
+
const hrs = Math.floor(mins / 60);
|
|
86
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
87
|
+
return `${Math.floor(hrs / 24)}d ago`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { INDEX_FILE, SCAN_ROOTS, recordProject, listProjects, scanForProjects, fmtAgo };
|
package/lib/ui.js
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
// Zero dependencies. Pure ANSI. The terminal breathes.
|
|
4
4
|
|
|
5
5
|
// โโ PHEWSH palette โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
6
|
+
// 256-color (38;5;n), NOT 24-bit (38;2;r;g;b). Terminals without truecolor
|
|
7
|
+
// (Apple Terminal et al.) misparse 24-bit params as separate SGR codes โ
|
|
8
|
+
// e.g. teal's red channel "100" became "bright black BACKGROUND" = grey
|
|
9
|
+
// boxes behind text. 256-color is a single param: renders the same
|
|
10
|
+
// everywhere. Comfy in every terminal beats precise in some.
|
|
11
|
+
const c256 = (n) => (s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
|
|
12
|
+
const rgb = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`; // kept for callers; avoid for new UI
|
|
9
13
|
const rgbBg = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[0m`;
|
|
10
14
|
|
|
11
15
|
// Brand colors โ relief, quiet, future
|
|
12
|
-
const teal =
|
|
13
|
-
const peach =
|
|
14
|
-
const sage =
|
|
15
|
-
const slate =
|
|
16
|
-
const cream =
|
|
17
|
-
const ember =
|
|
16
|
+
const teal = c256(79); // #5fd7af cool calm โ primary
|
|
17
|
+
const peach = c256(216); // #ffaf87 warm exhale โ accent
|
|
18
|
+
const sage = c256(151); // #afd7af quiet โ secondary text
|
|
19
|
+
const slate = c256(247); // #9e9e9e whisper โ dim but legible
|
|
20
|
+
const cream = c256(230); // #ffffd7 clarity โ bright text
|
|
21
|
+
const ember = c256(173); // #d7875f glow โ warnings/energy
|
|
18
22
|
|
|
19
23
|
// Standard ANSI fallbacks (used where 24-bit might not render)
|
|
20
24
|
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -80,87 +84,22 @@ function spinner(text = 'thinking', style = 'exhale') {
|
|
|
80
84
|
};
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
// โโ The Exhale: signature brand
|
|
84
|
-
//
|
|
85
|
-
//
|
|
87
|
+
// โโ The Exhale: signature brand reveal โโโโโโโโโโโโโโโโโโโ
|
|
88
|
+
// Crisp over clever: plain prints with a breath of timing. No in-place
|
|
89
|
+
// rewrites, no cursor tricks, no dim-wrapped emoji โ those are exactly
|
|
90
|
+
// what render as grey boxes and artifacts across terminals. Every line
|
|
91
|
+
// lands once and stays put.
|
|
86
92
|
async function brandReveal(fast = false) {
|
|
87
|
-
|
|
88
|
-
console.log('');
|
|
89
|
-
console.log(` ${d('๐ฎ\u200d๐จ')} ${d('๐คซ')}`);
|
|
90
|
-
console.log('');
|
|
91
|
-
console.log(` ${b(cream('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
92
|
-
console.log(` ${b(cream('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
93
|
-
console.log('');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
process.stdout.write(hide);
|
|
98
|
-
|
|
99
|
-
// Phase 1: The inhale โ brief stillness
|
|
100
|
-
console.log('');
|
|
101
|
-
await sleep(200);
|
|
102
|
-
|
|
103
|
-
// Phase 2: The exhale โ particles drift outward
|
|
104
|
-
const exhaleStages = [
|
|
105
|
-
' ยท',
|
|
106
|
-
' ยท ยท ยท',
|
|
107
|
-
' ยท ยท ยท ยท',
|
|
108
|
-
' ยท ยท ยท ยท ยท',
|
|
109
|
-
' ยท ยท ยท ยท ยท',
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
for (const stage of exhaleStages) {
|
|
113
|
-
process.stdout.write(`${clearLine} ${slate(stage)}`);
|
|
114
|
-
await sleep(70);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Phase 3: Particles converge into the emoji
|
|
118
|
-
await sleep(100);
|
|
119
|
-
process.stdout.write(`${clearLine}`);
|
|
120
|
-
console.log(` ${d('๐ฎ\u200d๐จ')} ${d('๐คซ')}`);
|
|
93
|
+
const pause = (ms) => fast ? Promise.resolve() : sleep(ms);
|
|
121
94
|
console.log('');
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Phase 4: Logo wave โ each letter block appears left to right
|
|
125
|
-
const logoTop = ['โโโ', 'โโโ', 'โโโ', 'โโโ', 'โโ', 'โโโ'];
|
|
126
|
-
const logoBot = ['โโโ', 'โโโ', 'โโโ', 'โโโ', 'โโ', 'โโโ'];
|
|
127
|
-
|
|
128
|
-
let topLine = ' ';
|
|
129
|
-
let botLine = ' ';
|
|
130
|
-
|
|
131
|
-
for (let i = 0; i < logoTop.length; i++) {
|
|
132
|
-
topLine += cream(logoTop[i]) + ' ';
|
|
133
|
-
botLine += cream(logoBot[i]) + ' ';
|
|
134
|
-
|
|
135
|
-
// Overwrite both lines
|
|
136
|
-
if (i === 0) {
|
|
137
|
-
process.stdout.write(` ${b(topLine.trim())}`);
|
|
138
|
-
process.stdout.write('\n');
|
|
139
|
-
process.stdout.write(` ${b(botLine.trim())}`);
|
|
140
|
-
} else {
|
|
141
|
-
process.stdout.write(up(1));
|
|
142
|
-
process.stdout.write(`${clearLine} ${b(topLine.trim())}`);
|
|
143
|
-
process.stdout.write('\n');
|
|
144
|
-
process.stdout.write(`${clearLine} ${b(botLine.trim())}`);
|
|
145
|
-
}
|
|
146
|
-
await sleep(55);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
process.stdout.write('\n');
|
|
150
|
-
await sleep(100);
|
|
151
|
-
|
|
152
|
-
// Phase 5: Tagline fades in โ dim โ sage โ cream
|
|
153
|
-
const tagline = '.intent/ is your project\'s working memory.';
|
|
154
|
-
process.stdout.write(` ${slate(tagline)}`);
|
|
155
|
-
await sleep(200);
|
|
156
|
-
process.stdout.write(`${clearLine} ${sage(tagline)}`);
|
|
157
|
-
await sleep(200);
|
|
158
|
-
process.stdout.write(`${clearLine} ${teal(tagline)}`);
|
|
159
|
-
await sleep(300);
|
|
160
|
-
|
|
95
|
+
console.log(` ๐ฎโ๐จ ๐คซ`);
|
|
161
96
|
console.log('');
|
|
97
|
+
await pause(160);
|
|
98
|
+
console.log(` ${b(cream('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
99
|
+
console.log(` ${b(cream('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
100
|
+
await pause(160);
|
|
101
|
+
console.log(` ${sage(".intent/ is your project's working memory.")}`);
|
|
162
102
|
console.log('');
|
|
163
|
-
process.stdout.write(show);
|
|
164
103
|
}
|
|
165
104
|
|
|
166
105
|
// โโ Status panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|