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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/agent/editing/lsp-report.mjs +69 -0
  3. package/src/agent/file-edit-tool.mjs +10 -24
  4. package/src/agent/model-payload-dumper.mjs +11 -4
  5. package/src/agent/runner/runner-utils.mjs +18 -0
  6. package/src/agent/runner.mjs +26 -27
  7. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  8. package/src/agent/turn/turn-logging.mjs +30 -0
  9. package/src/agent/turn/turn-runner.mjs +40 -0
  10. package/src/cli/commands/status-command.mjs +4 -16
  11. package/src/cli/permissions.mjs +1 -1
  12. package/src/cli/shell/shell-drawer.mjs +1 -1
  13. package/src/cli/startup/runtime-close.mjs +23 -0
  14. package/src/cli/tui/output/visible-lines.mjs +8 -0
  15. package/src/cli/tui/output-buffer.mjs +30 -21
  16. package/src/cli/tui/render/stream-delta-buffer.mjs +46 -0
  17. package/src/cli/tui/selection-screen.mjs +12 -4
  18. package/src/cli/tui/tool-rendering.mjs +1 -1
  19. package/src/cli/ui.mjs +16 -17
  20. package/src/config/loader.mjs +28 -1
  21. package/src/context/system-core/base.md +1 -1
  22. package/src/debug/logger.mjs +141 -0
  23. package/src/lsp/client.mjs +2 -2
  24. package/src/lsp/diagnostic-store.mjs +5 -2
  25. package/src/lsp/diagnostics-format.mjs +3 -1
  26. package/src/lsp/managed-node-server.mjs +94 -0
  27. package/src/lsp/path-match.mjs +10 -0
  28. package/src/lsp/servers.mjs +56 -13
  29. package/src/lsp/service.mjs +6 -6
  30. package/src/lsp/status-message.mjs +1 -0
  31. package/src/lsp/typescript-project-resolver.mjs +186 -0
  32. package/src/main.mjs +17 -24
  33. package/src/platform/spawn-command.mjs +27 -0
  34. package/src/provider/hosted-tools.mjs +111 -0
  35. package/src/shell/runtime.mjs +9 -1
  36. 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
+ }
@@ -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.rootMarkers.length > 0
143
- ? findNearestRoot(dirname(filePath), workspaceRoot, def.rootMarkers) ?? workspaceRoot
144
- : workspaceRoot;
145
- const command = findCommand(def.command, { root, workspaceRoot });
146
- if (!command) {
147
- return { status: "unavailable", id: def.id, root, reason: `missing ${def.command[0]}` };
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) : null;
249
+ return tsserver ? dirname(tsserver) : findManagedTypeScriptSdk();
207
250
  }
208
251
 
209
252
  function resolveModuleFromRoots(id, { root, workspaceRoot }) {
@@ -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 diagnostics = this.store.snapshot();
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
+ }