march-cli 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/agent/editing/lsp-report.mjs +69 -0
- package/src/agent/file-edit-tool.mjs +10 -24
- package/src/agent/model-payload-dumper.mjs +11 -4
- package/src/agent/runner/runner-utils.mjs +18 -0
- package/src/agent/runner.mjs +26 -27
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/turn/turn-logging.mjs +30 -0
- package/src/agent/turn/turn-runner.mjs +40 -0
- package/src/cli/commands/status-command.mjs +4 -16
- package/src/cli/permissions.mjs +1 -1
- package/src/cli/shell/shell-drawer.mjs +1 -1
- package/src/cli/startup/runtime-close.mjs +23 -0
- package/src/cli/tui/output/visible-lines.mjs +8 -0
- package/src/cli/tui/output-buffer.mjs +30 -21
- package/src/cli/tui/render/stream-delta-buffer.mjs +46 -0
- package/src/cli/tui/selection-screen.mjs +12 -4
- package/src/cli/tui/tool-rendering.mjs +1 -1
- package/src/cli/ui.mjs +16 -17
- package/src/config/loader.mjs +28 -1
- package/src/context/system-core/base.md +1 -1
- package/src/debug/logger.mjs +141 -0
- package/src/lsp/client.mjs +2 -2
- package/src/lsp/diagnostic-store.mjs +5 -2
- package/src/lsp/diagnostics-format.mjs +3 -1
- package/src/lsp/managed-node-server.mjs +94 -0
- package/src/lsp/path-match.mjs +10 -0
- package/src/lsp/servers.mjs +56 -13
- package/src/lsp/service.mjs +6 -6
- package/src/lsp/status-message.mjs +1 -0
- package/src/lsp/typescript-project-resolver.mjs +186 -0
- package/src/main.mjs +17 -24
- package/src/platform/spawn-command.mjs +27 -0
- package/src/provider/hosted-tools.mjs +111 -0
- package/src/shell/runtime.mjs +9 -1
- package/src/web/tools.mjs +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnCommand } from "../platform/spawn-command.mjs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CACHE_ROOT = join(homedir(), ".march", "lsp", "node");
|
|
7
|
+
|
|
8
|
+
const MANAGED_PACKAGES = {
|
|
9
|
+
"typescript-language-server": ["typescript-language-server", "typescript"],
|
|
10
|
+
"vue-language-server": ["@vue/language-server", "typescript"],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const installs = new Map();
|
|
14
|
+
|
|
15
|
+
export function getManagedNodeLspRoot() {
|
|
16
|
+
return process.env.MARCH_LSP_NODE_ROOT || DEFAULT_CACHE_ROOT;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureManagedNodeCommand(name) {
|
|
20
|
+
if (!MANAGED_PACKAGES[name]) return null;
|
|
21
|
+
const root = getManagedNodeLspRoot();
|
|
22
|
+
const existing = findManagedBin(name, root);
|
|
23
|
+
if (existing) return { command: existing, root, installed: false };
|
|
24
|
+
|
|
25
|
+
await ensureManagedPackages(name, root);
|
|
26
|
+
const command = findManagedBin(name, root);
|
|
27
|
+
if (!command) throw new Error(`managed install did not provide ${name}`);
|
|
28
|
+
return { command, root, installed: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function ensureManagedTypeScript() {
|
|
32
|
+
const root = getManagedNodeLspRoot();
|
|
33
|
+
if (findManagedTypeScriptServer()) return { root, installed: false };
|
|
34
|
+
await installPackages(root, ["typescript"]);
|
|
35
|
+
if (!findManagedTypeScriptServer()) throw new Error("managed install did not provide typescript");
|
|
36
|
+
return { root, installed: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findManagedTypeScriptServer() {
|
|
40
|
+
const root = getManagedNodeLspRoot();
|
|
41
|
+
const tsserver = join(root, "node_modules", "typescript", "lib", "tsserver.js");
|
|
42
|
+
return existsSync(tsserver) ? tsserver : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findManagedTypeScriptSdk() {
|
|
46
|
+
const root = getManagedNodeLspRoot();
|
|
47
|
+
const lib = join(root, "node_modules", "typescript", "lib");
|
|
48
|
+
return existsSync(join(lib, "tsserverlibrary.js")) || existsSync(join(lib, "tsserver.js")) ? lib : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function ensureManagedPackages(name, root) {
|
|
52
|
+
const key = `${root}:${name}`;
|
|
53
|
+
if (!installs.has(key)) {
|
|
54
|
+
installs.set(key, installPackages(root, MANAGED_PACKAGES[name]).finally(() => installs.delete(key)));
|
|
55
|
+
}
|
|
56
|
+
return await installs.get(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function installPackages(root, packages) {
|
|
60
|
+
mkdirSync(root, { recursive: true });
|
|
61
|
+
const packageJson = join(root, "package.json");
|
|
62
|
+
if (!existsSync(packageJson)) writeFileSync(packageJson, JSON.stringify({ private: true, name: "march-managed-lsp" }, null, 2));
|
|
63
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
64
|
+
await run(npm, ["install", "--no-audit", "--no-fund", "--save-exact", ...packages], { cwd: root });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findManagedBin(name, root) {
|
|
68
|
+
for (const bin of platformCommandNames(name)) {
|
|
69
|
+
const candidate = join(root, "node_modules", ".bin", bin);
|
|
70
|
+
if (existsSync(candidate)) return candidate;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function platformCommandNames(name) {
|
|
76
|
+
if (process.platform !== "win32") return [name];
|
|
77
|
+
return [`${name}.cmd`, `${name}.exe`, name];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function run(command, args, { cwd }) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const child = spawnCommand(command, args, { cwd, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
83
|
+
let stderr = "";
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
stderr += chunk.toString("utf8");
|
|
86
|
+
if (stderr.length > 4000) stderr = stderr.slice(-4000);
|
|
87
|
+
});
|
|
88
|
+
child.on("error", reject);
|
|
89
|
+
child.on("exit", (code) => {
|
|
90
|
+
if (code === 0) resolve();
|
|
91
|
+
else reject(new Error(`${command} ${args.join(" ")} failed with exit ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { normalize } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sameLspPath(left, right) {
|
|
4
|
+
return lspPathKey(left) === lspPathKey(right);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function lspPathKey(path) {
|
|
8
|
+
const normalized = normalize(String(path ?? ""));
|
|
9
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
10
|
+
}
|
package/src/lsp/servers.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { ensureManagedNodeCommand, ensureManagedTypeScript, findManagedTypeScriptSdk, findManagedTypeScriptServer } from "./managed-node-server.mjs";
|
|
5
|
+
import { resolveTypeScriptProjectRoot } from "./typescript-project-resolver.mjs";
|
|
4
6
|
|
|
5
7
|
const NODE_ROOT_MARKERS = ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock", "package.json"];
|
|
6
8
|
|
|
@@ -10,23 +12,28 @@ const LSP_SERVERS = [
|
|
|
10
12
|
extensions: [".vue"],
|
|
11
13
|
rootMarkers: NODE_ROOT_MARKERS,
|
|
12
14
|
command: ["vue-language-server"],
|
|
15
|
+
managedCommand: "vue-language-server",
|
|
13
16
|
args: ["--stdio"],
|
|
14
17
|
initialization: ({ root, workspaceRoot }) => {
|
|
15
18
|
const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
|
|
16
19
|
return tsdk ? { typescript: { tsdk } } : null;
|
|
17
20
|
},
|
|
21
|
+
managedTypeScript: true,
|
|
18
22
|
missingInitialization: "missing project typescript SDK",
|
|
19
23
|
},
|
|
20
24
|
{
|
|
21
25
|
id: "typescript",
|
|
22
26
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
23
27
|
rootMarkers: NODE_ROOT_MARKERS,
|
|
28
|
+
projectRoot: resolveTypeScriptProjectRoot,
|
|
24
29
|
command: ["typescript-language-server"],
|
|
30
|
+
managedCommand: "typescript-language-server",
|
|
25
31
|
args: ["--stdio"],
|
|
26
32
|
initialization: ({ root, workspaceRoot }) => {
|
|
27
33
|
const tsserver = resolveTypeScriptServer({ root, workspaceRoot });
|
|
28
34
|
return tsserver ? { tsserver: { path: tsserver } } : null;
|
|
29
35
|
},
|
|
36
|
+
managedTypeScript: true,
|
|
30
37
|
missingInitialization: "missing project typescript/tsserver.js",
|
|
31
38
|
},
|
|
32
39
|
{
|
|
@@ -129,23 +136,23 @@ const LSP_SERVERS = [
|
|
|
129
136
|
},
|
|
130
137
|
];
|
|
131
138
|
|
|
132
|
-
export function resolveLspServer({ filePath, workspaceRoot }) {
|
|
133
|
-
const result = resolveLspServerStatus({ filePath, workspaceRoot });
|
|
139
|
+
export async function resolveLspServer({ filePath, workspaceRoot, onEvent = null } = {}) {
|
|
140
|
+
const result = await resolveLspServerStatus({ filePath, workspaceRoot, onEvent });
|
|
134
141
|
return result.status === "available" ? result.server : null;
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
export function resolveLspServerStatus({ filePath, workspaceRoot }) {
|
|
144
|
+
export async function resolveLspServerStatus({ filePath, workspaceRoot, onEvent = null } = {}) {
|
|
138
145
|
const ext = extensionOf(filePath);
|
|
139
146
|
const def = LSP_SERVERS.find((server) => server.extensions.includes(ext));
|
|
140
147
|
if (!def) return { status: "unsupported", extension: ext };
|
|
141
148
|
|
|
142
|
-
const root = def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
+
const root = resolveServerRoot(def, { filePath, workspaceRoot });
|
|
150
|
+
const command = await resolveCommand(def, { root, workspaceRoot, onEvent });
|
|
151
|
+
if (command?.error) return { status: "unavailable", id: def.id, root, reason: command.error };
|
|
152
|
+
if (!command) return { status: "unavailable", id: def.id, root, reason: `missing ${def.command[0]}` };
|
|
153
|
+
|
|
154
|
+
const managedTypeScript = await ensureTypeScriptFallback(def, { root, workspaceRoot, onEvent });
|
|
155
|
+
if (managedTypeScript?.error) return { status: "unavailable", id: def.id, root, reason: managedTypeScript.error };
|
|
149
156
|
|
|
150
157
|
const initialization = def.initialization?.({ root, workspaceRoot }) ?? {};
|
|
151
158
|
if (initialization === null) {
|
|
@@ -153,7 +160,7 @@ export function resolveLspServerStatus({ filePath, workspaceRoot }) {
|
|
|
153
160
|
}
|
|
154
161
|
return {
|
|
155
162
|
status: "available",
|
|
156
|
-
server: { id: def.id, command, args: def.args, root, initialization },
|
|
163
|
+
server: { id: def.id, command: command.command, args: def.args, root, initialization, managed: command.managed },
|
|
157
164
|
};
|
|
158
165
|
}
|
|
159
166
|
|
|
@@ -161,6 +168,42 @@ export function listLspServerDefinitions() {
|
|
|
161
168
|
return LSP_SERVERS.map(({ id, extensions, rootMarkers, command, args }) => ({ id, extensions, rootMarkers, command, args }));
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
async function resolveCommand(def, { root, workspaceRoot, onEvent }) {
|
|
172
|
+
const local = findCommand(def.command, { root, workspaceRoot });
|
|
173
|
+
if (local) return { command: local, managed: false };
|
|
174
|
+
if (!def.managedCommand) return null;
|
|
175
|
+
onEvent?.({ status: "installing", id: def.id, root, reason: `installing ${def.managedCommand}` });
|
|
176
|
+
try {
|
|
177
|
+
const managed = await ensureManagedNodeCommand(def.managedCommand);
|
|
178
|
+
return { command: managed.command, managed: true };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const reason = err.message;
|
|
181
|
+
onEvent?.({ status: "failed", id: def.id, root, reason });
|
|
182
|
+
return { error: reason };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function ensureTypeScriptFallback(def, { root, workspaceRoot, onEvent }) {
|
|
187
|
+
if (!def.managedTypeScript || resolveTypeScriptServer({ root, workspaceRoot })) return null;
|
|
188
|
+
onEvent?.({ status: "installing", id: def.id, root, reason: "installing typescript" });
|
|
189
|
+
try {
|
|
190
|
+
await ensureManagedTypeScript();
|
|
191
|
+
return null;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const reason = err.message;
|
|
194
|
+
onEvent?.({ status: "failed", id: def.id, root, reason });
|
|
195
|
+
return { error: reason };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveServerRoot(def, { filePath, workspaceRoot }) {
|
|
200
|
+
const projectRoot = def.projectRoot?.({ filePath, workspaceRoot });
|
|
201
|
+
if (projectRoot) return projectRoot;
|
|
202
|
+
return def.rootMarkers.length > 0
|
|
203
|
+
? findNearestRoot(dirname(filePath), workspaceRoot, def.rootMarkers) ?? workspaceRoot
|
|
204
|
+
: workspaceRoot;
|
|
205
|
+
}
|
|
206
|
+
|
|
164
207
|
function findCommand(names, { root, workspaceRoot }) {
|
|
165
208
|
for (const name of names) {
|
|
166
209
|
const hit = findBin(name, { root, workspaceRoot });
|
|
@@ -197,13 +240,13 @@ function findOnPath(names) {
|
|
|
197
240
|
}
|
|
198
241
|
|
|
199
242
|
function resolveTypeScriptServer({ root, workspaceRoot }) {
|
|
200
|
-
return resolveModuleFromRoots("typescript/lib/tsserver.js", { root, workspaceRoot });
|
|
243
|
+
return resolveModuleFromRoots("typescript/lib/tsserver.js", { root, workspaceRoot }) ?? findManagedTypeScriptServer();
|
|
201
244
|
}
|
|
202
245
|
|
|
203
246
|
function resolveTypeScriptSdk({ root, workspaceRoot }) {
|
|
204
247
|
const serverLibrary = resolveModuleFromRoots("typescript/lib/tsserverlibrary.js", { root, workspaceRoot });
|
|
205
248
|
const tsserver = serverLibrary ?? resolveTypeScriptServer({ root, workspaceRoot });
|
|
206
|
-
return tsserver ? dirname(tsserver) :
|
|
249
|
+
return tsserver ? dirname(tsserver) : findManagedTypeScriptSdk();
|
|
207
250
|
}
|
|
208
251
|
|
|
209
252
|
function resolveModuleFromRoots(id, { root, workspaceRoot }) {
|
package/src/lsp/service.mjs
CHANGED
|
@@ -13,8 +13,8 @@ export class LspService {
|
|
|
13
13
|
this.announced = new Set();
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
touchFile(path) {
|
|
17
|
-
const result = resolveLspServerStatus({ filePath: path, workspaceRoot: this.cwd });
|
|
16
|
+
async touchFile(path) {
|
|
17
|
+
const result = await resolveLspServerStatus({ filePath: path, workspaceRoot: this.cwd, onEvent: (event) => this.onEvent?.(event) });
|
|
18
18
|
if (result.status === "unsupported") return result;
|
|
19
19
|
if (result.status === "unavailable") {
|
|
20
20
|
this.unavailable.set(result.id, result);
|
|
@@ -34,7 +34,7 @@ export class LspService {
|
|
|
34
34
|
return { status: "starting", id: server.id, root: server.root };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root });
|
|
37
|
+
this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root, managed: server.managed });
|
|
38
38
|
const task = this.#startClient(server, key).then((client) => {
|
|
39
39
|
client?.touchFile(path);
|
|
40
40
|
return client;
|
|
@@ -47,7 +47,7 @@ export class LspService {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
snapshot() {
|
|
50
|
-
const
|
|
50
|
+
const storeSnapshot = this.store.snapshot();
|
|
51
51
|
const servers = [
|
|
52
52
|
...[...this.clients.values()].map((client) => ({
|
|
53
53
|
id: client.serverId,
|
|
@@ -61,7 +61,7 @@ export class LspService {
|
|
|
61
61
|
})),
|
|
62
62
|
...this.unavailable.values(),
|
|
63
63
|
];
|
|
64
|
-
return { status: summarizeStatus(servers), diagnostics, servers };
|
|
64
|
+
return { status: summarizeStatus(servers), diagnostics: storeSnapshot.diagnostics, files: storeSnapshot.files, servers };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
async dispose() {
|
|
@@ -81,7 +81,7 @@ export class LspService {
|
|
|
81
81
|
await client.start();
|
|
82
82
|
this.clients.set(key, client);
|
|
83
83
|
this.unavailable.delete(server.id);
|
|
84
|
-
this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root });
|
|
84
|
+
this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root, managed: server.managed });
|
|
85
85
|
return client;
|
|
86
86
|
} catch (err) {
|
|
87
87
|
client.status = "failed";
|
|
@@ -2,6 +2,7 @@ export function formatLspServiceEvent(event) {
|
|
|
2
2
|
const id = event?.id ? String(event.id) : "server";
|
|
3
3
|
if (event?.status === "attached") return `LSP attached: ${id}`;
|
|
4
4
|
if (event?.status === "starting") return `LSP starting: ${id}`;
|
|
5
|
+
if (event?.status === "installing") return `LSP installing: ${id} - ${event.reason}`;
|
|
5
6
|
if (event?.status === "failed") return `LSP failed: ${id} - ${event.reason}`;
|
|
6
7
|
if (event?.status === "unavailable") return `LSP unavailable: ${id} - ${event.reason}`;
|
|
7
8
|
return `LSP ${event?.status ?? "status"}: ${id}`;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const CONFIG_RE = /^(?:tsconfig(?:\..+)?|jsconfig)\.json$/;
|
|
5
|
+
const DEFAULT_EXCLUDES = ["node_modules", "bower_components", "jspm_packages"];
|
|
6
|
+
const MATCH_EXTENSIONS = [".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
7
|
+
|
|
8
|
+
export function resolveTypeScriptProjectRoot({ filePath, workspaceRoot }) {
|
|
9
|
+
const configs = findTypeScriptConfigs(dirname(filePath), workspaceRoot);
|
|
10
|
+
if (configs.length === 0) return null;
|
|
11
|
+
|
|
12
|
+
const included = configs.find((config) => configIncludesFile(config, filePath));
|
|
13
|
+
return dirname(included ?? configs[0]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findTypeScriptConfigs(start, stop) {
|
|
17
|
+
const configs = [];
|
|
18
|
+
let dir = resolve(start);
|
|
19
|
+
const boundary = resolve(stop);
|
|
20
|
+
for (;;) {
|
|
21
|
+
configs.push(...configFilesIn(dir));
|
|
22
|
+
if (dir === boundary || dirname(dir) === dir) return configs;
|
|
23
|
+
dir = dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function configFilesIn(dir) {
|
|
28
|
+
if (!existsSync(dir)) return [];
|
|
29
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
30
|
+
.filter((entry) => entry.isFile() && CONFIG_RE.test(entry.name))
|
|
31
|
+
.map((entry) => entry.name)
|
|
32
|
+
.sort(configSort)
|
|
33
|
+
.map((name) => join(dir, name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function configSort(a, b) {
|
|
37
|
+
return configRank(a) - configRank(b) || a.localeCompare(b);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function configRank(name) {
|
|
41
|
+
if (name === "tsconfig.json") return 0;
|
|
42
|
+
if (name === "jsconfig.json") return 1;
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function configIncludesFile(configPath, filePath) {
|
|
47
|
+
const config = readConfigChain(configPath);
|
|
48
|
+
if (!config) return false;
|
|
49
|
+
|
|
50
|
+
const exclude = config.exclude ?? DEFAULT_EXCLUDES.map((pattern) => ({ base: dirname(configPath), pattern }));
|
|
51
|
+
if (exclude.some(({ base, pattern }) => matchesPatternFromBase(filePath, base, pattern))) return false;
|
|
52
|
+
|
|
53
|
+
if (config.files) {
|
|
54
|
+
return config.files.some(({ base, pattern }) => normalizePath(relative(base, filePath)) === normalizePath(pattern));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (config.include) {
|
|
58
|
+
return config.include.some(({ base, pattern }) => matchesPatternFromBase(filePath, base, pattern));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const file = normalizePath(relative(dirname(configPath), filePath));
|
|
62
|
+
return !isOutside(file) && MATCH_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readConfigChain(path, seen = new Set()) {
|
|
66
|
+
if (seen.has(path)) return null;
|
|
67
|
+
seen.add(path);
|
|
68
|
+
|
|
69
|
+
const raw = readConfig(path);
|
|
70
|
+
if (!raw) return null;
|
|
71
|
+
const base = resolveExtends(path, raw.extends, seen);
|
|
72
|
+
return {
|
|
73
|
+
files: patternList(raw.files, dirname(path)) ?? base?.files ?? null,
|
|
74
|
+
include: patternList(raw.include, dirname(path)) ?? base?.include ?? null,
|
|
75
|
+
exclude: patternList(raw.exclude, dirname(path)) ?? base?.exclude ?? null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveExtends(configPath, value, seen) {
|
|
80
|
+
if (typeof value !== "string" || !value.startsWith(".")) return null;
|
|
81
|
+
const resolved = resolve(dirname(configPath), value.endsWith(".json") ? value : `${value}.json`);
|
|
82
|
+
return existsSync(resolved) ? readConfigChain(resolved, seen) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readConfig(path) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function patternList(value, base) {
|
|
94
|
+
const items = arrayOfStrings(value);
|
|
95
|
+
return items ? items.map((pattern) => ({ base, pattern })) : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function arrayOfStrings(value) {
|
|
99
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isOutside(path) {
|
|
103
|
+
return path === ".." || path.startsWith("../") || path.includes(":");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function matchesPatternFromBase(filePath, base, pattern) {
|
|
107
|
+
const file = normalizePath(relative(base, filePath));
|
|
108
|
+
return !isOutside(file) && matchesConfigPattern(file, pattern);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function matchesConfigPattern(file, pattern) {
|
|
112
|
+
const normalized = normalizePath(pattern);
|
|
113
|
+
if (!normalized.includes("*") && file === normalized) return true;
|
|
114
|
+
const glob = normalized.includes("*") ? normalized : `${trimSlash(normalized)}/**/*`;
|
|
115
|
+
return globToRegExp(glob).test(file);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function globToRegExp(glob) {
|
|
119
|
+
let source = "^";
|
|
120
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
121
|
+
const char = glob[i];
|
|
122
|
+
if (char === "*") {
|
|
123
|
+
if (glob[i + 1] === "*") {
|
|
124
|
+
i += 1;
|
|
125
|
+
if (glob[i + 1] === "/") {
|
|
126
|
+
i += 1;
|
|
127
|
+
source += "(?:.*/)?";
|
|
128
|
+
} else {
|
|
129
|
+
source += ".*";
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
source += "[^/]*";
|
|
133
|
+
}
|
|
134
|
+
} else if (char === "?") {
|
|
135
|
+
source += "[^/]";
|
|
136
|
+
} else {
|
|
137
|
+
source += escapeRegExp(char);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return new RegExp(`${source}$`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function trimSlash(path) {
|
|
144
|
+
return path.replace(/\/+$/, "");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizePath(path) {
|
|
148
|
+
return path.replaceAll("\\", "/");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function escapeRegExp(value) {
|
|
152
|
+
return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stripJsonComments(source) {
|
|
156
|
+
let out = "";
|
|
157
|
+
let inString = false;
|
|
158
|
+
let quote = "";
|
|
159
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
160
|
+
const char = source[i];
|
|
161
|
+
const next = source[i + 1];
|
|
162
|
+
if (inString) {
|
|
163
|
+
out += char;
|
|
164
|
+
if (char === "\\") {
|
|
165
|
+
out += next ?? "";
|
|
166
|
+
i += 1;
|
|
167
|
+
} else if (char === quote) {
|
|
168
|
+
inString = false;
|
|
169
|
+
}
|
|
170
|
+
} else if (char === '"' || char === "'") {
|
|
171
|
+
inString = true;
|
|
172
|
+
quote = char;
|
|
173
|
+
out += char;
|
|
174
|
+
} else if (char === "/" && next === "/") {
|
|
175
|
+
while (i < source.length && source[i] !== "\n") i += 1;
|
|
176
|
+
out += "\n";
|
|
177
|
+
} else if (char === "/" && next === "*") {
|
|
178
|
+
i += 2;
|
|
179
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1;
|
|
180
|
+
i += 1;
|
|
181
|
+
} else {
|
|
182
|
+
out += char;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out.replace(/,\s*([}\]])/g, "$1");
|
|
186
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { createInputHistoryStore } from "./cli/input/history-store.mjs";
|
|
|
10
10
|
import { createModeState } from "./cli/input/mode-state.mjs";
|
|
11
11
|
import { loadPromptTemplates } from "./cli/input/prompt-templates.mjs";
|
|
12
12
|
import { runInteractiveRepl, runSingleShotPrompt } from "./cli/repl-loop.mjs";
|
|
13
|
+
import { closeMarchRuntime } from "./cli/startup/runtime-close.mjs";
|
|
13
14
|
import { createStatusLineUpdater } from "./cli/status-line-updater.mjs";
|
|
14
15
|
import { wireTuiHandlers } from "./cli/tui/tui-handlers.mjs";
|
|
15
16
|
import { createMarchAuthStorage } from "./auth/storage.mjs";
|
|
@@ -28,6 +29,7 @@ import { formatStartupBanner } from "./cli/startup/startup-banner.mjs";
|
|
|
28
29
|
import { initializeMcp } from "./mcp/index.mjs";
|
|
29
30
|
import { createWebToolsFromConfig } from "./web/tools.mjs";
|
|
30
31
|
import { createModelContextDumper } from "./debug/model-context-dumper.mjs";
|
|
32
|
+
import { createLogger, installProcessLogHandlers } from "./debug/logger.mjs";
|
|
31
33
|
import { defaultCenterMemoryPath } from "./context/center-memory.mjs";
|
|
32
34
|
import { runProviderConfigCommand } from "./provider/config-command.mjs";
|
|
33
35
|
import { runWebSearchConfigCommand } from "./web/config-command.mjs";
|
|
@@ -68,6 +70,15 @@ export async function run(argv) {
|
|
|
68
70
|
|
|
69
71
|
const stateRoot = join(homedir(), ".march");
|
|
70
72
|
if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
|
|
73
|
+
const logger = createLogger({ logDir: join(stateRoot, "logs") });
|
|
74
|
+
installProcessLogHandlers(logger);
|
|
75
|
+
logger.event("process.start", {
|
|
76
|
+
cwd,
|
|
77
|
+
argv,
|
|
78
|
+
version: process.version,
|
|
79
|
+
platform: process.platform,
|
|
80
|
+
logPath: logger.path,
|
|
81
|
+
});
|
|
71
82
|
|
|
72
83
|
const provider = args.provider ?? config.provider ?? null;
|
|
73
84
|
const serviceTier = config.serviceTier ?? null;
|
|
@@ -96,7 +107,6 @@ export async function run(argv) {
|
|
|
96
107
|
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
|
|
97
108
|
const memoryTools = createMarkdownMemoryTools(memoryStore);
|
|
98
109
|
const currentProject = basename(cwd);
|
|
99
|
-
|
|
100
110
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
101
111
|
|
|
102
112
|
// MCP: connect to configured MCP servers
|
|
@@ -182,9 +192,11 @@ export async function run(argv) {
|
|
|
182
192
|
authStorage: authConfig.authStorage,
|
|
183
193
|
maxTurns: config.maxTurns ?? undefined,
|
|
184
194
|
trimBatch: config.trimBatch ?? undefined,
|
|
195
|
+
hostedTools: config.hostedTools,
|
|
185
196
|
permissionController,
|
|
186
197
|
modelContextDumper,
|
|
187
198
|
turnNotifier,
|
|
199
|
+
logger,
|
|
188
200
|
onModelPayload: ({ estimatedTokens }) => {
|
|
189
201
|
refreshStatusBar?.({ contextTokens: estimatedTokens });
|
|
190
202
|
},
|
|
@@ -235,8 +247,9 @@ export async function run(argv) {
|
|
|
235
247
|
});
|
|
236
248
|
} finally {
|
|
237
249
|
turnRunning = false;
|
|
238
|
-
await closeMarchRuntime({ runner, memoryStore, ui, blankLine: true });
|
|
250
|
+
await closeMarchRuntime({ runner, memoryStore, ui, logger, blankLine: true });
|
|
239
251
|
}
|
|
252
|
+
logger.event("process.exit", { code: 0 });
|
|
240
253
|
return 0;
|
|
241
254
|
}
|
|
242
255
|
|
|
@@ -264,32 +277,12 @@ export async function run(argv) {
|
|
|
264
277
|
modeState,
|
|
265
278
|
});
|
|
266
279
|
} finally {
|
|
267
|
-
await closeMarchRuntime({ runner, memoryStore, ui });
|
|
280
|
+
await closeMarchRuntime({ runner, memoryStore, ui, logger });
|
|
268
281
|
}
|
|
282
|
+
logger.event("process.exit", { code: 0 });
|
|
269
283
|
return 0;
|
|
270
284
|
}
|
|
271
285
|
|
|
272
|
-
async function closeMarchRuntime({ runner, memoryStore, ui, blankLine = false }) {
|
|
273
|
-
let firstError = null;
|
|
274
|
-
try {
|
|
275
|
-
await runner.dispose();
|
|
276
|
-
} catch (err) {
|
|
277
|
-
firstError ??= err;
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
memoryStore.close();
|
|
281
|
-
} catch (err) {
|
|
282
|
-
firstError ??= err;
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
if (blankLine) ui.writeln("");
|
|
286
|
-
await ui.close();
|
|
287
|
-
} catch (err) {
|
|
288
|
-
firstError ??= err;
|
|
289
|
-
}
|
|
290
|
-
if (firstError) throw firstError;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
286
|
function resolveMemoryRoot(configured, stateRoot) {
|
|
294
287
|
if (configured) return resolve(String(configured));
|
|
295
288
|
if (process.env.MARCH_MEMORY_ROOT) return resolve(process.env.MARCH_MEMORY_ROOT);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function spawnCommand(command, args = [], options = {}) {
|
|
4
|
+
const resolved = resolveSpawnCommand(command, args);
|
|
5
|
+
return spawn(resolved.command, resolved.args, { ...options, ...resolved.options });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveSpawnCommand(command, args = []) {
|
|
9
|
+
if (process.platform !== "win32" || !isWindowsScriptCommand(command)) {
|
|
10
|
+
return { command, args };
|
|
11
|
+
}
|
|
12
|
+
// Node can fail to spawn .cmd/.bat directly on Windows; cmd.exe runs them reliably.
|
|
13
|
+
return {
|
|
14
|
+
command: "cmd.exe",
|
|
15
|
+
args: ["/d", "/s", "/c", `"${[quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ")}"`],
|
|
16
|
+
options: { windowsVerbatimArguments: true },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isWindowsScriptCommand(command) {
|
|
21
|
+
const lower = command.toLowerCase();
|
|
22
|
+
return lower.endsWith(".cmd") || lower.endsWith(".bat");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function quoteCmdArg(value) {
|
|
26
|
+
return /[\s&()^|<>"%]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
|
|
27
|
+
}
|