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.
@@ -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,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 { 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
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 groups = sqliteTable("groups", {
732
+ var categories = sqliteTable("categories", {
599
733
  id: text("id").primaryKey(),
600
- parentId: text("parent_id").references(() => groups.id, {
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("groups_parent_slug").on(t.parentId, t.slug),
611
- index("groups_path").on(t.path)
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
- groupId: text("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }),
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("sessions_group_slug").on(t.groupId, t.slug),
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 _db = null;
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 (_db)
700
- return _db;
701
- mkdirSync(dirname(paths.db), { recursive: true });
702
- 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);
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
- _db = drizzle(sqlite, { schema: exports_schema });
709
- 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;
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, 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));
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 group: "group/session" (got "${trimmed}")`);
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 groupPath = segments.slice(0, -1).join("/");
811
- return { groupPath, slug };
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.groupPath}/${s.session.slug}`,
839
- label: `${s.groupPath}/${s.session.slug} ${status}`,
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 groupHeader(groupPath) {
1043
+ function categoryHeader(categoryPath) {
886
1044
  return {
887
- value: `__group:${groupPath}`,
888
- label: groupPath,
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.groupPath.localeCompare(b.groupPath);
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 lastGroup = null;
1077
+ let lastCategory = null;
920
1078
  for (const s of visibleSessions) {
921
- if (s.groupPath !== lastGroup) {
922
- rows.push(groupHeader(s.groupPath));
923
- lastGroup = s.groupPath;
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 groups2 = new Set;
1088
+ const categories2 = new Set;
931
1089
  const names = [];
932
1090
  for (const s of allSessions) {
933
- groups2.add(`${s.groupPath}/`);
934
- names.push(`${s.groupPath}/${s.session.slug}`);
1091
+ categories2.add(`${s.categoryPath}/`);
1092
+ names.push(`${s.categoryPath}/${s.session.slug}`);
935
1093
  }
936
- return [...groups2, ...names];
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.groupPath}/${s.session.slug}`, 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 { groupPath, slug } = parseSessionName(value);
986
- select({ type: "create", groupPath, slug });
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 group/slug to create"
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 group/slug to create\u2026",
1091
- 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.",
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 group \xB7 enter continue/create \xB7 ctrl+a",
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 jsxDEV9 } from "react/jsx-dev-runtime";
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__ */ 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, {
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__ */ jsxDEV9(Exit, {
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__ */ jsxDEV9(Resume, {
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
- writeFileSync(outputPath, JSON.stringify(result));
1790
+ writeFileSync2(outputPath, JSON.stringify(result));
1455
1791
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertrand",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },