@ctrl-spc/cli 1.1.1 → 1.1.3
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/dist/auth.js +30 -7
- package/dist/daemon.js +39 -20
- package/dist/devices.js +52 -54
- package/dist/index.js +56 -2
- package/dist/init.js +79 -0
- package/dist/launchd.js +33 -0
- package/dist/lifecycle.js +37 -0
- package/dist/mcpConfig.js +12 -0
- package/dist/repo.js +52 -0
- package/dist/session.js +5 -3
- package/dist/setup.js +10 -7
- package/dist/supabase.js +2 -1
- package/dist/windows-service.js +35 -0
- package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.d.ts +24 -0
- package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.js +32 -0
- package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.d.ts +2 -0
- package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.js +21 -0
- package/node_modules/@ctrl-spc/mcp-server/dist/index.d.ts +1 -0
- package/node_modules/@ctrl-spc/mcp-server/dist/index.js +22 -0
- package/node_modules/@ctrl-spc/mcp-server/package.json +38 -0
- package/package.json +8 -1
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { createSupabaseClient } from './supabase.js';
|
|
3
|
+
import { createSupabaseClient, SUPABASE_ANON_KEY, SUPABASE_FUNCTIONS_URL } from './supabase.js';
|
|
4
4
|
import { getOrCreateMachineId, saveMachineState } from './session.js';
|
|
5
5
|
const CALLBACK_HOST = '127.0.0.1';
|
|
6
6
|
const CALLBACK_PORT = 54321;
|
|
@@ -47,6 +47,21 @@ export function buildCallbackHtml(state) {
|
|
|
47
47
|
export function isValidTokenCallback(body, expectedState) {
|
|
48
48
|
return typeof body.state === 'string' && body.state === expectedState;
|
|
49
49
|
}
|
|
50
|
+
export async function requestCliLoginEmail(email, redirectTo, fetchImpl = fetch) {
|
|
51
|
+
const response = await fetchImpl(`${SUPABASE_FUNCTIONS_URL}/request-cli-login`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
apikey: SUPABASE_ANON_KEY,
|
|
55
|
+
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({ email, redirectTo }),
|
|
59
|
+
});
|
|
60
|
+
if (response.ok)
|
|
61
|
+
return;
|
|
62
|
+
const message = await readFunctionError(response);
|
|
63
|
+
throw new Error(message ?? SEND_EMAIL_FAILED_MESSAGE);
|
|
64
|
+
}
|
|
50
65
|
export async function authenticate(email) {
|
|
51
66
|
const client = createSupabaseClient();
|
|
52
67
|
const state = randomUUID();
|
|
@@ -108,12 +123,11 @@ export async function authenticate(email) {
|
|
|
108
123
|
settle(() => reject(new Error(SEND_EMAIL_FAILED_MESSAGE)));
|
|
109
124
|
});
|
|
110
125
|
server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
|
|
111
|
-
|
|
112
|
-
email,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
settle(() => reject(new Error(SEND_EMAIL_FAILED_MESSAGE)));
|
|
126
|
+
try {
|
|
127
|
+
await requestCliLoginEmail(email, redirectUrl);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
settle(() => reject(error instanceof Error ? error : new Error(SEND_EMAIL_FAILED_MESSAGE)));
|
|
117
131
|
}
|
|
118
132
|
});
|
|
119
133
|
function settle(done) {
|
|
@@ -125,6 +139,15 @@ export async function authenticate(email) {
|
|
|
125
139
|
}
|
|
126
140
|
});
|
|
127
141
|
}
|
|
142
|
+
async function readFunctionError(response) {
|
|
143
|
+
try {
|
|
144
|
+
const body = await response.json();
|
|
145
|
+
return typeof body.error === 'string' ? body.error : null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
128
151
|
function readJson(req) {
|
|
129
152
|
return new Promise((resolve) => {
|
|
130
153
|
let body = '';
|
package/dist/daemon.js
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSessionClient, heartbeat, setDeviceStatus } from './devices.js';
|
|
2
2
|
import { loadMachineState } from './session.js';
|
|
3
3
|
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
4
|
-
const
|
|
4
|
+
const SESSION_RETRY_INTERVAL_MS = 5_000;
|
|
5
|
+
export async function heartbeatOnce(session, deps) {
|
|
6
|
+
try {
|
|
7
|
+
await deps.heartbeat(session);
|
|
8
|
+
return session;
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
deps.log(err instanceof Error ? err.message : 'Heartbeat failed');
|
|
12
|
+
// A re-run of `setup` may have rotated the token on disk; rebuild and let the next beat retry.
|
|
13
|
+
try {
|
|
14
|
+
return await deps.createSession();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return session;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
5
21
|
export async function runDaemon() {
|
|
6
|
-
const
|
|
22
|
+
const deps = {
|
|
23
|
+
createSession: createSessionClient,
|
|
24
|
+
heartbeat,
|
|
25
|
+
log: (msg) => process.stderr.write(`${msg}\n`),
|
|
26
|
+
};
|
|
27
|
+
let session = await acquireSession(deps);
|
|
7
28
|
let shuttingDown = false;
|
|
8
29
|
const shutdown = async () => {
|
|
9
30
|
if (shuttingDown)
|
|
10
31
|
return;
|
|
11
32
|
shuttingDown = true;
|
|
12
33
|
try {
|
|
13
|
-
await
|
|
34
|
+
await setDeviceStatus(session, 'disconnected');
|
|
14
35
|
}
|
|
15
36
|
catch {
|
|
16
37
|
// Best effort: a hard kill is covered by heartbeat staleness.
|
|
@@ -19,26 +40,24 @@ export async function runDaemon() {
|
|
|
19
40
|
};
|
|
20
41
|
process.once('SIGINT', () => void shutdown());
|
|
21
42
|
process.once('SIGTERM', () => void shutdown());
|
|
22
|
-
await
|
|
43
|
+
session = await heartbeatOnce(session, deps);
|
|
23
44
|
while (true) {
|
|
24
45
|
await sleep(HEARTBEAT_INTERVAL_MS);
|
|
25
|
-
await
|
|
46
|
+
session = await heartbeatOnce(session, deps);
|
|
26
47
|
}
|
|
27
48
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
catch (err) {
|
|
41
|
-
process.stderr.write(`${err instanceof Error ? err.message : 'Heartbeat failed'}\n`);
|
|
49
|
+
async function acquireSession(deps) {
|
|
50
|
+
while (true) {
|
|
51
|
+
while (!loadMachineState()) {
|
|
52
|
+
await sleep(SESSION_RETRY_INTERVAL_MS);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return await deps.createSession();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
deps.log(err instanceof Error ? err.message : 'Session unavailable');
|
|
59
|
+
await sleep(SESSION_RETRY_INTERVAL_MS);
|
|
60
|
+
}
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
63
|
function sleep(ms) {
|
package/dist/devices.js
CHANGED
|
@@ -1,66 +1,64 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { createSupabaseClient } from './supabase.js';
|
|
3
|
-
import { saveMachineState } from './session.js';
|
|
4
|
-
const
|
|
5
|
-
const SESSION_EXPIRED_MESSAGE = '
|
|
3
|
+
import { loadMachineState, saveMachineState } from './session.js';
|
|
4
|
+
export const NOT_CONNECTED_MESSAGE = 'This machine is not connected. Run `ctrl-spc setup` first.';
|
|
5
|
+
export const SESSION_EXPIRED_MESSAGE = 'Your machine session expired. Run `ctrl-spc setup` to reconnect.';
|
|
6
|
+
const REGISTER_FAILED_MESSAGE = "Connected to your account but couldn't register this machine. Check your connection and try again.";
|
|
6
7
|
export function hashToken(token) {
|
|
7
8
|
return createHash('sha256').update(token).digest('hex');
|
|
8
9
|
}
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
const activeState = await restoreSession(client, state);
|
|
12
|
-
const { data, error: userError } = await client.auth.getUser();
|
|
13
|
-
const user = data.user;
|
|
14
|
-
if (userError || !user) {
|
|
15
|
-
throw new Error(REGISTER_FAILED_MESSAGE);
|
|
16
|
-
}
|
|
17
|
-
const { error } = await client.from('devices').upsert({
|
|
18
|
-
id: activeState.machineId,
|
|
19
|
-
owner_id: user.id,
|
|
20
|
-
label,
|
|
21
|
-
token_hash: hashToken(activeState.refreshToken),
|
|
22
|
-
status: 'connected',
|
|
23
|
-
last_seen_at: new Date().toISOString(),
|
|
24
|
-
}, { onConflict: 'id' });
|
|
25
|
-
if (error) {
|
|
26
|
-
throw new Error(REGISTER_FAILED_MESSAGE);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export async function heartbeat(state) {
|
|
30
|
-
const client = createSupabaseClient();
|
|
31
|
-
const activeState = await restoreSession(client, state);
|
|
32
|
-
const { error } = await client
|
|
33
|
-
.from('devices')
|
|
34
|
-
.update({
|
|
35
|
-
last_seen_at: new Date().toISOString(),
|
|
36
|
-
status: 'connected',
|
|
37
|
-
})
|
|
38
|
-
.eq('id', activeState.machineId);
|
|
39
|
-
if (error) {
|
|
40
|
-
throw new Error(`Heartbeat failed: ${error.message}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export async function disconnect(state) {
|
|
44
|
-
const client = createSupabaseClient();
|
|
45
|
-
const activeState = await restoreSession(client, state);
|
|
46
|
-
const { error } = await client.from('devices').update({ status: 'disconnected' }).eq('id', activeState.machineId);
|
|
47
|
-
if (error) {
|
|
48
|
-
throw new Error(`Disconnect failed: ${error.message}`);
|
|
49
|
-
}
|
|
10
|
+
export function sessionToState(machineId, session) {
|
|
11
|
+
return { machineId, accessToken: session.access_token, refreshToken: session.refresh_token };
|
|
50
12
|
}
|
|
51
|
-
async function
|
|
13
|
+
export async function createSessionClient(deps = {}) {
|
|
14
|
+
const loadState = deps.loadState ?? loadMachineState;
|
|
15
|
+
const saveState = deps.saveState ?? saveMachineState;
|
|
16
|
+
const createClient = deps.createClient ?? createSupabaseClient;
|
|
17
|
+
const state = loadState();
|
|
18
|
+
if (!state)
|
|
19
|
+
throw new Error(NOT_CONNECTED_MESSAGE);
|
|
20
|
+
const client = createClient();
|
|
21
|
+
const machineId = state.machineId;
|
|
22
|
+
client.auth.onAuthStateChange((_event, session) => {
|
|
23
|
+
if (session)
|
|
24
|
+
saveState(sessionToState(machineId, session));
|
|
25
|
+
});
|
|
52
26
|
const { data, error } = await client.auth.setSession({
|
|
53
27
|
access_token: state.accessToken,
|
|
54
28
|
refresh_token: state.refreshToken,
|
|
55
29
|
});
|
|
56
|
-
if (error || !data.session)
|
|
30
|
+
if (error || !data.session)
|
|
57
31
|
throw new Error(SESSION_EXPIRED_MESSAGE);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
32
|
+
return { client, machineId };
|
|
33
|
+
}
|
|
34
|
+
export async function heartbeat(session) {
|
|
35
|
+
const { error } = await session.client
|
|
36
|
+
.from('devices')
|
|
37
|
+
.update({ last_seen_at: new Date().toISOString(), status: 'connected' })
|
|
38
|
+
.eq('id', session.machineId);
|
|
39
|
+
if (error)
|
|
40
|
+
throw new Error(`Heartbeat failed: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
export async function setDeviceStatus(session, status) {
|
|
43
|
+
const { error } = await session.client.from('devices').update({ status }).eq('id', session.machineId);
|
|
44
|
+
if (error)
|
|
45
|
+
throw new Error(`Update failed: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
export async function upsertDevice(session, label, loadState = loadMachineState) {
|
|
48
|
+
const { data, error: userError } = await session.client.auth.getUser();
|
|
49
|
+
if (userError || !data.user)
|
|
50
|
+
throw new Error(REGISTER_FAILED_MESSAGE);
|
|
51
|
+
const state = loadState();
|
|
52
|
+
if (!state)
|
|
53
|
+
throw new Error(NOT_CONNECTED_MESSAGE);
|
|
54
|
+
const { error } = await session.client.from('devices').upsert({
|
|
55
|
+
id: session.machineId,
|
|
56
|
+
owner_id: data.user.id,
|
|
57
|
+
label,
|
|
58
|
+
token_hash: hashToken(state.refreshToken),
|
|
59
|
+
status: 'connected',
|
|
60
|
+
last_seen_at: new Date().toISOString(),
|
|
61
|
+
}, { onConflict: 'id' });
|
|
62
|
+
if (error)
|
|
63
|
+
throw new Error(REGISTER_FAILED_MESSAGE);
|
|
66
64
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
const [, , command] = process.argv;
|
|
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
|
+
}
|
|
2
25
|
async function main() {
|
|
3
26
|
if (command === 'setup') {
|
|
4
27
|
const { runSetup } = await import('./setup.js');
|
|
@@ -10,7 +33,38 @@ async function main() {
|
|
|
10
33
|
await runDaemon();
|
|
11
34
|
return;
|
|
12
35
|
}
|
|
13
|
-
|
|
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;
|
|
45
|
+
}
|
|
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>');
|
|
14
68
|
process.exitCode = 1;
|
|
15
69
|
}
|
|
16
70
|
main().catch((err) => {
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import * as readline from 'node:readline/promises';
|
|
3
|
+
import { claimProject as claimWithServer } from '@ctrl-spc/mcp-server/claimResolver';
|
|
4
|
+
import { createSupabaseClient } from './supabase.js';
|
|
5
|
+
import { loadMachineState, saveMachineState } from './session.js';
|
|
6
|
+
import { detectMonorepoProjects, detectRepo, parseProjectSelection } from './repo.js';
|
|
7
|
+
import { writeMcpConfig as writeConfig } from './mcpConfig.js';
|
|
8
|
+
export async function runInitForProjects(options) {
|
|
9
|
+
const messages = [];
|
|
10
|
+
for (const project of options.projects) {
|
|
11
|
+
const result = await options.claimProject({
|
|
12
|
+
name: project.name,
|
|
13
|
+
remoteUrl: options.repo.normalizedRemoteUrl,
|
|
14
|
+
localPath: project.path,
|
|
15
|
+
fingerprint: options.repo.fingerprint,
|
|
16
|
+
orgId: options.orgId,
|
|
17
|
+
isMonorepoSubproject: project.path !== options.repo.root,
|
|
18
|
+
});
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
messages.push(result.message);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
messages.push(`Project registered: ${result.projectName}`);
|
|
24
|
+
}
|
|
25
|
+
options.writeMcpConfig(options.repo.root);
|
|
26
|
+
return messages;
|
|
27
|
+
}
|
|
28
|
+
export async function runInit(argv, cwd = process.cwd()) {
|
|
29
|
+
const orgFlagIndex = argv.indexOf('--org');
|
|
30
|
+
const personal = argv.includes('--personal');
|
|
31
|
+
const orgId = orgFlagIndex >= 0 ? argv[orgFlagIndex + 1] : null;
|
|
32
|
+
if (!personal && !orgId)
|
|
33
|
+
throw new Error('Run init from the copied Add-project prompt so Control Space knows which org to use.');
|
|
34
|
+
const repo = detectRepo(cwd);
|
|
35
|
+
const detected = detectMonorepoProjects(repo.root);
|
|
36
|
+
const projects = detected.length <= 1 ? detected : await askWhichProjects(detected);
|
|
37
|
+
if (projects.length === 0) {
|
|
38
|
+
console.log('No projects registered.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const state = loadMachineState();
|
|
42
|
+
if (!state)
|
|
43
|
+
throw new Error('Run `ctrl-spc setup` first so Control Space can act as you.');
|
|
44
|
+
const client = createSupabaseClient();
|
|
45
|
+
const { data, error } = await client.auth.setSession({
|
|
46
|
+
access_token: state.accessToken,
|
|
47
|
+
refresh_token: state.refreshToken,
|
|
48
|
+
});
|
|
49
|
+
if (error || !data.session)
|
|
50
|
+
throw new Error('Your machine session has expired. Run `ctrl-spc setup` again.');
|
|
51
|
+
if (data.session.access_token !== state.accessToken ||
|
|
52
|
+
data.session.refresh_token !== state.refreshToken) {
|
|
53
|
+
state.accessToken = data.session.access_token;
|
|
54
|
+
state.refreshToken = data.session.refresh_token;
|
|
55
|
+
saveMachineState(state);
|
|
56
|
+
}
|
|
57
|
+
const messages = await runInitForProjects({
|
|
58
|
+
repo,
|
|
59
|
+
projects,
|
|
60
|
+
orgId,
|
|
61
|
+
claimProject: (claimInput) => claimWithServer(client, claimInput),
|
|
62
|
+
writeMcpConfig: writeConfig,
|
|
63
|
+
});
|
|
64
|
+
for (const message of messages)
|
|
65
|
+
console.log(message);
|
|
66
|
+
console.log('MCP config written: .mcp.json');
|
|
67
|
+
}
|
|
68
|
+
async function askWhichProjects(projects) {
|
|
69
|
+
console.log('Detected multiple sub-projects:');
|
|
70
|
+
projects.forEach((project, index) => console.log(`${index + 1}. ${project.name} - ${project.path}`));
|
|
71
|
+
const rl = readline.createInterface({ input, output });
|
|
72
|
+
try {
|
|
73
|
+
const answer = await rl.question('Which should Control Space register? Enter numbers separated by commas: ');
|
|
74
|
+
return parseProjectSelection(answer, projects);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rl.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/launchd.js
CHANGED
|
@@ -6,6 +6,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { ensureStateDir, getStatePaths } from './session.js';
|
|
7
7
|
export const PLIST_LABEL = 'com.ctrl-spc.daemon';
|
|
8
8
|
const INSTALL_FAILED_MESSAGE = "Couldn't install the background helper. Try again, or download the installer from the web app.";
|
|
9
|
+
const STOP_FAILED_MESSAGE = "Couldn't stop the background helper.";
|
|
10
|
+
const RESTART_FAILED_MESSAGE = "Couldn't restart the background helper. Try `ctrl-spc setup` again.";
|
|
11
|
+
const defaultExec = (command, args) => {
|
|
12
|
+
execFileSync(command, args, { stdio: 'pipe' });
|
|
13
|
+
};
|
|
9
14
|
export function getPlistPath(home = homedir()) {
|
|
10
15
|
return join(home, 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
11
16
|
}
|
|
@@ -66,6 +71,34 @@ export async function installDaemon() {
|
|
|
66
71
|
export function isDaemonInstalled(home = homedir()) {
|
|
67
72
|
return existsSync(getPlistPath(home));
|
|
68
73
|
}
|
|
74
|
+
export function bootoutArgs(uid, plistPath) {
|
|
75
|
+
return ['bootout', `gui/${uid}`, plistPath];
|
|
76
|
+
}
|
|
77
|
+
export function kickstartArgs(uid) {
|
|
78
|
+
return ['kickstart', '-k', `gui/${uid}/${PLIST_LABEL}`];
|
|
79
|
+
}
|
|
80
|
+
export async function stopDaemon(home = homedir(), exec = defaultExec) {
|
|
81
|
+
const uid = process.getuid?.();
|
|
82
|
+
if (uid === undefined)
|
|
83
|
+
throw new Error(STOP_FAILED_MESSAGE);
|
|
84
|
+
try {
|
|
85
|
+
exec('launchctl', bootoutArgs(uid, getPlistPath(home)));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Already stopped — fine.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function restartDaemon(home = homedir(), exec = defaultExec) {
|
|
92
|
+
const uid = process.getuid?.();
|
|
93
|
+
if (uid === undefined)
|
|
94
|
+
throw new Error(RESTART_FAILED_MESSAGE);
|
|
95
|
+
try {
|
|
96
|
+
exec('launchctl', kickstartArgs(uid));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error(RESTART_FAILED_MESSAGE);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
69
102
|
function escapeXml(value) {
|
|
70
103
|
return value
|
|
71
104
|
.replace(/&/g, '&')
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
async function ensureDaemonRunning(daemon, forceRestart) {
|
|
2
|
+
if (!daemon.isDaemonInstalled()) {
|
|
3
|
+
await daemon.installDaemon();
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
if (forceRestart)
|
|
7
|
+
await daemon.restartDaemon();
|
|
8
|
+
}
|
|
9
|
+
export async function runConnect(deps) {
|
|
10
|
+
const session = await deps.createSession();
|
|
11
|
+
await deps.upsertDevice(session, deps.label);
|
|
12
|
+
await ensureDaemonRunning(deps.daemon, true);
|
|
13
|
+
deps.log('Connected. This machine now shows as connected in the web app.');
|
|
14
|
+
}
|
|
15
|
+
export async function runReconnect(deps) {
|
|
16
|
+
const session = await deps.createSession();
|
|
17
|
+
await deps.setDeviceStatus(session, 'connected');
|
|
18
|
+
await ensureDaemonRunning(deps.daemon, true);
|
|
19
|
+
deps.log('Reconnected - no email needed.');
|
|
20
|
+
}
|
|
21
|
+
export async function runDisconnect(deps) {
|
|
22
|
+
const session = await deps.createSession();
|
|
23
|
+
await deps.setDeviceStatus(session, 'disconnected');
|
|
24
|
+
await deps.daemon.stopDaemon();
|
|
25
|
+
deps.log('Disconnected. Run `ctrl-spc reconnect` to come back online - no email needed.');
|
|
26
|
+
}
|
|
27
|
+
export async function runStatus(deps) {
|
|
28
|
+
try {
|
|
29
|
+
await deps.createSession();
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
deps.log(err instanceof Error ? err.message : 'This machine is not connected.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const installed = deps.daemon.isDaemonInstalled() ? 'yes' : 'no';
|
|
36
|
+
deps.log(`Connected (session valid). Background helper installed: ${installed}.`);
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function writeMcpConfig(repoRoot) {
|
|
4
|
+
writeFileSync(join(repoRoot, '.mcp.json'), JSON.stringify({
|
|
5
|
+
mcpServers: {
|
|
6
|
+
'ctrl-spc': {
|
|
7
|
+
command: 'ctrl-spc',
|
|
8
|
+
args: ['serve'],
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
}, null, 2));
|
|
12
|
+
}
|
package/dist/repo.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { fingerprintRepo, normalizeRemoteUrl } from '@ctrl-spc/mcp-server/fingerprint';
|
|
5
|
+
function git(cwd, args) {
|
|
6
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
7
|
+
}
|
|
8
|
+
export function detectRepo(cwd) {
|
|
9
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']);
|
|
10
|
+
const rootCommit = git(root, ['rev-list', '--max-parents=0', 'HEAD']).split('\n')[0];
|
|
11
|
+
let remoteUrl = '';
|
|
12
|
+
try {
|
|
13
|
+
remoteUrl = git(root, ['config', '--get', 'remote.origin.url']);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
remoteUrl = '';
|
|
17
|
+
}
|
|
18
|
+
const normalizedRemoteUrl = normalizeRemoteUrl(remoteUrl);
|
|
19
|
+
return {
|
|
20
|
+
root,
|
|
21
|
+
rootCommit,
|
|
22
|
+
remoteUrl,
|
|
23
|
+
normalizedRemoteUrl,
|
|
24
|
+
fingerprint: fingerprintRepo(rootCommit, normalizedRemoteUrl),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function detectMonorepoProjects(root) {
|
|
28
|
+
const rootPackage = join(root, 'package.json');
|
|
29
|
+
const hasWorkspaceMarker = existsSync(join(root, 'pnpm-workspace.yaml')) ||
|
|
30
|
+
existsSync(join(root, 'turbo.json')) ||
|
|
31
|
+
existsSync(join(root, 'nx.json')) ||
|
|
32
|
+
existsSync(join(root, 'lerna.json')) ||
|
|
33
|
+
(existsSync(rootPackage) && Boolean(JSON.parse(readFileSync(rootPackage, 'utf8')).workspaces));
|
|
34
|
+
if (!hasWorkspaceMarker)
|
|
35
|
+
return [{ name: basename(root), path: root }];
|
|
36
|
+
const packagesDir = join(root, 'packages');
|
|
37
|
+
if (!existsSync(packagesDir))
|
|
38
|
+
return [{ name: basename(root), path: root }];
|
|
39
|
+
const projects = readdirSync(packagesDir, { withFileTypes: true })
|
|
40
|
+
.filter((entry) => entry.isDirectory())
|
|
41
|
+
.map((entry) => join(packagesDir, entry.name))
|
|
42
|
+
.filter((path) => existsSync(join(path, 'package.json')))
|
|
43
|
+
.map((path) => ({ name: basename(path), path }))
|
|
44
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
+
return projects.length > 0 ? projects : [{ name: basename(root), path: root }];
|
|
46
|
+
}
|
|
47
|
+
export function parseProjectSelection(input, projects) {
|
|
48
|
+
if (!input.trim())
|
|
49
|
+
return [];
|
|
50
|
+
const indexes = new Set(input.split(',').map((value) => Number(value.trim()) - 1));
|
|
51
|
+
return projects.filter((_, index) => indexes.has(index));
|
|
52
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
@@ -35,8 +35,10 @@ export function loadMachineState(statePath = getStatePaths().statePath) {
|
|
|
35
35
|
}
|
|
36
36
|
export function saveMachineState(state, statePath = getStatePaths().statePath) {
|
|
37
37
|
mkdirSync(path.dirname(statePath), { recursive: true });
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const tmpPath = `${statePath}.tmp`;
|
|
39
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
40
|
+
chmodSync(tmpPath, 0o600);
|
|
41
|
+
renameSync(tmpPath, statePath);
|
|
40
42
|
}
|
|
41
43
|
export function getOrCreateMachineId(statePath = getStatePaths().statePath) {
|
|
42
44
|
return loadMachineState(statePath)?.machineId ?? randomUUID();
|
package/dist/setup.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function runSetup() {
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
const { authenticate } = await import('./auth.js');
|
|
17
|
-
const { upsertDevice } = await import('./devices.js');
|
|
17
|
+
const { createSessionClient, upsertDevice } = await import('./devices.js');
|
|
18
18
|
const installer = process.platform === 'darwin'
|
|
19
19
|
? await import('./launchd.js')
|
|
20
20
|
: await import('./windows-service.js');
|
|
@@ -22,12 +22,15 @@ export async function runSetup() {
|
|
|
22
22
|
await completeSetup({
|
|
23
23
|
email,
|
|
24
24
|
label: hostname(),
|
|
25
|
-
installDaemon: installer.installDaemon,
|
|
26
25
|
onReadyForAuth: () => {
|
|
27
26
|
console.log('Check your email for a sign-in link to connect this machine.');
|
|
28
27
|
},
|
|
29
|
-
authenticate,
|
|
30
|
-
|
|
28
|
+
authenticate: async (addr) => { await authenticate(addr); },
|
|
29
|
+
registerDevice: async (label) => {
|
|
30
|
+
const session = await createSessionClient();
|
|
31
|
+
await upsertDevice(session, label);
|
|
32
|
+
},
|
|
33
|
+
installDaemon: installer.installDaemon,
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
36
|
catch (err) {
|
|
@@ -50,8 +53,8 @@ function exitWithMessage(message) {
|
|
|
50
53
|
process.exitCode = 1;
|
|
51
54
|
}
|
|
52
55
|
export async function completeSetup(options) {
|
|
53
|
-
await options.installDaemon();
|
|
54
56
|
options.onReadyForAuth?.();
|
|
55
|
-
|
|
56
|
-
await options.
|
|
57
|
+
await options.authenticate(options.email);
|
|
58
|
+
await options.registerDevice(options.label);
|
|
59
|
+
await options.installDaemon();
|
|
57
60
|
}
|
package/dist/supabase.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createClient } from '@supabase/supabase-js';
|
|
2
2
|
export const SUPABASE_URL = 'https://dxlxlphkypjjyqfgpood.supabase.co';
|
|
3
3
|
export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImR4bHhscGhreXBqanlxZmdwb29kIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODAwNzcxMjEsImV4cCI6MjA5NTY1MzEyMX0.s0RkrQUqGRN45Q0lcBb_LkVEqYdYc0FKuVupzKKJrtQ';
|
|
4
|
+
export const SUPABASE_FUNCTIONS_URL = `${SUPABASE_URL}/functions/v1`;
|
|
4
5
|
export function createSupabaseClient() {
|
|
5
6
|
return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
6
7
|
auth: {
|
|
7
|
-
autoRefreshToken:
|
|
8
|
+
autoRefreshToken: true,
|
|
8
9
|
detectSessionInUrl: false,
|
|
9
10
|
persistSession: false,
|
|
10
11
|
},
|
package/dist/windows-service.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { ensureStateDir, getStatePaths } from './session.js';
|
|
7
7
|
export const WINDOWS_SERVICE_NAME = 'ctrl-spc-daemon';
|
|
8
8
|
const INSTALL_FAILED_MESSAGE = "Couldn't install the background helper. Try again, or download the installer from the web app.";
|
|
9
|
+
const STOP_FAILED_MESSAGE = "Couldn't stop the background helper.";
|
|
9
10
|
export function resolveBinPath(moduleUrl = import.meta.url) {
|
|
10
11
|
return fileURLToPath(new URL('../bin/ctrl-spc.js', moduleUrl));
|
|
11
12
|
}
|
|
@@ -85,6 +86,40 @@ export function installAndStartService(service, serviceDir) {
|
|
|
85
86
|
}
|
|
86
87
|
});
|
|
87
88
|
}
|
|
89
|
+
export function stopService(service) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
let settled = false;
|
|
92
|
+
const timeout = setTimeout(() => finish(() => reject(new Error(STOP_FAILED_MESSAGE))), 60_000);
|
|
93
|
+
service.once('stop', () => finish(resolve));
|
|
94
|
+
service.once('error', () => finish(() => reject(new Error(STOP_FAILED_MESSAGE))));
|
|
95
|
+
try {
|
|
96
|
+
service.stop();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
finish(() => reject(new Error(STOP_FAILED_MESSAGE)));
|
|
100
|
+
}
|
|
101
|
+
function finish(done) {
|
|
102
|
+
if (settled)
|
|
103
|
+
return;
|
|
104
|
+
settled = true;
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
done();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function stopDaemon() {
|
|
111
|
+
if (process.platform !== 'win32')
|
|
112
|
+
throw new Error(STOP_FAILED_MESSAGE);
|
|
113
|
+
const userProfile = process.env.USERPROFILE ?? homedir();
|
|
114
|
+
const appData = process.env.APPDATA ?? path.win32.join(userProfile, 'AppData', 'Roaming');
|
|
115
|
+
const service = createWindowsService(buildWindowsServiceConfig({ userProfile, appData }));
|
|
116
|
+
await stopService(service);
|
|
117
|
+
}
|
|
118
|
+
export async function restartDaemon() {
|
|
119
|
+
await stopDaemon();
|
|
120
|
+
// installDaemon is idempotent: 'alreadyinstalled' -> start.
|
|
121
|
+
await installDaemon();
|
|
122
|
+
}
|
|
88
123
|
export function isDaemonInstalled() {
|
|
89
124
|
const userProfile = process.env.USERPROFILE ?? homedir();
|
|
90
125
|
const appData = process.env.APPDATA ?? path.win32.join(userProfile, 'AppData', 'Roaming');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
export type ClaimInput = {
|
|
3
|
+
name: string;
|
|
4
|
+
remoteUrl: string;
|
|
5
|
+
localPath: string;
|
|
6
|
+
fingerprint: string;
|
|
7
|
+
orgId: string | null;
|
|
8
|
+
isMonorepoSubproject?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ClaimResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
projectId: string;
|
|
13
|
+
projectName: string;
|
|
14
|
+
claimed: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function mapClaimError(message: string): string;
|
|
20
|
+
export declare function claimProject(client: SupabaseClient, input: ClaimInput): Promise<ClaimResult>;
|
|
21
|
+
export declare function releaseProjectClaim(client: SupabaseClient, projectId: string): Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
message?: string;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function mapClaimError(message) {
|
|
2
|
+
const claimed = message.match(/repo_claimed:([^"]+)$/);
|
|
3
|
+
if (claimed) {
|
|
4
|
+
return `This repository is managed by ${claimed[1]}. Ask an admin for access.`;
|
|
5
|
+
}
|
|
6
|
+
if (message.includes('manage_projects_required')) {
|
|
7
|
+
return "You do not have permission to manage projects in this context.";
|
|
8
|
+
}
|
|
9
|
+
return 'Something went wrong. Please try again.';
|
|
10
|
+
}
|
|
11
|
+
export async function claimProject(client, input) {
|
|
12
|
+
const { data, error } = await client.rpc('create_project_with_links', {
|
|
13
|
+
p_name: input.name,
|
|
14
|
+
p_remote_url: input.remoteUrl,
|
|
15
|
+
p_local_path: input.localPath,
|
|
16
|
+
p_fingerprint: input.fingerprint,
|
|
17
|
+
p_org_id: input.orgId,
|
|
18
|
+
p_is_monorepo_subproject: Boolean(input.isMonorepoSubproject),
|
|
19
|
+
});
|
|
20
|
+
if (error)
|
|
21
|
+
return { ok: false, message: mapClaimError(error.message) };
|
|
22
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
23
|
+
if (!row)
|
|
24
|
+
return { ok: false, message: 'Something went wrong. Please try again.' };
|
|
25
|
+
return { ok: true, projectId: row.project_id, projectName: row.project_name, claimed: Boolean(row.claimed) };
|
|
26
|
+
}
|
|
27
|
+
export async function releaseProjectClaim(client, projectId) {
|
|
28
|
+
const { error } = await client.rpc('release_project_claim', { p_project_id: projectId });
|
|
29
|
+
if (error)
|
|
30
|
+
return { ok: false, message: mapClaimError(error.message) };
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function normalizeRemoteUrl(remote) {
|
|
3
|
+
if (!remote)
|
|
4
|
+
return '';
|
|
5
|
+
const trimmed = remote.trim().replace(/\/+$/, '').replace(/\.git$/i, '');
|
|
6
|
+
const ssh = trimmed.match(/^git@([^:]+):(.+)$/);
|
|
7
|
+
if (ssh) {
|
|
8
|
+
return `https://${ssh[1].toLowerCase()}/${ssh[2].toLowerCase().replace(/\.git$/i, '')}`;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(trimmed);
|
|
12
|
+
const path = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '').replace(/\.git$/i, '');
|
|
13
|
+
return `https://${url.host.toLowerCase()}/${path.toLowerCase()}`;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return trimmed.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function fingerprintRepo(rootCommitSha, normalizedRemoteUrl) {
|
|
20
|
+
return createHash('sha256').update(`${rootCommitSha}\n${normalizedRemoteUrl}`).digest('hex');
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
const json = (body) => ({ content: [{ type: 'text', text: JSON.stringify(body) }] });
|
|
5
|
+
export async function startMcpServer() {
|
|
6
|
+
const server = new Server({ name: 'ctrl-spc', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
7
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
8
|
+
tools: [
|
|
9
|
+
{
|
|
10
|
+
name: 'get_status',
|
|
11
|
+
description: 'Returns a small health response from the Control Space MCP server.',
|
|
12
|
+
inputSchema: { type: 'object', properties: {} },
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
}));
|
|
16
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
17
|
+
if (request.params.name === 'get_status')
|
|
18
|
+
return json({ ok: true, server: 'ctrl-spc' });
|
|
19
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
20
|
+
});
|
|
21
|
+
await server.connect(new StdioServerTransport());
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ctrl-spc/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Control Space MCP server and governance chokepoint",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist/"],
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./fingerprint": {
|
|
14
|
+
"types": "./dist/fingerprint.d.ts",
|
|
15
|
+
"import": "./dist/fingerprint.js"
|
|
16
|
+
},
|
|
17
|
+
"./claimResolver": {
|
|
18
|
+
"types": "./dist/claimResolver.d.ts",
|
|
19
|
+
"import": "./dist/claimResolver.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
28
|
+
"@supabase/supabase-js": "^2.108.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.13.2",
|
|
32
|
+
"typescript": "~6.0.2",
|
|
33
|
+
"vitest": "^4.1.9"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctrl-spc/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Control Space CLI - machine setup and background daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -15,12 +15,19 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
|
+
"prepack": "node scripts/bundle-mcp-server.mjs",
|
|
19
|
+
"postpack": "node scripts/cleanup-bundle.mjs",
|
|
18
20
|
"test": "vitest run"
|
|
19
21
|
},
|
|
20
22
|
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
24
|
+
"@ctrl-spc/mcp-server": "0.1.0",
|
|
21
25
|
"@supabase/supabase-js": "^2.108.2",
|
|
22
26
|
"node-windows": "^1.0.0-beta.8"
|
|
23
27
|
},
|
|
28
|
+
"bundledDependencies": [
|
|
29
|
+
"@ctrl-spc/mcp-server"
|
|
30
|
+
],
|
|
24
31
|
"devDependencies": {
|
|
25
32
|
"@types/node": "^24.13.2",
|
|
26
33
|
"typescript": "~6.0.2",
|