bertrand 0.18.0 → 0.19.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/dist/bertrand.js +1253 -343
- package/dist/dashboard/assets/{index-e53khqNa.js → index-ofrZyO9k.js} +102 -102
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/sw.js +1 -1
- package/dist/migrations/0003_categories_rename.sql +8 -0
- package/dist/migrations/meta/0003_snapshot.json +777 -0
- package/dist/migrations/meta/_journal.json +7 -0
- package/dist/run-screen.js +384 -48
- package/package.json +1 -1
package/dist/run-screen.js
CHANGED
|
@@ -15,7 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// src/tui/run-screen.tsx
|
|
18
|
-
import { writeFileSync } from "fs";
|
|
18
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
19
19
|
import { render } from "@orchetron/storm";
|
|
20
20
|
|
|
21
21
|
// src/tui/screens/launch/index.tsx
|
|
@@ -566,21 +566,155 @@ import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
|
|
|
566
566
|
// src/db/client.ts
|
|
567
567
|
import { Database } from "bun:sqlite";
|
|
568
568
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
569
|
-
import {
|
|
569
|
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
570
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
570
571
|
import { dirname } from "path";
|
|
571
572
|
|
|
573
|
+
// src/lib/projects/registry.ts
|
|
574
|
+
import {
|
|
575
|
+
existsSync,
|
|
576
|
+
mkdirSync,
|
|
577
|
+
readFileSync,
|
|
578
|
+
readdirSync,
|
|
579
|
+
renameSync,
|
|
580
|
+
statSync,
|
|
581
|
+
writeFileSync
|
|
582
|
+
} from "fs";
|
|
583
|
+
import { join as join2 } from "path";
|
|
584
|
+
|
|
572
585
|
// src/lib/paths.ts
|
|
573
586
|
import { homedir } from "os";
|
|
574
587
|
import { join } from "path";
|
|
575
588
|
var BERTRAND_DIR = ".bertrand";
|
|
576
589
|
var paths = {
|
|
577
590
|
root: join(homedir(), BERTRAND_DIR),
|
|
578
|
-
db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
|
|
579
591
|
hooks: join(homedir(), BERTRAND_DIR, "hooks"),
|
|
580
592
|
sessions: join(homedir(), BERTRAND_DIR, "sessions"),
|
|
593
|
+
db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
|
|
581
594
|
syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
|
|
582
595
|
};
|
|
583
596
|
|
|
597
|
+
// src/lib/projects/registry.ts
|
|
598
|
+
var DEFAULT_PROJECT_SLUG = "default";
|
|
599
|
+
var _registryDir = paths.root;
|
|
600
|
+
function _getRegistryDir() {
|
|
601
|
+
return _registryDir;
|
|
602
|
+
}
|
|
603
|
+
function registryPath() {
|
|
604
|
+
return join2(_registryDir, "projects.json");
|
|
605
|
+
}
|
|
606
|
+
function projectsDir() {
|
|
607
|
+
return join2(_registryDir, "projects");
|
|
608
|
+
}
|
|
609
|
+
function readRegistry() {
|
|
610
|
+
const path = registryPath();
|
|
611
|
+
if (!existsSync(path))
|
|
612
|
+
return null;
|
|
613
|
+
try {
|
|
614
|
+
const raw = readFileSync(path, "utf8");
|
|
615
|
+
const parsed = JSON.parse(raw);
|
|
616
|
+
if (!isRegistry(parsed))
|
|
617
|
+
return null;
|
|
618
|
+
return parsed;
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function recoverFromDisk() {
|
|
624
|
+
const dir = projectsDir();
|
|
625
|
+
if (!existsSync(dir))
|
|
626
|
+
return null;
|
|
627
|
+
let entries;
|
|
628
|
+
try {
|
|
629
|
+
entries = readdirSync(dir);
|
|
630
|
+
} catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const projects = [];
|
|
634
|
+
for (const slug of entries) {
|
|
635
|
+
const subdir = join2(dir, slug);
|
|
636
|
+
let mtime;
|
|
637
|
+
try {
|
|
638
|
+
const s = statSync(subdir);
|
|
639
|
+
if (!s.isDirectory())
|
|
640
|
+
continue;
|
|
641
|
+
mtime = new Date(s.mtimeMs);
|
|
642
|
+
} catch {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (!existsSync(join2(subdir, "bertrand.db")))
|
|
646
|
+
continue;
|
|
647
|
+
projects.push({
|
|
648
|
+
slug,
|
|
649
|
+
name: slug,
|
|
650
|
+
createdAt: mtime.toISOString(),
|
|
651
|
+
lastUsedAt: mtime.toISOString()
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (projects.length === 0)
|
|
655
|
+
return null;
|
|
656
|
+
projects.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
657
|
+
const envSlug = process.env.BERTRAND_PROJECT;
|
|
658
|
+
const hasEnv = !!envSlug && projects.some((p) => p.slug === envSlug);
|
|
659
|
+
const hasDefault = projects.some((p) => p.slug === DEFAULT_PROJECT_SLUG);
|
|
660
|
+
const activeProjectSlug = hasEnv ? envSlug : hasDefault ? DEFAULT_PROJECT_SLUG : projects[0].slug;
|
|
661
|
+
return { activeProjectSlug, projects };
|
|
662
|
+
}
|
|
663
|
+
function loadRegistry() {
|
|
664
|
+
return readRegistry() ?? recoverFromDisk();
|
|
665
|
+
}
|
|
666
|
+
function listProjects() {
|
|
667
|
+
return loadRegistry()?.projects ?? [];
|
|
668
|
+
}
|
|
669
|
+
function getActiveProjectSlug() {
|
|
670
|
+
return loadRegistry()?.activeProjectSlug ?? DEFAULT_PROJECT_SLUG;
|
|
671
|
+
}
|
|
672
|
+
function isRegistry(value) {
|
|
673
|
+
if (typeof value !== "object" || value === null)
|
|
674
|
+
return false;
|
|
675
|
+
const r = value;
|
|
676
|
+
if (typeof r.activeProjectSlug !== "string")
|
|
677
|
+
return false;
|
|
678
|
+
if (!Array.isArray(r.projects))
|
|
679
|
+
return false;
|
|
680
|
+
return r.projects.every((p) => {
|
|
681
|
+
if (typeof p !== "object" || p === null)
|
|
682
|
+
return false;
|
|
683
|
+
const e = p;
|
|
684
|
+
return typeof e.slug === "string" && typeof e.name === "string" && typeof e.createdAt === "string" && typeof e.lastUsedAt === "string";
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/lib/projects/paths.ts
|
|
689
|
+
import { join as join3 } from "path";
|
|
690
|
+
function projectPaths(slug) {
|
|
691
|
+
const root = join3(_getRegistryDir(), "projects", slug);
|
|
692
|
+
return {
|
|
693
|
+
root,
|
|
694
|
+
db: join3(root, "bertrand.db"),
|
|
695
|
+
syncEnv: join3(root, "sync.env"),
|
|
696
|
+
snapshots: join3(root, "snapshots")
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/lib/projects/resolve.ts
|
|
701
|
+
var _cached = null;
|
|
702
|
+
function resolveActiveProject() {
|
|
703
|
+
if (_cached)
|
|
704
|
+
return _cached;
|
|
705
|
+
const envSlug = process.env.BERTRAND_PROJECT;
|
|
706
|
+
let slug;
|
|
707
|
+
if (envSlug && envSlug.trim()) {
|
|
708
|
+
slug = envSlug.trim();
|
|
709
|
+
} else {
|
|
710
|
+
slug = getActiveProjectSlug();
|
|
711
|
+
}
|
|
712
|
+
const entry = listProjects().find((p) => p.slug === slug);
|
|
713
|
+
const name = entry?.name ?? slug;
|
|
714
|
+
_cached = { slug, name, ...projectPaths(slug) };
|
|
715
|
+
return _cached;
|
|
716
|
+
}
|
|
717
|
+
|
|
584
718
|
// src/db/schema.ts
|
|
585
719
|
var exports_schema = {};
|
|
586
720
|
__export(exports_schema, {
|
|
@@ -589,15 +723,15 @@ __export(exports_schema, {
|
|
|
589
723
|
sessionStats: () => sessionStats,
|
|
590
724
|
sessionLabels: () => sessionLabels,
|
|
591
725
|
labels: () => labels,
|
|
592
|
-
groups: () => groups,
|
|
593
726
|
events: () => events,
|
|
594
|
-
conversations: () => conversations
|
|
727
|
+
conversations: () => conversations,
|
|
728
|
+
categories: () => categories
|
|
595
729
|
});
|
|
596
730
|
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
597
731
|
import { sql } from "drizzle-orm";
|
|
598
|
-
var
|
|
732
|
+
var categories = sqliteTable("categories", {
|
|
599
733
|
id: text("id").primaryKey(),
|
|
600
|
-
parentId: text("parent_id").references(() =>
|
|
734
|
+
parentId: text("parent_id").references(() => categories.id, {
|
|
601
735
|
onDelete: "cascade"
|
|
602
736
|
}),
|
|
603
737
|
slug: text("slug").notNull(),
|
|
@@ -607,8 +741,8 @@ var groups = sqliteTable("groups", {
|
|
|
607
741
|
color: text("color"),
|
|
608
742
|
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
609
743
|
}, (t) => [
|
|
610
|
-
uniqueIndex("
|
|
611
|
-
index("
|
|
744
|
+
uniqueIndex("categories_parent_slug").on(t.parentId, t.slug),
|
|
745
|
+
index("categories_path").on(t.path)
|
|
612
746
|
]);
|
|
613
747
|
var labels = sqliteTable("labels", {
|
|
614
748
|
id: text("id").primaryKey(),
|
|
@@ -618,7 +752,7 @@ var labels = sqliteTable("labels", {
|
|
|
618
752
|
});
|
|
619
753
|
var sessions = sqliteTable("sessions", {
|
|
620
754
|
id: text("id").primaryKey(),
|
|
621
|
-
|
|
755
|
+
categoryId: text("category_id").notNull().references(() => categories.id, { onDelete: "cascade" }),
|
|
622
756
|
slug: text("slug").notNull(),
|
|
623
757
|
name: text("name").notNull(),
|
|
624
758
|
status: text("status", {
|
|
@@ -631,7 +765,7 @@ var sessions = sqliteTable("sessions", {
|
|
|
631
765
|
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
632
766
|
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
633
767
|
}, (t) => [
|
|
634
|
-
uniqueIndex("
|
|
768
|
+
uniqueIndex("sessions_category_slug").on(t.categoryId, t.slug),
|
|
635
769
|
index("sessions_status").on(t.status),
|
|
636
770
|
index("sessions_started").on(t.startedAt)
|
|
637
771
|
]);
|
|
@@ -694,19 +828,43 @@ var sessionStats = sqliteTable("session_stats", {
|
|
|
694
828
|
});
|
|
695
829
|
|
|
696
830
|
// src/db/client.ts
|
|
697
|
-
var
|
|
831
|
+
var MIGRATIONS_FOLDER = import.meta.dir + "/migrations";
|
|
832
|
+
var _cache = new Map;
|
|
833
|
+
var _migrated = new Set;
|
|
834
|
+
var _testDb = null;
|
|
698
835
|
function getDb() {
|
|
699
|
-
if (
|
|
700
|
-
return
|
|
701
|
-
|
|
702
|
-
|
|
836
|
+
if (_testDb)
|
|
837
|
+
return _testDb;
|
|
838
|
+
return openDb(resolveActiveProject().db);
|
|
839
|
+
}
|
|
840
|
+
function getDbForProject(slug) {
|
|
841
|
+
if (_testDb)
|
|
842
|
+
return _testDb;
|
|
843
|
+
return openDb(projectPaths(slug).db);
|
|
844
|
+
}
|
|
845
|
+
function openDb(dbPath) {
|
|
846
|
+
const cached = _cache.get(dbPath);
|
|
847
|
+
if (cached)
|
|
848
|
+
return cached;
|
|
849
|
+
mkdirSync2(dirname(dbPath), { recursive: true });
|
|
850
|
+
const sqlite = new Database(dbPath);
|
|
703
851
|
sqlite.exec("PRAGMA journal_mode = WAL");
|
|
704
852
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
705
853
|
sqlite.exec("PRAGMA synchronous = NORMAL");
|
|
706
854
|
sqlite.exec("PRAGMA cache_size = -8000");
|
|
707
855
|
sqlite.exec("PRAGMA temp_store = MEMORY");
|
|
708
|
-
|
|
709
|
-
|
|
856
|
+
const db = drizzle(sqlite, { schema: exports_schema });
|
|
857
|
+
if (!_migrated.has(dbPath)) {
|
|
858
|
+
try {
|
|
859
|
+
migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
|
860
|
+
} catch (err) {
|
|
861
|
+
sqlite.close();
|
|
862
|
+
throw err;
|
|
863
|
+
}
|
|
864
|
+
_migrated.add(dbPath);
|
|
865
|
+
}
|
|
866
|
+
_cache.set(dbPath, db);
|
|
867
|
+
return db;
|
|
710
868
|
}
|
|
711
869
|
|
|
712
870
|
// src/lib/id.ts
|
|
@@ -718,7 +876,7 @@ function getSession(id) {
|
|
|
718
876
|
}
|
|
719
877
|
function getAllSessions(opts) {
|
|
720
878
|
const db = getDb();
|
|
721
|
-
const query = db.select({ session: sessions,
|
|
879
|
+
const query = db.select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories, eq(sessions.categoryId, categories.id));
|
|
722
880
|
if (opts?.excludeArchived) {
|
|
723
881
|
return query.where(inArray(sessions.status, [
|
|
724
882
|
"active",
|
|
@@ -799,7 +957,7 @@ function parseSessionName(input) {
|
|
|
799
957
|
}
|
|
800
958
|
const segments = trimmed.split("/").filter(Boolean);
|
|
801
959
|
if (segments.length < 2) {
|
|
802
|
-
throw new Error(`Session name must include at least one
|
|
960
|
+
throw new Error(`Session name must include at least one category: "category/session" (got "${trimmed}")`);
|
|
803
961
|
}
|
|
804
962
|
for (const segment of segments) {
|
|
805
963
|
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
|
|
@@ -807,8 +965,8 @@ function parseSessionName(input) {
|
|
|
807
965
|
}
|
|
808
966
|
}
|
|
809
967
|
const slug = segments[segments.length - 1];
|
|
810
|
-
const
|
|
811
|
-
return {
|
|
968
|
+
const categoryPath = segments.slice(0, -1).join("/");
|
|
969
|
+
return { categoryPath, slug };
|
|
812
970
|
}
|
|
813
971
|
|
|
814
972
|
// src/tui/screens/launch/index.tsx
|
|
@@ -835,8 +993,8 @@ function sessionRow(s) {
|
|
|
835
993
|
const disabled = status === "waiting";
|
|
836
994
|
const isArchived = status === "archived";
|
|
837
995
|
return {
|
|
838
|
-
value: `${s.
|
|
839
|
-
label: `${s.
|
|
996
|
+
value: `${s.categoryPath}/${s.session.slug}`,
|
|
997
|
+
label: `${s.categoryPath}/${s.session.slug} ${status}`,
|
|
840
998
|
meta: formatAgo(recencyKey(s)),
|
|
841
999
|
disabled,
|
|
842
1000
|
dim: isArchived,
|
|
@@ -882,10 +1040,10 @@ function sessionRow(s) {
|
|
|
882
1040
|
}
|
|
883
1041
|
};
|
|
884
1042
|
}
|
|
885
|
-
function
|
|
1043
|
+
function categoryHeader(categoryPath) {
|
|
886
1044
|
return {
|
|
887
|
-
value: `
|
|
888
|
-
label:
|
|
1045
|
+
value: `__category:${categoryPath}`,
|
|
1046
|
+
label: categoryPath,
|
|
889
1047
|
kind: "header"
|
|
890
1048
|
};
|
|
891
1049
|
}
|
|
@@ -905,7 +1063,7 @@ function Launch({ onSelect }) {
|
|
|
905
1063
|
return showArchived;
|
|
906
1064
|
return false;
|
|
907
1065
|
}).sort((a, b) => {
|
|
908
|
-
const g = a.
|
|
1066
|
+
const g = a.categoryPath.localeCompare(b.categoryPath);
|
|
909
1067
|
if (g !== 0)
|
|
910
1068
|
return g;
|
|
911
1069
|
const r = statusRank(a.session.status) - statusRank(b.session.status);
|
|
@@ -916,29 +1074,29 @@ function Launch({ onSelect }) {
|
|
|
916
1074
|
}, [allSessions, showArchived]);
|
|
917
1075
|
const items = useMemo2(() => {
|
|
918
1076
|
const rows = [];
|
|
919
|
-
let
|
|
1077
|
+
let lastCategory = null;
|
|
920
1078
|
for (const s of visibleSessions) {
|
|
921
|
-
if (s.
|
|
922
|
-
rows.push(
|
|
923
|
-
|
|
1079
|
+
if (s.categoryPath !== lastCategory) {
|
|
1080
|
+
rows.push(categoryHeader(s.categoryPath));
|
|
1081
|
+
lastCategory = s.categoryPath;
|
|
924
1082
|
}
|
|
925
1083
|
rows.push(sessionRow(s));
|
|
926
1084
|
}
|
|
927
1085
|
return rows;
|
|
928
1086
|
}, [visibleSessions]);
|
|
929
1087
|
const suggestions = useMemo2(() => {
|
|
930
|
-
const
|
|
1088
|
+
const categories2 = new Set;
|
|
931
1089
|
const names = [];
|
|
932
1090
|
for (const s of allSessions) {
|
|
933
|
-
|
|
934
|
-
names.push(`${s.
|
|
1091
|
+
categories2.add(`${s.categoryPath}/`);
|
|
1092
|
+
names.push(`${s.categoryPath}/${s.session.slug}`);
|
|
935
1093
|
}
|
|
936
|
-
return [...
|
|
1094
|
+
return [...categories2, ...names];
|
|
937
1095
|
}, [allSessions]);
|
|
938
1096
|
const sessionByValue = useMemo2(() => {
|
|
939
1097
|
const map = new Map;
|
|
940
1098
|
for (const s of allSessions) {
|
|
941
|
-
map.set(`${s.
|
|
1099
|
+
map.set(`${s.categoryPath}/${s.session.slug}`, s);
|
|
942
1100
|
}
|
|
943
1101
|
return map;
|
|
944
1102
|
}, [allSessions]);
|
|
@@ -982,8 +1140,8 @@ function Launch({ onSelect }) {
|
|
|
982
1140
|
return;
|
|
983
1141
|
}
|
|
984
1142
|
try {
|
|
985
|
-
const {
|
|
986
|
-
select({ type: "create",
|
|
1143
|
+
const { categoryPath, slug } = parseSessionName(value);
|
|
1144
|
+
select({ type: "create", categoryPath, slug });
|
|
987
1145
|
} catch (e) {
|
|
988
1146
|
setError(e instanceof Error ? e.message : "Invalid name");
|
|
989
1147
|
}
|
|
@@ -1031,7 +1189,7 @@ function Launch({ onSelect }) {
|
|
|
1031
1189
|
}, undefined, false, undefined, this),
|
|
1032
1190
|
visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV6(Text6, {
|
|
1033
1191
|
dim: true,
|
|
1034
|
-
children: "\xB7 none \u2014 type
|
|
1192
|
+
children: "\xB7 none \u2014 type category/slug to create"
|
|
1035
1193
|
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6(Fragment, {
|
|
1036
1194
|
children: [
|
|
1037
1195
|
/* @__PURE__ */ jsxDEV6(Text6, {
|
|
@@ -1087,8 +1245,8 @@ function Launch({ onSelect }) {
|
|
|
1087
1245
|
isFocused: true,
|
|
1088
1246
|
maxVisible: 24,
|
|
1089
1247
|
suggest: suggestions,
|
|
1090
|
-
placeholder: "Filter or type
|
|
1091
|
-
emptyHint: showArchived ? "No sessions. Type
|
|
1248
|
+
placeholder: "Filter or type category/slug to create\u2026",
|
|
1249
|
+
emptyHint: showArchived ? "No sessions. Type category/slug to create one." : "No paused sessions. Type category/slug to create one.",
|
|
1092
1250
|
onSubmit: handleSubmit,
|
|
1093
1251
|
onKey: (e, cursorItem) => {
|
|
1094
1252
|
if (e.key === "c" && e.ctrl) {
|
|
@@ -1113,7 +1271,7 @@ function Launch({ onSelect }) {
|
|
|
1113
1271
|
/* @__PURE__ */ jsxDEV6(Text6, {
|
|
1114
1272
|
dim: true,
|
|
1115
1273
|
children: [
|
|
1116
|
-
"\u2191\u2193 navigate \xB7 \u2190\u2192 skip
|
|
1274
|
+
"\u2191\u2193 navigate \xB7 \u2190\u2192 skip category \xB7 enter continue/create \xB7 ctrl+a",
|
|
1117
1275
|
" ",
|
|
1118
1276
|
showArchived ? "(un)archive" : "archive",
|
|
1119
1277
|
" \xB7 tab",
|
|
@@ -1390,8 +1548,174 @@ function Resume({ sessionId, onSelect }) {
|
|
|
1390
1548
|
}, undefined, true, undefined, this);
|
|
1391
1549
|
}
|
|
1392
1550
|
|
|
1551
|
+
// src/tui/screens/project-picker/index.tsx
|
|
1552
|
+
import { useMemo as useMemo3 } from "react";
|
|
1553
|
+
import { Box as Box8, Text as Text9, useTui as useTui4 } from "@orchetron/storm";
|
|
1554
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1555
|
+
import { jsxDEV as jsxDEV9, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
1556
|
+
var SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
1557
|
+
function statsFor(slug) {
|
|
1558
|
+
const dbFile = projectPaths(slug).db;
|
|
1559
|
+
if (!existsSync2(dbFile))
|
|
1560
|
+
return { total: 0, active: 0, unreadable: false };
|
|
1561
|
+
try {
|
|
1562
|
+
const db = getDbForProject(slug);
|
|
1563
|
+
const all = db.select({ status: sessions.status }).from(sessions).all();
|
|
1564
|
+
return {
|
|
1565
|
+
total: all.length,
|
|
1566
|
+
active: all.filter((s) => s.status === "active" || s.status === "waiting").length,
|
|
1567
|
+
unreadable: false
|
|
1568
|
+
};
|
|
1569
|
+
} catch {
|
|
1570
|
+
return { total: 0, active: 0, unreadable: true };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function projectRow(entry, isActive) {
|
|
1574
|
+
const stats = statsFor(entry.slug);
|
|
1575
|
+
const sessionsLabel = stats.unreadable ? "?" : `${stats.active}/${stats.total}`;
|
|
1576
|
+
const lastUsed = formatAgo(entry.lastUsedAt);
|
|
1577
|
+
return {
|
|
1578
|
+
value: entry.slug,
|
|
1579
|
+
label: `${entry.slug} ${entry.name} ${sessionsLabel}`,
|
|
1580
|
+
meta: lastUsed,
|
|
1581
|
+
display: (isCursor) => {
|
|
1582
|
+
const cursorColor = "green";
|
|
1583
|
+
const slugColor = isCursor ? cursorColor : undefined;
|
|
1584
|
+
const nameColor = isCursor ? cursorColor : undefined;
|
|
1585
|
+
return /* @__PURE__ */ jsxDEV9(Fragment2, {
|
|
1586
|
+
children: [
|
|
1587
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1588
|
+
color: cursorColor,
|
|
1589
|
+
bold: true,
|
|
1590
|
+
children: isCursor ? "\u276F " : " "
|
|
1591
|
+
}, undefined, false, undefined, this),
|
|
1592
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1593
|
+
bold: isActive,
|
|
1594
|
+
color: isActive ? "gold" : slugColor,
|
|
1595
|
+
children: isActive ? "\u25CF " : " "
|
|
1596
|
+
}, undefined, false, undefined, this),
|
|
1597
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1598
|
+
color: slugColor,
|
|
1599
|
+
bold: isCursor || isActive,
|
|
1600
|
+
children: entry.slug.padEnd(16)
|
|
1601
|
+
}, undefined, false, undefined, this),
|
|
1602
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1603
|
+
color: nameColor,
|
|
1604
|
+
dim: !isCursor,
|
|
1605
|
+
children: entry.name.padEnd(20)
|
|
1606
|
+
}, undefined, false, undefined, this),
|
|
1607
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1608
|
+
dim: true,
|
|
1609
|
+
children: stats.unreadable ? " (unreadable)" : ` ${sessionsLabel} active/total`
|
|
1610
|
+
}, undefined, false, undefined, this)
|
|
1611
|
+
]
|
|
1612
|
+
}, undefined, true, undefined, this);
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
function ProjectPicker({ onSelect }) {
|
|
1617
|
+
const { exit } = useTui4();
|
|
1618
|
+
const allProjects = useMemo3(() => listProjects(), []);
|
|
1619
|
+
const activeSlug = useMemo3(() => getActiveProjectSlug(), []);
|
|
1620
|
+
const items = useMemo3(() => {
|
|
1621
|
+
return allProjects.slice().sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt)).map((p) => projectRow(p, p.slug === activeSlug));
|
|
1622
|
+
}, [allProjects, activeSlug]);
|
|
1623
|
+
const suggestions = useMemo3(() => allProjects.map((p) => p.slug), [allProjects]);
|
|
1624
|
+
const knownSlugs = useMemo3(() => new Set(allProjects.map((p) => p.slug)), [allProjects]);
|
|
1625
|
+
const select = (selection) => {
|
|
1626
|
+
onSelect(selection);
|
|
1627
|
+
exit();
|
|
1628
|
+
};
|
|
1629
|
+
const handleSubmit = (value) => {
|
|
1630
|
+
const trimmed = value.trim();
|
|
1631
|
+
if (!trimmed)
|
|
1632
|
+
return;
|
|
1633
|
+
if (knownSlugs.has(trimmed)) {
|
|
1634
|
+
select({ type: "select", slug: trimmed });
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (!SLUG_PATTERN.test(trimmed)) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
select({ type: "create", slug: trimmed });
|
|
1641
|
+
};
|
|
1642
|
+
return /* @__PURE__ */ jsxDEV9(Box8, {
|
|
1643
|
+
flexDirection: "column",
|
|
1644
|
+
paddingY: 1,
|
|
1645
|
+
gap: 1,
|
|
1646
|
+
children: [
|
|
1647
|
+
/* @__PURE__ */ jsxDEV9(Box8, {
|
|
1648
|
+
marginX: 1,
|
|
1649
|
+
children: /* @__PURE__ */ jsxDEV9(Logo, {}, undefined, false, undefined, this)
|
|
1650
|
+
}, undefined, false, undefined, this),
|
|
1651
|
+
/* @__PURE__ */ jsxDEV9(Box8, {
|
|
1652
|
+
flexDirection: "column",
|
|
1653
|
+
marginX: 2,
|
|
1654
|
+
gap: 1,
|
|
1655
|
+
children: [
|
|
1656
|
+
/* @__PURE__ */ jsxDEV9(AppDetails, {}, undefined, false, undefined, this),
|
|
1657
|
+
/* @__PURE__ */ jsxDEV9(Box8, {
|
|
1658
|
+
flexDirection: "column",
|
|
1659
|
+
gap: 1,
|
|
1660
|
+
children: [
|
|
1661
|
+
/* @__PURE__ */ jsxDEV9(Box8, {
|
|
1662
|
+
flexDirection: "row",
|
|
1663
|
+
gap: 1,
|
|
1664
|
+
children: [
|
|
1665
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1666
|
+
bold: true,
|
|
1667
|
+
children: "Projects"
|
|
1668
|
+
}, undefined, false, undefined, this),
|
|
1669
|
+
allProjects.length > 0 ? /* @__PURE__ */ jsxDEV9(Fragment2, {
|
|
1670
|
+
children: [
|
|
1671
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1672
|
+
dim: true,
|
|
1673
|
+
children: "\xB7"
|
|
1674
|
+
}, undefined, false, undefined, this),
|
|
1675
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1676
|
+
dim: true,
|
|
1677
|
+
children: [
|
|
1678
|
+
allProjects.length,
|
|
1679
|
+
" total",
|
|
1680
|
+
activeSlug ? ` \xB7 active: ${activeSlug}` : ""
|
|
1681
|
+
]
|
|
1682
|
+
}, undefined, true, undefined, this)
|
|
1683
|
+
]
|
|
1684
|
+
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV9(Text9, {
|
|
1685
|
+
dim: true,
|
|
1686
|
+
children: "\xB7 none \u2014 type a slug to create your first project"
|
|
1687
|
+
}, undefined, false, undefined, this)
|
|
1688
|
+
]
|
|
1689
|
+
}, undefined, true, undefined, this),
|
|
1690
|
+
/* @__PURE__ */ jsxDEV9(Picker, {
|
|
1691
|
+
mode: "single",
|
|
1692
|
+
items,
|
|
1693
|
+
isFocused: true,
|
|
1694
|
+
maxVisible: 16,
|
|
1695
|
+
suggest: suggestions,
|
|
1696
|
+
placeholder: "Filter or type a new slug to create\u2026",
|
|
1697
|
+
emptyHint: "No projects. Type a slug to create one.",
|
|
1698
|
+
onSubmit: handleSubmit,
|
|
1699
|
+
onKey: (e) => {
|
|
1700
|
+
if (e.key === "c" && e.ctrl) {
|
|
1701
|
+
select({ type: "quit" });
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}, undefined, false, undefined, this),
|
|
1705
|
+
/* @__PURE__ */ jsxDEV9(Text9, {
|
|
1706
|
+
dim: true,
|
|
1707
|
+
children: "\u2191\u2193 navigate \xB7 enter select \xB7 type to filter or create new \xB7 ctrl+c quit"
|
|
1708
|
+
}, undefined, false, undefined, this)
|
|
1709
|
+
]
|
|
1710
|
+
}, undefined, true, undefined, this)
|
|
1711
|
+
]
|
|
1712
|
+
}, undefined, true, undefined, this)
|
|
1713
|
+
]
|
|
1714
|
+
}, undefined, true, undefined, this);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1393
1717
|
// src/tui/run-screen.tsx
|
|
1394
|
-
import { jsxDEV as
|
|
1718
|
+
import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
|
|
1395
1719
|
var [, , screen, outputPath, ...args] = process.argv;
|
|
1396
1720
|
if (!screen || !outputPath) {
|
|
1397
1721
|
console.error("Usage: run-screen <screen> <outputPath> [args...]");
|
|
@@ -1401,7 +1725,19 @@ var result;
|
|
|
1401
1725
|
switch (screen) {
|
|
1402
1726
|
case "launch": {
|
|
1403
1727
|
let selection = { type: "quit" };
|
|
1404
|
-
const app = render(/* @__PURE__ */
|
|
1728
|
+
const app = render(/* @__PURE__ */ jsxDEV10(Launch, {
|
|
1729
|
+
onSelect: (s) => {
|
|
1730
|
+
selection = s;
|
|
1731
|
+
}
|
|
1732
|
+
}, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
|
|
1733
|
+
await app.waitUntilExit();
|
|
1734
|
+
app.unmount();
|
|
1735
|
+
result = selection;
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
case "project-picker": {
|
|
1739
|
+
let selection = { type: "quit" };
|
|
1740
|
+
const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
|
|
1405
1741
|
onSelect: (s) => {
|
|
1406
1742
|
selection = s;
|
|
1407
1743
|
}
|
|
@@ -1418,7 +1754,7 @@ switch (screen) {
|
|
|
1418
1754
|
process.exit(1);
|
|
1419
1755
|
}
|
|
1420
1756
|
let action = "save";
|
|
1421
|
-
const app = render(/* @__PURE__ */
|
|
1757
|
+
const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
|
|
1422
1758
|
sessionId,
|
|
1423
1759
|
onAction: (a) => {
|
|
1424
1760
|
action = a;
|
|
@@ -1436,7 +1772,7 @@ switch (screen) {
|
|
|
1436
1772
|
process.exit(1);
|
|
1437
1773
|
}
|
|
1438
1774
|
let selection = { type: "back" };
|
|
1439
|
-
const app = render(/* @__PURE__ */
|
|
1775
|
+
const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
|
|
1440
1776
|
sessionId,
|
|
1441
1777
|
onSelect: (s) => {
|
|
1442
1778
|
selection = s;
|
|
@@ -1451,5 +1787,5 @@ switch (screen) {
|
|
|
1451
1787
|
console.error(`Unknown screen: ${screen}`);
|
|
1452
1788
|
process.exit(1);
|
|
1453
1789
|
}
|
|
1454
|
-
|
|
1790
|
+
writeFileSync2(outputPath, JSON.stringify(result));
|
|
1455
1791
|
process.exit(0);
|