@ctrl-spc/cli 1.1.14 → 1.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.
Files changed (42) hide show
  1. package/dist/agents.js +54 -0
  2. package/dist/collision.js +44 -0
  3. package/dist/commands/fetch.js +213 -0
  4. package/dist/commands/init.js +457 -0
  5. package/dist/commands/link.js +152 -0
  6. package/dist/commands/login.js +332 -0
  7. package/dist/commands/logout.js +14 -0
  8. package/dist/commands/run.js +622 -0
  9. package/dist/config.js +68 -0
  10. package/dist/env.js +5 -0
  11. package/dist/git.js +70 -0
  12. package/dist/index.js +56 -72
  13. package/dist/mcp.js +1732 -0
  14. package/dist/presence.js +62 -0
  15. package/dist/prompt.js +47 -0
  16. package/dist/protocol.js +117 -0
  17. package/dist/skills.js +148 -0
  18. package/dist/supabase.js +40 -10
  19. package/dist/topology.js +602 -0
  20. package/package.json +25 -23
  21. package/bin/ctrl-spc.js +0 -6
  22. package/dist/auth.js +0 -172
  23. package/dist/controlRequests.js +0 -154
  24. package/dist/daemon.js +0 -152
  25. package/dist/devices.js +0 -64
  26. package/dist/flagAssembler.js +0 -29
  27. package/dist/init.js +0 -79
  28. package/dist/launchd.js +0 -154
  29. package/dist/lifecycle.js +0 -37
  30. package/dist/mcpConfig.js +0 -12
  31. package/dist/repo.js +0 -52
  32. package/dist/session.js +0 -58
  33. package/dist/setup.js +0 -60
  34. package/dist/spawner.js +0 -138
  35. package/dist/windows-service.js +0 -142
  36. package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.d.ts +0 -24
  37. package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.js +0 -32
  38. package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.d.ts +0 -2
  39. package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.js +0 -21
  40. package/node_modules/@ctrl-spc/mcp-server/dist/index.d.ts +0 -1
  41. package/node_modules/@ctrl-spc/mcp-server/dist/index.js +0 -22
  42. package/node_modules/@ctrl-spc/mcp-server/package.json +0 -38
