@ctrl-spc/cs 0.1.0 → 0.2.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.
@@ -0,0 +1,108 @@
1
+ import { hostedRemoteIdentity } from './git-remote.js';
2
+ /** Raised when a folder's git remote isn't a real hosted repository URL, so it
3
+ * can't become a codebase. The route surfaces it as a 400 refusal. */
4
+ export class NotHostedRemoteError extends Error {
5
+ constructor(message = "This folder's git remote isn't a hosted repository URL, so it can't become a codebase.") {
6
+ super(message);
7
+ this.name = 'NotHostedRemoteError';
8
+ }
9
+ }
10
+ /**
11
+ * The codebases registered on one project.
12
+ *
13
+ * Rows live in the isolated `cliv2_codebases` table (org-scoped RLS mirroring
14
+ * `public.projects`, so this returns every codebase on the project — a codebase
15
+ * is shared project state visible to every member of the project's org, not a
16
+ * per-user list). `project_id` references a shared `projects` row by value with
17
+ * no FK — the v2 surface stays decoupled from v1-owned schema. Only path-free
18
+ * identities (canonical remote + derived name) are stored, never an absolute
19
+ * local path (each user's local folder path stays in the CLI's local config).
20
+ */
21
+ export async function listCodebases(client, projectId) {
22
+ const { data, error } = await client
23
+ .from('cliv2_codebases')
24
+ .select('id, git_remote_url, name')
25
+ .eq('project_id', projectId)
26
+ .order('created_at', { ascending: true });
27
+ if (error)
28
+ throw new Error(error.message);
29
+ return (data ?? []).map((row) => ({
30
+ id: row.id,
31
+ gitRemoteUrl: row.git_remote_url,
32
+ name: row.name,
33
+ }));
34
+ }
35
+ /**
36
+ * A display name from a canonical `host/path` remote: its last `/` segment.
37
+ * E.g. `github.com/acme/web` → `web`. Throws rather than ever returning an
38
+ * empty name (the table's `name` column is `not null`, non-blank).
39
+ */
40
+ export function deriveCodebaseName(identity) {
41
+ const name = identity.split('/').pop()?.trim() ?? '';
42
+ if (!name)
43
+ throw new Error(`Could not derive a codebase name from "${identity}".`);
44
+ return name;
45
+ }
46
+ /**
47
+ * Add a codebase to a project by its git remote URL. The raw remote is
48
+ * canonicalized to `host/path` (stripping scheme, credentials, `.git`, trailing
49
+ * slash) BEFORE insert, so only a path-free, shareable identity reaches the
50
+ * cloud and the unique dedupe key actually holds ("same remote = same
51
+ * codebase"). A remote that isn't a real hosted repo is refused
52
+ * (NotHostedRemoteError) and never inserted. `added_by` defaults to auth.uid().
53
+ *
54
+ * Returns `{ added: false }` on a unique-violation — the dedupe key is now
55
+ * (project_id, git_remote_url), so this fires when ANY org member has already
56
+ * added this remote to the project — a no-op dedupe, never surfaced as an error.
57
+ */
58
+ export async function addCodebase(client, projectId, gitRemoteUrl) {
59
+ const identity = hostedRemoteIdentity(gitRemoteUrl);
60
+ if (!identity)
61
+ throw new NotHostedRemoteError();
62
+ const name = deriveCodebaseName(identity);
63
+ const { error } = await client
64
+ .from('cliv2_codebases')
65
+ .insert({ project_id: projectId, git_remote_url: identity, name });
66
+ if (error) {
67
+ if (error.code === '23505')
68
+ return { added: false }; // dedupe: already on the project
69
+ throw new Error(error.message);
70
+ }
71
+ return { added: true };
72
+ }
73
+ /**
74
+ * Remove a codebase from its project — Phase 3. Deletes the ORG-SHARED
75
+ * `cliv2_codebases` row by id, so the codebase disappears for every member of
76
+ * the project's org (its RLS `using` = org membership, so any member may delete;
77
+ * no extra scoping needed here).
78
+ *
79
+ * Deliberately narrow: this deletes ONLY the codebase row. It does NOT touch
80
+ * per-machine availability (`cliv2_codebase_locations`) or the local folder
81
+ * mapping (`codebase-paths.json`) — those are per-repo truth, kept so a later
82
+ * re-add of the same remote shows this computer located again for free.
83
+ */
84
+ export async function removeCodebase(client, codebaseId) {
85
+ const { error } = await client.from('cliv2_codebases').delete().eq('id', codebaseId);
86
+ if (error)
87
+ throw new Error(error.message);
88
+ }
89
+ /**
90
+ * Record that THIS machine has a codebase's folder checked out — Phase 2
91
+ * per-machine availability. Upserts one path-free row into the owner-scoped
92
+ * `cliv2_codebase_locations` table on `(user_id, machine_id, git_remote_url)`,
93
+ * refreshing `updated_at`. `user_id` defaults to `auth.uid()` server-side — the
94
+ * client never sets it — and RLS confines the row to the signed-in user's own
95
+ * machines (a user's availability is private, mirroring `cliv2_agents`).
96
+ *
97
+ * `identity` MUST already be a canonical `host/path` remote (the caller
98
+ * re-derives it from the folder via `hostedRemoteIdentity`). An absolute local
99
+ * path is NEVER sent — only the identity + this machine's id, honoring the
100
+ * product's path-privacy invariant.
101
+ */
102
+ export async function reportLocated(client, machineId, identity) {
103
+ const { error } = await client
104
+ .from('cliv2_codebase_locations')
105
+ .upsert({ machine_id: machineId, git_remote_url: identity, updated_at: new Date().toISOString() }, { onConflict: 'user_id,machine_id,git_remote_url' });
106
+ if (error)
107
+ throw new Error(error.message);
108
+ }