project.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import z from "zod/v4"
  2. import { Filesystem } from "../util/filesystem"
  3. import path from "path"
  4. import { $ } from "bun"
  5. import { Storage } from "../storage/storage"
  6. import { Log } from "../util/log"
  7. export namespace Project {
  8. const log = Log.create({ service: "project" })
  9. export const Info = z
  10. .object({
  11. id: z.string(),
  12. worktree: z.string(),
  13. vcs: z.literal("git").optional(),
  14. time: z.object({
  15. created: z.number(),
  16. initialized: z.number().optional(),
  17. }),
  18. })
  19. .meta({
  20. ref: "Project",
  21. })
  22. export type Info = z.infer<typeof Info>
  23. const cache = new Map<string, Info>()
  24. export async function fromDirectory(directory: string) {
  25. log.info("fromDirectory", { directory })
  26. const fn = async () => {
  27. const matches = Filesystem.up({ targets: [".git"], start: directory })
  28. const git = await matches.next().then((x) => x.value)
  29. await matches.return()
  30. if (!git) {
  31. const project: Info = {
  32. id: "global",
  33. worktree: "/",
  34. time: {
  35. created: Date.now(),
  36. },
  37. }
  38. await Storage.write<Info>(["project", "global"], project)
  39. return project
  40. }
  41. let worktree = path.dirname(git)
  42. const [id] = await $`git rev-list --max-parents=0 --all`
  43. .quiet()
  44. .nothrow()
  45. .cwd(worktree)
  46. .text()
  47. .then((x) =>
  48. x
  49. .split("\n")
  50. .filter(Boolean)
  51. .map((x) => x.trim())
  52. .toSorted(),
  53. )
  54. if (!id) {
  55. const project: Info = {
  56. id: "global",
  57. worktree: "/",
  58. time: {
  59. created: Date.now(),
  60. },
  61. }
  62. await Storage.write<Info>(["project", "global"], project)
  63. return project
  64. }
  65. worktree = path.dirname(
  66. await $`git rev-parse --path-format=absolute --git-common-dir`
  67. .quiet()
  68. .nothrow()
  69. .cwd(worktree)
  70. .text()
  71. .then((x) => x.trim()),
  72. )
  73. const project: Info = {
  74. id,
  75. worktree,
  76. vcs: "git",
  77. time: {
  78. created: Date.now(),
  79. },
  80. }
  81. await Storage.write<Info>(["project", id], project)
  82. return project
  83. }
  84. if (cache.has(directory)) {
  85. return cache.get(directory)!
  86. }
  87. const result = await fn()
  88. cache.set(directory, result)
  89. return result
  90. }
  91. export async function setInitialized(projectID: string) {
  92. await Storage.update<Info>(["project", projectID], (draft) => {
  93. draft.time.initialized = Date.now()
  94. })
  95. }
  96. export async function list() {
  97. const keys = await Storage.list(["project"])
  98. return await Promise.all(keys.map((x) => Storage.read<Info>(x)))
  99. }
  100. }