@bevel-software/platform-shared 0.1.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.
Files changed (77) hide show
  1. package/LICENSE +202 -0
  2. package/dist/auth/types.d.ts +15 -0
  3. package/dist/auth/types.d.ts.map +1 -0
  4. package/dist/auth/types.js +2 -0
  5. package/dist/auth/types.js.map +1 -0
  6. package/dist/chat/types.d.ts +39 -0
  7. package/dist/chat/types.d.ts.map +1 -0
  8. package/dist/chat/types.js +13 -0
  9. package/dist/chat/types.js.map +1 -0
  10. package/dist/git/branchAuthor.d.ts +28 -0
  11. package/dist/git/branchAuthor.d.ts.map +1 -0
  12. package/dist/git/branchAuthor.js +45 -0
  13. package/dist/git/branchAuthor.js.map +1 -0
  14. package/dist/git/pr.types.d.ts +264 -0
  15. package/dist/git/pr.types.d.ts.map +1 -0
  16. package/dist/git/pr.types.js +2 -0
  17. package/dist/git/pr.types.js.map +1 -0
  18. package/dist/git/protected.d.ts +52 -0
  19. package/dist/git/protected.d.ts.map +1 -0
  20. package/dist/git/protected.js +92 -0
  21. package/dist/git/protected.js.map +1 -0
  22. package/dist/git/review.types.d.ts +49 -0
  23. package/dist/git/review.types.d.ts.map +1 -0
  24. package/dist/git/review.types.js +2 -0
  25. package/dist/git/review.types.js.map +1 -0
  26. package/dist/git/types.d.ts +107 -0
  27. package/dist/git/types.d.ts.map +1 -0
  28. package/dist/git/types.js +2 -0
  29. package/dist/git/types.js.map +1 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +20 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/workflow/events.d.ts +218 -0
  35. package/dist/workflow/events.d.ts.map +1 -0
  36. package/dist/workflow/events.js +48 -0
  37. package/dist/workflow/events.js.map +1 -0
  38. package/dist/workflow/interface.d.ts +233 -0
  39. package/dist/workflow/interface.d.ts.map +1 -0
  40. package/dist/workflow/interface.js +20 -0
  41. package/dist/workflow/interface.js.map +1 -0
  42. package/dist/workflow/types.d.ts +131 -0
  43. package/dist/workflow/types.d.ts.map +1 -0
  44. package/dist/workflow/types.js +17 -0
  45. package/dist/workflow/types.js.map +1 -0
  46. package/dist/workspace/filename.d.ts +33 -0
  47. package/dist/workspace/filename.d.ts.map +1 -0
  48. package/dist/workspace/filename.js +98 -0
  49. package/dist/workspace/filename.js.map +1 -0
  50. package/dist/workspace/frontmatter.d.ts +23 -0
  51. package/dist/workspace/frontmatter.d.ts.map +1 -0
  52. package/dist/workspace/frontmatter.js +48 -0
  53. package/dist/workspace/frontmatter.js.map +1 -0
  54. package/dist/workspace/kb-layout.d.ts +66 -0
  55. package/dist/workspace/kb-layout.d.ts.map +1 -0
  56. package/dist/workspace/kb-layout.js +63 -0
  57. package/dist/workspace/kb-layout.js.map +1 -0
  58. package/dist/workspace/types.d.ts +57 -0
  59. package/dist/workspace/types.d.ts.map +1 -0
  60. package/dist/workspace/types.js +2 -0
  61. package/dist/workspace/types.js.map +1 -0
  62. package/package.json +41 -0
  63. package/src/auth/types.ts +16 -0
  64. package/src/chat/types.ts +53 -0
  65. package/src/git/branchAuthor.ts +46 -0
  66. package/src/git/pr.types.ts +274 -0
  67. package/src/git/protected.ts +121 -0
  68. package/src/git/review.types.ts +51 -0
  69. package/src/git/types.ts +170 -0
  70. package/src/index.ts +23 -0
  71. package/src/workflow/events.ts +271 -0
  72. package/src/workflow/interface.ts +357 -0
  73. package/src/workflow/types.ts +156 -0
  74. package/src/workspace/filename.ts +102 -0
  75. package/src/workspace/frontmatter.ts +45 -0
  76. package/src/workspace/kb-layout.ts +81 -0
  77. package/src/workspace/types.ts +59 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Workflow vocabulary — the abstraction our app speaks instead of raw git/gh.