package/dist/env.js ADDED
@@ -0,0 +1,5 @@
1
+ /** Baked-in Supabase project defaults, overridable per Global Constraints. */
2
+ export const SUPABASE_URL = process.env.CTRL_SPC_SUPABASE_URL || 'https://gdioapkjgnlddsutysne.supabase.co';
3
+ export const SUPABASE_KEY = process.env.CTRL_SPC_SUPABASE_KEY || 'sb_publishable_izf3buHACC2LJKm2JH96YQ_safmL1k0';
4
+ /** MCP server port (Task 4 binds here); override via `CTRL_SPC_PORT`. */
5
+ export const MCP_PORT = Number(process.env.CTRL_SPC_PORT) || 4571;
package/dist/git.js ADDED
@@ -0,0 +1,70 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ /**
3
+ * Normalizes a git remote URL to a canonical `host/path` key (no scheme,
4
+ * lowercase throughout — hosts are case-insensitive and GitHub/GitLab paths
5
+ * redirect case-insensitively, so one repo should always map to one key).
6
+ * Non-default ports are kept (`host:port/path`); 22/443/80 are stripped.
7
+ *
8
+ * Verbatim from the task brief (spec §11 open item):
9
+ * git@github.com:User/Repo.git -> github.com/user/repo
10
+ * https://github.com/User/Repo/ -> github.com/user/repo
11
+ * ssh://git@Host.com:2222/x/y.git -> host.com:2222/x/y
12
+ * https://user:pass@host/x/y.git -> host/x/y
13
+ */
14
+ export function normalizeRemoteUrl(raw) {
15
+ let s = raw.trim();
16
+ const scp = /^(?:[^@/]+@)([^:/]+):(?!\/\/)(.+)$/.exec(s); // scp-like git@host:path
17
+ if (scp)
18
+ s = `${scp[1]}/${scp[2]}`;
19
+ else {
20
+ s = s.replace(/^[a-z+]+:\/\//i, ''); // scheme
21
+ s = s.replace(/^[^@/]+(?::[^@/]*)?@/, ''); // user[:pass]@
22
+ }
23
+ s = s.replace(/\/+$/, '').replace(/\.git$/i, '');
24
+ s = s.replace(/^([^/:]+):(22|443|80)(?=\/)/, '$1'); // default ports
25
+ return s.toLowerCase();
26
+ }
27
+ /**
28
+ * Runs `git remote get-url origin` in `cwd`. Returns `null` if there's no
29
+ * `origin` remote, `cwd` isn't a git repository, or `git` isn't on PATH —
30
+ * callers treat all of those the same way ("no remote here").
31
+ */
32
+ export function currentRepoRemote(cwd) {
33
+ try {
34
+ const out = execFileSync('git', ['remote', 'get-url', 'origin'], {
35
+ cwd,
36
+ encoding: 'utf8',
37
+ stdio: ['ignore', 'pipe', 'ignore'],
38
+ });
39
+ const trimmed = out.trim();
40
+ return trimmed || null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Looks up the project registered for the current repo's git remote, scoped
48
+ * to the signed-in user's orgs by RLS (`projects_select` only surfaces rows
49
+ * in orgs the user is a member of). Returns `null` when there's no remote
50
+ * here, or no project the user can see is keyed on it. Throws on a genuine
51
+ * query/network error — that's distinct from "not registered" and callers
52
+ * (`init`/`link`) must not treat it as such.
53
+ */
54
+ export async function resolveProject(client, cwd) {
55
+ const remote = currentRepoRemote(cwd);
56
+ if (!remote)
57
+ return null;
58
+ const key = normalizeRemoteUrl(remote);
59
+ const { data, error } = await client
60
+ .from('projects')
61
+ .select('id, org_id, name, organizations(name)')
62
+ .eq('git_remote_url', key)
63
+ .maybeSingle();
64
+ if (error)
65
+ throw new Error(`Failed to look up project for ${key}: ${error.message}`);
66
+ if (!data)
67
+ return null;
68
+ const row = data;
69
+ return { id: row.id, orgId: row.org_id, name: row.name, orgName: row.organizations?.name ?? '' };
70
+ }
package/dist/index.js CHANGED
@@ -1,74 +1,58 @@
1
- const [, , command, ...args] = process.argv;
2
- async function loadDaemonControl() {
3
- const mod = process.platform === 'darwin'
4
- ? await import('./launchd.js')
5
- : await import('./windows-service.js');
6
- return {
7
- installDaemon: mod.installDaemon,
8
- restartDaemon: mod.restartDaemon,
9
- stopDaemon: mod.stopDaemon,
10
- isDaemonInstalled: mod.isDaemonInstalled,
11
- };
12
- }
13
- async function lifecycleDeps() {
14
- const { hostname } = await import('node:os');
15
- const { createSessionClient, upsertDevice, setDeviceStatus } = await import('./devices.js');
16
- return {
17
- createSession: createSessionClient,
18
- upsertDevice,
19
- setDeviceStatus,
20
- daemon: await loadDaemonControl(),
21
- label: hostname(),
22
- log: (msg) => console.log(msg),
23
- };
24
- }
25
- async function main() {
26
- if (command === 'setup') {
27
- const { runSetup } = await import('./setup.js');
28
- await runSetup();
29
- return;
30
- }
31
- if (command === 'daemon') {
32
- const { runDaemon } = await import('./daemon.js');
33
- await runDaemon();
34
- return;
35
- }
36
- if (command === 'init') {
37
- const { runInit } = await import('./init.js');
38
- await runInit(args);
39
- return;
40
- }
41
- if (command === 'serve') {
42
- const { startMcpServer } = await import('@ctrl-spc/mcp-server');
43
- await startMcpServer();
44
- return;
1
+ #!/usr/bin/env node
2
+ import { login } from './commands/login.js';
3
+ import { logout } from './commands/logout.js';
4
+ import { init } from './commands/init.js';
5
+ import { link } from './commands/link.js';
6
+ import { fetchRepository } from './commands/fetch.js';
7
+ import { run } from './commands/run.js';
8
+ export const HELP_TEXT = `ctrl-spc — CTRL+SPC CLI
9
+
10
+ Usage:
11
+ ctrl-spc login Sign in via the browser and store a session
12
+ ctrl-spc logout Remove the stored session from this machine
13
+ ctrl-spc init Discover repositories and explicitly group them into projects
14
+ ctrl-spc link Reconcile a project's full topology after grouping changes
15
+ [--project <project-id>]
16
+ ctrl-spc fetch Clone and link one unavailable project repository
17
+ [repository-id] [--project <project-id>] [--destination <path>] [--yes]
18
+ ctrl-spc Run the local agent process (presence + MCP server)
19
+ ctrl-spc --help Show this help text
20
+ `;
21
+ export async function main(argv = process.argv.slice(2)) {
22
+ const command = argv[0];
23
+ switch (command) {
24
+ case 'login':
25
+ await login();
26
+ return;
27
+ case 'logout':
28
+ logout();
29
+ return;
30
+ case 'init':
31
+ await init(argv.slice(1));
32
+ return;
33
+ case 'link':
34
+ await link(argv.slice(1));
35
+ return;
36
+ case 'fetch':
37
+ await fetchRepository(argv.slice(1));
38
+ return;
39
+ case '--help':
40
+ case '-h':
41
+ console.log(HELP_TEXT);
42
+ return;
43
+ case undefined:
44
+ await run();
45
+ return;
46
+ default:
47
+ console.error(`Unknown command: ${command}\n`);
48
+ console.log(HELP_TEXT);
49
+ process.exitCode = 1;
50
+ return;
45
51
  }
46
- if (command === 'status') {
47
- const { runStatus } = await import('./lifecycle.js');
48
- const deps = await lifecycleDeps();
49
- await runStatus({ createSession: deps.createSession, daemon: deps.daemon, log: deps.log });
50
- return;
51
- }
52
- if (command === 'connect') {
53
- const { runConnect } = await import('./lifecycle.js');
54
- await runConnect(await lifecycleDeps());
55
- return;
56
- }
57
- if (command === 'reconnect') {
58
- const { runReconnect } = await import('./lifecycle.js');
59
- await runReconnect(await lifecycleDeps());
60
- return;
61
- }
62
- if (command === 'disconnect') {
63
- const { runDisconnect } = await import('./lifecycle.js');
64
- await runDisconnect(await lifecycleDeps());
65
- return;
66
- }
67
- console.error('Usage: ctrl-spc <setup|status|connect|reconnect|disconnect|init|serve>');
68
- process.exitCode = 1;
69
52
  }
70
- main().catch((err) => {
71
- console.error(err instanceof Error ? err.message : 'Unexpected ctrl-spc error');
72
- process.exit(1);
73
- });
74
- export {};
53
+ if (!process.env.VITEST) {
54
+ main().catch((err) => {
55
+ console.error(err instanceof Error ? err.message : String(err));
56
+ process.exit(1);
57
+ });
58
+ }