bertrand 0.19.0 → 0.20.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.
@@ -18,9 +18,14 @@ var __export = (target, all) => {
18
18
  import { writeFileSync as writeFileSync2 } from "fs";
19
19
  import { render } from "@orchetron/storm";
20
20
 
21
- // src/tui/screens/launch/index.tsx
22
- import { useMemo as useMemo2, useState as useState3 } from "react";
23
- import { Box as Box5, Text as Text6, useTui } from "@orchetron/storm";
21
+ // src/tui/screens/startup/index.tsx
22
+ import { useState as useState4 } from "react";
23
+ import { useTui } from "@orchetron/storm";
24
+
25
+ // src/tui/screens/project-picker/index.tsx
26
+ import { useMemo as useMemo2 } from "react";
27
+ import { Box as Box5, Text as Text6 } from "@orchetron/storm";
28
+ import { existsSync as existsSync2 } from "fs";
24
29
 
25
30
  // src/tui/components/app-details.tsx
26
31
  import { Box, Text } from "@orchetron/storm";
@@ -560,16 +565,6 @@ function StatusDot({ status }) {
560
565
  children: "\u25CF"
561
566
  }, undefined, false, undefined, this);
562
567
  }
563
- // src/db/queries/sessions.ts
564
- import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
565
-
566
- // src/db/client.ts
567
- import { Database } from "bun:sqlite";
568
- import { drizzle } from "drizzle-orm/bun-sqlite";
569
- import { migrate } from "drizzle-orm/bun-sqlite/migrator";
570
- import { mkdirSync as mkdirSync2 } from "fs";
571
- import { dirname } from "path";
572
-
573
568
  // src/lib/projects/registry.ts
574
569
  import {
575
570
  existsSync,
@@ -580,6 +575,7 @@ import {
580
575
  statSync,
581
576
  writeFileSync
582
577
  } from "fs";
578
+ import { randomBytes } from "crypto";
583
579
  import { join as join2 } from "path";
584
580
 
585
581
  // src/lib/paths.ts
@@ -620,6 +616,14 @@ function readRegistry() {
620
616
  return null;
621
617
  }
622
618
  }
619
+ function writeRegistry(registry) {
620
+ const path = registryPath();
621
+ mkdirSync(_registryDir, { recursive: true });
622
+ const tmp = `${path}.tmp-${process.pid}-${process.hrtime.bigint()}-${randomBytes(4).toString("hex")}`;
623
+ writeFileSync(tmp, JSON.stringify(registry, null, 2) + `
624
+ `);
625
+ renameSync(tmp, path);
626
+ }
623
627
  function recoverFromDisk() {
624
628
  const dir = projectsDir();
625
629
  if (!existsSync(dir))
@@ -669,6 +673,52 @@ function listProjects() {
669
673
  function getActiveProjectSlug() {
670
674
  return loadRegistry()?.activeProjectSlug ?? DEFAULT_PROJECT_SLUG;
671
675
  }
676
+ function setActiveProjectSlug(slug) {
677
+ const registry = loadRegistry();
678
+ if (!registry) {
679
+ throw new Error(`No registry to update \u2014 create a project first (no ${registryPath()} and no ${projectsDir()}/* to recover from)`);
680
+ }
681
+ if (!registry.projects.some((p) => p.slug === slug)) {
682
+ throw new Error(`Unknown project slug "${slug}"`);
683
+ }
684
+ registry.activeProjectSlug = slug;
685
+ const idx = registry.projects.findIndex((p) => p.slug === slug);
686
+ if (idx !== -1) {
687
+ registry.projects[idx].lastUsedAt = new Date().toISOString();
688
+ }
689
+ writeRegistry(registry);
690
+ }
691
+ function registerProject(opts) {
692
+ const registry = loadRegistry() ?? {
693
+ activeProjectSlug: opts.slug,
694
+ projects: []
695
+ };
696
+ if (registry.projects.some((p) => p.slug === opts.slug)) {
697
+ throw new Error(`Project "${opts.slug}" already exists`);
698
+ }
699
+ const now = new Date().toISOString();
700
+ const entry = {
701
+ slug: opts.slug,
702
+ name: opts.name,
703
+ color: opts.color,
704
+ createdAt: now,
705
+ lastUsedAt: now
706
+ };
707
+ registry.projects.push(entry);
708
+ writeRegistry(registry);
709
+ return entry;
710
+ }
711
+ function removeProject(slug) {
712
+ const registry = loadRegistry();
713
+ if (!registry)
714
+ return;
715
+ registry.projects = registry.projects.filter((p) => p.slug !== slug);
716
+ if (registry.activeProjectSlug === slug) {
717
+ const survivor = [...registry.projects].sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))[0];
718
+ registry.activeProjectSlug = survivor?.slug ?? DEFAULT_PROJECT_SLUG;
719
+ }
720
+ writeRegistry(registry);
721
+ }
672
722
  function isRegistry(value) {
673
723
  if (typeof value !== "object" || value === null)
674
724
  return false;
@@ -697,6 +747,13 @@ function projectPaths(slug) {
697
747
  };
698
748
  }
699
749
 
750
+ // src/db/client.ts
751
+ import { Database } from "bun:sqlite";
752
+ import { drizzle } from "drizzle-orm/bun-sqlite";
753
+ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
754
+ import { mkdirSync as mkdirSync2 } from "fs";
755
+ import { dirname } from "path";
756
+
700
757
  // src/lib/projects/resolve.ts
701
758
  var _cached = null;
