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.
- package/dist/bertrand.js +1952 -291
- package/dist/dashboard/assets/{index-e53khqNa.js → index-ofrZyO9k.js} +102 -102
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/sw.js +1 -1
- package/dist/migrations/0003_categories_rename.sql +8 -0
- package/dist/migrations/meta/0003_snapshot.json +777 -0
- package/dist/migrations/meta/_journal.json +7 -0
- package/dist/run-screen.js +386 -49
- package/package.json +2 -1
package/dist/run-screen.js
CHANGED
|
@@ -15,7 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// src/tui/run-screen.tsx
|
|
18
|
-
import { writeFileSync } from "fs";
|
|
18
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
19
19
|
import { render } from "@orchetron/storm";
|
|
20
20
|
|
|
21
21
|
// src/tui/screens/launch/index.tsx
|
|
@@ -566,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 {
|
|
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
|
|
732
|
+
var categories = sqliteTable("categories", {
|
|
598
733
|
id: text("id").primaryKey(),
|
|
599
|
-
parentId: text("parent_id").references(() =>
|
|
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("
|
|
610
|
-
index("
|
|
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
|
-
|
|
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("
|
|
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
|
|
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 (
|
|
699
|
-
return
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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,
|
|
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
|
|
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
|
|
810
|
-
return {
|
|
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.
|
|
838
|
-
label: `${s.
|
|
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
|
|
1043
|
+
function categoryHeader(categoryPath) {
|
|
885
1044
|
return {
|
|
886
|
-
value: `
|
|
887
|
-
label:
|
|
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.
|
|
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
|
|
1077
|
+
let lastCategory = null;
|
|
919
1078
|
for (const s of visibleSessions) {
|
|
920
|
-
if (s.
|
|
921
|
-
rows.push(
|
|
922
|
-
|
|
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
|
|
1088
|
+
const categories2 = new Set;
|
|
930
1089
|
const names = [];
|
|
931
1090
|
for (const s of allSessions) {
|
|
932
|
-
|
|
933
|
-
names.push(`${s.
|
|
1091
|
+
categories2.add(`${s.categoryPath}/`);
|
|
1092
|
+
names.push(`${s.categoryPath}/${s.session.slug}`);
|
|
934
1093
|
}
|
|
935
|
-
return [...
|
|
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.
|
|
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 {
|
|
985
|
-
select({ type: "create",
|
|
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
|
|
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
|
|
1090
|
-
emptyHint: showArchived ? "No sessions. Type
|
|
1248
|
+
placeholder: "Filter or type category/slug to create\u2026",
|
|
1249
|
+
emptyHint: showArchived ? "No sessions. Type category/slug to create one." : "No paused sessions. Type category/slug to create one.",
|
|
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
|
|
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
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
-
|
|
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.
|
|
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"
|