@phnx-labs/agents-cli 1.20.33 → 1.20.35
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 +29 -0
- package/README.md +32 -2
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +108 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +93 -17
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +42 -0
- package/dist/commands/view.js +213 -83
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +61 -32
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +145 -41
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +27 -0
- package/dist/lib/browser/drivers/ssh.js +94 -19
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +43 -11
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +30 -2
- package/dist/lib/shims.js +107 -50
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +27 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
package/dist/lib/ssh-tunnel.js
CHANGED
|
@@ -22,7 +22,6 @@ import * as fs from 'fs';
|
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
24
|
import { randomBytes } from 'crypto';
|
|
25
|
-
import { Transform } from 'stream';
|
|
26
25
|
import { sshExec, SSH_OPTS } from './ssh-exec.js';
|
|
27
26
|
import { encodePowerShell } from './browser/drivers/ssh.js';
|
|
28
27
|
import { getDevice } from './devices/registry.js';
|
|
@@ -30,19 +29,18 @@ import { sshTargetFor } from './devices/connect.js';
|
|
|
30
29
|
import { hostNameFor } from './devices/ssh-config.js';
|
|
31
30
|
import { getCacheDir } from './state.js';
|
|
32
31
|
import { openComputerClient, resolveTcpEndpoint } from './computer-rpc.js';
|
|
33
|
-
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
32
|
+
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
33
|
+
*
|
|
34
|
+
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
35
|
+
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
36
|
+
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
34
37
|
export function buildTunnelArgs(user, host, localPort, remotePort) {
|
|
35
38
|
return [
|
|
36
39
|
'-L',
|
|
37
40
|
`${localPort}:127.0.0.1:${remotePort}`,
|
|
38
41
|
`${user}@${host}`,
|
|
39
42
|
'-N',
|
|
40
|
-
|
|
41
|
-
'StrictHostKeyChecking=accept-new',
|
|
42
|
-
'-o',
|
|
43
|
-
'BatchMode=yes',
|
|
44
|
-
'-o',
|
|
45
|
-
'ConnectTimeout=10',
|
|
43
|
+
...SSH_OPTS,
|
|
46
44
|
];
|
|
47
45
|
}
|
|
48
46
|
/**
|
|
@@ -150,10 +148,15 @@ export async function resolveRemoteDevice(name) {
|
|
|
150
148
|
return { device, target, user, host };
|
|
151
149
|
}
|
|
152
150
|
/**
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
* Single-quote a string for embedding inside a PowerShell literal.
|
|
152
|
+
*/
|
|
153
|
+
function psSingleQuote(s) {
|
|
154
|
+
return "'" + s.replace(/'/g, "''") + "'";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* PowerShell that resolves the destination under %LOCALAPPDATA%\agents and
|
|
158
|
+
* stops any running instance first so the file is not locked. The caller copies
|
|
159
|
+
* the exe with scp and then verifies the byte count separately.
|
|
157
160
|
*/
|
|
158
161
|
export function buildPushScript() {
|
|
159
162
|
return [
|
|
@@ -161,15 +164,18 @@ export function buildPushScript() {
|
|
|
161
164
|
`New-Item -ItemType Directory -Force -Path $dir | Out-Null`,
|
|
162
165
|
`$dst = Join-Path $dir '${WIN_HELPER_EXE}'`,
|
|
163
166
|
`Get-Process -Name 'computer-helper-win' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue`,
|
|
164
|
-
`$si = [Console]::OpenStandardInput()`,
|
|
165
|
-
`$t = New-Object Security.Cryptography.FromBase64Transform`,
|
|
166
|
-
`$cs = New-Object Security.Cryptography.CryptoStream($si, $t, [Security.Cryptography.CryptoStreamMode]::Read)`,
|
|
167
|
-
`$fs = [IO.File]::Create($dst)`,
|
|
168
|
-
`$cs.CopyTo($fs)`,
|
|
169
|
-
`$fs.Close(); $cs.Close()`,
|
|
170
167
|
`Write-Output $dst`,
|
|
171
168
|
].join('; ');
|
|
172
169
|
}
|
|
170
|
+
/** PowerShell that verifies scp wrote the expected number of bytes. */
|
|
171
|
+
export function buildVerifyPushScript(remotePath, expectedBytes) {
|
|
172
|
+
return [
|
|
173
|
+
`$dst = ${psSingleQuote(remotePath)}`,
|
|
174
|
+
`$item = Get-Item -LiteralPath $dst -ErrorAction Stop`,
|
|
175
|
+
`if ($item.Length -ne ${expectedBytes}) { throw "helper copy length mismatch: expected ${expectedBytes}, got $($item.Length)" }`,
|
|
176
|
+
`Write-Output "$dst $($item.Length)"`,
|
|
177
|
+
].join('; ');
|
|
178
|
+
}
|
|
173
179
|
/**
|
|
174
180
|
* PowerShell that registers the daemon as a LOGON scheduled task. Interactive
|
|
175
181
|
* logon type + Highest run level so the daemon runs in the real desktop session
|
|
@@ -195,51 +201,32 @@ export function buildUnregisterTaskScript(taskName) {
|
|
|
195
201
|
`Get-Process -Name 'computer-helper-win' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue`,
|
|
196
202
|
].join('; ');
|
|
197
203
|
}
|
|
204
|
+
/** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
|
|
205
|
+
export function scpRemotePath(remotePath) {
|
|
206
|
+
return remotePath.replace(/\\/g, '/');
|
|
207
|
+
}
|
|
198
208
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*/
|
|
203
|
-
/**
|
|
204
|
-
* Base64-encode a byte stream in 3-byte-aligned chunks so the concatenated
|
|
205
|
-
* output is valid (every chunk boundary lands on a base64 quantum).
|
|
209
|
+
* Build the scp argv used for the helper exe transfer. Exported so tests can
|
|
210
|
+
* assert the real binary copy path keeps BatchMode and does not route bytes
|
|
211
|
+
* through a PowerShell decoder.
|
|
206
212
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
_transform(chunk, _enc, cb) {
|
|
210
|
-
const buf = this.leftover.length ? Buffer.concat([this.leftover, chunk]) : chunk;
|
|
211
|
-
const usable = buf.length - (buf.length % 3);
|
|
212
|
-
this.leftover = Buffer.from(buf.subarray(usable));
|
|
213
|
-
if (usable > 0)
|
|
214
|
-
this.push(buf.subarray(0, usable).toString('base64'));
|
|
215
|
-
cb();
|
|
216
|
-
}
|
|
217
|
-
_flush(cb) {
|
|
218
|
-
if (this.leftover.length)
|
|
219
|
-
this.push(this.leftover.toString('base64'));
|
|
220
|
-
cb();
|
|
221
|
-
}
|
|
213
|
+
export function buildScpArgs(target, remotePath, filePath) {
|
|
214
|
+
return [...SSH_OPTS, filePath, `${target}:${scpRemotePath(remotePath)}`];
|
|
222
215
|
}
|
|
223
216
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* `spawnSync({ input })` blob deadlocked once the ssh socket buffer filled
|
|
227
|
-
* (~4MB) on large files (the 157MB Windows helper reproduced this reliably),
|
|
228
|
-
* and worse, reported a false success leaving a 0-byte remote file. Rejects on
|
|
229
|
-
* any pipe error so a broken transfer fails loudly instead.
|
|
217
|
+
* Copy a local file to the remote destination with scp. This is a binary
|
|
218
|
+
* transfer; no base64 transform runs on either side.
|
|
230
219
|
*/
|
|
231
|
-
function
|
|
220
|
+
function copyFileOverScp(target, remotePath, filePath, timeoutMs = 600_000) {
|
|
232
221
|
return new Promise((resolve, reject) => {
|
|
233
|
-
const child = spawn('
|
|
234
|
-
stdio: ['
|
|
222
|
+
const child = spawn('scp', buildScpArgs(target, remotePath, filePath), {
|
|
223
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
235
224
|
});
|
|
236
225
|
let stderr = '';
|
|
237
|
-
let stdout = '';
|
|
238
226
|
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
239
|
-
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
240
227
|
const timer = setTimeout(() => {
|
|
241
228
|
child.kill('SIGKILL');
|
|
242
|
-
reject(new Error(`
|
|
229
|
+
reject(new Error(`scp push to ${target} timed out after ${timeoutMs}ms`));
|
|
243
230
|
}, timeoutMs);
|
|
244
231
|
const fail = (e) => {
|
|
245
232
|
clearTimeout(timer);
|
|
@@ -247,30 +234,41 @@ function streamFileOverSsh(target, remoteCmd, filePath, timeoutMs = 600_000) {
|
|
|
247
234
|
reject(e);
|
|
248
235
|
};
|
|
249
236
|
child.on('error', fail);
|
|
250
|
-
child.stdin.on('error', fail); // EPIPE if the remote decoder dies mid-stream
|
|
251
237
|
child.on('close', (code) => {
|
|
252
238
|
clearTimeout(timer);
|
|
253
|
-
resolve({ code, stderr
|
|
239
|
+
resolve({ code, stderr });
|
|
254
240
|
});
|
|
255
|
-
const src = fs.createReadStream(filePath);
|
|
256
|
-
src.on('error', fail);
|
|
257
|
-
// disk -> aligned base64 -> ssh stdin; .pipe() applies backpressure
|
|
258
|
-
src.pipe(new Base64Encode()).pipe(child.stdin);
|
|
259
241
|
});
|
|
260
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* `setup --host`: push the exe, then register + start the LOGON task. Remote
|
|
245
|
+
* PowerShell hops go through `sshExec` (BatchMode key auth — the same hardening
|
|
246
|
+
* the browser driver and `agents ssh` use), and the large exe rides a binary
|
|
247
|
+
* scp transfer. Throws with the remote stderr on any failure.
|
|
248
|
+
*/
|
|
261
249
|
export async function setupRemoteHelper(name) {
|
|
262
250
|
const { target } = await resolveRemoteDevice(name);
|
|
263
251
|
const exe = resolveWinHelperExe();
|
|
264
252
|
if (!exe) {
|
|
265
253
|
throw new Error(`Windows helper exe not built. Run: bash scripts/build-win.sh`);
|
|
266
254
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
255
|
+
const prep = sshExec(target, encodePowerShell(buildPushScript()), { timeoutMs: 60_000 });
|
|
256
|
+
if (prep.code !== 0) {
|
|
257
|
+
throw new Error(`preparing helper exe path on '${name}' failed (exit ${prep.code ?? 'null'}): ${prep.stderr.trim() || prep.stdout.trim()}`);
|
|
258
|
+
}
|
|
259
|
+
const remotePath = prep.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
260
|
+
if (!remotePath) {
|
|
261
|
+
throw new Error(`preparing helper exe path on '${name}' did not return a destination path`);
|
|
262
|
+
}
|
|
263
|
+
const push = await copyFileOverScp(target, remotePath, exe);
|
|
271
264
|
if (push.code !== 0) {
|
|
272
265
|
throw new Error(`pushing helper exe to '${name}' failed (exit ${push.code ?? 'null'}): ${push.stderr.trim()}`);
|
|
273
266
|
}
|
|
267
|
+
const expectedBytes = fs.statSync(exe).size;
|
|
268
|
+
const verify = sshExec(target, encodePowerShell(buildVerifyPushScript(remotePath, expectedBytes)), { timeoutMs: 60_000 });
|
|
269
|
+
if (verify.code !== 0) {
|
|
270
|
+
throw new Error(`verifying helper exe on '${name}' failed (exit ${verify.code ?? 'null'}): ${verify.stderr.trim() || verify.stdout.trim()}`);
|
|
271
|
+
}
|
|
274
272
|
// Register + start the LOGON task.
|
|
275
273
|
const reg = sshExec(target, encodePowerShell(buildRegisterTaskScript(REMOTE_HELPER_PORT, REMOTE_TASK_NAME)), {
|
|
276
274
|
timeoutMs: 60_000,
|
|
@@ -32,7 +32,7 @@ function buildHooksWriter(agent) {
|
|
|
32
32
|
}
|
|
33
33
|
// Native hook registration in settings.json/hooks.json. Grok auto-
|
|
34
34
|
// discovers from ~/.grok/hooks/ so the file copy is sufficient.
|
|
35
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity' || agent === 'kimi') {
|
|
35
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid') {
|
|
36
36
|
registerHooksToSettings(agent, versionHome);
|
|
37
37
|
}
|
|
38
38
|
return { synced };
|
|
@@ -49,6 +49,7 @@ export declare const loadPrune: ModuleLoader;
|
|
|
49
49
|
export declare const loadTrash: ModuleLoader;
|
|
50
50
|
export declare const loadRestore: ModuleLoader;
|
|
51
51
|
export declare const loadDoctor: ModuleLoader;
|
|
52
|
+
export declare const loadStatus: ModuleLoader;
|
|
52
53
|
export declare const loadProfiles: ModuleLoader;
|
|
53
54
|
export declare const loadSecrets: ModuleLoader;
|
|
54
55
|
export declare const loadWallet: ModuleLoader;
|
|
@@ -68,6 +69,8 @@ export declare const loadTmux: ModuleLoader;
|
|
|
68
69
|
export declare const loadBrowser: ModuleLoader;
|
|
69
70
|
export declare const loadComputer: ModuleLoader;
|
|
70
71
|
export declare const loadHosts: ModuleLoader;
|
|
72
|
+
export declare const loadLogs: ModuleLoader;
|
|
73
|
+
export declare const loadEvents: ModuleLoader;
|
|
71
74
|
export declare const loadSsh: ModuleLoader;
|
|
72
75
|
export declare const loadPull: ModuleLoader;
|
|
73
76
|
export declare const loadPush: ModuleLoader;
|
|
@@ -27,6 +27,7 @@ export const loadPrune = async () => (await import('../../commands/prune.js')).r
|
|
|
27
27
|
export const loadTrash = async () => (await import('../../commands/trash.js')).registerTrashCommands;
|
|
28
28
|
export const loadRestore = async () => (await import('../../commands/trash.js')).registerRestoreCommand;
|
|
29
29
|
export const loadDoctor = async () => (await import('../../commands/doctor.js')).registerDoctorCommand;
|
|
30
|
+
export const loadStatus = async () => (await import('../../commands/status.js')).registerStatusCommand;
|
|
30
31
|
export const loadProfiles = async () => (await import('../../commands/profiles.js')).registerProfilesCommands;
|
|
31
32
|
export const loadSecrets = async () => (await import('../../commands/secrets.js')).registerSecretsCommands;
|
|
32
33
|
export const loadWallet = async () => (await import('../../commands/wallet.js')).registerWalletCommands;
|
|
@@ -46,6 +47,8 @@ export const loadTmux = async () => (await import('../../commands/tmux.js')).reg
|
|
|
46
47
|
export const loadBrowser = async () => (await import('../../commands/browser.js')).registerBrowserCommand;
|
|
47
48
|
export const loadComputer = async () => (await import('../../commands/computer.js')).registerComputerCommand;
|
|
48
49
|
export const loadHosts = async () => (await import('../../commands/hosts.js')).registerHostsCommand;
|
|
50
|
+
export const loadLogs = async () => (await import('../../commands/logs.js')).registerLogsCommand;
|
|
51
|
+
export const loadEvents = async () => (await import('../../commands/events.js')).registerEventsCommand;
|
|
49
52
|
export const loadSsh = async () => (await import('../../commands/ssh.js')).registerSshCommands;
|
|
50
53
|
export const loadPull = async () => (await import('../../commands/pull.js')).registerPullCommand;
|
|
51
54
|
export const loadPush = async () => (await import('../../commands/push.js')).registerPushCommand;
|
|
@@ -110,6 +113,7 @@ export const COMMAND_LOADERS = {
|
|
|
110
113
|
trash: [loadTrash],
|
|
111
114
|
restore: [loadRestore],
|
|
112
115
|
doctor: [loadDoctor],
|
|
116
|
+
status: [loadStatus],
|
|
113
117
|
profiles: [loadProfiles],
|
|
114
118
|
secrets: [loadSecrets],
|
|
115
119
|
wallet: [loadWallet],
|
|
@@ -129,6 +133,8 @@ export const COMMAND_LOADERS = {
|
|
|
129
133
|
browser: [loadBrowser],
|
|
130
134
|
computer: [loadComputer],
|
|
131
135
|
hosts: [loadHosts],
|
|
136
|
+
logs: [loadLogs],
|
|
137
|
+
events: [loadEvents],
|
|
132
138
|
ssh: [loadSsh],
|
|
133
139
|
devices: [loadSsh],
|
|
134
140
|
pull: [loadPull],
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified sync-status engine — the SINGLE source of truth for "is this resource
|
|
3
|
+
* synced to this agent version?" consumed by `agents doctor`, `agents view`, the
|
|
4
|
+
* menu-bar app, and any Agency surface.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: before this module there were four different notions of
|
|
7
|
+
* "synced" living in four files:
|
|
8
|
+
* - doctor: isStale() vs .sync-manifest.json — catches SOURCE drift only.
|
|
9
|
+
* - view: git working-tree state of ~/.agents/ — a resource can show green
|
|
10
|
+
* while its installed copy is stale/deleted/corrupted (false positive).
|
|
11
|
+
* - lists: file-exists-in-home — never reports content drift at all.
|
|
12
|
+
* - menubar: read doctor --json (so it inherited doctor's source-only blind spot).
|
|
13
|
+
*
|
|
14
|
+
* The reliable signal is diffVersionResources() (src/lib/doctor-diff.ts): it reads
|
|
15
|
+
* the ACTUAL version home and compares it to the resolved sources, so it catches
|
|
16
|
+
* every drift class — source-side changes AND home-side rot (deleted / corrupted /
|
|
17
|
+
* hand-edited installed copies) AND orphans. This module wraps it once, maps its
|
|
18
|
+
* per-resource DiffStatus onto one stable enum, folds in `.system` repo freshness,
|
|
19
|
+
* and lets every surface render the same warnings instead of re-deriving them.
|
|
20
|
+
*/
|
|
21
|
+
import { AgentId } from './types.js';
|
|
22
|
+
import { type DoctorKind } from './doctor-diff.js';
|
|
23
|
+
/**
|
|
24
|
+
* One stable status per resource, unified across every surface.
|
|
25
|
+
* - `synced` — installed copy matches the resolved source (DiffStatus 'ok').
|
|
26
|
+
* - `drifted` — installed copy exists but differs from source (DiffStatus 'diff').
|
|
27
|
+
* - `missing` — source exists, nothing installed in the version home ('missing').
|
|
28
|
+
* - `orphan` — installed in the home with no source ('extra'); prune's job, not sync's.
|
|
29
|
+
*/
|
|
30
|
+
export type ResourceSyncStatus = 'synced' | 'drifted' | 'missing' | 'orphan';
|
|
31
|
+
export interface ResourceStatusRow {
|
|
32
|
+
agent: AgentId;
|
|
33
|
+
version: string;
|
|
34
|
+
kind: DoctorKind;
|
|
35
|
+
name: string;
|
|
36
|
+
status: ResourceSyncStatus;
|
|
37
|
+
/** Human-readable specifics for a drifted row (e.g. plugin version delta). */
|
|
38
|
+
detail?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AgentVersionStatus {
|
|
41
|
+
agent: AgentId;
|
|
42
|
+
version: string;
|
|
43
|
+
isDefault: boolean;
|
|
44
|
+
/** False = no .sync-manifest.json: this version was never synced (cold). */
|
|
45
|
+
everSynced: boolean;
|
|
46
|
+
counts: {
|
|
47
|
+
synced: number;
|
|
48
|
+
drifted: number;
|
|
49
|
+
missing: number;
|
|
50
|
+
orphan: number;
|
|
51
|
+
};
|
|
52
|
+
/** drifted + missing > 0 — a real reconcile is owed. Orphans do NOT set this
|
|
53
|
+
* (heal never deletes; orphan removal is `agents prune cleanup`). */
|
|
54
|
+
needsSync: boolean;
|
|
55
|
+
resources: ResourceStatusRow[];
|
|
56
|
+
}
|
|
57
|
+
export interface SystemRepoStatus {
|
|
58
|
+
dir: string;
|
|
59
|
+
/** Commits the local `.system` checkout is behind its tracking branch, as of
|
|
60
|
+
* the last background fetch (no network is performed here). 0 = up to date. */
|
|
61
|
+
behind: number;
|
|
62
|
+
ahead: number;
|
|
63
|
+
branch: string | null;
|
|
64
|
+
/** True when the dir isn't a git repo or has no upstream — behind is unknown. */
|
|
65
|
+
unknown: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface UnifiedSyncStatus {
|
|
68
|
+
system: SystemRepoStatus;
|
|
69
|
+
agents: AgentVersionStatus[];
|
|
70
|
+
totals: {
|
|
71
|
+
drifted: number;
|
|
72
|
+
missing: number;
|
|
73
|
+
orphan: number;
|
|
74
|
+
/** Versions with a manifest that are behind on content. */
|
|
75
|
+
versionsNeedingSync: number;
|
|
76
|
+
/** Versions that were never synced at all. */
|
|
77
|
+
versionsNeverSynced: number;
|
|
78
|
+
/** Distinct agent ids that own at least one version needing sync. */
|
|
79
|
+
agentsNeedingSync: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface SyncStatusOptions {
|
|
83
|
+
cwd?: string;
|
|
84
|
+
/** Restrict to specific agent ids; undefined = every supported agent. */
|
|
85
|
+
agents?: AgentId[];
|
|
86
|
+
/** Restrict to specific resource kinds; undefined = all. */
|
|
87
|
+
kinds?: DoctorKind[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read `.system` repo freshness WITHOUT touching the network. `git status`
|
|
91
|
+
* reports ahead/behind against the remote-tracking ref, which the detached
|
|
92
|
+
* auto-pull worker keeps warm via periodic `git fetch`. This is the same number
|
|
93
|
+
* the menu-bar surfaces; we read it once, here, so every surface agrees.
|
|
94
|
+
*/
|
|
95
|
+
export declare function getSystemRepoStatus(): Promise<SystemRepoStatus>;
|
|
96
|
+
/**
|
|
97
|
+
* Compute unified sync status across the fleet. Resolves against non-project
|
|
98
|
+
* layers only (`excludeProject: true`) — the GLOBAL version home is never
|
|
99
|
+
* reconciled against per-cwd `<cwd>/.agents/` resources, so counting them as
|
|
100
|
+
* "missing" there would be a false gap (matches doctor's overview semantics).
|
|
101
|
+
*/
|
|
102
|
+
export declare function computeSyncStatus(options?: SyncStatusOptions): Promise<UnifiedSyncStatus>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified sync-status engine — the SINGLE source of truth for "is this resource
|
|
3
|
+
* synced to this agent version?" consumed by `agents doctor`, `agents view`, the
|
|
4
|
+
* menu-bar app, and any Agency surface.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: before this module there were four different notions of
|
|
7
|
+
* "synced" living in four files:
|
|
8
|
+
* - doctor: isStale() vs .sync-manifest.json — catches SOURCE drift only.
|
|
9
|
+
* - view: git working-tree state of ~/.agents/ — a resource can show green
|
|
10
|
+
* while its installed copy is stale/deleted/corrupted (false positive).
|
|
11
|
+
* - lists: file-exists-in-home — never reports content drift at all.
|
|
12
|
+
* - menubar: read doctor --json (so it inherited doctor's source-only blind spot).
|
|
13
|
+
*
|
|
14
|
+
* The reliable signal is diffVersionResources() (src/lib/doctor-diff.ts): it reads
|
|
15
|
+
* the ACTUAL version home and compares it to the resolved sources, so it catches
|
|
16
|
+
* every drift class — source-side changes AND home-side rot (deleted / corrupted /
|
|
17
|
+
* hand-edited installed copies) AND orphans. This module wraps it once, maps its
|
|
18
|
+
* per-resource DiffStatus onto one stable enum, folds in `.system` repo freshness,
|
|
19
|
+
* and lets every surface render the same warnings instead of re-deriving them.
|
|
20
|
+
*/
|
|
21
|
+
import simpleGit from 'simple-git';
|
|
22
|
+
import { ALL_AGENT_IDS } from './agents.js';
|
|
23
|
+
import { diffVersionResources, } from './doctor-diff.js';
|
|
24
|
+
import { listInstalledVersions, getGlobalDefault } from './versions.js';
|
|
25
|
+
import { loadManifest } from './staleness/index.js';
|
|
26
|
+
import { getSystemAgentsDir } from './state.js';
|
|
27
|
+
import { isGitRepo } from './git.js';
|
|
28
|
+
const STATUS_MAP = {
|
|
29
|
+
ok: 'synced',
|
|
30
|
+
diff: 'drifted',
|
|
31
|
+
missing: 'missing',
|
|
32
|
+
extra: 'orphan',
|
|
33
|
+
};
|
|
34
|
+
function rowsFromReport(agent, version, report) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const list of Object.values(report.kinds)) {
|
|
37
|
+
for (const r of list) {
|
|
38
|
+
out.push({
|
|
39
|
+
agent,
|
|
40
|
+
version,
|
|
41
|
+
kind: r.kind,
|
|
42
|
+
name: r.name,
|
|
43
|
+
status: STATUS_MAP[r.status],
|
|
44
|
+
...(r.detail ? { detail: r.detail } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read `.system` repo freshness WITHOUT touching the network. `git status`
|
|
52
|
+
* reports ahead/behind against the remote-tracking ref, which the detached
|
|
53
|
+
* auto-pull worker keeps warm via periodic `git fetch`. This is the same number
|
|
54
|
+
* the menu-bar surfaces; we read it once, here, so every surface agrees.
|
|
55
|
+
*/
|
|
56
|
+
export async function getSystemRepoStatus() {
|
|
57
|
+
const dir = getSystemAgentsDir();
|
|
58
|
+
const base = { dir, behind: 0, ahead: 0, branch: null, unknown: true };
|
|
59
|
+
if (!isGitRepo(dir))
|
|
60
|
+
return base;
|
|
61
|
+
try {
|
|
62
|
+
const status = await simpleGit(dir).status();
|
|
63
|
+
return {
|
|
64
|
+
dir,
|
|
65
|
+
behind: status.behind ?? 0,
|
|
66
|
+
ahead: status.ahead ?? 0,
|
|
67
|
+
branch: status.tracking ?? status.current ?? null,
|
|
68
|
+
// Without a tracking branch there's no upstream to compare against.
|
|
69
|
+
unknown: !status.tracking,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return base;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compute unified sync status across the fleet. Resolves against non-project
|
|
78
|
+
* layers only (`excludeProject: true`) — the GLOBAL version home is never
|
|
79
|
+
* reconciled against per-cwd `<cwd>/.agents/` resources, so counting them as
|
|
80
|
+
* "missing" there would be a false gap (matches doctor's overview semantics).
|
|
81
|
+
*/
|
|
82
|
+
export async function computeSyncStatus(options = {}) {
|
|
83
|
+
const cwd = options.cwd ?? process.cwd();
|
|
84
|
+
const agentIds = options.agents ?? ALL_AGENT_IDS;
|
|
85
|
+
const agents = [];
|
|
86
|
+
for (const agent of agentIds) {
|
|
87
|
+
const def = getGlobalDefault(agent);
|
|
88
|
+
for (const version of listInstalledVersions(agent)) {
|
|
89
|
+
const report = diffVersionResources(agent, version, {
|
|
90
|
+
cwd,
|
|
91
|
+
excludeProject: true,
|
|
92
|
+
...(options.kinds ? { kinds: options.kinds } : {}),
|
|
93
|
+
});
|
|
94
|
+
const resources = rowsFromReport(agent, version, report);
|
|
95
|
+
const counts = { synced: 0, drifted: 0, missing: 0, orphan: 0 };
|
|
96
|
+
for (const r of resources)
|
|
97
|
+
counts[r.status]++;
|
|
98
|
+
agents.push({
|
|
99
|
+
agent,
|
|
100
|
+
version,
|
|
101
|
+
isDefault: version === def,
|
|
102
|
+
everSynced: loadManifest(agent, version) !== null,
|
|
103
|
+
counts,
|
|
104
|
+
needsSync: counts.drifted + counts.missing > 0,
|
|
105
|
+
resources,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const system = await getSystemRepoStatus();
|
|
110
|
+
const agentsNeedingSync = new Set();
|
|
111
|
+
let drifted = 0, missing = 0, orphan = 0, versionsNeedingSync = 0, versionsNeverSynced = 0;
|
|
112
|
+
for (const v of agents) {
|
|
113
|
+
drifted += v.counts.drifted;
|
|
114
|
+
missing += v.counts.missing;
|
|
115
|
+
orphan += v.counts.orphan;
|
|
116
|
+
if (!v.everSynced)
|
|
117
|
+
versionsNeverSynced++;
|
|
118
|
+
if (v.needsSync) {
|
|
119
|
+
versionsNeedingSync++;
|
|
120
|
+
agentsNeedingSync.add(v.agent);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
system,
|
|
125
|
+
agents,
|
|
126
|
+
totals: {
|
|
127
|
+
drifted,
|
|
128
|
+
missing,
|
|
129
|
+
orphan,
|
|
130
|
+
versionsNeedingSync,
|
|
131
|
+
versionsNeverSynced,
|
|
132
|
+
agentsNeedingSync: agentsNeedingSync.size,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -68,6 +68,30 @@ export declare function checkAllClis(): Record<string, {
|
|
|
68
68
|
path: string | null;
|
|
69
69
|
error: string | null;
|
|
70
70
|
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Advisory sign-in probe for a teammate's agent. Reads the account-global login
|
|
73
|
+
* (no `home` → active config) via `getAccountInfo`. Deliberately best-effort:
|
|
74
|
+
* sign-in detection is UNRELIABLE for opaque-credential agents (Kimi/Antigravity
|
|
75
|
+
* store an OAuth/JWT with no email claim) and for keychain-probed agents, so a
|
|
76
|
+
* `false` here is often a false negative. Callers must WARN and continue — never
|
|
77
|
+
* block a team on this result. Never throws (returns false on any error).
|
|
78
|
+
*/
|
|
79
|
+
export declare function checkCliSignedIn(agentType: AgentType): Promise<boolean>;
|
|
80
|
+
/** Advisory sign-in status for a `teams doctor` row. */
|
|
81
|
+
export interface SignInAdvisory {
|
|
82
|
+
/** true / false from the probe, or null when the agent isn't installed. */
|
|
83
|
+
signedIn: boolean | null;
|
|
84
|
+
/** Whether the agent is currently a running teammate. */
|
|
85
|
+
running: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the advisory sign-in status shown by `teams doctor`. An agent that is
|
|
89
|
+
* currently RUNNING in a team is live proof it works, so it overrides a
|
|
90
|
+
* (frequently false-negative) sign-in probe — doctor must never report a
|
|
91
|
+
* working agent as logged out. Not installed → `signedIn: null` (nothing to
|
|
92
|
+
* probe). Never flips the authoritative installed/ready column.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolveSignInAdvisory(installed: boolean, running: boolean, probeSignedIn: boolean): SignInAdvisory;
|
|
71
95
|
/** Resolve and cache the base directory where teammate process data is stored. */
|
|
72
96
|
export declare function getAgentsDir(): Promise<string>;
|
|
73
97
|
/**
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -19,7 +19,7 @@ import { normalizeEvents } from './parsers.js';
|
|
|
19
19
|
import { debug } from './debug.js';
|
|
20
20
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from '../gemini-settings.js';
|
|
21
21
|
import { getAgentsDir as getSystemAgentsDir, getShimsDir } from '../state.js';
|
|
22
|
-
import { AGENTS } from '../agents.js';
|
|
22
|
+
import { AGENTS, getAccountInfo } from '../agents.js';
|
|
23
23
|
import { sanitizeProcessEnv } from '../secrets/bundles.js';
|
|
24
24
|
let lastMemoryWarnAt = 0;
|
|
25
25
|
// On macOS, os.freemem() returns only the truly-free pool and ignores the
|
|
@@ -353,6 +353,35 @@ export function checkAllClis() {
|
|
|
353
353
|
}
|
|
354
354
|
return results;
|
|
355
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Advisory sign-in probe for a teammate's agent. Reads the account-global login
|
|
358
|
+
* (no `home` → active config) via `getAccountInfo`. Deliberately best-effort:
|
|
359
|
+
* sign-in detection is UNRELIABLE for opaque-credential agents (Kimi/Antigravity
|
|
360
|
+
* store an OAuth/JWT with no email claim) and for keychain-probed agents, so a
|
|
361
|
+
* `false` here is often a false negative. Callers must WARN and continue — never
|
|
362
|
+
* block a team on this result. Never throws (returns false on any error).
|
|
363
|
+
*/
|
|
364
|
+
export async function checkCliSignedIn(agentType) {
|
|
365
|
+
try {
|
|
366
|
+
const info = await getAccountInfo(agentType);
|
|
367
|
+
return info.signedIn;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Resolve the advisory sign-in status shown by `teams doctor`. An agent that is
|
|
375
|
+
* currently RUNNING in a team is live proof it works, so it overrides a
|
|
376
|
+
* (frequently false-negative) sign-in probe — doctor must never report a
|
|
377
|
+
* working agent as logged out. Not installed → `signedIn: null` (nothing to
|
|
378
|
+
* probe). Never flips the authoritative installed/ready column.
|
|
379
|
+
*/
|
|
380
|
+
export function resolveSignInAdvisory(installed, running, probeSignedIn) {
|
|
381
|
+
if (!installed)
|
|
382
|
+
return { signedIn: null, running: false };
|
|
383
|
+
return { signedIn: running ? true : probeSignedIn, running };
|
|
384
|
+
}
|
|
356
385
|
let AGENTS_DIR = null;
|
|
357
386
|
/** Resolve and cache the base directory where teammate process data is stored. */
|
|
358
387
|
export async function getAgentsDir() {
|
|
@@ -13,6 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { randomBytes } from 'crypto';
|
|
14
14
|
import lockfile from 'proper-lockfile';
|
|
15
15
|
import { getTeamsRegistryPath } from '../state.js';
|
|
16
|
+
import { emit } from '../events.js';
|
|
16
17
|
async function registryPath() {
|
|
17
18
|
return getTeamsRegistryPath();
|
|
18
19
|
}
|
|
@@ -98,39 +99,50 @@ export async function createTeam(name, options) {
|
|
|
98
99
|
throw new Error('Cannot use both --enable-worktrees and --use-worktree. Pick one.');
|
|
99
100
|
}
|
|
100
101
|
const p = await registryPath();
|
|
101
|
-
|
|
102
|
+
const meta = await withRegistryLock(p, async () => {
|
|
102
103
|
const reg = await loadTeams();
|
|
103
104
|
if (reg[name]) {
|
|
104
105
|
throw new Error(`Team '${name}' already exists`);
|
|
105
106
|
}
|
|
106
|
-
const
|
|
107
|
+
const m = {
|
|
107
108
|
created_at: new Date().toISOString(),
|
|
108
109
|
...(options?.description ? { description: options.description } : {}),
|
|
109
110
|
...(options?.enableWorktrees ? { enable_worktrees: true } : {}),
|
|
110
111
|
...(options?.useWorktree ? { use_worktree: options.useWorktree } : {}),
|
|
111
112
|
};
|
|
112
|
-
reg[name] =
|
|
113
|
+
reg[name] = m;
|
|
113
114
|
await saveTeams(reg);
|
|
114
|
-
return
|
|
115
|
+
return m;
|
|
115
116
|
});
|
|
117
|
+
// Audit the lifecycle boundary, not the CLI shell — captures every creation
|
|
118
|
+
// path (create + ensure) with team metadata the generic command log lacks.
|
|
119
|
+
emit('teams.create', { module: 'teams', team: name, worktrees: Boolean(options?.enableWorktrees || options?.useWorktree) });
|
|
120
|
+
return meta;
|
|
116
121
|
}
|
|
117
122
|
/** Return existing team metadata or create a new team if it does not exist. */
|
|
118
123
|
export async function ensureTeam(name) {
|
|
119
124
|
const p = await registryPath();
|
|
120
|
-
|
|
125
|
+
let created = false;
|
|
126
|
+
const meta = await withRegistryLock(p, async () => {
|
|
121
127
|
const reg = await loadTeams();
|
|
122
128
|
if (reg[name])
|
|
123
129
|
return reg[name];
|
|
124
|
-
const
|
|
125
|
-
reg[name] =
|
|
130
|
+
const m = { created_at: new Date().toISOString() };
|
|
131
|
+
reg[name] = m;
|
|
126
132
|
await saveTeams(reg);
|
|
127
|
-
|
|
133
|
+
created = true;
|
|
134
|
+
return m;
|
|
128
135
|
});
|
|
136
|
+
// `teams add` auto-creates the team on first teammate — audit that creation
|
|
137
|
+
// too, but only when it actually happened (not the get-existing path).
|
|
138
|
+
if (created)
|
|
139
|
+
emit('teams.create', { module: 'teams', team: name, worktrees: false });
|
|
140
|
+
return meta;
|
|
129
141
|
}
|
|
130
142
|
/** Remove a team from the registry. Returns false if the team did not exist. */
|
|
131
143
|
export async function removeTeam(name) {
|
|
132
144
|
const p = await registryPath();
|
|
133
|
-
|
|
145
|
+
const existed = await withRegistryLock(p, async () => {
|
|
134
146
|
const reg = await loadTeams();
|
|
135
147
|
if (!reg[name])
|
|
136
148
|
return false;
|
|
@@ -138,6 +150,10 @@ export async function removeTeam(name) {
|
|
|
138
150
|
await saveTeams(reg);
|
|
139
151
|
return true;
|
|
140
152
|
});
|
|
153
|
+
// "Disband" — only fires when a real team was removed, not a no-op.
|
|
154
|
+
if (existed)
|
|
155
|
+
emit('teams.disband', { module: 'teams', team: name });
|
|
156
|
+
return existed;
|
|
141
157
|
}
|
|
142
158
|
/** Check whether a team with the given name exists in the registry. */
|
|
143
159
|
export async function teamExists(name) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TerminalBackend, SplitDirection } from '../types.js';
|
|
2
|
+
/** AppleScript that opens a Ghostty tab (a window if none is open). */
|
|
3
|
+
export declare function ghosttyTabScript(cwd: string, command: string[]): string;
|
|
4
|
+
/** AppleScript that splits the current Ghostty surface (a window if none is open). */
|
|
5
|
+
export declare function ghosttySplitScript(cwd: string, command: string[], direction: SplitDirection): string;
|
|
6
|
+
export declare const ghosttyBackend: TerminalBackend;
|