codelocal 1.5.0-beta.1

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.
@@ -0,0 +1,6 @@
1
+ // MCP configuration is intentionally local-only.
2
+ // This compatibility shim remains so older compiled entrypoints fail closed
3
+ // instead of attempting to fetch MCP metadata from CodeLocal Cloud.
4
+ export async function syncCloudMcpBeforeClient() {
5
+ return { skipped: true, reason: "MCP configuration is local-only" };
6
+ }
@@ -0,0 +1,295 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { WorkspaceIntelligenceIndex } from "./workspace-index.js";
4
+ const MAX_CONTEXT_CHARS = Math.max(8_000, Number(process.env.CODELOCAL_CONTEXT_MAX_CHARS ?? 48_000) || 48_000);
5
+ const MAX_SNIPPET_CHARS = Math.max(1_000, Number(process.env.CODELOCAL_CONTEXT_SNIPPET_CHARS ?? 7_000) || 7_000);
6
+ const MAX_CONTEXT_FILES = Math.max(2, Number(process.env.CODELOCAL_CONTEXT_FILES ?? 8) || 8);
7
+ function unique(values) {
8
+ return [...new Set(values)];
9
+ }
10
+ function normalizeLanguage(language) {
11
+ if (!language)
12
+ return null;
13
+ if (language === "typescript" || language === "javascript")
14
+ return "typescript/javascript";
15
+ if (language === "c" || language === "cpp")
16
+ return "c/c++";
17
+ return language;
18
+ }
19
+ function taskTerms(taskHint) {
20
+ return unique(taskHint.toLowerCase().split(/[^a-z0-9_$-]+/).filter((value) => value.length >= 2)).slice(0, 14);
21
+ }
22
+ function symbolMatches(symbol, terms) {
23
+ const name = (symbol.name ?? "").toLowerCase();
24
+ const file = symbol.path.toLowerCase();
25
+ return terms.some((term) => name.includes(term) || file.includes(term));
26
+ }
27
+ export class ProjectContextEngine {
28
+ root;
29
+ semantic;
30
+ intelligence;
31
+ cached = null;
32
+ epoch = 0;
33
+ constructor(root, semantic, intelligence = new WorkspaceIntelligenceIndex(root)) {
34
+ this.root = root;
35
+ this.semantic = semantic;
36
+ this.intelligence = intelligence;
37
+ }
38
+ invalidate(paths) {
39
+ this.cached = null;
40
+ this.epoch++;
41
+ this.intelligence.invalidate(paths);
42
+ }
43
+ noteChange(relativePath) {
44
+ this.cached = null;
45
+ this.epoch++;
46
+ this.intelligence.noteChange(relativePath);
47
+ }
48
+ indexSummary() {
49
+ return this.intelligence.summary();
50
+ }
51
+ async refresh(force = false) {
52
+ const summary = await this.intelligence.ensureFresh(force);
53
+ if (force)
54
+ this.cached = null;
55
+ return summary;
56
+ }
57
+ async packageMetadata(manifests) {
58
+ const frameworks = new Set();
59
+ const scriptCommands = new Map();
60
+ const packageFiles = manifests.filter((file) => file.endsWith("package.json")).slice(0, 80);
61
+ for (const relative of packageFiles) {
62
+ try {
63
+ const parsed = JSON.parse(await fs.readFile(path.join(this.root, relative), "utf8"));
64
+ const deps = { ...(parsed?.dependencies ?? {}), ...(parsed?.devDependencies ?? {}) };
65
+ for (const name of ["next", "@nestjs/core", "react", "vue", "@angular/core", "express", "fastify", "svelte", "nuxt", "expo", "react-native"]) {
66
+ if (name in deps)
67
+ frameworks.add(name);
68
+ }
69
+ const scripts = parsed?.scripts ?? {};
70
+ const dir = path.posix.dirname(relative) === "." ? "." : path.posix.dirname(relative);
71
+ for (const [name] of Object.entries(scripts)) {
72
+ const list = scriptCommands.get(name) ?? [];
73
+ list.push(dir);
74
+ scriptCommands.set(name, list);
75
+ }
76
+ }
77
+ catch { }
78
+ }
79
+ return { frameworks: [...frameworks], scriptCommands };
80
+ }
81
+ async map(force = false) {
82
+ const indexSummary = await this.intelligence.ensureFresh(force);
83
+ if (this.cached && !force && this.cached.intelligence.builtAt === indexSummary.builtAt && this.cached.intelligence.dirty === indexSummary.dirty)
84
+ return this.cached;
85
+ const records = this.intelligence.allFiles();
86
+ const files = records.map((record) => record.path);
87
+ const manifests = records.filter((record) => record.kind === "manifest").map((record) => record.path);
88
+ const lockfiles = files.filter((file) => /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|poetry\.lock|uv\.lock|Cargo\.lock|go\.sum|composer\.lock|pubspec\.lock|Gemfile\.lock|Podfile\.lock)$/i.test(file));
89
+ const languages = unique(records.map((record) => normalizeLanguage(record.language)).filter((value) => !!value));
90
+ const packageMeta = await this.packageMetadata(manifests);
91
+ const frameworks = [...packageMeta.frameworks];
92
+ for (const relative of manifests.filter((file) => file.endsWith("pyproject.toml")).slice(0, 20)) {
93
+ const py = await fs.readFile(path.join(this.root, relative), "utf8").catch(() => "");
94
+ if (/fastapi/i.test(py))
95
+ frameworks.push("fastapi");
96
+ if (/django/i.test(py))
97
+ frameworks.push("django");
98
+ if (/flask/i.test(py))
99
+ frameworks.push("flask");
100
+ }
101
+ if (manifests.some((file) => file.endsWith("pubspec.yaml")))
102
+ frameworks.push("flutter/dart");
103
+ if (files.some((file) => /(^|\/)ios\/Runner\.xcodeproj\//.test(file)))
104
+ frameworks.push("ios/xcode");
105
+ const packageManager = files.some((file) => /(^|\/)pnpm-lock\.yaml$/.test(file)) ? "pnpm"
106
+ : files.some((file) => /(^|\/)yarn\.lock$/.test(file)) ? "yarn"
107
+ : files.some((file) => /(^|\/)bun\.lockb?$/.test(file)) ? "bun"
108
+ : files.some((file) => /(^|\/)package-lock\.json$/.test(file)) ? "npm"
109
+ : null;
110
+ const scriptCommand = (name, dir) => {
111
+ const runner = packageManager === "npm" ? `npm run ${name}` : packageManager ? `${packageManager} ${name}` : `npm run ${name}`;
112
+ return dir === "." ? runner : `cd ${dir} && ${runner}`;
113
+ };
114
+ const commandsFor = (pattern) => [...packageMeta.scriptCommands.entries()]
115
+ .filter(([name]) => pattern.test(name))
116
+ .flatMap(([name, dirs]) => dirs.map((dir) => scriptCommand(name, dir)));
117
+ const buildCommands = commandsFor(/^(build|compile)(:|$)/);
118
+ const testCommands = commandsFor(/^(test)(:|$)/);
119
+ const lintCommands = commandsFor(/^(lint)(:|$)/);
120
+ const typecheckCommands = commandsFor(/^(typecheck|check)(:|$)/);
121
+ if (manifests.some((file) => file.endsWith("Cargo.toml"))) {
122
+ buildCommands.push("cargo check");
123
+ testCommands.push("cargo test");
124
+ }
125
+ if (manifests.some((file) => file.endsWith("go.mod"))) {
126
+ buildCommands.push("go build ./...");
127
+ testCommands.push("go test ./...");
128
+ }
129
+ if (manifests.some((file) => /pyproject|requirements/.test(file)))
130
+ testCommands.push("pytest");
131
+ if (manifests.some((file) => file.endsWith("pubspec.yaml"))) {
132
+ buildCommands.push("flutter analyze");
133
+ testCommands.push("flutter test");
134
+ }
135
+ const workspaceRoots = unique(manifests.map((file) => path.posix.dirname(file)).map((dir) => dir === "." ? "." : dir));
136
+ const rootsFromFiles = unique(files.map((file) => file.split("/")[0]).filter((name) => ["src", "app", "apps", "packages", "lib", "cmd", "internal", "pkg", "crates"].includes(name)));
137
+ const sourceRoots = unique([
138
+ ...rootsFromFiles,
139
+ ...workspaceRoots.flatMap((workspace) => ["lib", "src", "app"].map((name) => workspace === "." ? name : `${workspace}/${name}`).filter((candidate) => files.some((file) => file.startsWith(`${candidate}/`)))),
140
+ ]);
141
+ const testRoots = unique(files.map((file) => file.split("/").slice(0, -1).join("/")).filter((dir) => /(^|\/)(__tests__|test|tests|spec|specs)$/.test(dir))).slice(0, 200);
142
+ const entrypoints = files.filter((file) => /(^|\/)(main|index|server|app)\.(ts|tsx|js|jsx|py|rs|go|java|kt|cs|php|dart|swift)$/i.test(file)).slice(0, 150);
143
+ const instructionFiles = files.filter((file) => /(^|\/)(AGENTS\.md|CLAUDE\.md|copilot-instructions\.md)$/i.test(file));
144
+ const moduleCandidates = new Set();
145
+ for (const rootName of sourceRoots.length ? sourceRoots : ["src", "lib"]) {
146
+ for (const file of files) {
147
+ if (!file.startsWith(`${rootName}/`))
148
+ continue;
149
+ const rest = file.slice(rootName.length + 1);
150
+ const first = rest.split("/")[0];
151
+ if (first && !first.includes("."))
152
+ moduleCandidates.add(`${rootName}/${first}`);
153
+ }
154
+ }
155
+ this.cached = {
156
+ generatedAt: Date.now(),
157
+ rootName: path.basename(this.root),
158
+ languages,
159
+ frameworks: unique(frameworks),
160
+ workspaceRoots: workspaceRoots.length ? workspaceRoots : ["."],
161
+ entrypoints,
162
+ sourceRoots,
163
+ testRoots,
164
+ manifests,
165
+ lockfiles,
166
+ buildCommands: unique(buildCommands),
167
+ testCommands: unique(testCommands),
168
+ lintCommands: unique(lintCommands),
169
+ typecheckCommands: unique(typecheckCommands),
170
+ instructionFiles,
171
+ modules: [...moduleCandidates].slice(0, 300),
172
+ packageManager,
173
+ intelligence: indexSummary,
174
+ };
175
+ return this.cached;
176
+ }
177
+ async lspSymbolsForFiles(paths, terms, limit) {
178
+ const symbols = [];
179
+ for (const relative of paths.slice(0, Math.min(10, paths.length))) {
180
+ const document = await this.semantic.documentSymbols(relative, 160).catch(() => []);
181
+ const matches = terms.length ? document.filter((symbol) => symbolMatches(symbol, terms)) : document.slice(0, 12);
182
+ symbols.push(...(matches.length ? matches : document.slice(0, 5)));
183
+ if (symbols.length >= limit)
184
+ break;
185
+ }
186
+ return symbols.slice(0, limit);
187
+ }
188
+ async diagnosticsForFiles(paths, limit = 40) {
189
+ const diagnostics = [];
190
+ for (const relative of paths.slice(0, 5)) {
191
+ const values = await this.semantic.diagnostics(relative, 20).catch(() => []);
192
+ diagnostics.push(...values.map((item) => ({ ...item, path: item.path ?? relative })));
193
+ if (diagnostics.length >= limit)
194
+ break;
195
+ }
196
+ return diagnostics.slice(0, limit);
197
+ }
198
+ async snippet(relativePath, preferredLine, reason) {
199
+ try {
200
+ const absolute = path.resolve(this.root, relativePath);
201
+ const stat = await fs.stat(absolute);
202
+ if (!stat.isFile() || stat.size > 2 * 1024 * 1024)
203
+ return null;
204
+ const text = await fs.readFile(absolute, "utf8");
205
+ if (text.includes("\u0000"))
206
+ return null;
207
+ const lines = text.split(/\r?\n/);
208
+ const center = Math.max(1, Math.min(lines.length, preferredLine ?? 1));
209
+ const radius = preferredLine ? 24 : 32;
210
+ let startLine = Math.max(1, center - radius);
211
+ let endLine = Math.min(lines.length, center + radius);
212
+ let content = lines.slice(startLine - 1, endLine).join("\n");
213
+ if (content.length > MAX_SNIPPET_CHARS) {
214
+ content = content.slice(0, MAX_SNIPPET_CHARS);
215
+ const keptLines = content.split(/\r?\n/).length;
216
+ endLine = Math.min(endLine, startLine + keptLines - 1);
217
+ }
218
+ return { path: relativePath, startLine, endLine, totalLines: lines.length, content, reason };
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }
224
+ async relevant(taskHint, limit = 30) {
225
+ const project = await this.map();
226
+ await this.intelligence.ensureFresh();
227
+ const terms = taskTerms(taskHint);
228
+ const rankedFiles = this.intelligence.rank(taskHint, Math.max(limit * 3, 60));
229
+ const rankedPaths = rankedFiles.map((file) => file.path);
230
+ const workspaceSymbols = [];
231
+ for (const term of terms.slice(0, 10)) {
232
+ const found = await this.semantic.workspaceSymbols(term, Math.max(8, Math.ceil(limit / Math.max(1, terms.length))));
233
+ workspaceSymbols.push(...found);
234
+ if (workspaceSymbols.length >= limit * 2)
235
+ break;
236
+ }
237
+ const lspSymbols = await this.lspSymbolsForFiles(rankedPaths, terms, limit * 2);
238
+ const symbols = [...workspaceSymbols, ...lspSymbols].filter((symbol, index, all) => all.findIndex((candidate) => candidate.path === symbol.path && candidate.line === symbol.line && candidate.name === symbol.name) === index);
239
+ const symbolPaths = unique(symbols.map((symbol) => symbol.path).filter(Boolean));
240
+ const graphNeighbors = this.intelligence.neighbors([...rankedPaths.slice(0, 12), ...symbolPaths.slice(0, 12)], limit * 3);
241
+ const relevantPaths = unique([...rankedPaths, ...symbolPaths, ...graphNeighbors]).slice(0, limit);
242
+ const rankingByPath = new Map(rankedFiles.map((file) => [file.path, file]));
243
+ const diagnostics = await this.diagnosticsForFiles(relevantPaths);
244
+ const relevantSet = new Set(relevantPaths);
245
+ const graphEdges = this.intelligence.graph(5000)
246
+ .filter((edge) => relevantSet.has(edge.from) || relevantSet.has(edge.to))
247
+ .slice(0, 120);
248
+ const snippets = [];
249
+ let usedChars = 0;
250
+ for (const relative of relevantPaths.slice(0, MAX_CONTEXT_FILES)) {
251
+ const symbol = symbols.find((item) => item.path === relative && symbolMatches(item, terms)) ?? symbols.find((item) => item.path === relative);
252
+ const diagnostic = diagnostics.find((item) => item.path === relative);
253
+ const ranked = rankingByPath.get(relative);
254
+ const preferredLine = symbol?.line ?? diagnostic?.line;
255
+ const reason = ranked?.reasons?.join(", ") || (symbol ? "semantic-symbol" : diagnostic ? "diagnostic" : "graph-neighbor");
256
+ const value = await this.snippet(relative, preferredLine, reason);
257
+ if (!value)
258
+ continue;
259
+ if (usedChars + value.content.length > MAX_CONTEXT_CHARS)
260
+ break;
261
+ snippets.push(value);
262
+ usedChars += value.content.length;
263
+ }
264
+ return {
265
+ taskHint,
266
+ strategy: "persistent-incremental-index + per-root-LSP + semantic-symbols + dependency-neighbors + recent-change-ranking + bounded-source-snippets",
267
+ epoch: this.epoch,
268
+ index: this.intelligence.summary(),
269
+ project: {
270
+ languages: project.languages,
271
+ frameworks: project.frameworks,
272
+ workspaceRoots: project.workspaceRoots,
273
+ sourceRoots: project.sourceRoots,
274
+ testRoots: project.testRoots,
275
+ entrypoints: project.entrypoints.slice(0, 30),
276
+ commands: { build: project.buildCommands, test: project.testCommands, lint: project.lintCommands, typecheck: project.typecheckCommands },
277
+ },
278
+ rankedFiles: relevantPaths.map((file) => {
279
+ const ranked = rankingByPath.get(file);
280
+ return ranked
281
+ ? { path: file, score: ranked.score, reasons: ranked.reasons, language: ranked.language, symbols: ranked.symbols.slice(0, 20), imports: ranked.imports.slice(0, 20), changedAt: ranked.changedAt }
282
+ : { path: file, score: null, reasons: [symbolPaths.includes(file) ? "semantic-symbol" : "graph-neighbor"] };
283
+ }),
284
+ symbols: symbols.slice(0, limit * 2),
285
+ diagnostics,
286
+ graphEdges,
287
+ snippets,
288
+ relevantPaths,
289
+ contextBudget: { maxChars: MAX_CONTEXT_CHARS, usedChars, maxFiles: MAX_CONTEXT_FILES },
290
+ recommendation: relevantPaths.length
291
+ ? "Use this packet first. Ask for exact definitions/references or read a larger range only when the packet is insufficient; all side effects remain separate MCP calls."
292
+ : "Narrow the task hint or use semantic/text search before any broad scan.",
293
+ };
294
+ }
295
+ }
@@ -0,0 +1,205 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { spawn } from "node:child_process";
5
+ import { isSensitivePath } from "./security-policy.js";
6
+ function sha256(value) {
7
+ return createHash("sha256").update(value).digest("hex");
8
+ }
9
+ function offsetAt(text, line, column) {
10
+ const lines = text.split(/(?<=\n)/);
11
+ const row = Math.max(1, line);
12
+ const col = Math.max(1, column);
13
+ let offset = 0;
14
+ for (let i = 0; i < row - 1 && i < lines.length; i++)
15
+ offset += lines[i].length;
16
+ return Math.min(text.length, offset + col - 1);
17
+ }
18
+ async function executable(file) {
19
+ return fs.access(file).then(() => true).catch(() => false);
20
+ }
21
+ async function run(command, args, cwd, timeoutMs = 120_000) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(command, args, { cwd, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
24
+ let stdout = "", stderr = "", done = false;
25
+ const timer = setTimeout(() => { if (!done)
26
+ child.kill("SIGTERM"); }, timeoutMs);
27
+ child.stdout.on("data", (d) => { stdout = (stdout + d.toString()).slice(-1_000_000); });
28
+ child.stderr.on("data", (d) => { stderr = (stderr + d.toString()).slice(-1_000_000); });
29
+ child.on("error", reject);
30
+ child.on("close", (code) => { done = true; clearTimeout(timer); resolve({ code, stdout, stderr }); });
31
+ });
32
+ }
33
+ export class EditingEngine {
34
+ root;
35
+ constructor(root) {
36
+ this.root = root;
37
+ }
38
+ lexical(relativePath) {
39
+ if (path.isAbsolute(relativePath))
40
+ throw new Error("Absolute paths are not allowed.");
41
+ if (isSensitivePath(relativePath))
42
+ throw new Error(`Access blocked by sensitive-path policy: ${relativePath}`);
43
+ const candidate = path.resolve(this.root, relativePath);
44
+ const prefix = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
45
+ if (candidate !== this.root && !candidate.startsWith(prefix))
46
+ throw new Error("Path escapes PROJECT_ROOT.");
47
+ return candidate;
48
+ }
49
+ async existing(relativePath) {
50
+ const candidate = this.lexical(relativePath);
51
+ const real = await fs.realpath(candidate);
52
+ const realRoot = await fs.realpath(this.root);
53
+ const prefix = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;
54
+ if (real !== realRoot && !real.startsWith(prefix))
55
+ throw new Error("Resolved path escapes PROJECT_ROOT.");
56
+ return real;
57
+ }
58
+ async fileHash(relativePath) {
59
+ return sha256(await fs.readFile(await this.existing(relativePath)));
60
+ }
61
+ applyOne(text, edits) {
62
+ const normalized = edits.map((edit) => {
63
+ const start = edit.startOffset ?? (edit.startLine && edit.startColumn ? offsetAt(text, edit.startLine, edit.startColumn) : NaN);
64
+ const end = edit.endOffset ?? (edit.endLine && edit.endColumn ? offsetAt(text, edit.endLine, edit.endColumn) : start);
65
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || end > text.length)
66
+ throw new Error("Invalid edit range.");
67
+ return { start, end, replacement: edit.replacement };
68
+ }).sort((a, b) => b.start - a.start || b.end - a.end);
69
+ let previousStart = text.length + 1;
70
+ for (const edit of normalized) {
71
+ if (edit.end > previousStart)
72
+ throw new Error("Overlapping edits are not allowed.");
73
+ previousStart = edit.start;
74
+ }
75
+ let updated = text;
76
+ for (const edit of normalized)
77
+ updated = updated.slice(0, edit.start) + edit.replacement + updated.slice(edit.end);
78
+ return updated;
79
+ }
80
+ async applyEdits(files) {
81
+ if (!files.length)
82
+ throw new Error("No edits provided.");
83
+ const prepared = [];
84
+ const seen = new Set();
85
+ for (const fileEdit of files) {
86
+ if (seen.has(fileEdit.path))
87
+ throw new Error(`Duplicate edit target: ${fileEdit.path}`);
88
+ seen.add(fileEdit.path);
89
+ const absolute = await this.existing(fileEdit.path);
90
+ const original = await fs.readFile(absolute);
91
+ const originalHash = sha256(original);
92
+ if (fileEdit.expectedHash && fileEdit.expectedHash !== originalHash)
93
+ throw new Error(`File changed since read. Expected ${fileEdit.expectedHash}, got ${originalHash}.`);
94
+ const text = original.toString("utf8");
95
+ if (text.includes("\u0000"))
96
+ throw new Error(`Binary file cannot be structurally edited: ${fileEdit.path}`);
97
+ const updated = Buffer.from(this.applyOne(text, fileEdit.edits), "utf8");
98
+ prepared.push({
99
+ relative: fileEdit.path,
100
+ absolute,
101
+ original,
102
+ updated,
103
+ originalHash,
104
+ updatedHash: sha256(updated),
105
+ temp: path.join(path.dirname(absolute), `.${path.basename(absolute)}.${randomUUID()}.codelocal.tmp`),
106
+ backup: path.join(path.dirname(absolute), `.${path.basename(absolute)}.${randomUUID()}.codelocal.bak`),
107
+ });
108
+ }
109
+ for (const item of prepared) {
110
+ await fs.writeFile(item.temp, item.updated, { mode: (await fs.stat(item.absolute)).mode });
111
+ }
112
+ const committed = [];
113
+ try {
114
+ for (const item of prepared) {
115
+ await fs.rename(item.absolute, item.backup);
116
+ await fs.rename(item.temp, item.absolute);
117
+ committed.push(item);
118
+ }
119
+ for (const item of committed)
120
+ await fs.unlink(item.backup).catch(() => undefined);
121
+ }
122
+ catch (error) {
123
+ for (const item of committed.reverse()) {
124
+ await fs.unlink(item.absolute).catch(() => undefined);
125
+ await fs.rename(item.backup, item.absolute).catch(() => undefined);
126
+ }
127
+ throw error;
128
+ }
129
+ finally {
130
+ for (const item of prepared) {
131
+ await fs.unlink(item.temp).catch(() => undefined);
132
+ if (!committed.includes(item))
133
+ await fs.unlink(item.backup).catch(() => undefined);
134
+ }
135
+ }
136
+ return {
137
+ changed: prepared.map((item) => ({ path: item.relative, beforeHash: item.originalHash, afterHash: item.updatedHash, bytes: item.updated.length })),
138
+ atomicValidation: true,
139
+ rollbackOnFailure: true,
140
+ };
141
+ }
142
+ async formatChangedFiles(relativePaths) {
143
+ const paths = [...new Set(relativePaths)].filter(Boolean);
144
+ if (!paths.length)
145
+ return { formatted: [], skipped: [] };
146
+ for (const p of paths)
147
+ this.lexical(p);
148
+ const formatted = new Set();
149
+ const skipped = new Set();
150
+ const nodeFiles = paths.filter((p) => /\.(js|jsx|ts|tsx|json|css|scss|md|yaml|yml)$/.test(p));
151
+ if (nodeFiles.length) {
152
+ const prettier = path.join(this.root, "node_modules", ".bin", process.platform === "win32" ? "prettier.cmd" : "prettier");
153
+ const biome = path.join(this.root, "node_modules", ".bin", process.platform === "win32" ? "biome.cmd" : "biome");
154
+ if (await executable(prettier)) {
155
+ const result = await run(prettier, ["--write", ...nodeFiles], this.root);
156
+ if (result.code === 0)
157
+ nodeFiles.forEach((p) => formatted.add(p));
158
+ else
159
+ nodeFiles.forEach((p) => skipped.add(p));
160
+ }
161
+ else if (await executable(biome)) {
162
+ const result = await run(biome, ["format", "--write", ...nodeFiles], this.root);
163
+ if (result.code === 0)
164
+ nodeFiles.forEach((p) => formatted.add(p));
165
+ else
166
+ nodeFiles.forEach((p) => skipped.add(p));
167
+ }
168
+ else
169
+ nodeFiles.forEach((p) => skipped.add(p));
170
+ }
171
+ const groups = [
172
+ { ext: /\.py$/, command: "ruff", args: (p) => ["format", p] },
173
+ { ext: /\.rs$/, command: "rustfmt", args: (p) => [p] },
174
+ { ext: /\.go$/, command: "gofmt", args: (p) => ["-w", p] },
175
+ { ext: /\.(c|cc|cpp|cxx|h|hpp)$/, command: "clang-format", args: (p) => ["-i", p] },
176
+ ];
177
+ for (const group of groups) {
178
+ const candidates = paths.filter((p) => group.ext.test(p) && !formatted.has(p));
179
+ if (!candidates.length)
180
+ continue;
181
+ const available = await this.findOnPath(group.command);
182
+ if (!available) {
183
+ candidates.forEach((p) => skipped.add(p));
184
+ continue;
185
+ }
186
+ for (const p of candidates) {
187
+ const result = await run(group.command, group.args(p), this.root);
188
+ if (result.code === 0)
189
+ formatted.add(p);
190
+ else
191
+ skipped.add(p);
192
+ }
193
+ }
194
+ return { formatted: [...formatted], skipped: [...skipped] };
195
+ }
196
+ async findOnPath(command) {
197
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
198
+ for (const suffix of process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""]) {
199
+ if (await executable(path.join(dir, `${command}${suffix}`)))
200
+ return true;
201
+ }
202
+ }
203
+ return false;
204
+ }
205
+ }
@@ -0,0 +1,30 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { unlink } from "node:fs/promises";
4
+ import { DEFAULT_STATE_DIR, readJsonFile, writeJsonAtomic } from "./state.js";
5
+ const CREDENTIAL_FILE = process.env.CODELOCAL_CREDENTIAL_FILE ?? path.join(DEFAULT_STATE_DIR, "device-credential.json");
6
+ export async function loadLocalCredential(serverUrl) {
7
+ const value = await readJsonFile(CREDENTIAL_FILE, null);
8
+ if (!value)
9
+ return null;
10
+ if (serverUrl && value.serverUrl.replace(/\/$/, "") !== serverUrl.replace(/\/$/, ""))
11
+ return null;
12
+ return value;
13
+ }
14
+ export async function saveLocalCredential(value) {
15
+ const credential = {
16
+ ...value,
17
+ createdAt: value.createdAt ?? Date.now(),
18
+ };
19
+ await writeJsonAtomic(CREDENTIAL_FILE, credential);
20
+ return credential;
21
+ }
22
+ export async function deleteLocalCredential() {
23
+ await unlink(CREDENTIAL_FILE).catch(() => undefined);
24
+ }
25
+ export function defaultDeviceIdentity() {
26
+ return {
27
+ deviceId: process.env.CODELOCAL_DEVICE_ID ?? os.hostname(),
28
+ deviceName: process.env.CODELOCAL_DEVICE_NAME ?? os.hostname(),
29
+ };
30
+ }