@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 +57 -0
- package/dist/agents.js +6 -0
- package/dist/codebases.js +108 -0
- package/dist/companion-ui.js +1058 -0
- package/dist/companion.js +396 -0
- package/dist/config.js +169 -10
- package/dist/daemon.js +12 -79
- package/dist/env.js +15 -0
- package/dist/folders.js +67 -0
- package/dist/git-remote.js +79 -0
- package/dist/index.js +7 -18
- package/dist/mcp.js +1679 -0
- package/dist/presence.js +206 -0
- package/dist/projects.js +32 -0
- package/dist/supabase.js +21 -1
- package/package.json +4 -2
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
|
+
}
|