portal-agent-cli 3.0.3 → 3.0.5

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/lib/repl.mjs CHANGED
@@ -1,237 +1,2 @@
1
1
  import { createInterface } from "node:readline";
2
- import { existsSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { launch } from "./browser.mjs";
5
- import { getProvider, listProviders } from "./providers.mjs";
6
- import { waitForComposer } from "./wait.mjs";
7
- import { enableDeepThink } from "./deepthink.mjs";
8
- import { runTurn } from "./turn.mjs";
9
- import { setWorkspace, getWorkspace } from "./tools/path.mjs";
10
- import { setIgnore } from "./tools/walk.mjs";
11
- import { loadInstructions, activeInstructionFile } from "./instructions.mjs";
12
- import { newSessionId, saveSession, loadSession, listSessions } from "./sessions.mjs";
13
- import { theme } from "./theme.mjs";
14
- import { info, warn, error, ok } from "./log.mjs";
15
-
16
- export async function runRepl(config) {
17
- const provider = getProvider(config.provider);
18
- setWorkspace(config.ws);
19
- setIgnore(config.ignored);
20
-
21
- config.instructionsText = await loadInstructions(config.ws);
22
-
23
- let session;
24
- if (config.resume) {
25
- session = await loadSession(config.resume);
26
- if (!session) {
27
- warn("session not found. starting fresh.");
28
- session = newSession(config);
29
- }
30
- } else {
31
- session = newSession(config);
32
- }
33
-
34
- process.stdout.write("\n");
35
- process.stdout.write(theme.bold(theme.cyan("Portal")) + theme.dim(" v3.0.2") + "\n");
36
- process.stdout.write(theme.dim("workspace: ") + theme.accent(getWorkspace()) + "\n");
37
- process.stdout.write(theme.dim("provider: ") + theme.accent(provider.label) + "\n");
38
- process.stdout.write(theme.dim("session: ") + theme.accent(session.id) + "\n");
39
- if (config.instructionsText) {
40
- process.stdout.write(theme.dim("instructions: ") +
41
- theme.accent(activeInstructionFile() || "?") + "\n");
42
- }
43
- process.stdout.write("\n");
44
-
45
- const ctx = await launch();
46
- const page = await ctx.newPage();
47
- page.setDefaultTimeout(30000);
48
-
49
- info("opening " + provider.label + "...");
50
- await page.goto(provider.url);
51
-
52
- process.stdout.write("\n");
53
- process.stdout.write(theme.yellow("Log in to " + provider.label +
54
- " in the Chrome window.") + "\n");
55
- process.stdout.write(theme.dim("Waiting up to " +
56
- Math.round(config.loginTimeout / 1000) + "s.") + "\n");
57
- process.stdout.write("\n");
58
-
59
- await waitForComposer(page, provider, config.loginTimeout);
60
-
61
- if (provider.deepthink) {
62
- const on = await enableDeepThink(page);
63
- if (on) process.stdout.write(theme.green("+ DeepThink enabled") + "\n");
64
- }
65
-
66
- ok(provider.label + " ready");
67
- process.stdout.write(theme.dim("type /help for commands") + "\n");
68
- process.stdout.write("\n");
69
-
70
- const rl = createInterface({
71
- input: process.stdin,
72
- output: process.stdout,
73
- prompt: theme.user("> ")
74
- });
75
- rl.prompt();
76
-
77
- let busy = false;
78
- const queue = [];
79
-
80
- async function drain() {
81
- if (busy) return;
82
- const next = queue.shift();
83
- if (!next) return;
84
- busy = true;
85
- try {
86
- await runTurn(page, provider, next, config, session);
87
- await saveSession(session.id, session);
88
- } catch (e) {
89
- error(e.message || String(e));
90
- }
91
- busy = false;
92
- process.stdout.write("\n");
93
- rl.prompt();
94
- if (queue.length > 0) drain();
95
- }
96
-
97
- rl.on("line", async function (line) {
98
- const t = line.trim();
99
- if (!t) { rl.prompt(); return; }
100
-
101
- if (t === "/quit" || t === "/exit") { rl.close(); return; }
102
- if (t === "/help") { help(); rl.prompt(); return; }
103
- if (t === "/pwd") { process.stdout.write(getWorkspace() + "\n"); rl.prompt(); return; }
104
- if (t === "/queue") { showQueue(queue); rl.prompt(); return; }
105
- if (t === "/status") { showStatus(config, session, queue, busy); rl.prompt(); return; }
106
- if (t === "/clear") { clearScreen(); rl.prompt(); return; }
107
- if (t === "/deepthink") {
108
- const on = await enableDeepThink(page);
109
- process.stdout.write((on ? theme.ok("+") : theme.warn("!")) +
110
- " DeepThink " + (on ? "on" : "not found") + "\n");
111
- rl.prompt();
112
- return;
113
- }
114
- if (t === "/providers") { showProviders(); rl.prompt(); return; }
115
- if (t === "/sessions") { await showSessions(); rl.prompt(); return; }
116
- if (t.slice(0, 4) === "/cd ") { changeDir(t.slice(4).trim()); rl.prompt(); return; }
117
- if (t.slice(0, 10) === "/provider ") {
118
- await changeProvider(t.slice(10).trim(), page, config, provider);
119
- rl.prompt();
120
- return;
121
- }
122
-
123
- if (busy) {
124
- queue.push(t);
125
- process.stdout.write(theme.dim("queued (" + queue.length + ")") + "\n");
126
- rl.prompt();
127
- return;
128
- }
129
- queue.push(t);
130
- drain();
131
- });
132
-
133
- rl.on("close", async function () {
134
- process.stdout.write("\n");
135
- await saveSession(session.id, session).catch(function () {});
136
- await ctx.close().catch(function () {});
137
- process.stdout.write(theme.ok("+ done") + "\n");
138
- process.exit(0);
139
- });
140
- }
141
-
142
- function newSession(config) {
143
- return {
144
- id: newSessionId(),
145
- ws: config.ws,
146
- provider: config.provider,
147
- turns: [],
148
- approvedAlways: [],
149
- startedAt: Date.now(),
150
- updatedAt: Date.now()
151
- };
152
- }
153
-
154
- function help() {
155
- const lines = [
156
- "COMMANDS",
157
- " /help show this",
158
- " /cd <folder> change workspace",
159
- " /pwd show current workspace",
160
- " /provider <name> switch provider",
161
- " /providers list providers",
162
- " /deepthink force DeepThink on",
163
- " /sessions list saved sessions",
164
- " /queue list pending tasks",
165
- " /status session status",
166
- " /clear clear screen",
167
- " /quit exit"
168
- ];
169
- for (const l of lines) {
170
- process.stdout.write((l.startsWith(" ") ? theme.dim(l) : theme.bold(l)) + "\n");
171
- }
172
- }
173
-
174
- function showQueue(queue) {
175
- if (!queue.length) { process.stdout.write(theme.dim("(empty)") + "\n"); return; }
176
- process.stdout.write(theme.bold(queue.length + " waiting:") + "\n");
177
- queue.forEach(function (x, i) {
178
- process.stdout.write(theme.dim(" " + (i + 1) + ". ") + x + "\n");
179
- });
180
- }
181
-
182
- function showStatus(config, session, queue, busy) {
183
- process.stdout.write(theme.bold("session status") + "\n");
184
- process.stdout.write(theme.dim(" id: ") + session.id + "\n");
185
- process.stdout.write(theme.dim(" workspace: ") + getWorkspace() + "\n");
186
- process.stdout.write(theme.dim(" provider: ") + session.provider + "\n");
187
- process.stdout.write(theme.dim(" turns: ") + session.turns.length + "\n");
188
- process.stdout.write(theme.dim(" busy: ") + (busy ? "yes" : "no") + "\n");
189
- process.stdout.write(theme.dim(" queued: ") + queue.length + "\n");
190
- }
191
-
192
- function showProviders() {
193
- process.stdout.write(theme.bold("providers:") + "\n");
194
- const list = listProviders();
195
- for (const p of list) {
196
- process.stdout.write(theme.dim(" " + p.id.padEnd(10)) + " " + p.label + "\n");
197
- }
198
- }
199
-
200
- async function showSessions() {
201
- const list = await listSessions();
202
- if (!list.length) { process.stdout.write(theme.dim("(none)") + "\n"); return; }
203
- process.stdout.write(theme.bold("sessions:") + "\n");
204
- for (const s of list.slice(0, 15)) {
205
- process.stdout.write(theme.dim(" " + s.id + " " + s.provider +
206
- " " + s.turns + " turns") + "\n");
207
- }
208
- }
209
-
210
- function changeDir(target) {
211
- if (!target) {
212
- process.stdout.write(theme.dim("usage: /cd <folder>") + "\n");
213
- return;
214
- }
215
- const clean = target.replace(/^"|"$/g, "");
216
- const abs = resolve(clean);
217
- if (!existsSync(abs)) { warn("not found: " + abs); return; }
218
- setWorkspace(abs);
219
- ok("workspace: " + getWorkspace());
220
- }
221
-
222
- async function changeProvider(name, page, config, current) {
223
- if (name === current.id) return;
224
- let next;
225
- try { next = getProvider(name); } catch (e) { error(e.message); return; }
226
- info("switching to " + next.label + "...");
227
- await page.goto(next.url);
228
- await waitForComposer(page, next, 60000);
229
- if (next.deepthink) await enableDeepThink(page);
230
- config.provider = name;
231
- ok("switched to " + next.label);
232
- }
233
-
234
- function clearScreen() {
235
- process.stdout.write(String.fromCharCode(27) + "[2J");
236
- process.stdout.write(String.fromCharCode(27) + "[H");
237
- }
2
+ import { exists
package/lib/safety.mjs CHANGED
@@ -1,32 +1,16 @@
1
- import { createInterface } from "node:readline";
2
1
  import { theme } from "./theme.mjs";
3
2
 
4
- export const SENSITIVE = new Set([
5
- "shell",
6
- "delete_file",
7
- "move_file",
8
- "write_file",
9
- "edit_file"
10
- ]);
11
-
3
+ export const SENSITIVE = new Set(["shell", "delete_file", "move_file", "write_file", "edit_file"]);
12
4
  export const SAFE = new Set([
13
- "read_file",
14
- "list_files",
15
- "find_files",
16
- "grep_search",
17
- "git_status",
18
- "git_diff",
19
- "git_log"
5
+ "read_file", "list_files", "find_files", "grep_search",
6
+ "git_status", "git_diff", "git_log"
20
7
  ]);
21
8
 
22
- export function isSensitive(name) {
23
- return SENSITIVE.has(name);
24
- }
25
-
26
- export function isSafe(name) {
27
- return SAFE.has(name);
28
- }
9
+ export function isSensitive(name) { return SENSITIVE.has(name); }
10
+ export function isSafe(name) { return SAFE.has(name); }
29
11
 
12
+ // Read a single keypress from stdin. Does NOT open a second readline —
13
+ // the main REPL owns stdin, so a new readline would eat the keystroke.
30
14
  export async function askApproval(toolName, args) {
31
15
  const preview = JSON.stringify(args || {}, null, 2).slice(0, 500);
32
16
  process.stdout.write("\n");
@@ -39,22 +23,37 @@ export async function askApproval(toolName, args) {
39
23
  if (preview.split("\n").length > 12) {
40
24
  process.stdout.write(theme.dim(" ...") + "\n");
41
25
  }
26
+ process.stdout.write(
27
+ theme.bold(" [y]") + "es " +
28
+ theme.bold("[a]") + "lways " +
29
+ theme.bold("[n]") + "o > "
30
+ );
42
31
 
43
32
  return new Promise(function (resolve) {
44
- const rl = createInterface({
45
- input: process.stdin,
46
- output: process.stdout
47
- });
48
- rl.question(
49
- theme.bold(" allow? ") + theme.ok("y") + "es / " +
50
- theme.warn("a") + "lways / " + theme.error("n") + "o: ",
51
- function (answer) {
52
- rl.close();
53
- const a = answer.trim().toLowerCase();
54
- if (a === "y" || a === "yes") resolve("yes");
55
- else if (a === "a" || a === "always") resolve("always");
56
- else resolve("no");
33
+ let answered = false;
34
+
35
+ function onData(buf) {
36
+ if (answered) return;
37
+ const s = buf.toString("utf8").toLowerCase();
38
+ if (s.indexOf("y") >= 0) { answered = true; finish("yes"); }
39
+ else if (s.indexOf("a") >= 0) { answered = true; finish("always"); }
40
+ else if (s.indexOf("n") >= 0) { answered = true; finish("no"); }
41
+ else if (s.indexOf("\r") >= 0 || s.indexOf("\n") >= 0) {
42
+ answered = true; finish("no");
57
43
  }
58
- );
44
+ }
45
+
46
+ function finish(answer) {
47
+ process.stdin.removeListener("data", onData);
48
+ try { process.stdin.setRawMode(false); } catch (e) {}
49
+ process.stdin.pause();
50
+ process.stdout.write(answer + "\n");
51
+ resolve(answer);
52
+ }
53
+
54
+ try { process.stdin.setRawMode(true); }
55
+ catch (e) { return resolve("no"); }
56
+ process.stdin.resume();
57
+ process.stdin.on("data", onData);
59
58
  });
60
59
  }
package/lib/send.mjs CHANGED
@@ -5,17 +5,16 @@ import { theme } from "./theme.mjs";
5
5
  const SETTLE_MS = 1500;
6
6
  const POLL_MS = 150;
7
7
 
8
+ // Stream the reply as it grows. Only prints the delta so the terminal does
9
+ // not redraw the same text.
8
10
  function makeStreamer() {
9
11
  let printed = 0;
10
- let lastLength = 0;
11
12
  return function (text) {
12
13
  if (!text) return;
13
14
  const t = String(text);
14
- if (t.length > lastLength) {
15
- const delta = t.slice(printed);
16
- process.stdout.write(delta);
15
+ if (t.length > printed) {
16
+ process.stdout.write(t.slice(printed));
17
17
  printed = t.length;
18
- lastLength = t.length;
19
18
  }
20
19
  };
21
20
  }
@@ -36,34 +35,28 @@ export async function send(page, provider, prompt, timeoutMs) {
36
35
  await sleep(300);
37
36
  if (provider.paste) await dismissPaste(page);
38
37
 
39
- try {
40
- await input.press("Enter");
41
- } catch (e) {}
38
+ try { await input.press("Enter"); } catch (e) {}
42
39
 
43
40
  let ok = await composerEmpty(input, 5000);
44
41
  if (!ok) {
45
42
  const btn = await findFirstVisible(page, provider.submit, 1500);
46
- if (btn) {
47
- try { await btn.click({ timeout: 1500 }); } catch (e) {}
48
- }
43
+ if (btn) { try { await btn.click({ timeout: 1500 }); } catch (e) {} }
49
44
  ok = await composerEmpty(input, 3000);
50
45
  }
51
46
 
52
- if (!ok) {
53
- throw new Error(
54
- "Message stayed in composer. Check Chrome: if a Cancel/Send bar " +
55
- "is showing, click Send once manually, then resend."
56
- );
57
- }
47
+ if (!ok) throw new Error("Message stayed in composer. Check Chrome.");
58
48
 
59
49
  return waitForReply(page, provider, want, timeoutMs);
60
50
  }
61
51
 
52
+ // Returns { text, streamed }. If streamed is true, the caller should NOT
53
+ // print the text again.
62
54
  async function waitForReply(page, provider, want, timeoutMs) {
63
55
  const end = Date.now() + timeoutMs;
64
56
  let last = "";
65
57
  let stab = Date.now();
66
58
  let sawAnything = false;
59
+ let streamedAny = false;
67
60
  const stream = makeStreamer();
68
61
 
69
62
  process.stdout.write("\n" + theme.cyan("* ") + " ");
@@ -75,25 +68,23 @@ async function waitForReply(page, provider, want, timeoutMs) {
75
68
  try { tx = (await node.textContent()) || ""; } catch (e) { tx = ""; }
76
69
  const n = norm(tx);
77
70
 
78
- if (!n || n === want) {
79
- await sleep(POLL_MS);
80
- continue;
81
- }
71
+ if (!n || n === want) { await sleep(POLL_MS); continue; }
82
72
 
83
73
  if (tx !== last) {
84
74
  stream(tx);
75
+ streamedAny = true;
85
76
  last = tx;
86
77
  stab = Date.now();
87
78
  sawAnything = true;
88
79
  } else if (sawAnything && Date.now() - stab > SETTLE_MS) {
89
80
  process.stdout.write("\n\n");
90
- return tx;
81
+ return { text: tx, streamed: streamedAny };
91
82
  }
92
83
  }
93
84
  await sleep(POLL_MS);
94
85
  }
95
86
 
96
87
  process.stdout.write("\n\n");
97
- if (last) return last;
98
- throw new Error("Timed out waiting for a reply. Check the Chrome window.");
88
+ if (last) return { text: last, streamed: streamedAny };
89
+ throw new Error("Timed out waiting for a reply.");
99
90
  }
package/lib/sessions.mjs CHANGED
@@ -1,64 +1 @@
1
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
-
5
- function dir() {
6
- return join(homedir(), ".portal-agent", "sessions");
7
- }
8
-
9
- export function newSessionId() {
10
- const d = new Date();
11
- const p = function (n, w) { return String(n).padStart(w || 2, "0"); };
12
- return [
13
- d.getFullYear(),
14
- p(d.getMonth() + 1),
15
- p(d.getDate()),
16
- "-",
17
- p(d.getHours()),
18
- p(d.getMinutes()),
19
- p(d.getSeconds())
20
- ].join("");
21
- }
22
-
23
- export async function saveSession(id, data) {
24
- const d = dir();
25
- await mkdir(d, { recursive: true });
26
- const path = join(d, id + ".json");
27
- await writeFile(path, JSON.stringify(data, null, 2), "utf8");
28
- }
29
-
30
- export async function loadSession(id) {
31
- const path = join(dir(), id + ".json");
32
- try {
33
- const raw = await readFile(path, "utf8");
34
- return JSON.parse(raw);
35
- } catch (e) {
36
- return null;
37
- }
38
- }
39
-
40
- export async function listSessions() {
41
- const d = dir();
42
- let names;
43
- try {
44
- names = await readdir(d);
45
- } catch (e) {
46
- return [];
47
- }
48
-
49
- const metas = [];
50
- for (const n of names) {
51
- if (!n.endsWith(".json")) continue;
52
- const id = n.slice(0, -5);
53
- const s = await loadSession(id);
54
- if (!s) continue;
55
- metas.push({
56
- id: id,
57
- provider: s.provider || "",
58
- turns: (s.turns || []).length,
59
- updatedAt: s.updatedAt || 0
60
- });
61
- }
62
- metas.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
63
- return metas;
64
- }
1
+ import
package/lib/system.mjs CHANGED
@@ -2,43 +2,4 @@ import { CATALOG } from "./tools/index.mjs";
2
2
 
3
3
  export function buildSystem(ws, instructions) {
4
4
  const shown = ws.split("\\").join("/");
5
- const isWin = process.platform === "win32";
6
- const parts = [];
7
-
8
- parts.push([
9
- "You are a coding agent working in a terminal.",
10
- "Workspace: " + shown,
11
- "Platform: " + (isWin ? "Windows" : process.platform),
12
- "",
13
- "Think carefully about each step before acting. Explain your reasoning",
14
- "when it helps the user follow along. Take as much space as the task",
15
- "requires.",
16
- "",
17
- "In file paths inside tool arguments, use forward slashes (L:/this/file.txt).",
18
- isWin
19
- ? "In shell commands on Windows, use backslashes (del L:\\this\\file.txt)."
20
- : "In shell commands, forward slashes are fine.",
21
- "",
22
- "Prefer purpose-built tools over shell. Use delete_file, not rm or del.",
23
- "Do not call the same tool with the same arguments twice.",
24
- "Read a file before editing it.",
25
- "If a request is ambiguous, ask the user a real question.",
26
- "When the task is complete, say so plainly and stop calling tools."
27
- ].join("\n"));
28
-
29
- if (instructions) parts.push(instructions);
30
-
31
- parts.push([
32
- "Tool call format:",
33
- "",
34
- "To call a tool, write on its own line:",
35
- "portal-tool: {\"tool\":\"name\",\"args\":{\"key\":\"value\"}}",
36
- "",
37
- "The JSON must use double quotes. Plain text only, no backticks.",
38
- "You may write narrative text around the tool calls."
39
- ].join("\n"));
40
-
41
- parts.push("Available tools:\n" + CATALOG);
42
-
43
- return parts.join("\n\n");
44
- }
5
+ const isWin = process.pl
package/lib/theme.mjs CHANGED
@@ -33,14 +33,10 @@ export const theme = {
33
33
  ok: function (s) { return wrap(BGRN, s); },
34
34
  yellow: function (s) { return wrap(YEL, s); },
35
35
  warn: function (s) { return wrap(YEL, s); },
36
- blue: function (s) { return wrap(BLU, s); },
37
- magenta: function (s) { return wrap(MAG, s); },
38
36
  cyan: function (s) { return wrap(BCYN, s); },
39
37
  accent: function (s) { return wrap(CYN, s); },
40
38
  user: function (s) { return wrap(BLU, s); },
41
39
  tool: function (s) { return wrap(MAG, s); }
42
40
  };
43
41
 
44
- export function colorEnabled() {
45
- return MODE === "color";
46
- }
42
+ export function colorEnabled() { return MODE === "color"; }
@@ -1,25 +1,2 @@
1
1
  import { stat, rename } from "node:fs/promises";
2
- import { safePath } from "./path.mjs";
3
-
4
- export async function delete_file(args) {
5
- const abs = safePath(args.path);
6
- try {
7
- const st = await stat(abs);
8
- if (st.isDirectory()) return "delete_file cannot remove directories";
9
- await rename(abs, abs + ".portal-trash");
10
- return "moved " + args.path + " to trash";
11
- } catch (e) {
12
- return "delete_file failed: " + (e.message || e);
13
- }
14
- }
15
-
16
- export async function move_file(args) {
17
- const from = safePath(args.from);
18
- const to = safePath(args.to);
19
- try {
20
- await rename(from, to);
21
- return "moved " + args.from + " to " + args.to;
22
- } catch (e) {
23
- return "move_file failed: " + (e.message || e);
24
- }
25
- }
2
+ import { safePath } from "./
@@ -1,46 +1 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { safePath } from "./path.mjs";
3
-
4
- export async function edit_file(args) {
5
- const abs = safePath(args.path);
6
- const oldStr = String(args.old_string || "");
7
- const newStr = String(args.new_string || "");
8
-
9
- if (!oldStr) return "edit_file requires old_string";
10
- if (oldStr === newStr) return "old_string and new_string are identical";
11
-
12
- const orig = await readFile(abs, "utf8");
13
- const count = orig.split(oldStr).length - 1;
14
-
15
- if (count === 0) {
16
- return "old_string not found in " + args.path +
17
- ". Read the file first to see exact contents.";
18
- }
19
-
20
- if (count > 1 && !args.replace_all) {
21
- return "old_string appears " + count + " times. Add context or set replace_all:true.";
22
- }
23
-
24
- const next = args.replace_all
25
- ? orig.split(oldStr).join(newStr)
26
- : orig.replace(oldStr, newStr);
27
-
28
- await writeFile(abs + ".portal-bak", orig, "utf8");
29
- await writeFile(abs, next, "utf8");
30
-
31
- const delta = next.length - orig.length;
32
- const noun = args.replace_all ? count + " occurrences" : "1 occurrence";
33
- return "edited " + args.path + ": " + noun + " (" +
34
- (delta >= 0 ? "+" : "") + delta + " bytes)";
35
- }
36
-
37
- export async function undo_file(args) {
38
- const abs = safePath(args.path);
39
- try {
40
- const content = await readFile(abs + ".portal-bak", "utf8");
41
- await writeFile(abs, content, "utf8");
42
- return "restored " + args.path;
43
- } catch (e) {
44
- return "no backup for " + args.path;
45
- }
46
- }
1
+ import
@@ -1,39 +1,2 @@
1
1
  import { relative, sep } from "node:path";
2
- import { walk } from "./walk.mjs";
3
- import { toPosix, getWorkspace } from "./path.mjs";
4
-
5
- function globToRegex(glob) {
6
- let re = "";
7
- for (let i = 0; i < glob.length; i++) {
8
- const c = glob[i];
9
- if (c === "*") {
10
- if (glob[i + 1] === "*") { re += ".*"; i++; }
11
- else re += "[^/\\\\]*";
12
- } else if (c === "?") {
13
- re += ".";
14
- } else if (".+^$(){}[]|\\".indexOf(c) >= 0) {
15
- re += "\\" + c;
16
- } else {
17
- re += c;
18
- }
19
- }
20
- return new RegExp("^" + re + "$", "i");
21
- }
22
-
23
- export async function find_files(args) {
24
- const pattern = args.pattern || "*";
25
- const re = globToRegex(pattern);
26
- const ws = getWorkspace();
27
- const hits = [];
28
-
29
- for await (const f of walk(ws)) {
30
- if (hits.length >= 200) break;
31
- const rel = toPosix(relative(ws, f));
32
- const base = f.split(sep).pop();
33
- if (re.test(rel) || re.test(base)) hits.push(rel);
34
- }
35
-
36
- if (!hits.length) return "no files matched: " + pattern;
37
- hits.sort();
38
- return hits.length + " file(s):\n" + hits.join("\n");
39
- }
2
+ import { walk } from "./walk.m