@dreki-gg/pi-plan-mode 0.6.3 → 0.7.2

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.
@@ -1,396 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
3
- //#endregion
4
- //#region ../../node_modules/shell-quote/quote.js
5
- var require_quote = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6
- module.exports = function quote(xs) {
7
- return xs.map(function(s) {
8
- if (s === "") return "''";
9
- if (s && typeof s === "object") return s.op.replace(/(.)/g, "\\$1");
10
- if (/["\s\\]/.test(s) && !/'/.test(s)) return "'" + s.replace(/(['])/g, "\\$1") + "'";
11
- if (/["'\s]/.test(s)) return "\"" + s.replace(/(["\\$`!])/g, "\\$1") + "\"";
12
- return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
13
- }).join(" ");
14
- };
15
- }));
16
- //#endregion
17
- //#region ../../node_modules/shell-quote/parse.js
18
- var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
19
- var CONTROL = "(?:" + [
20
- "\\|\\|",
21
- "\\&\\&",
22
- ";;",
23
- "\\|\\&",
24
- "\\<\\(",
25
- "\\<\\<\\<",
26
- ">>",
27
- ">\\&",
28
- "<\\&",
29
- "[&;()|<>]"
30
- ].join("|") + ")";
31
- var controlRE = new RegExp("^" + CONTROL + "$");
32
- var META = "|&;()<> \\t";
33
- var SINGLE_QUOTE = "\"((\\\\\"|[^\"])*?)\"";
34
- var DOUBLE_QUOTE = "'((\\\\'|[^'])*?)'";
35
- var hash = /^#$/;
36
- var SQ = "'";
37
- var DQ = "\"";
38
- var DS = "$";
39
- var TOKEN = "";
40
- var mult = 4294967296;
41
- for (var i = 0; i < 4; i++) TOKEN += (mult * Math.random()).toString(16);
42
- var startsWithToken = new RegExp("^" + TOKEN);
43
- function matchAll(s, r) {
44
- var origIndex = r.lastIndex;
45
- var matches = [];
46
- var matchObj;
47
- while (matchObj = r.exec(s)) {
48
- matches.push(matchObj);
49
- if (r.lastIndex === matchObj.index) r.lastIndex += 1;
50
- }
51
- r.lastIndex = origIndex;
52
- return matches;
53
- }
54
- function getVar(env, pre, key) {
55
- var r = typeof env === "function" ? env(key) : env[key];
56
- if (typeof r === "undefined" && key != "") r = "";
57
- else if (typeof r === "undefined") r = "$";
58
- if (typeof r === "object") return pre + TOKEN + JSON.stringify(r) + TOKEN;
59
- return pre + r;
60
- }
61
- function parseInternal(string, env, opts) {
62
- if (!opts) opts = {};
63
- var BS = opts.escape || "\\";
64
- var BAREWORD = "(\\" + BS + "['\"" + META + "]|[^\\s'\"" + META + "])+";
65
- var matches = matchAll(string, new RegExp(["(" + CONTROL + ")", "(" + BAREWORD + "|" + SINGLE_QUOTE + "|" + DOUBLE_QUOTE + ")+"].join("|"), "g"));
66
- if (matches.length === 0) return [];
67
- if (!env) env = {};
68
- var commented = false;
69
- return matches.map(function(match) {
70
- var s = match[0];
71
- if (!s || commented) return;
72
- if (controlRE.test(s)) return { op: s };
73
- var quote = false;
74
- var esc = false;
75
- var out = "";
76
- var isGlob = false;
77
- var i;
78
- function parseEnvVar() {
79
- i += 1;
80
- var varend;
81
- var varname;
82
- var char = s.charAt(i);
83
- if (char === "{") {
84
- i += 1;
85
- if (s.charAt(i) === "}") throw new Error("Bad substitution: " + s.slice(i - 2, i + 1));
86
- varend = s.indexOf("}", i);
87
- if (varend < 0) throw new Error("Bad substitution: " + s.slice(i));
88
- varname = s.slice(i, varend);
89
- i = varend;
90
- } else if (/[*@#?$!_-]/.test(char)) {
91
- varname = char;
92
- i += 1;
93
- } else {
94
- var slicedFromI = s.slice(i);
95
- varend = slicedFromI.match(/[^\w\d_]/);
96
- if (!varend) {
97
- varname = slicedFromI;
98
- i = s.length;
99
- } else {
100
- varname = slicedFromI.slice(0, varend.index);
101
- i += varend.index - 1;
102
- }
103
- }
104
- return getVar(env, "", varname);
105
- }
106
- for (i = 0; i < s.length; i++) {
107
- var c = s.charAt(i);
108
- isGlob = isGlob || !quote && (c === "*" || c === "?");
109
- if (esc) {
110
- out += c;
111
- esc = false;
112
- } else if (quote) if (c === quote) quote = false;
113
- else if (quote == SQ) out += c;
114
- else if (c === BS) {
115
- i += 1;
116
- c = s.charAt(i);
117
- if (c === DQ || c === BS || c === DS) out += c;
118
- else out += BS + c;
119
- } else if (c === DS) out += parseEnvVar();
120
- else out += c;
121
- else if (c === DQ || c === SQ) quote = c;
122
- else if (controlRE.test(c)) return { op: s };
123
- else if (hash.test(c)) {
124
- commented = true;
125
- var commentObj = { comment: string.slice(match.index + i + 1) };
126
- if (out.length) return [out, commentObj];
127
- return [commentObj];
128
- } else if (c === BS) esc = true;
129
- else if (c === DS) out += parseEnvVar();
130
- else out += c;
131
- }
132
- if (isGlob) return {
133
- op: "glob",
134
- pattern: out
135
- };
136
- return out;
137
- }).reduce(function(prev, arg) {
138
- return typeof arg === "undefined" ? prev : prev.concat(arg);
139
- }, []);
140
- }
141
- module.exports = function parse(s, env, opts) {
142
- var mapped = parseInternal(s, env, opts);
143
- if (typeof env !== "function") return mapped;
144
- return mapped.reduce(function(acc, s) {
145
- if (typeof s === "object") return acc.concat(s);
146
- var xs = s.split(RegExp("(" + TOKEN + ".*?" + TOKEN + ")", "g"));
147
- if (xs.length === 1) return acc.concat(xs[0]);
148
- return acc.concat(xs.filter(Boolean).map(function(x) {
149
- if (startsWithToken.test(x)) return JSON.parse(x.split(TOKEN)[1]);
150
- return x;
151
- }));
152
- }, []);
153
- };
154
- }));
155
- //#endregion
156
- //#region src/parser.ts
157
- var import_shell_quote = (/* @__PURE__ */ __commonJSMin(((exports) => {
158
- exports.quote = require_quote();
159
- exports.parse = require_parse();
160
- })))();
161
- /** Operators that separate independent commands. */
162
- const COMMAND_SEPARATORS = new Set([
163
- "&&",
164
- "||",
165
- ";",
166
- "|"
167
- ]);
168
- /** Operators that indicate potentially dangerous constructs. */
169
- const DANGEROUS_OPS = new Set([">", ">>"]);
170
- /** Grouping operators (subshells) — we strip them and validate inner commands. */
171
- const GROUPING_OPS = new Set(["(", ")"]);
172
- /**
173
- * Parse a shell command into individual command segments.
174
- *
175
- * Uses shell-quote to properly tokenize the input, then splits on
176
- * command separators (&&, ||, ;, |). Each segment is returned as a
177
- * reconstructed command string.
178
- *
179
- * Subshell grouping operators `(` and `)` are stripped — the inner
180
- * commands are still validated individually.
181
- *
182
- * @example
183
- * ```ts
184
- * parseCommandSegments('cd foo && ls -la')
185
- * // → [{ command: 'cd foo', hasRedirect: false },
186
- * // { command: 'ls -la', hasRedirect: false }]
187
- *
188
- * parseCommandSegments('echo "hello && world"')
189
- * // → [{ command: 'echo "hello && world"', hasRedirect: false }]
190
- *
191
- * parseCommandSegments('curl -s url 2>/dev/null | head')
192
- * // → [{ command: 'curl -s url 2>/dev/null', hasRedirect: false },
193
- * // { command: 'head', hasRedirect: false }]
194
- * ```
195
- */
196
- function parseCommandSegments(input) {
197
- const tokens = (0, import_shell_quote.parse)(input);
198
- const segments = [];
199
- let currentTokens = [];
200
- let hasRedirect = false;
201
- function flushSegment() {
202
- if (currentTokens.length > 0) {
203
- segments.push({
204
- command: currentTokens.join(" "),
205
- hasRedirect
206
- });
207
- currentTokens = [];
208
- hasRedirect = false;
209
- }
210
- }
211
- for (const token of tokens) {
212
- if (typeof token === "string") {
213
- currentTokens.push(token);
214
- continue;
215
- }
216
- if (!("op" in token)) {
217
- currentTokens.push(String(token));
218
- continue;
219
- }
220
- const { op } = token;
221
- if (COMMAND_SEPARATORS.has(op)) {
222
- flushSegment();
223
- continue;
224
- }
225
- if (DANGEROUS_OPS.has(op)) {
226
- hasRedirect = true;
227
- currentTokens.push(op);
228
- continue;
229
- }
230
- if (GROUPING_OPS.has(op)) continue;
231
- currentTokens.push(op);
232
- }
233
- flushSegment();
234
- return segments;
235
- }
236
- /**
237
- * Check if the parsed tokens contain command substitution patterns.
238
- *
239
- * Command substitution ($(...) or `...`) can hide arbitrary commands
240
- * and should be blocked in sandboxed modes.
241
- */
242
- function hasCommandSubstitution(input) {
243
- return /\$\(/.test(input) || /`[^`]+`/.test(input);
244
- }
245
- //#endregion
246
- //#region src/patterns.ts
247
- /**
248
- * Destructive and safe command patterns for shell sandboxing.
249
- *
250
- * DESTRUCTIVE_PATTERNS — if any segment matches, the command is blocked.
251
- * SAFE_PATTERNS — a segment must match at least one to be allowed.
252
- */
253
- const DESTRUCTIVE_PATTERNS = [
254
- /\brm\b/i,
255
- /\brmdir\b/i,
256
- /\bmv\b/i,
257
- /\bcp\b/i,
258
- /\bmkdir\b/i,
259
- /\btouch\b/i,
260
- /\bchmod\b/i,
261
- /\bchown\b/i,
262
- /\bchgrp\b/i,
263
- /\bln\b/i,
264
- /\btee\b/i,
265
- /\btruncate\b/i,
266
- /\bdd\b/i,
267
- /\bshred\b/i,
268
- /\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
269
- /\byarn\s+(add|remove|install|publish)/i,
270
- /\bpnpm\s+(add|remove|install|publish)/i,
271
- /\bpip\s+(install|uninstall)/i,
272
- /\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
273
- /\bbrew\s+(install|uninstall|upgrade)/i,
274
- /\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
275
- /\bsudo\b/i,
276
- /\bsu\b/i,
277
- /\bkill\b/i,
278
- /\bpkill\b/i,
279
- /\bkillall\b/i,
280
- /\breboot\b/i,
281
- /\bshutdown\b/i,
282
- /\bsystemctl\s+(start|stop|restart|enable|disable)/i,
283
- /\bservice\s+\S+\s+(start|stop|restart)/i,
284
- /\b(vim?|nano|emacs|code|subl)\b/i,
285
- /\bdel\b/i,
286
- /\brd\b/i,
287
- /\bcopy\b/i,
288
- /\bmove\b/i,
289
- /\bren\b/i,
290
- /\brename\b/i,
291
- /\bicacls\b/i,
292
- /\battrib\b/i,
293
- /\bpowershell\b/i,
294
- /\bpwsh\b/i
295
- ];
296
- const SAFE_PATTERNS = [
297
- /^\s*cat\b/,
298
- /^\s*head\b/,
299
- /^\s*tail\b/,
300
- /^\s*less\b/,
301
- /^\s*more\b/,
302
- /^\s*grep\b/,
303
- /^\s*find\b/,
304
- /^\s*ls\b/,
305
- /^\s*pwd\b/,
306
- /^\s*echo\b/,
307
- /^\s*printf\b/,
308
- /^\s*wc\b/,
309
- /^\s*sort\b/,
310
- /^\s*uniq\b/,
311
- /^\s*diff\b/,
312
- /^\s*file\b/,
313
- /^\s*stat\b/,
314
- /^\s*du\b/,
315
- /^\s*df\b/,
316
- /^\s*tree\b/,
317
- /^\s*which\b/,
318
- /^\s*whereis\b/,
319
- /^\s*type\b/,
320
- /^\s*env\b/,
321
- /^\s*printenv\b/,
322
- /^\s*uname\b/,
323
- /^\s*whoami\b/,
324
- /^\s*id\b/,
325
- /^\s*date\b/,
326
- /^\s*cal\b/,
327
- /^\s*uptime\b/,
328
- /^\s*ps\b/,
329
- /^\s*top\b/,
330
- /^\s*htop\b/,
331
- /^\s*free\b/,
332
- /^\s*cd\b/,
333
- /^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
334
- /^\s*git\s+ls-/i,
335
- /^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
336
- /^\s*yarn\s+(list|info|why|audit)/i,
337
- /^\s*pnpm\s+(list|ls|why|audit|outdated)/i,
338
- /^\s*bun\s+(pm\s+ls)/i,
339
- /^\s*node\s+--version/i,
340
- /^\s*python\s+--version/i,
341
- /^\s*curl\s/i,
342
- /^\s*wget\s+-O\s*-/i,
343
- /^\s*jq\b/,
344
- /^\s*sed\s+-n/i,
345
- /^\s*awk\b/,
346
- /^\s*rg\b/,
347
- /^\s*fd\b/,
348
- /^\s*bat\b/,
349
- /^\s*eza\b/,
350
- /^\s*basename\b/,
351
- /^\s*dirname\b/,
352
- /^\s*realpath\b/,
353
- /^\s*readlink\b/,
354
- /^\s*dir\b/,
355
- /^\s*where\b/,
356
- /^\s*set\b/,
357
- /^\s*systeminfo\b/,
358
- /^\s*tasklist\b/
359
- ];
360
- //#endregion
361
- //#region src/sandbox.ts
362
- /**
363
- * Core sandbox logic — determines whether a shell command is safe to execute.
364
- */
365
- /**
366
- * Check if a shell command is safe to execute in a sandboxed mode.
367
- *
368
- * The command is parsed into segments using shell-quote, and each segment
369
- * is checked independently against the destructive and safe pattern lists.
370
- * A command is safe only if ALL segments pass.
371
- *
372
- * For a segment to pass:
373
- * 1. It must NOT match any destructive pattern
374
- * 2. It MUST match at least one safe pattern
375
- * 3. It must NOT contain redirects (unless explicitly allowed)
376
- *
377
- * Additionally, command substitution ($() and backticks) is blocked by default.
378
- */
379
- function isSafeCommand(command, options = {}) {
380
- const { extraSafe = [], extraDestructive = [], allowCommand, allowRedirects = false, allowCommandSubstitution = false } = options;
381
- if (allowCommand?.(command)) return true;
382
- if (!allowCommandSubstitution && hasCommandSubstitution(command)) return false;
383
- const allDestructive = [...DESTRUCTIVE_PATTERNS, ...extraDestructive];
384
- const allSafe = [...SAFE_PATTERNS, ...extraSafe];
385
- const segments = parseCommandSegments(command);
386
- if (segments.length === 0) return false;
387
- return segments.every((segment) => {
388
- if (!allowRedirects && segment.hasRedirect) return false;
389
- const cmd = segment.command;
390
- const isDestructive = allDestructive.some((p) => p.test(cmd));
391
- const isSafe = allSafe.some((p) => p.test(cmd));
392
- return !isDestructive && isSafe;
393
- });
394
- }
395
- //#endregion
396
- export { DESTRUCTIVE_PATTERNS, SAFE_PATTERNS, hasCommandSubstitution, isSafeCommand, parseCommandSegments };
@@ -1,42 +0,0 @@
1
- {
2
- "name": "@dreki-gg/pi-command-sandbox",
3
- "private": true,
4
- "version": "0.1.0",
5
- "description": "Shared command sandboxing utilities for pi extensions — validates shell commands against safe/destructive pattern lists using proper shell parsing",
6
- "author": "Juan Albarran <jalbarrandev@gmail.com>",
7
- "license": "MIT",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/dreki-gg/pi-extensions",
11
- "directory": "packages/command-sandbox"
12
- },
13
- "type": "module",
14
- "main": "./dist/index.mjs",
15
- "exports": {
16
- ".": {
17
- "import": "./dist/index.mjs",
18
- "types": "./dist/index.d.mts"
19
- }
20
- },
21
- "files": ["dist"],
22
- "scripts": {
23
- "build": "tsdown",
24
- "test": "bun test",
25
- "typecheck": "tsc --noEmit",
26
- "lint": "oxlint src",
27
- "format": "oxfmt --write src",
28
- "format:check": "oxfmt --check src"
29
- },
30
- "dependencies": {
31
- "shell-quote": "^1.8.3"
32
- },
33
- "devDependencies": {
34
- "@types/node": "24",
35
- "@types/shell-quote": "^1.7.5",
36
- "bun-types": "latest",
37
- "oxfmt": "^0.43.0",
38
- "oxlint": "^1.58.0",
39
- "tsdown": "^0.22.0",
40
- "typescript": "^6.0.0"
41
- }
42
- }