@phnx-labs/agents-cli 1.20.27 → 1.20.29
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 +3 -0
- package/dist/commands/doctor.js +57 -4
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +198 -15
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +108 -29
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +8 -24
- package/dist/commands/ssh.d.ts +14 -0
- package/dist/commands/ssh.js +263 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +3 -1
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +44 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/devices/connect.d.ts +34 -0
- package/dist/lib/devices/connect.js +101 -0
- package/dist/lib/devices/registry.d.ts +78 -0
- package/dist/lib/devices/registry.js +168 -0
- package/dist/lib/devices/ssh-config.d.ts +21 -0
- package/dist/lib/devices/ssh-config.js +33 -0
- package/dist/lib/devices/tailscale.d.ts +31 -0
- package/dist/lib/devices/tailscale.js +126 -0
- package/dist/lib/doctor-diff.d.ts +12 -0
- package/dist/lib/doctor-diff.js +89 -2
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugins.d.ts +10 -0
- package/dist/lib/plugins.js +1 -1
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/secrets/remote.d.ts +67 -0
- package/dist/lib/secrets/remote.js +133 -0
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +24 -0
- package/dist/lib/session/db.js +80 -5
- package/dist/lib/session/discover.d.ts +28 -0
- package/dist/lib/session/discover.js +303 -4
- package/dist/lib/session/parse.d.ts +7 -0
- package/dist/lib/session/parse.js +110 -0
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +31 -3
- package/dist/lib/session/remote.js +121 -14
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/types.d.ts +21 -0
- package/dist/lib/versions.d.ts +6 -2
- package/dist/lib/versions.js +8 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +62 -0
package/dist/commands/repo.d.ts
CHANGED
|
@@ -11,5 +11,34 @@
|
|
|
11
11
|
* agent version homes after the user repo's (user-wins on name collisions).
|
|
12
12
|
*/
|
|
13
13
|
import type { Command } from 'commander';
|
|
14
|
+
/**
|
|
15
|
+
* These are DotAgent *resource* repos, so changes are reported at the resource
|
|
16
|
+
* level ("2 skills, 1 hook") rather than the raw-file level. A resource is the
|
|
17
|
+
* unit a user reasons about: one skill, one command, one plugin — even if it
|
|
18
|
+
* spans several files on disk.
|
|
19
|
+
*/
|
|
20
|
+
type RepoResourceKind = 'skill' | 'command' | 'plugin' | 'hook' | 'mcp' | 'subagent' | 'rule' | 'workflow' | 'routine' | 'profile' | 'permission' | 'cli' | 'config' | 'other';
|
|
21
|
+
export type ChangeAction = 'new' | 'changed' | 'removed';
|
|
22
|
+
/**
|
|
23
|
+
* Map a repo-relative path to the resource unit it belongs to. Directory-based
|
|
24
|
+
* resources (skills/foo/SKILL.md) collapse to `skills/foo` so all their files
|
|
25
|
+
* count as one unit; flat config files (agents.yaml, hooks.yaml) count alone.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resourceUnit(file: string): {
|
|
28
|
+
kind: RepoResourceKind;
|
|
29
|
+
unit: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Render a set of changed files as a resource-level summary, e.g.
|
|
33
|
+
* `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
|
|
34
|
+
* three files all changed is "1 changed skill"), grouped by action then kind,
|
|
35
|
+
* and colors each phrase green/yellow/red for new/changed/removed. Caps at
|
|
36
|
+
* `maxParts` phrases, appending `+N more` so a big diff stays scannable.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatResourceDelta(entries: {
|
|
39
|
+
action: ChangeAction;
|
|
40
|
+
file: string;
|
|
41
|
+
}[], maxParts?: number): string;
|
|
14
42
|
/** Register the `agents repo` command tree. */
|
|
15
43
|
export declare function registerRepoCommands(program: Command): void;
|
|
44
|
+
export {};
|
package/dist/commands/repo.js
CHANGED
|
@@ -68,26 +68,117 @@ function deriveAlias(source) {
|
|
|
68
68
|
// becomes ~/.agents-<alias>/ on disk (e.g. ".agents-work" -> "work").
|
|
69
69
|
return base.replace(/^\.+/, '').replace(/^agents-/, '') || 'repo';
|
|
70
70
|
}
|
|
71
|
+
/** Top-level dir -> resource kind. Directory-based resources collapse to one unit. */
|
|
72
|
+
const RESOURCE_DIRS = {
|
|
73
|
+
skills: 'skill', commands: 'command', prompts: 'command', plugins: 'plugin',
|
|
74
|
+
hooks: 'hook', mcp: 'mcp', subagents: 'subagent', rules: 'rule',
|
|
75
|
+
workflows: 'workflow', routines: 'routine', profiles: 'profile',
|
|
76
|
+
permissions: 'permission', cli: 'cli',
|
|
77
|
+
};
|
|
78
|
+
/** [singular, plural] display labels per kind. */
|
|
79
|
+
const RESOURCE_LABELS = {
|
|
80
|
+
skill: ['skill', 'skills'], command: ['command', 'commands'],
|
|
81
|
+
plugin: ['plugin', 'plugins'], hook: ['hook', 'hooks'], mcp: ['MCP', 'MCPs'],
|
|
82
|
+
subagent: ['subagent', 'subagents'], rule: ['rule', 'rules'],
|
|
83
|
+
workflow: ['workflow', 'workflows'], routine: ['routine', 'routines'],
|
|
84
|
+
profile: ['profile', 'profiles'], permission: ['permission', 'permissions'],
|
|
85
|
+
cli: ['CLI', 'CLIs'], config: ['config file', 'config files'],
|
|
86
|
+
other: ['other file', 'other files'],
|
|
87
|
+
};
|
|
88
|
+
/** Display order — the resources a user cares about most come first. */
|
|
89
|
+
const RESOURCE_ORDER = [
|
|
90
|
+
'skill', 'command', 'plugin', 'hook', 'mcp', 'subagent', 'rule',
|
|
91
|
+
'workflow', 'routine', 'profile', 'permission', 'cli', 'config', 'other',
|
|
92
|
+
];
|
|
71
93
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
94
|
+
* Map a repo-relative path to the resource unit it belongs to. Directory-based
|
|
95
|
+
* resources (skills/foo/SKILL.md) collapse to `skills/foo` so all their files
|
|
96
|
+
* count as one unit; flat config files (agents.yaml, hooks.yaml) count alone.
|
|
75
97
|
*/
|
|
76
|
-
function
|
|
98
|
+
export function resourceUnit(file) {
|
|
99
|
+
const parts = file.split('/');
|
|
100
|
+
const top = parts[0];
|
|
101
|
+
if (top === 'agents.yaml' || top === 'hooks.yaml')
|
|
102
|
+
return { kind: 'config', unit: top };
|
|
103
|
+
const kind = RESOURCE_DIRS[top];
|
|
104
|
+
if (kind)
|
|
105
|
+
return { kind, unit: parts.length > 1 ? `${top}/${parts[1]}` : file };
|
|
106
|
+
return { kind: 'other', unit: file };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Render a set of changed files as a resource-level summary, e.g.
|
|
110
|
+
* `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
|
|
111
|
+
* three files all changed is "1 changed skill"), grouped by action then kind,
|
|
112
|
+
* and colors each phrase green/yellow/red for new/changed/removed. Caps at
|
|
113
|
+
* `maxParts` phrases, appending `+N more` so a big diff stays scannable.
|
|
114
|
+
*/
|
|
115
|
+
export function formatResourceDelta(entries, maxParts = 5) {
|
|
116
|
+
// Gather every action seen across a unit's files, then collapse to one action.
|
|
117
|
+
const units = new Map();
|
|
118
|
+
for (const { action, file } of entries) {
|
|
119
|
+
const { kind, unit } = resourceUnit(file);
|
|
120
|
+
const key = `${kind} ${unit}`;
|
|
121
|
+
const cur = units.get(key) ?? { kind, actions: new Set() };
|
|
122
|
+
cur.actions.add(action);
|
|
123
|
+
units.set(key, cur);
|
|
124
|
+
}
|
|
125
|
+
const counts = new Map(); // `${action} ${kind}` -> count
|
|
126
|
+
for (const { kind, actions } of units.values()) {
|
|
127
|
+
let action;
|
|
128
|
+
if (actions.size === 1)
|
|
129
|
+
action = [...actions][0];
|
|
130
|
+
else if ([...actions].every((a) => a === 'new'))
|
|
131
|
+
action = 'new';
|
|
132
|
+
else if ([...actions].every((a) => a === 'removed'))
|
|
133
|
+
action = 'removed';
|
|
134
|
+
else
|
|
135
|
+
action = 'changed'; // mixed add+modify within one unit reads as a change
|
|
136
|
+
const key = `${action} ${kind}`;
|
|
137
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
138
|
+
}
|
|
139
|
+
const COLOR = {
|
|
140
|
+
new: chalk.green, changed: chalk.yellow, removed: chalk.red,
|
|
141
|
+
};
|
|
77
142
|
const parts = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
for (const action of ['new', 'changed', 'removed']) {
|
|
144
|
+
for (const kind of RESOURCE_ORDER) {
|
|
145
|
+
const n = counts.get(`${action} ${kind}`);
|
|
146
|
+
if (!n)
|
|
147
|
+
continue;
|
|
148
|
+
const [singular, plural] = RESOURCE_LABELS[kind];
|
|
149
|
+
parts.push(COLOR[action](`${n} ${action} ${n === 1 ? singular : plural}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (parts.length > maxParts) {
|
|
153
|
+
const shown = parts.slice(0, maxParts);
|
|
154
|
+
shown.push(chalk.gray(`+${parts.length - maxParts} more`));
|
|
155
|
+
return shown.join(', ');
|
|
156
|
+
}
|
|
157
|
+
return parts.join(', ');
|
|
158
|
+
}
|
|
159
|
+
/** Parse `git diff --name-status <range>` into resource-delta entries. */
|
|
160
|
+
async function diffResourceEntries(git, range) {
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await git.diff(['--name-status', range]);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return []; // no upstream resolvable / range invalid — caller falls back to counts
|
|
167
|
+
}
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const line of raw.split('\n')) {
|
|
170
|
+
if (!line.trim())
|
|
171
|
+
continue;
|
|
172
|
+
const cols = line.split('\t');
|
|
173
|
+
const code = cols[0] ?? '';
|
|
174
|
+
const file = cols[cols.length - 1] ?? ''; // renames put the new path last
|
|
175
|
+
if (!file)
|
|
176
|
+
continue;
|
|
177
|
+
const c = code[0];
|
|
178
|
+
const action = c === 'A' ? 'new' : c === 'D' ? 'removed' : 'changed';
|
|
179
|
+
out.push({ action, file });
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
91
182
|
}
|
|
92
183
|
/** Visible character width of a string with embedded ANSI color codes. */
|
|
93
184
|
function visibleWidth(s) {
|
|
@@ -98,46 +189,75 @@ function padVisible(s, width) {
|
|
|
98
189
|
return s + ' '.repeat(Math.max(0, width - visibleWidth(s)));
|
|
99
190
|
}
|
|
100
191
|
/**
|
|
101
|
-
* Render one row
|
|
102
|
-
*
|
|
103
|
-
* `agents repo status` alias.
|
|
192
|
+
* Render one repo's row data: branch, resource-level sync (what a pull/push would
|
|
193
|
+
* move), resource-level local edits, and the remote URL + commit. Used by
|
|
194
|
+
* `agents repo list` and the hidden `agents repo status` alias.
|
|
104
195
|
*/
|
|
105
196
|
async function renderRepoRow(t) {
|
|
106
|
-
const aliasCol = chalk.cyan(t.alias.padEnd(12));
|
|
107
197
|
if (!fs.existsSync(t.dir)) {
|
|
108
|
-
return
|
|
198
|
+
return { alias: t.alias, raw: `${chalk.red('missing')} ${chalk.gray(t.dir)}` };
|
|
109
199
|
}
|
|
110
200
|
if (!isGitRepo(t.dir)) {
|
|
111
|
-
return
|
|
201
|
+
return { alias: t.alias, raw: `${chalk.gray('local (no git remote)')} ${chalk.gray(t.dir)}` };
|
|
112
202
|
}
|
|
113
203
|
try {
|
|
114
204
|
const git = simpleGit(t.dir);
|
|
115
205
|
const status = await git.status();
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
206
|
+
// Show the local branch name; the upstream remote is already implied by URL.
|
|
207
|
+
const branch = status.current || (status.tracking ? status.tracking.replace(/^origin\//, '') : '(detached)');
|
|
208
|
+
// SYNC: what a pull brings in / a push sends out, described by resource.
|
|
209
|
+
const ahead = status.ahead ?? 0;
|
|
210
|
+
const behind = status.behind ?? 0;
|
|
211
|
+
let sync;
|
|
212
|
+
if (!status.tracking) {
|
|
213
|
+
sync = chalk.gray('no upstream');
|
|
214
|
+
}
|
|
215
|
+
else if (ahead === 0 && behind === 0) {
|
|
216
|
+
sync = chalk.green('up to date');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const pieces = [];
|
|
220
|
+
if (behind > 0) {
|
|
221
|
+
// Three-dot isolates upstream's side via the merge-base, so a diverged
|
|
222
|
+
// branch reports exactly what a pull adds (not the inverse of local commits).
|
|
223
|
+
const incoming = formatResourceDelta(await diffResourceEntries(git, 'HEAD...@{upstream}'));
|
|
224
|
+
pieces.push(`${incoming || chalk.yellow(`${behind} commit${behind > 1 ? 's' : ''}`)} ${chalk.gray('to pull')}`);
|
|
225
|
+
}
|
|
226
|
+
if (ahead > 0) {
|
|
227
|
+
const outgoing = formatResourceDelta(await diffResourceEntries(git, '@{upstream}...HEAD'));
|
|
228
|
+
pieces.push(`${outgoing || chalk.yellow(`${ahead} commit${ahead > 1 ? 's' : ''}`)} ${chalk.gray('to push')}`);
|
|
229
|
+
}
|
|
230
|
+
sync = pieces.join(chalk.gray(' · '));
|
|
231
|
+
}
|
|
232
|
+
// CHANGES: uncommitted working-tree edits, described by resource.
|
|
233
|
+
const localEntries = [
|
|
234
|
+
...status.created.map((f) => ({ action: 'new', file: f })),
|
|
235
|
+
...status.not_added.map((f) => ({ action: 'new', file: f })),
|
|
236
|
+
...status.modified.map((f) => ({ action: 'changed', file: f })),
|
|
237
|
+
...status.conflicted.map((f) => ({ action: 'changed', file: f })),
|
|
238
|
+
...status.renamed.map((r) => ({ action: 'changed', file: r.to })),
|
|
239
|
+
...status.deleted.map((f) => ({ action: 'removed', file: f })),
|
|
240
|
+
];
|
|
241
|
+
const changes = status.isClean() ? chalk.green('clean') : formatResourceDelta(localEntries);
|
|
122
242
|
const remotes = await git.getRemotes(true);
|
|
123
243
|
const origin = remotes.find((r) => r.name === 'origin');
|
|
124
244
|
const url = origin?.refs?.fetch || '';
|
|
125
245
|
const commit = (await git.log({ maxCount: 1 })).latest?.hash.slice(0, 8) || '';
|
|
126
|
-
const
|
|
246
|
+
const remote = url
|
|
127
247
|
? chalk.gray(`${url}${commit ? ` (${commit})` : ''}`)
|
|
128
248
|
: commit
|
|
129
249
|
? chalk.gray(`(${commit})`)
|
|
130
250
|
: '';
|
|
131
|
-
return
|
|
251
|
+
return { alias: t.alias, cells: [branch, sync, changes, remote] };
|
|
132
252
|
}
|
|
133
253
|
catch (err) {
|
|
134
|
-
return
|
|
254
|
+
return { alias: t.alias, raw: `${chalk.red('error')} ${err.message}` };
|
|
135
255
|
}
|
|
136
256
|
}
|
|
137
257
|
/**
|
|
138
258
|
* Shared action body for `agents repo list` and the hidden `agents repo status`
|
|
139
|
-
* alias. Prints
|
|
140
|
-
* remote URL
|
|
259
|
+
* alias. Prints an aligned table: repo, branch, resource-level sync (to pull /
|
|
260
|
+
* to push), resource-level local changes, and remote URL + short commit.
|
|
141
261
|
*/
|
|
142
262
|
async function listRepos(alias) {
|
|
143
263
|
const targets = collectRepoTargets(alias);
|
|
@@ -149,10 +269,26 @@ async function listRepos(alias) {
|
|
|
149
269
|
console.log(chalk.gray('No repos to show.'));
|
|
150
270
|
return;
|
|
151
271
|
}
|
|
272
|
+
const rows = await Promise.all(targets.map(renderRepoRow));
|
|
273
|
+
const tableRows = rows.filter((r) => r.cells);
|
|
274
|
+
// Column widths grow to fit the widest visible content (resource summaries vary
|
|
275
|
+
// a lot in length), so the table stays aligned without truncating detail.
|
|
276
|
+
const headers = ['REPO', 'BRANCH', 'SYNC', 'CHANGES'];
|
|
277
|
+
const aliasW = Math.max(headers[0].length, ...rows.map((r) => r.alias.length));
|
|
278
|
+
const branchW = Math.max(headers[1].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[0])));
|
|
279
|
+
const syncW = Math.max(headers[2].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[1])));
|
|
280
|
+
const changesW = Math.max(headers[3].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[2])));
|
|
152
281
|
console.log('');
|
|
153
|
-
console.log(` ${chalk.gray(
|
|
154
|
-
for (const
|
|
155
|
-
|
|
282
|
+
console.log(` ${chalk.gray(headers[0].padEnd(aliasW))} ${chalk.gray(headers[1].padEnd(branchW))} ${chalk.gray(headers[2].padEnd(syncW))} ${chalk.gray(headers[3].padEnd(changesW))} ${chalk.gray('REMOTE')}`);
|
|
283
|
+
for (const r of rows) {
|
|
284
|
+
const aliasCol = chalk.cyan(r.alias.padEnd(aliasW));
|
|
285
|
+
if (r.cells) {
|
|
286
|
+
const [branch, sync, changes, remote] = r.cells;
|
|
287
|
+
console.log(` ${aliasCol} ${padVisible(branch, branchW)} ${padVisible(sync, syncW)} ${padVisible(changes, changesW)} ${remote}`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log(` ${aliasCol} ${r.raw}`);
|
|
291
|
+
}
|
|
156
292
|
}
|
|
157
293
|
const userDir = getUserAgentsDir();
|
|
158
294
|
if (!isGitRepo(userDir) && fs.existsSync(userDir)) {
|
|
@@ -358,7 +494,7 @@ export function registerRepoCommands(program) {
|
|
|
358
494
|
repoCmd
|
|
359
495
|
.command('list [alias]')
|
|
360
496
|
.alias('ls')
|
|
361
|
-
.description('Show all repos
|
|
497
|
+
.description('Show all repos with resource-level sync (skills/commands/plugins to pull or push) and local changes.')
|
|
362
498
|
.action(async (alias) => {
|
|
363
499
|
await listRepos(alias);
|
|
364
500
|
});
|
|
@@ -6,13 +6,8 @@
|
|
|
6
6
|
* Keychain. Bundles are injected at run time via `agents run --secrets`.
|
|
7
7
|
*/
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* or `user@host`. The strict allowlist blocks shell metacharacters and a leading `-`
|
|
12
|
-
* so a target can't be smuggled in as an ssh argv flag.
|
|
13
|
-
*/
|
|
14
|
-
export declare const SSH_TARGET_RE: RegExp;
|
|
15
|
-
export declare function assertValidSshTarget(host: string): void;
|
|
9
|
+
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
|
+
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
16
11
|
/**
|
|
17
12
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
18
13
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
package/dist/commands/secrets.js
CHANGED
|
@@ -9,6 +9,8 @@ import { Option } from 'commander';
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { spawnSync } from 'child_process';
|
|
12
|
+
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
13
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
12
14
|
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
13
15
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
14
16
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -134,17 +136,9 @@ function readStdinSync() {
|
|
|
134
136
|
}
|
|
135
137
|
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
136
138
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* so a target can't be smuggled in as an ssh argv flag.
|
|
141
|
-
*/
|
|
142
|
-
export const SSH_TARGET_RE = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
143
|
-
export function assertValidSshTarget(host) {
|
|
144
|
-
if (!SSH_TARGET_RE.test(host)) {
|
|
145
|
-
throw new Error(`Invalid SSH target ${JSON.stringify(host)}. Expected a host alias or user@host (letters, digits, '.', '_', '-').`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
139
|
+
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
140
|
+
// re-exported here for back-compat with existing importers of these symbols.
|
|
141
|
+
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
148
142
|
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
149
143
|
function shellQuote(s) {
|
|
150
144
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -167,6 +161,47 @@ export function bundleEnvToDotenv(env) {
|
|
|
167
161
|
}
|
|
168
162
|
return lines.join('\n') + '\n';
|
|
169
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Browse `agents secrets <args>` on one or more remote hosts over SSH and print
|
|
166
|
+
* each host's stdout verbatim (lossless — no parsing). With >1 host the output
|
|
167
|
+
* is grouped under a `── <host> ──` header. `tty` forces an interactive ssh
|
|
168
|
+
* session (run sequentially) so a remote Touch-ID / passphrase prompt can
|
|
169
|
+
* surface (e.g. `view --reveal`); otherwise hosts are queried in parallel.
|
|
170
|
+
* Exits non-zero if any host fails.
|
|
171
|
+
*/
|
|
172
|
+
async function browseRemote(targets, args, tty) {
|
|
173
|
+
const multi = targets.length > 1;
|
|
174
|
+
let failures = 0;
|
|
175
|
+
const render = (name, res) => {
|
|
176
|
+
if (multi)
|
|
177
|
+
console.log(chalk.bold.cyan(`\n── ${name} ──`));
|
|
178
|
+
if (res.code === 0) {
|
|
179
|
+
if (res.stdout)
|
|
180
|
+
process.stdout.write(res.stdout.endsWith('\n') ? res.stdout : `${res.stdout}\n`);
|
|
181
|
+
if (res.stderr.trim())
|
|
182
|
+
process.stderr.write(chalk.gray(res.stderr));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
failures++;
|
|
186
|
+
const msg = (res.stderr || res.stdout || '').trim();
|
|
187
|
+
const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
|
|
188
|
+
console.error(chalk.red(`${name}: ${why}${msg ? `: ${msg}` : ''}`));
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
if (tty) {
|
|
192
|
+
for (const t of targets) {
|
|
193
|
+
const target = await resolveSshTarget(t);
|
|
194
|
+
render(t, remoteSecretsRaw(target, args, { tty: true }));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const resolved = await Promise.all(targets.map((t) => resolveSshTarget(t)));
|
|
199
|
+
const results = resolved.map((target) => remoteSecretsRaw(target, args));
|
|
200
|
+
targets.forEach((t, i) => render(t, results[i]));
|
|
201
|
+
}
|
|
202
|
+
if (failures > 0)
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
170
205
|
/** Strip ANSI escape sequences so padding can be computed on visible width. */
|
|
171
206
|
function visibleWidth(s) {
|
|
172
207
|
// eslint-disable-next-line no-control-regex
|
|
@@ -414,7 +449,7 @@ export function registerSecretsCommands(program) {
|
|
|
414
449
|
eval "$(agents secrets export prod --plaintext)"
|
|
415
450
|
|
|
416
451
|
# Push the bundle to remote machine(s) over SSH (lands as a native bundle there)
|
|
417
|
-
agents secrets export prod --
|
|
452
|
+
agents secrets export prod --host yosemite-s0 --host yosemite-s1 --force
|
|
418
453
|
|
|
419
454
|
# Run a one-off command with secrets injected
|
|
420
455
|
agents secrets exec prod -- ./deploy.sh
|
|
@@ -457,8 +492,15 @@ export function registerSecretsCommands(program) {
|
|
|
457
492
|
cmd
|
|
458
493
|
.command('list')
|
|
459
494
|
.alias('ls')
|
|
460
|
-
.description('List configured secrets bundles')
|
|
461
|
-
.
|
|
495
|
+
.description('List configured secrets bundles (use --host/--hosts to list bundles on other machines over SSH)')
|
|
496
|
+
.option('--host <target>', 'List bundles on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
497
|
+
.option('--hosts <list>', 'Comma-separated hosts to list in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
498
|
+
.action(async (opts) => {
|
|
499
|
+
const targets = parseHostsOption(opts);
|
|
500
|
+
if (targets.length > 0) {
|
|
501
|
+
await browseRemote(targets, ['list'], false);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
462
504
|
const bundles = listBundles();
|
|
463
505
|
if (bundles.length === 0) {
|
|
464
506
|
console.log(chalk.gray('No secrets bundles configured.'));
|
|
@@ -488,8 +530,28 @@ export function registerSecretsCommands(program) {
|
|
|
488
530
|
.description('Show a bundle. Keychain values are masked by default — pass --reveal to see them.')
|
|
489
531
|
.option('--reveal', 'Print keychain-backed values in the clear (TTY only unless --plaintext)')
|
|
490
532
|
.option('--plaintext', 'Allow --reveal in non-interactive shells (use with care)')
|
|
533
|
+
.option('--host <target>', 'Show a bundle on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
534
|
+
.option('--hosts <list>', 'Comma-separated hosts to show in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
491
535
|
.action(async (name, opts) => {
|
|
492
536
|
try {
|
|
537
|
+
const targets = parseHostsOption(opts);
|
|
538
|
+
if (targets.length > 0) {
|
|
539
|
+
if (!name) {
|
|
540
|
+
console.error(chalk.red('A bundle name is required when viewing a remote host (interactive pick needs a local terminal).'));
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
const args = ['view', name];
|
|
544
|
+
if (opts.reveal)
|
|
545
|
+
args.push('--reveal');
|
|
546
|
+
if (opts.plaintext)
|
|
547
|
+
args.push('--plaintext');
|
|
548
|
+
// With --reveal, force a TTY so the remote keychain prompt can surface
|
|
549
|
+
// (and the remote's "--reveal in a non-TTY needs --plaintext" gate is
|
|
550
|
+
// satisfied) — only when this side is itself interactive.
|
|
551
|
+
const tty = Boolean(opts.reveal) && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
552
|
+
await browseRemote(targets, args, tty);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
493
555
|
const resolvedName = name ?? (await pickBundleName('view'));
|
|
494
556
|
const bundle = readBundle(resolvedName);
|
|
495
557
|
const entries = describeBundle(bundle);
|
|
@@ -1017,7 +1079,7 @@ Examples:
|
|
|
1017
1079
|
const requestedBackend = parseBackendOpt(opts.backend);
|
|
1018
1080
|
// Read the bundle if it exists (inheriting its backend); otherwise
|
|
1019
1081
|
// create it with the requested backend so a single `import --backend
|
|
1020
|
-
// file` works (this is what `export --
|
|
1082
|
+
// file` works (this is what `export --host ... --remote-backend file`
|
|
1021
1083
|
// drives on the remote).
|
|
1022
1084
|
let bundle;
|
|
1023
1085
|
if (bundleExists(resolvedBundleName)) {
|
|
@@ -1094,23 +1156,23 @@ Examples:
|
|
|
1094
1156
|
});
|
|
1095
1157
|
cmd
|
|
1096
1158
|
.command('export [bundle]')
|
|
1097
|
-
.description('Resolve a bundle and print KEY=VALUE lines, push it to a 1Password vault with --to-1password, or push it to remote machine(s) over SSH with --
|
|
1159
|
+
.description('Resolve a bundle and print KEY=VALUE lines, push it to a 1Password vault with --to-1password, or push it to remote machine(s) over SSH with --host.')
|
|
1098
1160
|
.option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear (shell export mode)')
|
|
1099
1161
|
.option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
|
|
1100
1162
|
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
1101
|
-
.option('--
|
|
1102
|
-
.option('--
|
|
1103
|
-
.option('--
|
|
1104
|
-
.option('--
|
|
1163
|
+
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
1164
|
+
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file (passphrase-encrypted, headless-readable). file forwards AGENTS_SECRETS_PASSPHRASE over stdin.', 'keychain')
|
|
1165
|
+
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
1166
|
+
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
1105
1167
|
.action(async (bundleName, opts) => {
|
|
1106
1168
|
try {
|
|
1107
1169
|
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
1108
1170
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1171
|
+
// The presence of --host selects SSH push: --host is the destination
|
|
1172
|
+
// and carries the mode (no separate --to-ssh needed — it would be
|
|
1173
|
+
// strictly redundant since SSH always requires at least one host).
|
|
1174
|
+
const hosts = opts.host ?? [];
|
|
1175
|
+
if (hosts.length > 0) {
|
|
1114
1176
|
for (const h of hosts)
|
|
1115
1177
|
assertValidSshTarget(h);
|
|
1116
1178
|
const remoteBackend = parseBackendOpt(opts.remoteBackend);
|
|
@@ -1212,11 +1274,21 @@ Examples:
|
|
|
1212
1274
|
console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
|
|
1213
1275
|
return;
|
|
1214
1276
|
}
|
|
1277
|
+
if (opts.format && opts.format !== 'shell' && opts.format !== 'json') {
|
|
1278
|
+
console.error(chalk.red(`Invalid --format ${JSON.stringify(opts.format)}. Expected 'shell' or 'json'.`));
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1215
1281
|
if (!opts.plaintext) {
|
|
1216
1282
|
console.error(chalk.red('export prints secrets in the clear and requires --plaintext (works for TTY and pipes alike).'));
|
|
1217
1283
|
process.exit(1);
|
|
1218
1284
|
}
|
|
1219
1285
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `export to shell` });
|
|
1286
|
+
if (opts.format === 'json') {
|
|
1287
|
+
// Lossless, machine-readable form consumed by `remoteResolveEnv` over
|
|
1288
|
+
// SSH. Single object of KEY -> value; values verbatim (newlines, quotes).
|
|
1289
|
+
process.stdout.write(JSON.stringify(env));
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1220
1292
|
const prefix = bundleToEnvPrefix(resolvedBundleName);
|
|
1221
1293
|
for (const [k, v] of Object.entries(env)) {
|
|
1222
1294
|
const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
|
|
@@ -1234,17 +1306,24 @@ Examples:
|
|
|
1234
1306
|
});
|
|
1235
1307
|
cmd
|
|
1236
1308
|
.command('exec <bundle> [command...]')
|
|
1237
|
-
.description('Run a command with the bundle\'s secrets injected into the environment')
|
|
1309
|
+
.description('Run a command with the bundle\'s secrets injected into the environment (use --host to resolve the bundle from a remote machine, ephemerally)')
|
|
1310
|
+
.option('--host <target>', 'Resolve <bundle> on a remote host over SSH and inject it (ephemeral — never stored on this machine)')
|
|
1238
1311
|
.allowUnknownOption()
|
|
1239
|
-
.action(async (bundleName, commandParts) => {
|
|
1312
|
+
.action(async (bundleName, commandParts, execOpts) => {
|
|
1240
1313
|
try {
|
|
1241
1314
|
if (commandParts.length === 0) {
|
|
1242
1315
|
console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
|
|
1243
1316
|
process.exit(1);
|
|
1244
1317
|
}
|
|
1245
|
-
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
1246
1318
|
const [cmd, ...args] = commandParts;
|
|
1247
|
-
|
|
1319
|
+
let secretEnv;
|
|
1320
|
+
if (execOpts.host) {
|
|
1321
|
+
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
1325
|
+
secretEnv = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` }).env;
|
|
1326
|
+
}
|
|
1248
1327
|
const { spawn } = await import('child_process');
|
|
1249
1328
|
const proc = spawn(cmd, args, {
|
|
1250
1329
|
stdio: 'inherit',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { SessionMeta } from '../lib/session/types.js';
|
|
3
3
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
4
|
+
import { type PickedSession } from './sessions-picker.js';
|
|
4
5
|
/** Grouped + sorted view of active sessions for the --active renderer. */
|
|
5
6
|
export interface ActiveSessionsLayout {
|
|
6
7
|
workspaces: Array<{
|
|
@@ -28,6 +29,7 @@ export interface ActiveSessionsLayout {
|
|
|
28
29
|
* - sessions within a window/flat bucket: input order preserved
|
|
29
30
|
*/
|
|
30
31
|
export declare function groupActiveSessions(sessions: ActiveSession[]): ActiveSessionsLayout;
|
|
32
|
+
export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number): Promise<PickedSession | null>;
|
|
31
33
|
/**
|
|
32
34
|
* Build the shell command that resumes a picked session.
|
|
33
35
|
*
|
|
@@ -21,6 +21,7 @@ import { discoverSessions, countSessionsInScope, resolveSessionById, searchConte
|
|
|
21
21
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
22
22
|
import { parseSession } from '../lib/session/parse.js';
|
|
23
23
|
import { runRemoteSessions } from '../lib/session/remote.js';
|
|
24
|
+
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
24
25
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
25
26
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
26
27
|
import { colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
@@ -656,7 +657,7 @@ function formatPickerLabel(s, query) {
|
|
|
656
657
|
renderTopicCell(label, topic, query, 48, 50) +
|
|
657
658
|
chalk.gray(when));
|
|
658
659
|
}
|
|
659
|
-
async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
660
|
+
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
660
661
|
if (hiddenCount > 0) {
|
|
661
662
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
662
663
|
}
|
|
@@ -752,6 +753,7 @@ export function buildResumeCommand(session) {
|
|
|
752
753
|
case 'hermes':
|
|
753
754
|
case 'grok':
|
|
754
755
|
case 'kimi':
|
|
756
|
+
case 'droid':
|
|
755
757
|
// Grok (and some others) sessions are captured artifacts, not resumable the same way.
|
|
756
758
|
return null;
|
|
757
759
|
}
|
|
@@ -1332,7 +1334,11 @@ function findClaudeResumeTimestamp(filePath, targetTimestampMs) {
|
|
|
1332
1334
|
}
|
|
1333
1335
|
}
|
|
1334
1336
|
function isWithinProject(sessionCwd, projectRoot) {
|
|
1335
|
-
|
|
1337
|
+
// Compare separator- and case-normalized (Windows folds `\`→`/` and lowercases)
|
|
1338
|
+
// so a backslash session cwd matches a forward-slash project root and vice versa.
|
|
1339
|
+
const cwd = toComparablePath(sessionCwd);
|
|
1340
|
+
const root = toComparablePath(projectRoot);
|
|
1341
|
+
return cwd === root || cwd.startsWith(root + '/');
|
|
1336
1342
|
}
|
|
1337
1343
|
function sessionDistance(session, historyEntry) {
|
|
1338
1344
|
if (!historyEntry.timestampMs)
|
|
@@ -1360,25 +1366,3 @@ function padRight(s, width) {
|
|
|
1360
1366
|
function truncate(s, max) {
|
|
1361
1367
|
return s.length > max ? s.slice(0, max - 1) + '.' : s;
|
|
1362
1368
|
}
|
|
1363
|
-
function formatRelativeTime(isoTimestamp) {
|
|
1364
|
-
const now = Date.now();
|
|
1365
|
-
const then = new Date(isoTimestamp).getTime();
|
|
1366
|
-
if (isNaN(then))
|
|
1367
|
-
return isoTimestamp;
|
|
1368
|
-
const diffMs = now - then;
|
|
1369
|
-
const diffMin = Math.floor(diffMs / 60_000);
|
|
1370
|
-
const diffHrs = Math.floor(diffMs / 3_600_000);
|
|
1371
|
-
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
1372
|
-
if (diffMin < 1)
|
|
1373
|
-
return 'just now';
|
|
1374
|
-
if (diffMin < 60)
|
|
1375
|
-
return `${diffMin} min ago`;
|
|
1376
|
-
if (diffHrs < 24)
|
|
1377
|
-
return `${diffHrs} hour${diffHrs === 1 ? '' : 's'} ago`;
|
|
1378
|
-
if (diffDays < 7)
|
|
1379
|
-
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
|
1380
|
-
// Older: show date
|
|
1381
|
-
const d = new Date(then);
|
|
1382
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1383
|
-
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
1384
|
-
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents devices` (registry) + `agents ssh` (smart wrapper).
|
|
3
|
+
*
|
|
4
|
+
* `agents devices` keeps a registry of SSH device profiles — platform, login
|
|
5
|
+
* user, address, and auth — self-populated from `tailscale status --json`.
|
|
6
|
+
* `agents ssh <name>` then connects through one hardened path: preflight
|
|
7
|
+
* (offline → fail fast instead of a 2-minute hang), platform-aware exec
|
|
8
|
+
* (PowerShell on Windows), and password-from-bundle auth via an askpass shim.
|
|
9
|
+
* Rendering the registry to an ssh_config include also lets plain ssh / scp /
|
|
10
|
+
* rsync / `agents sessions --host` resolve the same logical names.
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from 'commander';
|
|
13
|
+
/** Register both `agents ssh` and `agents devices`. */
|
|
14
|
+
export declare function registerSshCommands(program: Command): void;
|