@ctrl-spc/cs 0.1.0 → 0.3.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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @ctrl-spc/cs
2
+
3
+ The `cs` command keeps your computer online for **CTRL+SPC** so your coding
4
+ agents can be reached, and opens the **Companion** — a small local app for
5
+ signing in and attaching your codebases to your projects.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i -g @ctrl-spc/cs && cs
11
+ ```
12
+
13
+ That's it. The `&& cs` opens your browser straight to sign-in — you barely touch
14
+ the terminal:
15
+
16
+ ```
17
+ Welcome to CTRL+SPC — opening your browser to sign in…
18
+ ```
19
+
20
+ In the browser you'll:
21
+
22
+ 1. **Sign in** (or sign up on the web).
23
+ 2. **Attach a codebase** — point a project at a folder on this computer that has
24
+ a git remote.
25
+ 3. **Open the web app** — create work items and hand them to an agent.
26
+
27
+ Your folder paths stay on this computer; only path-free identifiers (a git
28
+ remote URL) are shared.
29
+
30
+ ## Stay online
31
+
32
+ ```sh
33
+ cs autostart on
34
+ ```
35
+
36
+ Comes online automatically at login, with no window. Turn it off with
37
+ `cs autostart off`.
38
+
39
+ ## Commands
40
+
41
+ ```
42
+ cs Open the Companion app (the front door)
43
+ cs open Open the Companion app in your browser
44
+ cs login Sign in from the terminal and link this computer
45
+ cs start Come online now, no window (used by auto-start)
46
+ cs status Show sign-in state, computer, and detected agents
47
+ cs autostart on Come online automatically at login
48
+ cs autostart off Stop coming online at login
49
+ cs logout Sign this computer out
50
+ cs help Show this help
51
+ ```
52
+
53
+ ## Requirements
54
+
55
+ - Node.js >= 22.
56
+
57
+ Not sure what a piece does? Open the Companion (`cs`) and click **How it works**.
package/dist/agents.js CHANGED
@@ -37,3 +37,9 @@ function resolve(agent) {
37
37
  export function detectAgents() {
38
38
  return AGENTS.filter((a) => resolve(a) !== null);
39
39
  }
40
+ /** Absolute path to an agent's binary (PATH first, then well-known fallbacks),
41
+ * or null when it isn't installed. Used to invoke `claude mcp add` at the same
42
+ * binary `detectAgents()` found. */
43
+ export function agentPath(agent) {
44
+ return resolve(agent);
45
+ }
@@ -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
+ }