702
759
  function resolveActiveProject() {
@@ -714,6 +771,9 @@ function resolveActiveProject() {
714
771
  _cached = { slug, name, ...projectPaths(slug) };
715
772
  return _cached;
716
773
  }
774
+ function _resetActiveProjectCache() {
775
+ _cached = null;
776
+ }
717
777
 
718
778
  // src/db/schema.ts
719
779
  var exports_schema = {};
@@ -848,6 +908,7 @@ function openDb(dbPath) {
848
908
  return cached;
849
909
  mkdirSync2(dirname(dbPath), { recursive: true });
850
910
  const sqlite = new Database(dbPath);
911
+ sqlite.exec("PRAGMA busy_timeout = 5000");
851
912
  sqlite.exec("PRAGMA journal_mode = WAL");
852
913
  sqlite.exec("PRAGMA foreign_keys = ON");
853
914
  sqlite.exec("PRAGMA synchronous = NORMAL");
@@ -857,6 +918,10 @@ function openDb(dbPath) {
857
918
  if (!_migrated.has(dbPath)) {
858
919
  try {
859
920
  migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
921
+ if (!hasSessionsTable(sqlite)) {
922
+ sqlite.exec("DROP TABLE IF EXISTS __drizzle_migrations");
923
+ migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
924
+ }
860
925
  } catch (err) {
861
926
  sqlite.close();
862
927
  throw err;
@@ -866,6 +931,211 @@ function openDb(dbPath) {
866
931
  _cache.set(dbPath, db);
867
932
  return db;
868
933
  }
934
+ function hasSessionsTable(sqlite) {
935
+ const row = sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get();
936
+ return row !== null;
937
+ }
938
+
939
+ // src/lib/format.ts
940
+ var SECOND = 1000;
941
+ var MINUTE = 60 * SECOND;
942
+ var HOUR = 60 * MINUTE;
943
+ var DAY = 24 * HOUR;
944
+ function formatDuration(ms) {
945
+ if (ms < MINUTE)
946
+ return `${Math.round(ms / SECOND)}s`;
947
+ const days = Math.floor(ms / DAY);
948
+ const hours = Math.floor(ms % DAY / HOUR);
949
+ const minutes = Math.floor(ms % HOUR / MINUTE);
950
+ if (days > 0)
951
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
952
+ if (hours > 0)
953
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
954
+ return `${minutes}m`;
955
+ }
956
+ function formatAgo(isoOrDate) {
957
+ const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
958
+ const ms = Date.now() - date.getTime();
959
+ if (ms < MINUTE)
960
+ return "just now";
961
+ if (ms < HOUR)
962
+ return `${Math.floor(ms / MINUTE)}m ago`;
963
+ if (ms < DAY)
964
+ return `${Math.floor(ms / HOUR)}h ago`;
965
+ if (ms < 2 * DAY)
966
+ return "yesterday";
967
+ if (ms < 7 * DAY)
968
+ return `${Math.floor(ms / DAY)}d ago`;
969
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
970
+ }
971
+
972
+ // src/tui/screens/project-picker/index.tsx
973
+ import { jsxDEV as jsxDEV6, Fragment } from "react/jsx-dev-runtime";
974
+ var SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
975
+ function statsFor(slug) {
976
+ const dbFile = projectPaths(slug).db;
977
+ if (!existsSync2(dbFile))
978
+ return { total: 0, active: 0, unreadable: false };
979
+ try {
980
+ const db = getDbForProject(slug);
981
+ const all = db.select({ status: sessions.status }).from(sessions).all();
982
+ return {
983
+ total: all.length,
984
+ active: all.filter((s) => s.status === "active" || s.status === "waiting").length,
985
+ unreadable: false
986
+ };
987
+ } catch {
988
+ return { total: 0, active: 0, unreadable: true };
989
+ }
990
+ }
991
+ function projectRow(entry, isActive) {
992
+ const stats = statsFor(entry.slug);
993
+ const sessionsLabel = stats.unreadable ? "?" : `${stats.active}/${stats.total}`;
994
+ const lastUsed = formatAgo(entry.lastUsedAt);
995
+ return {
996
+ value: entry.slug,
997
+ label: `${entry.slug} ${entry.name} ${sessionsLabel}`,
998
+ meta: lastUsed,
999
+ display: (isCursor) => {
1000
+ const cursorColor = "green";
1001
+ const slugColor = isCursor ? cursorColor : undefined;
1002
+ const nameColor = isCursor ? cursorColor : undefined;
1003
+ return /* @__PURE__ */ jsxDEV6(Fragment, {
1004
+ children: [
1005
+ /* @__PURE__ */ jsxDEV6(Text6, {
1006
+ color: cursorColor,
1007
+ bold: true,
1008
+ children: isCursor ? "\u276F " : " "
1009
+ }, undefined, false, undefined, this),
1010
+ /* @__PURE__ */ jsxDEV6(Text6, {
1011
+ bold: isActive,
1012
+ color: isActive ? "gold" : slugColor,
1013
+ children: isActive ? "\u25CF " : " "
1014
+ }, undefined, false, undefined, this),
1015
+ /* @__PURE__ */ jsxDEV6(Text6, {
1016
+ color: slugColor,
1017
+ bold: isCursor || isActive,
1018
+ children: entry.slug.padEnd(16)
1019
+ }, undefined, false, undefined, this),
1020
+ /* @__PURE__ */ jsxDEV6(Text6, {
1021
+ color: nameColor,
1022
+ dim: !isCursor,
1023
+ children: entry.name.padEnd(20)
1024
+ }, undefined, false, undefined, this),
1025
+ /* @__PURE__ */ jsxDEV6(Text6, {
1026
+ dim: true,
1027
+ children: stats.unreadable ? " (unreadable)" : ` ${sessionsLabel} active/total`
1028
+ }, undefined, false, undefined, this)
1029
+ ]
1030
+ }, undefined, true, undefined, this);
1031
+ }
1032
+ };
1033
+ }
1034
+ function ProjectPicker({ onSelect }) {
1035
+ const allProjects = useMemo2(() => listProjects(), []);
1036
+ const activeSlug = useMemo2(() => getActiveProjectSlug(), []);
1037
+ const items = useMemo2(() => {
1038
+ return allProjects.slice().sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt)).map((p) => projectRow(p, p.slug === activeSlug));
1039
+ }, [allProjects, activeSlug]);
1040
+ const suggestions = useMemo2(() => allProjects.map((p) => p.slug), [allProjects]);
1041
+ const knownSlugs = useMemo2(() => new Set(allProjects.map((p) => p.slug)), [allProjects]);
1042
+ const select = (selection) => {
1043
+ onSelect(selection);
1044
+ };
1045
+ const handleSubmit = (value) => {
1046
+ const trimmed = value.trim();
1047
+ if (!trimmed)
1048
+ return;
1049
+ if (knownSlugs.has(trimmed)) {
1050
+ select({ type: "select", slug: trimmed });
1051
+ return;
1052
+ }
1053
+ if (!SLUG_PATTERN.test(trimmed)) {
1054
+ return;
1055
+ }
1056
+ select({ type: "create", slug: trimmed });
1057
+ };
1058
+ return /* @__PURE__ */ jsxDEV6(Box5, {
1059
+ flexDirection: "column",
1060
+ paddingY: 1,
1061
+ gap: 1,
1062
+ children: [
1063
+ /* @__PURE__ */ jsxDEV6(Box5, {
1064
+ marginX: 1,
1065
+ children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
1066
+ }, undefined, false, undefined, this),
1067
+ /* @__PURE__ */ jsxDEV6(Box5, {
1068
+ flexDirection: "column",
1069
+ marginX: 2,
1070
+ gap: 1,
1071
+ children: [
1072
+ /* @__PURE__ */ jsxDEV6(AppDetails, {}, undefined, false, undefined, this),
1073
+ /* @__PURE__ */ jsxDEV6(Box5, {
1074
+ flexDirection: "column",
1075
+ gap: 1,
1076
+ children: [
1077
+ /* @__PURE__ */ jsxDEV6(Box5, {
1078
+ flexDirection: "row",
1079
+ gap: 1,
1080
+ children: [
1081
+ /* @__PURE__ */ jsxDEV6(Text6, {
1082
+ bold: true,
1083
+ children: "Projects"
1084
+ }, undefined, false, undefined, this),
1085
+ allProjects.length > 0 ? /* @__PURE__ */ jsxDEV6(Fragment, {
1086
+ children: [
1087
+ /* @__PURE__ */ jsxDEV6(Text6, {
1088
+ dim: true,
1089
+ children: "\xB7"
1090
+ }, undefined, false, undefined, this),
1091
+ /* @__PURE__ */ jsxDEV6(Text6, {
1092
+ dim: true,
1093
+ children: [
1094
+ allProjects.length,
1095
+ " total",
1096
+ activeSlug ? ` \xB7 active: ${activeSlug}` : ""
1097
+ ]
1098
+ }, undefined, true, undefined, this)
1099
+ ]
1100
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV6(Text6, {
1101
+ dim: true,
1102
+ children: "\xB7 none \u2014 type a slug to create your first project"
1103
+ }, undefined, false, undefined, this)
1104
+ ]
1105
+ }, undefined, true, undefined, this),
1106
+ /* @__PURE__ */ jsxDEV6(Picker, {
1107
+ mode: "single",
1108
+ items,
1109
+ isFocused: true,
1110
+ maxVisible: 16,
1111
+ suggest: suggestions,
1112
+ placeholder: "Filter or type a new slug to create\u2026",
1113
+ emptyHint: "No projects. Type a slug to create one.",
1114
+ onSubmit: handleSubmit,
1115
+ onKey: (e) => {
1116
+ if (e.key === "c" && e.ctrl) {
1117
+ select({ type: "quit" });
1118
+ }
1119
+ }
1120
+ }, undefined, false, undefined, this),
1121
+ /* @__PURE__ */ jsxDEV6(Text6, {
1122
+ dim: true,
1123
+ children: "\u2191\u2193 navigate \xB7 enter select \xB7 type to filter or create new \xB7 ctrl+c quit"
1124
+ }, undefined, false, undefined, this)
1125
+ ]
1126
+ }, undefined, true, undefined, this)
1127
+ ]
1128
+ }, undefined, true, undefined, this)
1129
+ ]
1130
+ }, undefined, true, undefined, this);
1131
+ }
1132
+
1133
+ // src/tui/screens/launch/index.tsx
1134
+ import { useMemo as useMemo3, useState as useState3 } from "react";
1135
+ import { Box as Box6, Text as Text7 } from "@orchetron/storm";
1136
+
1137
+ // src/db/queries/sessions.ts
1138
+ import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
869
1139
 
870
1140
  // src/lib/id.ts
871
1141
  import { nanoid } from "nanoid";
@@ -916,39 +1186,6 @@ function unarchiveSession(id) {
916
1186
  return { ok: true, session: updated };
917
1187
  }
918
1188
 
919
- // src/lib/format.ts
920
- var SECOND = 1000;
921
- var MINUTE = 60 * SECOND;
922
- var HOUR = 60 * MINUTE;
923
- var DAY = 24 * HOUR;
924
- function formatDuration(ms) {
925
- if (ms < MINUTE)
926
- return `${Math.round(ms / SECOND)}s`;
927
- const days = Math.floor(ms / DAY);
928
- const hours = Math.floor(ms % DAY / HOUR);
929
- const minutes = Math.floor(ms % HOUR / MINUTE);
930
- if (days > 0)
931
- return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
932
- if (hours > 0)
933
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
934
- return `${minutes}m`;
935
- }
936
- function formatAgo(isoOrDate) {
937
- const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
938
- const ms = Date.now() - date.getTime();
939
- if (ms < MINUTE)
940
- return "just now";
941
- if (ms < HOUR)
942
- return `${Math.floor(ms / MINUTE)}m ago`;
943
- if (ms < DAY)
944
- return `${Math.floor(ms / HOUR)}h ago`;
945
- if (ms < 2 * DAY)
946
- return "yesterday";
947
- if (ms < 7 * DAY)
948
- return `${Math.floor(ms / DAY)}d ago`;
949
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
950
- }
951
-
952
1189
  // src/lib/parse-session-name.ts
953
1190
  function parseSessionName(input) {
954
1191
  const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
@@ -970,7 +1207,7 @@ function parseSessionName(input) {
970
1207
  }
971
1208
 
972
1209
  // src/tui/screens/launch/index.tsx
973
- import { jsxDEV as jsxDEV6, Fragment } from "react/jsx-dev-runtime";
1210
+ import { jsxDEV as jsxDEV7, Fragment as Fragment2 } from "react/jsx-dev-runtime";
974
1211
  var STATUS_COLOR = {
975
1212
  paused: "gold",
976
1213
  waiting: "red",
@@ -1004,14 +1241,14 @@ function sessionRow(s) {
1004
1241
  const textColor = isCursor ? cursorColor : color;
1005
1242
  const slugColor = isCursor ? cursorColor : undefined;
1006
1243
  const dimText = !isCursor && (disabled || isArchived);
1007
- return /* @__PURE__ */ jsxDEV6(Fragment, {
1244
+ return /* @__PURE__ */ jsxDEV7(Fragment2, {
1008
1245
  children: [
1009
- /* @__PURE__ */ jsxDEV6(Text6, {
1246
+ /* @__PURE__ */ jsxDEV7(Text7, {
1010
1247
  color: cursorColor,
1011
1248
  bold: true,
1012
1249
  children: isCursor ? "\u276F " : " "
1013
1250
  }, undefined, false, undefined, this),
1014
- /* @__PURE__ */ jsxDEV6(Text6, {
1251
+ /* @__PURE__ */ jsxDEV7(Text7, {
1015
1252
  color: dotColor,
1016
1253
  bold: isCursor,
1017
1254
  children: [
@@ -1019,17 +1256,17 @@ function sessionRow(s) {
1019
1256
  " "
1020
1257
  ]
1021
1258
  }, undefined, true, undefined, this),
1022
- /* @__PURE__ */ jsxDEV6(Text6, {
1259
+ /* @__PURE__ */ jsxDEV7(Text7, {
1023
1260
  color: textColor,
1024
1261
  bold: isCursor,
1025
1262
  dim: dimText,
1026
1263
  children: status.padEnd(8)
1027
1264
  }, undefined, false, undefined, this),
1028
- /* @__PURE__ */ jsxDEV6(Text6, {
1265
+ /* @__PURE__ */ jsxDEV7(Text7, {
1029
1266
  color: slugColor,
1030
1267
  children: " "
1031
1268
  }, undefined, false, undefined, this),
1032
- /* @__PURE__ */ jsxDEV6(Text6, {
1269
+ /* @__PURE__ */ jsxDEV7(Text7, {
1033
1270
  color: slugColor,
1034
1271
  bold: isCursor,
1035
1272
  dim: dimText,
@@ -1048,13 +1285,12 @@ function categoryHeader(categoryPath) {
1048
1285
  };
1049
1286
  }
1050
1287
  function Launch({ onSelect }) {
1051
- const { exit } = useTui();
1052
1288
  const [error, setError] = useState3(null);
1053
1289
  const [notice, setNotice] = useState3(null);
1054
1290
  const [showArchived, setShowArchived] = useState3(false);
1055
1291
  const [refreshKey, setRefreshKey] = useState3(0);
1056
- const allSessions = useMemo2(() => getAllSessions({ excludeArchived: !showArchived }), [showArchived, refreshKey]);
1057
- const visibleSessions = useMemo2(() => {
1292
+ const allSessions = useMemo3(() => getAllSessions({ excludeArchived: !showArchived }), [showArchived, refreshKey]);
1293
+ const visibleSessions = useMemo3(() => {
1058
1294
  return allSessions.filter((s) => {
1059
1295
  const st = s.session.status;
1060
1296
  if (st === "paused" || st === "waiting")
@@ -1072,7 +1308,7 @@ function Launch({ onSelect }) {
1072
1308
  return recencyKey(b).localeCompare(recencyKey(a));
1073
1309
  });
1074
1310
  }, [allSessions, showArchived]);
1075
- const items = useMemo2(() => {
1311
+ const items = useMemo3(() => {
1076
1312
  const rows = [];
1077
1313
  let lastCategory = null;
1078
1314
  for (const s of visibleSessions) {
@@ -1084,7 +1320,7 @@ function Launch({ onSelect }) {
1084
1320
  }
1085
1321
  return rows;
1086
1322
  }, [visibleSessions]);
1087
- const suggestions = useMemo2(() => {
1323
+ const suggestions = useMemo3(() => {
1088
1324
  const categories2 = new Set;
1089
1325
  const names = [];
1090
1326
  for (const s of allSessions) {
@@ -1093,7 +1329,7 @@ function Launch({ onSelect }) {
1093
1329
  }
1094
1330
  return [...categories2, ...names];
1095
1331
  }, [allSessions]);
1096
- const sessionByValue = useMemo2(() => {
1332
+ const sessionByValue = useMemo3(() => {
1097
1333
  const map = new Map;
1098
1334
  for (const s of allSessions) {
1099
1335
  map.set(`${s.categoryPath}/${s.session.slug}`, s);
@@ -1127,7 +1363,6 @@ function Launch({ onSelect }) {
1127
1363
  };
1128
1364
  const select = (selection) => {
1129
1365
  onSelect(selection);
1130
- exit();
1131
1366
  };
1132
1367
  const handleSubmit = (value) => {
1133
1368
  const existing = sessionByValue.get(value);
@@ -1146,7 +1381,7 @@ function Launch({ onSelect }) {
1146
1381
  setError(e instanceof Error ? e.message : "Invalid name");
1147
1382
  }
1148
1383
  };
1149
- const counts = useMemo2(() => {
1384
+ const counts = useMemo3(() => {
1150
1385
  let paused = 0;
1151
1386
  let waiting = 0;
1152
1387
  let archived = 0;
@@ -1160,56 +1395,56 @@ function Launch({ onSelect }) {
1160
1395
  }
1161
1396
  return { paused, waiting, archived };
1162
1397
  }, [visibleSessions]);
1163
- return /* @__PURE__ */ jsxDEV6(Box5, {
1398
+ return /* @__PURE__ */ jsxDEV7(Box6, {
1164
1399
  flexDirection: "column",
1165
1400
  paddingY: 1,
1166
1401
  gap: 1,
1167
1402
  children: [
1168
- /* @__PURE__ */ jsxDEV6(Box5, {
1403
+ /* @__PURE__ */ jsxDEV7(Box6, {
1169
1404
  marginX: 1,
1170
- children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
1405
+ children: /* @__PURE__ */ jsxDEV7(Logo, {}, undefined, false, undefined, this)
1171
1406
  }, undefined, false, undefined, this),
1172
- /* @__PURE__ */ jsxDEV6(Box5, {
1407
+ /* @__PURE__ */ jsxDEV7(Box6, {
1173
1408
  flexDirection: "column",
1174
1409
  marginX: 2,
1175
1410
  gap: 1,
1176
1411
  children: [
1177
- /* @__PURE__ */ jsxDEV6(AppDetails, {}, undefined, false, undefined, this),
1178
- /* @__PURE__ */ jsxDEV6(Box5, {
1412
+ /* @__PURE__ */ jsxDEV7(AppDetails, {}, undefined, false, undefined, this),
1413
+ /* @__PURE__ */ jsxDEV7(Box6, {
1179
1414
  flexDirection: "column",
1180
1415
  gap: 1,
1181
1416
  children: [
1182
- /* @__PURE__ */ jsxDEV6(Box5, {
1417
+ /* @__PURE__ */ jsxDEV7(Box6, {
1183
1418
  flexDirection: "row",
1184
1419
  gap: 1,
1185
1420
  children: [
1186
- /* @__PURE__ */ jsxDEV6(Text6, {
1421
+ /* @__PURE__ */ jsxDEV7(Text7, {
1187
1422
  bold: true,
1188
1423
  children: "Sessions"
1189
1424
  }, undefined, false, undefined, this),
1190
- visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV6(Text6, {
1425
+ visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV7(Text7, {
1191
1426
  dim: true,
1192
1427
  children: "\xB7 none \u2014 type category/slug to create"
1193
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6(Fragment, {
1428
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV7(Fragment2, {
1194
1429
  children: [
1195
- /* @__PURE__ */ jsxDEV6(Text6, {
1430
+ /* @__PURE__ */ jsxDEV7(Text7, {
1196
1431
  dim: true,
1197
1432
  children: "\xB7"
1198
1433
  }, undefined, false, undefined, this),
1199
- /* @__PURE__ */ jsxDEV6(Text6, {
1434
+ /* @__PURE__ */ jsxDEV7(Text7, {
1200
1435
  color: "gold",
1201
1436
  children: [
1202
1437
  counts.paused,
1203
1438
  " paused"
1204
1439
  ]
1205
1440
  }, undefined, true, undefined, this),
1206
- counts.waiting > 0 && /* @__PURE__ */ jsxDEV6(Fragment, {
1441
+ counts.waiting > 0 && /* @__PURE__ */ jsxDEV7(Fragment2, {
1207
1442
  children: [
1208
- /* @__PURE__ */ jsxDEV6(Text6, {
1443
+ /* @__PURE__ */ jsxDEV7(Text7, {
1209
1444
  dim: true,
1210
1445
  children: "\xB7"
1211
1446
  }, undefined, false, undefined, this),
1212
- /* @__PURE__ */ jsxDEV6(Text6, {
1447
+ /* @__PURE__ */ jsxDEV7(Text7, {
1213
1448
  color: "red",
1214
1449
  dim: true,
1215
1450
  children: [
@@ -1219,13 +1454,13 @@ function Launch({ onSelect }) {
1219
1454
  }, undefined, true, undefined, this)
1220
1455
  ]
1221
1456
  }, undefined, true, undefined, this),
1222
- showArchived && counts.archived > 0 && /* @__PURE__ */ jsxDEV6(Fragment, {
1457
+ showArchived && counts.archived > 0 && /* @__PURE__ */ jsxDEV7(Fragment2, {
1223
1458
  children: [
1224
- /* @__PURE__ */ jsxDEV6(Text6, {
1459
+ /* @__PURE__ */ jsxDEV7(Text7, {
1225
1460
  dim: true,
1226
1461
  children: "\xB7"
1227
1462
  }, undefined, false, undefined, this),
1228
- /* @__PURE__ */ jsxDEV6(Text6, {
1463
+ /* @__PURE__ */ jsxDEV7(Text7, {
1229
1464
  color: "purple",
1230
1465
  dim: true,
1231
1466
  children: [
@@ -1239,7 +1474,7 @@ function Launch({ onSelect }) {
1239
1474
  }, undefined, true, undefined, this)
1240
1475
  ]
1241
1476
  }, undefined, true, undefined, this),
1242
- /* @__PURE__ */ jsxDEV6(Picker, {
1477
+ /* @__PURE__ */ jsxDEV7(Picker, {
1243
1478
  mode: "single",
1244
1479
  items,
1245
1480
  isFocused: true,
@@ -1260,15 +1495,15 @@ function Launch({ onSelect }) {
1260
1495
  }
1261
1496
  }
1262
1497
  }, undefined, false, undefined, this),
1263
- notice && /* @__PURE__ */ jsxDEV6(Text6, {
1498
+ notice && /* @__PURE__ */ jsxDEV7(Text7, {
1264
1499
  color: "green",
1265
1500
  children: notice
1266
1501
  }, undefined, false, undefined, this),
1267
- error && /* @__PURE__ */ jsxDEV6(Text6, {
1502
+ error && /* @__PURE__ */ jsxDEV7(Text7, {
1268
1503
  color: "red",
1269
1504
  children: error
1270
1505
  }, undefined, false, undefined, this),
1271
- /* @__PURE__ */ jsxDEV6(Text6, {
1506
+ /* @__PURE__ */ jsxDEV7(Text7, {
1272
1507
  dim: true,
1273
1508
  children: [
1274
1509
  "\u2191\u2193 navigate \xB7 \u2190\u2192 skip category \xB7 enter continue/create \xB7 ctrl+a",
@@ -1288,9 +1523,104 @@ function Launch({ onSelect }) {
1288
1523
  }, undefined, true, undefined, this);
1289
1524
  }
1290
1525
 
1526
+ // src/lib/projects/create.ts
1527
+ import { mkdirSync as mkdirSync4 } from "fs";
1528
+
1529
+ // src/db/migrate.ts
1530
+ import { Database as Database2 } from "bun:sqlite";
1531
+ import { drizzle as drizzle2 } from "drizzle-orm/bun-sqlite";
1532
+ import { migrate as migrate2 } from "drizzle-orm/bun-sqlite/migrator";
1533
+ import { mkdirSync as mkdirSync3 } from "fs";
1534
+ import { dirname as dirname2 } from "path";
1535
+ var MIGRATIONS_FOLDER2 = import.meta.dir + "/migrations";
1536
+ function runMigrations(dbPath) {
1537
+ const target = dbPath ?? resolveActiveProject().db;
1538
+ mkdirSync3(dirname2(target), { recursive: true });
1539
+ const sqlite = new Database2(target);
1540
+ sqlite.exec("PRAGMA busy_timeout = 5000");
1541
+ sqlite.exec("PRAGMA journal_mode = WAL");
1542
+ sqlite.exec("PRAGMA foreign_keys = ON");
1543
+ const db = drizzle2(sqlite);
1544
+ migrate2(db, { migrationsFolder: MIGRATIONS_FOLDER2 });
1545
+ sqlite.close();
1546
+ }
1547
+ if (false) {}
1548
+
1549
+ // src/lib/projects/create.ts
1550
+ function createProject(opts) {
1551
+ registerProject({ slug: opts.slug, name: opts.name ?? opts.slug });
1552
+ try {
1553
+ const paths2 = projectPaths(opts.slug);
1554
+ mkdirSync4(paths2.root, { recursive: true });
1555
+ runMigrations(paths2.db);
1556
+ } catch (err) {
1557
+ try {
1558
+ removeProject(opts.slug);
1559
+ } catch {}
1560
+ throw err;
1561
+ }
1562
+ }
1563
+
1564
+ // src/tui/screens/startup/index.tsx
1565
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
1566
+ function Startup({
1567
+ skipProjectPicker,
1568
+ initialProjectSlug,
1569
+ onSelect
1570
+ }) {
1571
+ const { exit } = useTui();
1572
+ const [activeSlug, setActiveSlug] = useState4(skipProjectPicker ? initialProjectSlug : null);
1573
+ const finish = (s) => {
1574
+ onSelect(s);
1575
+ exit();
1576
+ };
1577
+ const handleProjectSelect = (s) => {
1578
+ switch (s.type) {
1579
+ case "quit":
1580
+ finish({ type: "quit" });
1581
+ return;
1582
+ case "create":
1583
+ createProject({ slug: s.slug });
1584
+ setActiveProjectSlug(s.slug);
1585
+ _resetActiveProjectCache();
1586
+ setActiveSlug(s.slug);
1587
+ return;
1588
+ case "select":
1589
+ setActiveProjectSlug(s.slug);
1590
+ _resetActiveProjectCache();
1591
+ setActiveSlug(s.slug);
1592
+ return;
1593
+ }
1594
+ };
1595
+ const handleLaunchSelect = (s) => {
1596
+ if (s.type === "quit" || activeSlug === null) {
1597
+ finish({ type: "quit" });
1598
+ return;
1599
+ }
1600
+ if (s.type === "create") {
1601
+ finish({
1602
+ type: "create",
1603
+ projectSlug: activeSlug,
1604
+ categoryPath: s.categoryPath,
1605
+ slug: s.slug
1606
+ });
1607
+ return;
1608
+ }
1609
+ finish({ type: "pick", projectSlug: activeSlug, sessionId: s.sessionId });
1610
+ };
1611
+ if (activeSlug === null) {
1612
+ return /* @__PURE__ */ jsxDEV8(ProjectPicker, {
1613
+ onSelect: handleProjectSelect
1614
+ }, undefined, false, undefined, this);
1615
+ }
1616
+ return /* @__PURE__ */ jsxDEV8(Launch, {
1617
+ onSelect: handleLaunchSelect
1618
+ }, undefined, false, undefined, this);
1619
+ }
1620
+
1291
1621
  // src/tui/screens/Exit.tsx
1292
- import { useState as useState4 } from "react";
1293
- import { Box as Box6, Text as Text7, useInput as useInput3, useTui as useTui2 } from "@orchetron/storm";
1622
+ import { useState as useState5 } from "react";
1623
+ import { Box as Box7, Text as Text8, useInput as useInput3, useTui as useTui2 } from "@orchetron/storm";
1294
1624
 
1295
1625
  // src/db/queries/conversations.ts
1296
1626
  import { eq as eq2, and as and2, desc, sql as sql3 } from "drizzle-orm";
@@ -1299,7 +1629,7 @@ function getConversationsBySession(sessionId) {
1299
1629
  }
1300
1630
 
1301
1631
  // src/tui/screens/Exit.tsx
1302
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
1632
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1303
1633
  var OPTIONS = [
1304
1634
  { action: "save", label: "Save", hint: "Keep session paused for later" },
1305
1635
  {
@@ -1320,7 +1650,7 @@ var OPTIONS = [
1320
1650
  ];
1321
1651
  function Exit({ sessionId, onAction }) {
1322
1652
  const { exit } = useTui2();
1323
- const [cursor, setCursor] = useState4(0);
1653
+ const [cursor, setCursor] = useState5(0);
1324
1654
  const session = getSession(sessionId);
1325
1655
  const conversations2 = session ? getConversationsBySession(session.id) : [];
1326
1656
  useInput3((e) => {
@@ -1340,37 +1670,37 @@ function Exit({ sessionId, onAction }) {
1340
1670
  }
1341
1671
  });
1342
1672
  if (!session) {
1343
- return /* @__PURE__ */ jsxDEV7(Text7, {
1673
+ return /* @__PURE__ */ jsxDEV9(Text8, {
1344
1674
  color: "red",
1345
1675
  children: "Session not found"
1346
1676
  }, undefined, false, undefined, this);
1347
1677
  }
1348
1678
  const duration = session.endedAt && session.startedAt ? formatDuration(new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime()) : null;
1349
- return /* @__PURE__ */ jsxDEV7(Box6, {
1679
+ return /* @__PURE__ */ jsxDEV9(Box7, {
1350
1680
  flexDirection: "column",
1351
1681
  padding: 1,
1352
1682
  gap: 1,
1353
1683
  children: [
1354
- /* @__PURE__ */ jsxDEV7(Text7, {
1684
+ /* @__PURE__ */ jsxDEV9(Text8, {
1355
1685
  bold: true,
1356
1686
  color: "#82AAFF",
1357
1687
  children: "Session ended"
1358
1688
  }, undefined, false, undefined, this),
1359
- /* @__PURE__ */ jsxDEV7(Box6, {
1689
+ /* @__PURE__ */ jsxDEV9(Box7, {
1360
1690
  flexDirection: "column",
1361
1691
  children: [
1362
- /* @__PURE__ */ jsxDEV7(Box6, {
1692
+ /* @__PURE__ */ jsxDEV9(Box7, {
1363
1693
  flexDirection: "row",
1364
1694
  gap: 1,
1365
1695
  children: [
1366
- /* @__PURE__ */ jsxDEV7(StatusDot, {
1696
+ /* @__PURE__ */ jsxDEV9(StatusDot, {
1367
1697
  status: session.status
1368
1698
  }, undefined, false, undefined, this),
1369
- /* @__PURE__ */ jsxDEV7(Text7, {
1699
+ /* @__PURE__ */ jsxDEV9(Text8, {
1370
1700
  bold: true,
1371
1701
  children: session.name
1372
1702
  }, undefined, false, undefined, this),
1373
- duration && /* @__PURE__ */ jsxDEV7(Text7, {
1703
+ duration && /* @__PURE__ */ jsxDEV9(Text8, {
1374
1704
  dim: true,
1375
1705
  children: [
1376
1706
  "(",
@@ -1380,7 +1710,7 @@ function Exit({ sessionId, onAction }) {
1380
1710
  }, undefined, true, undefined, this)
1381
1711
  ]
1382
1712
  }, undefined, true, undefined, this),
1383
- /* @__PURE__ */ jsxDEV7(Text7, {
1713
+ /* @__PURE__ */ jsxDEV9(Text8, {
1384
1714
  dim: true,
1385
1715
  children: [
1386
1716
  conversations2.length,
@@ -1390,28 +1720,28 @@ function Exit({ sessionId, onAction }) {
1390
1720
  }, undefined, true, undefined, this)
1391
1721
  ]
1392
1722
  }, undefined, true, undefined, this),
1393
- /* @__PURE__ */ jsxDEV7(Box6, {
1723
+ /* @__PURE__ */ jsxDEV9(Box7, {
1394
1724
  flexDirection: "column",
1395
- children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV7(Box6, {
1725
+ children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV9(Box7, {
1396
1726
  flexDirection: "row",
1397
1727
  gap: 1,
1398
1728
  children: [
1399
- /* @__PURE__ */ jsxDEV7(Text7, {
1729
+ /* @__PURE__ */ jsxDEV9(Text8, {
1400
1730
  children: i === cursor ? "\u276F" : " "
1401
1731
  }, undefined, false, undefined, this),
1402
- /* @__PURE__ */ jsxDEV7(Text7, {
1732
+ /* @__PURE__ */ jsxDEV9(Text8, {
1403
1733
  bold: i === cursor,
1404
1734
  children: opt.label
1405
1735
  }, undefined, false, undefined, this),
1406
- /* @__PURE__ */ jsxDEV7(Text7, {
1736
+ /* @__PURE__ */ jsxDEV9(Text8, {
1407
1737
  dim: true,
1408
1738
  children: opt.hint
1409
1739
  }, undefined, false, undefined, this)
1410
1740
  ]
1411
1741
  }, opt.action, true, undefined, this))
1412
1742
  }, undefined, false, undefined, this),
1413
- /* @__PURE__ */ jsxDEV7(Box6, {
1414
- children: /* @__PURE__ */ jsxDEV7(Text7, {
1743
+ /* @__PURE__ */ jsxDEV9(Box7, {
1744
+ children: /* @__PURE__ */ jsxDEV9(Text8, {
1415
1745
  dim: true,
1416
1746
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
1417
1747
  }, undefined, false, undefined, this)
@@ -1421,12 +1751,12 @@ function Exit({ sessionId, onAction }) {
1421
1751
  }
1422
1752
 
1423
1753
  // src/tui/screens/Resume.tsx
1424
- import { useState as useState5 } from "react";
1425
- import { Box as Box7, Text as Text8, useInput as useInput4, useTui as useTui3 } from "@orchetron/storm";
1426
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
1754
+ import { useState as useState6 } from "react";
1755
+ import { Box as Box8, Text as Text9, useInput as useInput4, useTui as useTui3 } from "@orchetron/storm";
1756
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1427
1757
  function Resume({ sessionId, onSelect }) {
1428
1758
  const { exit } = useTui3();
1429
- const [cursor, setCursor] = useState5(0);
1759
+ const [cursor, setCursor] = useState6(0);
1430
1760
  const session = getSession(sessionId);
1431
1761
  const conversations2 = getConversationsBySession(sessionId);
1432
1762
  const totalOptions = conversations2.length + 1;
@@ -1453,17 +1783,17 @@ function Resume({ sessionId, onSelect }) {
1453
1783
  }
1454
1784
  });
1455
1785
  if (!session) {
1456
- return /* @__PURE__ */ jsxDEV8(Text8, {
1786
+ return /* @__PURE__ */ jsxDEV10(Text9, {
1457
1787
  color: "red",
1458
1788
  children: "Session not found"
1459
1789
  }, undefined, false, undefined, this);
1460
1790
  }
1461
- return /* @__PURE__ */ jsxDEV8(Box7, {
1791
+ return /* @__PURE__ */ jsxDEV10(Box8, {
1462
1792
  flexDirection: "column",
1463
1793
  padding: 1,
1464
1794
  gap: 1,
1465
1795
  children: [
1466
- /* @__PURE__ */ jsxDEV8(Text8, {
1796
+ /* @__PURE__ */ jsxDEV10(Text9, {
1467
1797
  bold: true,
1468
1798
  color: "#82AAFF",
1469
1799
  children: [
@@ -1471,17 +1801,17 @@ function Resume({ sessionId, onSelect }) {
1471
1801
  session.name
1472
1802
  ]
1473
1803
  }, undefined, true, undefined, this),
1474
- /* @__PURE__ */ jsxDEV8(Box7, {
1804
+ /* @__PURE__ */ jsxDEV10(Box8, {
1475
1805
  flexDirection: "column",
1476
1806
  children: [
1477
- /* @__PURE__ */ jsxDEV8(Box7, {
1807
+ /* @__PURE__ */ jsxDEV10(Box8, {
1478
1808
  flexDirection: "row",
1479
1809
  gap: 1,
1480
1810
  children: [
1481
- /* @__PURE__ */ jsxDEV8(Text8, {
1811
+ /* @__PURE__ */ jsxDEV10(Text9, {
1482
1812
  children: cursor === 0 ? "\u276F" : " "
1483
1813
  }, undefined, false, undefined, this),
1484
- /* @__PURE__ */ jsxDEV8(Text8, {
1814
+ /* @__PURE__ */ jsxDEV10(Text9, {
1485
1815
  bold: cursor === 0,
1486
1816
  color: "#34D399",
1487
1817
  children: "+ New conversation"
@@ -1493,42 +1823,42 @@ function Resume({ sessionId, onSelect }) {
1493
1823
  const isSelected = cursor === idx;
1494
1824
  const duration = conv.endedAt ? formatDuration(new Date(conv.endedAt).getTime() - new Date(conv.startedAt).getTime()) : "active";
1495
1825
  const ago = formatAgo(conv.startedAt);
1496
- return /* @__PURE__ */ jsxDEV8(Box7, {
1826
+ return /* @__PURE__ */ jsxDEV10(Box8, {
1497
1827
  flexDirection: "row",
1498
1828
  gap: 1,
1499
1829
  children: [
1500
- /* @__PURE__ */ jsxDEV8(Text8, {
1830
+ /* @__PURE__ */ jsxDEV10(Text9, {
1501
1831
  children: isSelected ? "\u276F" : " "
1502
1832
  }, undefined, false, undefined, this),
1503
- /* @__PURE__ */ jsxDEV8(Text8, {
1833
+ /* @__PURE__ */ jsxDEV10(Text9, {
1504
1834
  bold: isSelected,
1505
1835
  dim: conv.discarded,
1506
1836
  children: conv.id.slice(0, 8)
1507
1837
  }, undefined, false, undefined, this),
1508
- /* @__PURE__ */ jsxDEV8(Text8, {
1838
+ /* @__PURE__ */ jsxDEV10(Text9, {
1509
1839
  dim: true,
1510
1840
  children: ago
1511
1841
  }, undefined, false, undefined, this),
1512
- /* @__PURE__ */ jsxDEV8(Text8, {
1842
+ /* @__PURE__ */ jsxDEV10(Text9, {
1513
1843
  dim: true,
1514
1844
  children: "\xB7"
1515
1845
  }, undefined, false, undefined, this),
1516
- /* @__PURE__ */ jsxDEV8(Text8, {
1846
+ /* @__PURE__ */ jsxDEV10(Text9, {
1517
1847
  dim: true,
1518
1848
  children: [
1519
1849
  conv.eventCount,
1520
1850
  " events"
1521
1851
  ]
1522
1852
  }, undefined, true, undefined, this),
1523
- /* @__PURE__ */ jsxDEV8(Text8, {
1853
+ /* @__PURE__ */ jsxDEV10(Text9, {
1524
1854
  dim: true,
1525
1855
  children: "\xB7"
1526
1856
  }, undefined, false, undefined, this),
1527
- /* @__PURE__ */ jsxDEV8(Text8, {
1857
+ /* @__PURE__ */ jsxDEV10(Text9, {
1528
1858
  dim: true,
1529
1859
  children: duration
1530
1860
  }, undefined, false, undefined, this),
1531
- conv.discarded && /* @__PURE__ */ jsxDEV8(Text8, {
1861
+ conv.discarded && /* @__PURE__ */ jsxDEV10(Text9, {
1532
1862
  color: "red",
1533
1863
  dim: true,
1534
1864
  children: "(discarded)"
@@ -1538,8 +1868,8 @@ function Resume({ sessionId, onSelect }) {
1538
1868
  })
1539
1869
  ]
1540
1870
  }, undefined, true, undefined, this),
1541
- /* @__PURE__ */ jsxDEV8(Box7, {
1542
- children: /* @__PURE__ */ jsxDEV8(Text8, {
1871
+ /* @__PURE__ */ jsxDEV10(Box8, {
1872
+ children: /* @__PURE__ */ jsxDEV10(Text9, {
1543
1873
  dim: true,
1544
1874
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q back"
1545
1875
  }, undefined, false, undefined, this)
@@ -1548,174 +1878,8 @@ function Resume({ sessionId, onSelect }) {
1548
1878
  }, undefined, true, undefined, this);
1549
1879
  }
1550
1880
 
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
-
1717
1881
  // src/tui/run-screen.tsx
1718
- import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1882
+ import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
1719
1883
  var [, , screen, outputPath, ...args] = process.argv;
1720
1884
  if (!screen || !outputPath) {
1721
1885
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
@@ -1723,21 +1887,13 @@ if (!screen || !outputPath) {
1723
1887
  }
1724
1888
  var result;
1725
1889
  switch (screen) {
1726
- case "launch": {
1727
- let selection = { type: "quit" };
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": {
1890
+ case "startup": {
1891
+ const skipProjectPicker = args[0] === "true";
1892
+ const initialProjectSlug = args[1] ?? "";
1739
1893
  let selection = { type: "quit" };
1740
- const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
1894
+ const app = render(/* @__PURE__ */ jsxDEV11(Startup, {
1895
+ skipProjectPicker,
1896
+ initialProjectSlug,
1741
1897
  onSelect: (s) => {
1742
1898
  selection = s;
1743
1899
  }
@@ -1754,7 +1910,7 @@ switch (screen) {
1754
1910
  process.exit(1);
1755
1911
  }
1756
1912
  let action = "save";
1757
- const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1913
+ const app = render(/* @__PURE__ */ jsxDEV11(Exit, {
1758
1914
  sessionId,
1759
1915
  onAction: (a) => {
1760
1916
  action = a;
@@ -1772,7 +1928,7 @@ switch (screen) {
1772
1928
  process.exit(1);
1773
1929
  }
1774
1930
  let selection = { type: "back" };
1775
- const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1931
+ const app = render(/* @__PURE__ */ jsxDEV11(Resume, {
1776
1932
  sessionId,
1777
1933
  onSelect: (s) => {
1778
1934
  selection = s;