3
+ *
4
+ * Most types here alias existing engineering types in `@bevel-software/platform-shared/git/*` so
5
+ * the workflow layer is a single source of truth without duplicating the
6
+ * payload shapes our backend already produces. The point of the aliases is the
7
+ * *name*: callers depend on `Change` / `ChangeRequest` / `Branch`, never on
8
+ * `Commit` / `PullRequest` / `Branch` from the git module. When we eventually
9
+ * collapse `modules/git` and `modules/review-workflow` into `modules/workflow`,
10
+ * the alias targets move with the implementation; the consumer-facing names
11
+ * don't need to.
12
+ *
13
+ * New types that have no git counterpart (file locks, change-request open
14
+ * inputs, conflict-resolution payloads) live here directly.
15
+ */
16
+
17
+ import type {
18
+ BranchInfo,
19
+ CommitAttribution,
20
+ ShareChangesRequest,
21
+ WorkingTreeStatus,
22
+ } from '../git/types.js';
23
+ import type {
24
+ CancelPrResult,
25
+ FileApprovalState,
26
+ MergePrResult,
27
+ PullRequestDetail,
28
+ PullRequestState,
29
+ PullRequestSummary,
30
+ PostPrCommentInput,
31
+ PrReviewComment,
32
+ PullRequestFile,
33
+ } from '../git/pr.types.js';
34
+
35
+ // ── Branches ─────────────────────────────────────────────────────────────────
36
+
37
+ /** A variation of the ontology. Protected or not. */
38
+ export type Branch = BranchInfo;
39
+
40
+ /**
41
+ * Snapshot of the workspace tied to a branch — currently-checked-out branch,
42
+ * dirty bit, unshared commit count. Mirrors `WorkingTreeStatus` from the git
43
+ * module. Renames to `BranchWorkspaceStatus` in the workflow domain to signal
44
+ * "this describes the branch's workspace state", not raw git plumbing.
45
+ */
46
+ export type BranchWorkspaceStatus = WorkingTreeStatus;
47
+
48
+ // ── Changes ──────────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * A change is the atomic unit of history. Each change affects exactly one file.
52
+ * Creating an empty folder is modeled as a change creating a `.gitkeep` in it.
53
+ * Renames are a single change touching one path with the old path removed in
54
+ * the same atomic operation. Aliases `CommitAttribution` because that's
55
+ * exactly the shape the backend already returns for a single commit.
56
+ */
57
+ export type Change = CommitAttribution;
58
+
59
+ /**
60
+ * Input shape for committing a change. `summary` is required and single-line;
61
+ * `description` is optional and supports a multi-line body. Aliases
62
+ * `ShareChangesRequest` for now — once the workflow module owns the validator
63
+ * gate end-to-end, this shape may grow workflow-specific fields (lock token,
64
+ * forced single-path scope).
65
+ */
66
+ export type ChangeInput = ShareChangesRequest;
67
+
68
+ // ── File locks (new — no git counterpart) ────────────────────────────────────
69
+
70
+ /**
71
+ * A live edit lock on one (branch, path). Per the workflow spec:
72
+ * - acquired on first edit in edit mode, or on agent write
73
+ * - per-(branch, path); the same file on another branch is independently lockable
74
+ * - autosave checkpoint timer extends the lock and produces intermediate changes
75
+ * - stale locks (no heartbeat past TTL) auto-release so a disconnected client
76
+ * can't hold a file hostage
77
+ *
78
+ * Holder identity carries both `userId` (the join key against the users table)
79
+ * and `holderName` (display name, denormalised so consumers can render a
80
+ * "locked by Alice" badge without an extra round-trip). For agent-driven
81
+ * writes the holder is the user driving the agent.
82
+ */
83
+ export interface FileLock {
84
+ branch: string;
85
+ path: string;
86
+ holderUserId: string;
87
+ holderName: string;
88
+ /** ISO timestamp when the lock was first acquired. */
89
+ acquiredAt: string;
90
+ /** ISO timestamp of the most recent heartbeat — the autosave/keepalive ping. */
91
+ lastHeartbeatAt: string;
92
+ /** ISO timestamp after which a non-heartbeated lock is considered stale and may be reclaimed. */
93
+ expiresAt: string;
94
+ }
95
+
96
+ /**
97
+ * Result of attempting to acquire a lock. `acquired: false` carries the
98
+ * current holder so the UI can render "Locked by X" without a follow-up call.
99
+ */
100
+ export type AcquireLockResult =
101
+ | { acquired: true; lock: FileLock }
102
+ | { acquired: false; lock: FileLock };
103
+
104
+ // ── Change Requests ──────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * A change request is the reconciliation of two branches. In the current
108
+ * implementation this is backed by a GitHub pull request; the alias hides
109
+ * that from callers. `state` values map 1:1 (`open`/`merged`/`closed`).
110
+ *
111
+ * Opening a change request is defined in the spec as auto-merging target →
112
+ * source first; that part isn't yet implemented in the backing services, so
113
+ * `openChangeRequest` on `IWorkflowService` currently throws
114
+ * `NotImplementedWorkflowError`.
115
+ */
116
+ export type ChangeRequest = PullRequestSummary;
117
+ export type ChangeRequestDetail = PullRequestDetail;
118
+ export type ChangeRequestState = PullRequestState;
119
+ export type ChangedFile = PullRequestFile;
120
+
121
+ export type FileApproval = FileApprovalState;
122
+ export type MergeChangeRequestResult = MergePrResult;
123
+ export type CancelChangeRequestResult = CancelPrResult;
124
+ export type ChangeRequestComment = PrReviewComment;
125
+ export type PostChangeRequestCommentInput = PostPrCommentInput;
126
+
127
+ /**
128
+ * Input for opening a new change request. The workflow auto-merges
129
+ * `targetBranch` into `sourceBranch` as part of opening, so callers don't
130
+ * pass a head SHA — the workflow resolves the head itself.
131
+ *
132
+ * `title` + `description` are the change request's metadata; the description
133
+ * is plain text (no GitHub-flavoured markdown affordances exposed at this
134
+ * layer). The implementation may still render this as the PR body when the
135
+ * underlying GitHub PR is created.
136
+ */
137
+ export interface OpenChangeRequestInput {
138
+ sourceBranch: string;
139
+ targetBranch: string;
140
+ title: string;
141
+ description?: string;
142
+ }
143
+
144
+ /**
145
+ * Result of asking the workflow to merge a change request. Mirrors
146
+ * `MergePrResult` — caller learns the resulting change (commit) sha and the
147
+ * timestamp the merge landed.
148
+ *
149
+ * When the merge attempt hits conflicts the workflow falls back to an
150
+ * agent-driven resolution path; the caller does NOT get a `MergeChangeRequestResult`
151
+ * back in that case — they get a `ConflictPending` shape instead so the UI
152
+ * can route into the resolution flow.
153
+ */
154
+ export type MergeChangeRequestOutcome =
155
+ | { kind: 'merged'; result: MergeChangeRequestResult }
156
+ | { kind: 'conflicts-need-resolution'; conflictedPaths: string[] };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Cross-platform filename validation. A name that passes here must be writable
3
+ * as-is on Windows (NTFS), macOS (APFS), and Linux (ext4) — so the same
4
+ * workspace can round-trip between dev machines, the backend container, and
5
+ * any user's local clone without `git checkout` failing on a forbidden name.
6
+ *
7
+ * Used by:
8
+ * - frontend: gate the create / rename inputs in FileExplorer
9
+ * - backend: reject API requests at the workspace service boundary
10
+ * - agent prompt: the same rules are described to the LLM so it picks valid
11
+ * names when authoring KB files
12
+ *
13
+ * Keep this file dependency-free — both bundle targets import it directly.
14
+ */
15
+
16
+ /** Windows-reserved device names. Case-insensitive; the bare name AND
17
+ * `<name>.<ext>` are both reserved (e.g. `NUL.txt` is invalid on Windows). */
18
+ const WINDOWS_RESERVED_NAMES = new Set([
19
+ 'CON', 'PRN', 'AUX', 'NUL',
20
+ 'COM0', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
21
+ 'LPT0', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
22
+ ]);
23
+
24
+ /** Characters Windows forbids in any filename component. `/` is the path
25
+ * separator on Unix, included for the same reason. `\` is also a path
26
+ * separator on Windows. Control chars (0x00–0x1F) are rejected separately. */
27
+ // eslint-disable-next-line no-control-regex
28
+ const FORBIDDEN_CHARS = /[<>:"/\\|?*\x00-\x1F]/;
29
+
30
+ /** Per-component byte limit: NTFS = 255 UTF-16 units, ext4 = 255 bytes,
31
+ * APFS = 255 UTF-8 bytes. Use UTF-8 bytes — the strictest of the three. */
32
+ const MAX_NAME_BYTES = 255;
33
+
34
+ const utf8 = new TextEncoder();
35
+
36
+ /**
37
+ * Validate a single path component (file or folder name — no `/`, no `\`).
38
+ * Returns `null` if valid, or a short reason string suitable for surfacing
39
+ * in a UI error or API 400 body.
40
+ */
41
+ export function validateFilename(name: string): string | null {
42
+ if (typeof name !== 'string') return 'Name is required';
43
+ if (name.length === 0) return 'Name cannot be empty';
44
+ if (name !== name.trim()) return 'Name cannot start or end with whitespace';
45
+ if (name === '.' || name === '..') return 'Name cannot be "." or ".."';
46
+
47
+ if (FORBIDDEN_CHARS.test(name)) {
48
+ return 'Name cannot contain any of these characters: < > : " / \\ | ? *';
49
+ }
50
+
51
+ // Windows trims trailing dots and spaces silently — a name ending in either
52
+ // becomes a different filename on disk, breaking sync between OSes.
53
+ if (name.endsWith('.') || name.endsWith(' ')) {
54
+ return 'Name cannot end with a "." or a space';
55
+ }
56
+
57
+ // Windows reserved device names — match `NAME` and `NAME.ext`, case-insensitive.
58
+ const stem = name.split('.')[0].toUpperCase();
59
+ if (WINDOWS_RESERVED_NAMES.has(stem)) {
60
+ return `"${name}" is a reserved system name on Windows`;
61
+ }
62
+
63
+ if (utf8.encode(name).length > MAX_NAME_BYTES) {
64
+ return `Name is too long (max ${MAX_NAME_BYTES} bytes)`;
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Validate a workspace-relative path: a `/`-separated string of components.
72
+ * Each component is checked with {@link validateFilename}. Backslashes are
73
+ * rejected outright — the workspace API uses `/` everywhere and accepting
74
+ * `\` would round-trip differently on Windows vs the Linux container.
75
+ */
76
+ export function validateRelativePath(relativePath: string): string | null {
77
+ if (typeof relativePath !== 'string' || relativePath.length === 0) {
78
+ return 'Path is required';
79
+ }
80
+ if (relativePath.includes('\\')) {
81
+ return 'Path cannot contain "\\" — use "/" as the separator';
82
+ }
83
+ // Strip a single leading `./` so callers that build paths from a base dir
84
+ // (`./<name>`) don't get a spurious "." segment rejection.
85
+ const normalized = relativePath.replace(/^\.\//, '');
86
+ const segments = normalized.split('/').filter((s) => s.length > 0);
87
+ if (segments.length === 0) return 'Path is required';
88
+ for (const segment of segments) {
89
+ const reason = validateFilename(segment);
90
+ if (reason) return `Invalid path segment "${segment}": ${reason}`;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Throwing wrapper for the backend service layer — keeps call sites a single
97
+ * line and produces a clear `Error` the route handler can surface as a 400.
98
+ */
99
+ export function assertValidRelativePath(relativePath: string): void {
100
+ const reason = validateRelativePath(relativePath);
101
+ if (reason) throw new Error(`Invalid path: ${reason}`);
102
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The ONE `---` frontmatter splitter, shared by backend and frontend so no file
3
+ * type grows its own regex. Splits a leading `---`-fenced YAML block from the
4
+ * body; null when the text doesn't open with a fence. Parsing the YAML inside is
5
+ * the caller's concern (the access-control resolver deliberately keeps its own
6
+ * hardened line-based reader — see `modules/access/access-splice.ts`).
7
+ */
8
+ export function extractFrontmatter(text: string): { frontmatter: string; body: string } | null {
9
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
10
+ if (!m) return null;
11
+ return { frontmatter: m[1], body: m[2] };
12
+ }
13
+
14
+ /**
15
+ * Set a top-level scalar key in a file's `---` frontmatter, LINE-based: replace an
16
+ * existing `<key>:` line's value, else insert `<key>: <value>` at the top of the
17
+ * block (creating a `---` block if the file has none). Deliberately NOT a
18
+ * parse-and-re-serialize — a KB node's `nodeType:` quoted-markdown-link and access
19
+ * lines are read by a hand-rolled regex parser that a full YAML re-emit would
20
+ * corrupt, so every other line is preserved byte-for-byte. `value` is emitted as a
21
+ * bare scalar; callers pass simple id/name strings (no quoting/escaping needed for
22
+ * the `[a-z0-9_-]` id grammar).
23
+ */
24
+ export function setFrontmatterField(text: string, key: string, value: string): string {
25
+ const fm = extractFrontmatter(text);
26
+ const line = `${key}: ${value}`;
27
+ // Escape the key before interpolating — callers pass plain `id`/`name`, but a
28
+ // metacharacter must never silently change what the line-matcher matches.
29
+ const keyRe = new RegExp(`^(\\s*)${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:.*$`);
30
+ // Preserve the file's own line-ending convention: rebuilding a CRLF file with
31
+ // bare `\n` would dirty every frontmatter line, breaking the byte-for-byte
32
+ // guarantee this helper exists for. Judged from the FIRST line ending (the
33
+ // fence line's own), so a stray CRLF deep in the body can't flip the block.
34
+ const firstNl = text.indexOf('\n');
35
+ const eol = firstNl > 0 && text[firstNl - 1] === '\r' ? '\r\n' : '\n';
36
+ if (!fm) {
37
+ // No frontmatter — prepend a fresh block, keeping the original body intact.
38
+ return `---${eol}${line}${eol}---${eol}${text}`;
39
+ }
40
+ const fmLines = fm.frontmatter.split(/\r?\n/);
41
+ const idx = fmLines.findIndex((l) => keyRe.test(l));
42
+ if (idx >= 0) fmLines[idx] = line;
43
+ else fmLines.unshift(line);
44
+ return `---${eol}${fmLines.join(eol)}${eol}---${eol}${fm.body}`;
45
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Top-level layout of the KB repo (inside the `KB_DIR_NAME` clone).
3
+ *
4
+ * The repo root holds a small, fixed set of special folders that the app
5
+ * treats distinctly:
6
+ *
7
+ * <kbDirName>/
8
+ * ├── KnowledgeBase/ ← all team ontologies live here (the knowledge graph)
9
+ * ├── Data/ ← agent-produced records; parsed like KnowledgeBase/
10
+ * ├── Agents/ ← .agent files — agent role configurations (not the graph)
11
+ * ├── Pipelines/ ← .pipeline files — execution-layer processes (not the graph)
12
+ * ├── Skills/ ← reusable agent skills (NOT part of the graph)
13
+ * ├── Tools/ ← user-authored tool manuals (*.tool; not the graph)
14
+ * ├── roles.yaml ← identity → role mapping
15
+ * └── access.md ← repo-root access-control rules
16
+ *
17
+ * These names are the single source of truth for both sides of the app:
18
+ * - Backend: the graph parser discovers ontologies under the
19
+ * {@link ONTOLOGY_ROOTS} (`KnowledgeBase/` and `Data/`); `Skills/`,
20
+ * `Tools/`, `Agents/`, `Pipelines/` (and anything else at the root) are
21
+ * ignored by parsing, validation, and the diagram.
22
+ * - Frontend: the file tree renders these root folders as distinct
23
+ * top-level sections.
24
+ *
25
+ * Don't hard-code these strings elsewhere — import them from here.
26
+ */
27
+
28
+ /** Folder under the repo root that contains all team ontologies. */
29
+ export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
30
+
31
+ /** Folder under the repo root that holds reusable agent skills (not graph nodes). */
32
+ export const SKILLS_DIR = 'Skills';
33
+
34
+ /**
35
+ * Folder under the repo root that holds user-authored tool manuals (`*.tool`
36
+ * files). Each `.tool` is a UTCP manual (inline / http / mcp) that the MCP/UTCP
37
+ * endpoint loads for any user who can read it (access-controlled like Skills).
38
+ * Not part of the knowledge graph.
39
+ */
40
+ export const TOOLS_DIR = 'Tools';
41
+
42
+ /**
43
+ * Folder under the repo root for agent-produced records (pipeline instances,
44
+ * work items, intermediate outputs). Parsed exactly like `KnowledgeBase/`:
45
+ * its direct subfolders are self-contained ontologies.
46
+ */
47
+ export const DATA_DIR = 'Data';
48
+
49
+ /** Folder under the repo root that holds `.agent` files — agent role configurations (not graph nodes). */
50
+ export const AGENTS_DIR = 'Agents';
51
+
52
+ /** Folder under the repo root that holds `.pipeline` files — execution-layer processes (not graph nodes). */
53
+ export const PIPELINES_DIR = 'Pipelines';
54
+
55
+ /**
56
+ * The roots whose subfolders are discovered as ontologies by the graph parser
57
+ * (each subfolder with both `NodeTypes/` and `Knowledge/` is an ontology).
58
+ */
59
+ export const ONTOLOGY_ROOTS: readonly string[] = [KNOWLEDGE_BASE_DIR, DATA_DIR];
60
+
61
+ /** The `Knowledge/` marker subfolder of an ontology (holds the graph nodes). */
62
+ export const KNOWLEDGE_DIR = 'Knowledge';
63
+
64
+ /** The `NodeTypes/` marker subfolder of an ontology (holds the type definitions). */
65
+ export const NODETYPE_DIR = 'NodeTypes';
66
+
67
+ /** The marker subfolders that make a directory an ontology (it needs BOTH). */
68
+ export const ONTOLOGY_MARKERS = new Set([KNOWLEDGE_DIR, NODETYPE_DIR]);
69
+
70
+ /**
71
+ * A named ontology id, or `null` for the neutral bucket — content that belongs
72
+ * to no named ontology (root config, `Skills/`, root-level `Knowledge/`, etc.).
73
+ * The named id is the repo-root-relative path of the ontology directory,
74
+ * e.g. `KnowledgeBase/Product` or `KnowledgeBase/IT Architecture`.
75
+ */
76
+ export type Ontology = string | null;
77
+
78
+ // The implementation that resolves a path to its `Ontology` (`ontologyOf`) is
79
+ // backend-only — it lives in `packages/backend/src/shared/kb-layout.ts`, built
80
+ // from the `KNOWLEDGE_BASE_DIR` / `ONTOLOGY_MARKERS` constants above. This
81
+ // package holds only the cross-cutting constants and types, not logic.
@@ -0,0 +1,59 @@
1
+ export interface WorkspaceInfo {
2
+ /**
3
+ * Workspace id = `encodeURIComponent(branch)`. Stable across users — a
4
+ * branch's workspace is shared by everyone editing it (PLAN §3).
5
+ * Decoding rule: callers can derive the branch via
6
+ * `decodeURIComponent(id)` without a backend round-trip.
7
+ */
8
+ id: string;
9
+ /** Display name. Currently the branch name verbatim. */
10
+ name: string;
11
+ /** Absolute path on the backend's filesystem (debug / log surface only). */
12
+ absolutePath: string;
13
+ /** ISO timestamp. Per-branch workspaces have no meaningful creation time; epoch is fine. */
14
+ createdAt: string;
15
+ /**
16
+ * Directory name (relative to `absolutePath`) where the KB clone lives.
17
+ * Configured via `KB_DIR_NAME` on the backend; surfaced to the frontend
18
+ * so renderers and chat cards can build correct KB-relative paths
19
+ * without hardcoding the repo name.
20
+ */
21
+ kbDirName: string;
22
+ }
23
+
24
+ export interface FileTreeEntry {
25
+ name: string;
26
+ relativePath: string;
27
+ type: 'file' | 'directory';
28
+ children?: FileTreeEntry[];
29
+ }
30
+
31
+ import type { AuthUser } from '../auth/types.js';
32
+
33
+ export interface IWorkspaceService {
34
+ /**
35
+ * Get-or-create the workspace for a branch. Workspaces are per-branch
36
+ * (PLAN §3) — every user editing `branch` shares the same on-disk clone,
37
+ * coordinated by the file-lock service.
38
+ */
39
+ getOrCreateForBranch(branch: string): Promise<WorkspaceInfo>;
40
+ /**
41
+ * Convenience wrapper that defaults to the configured default branch
42
+ * (`DEFAULT_BRANCH`) when no branch is provided. Kept on the interface so route handlers that
43
+ * receive a logged-in user but no explicit branch can still bootstrap a
44
+ * workspace.
45
+ */
46
+ getOrCreateForUser(user: AuthUser, branch?: string): Promise<WorkspaceInfo>;
47
+ getMetadata(workspaceId: string): Promise<WorkspaceInfo | null>;
48
+ deleteWorkspace(workspaceId: string): Promise<void>;
49
+ listFiles(workspaceId: string): Promise<FileTreeEntry>;
50
+ readFile(workspaceId: string, relativePath: string): Promise<string>;
51
+ writeFile(workspaceId: string, relativePath: string, content: string): Promise<void>;
52
+ createDirectory(workspaceId: string, relativePath: string): Promise<void>;
53
+ writeFileBinary(workspaceId: string, relativePath: string, data: Uint8Array): Promise<void>;
54
+ deleteFile(workspaceId: string, relativePath: string): Promise<void>;
55
+ moveEntry(workspaceId: string, oldRelativePath: string, newRelativePath: string): Promise<void>;
56
+ getWorkspacePath(workspaceId: string): Promise<string>;
57
+ /** Remove the workspace's transient `tmp/` dir (staged docs, tool-chain spills). */
58
+ clearTmp(workspaceId: string): Promise<void>;
59
+ }