bertrand 0.17.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.
@@ -22,6 +22,13 @@
22
22
  "when": 1777762531910,
23
23
  "tag": "0002_omniscient_speedball",
24
24
  "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1781474706585,
30
+ "tag": "0003_categories_rename",
31
+ "breakpoints": true
25
32
  }
26
33
  ]
27
34
  }
@@ -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,20 +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 { mkdirSync } from "fs";
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
- sessions: join(homedir(), BERTRAND_DIR, "sessions")
592
+ sessions: join(homedir(), BERTRAND_DIR, "sessions"),
593
+ db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
594
+ syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
581
595
  };
582
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
+
583
718
  // src/db/schema.ts
584
719
  var exports_schema = {};
585
720
  __export(exports_schema, {
@@ -588,15 +723,15 @@ __export(exports_schema, {
588
723
  sessionStats: () => sessionStats,
589
724
  sessionLabels: () => sessionLabels,
590
725
  labels: () => labels,
591
- groups: () => groups,
592
726
  events: () => events,
593
- conversations: () => conversations
727
+ conversations: () => conversations,
728
+ categories: () => categories
594
729
  });
595
730
  import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
596
731
  import { sql } from "drizzle-orm";
597
- var groups = sqliteTable("groups", {
732
+ var categories = sqliteTable("categories", {
598
733
  id: text("id").primaryKey(),
599
- parentId: text("parent_id").references(() => groups.id, {
734
+ parentId: text("parent_id").references(() => categories.id, {
600
735
  onDelete: "cascade"
601
736
  }),
602
737
  slug: text("slug").notNull(),
@@ -606,8 +741,8 @@ var groups = sqliteTable("groups", {
606
741
  color: text("color"),
607
742
  createdAt: text("created_at").notNull().default(sql`(datetime('now'))`)
608
743
  }, (t) => [
609
- uniqueIndex("groups_parent_slug").on(t.parentId, t.slug),
610
- index("groups_path").on(t.path)
744
+ uniqueIndex("categories_parent_slug").on(t.parentId, t.slug),
745
+ index("categories_path").on(t.path)
611
746
  ]);
612
747
  var labels = sqliteTable("labels", {
613
748
  id: text("id").primaryKey(),
@@ -617,7 +752,7 @@ var labels = sqliteTable("labels", {
617
752
  });
618
753
  var sessions = sqliteTable("sessions", {
619
754
  id: text("id").primaryKey(),
620
- groupId: text("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }),
755
+ categoryId: text("category_id").notNull().references(() => categories.id, { onDelete: "cascade" }),
621
756
  slug: text("slug").notNull(),
622
757
  name: text("name").notNull(),
623
758
  status: text("status", {
@@ -630,7 +765,7 @@ var sessions = sqliteTable("sessions", {
630
765
  createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
631
766
  updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`)
632
767
  }, (t) => [
633
- uniqueIndex("sessions_group_slug").on(t.groupId, t.slug),
768
+ uniqueIndex("sessions_category_slug").on(t.categoryId, t.slug),
634
769
  index("sessions_status").on(t.status),
635
770
  index("sessions_started").on(t.startedAt)
636
771
  ]);
@@ -693,19 +828,43 @@ var sessionStats = sqliteTable("session_stats", {
693
828
  });
694
829
 
695
830
  // src/db/client.ts
696
- var _db = null;
831
+ var MIGRATIONS_FOLDER = import.meta.dir + "/migrations";
832
+ var _cache = new Map;
833
+ var _migrated = new Set;
834
+ var _testDb = null;
697
835
  function getDb() {
698
- if (_db)
699
- return _db;
700
- mkdirSync(dirname(paths.db), { recursive: true });
701
- const sqlite = new Database(paths.db);
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);
702
851
  sqlite.exec("PRAGMA journal_mode = WAL");
703
852
  sqlite.exec("PRAGMA foreign_keys = ON");
704
853
  sqlite.exec("PRAGMA synchronous = NORMAL");
705
854
  sqlite.exec("PRAGMA cache_size = -8000");
706
855
  sqlite.exec("PRAGMA temp_store = MEMORY");
707
- _db = drizzle(sqlite, { schema: exports_schema });
708
- return _db;
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;
709
868
  }
710
869
 
711
870
  // src/lib/id.ts
@@ -717,7 +876,7 @@ function getSession(id) {
717
876
  }
718
877
  function getAllSessions(opts) {
719
878
  const db = getDb();
720
- const query = db.select({ session: sessions, groupPath: groups.path }).from(sessions).innerJoin(groups, eq(sessions.groupId, groups.id));
879
+ const query = db.select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories, eq(sessions.categoryId, categories.id));
721
880
  if (opts?.excludeArchived) {
722
881
  return query.where(inArray(sessions.status, [
723
882
  "active",
@@ -798,7 +957,7 @@ function parseSessionName(input) {
798
957
  }
799
958
  const segments = trimmed.split("/").filter(Boolean);
800
959
  if (segments.length < 2) {
801
- throw new Error(`Session name must include at least one group: "group/session" (got "${trimmed}")`);
960
+ throw new Error(`Session name must include at least one category: "category/session" (got "${trimmed}")`);
802
961
  }
803
962
  for (const segment of segments) {
804
963
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
@@ -806,8 +965,8 @@ function parseSessionName(input) {
806
965
  }
807
966
  }
808
967
  const slug = segments[segments.length - 1];
809
- const groupPath = segments.slice(0, -1).join("/");
810
- return { groupPath, slug };
968
+ const categoryPath = segments.slice(0, -1).join("/");
969
+ return { categoryPath, slug };
811
970
  }
812
971
 
813
972
  // src/tui/screens/launch/index.tsx
@@ -834,8 +993,8 @@ function sessionRow(s) {
834
993
  const disabled = status === "waiting";
835
994
  const isArchived = status === "archived";
836
995
  return {
837
- value: `${s.groupPath}/${s.session.slug}`,
838
- label: `${s.groupPath}/${s.session.slug} ${status}`,
996
+ value: `${s.categoryPath}/${s.session.slug}`,
997
+ label: `${s.categoryPath}/${s.session.slug} ${status}`,
839
998
  meta: formatAgo(recencyKey(s)),
840
999
  disabled,
841
1000
  dim: isArchived,
@@ -881,10 +1040,10 @@ function sessionRow(s) {
881
1040
  }
882
1041
  };
883
1042
  }
884
- function groupHeader(groupPath) {
1043
+ function categoryHeader(categoryPath) {
885
1044
  return {
886
- value: `__group:${groupPath}`,
887
- label: groupPath,
1045
+ value: `__category:${categoryPath}`,
1046
+ label: categoryPath,
888
1047
  kind: "header"
889
1048
  };
890
1049
  }
@@ -904,7 +1063,7 @@ function Launch({ onSelect }) {
904
1063
  return showArchived;
905
1064
  return false;
906
1065
  }).sort((a, b) => {
907
- const g = a.groupPath.localeCompare(b.groupPath);
1066
+ const g = a.categoryPath.localeCompare(b.categoryPath);
908
1067
  if (g !== 0)
909
1068
  return g;
910
1069
  const r = statusRank(a.session.status) - statusRank(b.session.status);
@@ -915,29 +1074,29 @@ function Launch({ onSelect }) {
915
1074
  }, [allSessions, showArchived]);
916
1075
  const items = useMemo2(() => {
917
1076
  const rows = [];
918
- let lastGroup = null;
1077
+ let lastCategory = null;
919
1078
  for (const s of visibleSessions) {
920
- if (s.groupPath !== lastGroup) {
921
- rows.push(groupHeader(s.groupPath));
922
- lastGroup = s.groupPath;
1079
+ if (s.categoryPath !== lastCategory) {
1080
+ rows.push(categoryHeader(s.categoryPath));
1081
+ lastCategory = s.categoryPath;
923
1082
  }
924
1083
  rows.push(sessionRow(s));
925
1084
  }
926
1085
  return rows;
927
1086
  }, [visibleSessions]);
928
1087
  const suggestions = useMemo2(() => {
929
- const groups2 = new Set;
1088
+ const categories2 = new Set;
930
1089
  const names = [];
931
1090
  for (const s of allSessions) {
932
- groups2.add(`${s.groupPath}/`);
933
- names.push(`${s.groupPath}/${s.session.slug}`);
1091
+ categories2.add(`${s.categoryPath}/`);
1092
+ names.push(`${s.categoryPath}/${s.session.slug}`);
934
1093
  }
935
- return [...groups2, ...names];
1094
+ return [...categories2, ...names];
936
1095
  }, [allSessions]);
937
1096
  const sessionByValue = useMemo2(() => {
938
1097
  const map = new Map;
939
1098
  for (const s of allSessions) {
940
- map.set(`${s.groupPath}/${s.session.slug}`, s);
1099
+ map.set(`${s.categoryPath}/${s.session.slug}`, s);
941
1100
  }
942
1101
  return map;
943
1102
  }, [allSessions]);
@@ -981,8 +1140,8 @@ function Launch({ onSelect }) {
981
1140
  return;
982
1141
  }
983
1142
  try {
984
- const { groupPath, slug } = parseSessionName(value);
985
- select({ type: "create", groupPath, slug });
1143
+ const { categoryPath, slug } = parseSessionName(value);
1144
+ select({ type: "create", categoryPath, slug });
986
1145
  } catch (e) {
987
1146
  setError(e instanceof Error ? e.message : "Invalid name");
988
1147
  }
@@ -1030,7 +1189,7 @@ function Launch({ onSelect }) {
1030
1189
  }, undefined, false, undefined, this),
1031
1190
  visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV6(Text6, {
1032
1191
  dim: true,
1033
- children: "\xB7 none \u2014 type group/slug to create"
1192
+ children: "\xB7 none \u2014 type category/slug to create"
1034
1193
  }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6(Fragment, {
1035
1194
  children: [
1036
1195
  /* @__PURE__ */ jsxDEV6(Text6, {
@@ -1086,8 +1245,8 @@ function Launch({ onSelect }) {
1086
1245
  isFocused: true,
1087
1246
  maxVisible: 24,
1088
1247
  suggest: suggestions,
1089
- placeholder: "Filter or type group/slug to create\u2026",
1090
- emptyHint: showArchived ? "No sessions. Type group/slug to create one." : "No paused sessions. Type group/slug to create one.",
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.",
1091
1250
  onSubmit: handleSubmit,
1092
1251
  onKey: (e, cursorItem) => {
1093
1252
  if (e.key === "c" && e.ctrl) {
@@ -1112,7 +1271,7 @@ function Launch({ onSelect }) {
1112
1271
  /* @__PURE__ */ jsxDEV6(Text6, {
1113
1272
  dim: true,
1114
1273
  children: [
1115
- "\u2191\u2193 navigate \xB7 \u2190\u2192 skip group \xB7 enter continue/create \xB7 ctrl+a",
1274
+ "\u2191\u2193 navigate \xB7 \u2190\u2192 skip category \xB7 enter continue/create \xB7 ctrl+a",
1116
1275
  " ",
1117
1276
  showArchived ? "(un)archive" : "archive",
1118
1277
  " \xB7 tab",
@@ -1389,8 +1548,174 @@ function Resume({ sessionId, onSelect }) {
1389
1548
  }, undefined, true, undefined, this);
1390
1549
  }
1391
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
+
1392
1717
  // src/tui/run-screen.tsx
1393
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1718
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1394
1719
  var [, , screen, outputPath, ...args] = process.argv;
1395
1720
  if (!screen || !outputPath) {
1396
1721
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
@@ -1400,7 +1725,19 @@ var result;
1400
1725
  switch (screen) {
1401
1726
  case "launch": {
1402
1727
  let selection = { type: "quit" };
1403
- const app = render(/* @__PURE__ */ jsxDEV9(Launch, {
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, {
1404
1741
  onSelect: (s) => {
1405
1742
  selection = s;
1406
1743
  }
@@ -1417,7 +1754,7 @@ switch (screen) {
1417
1754
  process.exit(1);
1418
1755
  }
1419
1756
  let action = "save";
1420
- const app = render(/* @__PURE__ */ jsxDEV9(Exit, {
1757
+ const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1421
1758
  sessionId,
1422
1759
  onAction: (a) => {
1423
1760
  action = a;
@@ -1435,7 +1772,7 @@ switch (screen) {
1435
1772
  process.exit(1);
1436
1773
  }
1437
1774
  let selection = { type: "back" };
1438
- const app = render(/* @__PURE__ */ jsxDEV9(Resume, {
1775
+ const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1439
1776
  sessionId,
1440
1777
  onSelect: (s) => {
1441
1778
  selection = s;
@@ -1450,5 +1787,5 @@ switch (screen) {
1450
1787
  console.error(`Unknown screen: ${screen}`);
1451
1788
  process.exit(1);
1452
1789
  }
1453
- writeFileSync(outputPath, JSON.stringify(result));
1790
+ writeFileSync2(outputPath, JSON.stringify(result));
1454
1791
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertrand",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -47,6 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@orchetron/storm": "^0.2.0",
50
+ "@supabase/supabase-js": "^2.108.1",
50
51
  "drizzle-orm": "^0.44.0",
51
52
  "nanoid": "^5.1.5",
52
53
  "react": "^19.2.5"