loom-agent 1.2.0
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/.env.example +25 -0
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/LOOM.md +235 -0
- package/README.md +433 -0
- package/bin/loom-tui.js +43 -0
- package/bin/loom.js +44 -0
- package/docs/acp.md +151 -0
- package/docs/web.md +205 -0
- package/package.json +97 -0
- package/scripts/acp-smoke.js +146 -0
- package/src/acp/acp-server.js +287 -0
- package/src/config/provider-cmd.js +37 -0
- package/src/config/settings.js +164 -0
- package/src/core/agents.js +361 -0
- package/src/core/background-tasks.js +103 -0
- package/src/core/cli.js +579 -0
- package/src/core/custom-commands.js +70 -0
- package/src/core/errors.js +29 -0
- package/src/core/events.js +24 -0
- package/src/core/file-diffs.js +282 -0
- package/src/core/format.js +206 -0
- package/src/core/graph.js +257 -0
- package/src/core/hooks.js +82 -0
- package/src/core/lsp.js +385 -0
- package/src/core/memory.js +87 -0
- package/src/core/model-router.js +87 -0
- package/src/core/permissions.js +327 -0
- package/src/core/platform.js +33 -0
- package/src/core/plugin-cmd.js +380 -0
- package/src/core/restore.js +207 -0
- package/src/core/session-store.js +167 -0
- package/src/core/session.js +910 -0
- package/src/core/subagent-log.js +134 -0
- package/src/core/tokens.js +31 -0
- package/src/core/update.js +6 -0
- package/src/core/usage.js +166 -0
- package/src/index.js +41 -0
- package/src/mcp/mcp-client.js +201 -0
- package/src/mcp/mcp-manager.js +193 -0
- package/src/providers/anthropic.js +243 -0
- package/src/providers/google.js +29 -0
- package/src/providers/index.js +175 -0
- package/src/providers/local.js +27 -0
- package/src/providers/nvidia.js +85 -0
- package/src/providers/openai-compat.js +269 -0
- package/src/providers/openai.js +35 -0
- package/src/providers/openrouter.js +43 -0
- package/src/providers/registry.js +196 -0
- package/src/providers/tokenrouter.js +19 -0
- package/src/skills/skill-matcher.js +133 -0
- package/src/skills/skills-manager.js +213 -0
- package/src/tools/index.js +543 -0
- package/src/tui/App.tsx +1578 -0
- package/src/tui/components/BreadcrumbBar.tsx +34 -0
- package/src/tui/components/ChatArea.tsx +518 -0
- package/src/tui/components/InputBar.tsx +354 -0
- package/src/tui/components/MdText.tsx +105 -0
- package/src/tui/components/Modals.tsx +851 -0
- package/src/tui/components/PermissionPopup.tsx +264 -0
- package/src/tui/components/Sidebar.tsx +182 -0
- package/src/tui/components/SplashScreen.tsx +51 -0
- package/src/tui/components/SubagentPanel.tsx +217 -0
- package/src/tui/components/ToastOverlay.tsx +34 -0
- package/src/tui/keybinds.ts +318 -0
- package/src/tui/mcp-presets.ts +189 -0
- package/src/tui/md-render.ts +228 -0
- package/src/tui/store.ts +714 -0
- package/src/tui/suite-home.ts +20 -0
- package/src/tui/theme.ts +313 -0
- package/src/tui/themes.generated.ts +968 -0
- package/src/tui/tool-display.ts +176 -0
- package/src/tui/toolname.ts +60 -0
- package/src/tui/tui-config.ts +28 -0
- package/src/tui-open.tsx +51 -0
- package/src/web/attach.js +242 -0
- package/src/web/graph-view.html +262 -0
- package/src/web/index.html +824 -0
- package/src/web/web-server.js +470 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
const ADMIN_PATTERNS = [
|
|
5
|
+
{ re: /\bsudo\b/, label: 'sudo detected' },
|
|
6
|
+
{ re: /\brunas\b/, label: 'runas/administrator detected' },
|
|
7
|
+
{ re: /\bdoas\b/, label: 'doas detected' },
|
|
8
|
+
{ re: /\bchmod\s+[0-7]/, label: 'chmod with octal mode' },
|
|
9
|
+
{ re: /\bchown\s+/, label: 'chown detected' },
|
|
10
|
+
{ re: /\bshutdown\b/, label: 'shutdown command' },
|
|
11
|
+
{ re: /\breboot\b/, label: 'reboot command' },
|
|
12
|
+
{ re: /\bnpm\s+(install|i)\s+-g\b/, label: 'npm global install' },
|
|
13
|
+
{ re: /\bkill\s+-9\b/, label: 'force kill (-9)' },
|
|
14
|
+
{ re: /\bmkfs\b/, label: 'make filesystem (mkfs)' },
|
|
15
|
+
{ re: /\bdd\s+if=/, label: 'disk destroy (dd)' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const DANGER_PATTERNS = [
|
|
19
|
+
{ re: /\brm\s+-rf\b/, label: 'recursive force delete (rm -rf)' },
|
|
20
|
+
{ re: /\bgit\s+push\s+--force\b/, label: 'force push to git remote' },
|
|
21
|
+
{ re: /\bgit\s+reset\s+--hard\b/, label: 'hard git reset (destructive)' },
|
|
22
|
+
{ re: /\bdocker\s+(rmi|system prune)\b/, label: 'destructive docker command' },
|
|
23
|
+
{ re: /DROP\s+(TABLE|DATABASE)/i, label: 'SQL table/database drop' },
|
|
24
|
+
{ re: /\bscp\b.*\blocalhost\b|\bscp\b.*\/etc\//, label: 'potentially dangerous scp' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// OpenCode-style permission keys and their defaults. Most tools default to
|
|
28
|
+
// allow; edit/task/skill/doom_loop/external_directory default to ask, and
|
|
29
|
+
// bash allows normal commands but STILL asks for dangerous ones (the
|
|
30
|
+
// ADMIN/DANGER heuristics below are enforced regardless of the tree, so
|
|
31
|
+
// "git push --force", "rm -rf", "sudo …", etc. always prompt unless the user
|
|
32
|
+
// saved an exact rule). read allows everything except .env files (denied by
|
|
33
|
+
// default — the built-in .*env rules below are part of the default tree, not
|
|
34
|
+
// user config).
|
|
35
|
+
const PERMISSION_KEYS = [
|
|
36
|
+
'read', 'edit', 'glob', 'grep', 'bash', 'task', 'skill', 'lsp', 'question',
|
|
37
|
+
'webfetch', 'websearch',
|
|
38
|
+
'external_directory', 'doom_loop',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const DEFAULT_PERMISSIONS = {
|
|
42
|
+
'*': 'allow',
|
|
43
|
+
bash: 'allow',
|
|
44
|
+
edit: 'ask',
|
|
45
|
+
task: 'ask',
|
|
46
|
+
skill: 'ask',
|
|
47
|
+
question: 'ask',
|
|
48
|
+
doom_loop: 'ask',
|
|
49
|
+
external_directory: 'ask',
|
|
50
|
+
read: {
|
|
51
|
+
'*': 'allow',
|
|
52
|
+
'*.env': 'deny',
|
|
53
|
+
'*.env.*': 'deny',
|
|
54
|
+
'*.env.example': 'allow',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Tools that write files map onto the "edit" permission (OpenCode semantics:
|
|
59
|
+
// edit covers edit, write and patch).
|
|
60
|
+
const TOOL_TO_PERMISSION = {
|
|
61
|
+
read: 'read',
|
|
62
|
+
edit: 'edit',
|
|
63
|
+
write: 'edit',
|
|
64
|
+
glob: 'glob',
|
|
65
|
+
grep: 'grep',
|
|
66
|
+
bash: 'bash',
|
|
67
|
+
task: 'task',
|
|
68
|
+
skill: 'skill',
|
|
69
|
+
lsp: 'lsp',
|
|
70
|
+
question: 'question',
|
|
71
|
+
ask: 'question',
|
|
72
|
+
webfetch: 'webfetch',
|
|
73
|
+
websearch: 'websearch',
|
|
74
|
+
todowrite: null, // never prompts
|
|
75
|
+
mcp: null, // wired by the session (only the "add" action prompts)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function expandHome(p, home) {
|
|
79
|
+
const h = home || os.homedir();
|
|
80
|
+
if (p === '~') return h;
|
|
81
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(h, p.slice(2));
|
|
82
|
+
if (p.startsWith('$HOME/') || p.startsWith('$HOME\\')) return path.join(h, p.slice(6));
|
|
83
|
+
return p;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// '*' matches any run of chars (including /), '?' matches exactly one; all
|
|
87
|
+
// other characters are literal. Case-insensitive on Windows paths.
|
|
88
|
+
function wildcardToRegExp(pattern) {
|
|
89
|
+
const esc = (s) => s.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (const ch of String(pattern)) {
|
|
92
|
+
if (ch === '*') parts.push('.*');
|
|
93
|
+
else if (ch === '?') parts.push('.');
|
|
94
|
+
else parts.push(esc(ch));
|
|
95
|
+
}
|
|
96
|
+
return new RegExp('^' + parts.join('') + '$', process.platform === 'win32' ? 'i' : '');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Match `arg` against a permission node (string | object of pattern→action).
|
|
100
|
+
// Returns the action or null when nothing matched.
|
|
101
|
+
function matchNode(node, arg) {
|
|
102
|
+
if (node == null) return null;
|
|
103
|
+
if (typeof node === 'string') return normalizeAction(node);
|
|
104
|
+
if (typeof node !== 'object') return null;
|
|
105
|
+
let winner = null; // last match wins
|
|
106
|
+
const a = String(arg ?? '');
|
|
107
|
+
for (const [pat, action] of Object.entries(node)) {
|
|
108
|
+
// Normalize the expanded pattern the same way args are normalized
|
|
109
|
+
// (forward slashes) so ~/... patterns match on Windows too.
|
|
110
|
+
const expanded = expandHome(pat).replace(/\\/g, '/');
|
|
111
|
+
if (wildcardToRegExp(expanded).test(a)) winner = normalizeAction(action);
|
|
112
|
+
}
|
|
113
|
+
return winner;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeAction(a) {
|
|
117
|
+
if (a === 'never') return 'deny'; // legacy popup value
|
|
118
|
+
return a === 'allow' || a === 'ask' || a === 'deny' ? a : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Normalize any path-like argument for matching: absolute, forward slashes.
|
|
122
|
+
function normPathArg(arg, cwd) {
|
|
123
|
+
if (!arg) return '';
|
|
124
|
+
let p = expandHome(String(arg));
|
|
125
|
+
if (!path.isAbsolute(p)) p = path.resolve(cwd || process.cwd(), p);
|
|
126
|
+
return p.replace(/\\/g, '/');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class PermissionManager {
|
|
130
|
+
constructor() {
|
|
131
|
+
this.sessionRules = new Map(); // legacy exact-command rules (popup picks)
|
|
132
|
+
this.pendingRequest = null;
|
|
133
|
+
this.onRuleChange = null; // session wires this to persist // popup choices into ~/.loom/config.json (config.permissionRules)
|
|
134
|
+
this.config = null; // loadConfig snapshot (config.permission, config.agent)
|
|
135
|
+
this.agentId = null; // current agent for agent-scoped rules
|
|
136
|
+
this.auto = false; // auto-approve mode (off by default)
|
|
137
|
+
this.cwd = process.cwd();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
loadRules(rules) {
|
|
141
|
+
if (!rules || typeof rules !== 'object') return;
|
|
142
|
+
for (const [key, value] of Object.entries(rules)) {
|
|
143
|
+
const v = normalizeAction(value);
|
|
144
|
+
if (v) this.sessionRules.set(key, v);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
loadConfig(cfg) {
|
|
149
|
+
this.config = cfg || null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setAgent(agentId) { this.agentId = agentId || null; }
|
|
153
|
+
setAuto(on) { this.auto = !!on; }
|
|
154
|
+
|
|
155
|
+
_match(cmd, patterns) {
|
|
156
|
+
const lower = (cmd || '').toLowerCase();
|
|
157
|
+
for (const p of patterns) if (p.re.test(lower)) return p.label;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isAdmin(cmd) { return !!this._match(cmd, ADMIN_PATTERNS); }
|
|
162
|
+
isDangerous(cmd) { return !!this._match(cmd, DANGER_PATTERNS) || this.isAdmin(cmd); }
|
|
163
|
+
getDangerLabel(cmd) {
|
|
164
|
+
return this._match(cmd, ADMIN_PATTERNS) || this._match(cmd, DANGER_PATTERNS) || 'dangerous command';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getRule(key) { return this.sessionRules.get(key); }
|
|
168
|
+
|
|
169
|
+
setRule(key, value, persist) {
|
|
170
|
+
this.sessionRules.set(key, value);
|
|
171
|
+
if (persist && this.onRuleChange) this.onRuleChange(key, value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
clearRule(key) {
|
|
175
|
+
this.sessionRules.delete(key);
|
|
176
|
+
if (this.onRuleChange) this.onRuleChange(key, null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
checkRule(key) {
|
|
180
|
+
if (this.sessionRules.has('*')) return this.sessionRules.get('*');
|
|
181
|
+
if (this.sessionRules.has(key)) return this.sessionRules.get(key);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Legacy pre-OpenCode check: an exact command string. Kept for callers that
|
|
186
|
+
// still think in commands (the bash session gate) — see resolve() for the
|
|
187
|
+
// general tool layer.
|
|
188
|
+
async check(cmd) {
|
|
189
|
+
const rule = this.checkRule(cmd);
|
|
190
|
+
if (rule) return rule === 'deny' ? 'deny' : rule;
|
|
191
|
+
if (this.isDangerous(cmd)) return 'ask';
|
|
192
|
+
return 'allow';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
reset() { this.sessionRules.clear(); }
|
|
196
|
+
|
|
197
|
+
// ── OpenCode-style resolution ──────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the action for (toolName, arg) given the current agent, config and
|
|
201
|
+
* session rules. arg is the command for bash, the file path for read/edit,
|
|
202
|
+
* the agent id for task, the skill name for skill, etc. Returns
|
|
203
|
+
* 'allow' | 'ask' | 'deny'.
|
|
204
|
+
*/
|
|
205
|
+
resolve(toolName, arg) {
|
|
206
|
+
const key = TOOL_TO_PERMISSION[toolName] !== undefined ? TOOL_TO_PERMISSION[toolName] : toolName;
|
|
207
|
+
if (!key) return 'allow';
|
|
208
|
+
|
|
209
|
+
// Safety heuristics always run for dangerous shell commands — even when
|
|
210
|
+
// a rule would allow the rest (defense in depth; unavoidable on
|
|
211
|
+
// "git push --force" unless the user explicitly disabled them).
|
|
212
|
+
if (key === 'bash' && this.isDangerous(String(arg || ''))) {
|
|
213
|
+
// An exact session rule overrides heuristics ("Always allow" picked in
|
|
214
|
+
// the popup for this very command).
|
|
215
|
+
const exact = this.sessionRules.get(String(arg || ''));
|
|
216
|
+
if (exact === 'allow') return 'allow';
|
|
217
|
+
if (exact === 'deny') return 'deny';
|
|
218
|
+
return 'ask';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 1. Legacy exact session rule (popup "Always allow"/"Never") — the
|
|
222
|
+
// strongest signal; the user picked it for this exact command/path.
|
|
223
|
+
const exact = this.sessionRules.get(String(arg ?? ''));
|
|
224
|
+
if (exact) return exact;
|
|
225
|
+
if (key === 'bash' && this.sessionRules.has('*')) return this.sessionRules.get('*');
|
|
226
|
+
|
|
227
|
+
const cfgPerm = this.config && this.config.permission;
|
|
228
|
+
// 2. Agent-scoped rules take precedence over the global permission tree.
|
|
229
|
+
const agentTree = this.agentId && this.config && this.config.agent && this.config.agent[this.agentId]
|
|
230
|
+
? this.config.agent[this.agentId].permission
|
|
231
|
+
: null;
|
|
232
|
+
if (agentTree) {
|
|
233
|
+
const v = matchNode(agentTree[key] ?? agentTree['*'], arg);
|
|
234
|
+
if (v) return v;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 3. Global permission tree from config.permission.
|
|
238
|
+
if (cfgPerm != null) {
|
|
239
|
+
const v = matchNode(typeof cfgPerm === 'string' ? cfgPerm : (cfgPerm[key] ?? cfgPerm['*']), arg);
|
|
240
|
+
if (v) return v;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 4. Defaults.
|
|
244
|
+
const def = DEFAULT_PERMISSIONS[key];
|
|
245
|
+
const d = matchNode(def ?? DEFAULT_PERMISSIONS['*'], arg) ?? matchNode(DEFAULT_PERMISSIONS['*'], arg);
|
|
246
|
+
return d || 'allow';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* External-directory check: resolvedPath is an absolute normalized path; if
|
|
251
|
+
* it lies outside the session cwd, consult permission.external_directory
|
|
252
|
+
* patterns (matched against the normalized absolute path). Returns
|
|
253
|
+
* 'allow' when inside or a pattern allowed it, otherwise the rule's action
|
|
254
|
+
* (default 'ask').
|
|
255
|
+
*/
|
|
256
|
+
checkExternal(resolvedPath) {
|
|
257
|
+
const cwd = (this.cwd || process.cwd()).replace(/\\/g, '/');
|
|
258
|
+
const p = String(resolvedPath || '').replace(/\\/g, '/');
|
|
259
|
+
if (!p || p === cwd || p.startsWith(cwd.replace(/\/+$/, '') + '/')) return 'allow';
|
|
260
|
+
const rule = this.resolveKey('external_directory', p);
|
|
261
|
+
return rule;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Resolved rule for an arbitrary permission key (used by external_directory
|
|
266
|
+
* and doom_loop which aren't tool names).
|
|
267
|
+
*/
|
|
268
|
+
resolveKey(permKey, arg) {
|
|
269
|
+
const cfgPerm = this.config && this.config.permission;
|
|
270
|
+
const agentTree = this.agentId && this.config && this.config.agent && this.config.agent[this.agentId]
|
|
271
|
+
? this.config.agent[this.agentId].permission
|
|
272
|
+
: null;
|
|
273
|
+
if (agentTree) {
|
|
274
|
+
const v = matchNode(agentTree[permKey] ?? agentTree['*'], arg);
|
|
275
|
+
if (v) return v;
|
|
276
|
+
}
|
|
277
|
+
if (cfgPerm != null) {
|
|
278
|
+
const v = matchNode(typeof cfgPerm === 'string' ? cfgPerm : (cfgPerm[permKey] ?? cfgPerm['*']), arg);
|
|
279
|
+
if (v) return v;
|
|
280
|
+
}
|
|
281
|
+
const def = DEFAULT_PERMISSIONS[permKey];
|
|
282
|
+
const d = matchNode(def ?? DEFAULT_PERMISSIONS['*'], arg) ?? matchNode(DEFAULT_PERMISSIONS['*'], arg);
|
|
283
|
+
return d || 'allow';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The argument a permission key is matched against for a given tool call. */
|
|
287
|
+
permissionArg(toolName, input, cwdOverride) {
|
|
288
|
+
const cwd = cwdOverride || this.cwd || process.cwd();
|
|
289
|
+
const inp = input || {};
|
|
290
|
+
switch (toolName) {
|
|
291
|
+
case 'bash': return String(inp.command ?? inp.args ?? '');
|
|
292
|
+
case 'read': return normPathArg(inp.filePath || inp.path || '', cwd);
|
|
293
|
+
case 'edit':
|
|
294
|
+
case 'write': return normPathArg(inp.filePath || inp.path || '', cwd);
|
|
295
|
+
case 'glob': return String(inp.pattern ?? '');
|
|
296
|
+
case 'grep': return String(inp.pattern ?? '');
|
|
297
|
+
case 'task': return String(inp.agent ?? inp.subagent ?? inp.agentId ?? '');
|
|
298
|
+
case 'skill': return String(inp.name ?? inp.skill ?? '');
|
|
299
|
+
case 'lsp': return String(inp.query ?? '');
|
|
300
|
+
case 'question':
|
|
301
|
+
case 'ask': return String(inp.question ?? inp.text ?? '');
|
|
302
|
+
case 'webfetch': return String(inp.url ?? '');
|
|
303
|
+
case 'websearch': return String(inp.query ?? '');
|
|
304
|
+
default: return String(inp.command ?? inp.filePath ?? inp.path ?? inp.pattern ?? inp.url ?? '');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Defense-in-depth label used by the tool layer: same heuristics as the
|
|
310
|
+
// permission manager, so tools can refuse destructive commands even when the
|
|
311
|
+
// session gate is bypassed (direct calls). Returns the label or null.
|
|
312
|
+
function commandRiskLabel(cmd) {
|
|
313
|
+
const lower = (cmd || '').toLowerCase();
|
|
314
|
+
for (const p of ADMIN_PATTERNS.concat(DANGER_PATTERNS)) {
|
|
315
|
+
if (p.re.test(lower)) return p.label;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = {
|
|
324
|
+
PermissionManager, ADMIN_PATTERNS, DANGER_PATTERNS, commandRiskLabel,
|
|
325
|
+
PERMISSION_KEYS, DEFAULT_PERMISSIONS, TOOL_TO_PERMISSION,
|
|
326
|
+
expandHome, wildcardToRegExp, matchNode, normPathArg,
|
|
327
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
|
|
3
|
+
let cache = null;
|
|
4
|
+
|
|
5
|
+
function detect() {
|
|
6
|
+
if (cache) return cache;
|
|
7
|
+
const p = os.platform();
|
|
8
|
+
const r = os.release();
|
|
9
|
+
const a = os.arch();
|
|
10
|
+
let wsl = false;
|
|
11
|
+
if (p === 'linux') {
|
|
12
|
+
try {
|
|
13
|
+
const low = r.toLowerCase();
|
|
14
|
+
if (low.includes('microsoft') || low.includes('wsl')) wsl = true;
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
}
|
|
17
|
+
cache = { platform: p, release: r, arch: a, isWSL: wsl };
|
|
18
|
+
return cache;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function verifyCommand(cmd) {
|
|
22
|
+
const info = detect();
|
|
23
|
+
if (info.platform === 'win32') return 'cmd /c "' + cmd + '"';
|
|
24
|
+
return cmd;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shellEnv() {
|
|
28
|
+
const info = detect();
|
|
29
|
+
if (info.platform === 'win32') return 'powershell';
|
|
30
|
+
return '/bin/bash';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { detect, verifyCommand, shellEnv };
|