codelocal 1.5.0-beta.4 → 1.5.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 +12 -9
- package/bin/codelocal.js +27 -0
- package/bin/native/codelocal-darwin-arm64 +0 -0
- package/bin/native/codelocal-darwin-x64 +0 -0
- package/bin/native/codelocal-linux-arm64 +0 -0
- package/bin/native/codelocal-linux-x64 +0 -0
- package/bin/native/codelocal-win32-arm64.exe +0 -0
- package/bin/native/codelocal-win32-x64.exe +0 -0
- package/package.json +16 -24
- package/dist/approval-memory.js +0 -105
- package/dist/audit.js +0 -34
- package/dist/chat-approval.js +0 -77
- package/dist/cli-saas.js +0 -311
- package/dist/cli.js +0 -344
- package/dist/client-entry-v2.js +0 -22
- package/dist/client-v2.js +0 -991
- package/dist/cloud-client-sync.js +0 -6
- package/dist/context-engine.js +0 -295
- package/dist/editing-engine.js +0 -205
- package/dist/identity.js +0 -30
- package/dist/log.js +0 -248
- package/dist/lsp.js +0 -288
- package/dist/mcp-cloud-sync.js +0 -3
- package/dist/mcp-hub.js +0 -520
- package/dist/native-watcher.js +0 -148
- package/dist/process-manager.js +0 -285
- package/dist/protocol.js +0 -52
- package/dist/runtime-daemon.js +0 -163
- package/dist/security-policy.js +0 -293
- package/dist/semantic-router.js +0 -378
- package/dist/semantic.js +0 -263
- package/dist/state.js +0 -110
- package/dist/terminal-history.js +0 -102
- package/dist/verification.js +0 -66
- package/dist/version.js +0 -1
- package/dist/workspace-index.js +0 -530
- package/dist/workspace-registry.js +0 -86
package/README.md
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
# CodeLocal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native Go local development runtime for ChatGPT.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm i -g codelocal
|
|
8
|
+
npm i -g codelocal@beta
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Authorize a project
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
cd /path/to/project
|
|
15
|
+
codelocal .
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
`codelocal .` only authorizes that folder locally. It does not pair the machine or connect to CodeLocal Cloud.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
## Start CodeLocal
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
|
-
codelocal
|
|
23
|
+
codelocal
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
The Go runtime pairs this machine on first use, syncs authorized workspaces, then waits for ChatGPT. One machine runs one runtime; multiple workspaces activate lazily inside it.
|
|
27
|
+
|
|
28
|
+
Use `codelocal status` to inspect it and `codelocal stop` to stop it.
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
This npm package contains compiled native binaries only. CodeLocal source code and the Cloud backend are not distributed in the package.
|
package/bin/codelocal.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawnSync } = require('node:child_process');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const key = process.platform + '-' + process.arch;
|
|
5
|
+
const files = {
|
|
6
|
+
'darwin-arm64': 'codelocal-darwin-arm64',
|
|
7
|
+
'darwin-x64': 'codelocal-darwin-x64',
|
|
8
|
+
'linux-arm64': 'codelocal-linux-arm64',
|
|
9
|
+
'linux-x64': 'codelocal-linux-x64',
|
|
10
|
+
'win32-x64': 'codelocal-win32-x64.exe',
|
|
11
|
+
'win32-arm64': 'codelocal-win32-arm64.exe'
|
|
12
|
+
};
|
|
13
|
+
const file = files[key];
|
|
14
|
+
if (!file) {
|
|
15
|
+
console.error('CodeLocal does not have a native binary for ' + key + '.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const binary = path.join(__dirname, 'native', file);
|
|
19
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit', env: process.env });
|
|
20
|
+
if (result.error) {
|
|
21
|
+
console.error(result.error.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (result.signal) {
|
|
25
|
+
process.kill(process.pid, result.signal);
|
|
26
|
+
}
|
|
27
|
+
process.exit(result.status ?? 1);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "codelocal",
|
|
3
|
-
"version": "1.5.0-beta.4",
|
|
4
|
-
"description": "CodeLocal local code intelligence and execution runtime for ChatGPT.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"type": "module",
|
|
7
2
|
"bin": {
|
|
8
|
-
"codelocal": "
|
|
3
|
+
"codelocal": "bin/codelocal.js"
|
|
4
|
+
},
|
|
5
|
+
"description": "Native Go runtime that securely connects ChatGPT to local development workspaces.",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
|
11
|
-
"
|
|
10
|
+
"bin/",
|
|
12
11
|
"README.md"
|
|
13
12
|
],
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
"optionalDependencies": {
|
|
26
|
-
"node-pty": "^1.0.0"
|
|
27
|
-
},
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=20"
|
|
30
|
-
}
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chatgpt",
|
|
15
|
+
"mcp",
|
|
16
|
+
"coding",
|
|
17
|
+
"local",
|
|
18
|
+
"go"
|
|
19
|
+
],
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"name": "codelocal",
|
|
22
|
+
"version": "1.5.0"
|
|
31
23
|
}
|
package/dist/approval-memory.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { DEFAULT_STATE_DIR, readJsonFile, writeJsonAtomic } from "./state.js";
|
|
5
|
-
function workspaceHash(workspaceKey) {
|
|
6
|
-
return createHash("sha256").update(workspaceKey).digest("hex").slice(0, 24);
|
|
7
|
-
}
|
|
8
|
-
export class ApprovalMemory {
|
|
9
|
-
rootDir;
|
|
10
|
-
constructor(rootDir = path.join(DEFAULT_STATE_DIR, "approvals")) {
|
|
11
|
-
this.rootDir = rootDir;
|
|
12
|
-
}
|
|
13
|
-
fileFor(workspaceKey) {
|
|
14
|
-
return path.join(this.rootDir, `${workspaceHash(workspaceKey)}.json`);
|
|
15
|
-
}
|
|
16
|
-
async read(workspaceKey) {
|
|
17
|
-
const value = await readJsonFile(this.fileFor(workspaceKey), { version: 1, workspaceKey, approvals: [] });
|
|
18
|
-
const approvals = Array.isArray(value.approvals)
|
|
19
|
-
? value.approvals.filter((entry) => entry?.workspaceKey === workspaceKey && typeof entry.actionKey === "string")
|
|
20
|
-
: [];
|
|
21
|
-
return { version: 1, workspaceKey, approvals };
|
|
22
|
-
}
|
|
23
|
-
async write(workspaceKey, approvals) {
|
|
24
|
-
const unique = new Map();
|
|
25
|
-
for (const entry of approvals)
|
|
26
|
-
unique.set(entry.actionKey, entry);
|
|
27
|
-
await writeJsonAtomic(this.fileFor(workspaceKey), {
|
|
28
|
-
version: 1,
|
|
29
|
-
workspaceKey,
|
|
30
|
-
approvals: [...unique.values()].sort((a, b) => b.lastUsedAt - a.lastUsedAt),
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
async find(workspaceKey, actionKey) {
|
|
34
|
-
const data = await this.read(workspaceKey);
|
|
35
|
-
return data.approvals.find((entry) => entry.actionKey === actionKey) ?? null;
|
|
36
|
-
}
|
|
37
|
-
async remember(workspaceKey, decision) {
|
|
38
|
-
if (decision.approvalPolicy !== "rememberable" || !decision.approvalKey)
|
|
39
|
-
return null;
|
|
40
|
-
const data = await this.read(workspaceKey);
|
|
41
|
-
const previous = data.approvals.find((entry) => entry.actionKey === decision.approvalKey);
|
|
42
|
-
const now = Date.now();
|
|
43
|
-
const entry = {
|
|
44
|
-
id: previous?.id ?? randomUUID(),
|
|
45
|
-
workspaceKey,
|
|
46
|
-
actionKey: decision.approvalKey,
|
|
47
|
-
label: decision.approvalLabel ?? decision.redactedCommand,
|
|
48
|
-
redactedCommand: decision.redactedCommand,
|
|
49
|
-
riskLevel: decision.riskLevel,
|
|
50
|
-
matchedRules: [...decision.matchedRules],
|
|
51
|
-
createdAt: previous?.createdAt ?? now,
|
|
52
|
-
lastUsedAt: now,
|
|
53
|
-
useCount: (previous?.useCount ?? 0) + 1,
|
|
54
|
-
};
|
|
55
|
-
await this.write(workspaceKey, [...data.approvals.filter((item) => item.actionKey !== entry.actionKey), entry]);
|
|
56
|
-
return entry;
|
|
57
|
-
}
|
|
58
|
-
async touch(workspaceKey, actionKey) {
|
|
59
|
-
const data = await this.read(workspaceKey);
|
|
60
|
-
const entry = data.approvals.find((item) => item.actionKey === actionKey);
|
|
61
|
-
if (!entry)
|
|
62
|
-
return null;
|
|
63
|
-
entry.lastUsedAt = Date.now();
|
|
64
|
-
entry.useCount = Math.max(0, Number(entry.useCount) || 0) + 1;
|
|
65
|
-
await this.write(workspaceKey, data.approvals);
|
|
66
|
-
return entry;
|
|
67
|
-
}
|
|
68
|
-
async list(workspaceKey) {
|
|
69
|
-
if (workspaceKey)
|
|
70
|
-
return (await this.read(workspaceKey)).approvals;
|
|
71
|
-
const names = await fs.readdir(this.rootDir).catch(() => []);
|
|
72
|
-
const output = [];
|
|
73
|
-
for (const name of names) {
|
|
74
|
-
if (!name.endsWith(".json"))
|
|
75
|
-
continue;
|
|
76
|
-
const value = await readJsonFile(path.join(this.rootDir, name), { version: 1, workspaceKey: "", approvals: [] });
|
|
77
|
-
if (!value.workspaceKey || !Array.isArray(value.approvals))
|
|
78
|
-
continue;
|
|
79
|
-
output.push(...value.approvals.filter((entry) => entry?.workspaceKey === value.workspaceKey && typeof entry.actionKey === "string"));
|
|
80
|
-
}
|
|
81
|
-
return output.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
82
|
-
}
|
|
83
|
-
async revoke(identifier, workspaceKey) {
|
|
84
|
-
const targets = workspaceKey ? [workspaceKey] : [...new Set((await this.list()).map((entry) => entry.workspaceKey))];
|
|
85
|
-
let removed = 0;
|
|
86
|
-
for (const key of targets) {
|
|
87
|
-
const data = await this.read(key);
|
|
88
|
-
const next = data.approvals.filter((entry) => entry.id !== identifier && entry.actionKey !== identifier);
|
|
89
|
-
removed += data.approvals.length - next.length;
|
|
90
|
-
if (next.length !== data.approvals.length)
|
|
91
|
-
await this.write(key, next);
|
|
92
|
-
}
|
|
93
|
-
return removed;
|
|
94
|
-
}
|
|
95
|
-
async reset(workspaceKey) {
|
|
96
|
-
if (workspaceKey) {
|
|
97
|
-
const data = await this.read(workspaceKey);
|
|
98
|
-
await fs.rm(this.fileFor(workspaceKey), { force: true });
|
|
99
|
-
return data.approvals.length;
|
|
100
|
-
}
|
|
101
|
-
const count = (await this.list()).length;
|
|
102
|
-
await fs.rm(this.rootDir, { recursive: true, force: true });
|
|
103
|
-
return count;
|
|
104
|
-
}
|
|
105
|
-
}
|
package/dist/audit.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { appendPrivateJsonl, DEFAULT_STATE_DIR } from "./state.js";
|
|
3
|
-
import { redactCommand } from "./security-policy.js";
|
|
4
|
-
const AUDIT_ENABLED = process.env.CODELOCAL_AUDIT_FILE !== "0";
|
|
5
|
-
const AUDIT_FILE = process.env.CODELOCAL_AUDIT_PATH ?? path.join(DEFAULT_STATE_DIR, "audit.jsonl");
|
|
6
|
-
function sanitize(value, key = "") {
|
|
7
|
-
if (value instanceof Error)
|
|
8
|
-
return { name: value.name, message: value.message };
|
|
9
|
-
if (Array.isArray(value))
|
|
10
|
-
return value.map((item) => sanitize(item));
|
|
11
|
-
if (typeof value === "string") {
|
|
12
|
-
if (/command|detail|query/i.test(key))
|
|
13
|
-
return redactCommand(value).slice(0, 2000);
|
|
14
|
-
if (/content|patch|input|oldText|newText/i.test(key))
|
|
15
|
-
return `[${Buffer.byteLength(value, "utf8")} bytes]`;
|
|
16
|
-
return value.slice(0, 4000);
|
|
17
|
-
}
|
|
18
|
-
if (!value || typeof value !== "object")
|
|
19
|
-
return value;
|
|
20
|
-
const out = {};
|
|
21
|
-
for (const [childKey, childValue] of Object.entries(value)) {
|
|
22
|
-
if (/token|secret|password|authorization|cookie|credentialSecret/i.test(childKey))
|
|
23
|
-
out[childKey] = "[REDACTED]";
|
|
24
|
-
else
|
|
25
|
-
out[childKey] = sanitize(childValue, childKey);
|
|
26
|
-
}
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
export async function audit(event) {
|
|
30
|
-
if (!AUDIT_ENABLED)
|
|
31
|
-
return;
|
|
32
|
-
const record = sanitize({ ts: event.ts ?? new Date().toISOString(), ...event });
|
|
33
|
-
await appendPrivateJsonl(AUDIT_FILE, record).catch(() => undefined);
|
|
34
|
-
}
|
package/dist/chat-approval.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
-
function hash(value) {
|
|
3
|
-
return createHash("sha256").update(value).digest("hex");
|
|
4
|
-
}
|
|
5
|
-
function sameHex(a, b) {
|
|
6
|
-
const aa = Buffer.from(a, "hex");
|
|
7
|
-
const bb = Buffer.from(b, "hex");
|
|
8
|
-
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
9
|
-
}
|
|
10
|
-
export class ChatApprovalBroker {
|
|
11
|
-
ttlMs;
|
|
12
|
-
pending = new Map();
|
|
13
|
-
constructor(ttlMs = Number(process.env.CODELOCAL_CHAT_APPROVAL_TTL_MS ?? 5 * 60_000)) {
|
|
14
|
-
this.ttlMs = ttlMs;
|
|
15
|
-
}
|
|
16
|
-
fingerprint(command, cwd, decision) {
|
|
17
|
-
return hash(JSON.stringify({
|
|
18
|
-
rawCommandHash: hash(command),
|
|
19
|
-
redactedCommand: decision.redactedCommand,
|
|
20
|
-
cwd,
|
|
21
|
-
rules: [...decision.matchedRules].sort(),
|
|
22
|
-
risk: decision.riskLevel,
|
|
23
|
-
approvalPolicy: decision.approvalPolicy,
|
|
24
|
-
approvalKey: decision.approvalKey ?? null,
|
|
25
|
-
}));
|
|
26
|
-
}
|
|
27
|
-
prune() {
|
|
28
|
-
const now = Date.now();
|
|
29
|
-
for (const [id, approval] of this.pending)
|
|
30
|
-
if (approval.expiresAt <= now)
|
|
31
|
-
this.pending.delete(id);
|
|
32
|
-
}
|
|
33
|
-
preflight(command, cwd, decision) {
|
|
34
|
-
this.prune();
|
|
35
|
-
if (decision.blocked)
|
|
36
|
-
return { status: "blocked", riskLevel: decision.riskLevel, reason: decision.reason, matchedRules: decision.matchedRules, command: decision.redactedCommand, approvalPolicy: decision.approvalPolicy, approvalKey: decision.approvalKey, approvalLabel: decision.approvalLabel };
|
|
37
|
-
if (!decision.requiresApproval)
|
|
38
|
-
return { status: "safe", riskLevel: decision.riskLevel, reason: decision.reason, matchedRules: decision.matchedRules, command: decision.redactedCommand, approvalPolicy: decision.approvalPolicy, approvalKey: decision.approvalKey, approvalLabel: decision.approvalLabel };
|
|
39
|
-
const approvalToken = randomUUID() + randomUUID().replaceAll("-", "");
|
|
40
|
-
const id = randomUUID();
|
|
41
|
-
const expiresAt = Date.now() + this.ttlMs;
|
|
42
|
-
this.pending.set(id, { tokenHash: hash(approvalToken), fingerprint: this.fingerprint(command, cwd, decision), expiresAt });
|
|
43
|
-
return {
|
|
44
|
-
status: "approval_required",
|
|
45
|
-
riskLevel: decision.riskLevel,
|
|
46
|
-
reason: decision.reason,
|
|
47
|
-
matchedRules: decision.matchedRules,
|
|
48
|
-
command: decision.redactedCommand,
|
|
49
|
-
approvalPolicy: decision.approvalPolicy,
|
|
50
|
-
approvalKey: decision.approvalKey,
|
|
51
|
-
approvalLabel: decision.approvalLabel,
|
|
52
|
-
approvalToken: `${id}.${approvalToken}`,
|
|
53
|
-
expiresAt,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
consume(approvalToken, command, cwd, decision) {
|
|
57
|
-
this.prune();
|
|
58
|
-
if (!decision.requiresApproval || decision.blocked)
|
|
59
|
-
return !decision.blocked;
|
|
60
|
-
if (!approvalToken)
|
|
61
|
-
return false;
|
|
62
|
-
const dot = approvalToken.indexOf(".");
|
|
63
|
-
if (dot <= 0)
|
|
64
|
-
return false;
|
|
65
|
-
const id = approvalToken.slice(0, dot);
|
|
66
|
-
const secret = approvalToken.slice(dot + 1);
|
|
67
|
-
const pending = this.pending.get(id);
|
|
68
|
-
if (!pending)
|
|
69
|
-
return false;
|
|
70
|
-
this.pending.delete(id);
|
|
71
|
-
if (pending.expiresAt <= Date.now())
|
|
72
|
-
return false;
|
|
73
|
-
if (!sameHex(pending.tokenHash, hash(secret)))
|
|
74
|
-
return false;
|
|
75
|
-
return sameHex(pending.fingerprint, this.fingerprint(command, cwd, decision));
|
|
76
|
-
}
|
|
77
|
-
}
|
package/dist/cli-saas.js
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { promises as fs } from "node:fs";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { defaultDeviceIdentity, deleteLocalCredential, loadLocalCredential, saveLocalCredential } from "./identity.js";
|
|
7
|
-
import { WorkspaceRegistry } from "./workspace-registry.js";
|
|
8
|
-
import { ApprovalMemory } from "./approval-memory.js";
|
|
9
|
-
import { RuntimeDaemon } from "./runtime-daemon.js";
|
|
10
|
-
const DEFAULT_CLOUD = process.env.CODELOCAL_SERVER ?? "https://codelocal.cloud";
|
|
11
|
-
function usage() {
|
|
12
|
-
console.log(`CodeLocal CLI · machine runtime
|
|
13
|
-
|
|
14
|
-
Quick start:
|
|
15
|
-
codelocal
|
|
16
|
-
|
|
17
|
-
One-time workspace access:
|
|
18
|
-
codelocal grant ~/Projects/my-app
|
|
19
|
-
|
|
20
|
-
Then keep only this running:
|
|
21
|
-
codelocal
|
|
22
|
-
|
|
23
|
-
ChatGPT can list your previously granted workspaces and activate the one you choose in chat.
|
|
24
|
-
|
|
25
|
-
Commands:
|
|
26
|
-
codelocal Start the machine runtime; no project cwd required
|
|
27
|
-
codelocal grant <project> Authorize a project folder locally
|
|
28
|
-
codelocal ungrant <id|project> Remove a project's local authorization
|
|
29
|
-
codelocal workspaces List authorized local workspaces
|
|
30
|
-
codelocal . Backward-compatible: grant + activate current project
|
|
31
|
-
codelocal <project-path> Backward-compatible: grant + activate a project
|
|
32
|
-
codelocal login Open CodeLocal login
|
|
33
|
-
codelocal dashboard Open CodeLocal dashboard
|
|
34
|
-
codelocal pair [gateway] Pair this machine manually
|
|
35
|
-
codelocal status Show pairing + workspace status
|
|
36
|
-
codelocal approvals List remembered local approvals
|
|
37
|
-
codelocal approvals revoke <id> Revoke one remembered approval
|
|
38
|
-
codelocal approvals reset Forget all remembered approvals
|
|
39
|
-
codelocal doctor <project> Run local environment checks
|
|
40
|
-
codelocal mcp ... Manage local MCP extensions
|
|
41
|
-
|
|
42
|
-
Default cloud:
|
|
43
|
-
${DEFAULT_CLOUD}
|
|
44
|
-
`);
|
|
45
|
-
}
|
|
46
|
-
function normalizeBase(value) {
|
|
47
|
-
const url = new URL(value);
|
|
48
|
-
if (url.protocol === "ws:")
|
|
49
|
-
url.protocol = "http:";
|
|
50
|
-
if (url.protocol === "wss:")
|
|
51
|
-
url.protocol = "https:";
|
|
52
|
-
url.pathname = "";
|
|
53
|
-
url.search = "";
|
|
54
|
-
url.hash = "";
|
|
55
|
-
return url.toString().replace(/\/$/, "");
|
|
56
|
-
}
|
|
57
|
-
function httpToWs(base) {
|
|
58
|
-
const url = new URL(normalizeBase(base));
|
|
59
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
60
|
-
url.pathname = "/client";
|
|
61
|
-
return url.toString();
|
|
62
|
-
}
|
|
63
|
-
function wsToHttp(value) {
|
|
64
|
-
return normalizeBase(value);
|
|
65
|
-
}
|
|
66
|
-
function openBrowser(url) {
|
|
67
|
-
try {
|
|
68
|
-
let command;
|
|
69
|
-
let args;
|
|
70
|
-
if (process.platform === "darwin") {
|
|
71
|
-
command = "open";
|
|
72
|
-
args = [url];
|
|
73
|
-
}
|
|
74
|
-
else if (process.platform === "win32") {
|
|
75
|
-
command = "cmd";
|
|
76
|
-
args = ["/c", "start", "", url];
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
command = "xdg-open";
|
|
80
|
-
args = [url];
|
|
81
|
-
}
|
|
82
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore", shell: false });
|
|
83
|
-
child.unref();
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async function pair(baseArg = DEFAULT_CLOUD) {
|
|
91
|
-
const base = normalizeBase(baseArg || DEFAULT_CLOUD);
|
|
92
|
-
const wsUrl = httpToWs(base);
|
|
93
|
-
const existing = await loadLocalCredential(wsUrl);
|
|
94
|
-
if (existing)
|
|
95
|
-
return existing;
|
|
96
|
-
const device = defaultDeviceIdentity();
|
|
97
|
-
const response = await fetch(`${base}/pair/start`, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: { "content-type": "application/json" },
|
|
100
|
-
body: JSON.stringify(device),
|
|
101
|
-
signal: AbortSignal.timeout(15_000),
|
|
102
|
-
});
|
|
103
|
-
if (!response.ok)
|
|
104
|
-
throw new Error(`Unable to start device pairing (${response.status}).`);
|
|
105
|
-
const pairing = await response.json();
|
|
106
|
-
console.log(`\nCodeLocal needs to pair this machine.\n`);
|
|
107
|
-
console.log(`Pairing code: ${pairing.code}`);
|
|
108
|
-
console.log(`Approve: ${pairing.approveUrl}\n`);
|
|
109
|
-
if (openBrowser(pairing.approveUrl))
|
|
110
|
-
console.log("Opened your default browser. Sign in to CodeLocal and approve this device.");
|
|
111
|
-
else
|
|
112
|
-
console.log("Open the approval URL in your browser, sign in, and approve this device.");
|
|
113
|
-
console.log("Waiting for approval…");
|
|
114
|
-
while (Date.now() < pairing.expiresAt) {
|
|
115
|
-
await new Promise((resolve) => setTimeout(resolve, 1800));
|
|
116
|
-
const claim = await fetch(`${base}/pair/claim`, {
|
|
117
|
-
method: "POST",
|
|
118
|
-
headers: { "content-type": "application/json" },
|
|
119
|
-
body: JSON.stringify({ pairingId: pairing.pairingId, code: pairing.code }),
|
|
120
|
-
signal: AbortSignal.timeout(10_000),
|
|
121
|
-
}).catch(() => null);
|
|
122
|
-
if (!claim?.ok)
|
|
123
|
-
continue;
|
|
124
|
-
const credential = await claim.json();
|
|
125
|
-
const saved = await saveLocalCredential({ ...credential, serverUrl: wsUrl });
|
|
126
|
-
console.log(`✓ ${credential.deviceName} paired with CodeLocal Cloud.\n`);
|
|
127
|
-
return saved;
|
|
128
|
-
}
|
|
129
|
-
throw new Error("Pairing expired. Run `codelocal` again to create a new pairing request.");
|
|
130
|
-
}
|
|
131
|
-
async function validateCredential(server, credential) {
|
|
132
|
-
try {
|
|
133
|
-
const response = await fetch(`${wsToHttp(server)}/api/client/auth/check`, {
|
|
134
|
-
method: "POST",
|
|
135
|
-
headers: {
|
|
136
|
-
"content-type": "application/json",
|
|
137
|
-
"x-codelocal-credential-id": credential.credentialId,
|
|
138
|
-
authorization: `Device ${credential.credentialSecret}`,
|
|
139
|
-
},
|
|
140
|
-
body: "{}",
|
|
141
|
-
signal: AbortSignal.timeout(8_000),
|
|
142
|
-
});
|
|
143
|
-
if (response.status === 401 || response.status === 403 || response.status === 404)
|
|
144
|
-
return false;
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
async function resolvedRuntime(serverArg) {
|
|
152
|
-
// The selected gateway must come from an explicit override or this build's default.
|
|
153
|
-
// Never let a credential saved for an older gateway silently retarget CodeLocal.
|
|
154
|
-
const configured = serverArg || process.env.SERVER_URL || httpToWs(DEFAULT_CLOUD);
|
|
155
|
-
const server = configured.startsWith("ws://") || configured.startsWith("wss://") ? configured : httpToWs(configured);
|
|
156
|
-
let credential = await loadLocalCredential(server);
|
|
157
|
-
if (credential && !(await validateCredential(server, credential))) {
|
|
158
|
-
console.log("Stored CodeLocal credential is no longer valid or the gateway is incompatible. Pairing this machine again…");
|
|
159
|
-
await deleteLocalCredential();
|
|
160
|
-
credential = null;
|
|
161
|
-
}
|
|
162
|
-
if (!credential)
|
|
163
|
-
credential = await pair(wsToHttp(server));
|
|
164
|
-
return { server, credential };
|
|
165
|
-
}
|
|
166
|
-
async function runRuntime(projectArg, serverArg) {
|
|
167
|
-
const registry = new WorkspaceRegistry();
|
|
168
|
-
let initialWorkspaceId;
|
|
169
|
-
if (projectArg) {
|
|
170
|
-
const granted = await registry.grant(projectArg);
|
|
171
|
-
initialWorkspaceId = granted.workspaceId;
|
|
172
|
-
console.log(`✓ Workspace granted: ${granted.workspaceName}`);
|
|
173
|
-
}
|
|
174
|
-
const { server, credential } = await resolvedRuntime(serverArg);
|
|
175
|
-
const daemon = new RuntimeDaemon({ baseUrl: wsToHttp(server), serverUrl: server, credential, initialWorkspaceId });
|
|
176
|
-
const stop = async () => { await daemon.stop(); process.exit(0); };
|
|
177
|
-
process.once("SIGINT", () => { void stop(); });
|
|
178
|
-
process.once("SIGTERM", () => { void stop(); });
|
|
179
|
-
await daemon.run();
|
|
180
|
-
}
|
|
181
|
-
async function grant(projectArg) {
|
|
182
|
-
if (!projectArg)
|
|
183
|
-
throw new Error("Usage: codelocal grant <project-folder>");
|
|
184
|
-
const entry = await new WorkspaceRegistry().grant(projectArg);
|
|
185
|
-
console.log(`✓ Granted ${entry.workspaceName}`);
|
|
186
|
-
console.log(` ID: ${entry.workspaceId}`);
|
|
187
|
-
console.log(` Path: ${entry.localPath}`);
|
|
188
|
-
console.log("If `codelocal` is already running, it will sync this workspace shortly.");
|
|
189
|
-
}
|
|
190
|
-
async function ungrant(identifier) {
|
|
191
|
-
if (!identifier)
|
|
192
|
-
throw new Error("Usage: codelocal ungrant <workspace-id|project-folder>");
|
|
193
|
-
const removed = await new WorkspaceRegistry().revoke(identifier);
|
|
194
|
-
console.log(removed ? "✓ Workspace authorization removed." : "Workspace was not found in the local authorization registry.");
|
|
195
|
-
}
|
|
196
|
-
async function listWorkspaces() {
|
|
197
|
-
const workspaces = await new WorkspaceRegistry().list();
|
|
198
|
-
if (!workspaces.length) {
|
|
199
|
-
console.log("No authorized workspaces. Use `codelocal grant /path/to/project`.");
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
for (const workspace of workspaces) {
|
|
203
|
-
console.log(`${workspace.workspaceName}\n ${workspace.workspaceId}\n ${workspace.localPath}${workspace.lastActivatedAt ? `\n last activated ${new Date(workspace.lastActivatedAt).toLocaleString()}` : ""}\n`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async function approvalsCommand(args) {
|
|
207
|
-
const memory = new ApprovalMemory();
|
|
208
|
-
const action = args[0] ?? "list";
|
|
209
|
-
if (action === "list") {
|
|
210
|
-
const approvals = await memory.list();
|
|
211
|
-
if (!approvals.length) {
|
|
212
|
-
console.log("No remembered approvals.");
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
for (const approval of approvals) {
|
|
216
|
-
console.log(`${approval.label}\n ID: ${approval.id}\n Workspace: ${approval.workspaceKey}\n Key: ${approval.actionKey}\n Used: ${approval.useCount} · last ${new Date(approval.lastUsedAt).toLocaleString()}\n`);
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (action === "revoke") {
|
|
221
|
-
const id = args[1];
|
|
222
|
-
if (!id)
|
|
223
|
-
throw new Error("Usage: codelocal approvals revoke <id|action-key>");
|
|
224
|
-
const removed = await memory.revoke(id);
|
|
225
|
-
console.log(removed ? `✓ Removed ${removed} remembered approval${removed === 1 ? "" : "s"}.` : "Approval not found.");
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (action === "reset") {
|
|
229
|
-
const removed = await memory.reset();
|
|
230
|
-
console.log(`✓ Forgot ${removed} remembered approval${removed === 1 ? "" : "s"}.`);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
throw new Error("Usage: codelocal approvals [list|revoke <id|action-key>|reset]");
|
|
234
|
-
}
|
|
235
|
-
async function status() {
|
|
236
|
-
const credential = await loadLocalCredential();
|
|
237
|
-
const workspaces = await new WorkspaceRegistry().list();
|
|
238
|
-
console.log(JSON.stringify({
|
|
239
|
-
device: defaultDeviceIdentity(),
|
|
240
|
-
paired: !!credential,
|
|
241
|
-
authorizedWorkspaces: workspaces.map(({ workspaceId, workspaceName, localPath, grantedAt, lastActivatedAt }) => ({ workspaceId, workspaceName, localPath, grantedAt, lastActivatedAt })),
|
|
242
|
-
credential: credential ? {
|
|
243
|
-
credentialId: credential.credentialId,
|
|
244
|
-
deviceId: credential.deviceId,
|
|
245
|
-
deviceName: credential.deviceName,
|
|
246
|
-
serverUrl: credential.serverUrl,
|
|
247
|
-
createdAt: credential.createdAt,
|
|
248
|
-
credentialSecret: "[REDACTED]",
|
|
249
|
-
} : null,
|
|
250
|
-
stateDir: process.env.CODELOCAL_STATE_DIR ?? path.join(os.homedir(), ".codelocal"),
|
|
251
|
-
}, null, 2));
|
|
252
|
-
}
|
|
253
|
-
async function login(baseArg = DEFAULT_CLOUD, dashboard = false) {
|
|
254
|
-
const base = normalizeBase(baseArg || DEFAULT_CLOUD);
|
|
255
|
-
const url = dashboard ? `${base}/dashboard` : `${base}/login`;
|
|
256
|
-
console.log(url);
|
|
257
|
-
if (!openBrowser(url))
|
|
258
|
-
console.log("Open the URL above in your browser.");
|
|
259
|
-
}
|
|
260
|
-
async function looksLikeProjectPath(value) {
|
|
261
|
-
if (!value || value.startsWith("-"))
|
|
262
|
-
return false;
|
|
263
|
-
if (value === "." || value === ".." || value.startsWith("./") || value.startsWith("../") || path.isAbsolute(value))
|
|
264
|
-
return true;
|
|
265
|
-
return fs.stat(path.resolve(value)).then((stat) => stat.isDirectory()).catch(() => false);
|
|
266
|
-
}
|
|
267
|
-
async function delegateLegacyCli() {
|
|
268
|
-
await import("./cli.js");
|
|
269
|
-
}
|
|
270
|
-
const [, , command, ...args] = process.argv;
|
|
271
|
-
try {
|
|
272
|
-
if (!command)
|
|
273
|
-
await runRuntime();
|
|
274
|
-
else if (command === "help" || command === "--help" || command === "-h")
|
|
275
|
-
usage();
|
|
276
|
-
else if (command === "login")
|
|
277
|
-
await login(args[0] ?? DEFAULT_CLOUD, false);
|
|
278
|
-
else if (command === "dashboard")
|
|
279
|
-
await login(args[0] ?? DEFAULT_CLOUD, true);
|
|
280
|
-
else if (command === "pair") {
|
|
281
|
-
await pair(args[0] ?? DEFAULT_CLOUD);
|
|
282
|
-
}
|
|
283
|
-
else if (command === "status")
|
|
284
|
-
await status();
|
|
285
|
-
else if (command === "approvals")
|
|
286
|
-
await approvalsCommand(args);
|
|
287
|
-
else if (command === "workspaces")
|
|
288
|
-
await listWorkspaces();
|
|
289
|
-
else if (command === "grant")
|
|
290
|
-
await grant(args[0]);
|
|
291
|
-
else if (command === "ungrant")
|
|
292
|
-
await ungrant(args[0]);
|
|
293
|
-
else if (command === "start")
|
|
294
|
-
await runRuntime(args[0] ?? ".", args[1]);
|
|
295
|
-
else if (command === "doctor" || command === "mcp")
|
|
296
|
-
await delegateLegacyCli();
|
|
297
|
-
else if (command === "rotate" || command === "revoke") {
|
|
298
|
-
console.log("Device rotation/revocation is account-scoped in SaaS mode. Opening Security/Devices dashboard…");
|
|
299
|
-
await login(DEFAULT_CLOUD, true);
|
|
300
|
-
}
|
|
301
|
-
else if (await looksLikeProjectPath(command))
|
|
302
|
-
await runRuntime(command, args[0]);
|
|
303
|
-
else {
|
|
304
|
-
usage();
|
|
305
|
-
process.exitCode = 1;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
310
|
-
process.exitCode = 1;
|
|
311
|
-
}
|