reslop 0.3.2 → 0.3.3
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/CHANGELOG.md +13 -1
- package/README.md +19 -11
- package/lib/agents/agents.js +141 -32
- package/lib/agents/script.js +93 -0
- package/lib/render/agents.js +34 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.3][] - 2026-10-16
|
|
6
|
+
|
|
7
|
+
- Add Junie by JetBrains support
|
|
8
|
+
- Add GitHub Copilot support
|
|
9
|
+
- Add Pi coding agent support with its ACP adapter
|
|
10
|
+
- Add Kiro by AWS support, installed with its vendor script
|
|
11
|
+
- Add Cline support
|
|
12
|
+
- Add Windsurf support through Devin CLI, its current agent
|
|
13
|
+
- Add Kilo Code support
|
|
14
|
+
- Show agents in two columns on the dashboard, without models
|
|
15
|
+
|
|
5
16
|
## [0.3.2][] - 2026-10-15
|
|
6
17
|
|
|
7
18
|
- Communicate with agents over the Agent Client Protocol instead of CLI
|
|
@@ -213,7 +224,8 @@
|
|
|
213
224
|
- Feedback and todos that produce a repair plan for an agent
|
|
214
225
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
215
226
|
|
|
216
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.
|
|
227
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.3...HEAD
|
|
228
|
+
[0.3.3]: https://github.com/tshemsedinov/reslop/compare/v0.3.2...v0.3.3
|
|
217
229
|
[0.3.2]: https://github.com/tshemsedinov/reslop/compare/v0.3.1...v0.3.2
|
|
218
230
|
[0.3.1]: https://github.com/tshemsedinov/reslop/compare/v0.3.0...v0.3.1
|
|
219
231
|
[0.3.0]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...v0.3.0
|
package/README.md
CHANGED
|
@@ -25,13 +25,14 @@ Review → Plan → Repair → Verify
|
|
|
25
25
|
- Auto-reload local diffs when files change, keeping the current screen
|
|
26
26
|
- Import GitHub PR and GitLab MR review comments, and GitHub/GitLab issues, into tasks and the local plan
|
|
27
27
|
- Commits list: brief and full, view a commit's diff, commit, update, amend, apply, edit, fixup, and delete
|
|
28
|
-
- Branches: checkout, create, rebase, delete, pull, and push
|
|
28
|
+
- Branches: checkout, create, rebase, delete, pull, and push; git worktrees as tiles above the branches
|
|
29
|
+
- Pop-up menus on tables and controls
|
|
29
30
|
- npm scripts and bins: run, edit, and reorder; double-click to run; clean logs older than 5 days
|
|
30
31
|
- Reduced test and lint output for agents: failures and a short summary, not passing noise
|
|
31
32
|
- Review, renew, audit and update npm dependencies in `package.json` and the lockfile
|
|
32
33
|
- Auto-update reslop patch and minor releases; confirm a new major
|
|
33
34
|
- Work with git, npm, and fetch in the background; run tests in the background while you review
|
|
34
|
-
- Execute the plan in local agents over ACP: pick agent, model, and effort; watch progress; sessions, history, and continue or new runs
|
|
35
|
+
- Execute the plan in local agents over ACP: pick agent, model, and effort; watch progress; sessions, history, and continue or new runs. The dashboard lists agents in two columns, without models
|
|
35
36
|
- Agent defaults in `.reslop` (grouped settings sections)
|
|
36
37
|
- Optional line numbers in diff and file views (`Ctrl+L`)
|
|
37
38
|
|
|
@@ -82,15 +83,22 @@ Reviews go in `.plan/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
|
82
83
|
|
|
83
84
|
reslop runs a plan in an agent over the [Agent Client Protocol](https://agentclientprotocol.com). An agent is listed as available when the command that serves ACP is on `PATH`:
|
|
84
85
|
|
|
85
|
-
| Agent | Command | Install
|
|
86
|
-
| ----------- | -------------------- |
|
|
87
|
-
| Claude Code | `claude-agent-acp` | `npm i -g @agentclientprotocol/claude-agent-acp`
|
|
88
|
-
| Codex | `codex-acp` | `npm i -g @agentclientprotocol/codex-acp`
|
|
89
|
-
| Cursor | `cursor-agent acp` | comes with the Cursor CLI
|
|
90
|
-
| OpenCode | `opencode acp` | `npm i -g opencode-ai`
|
|
91
|
-
| Antigravity | `agy_acp_server.par` | downloaded from the official ACP registry
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
| Agent | Command | Install |
|
|
87
|
+
| ----------- | -------------------- | ------------------------------------------------- |
|
|
88
|
+
| Claude Code | `claude-agent-acp` | `npm i -g @agentclientprotocol/claude-agent-acp` |
|
|
89
|
+
| Codex | `codex-acp` | `npm i -g @agentclientprotocol/codex-acp` |
|
|
90
|
+
| Cursor | `cursor-agent acp` | comes with the Cursor CLI |
|
|
91
|
+
| OpenCode | `opencode acp` | `npm i -g opencode-ai` |
|
|
92
|
+
| Antigravity | `agy_acp_server.par` | downloaded from the official ACP registry |
|
|
93
|
+
| Junie | `junie --acp=true` | `npm i -g @jetbrains/junie` |
|
|
94
|
+
| Copilot | `copilot --acp` | `npm i -g @github/copilot` |
|
|
95
|
+
| Pi | `pi-acp` and `pi` | `npm i -g pi-acp @earendil-works/pi-coding-agent` |
|
|
96
|
+
| Kiro | `kiro-cli acp` | install script from cli.kiro.dev |
|
|
97
|
+
| Cline | `cline --acp` | `npm i -g cline` |
|
|
98
|
+
| Windsurf | `devin acp` | install script from cli.devin.ai |
|
|
99
|
+
| Kilo Code | `kilo acp` | `npm i -g @kilocode/cli` |
|
|
100
|
+
|
|
101
|
+
A missing agent shows what it needs, in the warning color; `i` on that row proposes installing it. npm adapters are installed globally, together with the agent's own CLI when the adapter runs it and it is not on `PATH`, while Google Antigravity's official ACP runtime is downloaded from the ACP registry into reslop's user data directory. Windsurf is listed as `devin`: Cognition's Devin CLI is now the agent of Windsurf, and the Windsurf editor does not serve ACP. An agent CLI published with an install script, such as Kiro's or Devin's, is installed by running the vendor's script, and its command is also found in `~/.local/bin`. reslop also discovers that runtime on `PATH` and in Zed's registry directory. After confirmation, reslop lists the agent as soon as the install finishes and does not restart. [docs/acp-adapters.md](docs/acp-adapters.md) describes the integrations.
|
|
94
102
|
|
|
95
103
|
Models are the ones the agent lists; efforts, context sizes, and fast mode are the ones it offers for the picked model. When a run ends, the session is set back to what it was, so the defaults of the agent's own CLI stay as they were. A run has nobody to ask, so reslop answers permission requests itself: edits inside the repository and `npm`, `npx`, `node`, `reslop`, and `git` commands, piped through read-only filters such as `grep` or `tail`, are allowed; everything else is refused.
|
|
96
104
|
|
package/lib/agents/agents.js
CHANGED
|
@@ -11,6 +11,7 @@ const { choiceOf } = models;
|
|
|
11
11
|
const { startRun, probeSession } = require('./run.js');
|
|
12
12
|
const registry = require('./registry.js');
|
|
13
13
|
const { installedRegistryAgent } = registry;
|
|
14
|
+
const { localBin, scriptUrl } = require('./script.js');
|
|
14
15
|
const { resolveGitDirs } = require('../git/dir.js');
|
|
15
16
|
|
|
16
17
|
const CODEX_SANDBOX = 'sandbox_workspace_write';
|
|
@@ -58,9 +59,15 @@ const defineAgent = (spec) => ({
|
|
|
58
59
|
title: spec.title,
|
|
59
60
|
commands: spec.commands,
|
|
60
61
|
package: spec.package ?? '',
|
|
62
|
+
// The cli of the agent that an adapter starts, as { bin, package }
|
|
63
|
+
requires: spec.requires ?? null,
|
|
61
64
|
registry: spec.registry ?? '',
|
|
65
|
+
// The install scripts of the vendor, as { unix, win } https urls
|
|
66
|
+
script: spec.script ?? null,
|
|
62
67
|
installLabel: spec.installLabel ?? '',
|
|
63
68
|
hint: spec.hint ?? '',
|
|
69
|
+
// A program of the vendor that does not serve ACP, as { bin, hint }
|
|
70
|
+
related: spec.related ?? null,
|
|
64
71
|
mode: spec.mode ?? '',
|
|
65
72
|
env: spec.env ?? (() => ({})),
|
|
66
73
|
});
|
|
@@ -104,8 +111,73 @@ const AGENTS = [
|
|
|
104
111
|
registry: 'antigravity-acp',
|
|
105
112
|
installLabel: 'Google Antigravity ACP',
|
|
106
113
|
hint: 'install Google Antigravity ACP',
|
|
114
|
+
related: {
|
|
115
|
+
bin: 'agy',
|
|
116
|
+
hint: 'agy installed; install Google Antigravity ACP',
|
|
117
|
+
},
|
|
107
118
|
env: antigravityEnv,
|
|
108
119
|
}),
|
|
120
|
+
defineAgent({
|
|
121
|
+
id: 'junie',
|
|
122
|
+
title: 'Junie',
|
|
123
|
+
commands: [['junie', '--acp=true']],
|
|
124
|
+
package: '@jetbrains/junie',
|
|
125
|
+
}),
|
|
126
|
+
defineAgent({
|
|
127
|
+
id: 'copilot',
|
|
128
|
+
title: 'Copilot',
|
|
129
|
+
commands: [['copilot', '--acp']],
|
|
130
|
+
package: '@github/copilot',
|
|
131
|
+
}),
|
|
132
|
+
defineAgent({
|
|
133
|
+
id: 'pi',
|
|
134
|
+
title: 'Pi',
|
|
135
|
+
commands: [['pi-acp']],
|
|
136
|
+
package: 'pi-acp',
|
|
137
|
+
requires: { bin: 'pi', package: '@earendil-works/pi-coding-agent' },
|
|
138
|
+
}),
|
|
139
|
+
defineAgent({
|
|
140
|
+
id: 'kiro',
|
|
141
|
+
title: 'Kiro',
|
|
142
|
+
commands: [['kiro-cli', 'acp', '--agent-engine=v3']],
|
|
143
|
+
script: {
|
|
144
|
+
unix: 'https://cli.kiro.dev/install',
|
|
145
|
+
win: 'https://cli.kiro.dev/install.ps1',
|
|
146
|
+
},
|
|
147
|
+
installLabel: 'Kiro CLI from cli.kiro.dev',
|
|
148
|
+
hint: 'install the Kiro CLI from cli.kiro.dev',
|
|
149
|
+
}),
|
|
150
|
+
defineAgent({
|
|
151
|
+
id: 'cline',
|
|
152
|
+
title: 'Cline',
|
|
153
|
+
commands: [['cline', '--acp']],
|
|
154
|
+
package: 'cline',
|
|
155
|
+
}),
|
|
156
|
+
// Windsurf's agent is now the Devin CLI of Cognition; the editor has no ACP
|
|
157
|
+
defineAgent({
|
|
158
|
+
id: 'devin',
|
|
159
|
+
title: 'Devin for Windsurf',
|
|
160
|
+
commands: [['devin', 'acp']],
|
|
161
|
+
script: {
|
|
162
|
+
unix: 'https://cli.devin.ai/install.sh',
|
|
163
|
+
win: 'https://static.devin.ai/cli/setup.ps1',
|
|
164
|
+
},
|
|
165
|
+
installLabel: 'Devin CLI from cli.devin.ai',
|
|
166
|
+
hint: 'install the Devin CLI, the agent of Windsurf',
|
|
167
|
+
related: {
|
|
168
|
+
bin: 'windsurf',
|
|
169
|
+
hint: 'windsurf installed; install the Devin CLI',
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
defineAgent({
|
|
173
|
+
id: 'kilo',
|
|
174
|
+
title: 'Kilo Code',
|
|
175
|
+
commands: [
|
|
176
|
+
['kilo', 'acp'],
|
|
177
|
+
['kilocode', 'acp'],
|
|
178
|
+
],
|
|
179
|
+
package: '@kilocode/cli',
|
|
180
|
+
}),
|
|
109
181
|
];
|
|
110
182
|
|
|
111
183
|
const agentSpec = (id) => AGENTS.find((spec) => spec.id === id) ?? null;
|
|
@@ -139,8 +211,8 @@ const isBin = (file) => {
|
|
|
139
211
|
}
|
|
140
212
|
};
|
|
141
213
|
|
|
142
|
-
const
|
|
143
|
-
for (const dir of
|
|
214
|
+
const findIn = (dirs, name, env) => {
|
|
215
|
+
for (const dir of dirs) {
|
|
144
216
|
for (const file of binNames(name, env)) {
|
|
145
217
|
const full = path.join(dir, file);
|
|
146
218
|
if (isBin(full)) return full;
|
|
@@ -149,9 +221,19 @@ const findBin = (name, env = process.env) => {
|
|
|
149
221
|
return '';
|
|
150
222
|
};
|
|
151
223
|
|
|
224
|
+
const findBin = (name, env = process.env) => findIn(pathDirs(env), name, env);
|
|
225
|
+
|
|
226
|
+
// An install script may put its command in a directory missing from PATH
|
|
227
|
+
const commandDirs = (spec, env) => {
|
|
228
|
+
const dirs = pathDirs(env);
|
|
229
|
+
const local = spec.script ? localBin(env) : '';
|
|
230
|
+
return local && !dirs.includes(local) ? [...dirs, local] : dirs;
|
|
231
|
+
};
|
|
232
|
+
|
|
152
233
|
const findCommand = (spec, env) => {
|
|
234
|
+
const dirs = commandDirs(spec, env);
|
|
153
235
|
for (const [name, ...args] of spec.commands) {
|
|
154
|
-
const bin =
|
|
236
|
+
const bin = findIn(dirs, name, env);
|
|
155
237
|
if (bin) return { bin, args };
|
|
156
238
|
}
|
|
157
239
|
if (spec.registry) {
|
|
@@ -161,27 +243,44 @@ const findCommand = (spec, env) => {
|
|
|
161
243
|
return { bin: '', args: [] };
|
|
162
244
|
};
|
|
163
245
|
|
|
164
|
-
//
|
|
165
|
-
const
|
|
246
|
+
// npm packages still missing: the adapter and the cli of the agent it starts
|
|
247
|
+
const missingPackages = (spec, env, found) => {
|
|
248
|
+
const names = [];
|
|
249
|
+
if (!found.bin && spec.package) names.push(spec.package);
|
|
250
|
+
const cli = spec.requires;
|
|
251
|
+
if (cli && !findBin(cli.bin, env)) names.push(cli.package);
|
|
252
|
+
return names;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// What a missing agent takes: npm packages, a registry binary, or a script
|
|
256
|
+
const installHint = (spec, env, packages) => {
|
|
257
|
+
if (packages.length) return `npm i -g ${packages.join(' ')}`;
|
|
166
258
|
if (spec.package) return `npm i -g ${spec.package}`;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
259
|
+
const { related } = spec;
|
|
260
|
+
if (related && findBin(related.bin, env)) return related.hint;
|
|
170
261
|
return spec.hint;
|
|
171
262
|
};
|
|
172
263
|
|
|
173
|
-
const
|
|
174
|
-
|
|
264
|
+
const detectAgent = (spec, env) => {
|
|
265
|
+
const found = findCommand(spec, env);
|
|
266
|
+
const packages = missingPackages(spec, env, found);
|
|
267
|
+
const command = packages.length ? { bin: '', args: [] } : found;
|
|
268
|
+
return {
|
|
175
269
|
id: spec.id,
|
|
176
270
|
name: spec.name,
|
|
177
271
|
spec,
|
|
178
|
-
package: spec.package,
|
|
179
|
-
|
|
272
|
+
package: packages.join(' ') || spec.package,
|
|
273
|
+
packages,
|
|
274
|
+
installable: Boolean(spec.package || spec.registry || spec.script),
|
|
180
275
|
installLabel: spec.installLabel,
|
|
181
|
-
hint: installHint(spec, env),
|
|
276
|
+
hint: installHint(spec, env, packages),
|
|
182
277
|
catalog: null,
|
|
183
|
-
...
|
|
184
|
-
}
|
|
278
|
+
...command,
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const detectAgents = (env = process.env) =>
|
|
283
|
+
AGENTS.map((spec) => detectAgent(spec, env));
|
|
185
284
|
|
|
186
285
|
const listModels = (row, options = {}) => {
|
|
187
286
|
if (!row || !row.bin) return Promise.resolve(null);
|
|
@@ -239,34 +338,44 @@ const buildLaunch = (row, picked, reviewPath, cwd, sessionId = '') => {
|
|
|
239
338
|
return launch;
|
|
240
339
|
};
|
|
241
340
|
|
|
341
|
+
// A helper of reslop downloads a registry binary or runs a vendor script
|
|
342
|
+
const helperInstall = (row, installer, file, arg) => ({
|
|
343
|
+
installer,
|
|
344
|
+
cmd: process.execPath,
|
|
345
|
+
args: [path.join(__dirname, file), arg],
|
|
346
|
+
command: `install ${row.spec.installLabel || row.name}`,
|
|
347
|
+
});
|
|
348
|
+
|
|
242
349
|
// The package lands in the global prefix of npm, where PATH finds its command
|
|
350
|
+
const installCommand = (row) => {
|
|
351
|
+
const { spec } = row;
|
|
352
|
+
if (spec.registry) {
|
|
353
|
+
return helperInstall(row, 'registry', 'registry.js', spec.registry);
|
|
354
|
+
}
|
|
355
|
+
if (spec.script) {
|
|
356
|
+
const url = scriptUrl(spec.script);
|
|
357
|
+
return helperInstall(row, 'script', 'script.js', url);
|
|
358
|
+
}
|
|
359
|
+
const names = row.packages ?? [spec.package].filter(Boolean);
|
|
360
|
+
if (!names.length) return null;
|
|
361
|
+
const args = ['install', '--global', ...names];
|
|
362
|
+
const command = `npm ${args.join(' ')}`;
|
|
363
|
+
return { installer: 'npm', cmd: npmBin(), args, command };
|
|
364
|
+
};
|
|
365
|
+
|
|
243
366
|
const buildInstall = (row) => {
|
|
244
367
|
if (!row) return { ok: false, error: 'no agent' };
|
|
245
368
|
if (row.bin) return { ok: false, error: 'installed' };
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
if (!name && !registryId) {
|
|
249
|
-
return { ok: false, error: row.hint || 'no installer' };
|
|
250
|
-
}
|
|
251
|
-
const npmArgs = name ? ['install', '--global', name] : [];
|
|
252
|
-
const args = registryId
|
|
253
|
-
? [path.join(__dirname, 'registry.js'), registryId]
|
|
254
|
-
: npmArgs;
|
|
255
|
-
const cmd = registryId ? process.execPath : npmBin();
|
|
256
|
-
const command = registryId
|
|
257
|
-
? `install ${row.spec.installLabel || row.name}`
|
|
258
|
-
: `npm ${npmArgs.join(' ')}`;
|
|
369
|
+
const install = installCommand(row);
|
|
370
|
+
if (!install) return { ok: false, error: row.hint || 'no installer' };
|
|
259
371
|
return {
|
|
260
372
|
ok: true,
|
|
261
373
|
id: row.id,
|
|
262
374
|
name: row.name,
|
|
263
375
|
kind: 'install',
|
|
264
|
-
|
|
265
|
-
cmd,
|
|
266
|
-
args,
|
|
376
|
+
...install,
|
|
267
377
|
plan: '',
|
|
268
378
|
session: '',
|
|
269
|
-
command,
|
|
270
379
|
};
|
|
271
380
|
};
|
|
272
381
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawn } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
// Where the install scripts of agent CLIs put their command
|
|
9
|
+
const localBin = (env = process.env) => {
|
|
10
|
+
const home = env.HOME || env.USERPROFILE;
|
|
11
|
+
return home ? path.join(home, '.local', 'bin') : '';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const httpsUrl = (href) => {
|
|
15
|
+
if (!href) throw new Error('no install script for this platform');
|
|
16
|
+
const url = new URL(href);
|
|
17
|
+
if (url.protocol !== 'https:') {
|
|
18
|
+
throw new Error('install script must use https');
|
|
19
|
+
}
|
|
20
|
+
return url.href;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The script a vendor publishes for this platform
|
|
24
|
+
const scriptUrl = (script, platform = process.platform) =>
|
|
25
|
+
httpsUrl(platform === 'win32' ? script.win : script.unix);
|
|
26
|
+
|
|
27
|
+
const shellOf = (file, platform) => {
|
|
28
|
+
if (platform !== 'win32') return ['bash', [file]];
|
|
29
|
+
const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', file];
|
|
30
|
+
return ['powershell.exe', args];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// The script finds the directory it installs to on PATH and does not fail
|
|
34
|
+
const scriptEnv = (env) => {
|
|
35
|
+
const bin = localBin(env);
|
|
36
|
+
const key = Object.keys(env).find((name) => name.toUpperCase() === 'PATH');
|
|
37
|
+
const name = key ?? 'PATH';
|
|
38
|
+
const dirs = `${env[name] ?? ''}`.split(path.delimiter).filter(Boolean);
|
|
39
|
+
if (!bin || dirs.includes(bin)) return { ...env };
|
|
40
|
+
return { ...env, [name]: [bin, ...dirs].join(path.delimiter) };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const runFile = (cmd, args, env, spawnImpl) =>
|
|
44
|
+
new Promise((resolve, reject) => {
|
|
45
|
+
const stdio = ['ignore', 'inherit', 'inherit'];
|
|
46
|
+
const child = spawnImpl(cmd, args, { env, stdio, windowsHide: true });
|
|
47
|
+
child.on('error', reject);
|
|
48
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const runScript = async (url, options = {}) => {
|
|
52
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
53
|
+
if (typeof fetchImpl !== 'function') {
|
|
54
|
+
throw new Error('fetch is not available');
|
|
55
|
+
}
|
|
56
|
+
const env = options.env ?? process.env;
|
|
57
|
+
const platform = options.platform ?? process.platform;
|
|
58
|
+
const log = options.log ?? (() => {});
|
|
59
|
+
const href = httpsUrl(url);
|
|
60
|
+
log(`Downloading ${href}…`);
|
|
61
|
+
const response = await fetchImpl(href);
|
|
62
|
+
if (!response || !response.ok) {
|
|
63
|
+
const status = response ? ` (${response.status})` : '';
|
|
64
|
+
throw new Error(`download failed${status}`);
|
|
65
|
+
}
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'reslop-'));
|
|
68
|
+
try {
|
|
69
|
+
const name = platform === 'win32' ? 'install.ps1' : 'install.sh';
|
|
70
|
+
const file = path.join(dir, name);
|
|
71
|
+
await fs.promises.writeFile(file, text, { mode: 0o600 });
|
|
72
|
+
const [cmd, args] = shellOf(file, platform);
|
|
73
|
+
const spawnImpl = options.spawn ?? spawn;
|
|
74
|
+
return await runFile(cmd, args, scriptEnv(env), spawnImpl);
|
|
75
|
+
} finally {
|
|
76
|
+
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const main = async () => {
|
|
81
|
+
try {
|
|
82
|
+
process.exitCode = await runScript(process.argv[2], {
|
|
83
|
+
log: (line) => process.stdout.write(`${line}\n`),
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
process.stderr.write(`${error.message}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (require.main === module) void main();
|
|
92
|
+
|
|
93
|
+
module.exports = { localBin, scriptUrl, scriptEnv, runScript };
|
package/lib/render/agents.js
CHANGED
|
@@ -21,6 +21,7 @@ const { paintField, queryTones, fieldHit } = require('./field.js');
|
|
|
21
21
|
|
|
22
22
|
const BUBBLE = '●';
|
|
23
23
|
const BUBBLE_GAP = 1;
|
|
24
|
+
const COLUMN_GAP = 2;
|
|
24
25
|
const BUBBLE_PHASE = ['key', 'add', 'sha', 'warn'];
|
|
25
26
|
const PAD_W = 2;
|
|
26
27
|
const TAIL = 2;
|
|
@@ -642,13 +643,29 @@ const agentLead = (item, frame) => {
|
|
|
642
643
|
return cellSegs(segs);
|
|
643
644
|
};
|
|
644
645
|
|
|
645
|
-
|
|
646
|
+
// The last column clips its names when the tile is narrow
|
|
647
|
+
const agentCells = (item, frame, last) => {
|
|
648
|
+
if (!item) return [];
|
|
646
649
|
const tone = item.bin ? 'text' : 'muted';
|
|
647
|
-
const name = flexCell(item.name, tone);
|
|
650
|
+
const name = last ? flexCell(item.name, tone) : cell(item.name, tone);
|
|
648
651
|
name.gap = BUBBLE_GAP;
|
|
649
|
-
return name;
|
|
652
|
+
return [agentLead(item, frame), name];
|
|
650
653
|
};
|
|
651
654
|
|
|
655
|
+
// Two columns while both fit their widest agent, else one
|
|
656
|
+
const columnCount = (items, width) => {
|
|
657
|
+
let lead = 0;
|
|
658
|
+
let name = 0;
|
|
659
|
+
for (const item of items) {
|
|
660
|
+
const sessions = item.running ? ` ${item.running}` : '';
|
|
661
|
+
lead = Math.max(lead, visibleWidth(`${BUBBLE}${sessions}`));
|
|
662
|
+
name = Math.max(name, visibleWidth(item.name));
|
|
663
|
+
}
|
|
664
|
+
const entry = lead + BUBBLE_GAP + name;
|
|
665
|
+
return entry * 2 + COLUMN_GAP <= width ? 2 : 1;
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// Agents fill the left column first
|
|
652
669
|
const agentsBlock = (model, width, height, ctx, tile) => {
|
|
653
670
|
const data = model.agents;
|
|
654
671
|
if (!data || !data.ready) return waiting('detecting agents…');
|
|
@@ -656,13 +673,20 @@ const agentsBlock = (model, width, height, ctx, tile) => {
|
|
|
656
673
|
const titleLine = titleAside(tile, aside, width);
|
|
657
674
|
const items = data.items ?? [];
|
|
658
675
|
if (!items.length) return notice('no agents', titleLine);
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
676
|
+
const columns = columnCount(items, width);
|
|
677
|
+
const perColumn = Math.ceil(items.length / columns);
|
|
678
|
+
const count = Math.min(Math.max(0, height), perColumn);
|
|
679
|
+
const rows = [];
|
|
680
|
+
for (let i = 0; i < count; i++) {
|
|
681
|
+
const left = agentCells(items[i], ctx.frame, columns === 1);
|
|
682
|
+
if (columns === 1) {
|
|
683
|
+
rows.push(left);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const right = agentCells(items[i + count], ctx.frame, true);
|
|
687
|
+
rows.push([...left, ...right]);
|
|
688
|
+
}
|
|
689
|
+
return { titleLine, lines: tableLines(rows, width, COLUMN_GAP) };
|
|
666
690
|
};
|
|
667
691
|
|
|
668
692
|
module.exports = { paintBodyAgents, agentsBlock };
|