@phnx-labs/agents-cli 1.20.49 → 1.20.51
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 +31 -0
- package/README.md +3 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +218 -97
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +43 -18
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- 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/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +25 -2
- package/dist/lib/shims.js +73 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +7 -26
- package/dist/lib/versions.js +44 -146
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/dist/lib/versions.js
CHANGED
|
@@ -21,7 +21,7 @@ import { exec, execFile } from 'child_process';
|
|
|
21
21
|
import { promisify } from 'util';
|
|
22
22
|
import chalk from 'chalk';
|
|
23
23
|
import { checkbox, select } from '@inquirer/prompts';
|
|
24
|
-
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta,
|
|
24
|
+
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getSkillsDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset, getHomeDir } from './state.js';
|
|
25
25
|
import { defaultPatterns, expandPatterns } from './resource-patterns.js';
|
|
26
26
|
import { listResources } from './resources.js';
|
|
27
27
|
// VERSION_RE + compareVersions are owned by the agent-spec engine primitives
|
|
@@ -40,7 +40,7 @@ import { discoverPlugins } from './plugins.js';
|
|
|
40
40
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
|
|
41
41
|
import { emit } from './events.js';
|
|
42
42
|
import { safeJoin } from './paths.js';
|
|
43
|
-
import {
|
|
43
|
+
import { readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
44
44
|
import { getWriter, getDetector } from './staleness/registry.js';
|
|
45
45
|
/** Promisified exec for running shell commands. */
|
|
46
46
|
const execAsync = promisify(exec);
|
|
@@ -1226,6 +1226,40 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1226
1226
|
/* non-fatal; the install itself succeeded */
|
|
1227
1227
|
}
|
|
1228
1228
|
}
|
|
1229
|
+
// The `npm install` above ran with `--ignore-scripts` — the right posture for
|
|
1230
|
+
// the dependency TREE (never run arbitrary transitive postinstalls), but it
|
|
1231
|
+
// also skips the agent package's OWN postinstall, which for some agents is a
|
|
1232
|
+
// required install step. @anthropic-ai/claude-code ships a ~500-byte stub at
|
|
1233
|
+
// `bin/claude.exe` plus per-arch native binaries as optional deps; its
|
|
1234
|
+
// `postinstall` (`node install.cjs`) is what copies the correct native binary
|
|
1235
|
+
// over the stub. Skip it and every launch dies with "native binary not
|
|
1236
|
+
// installed". So run the first-party package's declared postinstall here —
|
|
1237
|
+
// scoped to that one package, never `prepare` (claude-code's `prepare` is an
|
|
1238
|
+
// unconditional `exit 1` publish guard). Same precedent as the keychain-helper
|
|
1239
|
+
// postinstall re-run after a `--ignore-scripts` upgrade (see index.ts).
|
|
1240
|
+
// Best-effort: the integrity gate below is the real backstop — a still-broken
|
|
1241
|
+
// binary (postinstall failed, or a platform with no published native dep)
|
|
1242
|
+
// fails there with the correct message rather than throwing here.
|
|
1243
|
+
if (agentConfig.npmPackage) {
|
|
1244
|
+
// Recompute from installedVersion, not the (possibly renamed) `versionDir`:
|
|
1245
|
+
// the `latest` branch above may have renamed the dir to its concrete version.
|
|
1246
|
+
const pkgRoot = path.join(getVersionDir(agent, installedVersion), 'node_modules', agentConfig.npmPackage);
|
|
1247
|
+
try {
|
|
1248
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
|
|
1249
|
+
const postinstall = pkg?.scripts?.postinstall;
|
|
1250
|
+
if (typeof postinstall === 'string' && postinstall.trim()) {
|
|
1251
|
+
onProgress?.(`Running ${agentConfig.name} postinstall...`);
|
|
1252
|
+
// The declared postinstall is a shell command string (e.g. `node
|
|
1253
|
+
// install.cjs`), so it must run through a shell on ALL platforms —
|
|
1254
|
+
// shell:true, empty args. cwd is the package root; install.cjs anchors
|
|
1255
|
+
// its paths to __dirname, so that is correct and sufficient.
|
|
1256
|
+
await execFileAsync(postinstall, [], { cwd: pkgRoot, shell: true });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
/* non-fatal; the integrity gate below catches a still-broken binary */
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1229
1263
|
// Integrity gate: confirm the install actually launches, not just that the
|
|
1230
1264
|
// JS wrapper landed. A gutted install (wrapper present, native platform
|
|
1231
1265
|
// binary missing) otherwise gets silently pinned as the default and crashes
|
|
@@ -1603,9 +1637,16 @@ export async function getInstalledVersion(agent, version) {
|
|
|
1603
1637
|
* Deliberately narrow: only the missing-file signature counts. An agent that
|
|
1604
1638
|
* merely dislikes `--version` (nonzero exit, ordinary error text) or ignores it
|
|
1605
1639
|
* (times out) must NOT match, so a healthy install is never falsely condemned.
|
|
1640
|
+
*
|
|
1641
|
+
* The trailing phrases catch a gutted install that reports its own breakage
|
|
1642
|
+
* *politely* rather than with a raw ENOENT: @anthropic-ai/claude-code's stub
|
|
1643
|
+
* (run when its postinstall never copied the native binary in) prints "native
|
|
1644
|
+
* binary not installed / postinstall did not run / … optional dependency was not
|
|
1645
|
+
* downloaded" and exits nonzero — which the generic patterns above miss. Anchored
|
|
1646
|
+
* tightly so a healthy `--version` can never emit them.
|
|
1606
1647
|
*/
|
|
1607
1648
|
export function isMissingBinarySignature(output) {
|
|
1608
|
-
return /\bENOENT\b|no such file|cannot find|command not found|is not recognized/i.test(output);
|
|
1649
|
+
return /\bENOENT\b|no such file|cannot find|command not found|is not recognized|native binary not installed|postinstall did not run|optional dependency was not downloaded/i.test(output);
|
|
1609
1650
|
}
|
|
1610
1651
|
/**
|
|
1611
1652
|
* Verify a freshly-installed agent can actually LAUNCH — not merely that its JS
|
|
@@ -1785,149 +1826,6 @@ async function getCliVersionFromPath(agent) {
|
|
|
1785
1826
|
return null;
|
|
1786
1827
|
}
|
|
1787
1828
|
}
|
|
1788
|
-
/**
|
|
1789
|
-
* Get the diff between central resources (~/.agents/) and what's synced to a version.
|
|
1790
|
-
* Uses filesystem state - no tracking needed.
|
|
1791
|
-
*/
|
|
1792
|
-
export function getResourceDiff(agent, version) {
|
|
1793
|
-
const agentConfig = AGENTS[agent];
|
|
1794
|
-
const versionHome = getVersionHomePath(agent, version);
|
|
1795
|
-
const agentDir = path.join(versionHome, agentConfigDirName(agent));
|
|
1796
|
-
const diff = {
|
|
1797
|
-
commands: { added: [], dangling: [] },
|
|
1798
|
-
skills: { added: [], dangling: [] },
|
|
1799
|
-
hooks: { added: [], dangling: [] },
|
|
1800
|
-
memory: { added: [], dangling: [] },
|
|
1801
|
-
totalAdded: 0,
|
|
1802
|
-
totalDangling: 0,
|
|
1803
|
-
};
|
|
1804
|
-
// Helper to check symlink status
|
|
1805
|
-
const getSymlinkStatus = (linkPath) => {
|
|
1806
|
-
try {
|
|
1807
|
-
const stat = fs.lstatSync(linkPath);
|
|
1808
|
-
if (!stat.isSymbolicLink())
|
|
1809
|
-
return 'none';
|
|
1810
|
-
// Check if target exists
|
|
1811
|
-
try {
|
|
1812
|
-
fs.statSync(linkPath);
|
|
1813
|
-
return 'valid';
|
|
1814
|
-
}
|
|
1815
|
-
catch {
|
|
1816
|
-
return 'dangling';
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
catch {
|
|
1820
|
-
return 'none';
|
|
1821
|
-
}
|
|
1822
|
-
};
|
|
1823
|
-
// Commands: check directory symlink (or individual files for Gemini / generated skills for newer Codex)
|
|
1824
|
-
const centralCommands = getCommandsDir();
|
|
1825
|
-
const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1826
|
-
if (shouldInstallCommandAsSkill(agent, version)) {
|
|
1827
|
-
const centralFiles = fs.existsSync(centralCommands)
|
|
1828
|
-
? fs.readdirSync(centralCommands).filter(f => f.endsWith('.md'))
|
|
1829
|
-
: [];
|
|
1830
|
-
const centralNames = new Set(centralFiles.map(f => f.replace('.md', '')));
|
|
1831
|
-
const versionNames = new Set(listCommandSkillsInVersion(agentDir));
|
|
1832
|
-
for (const file of centralFiles) {
|
|
1833
|
-
const name = file.replace('.md', '');
|
|
1834
|
-
if (!versionNames.has(name)) {
|
|
1835
|
-
diff.commands.added.push(file);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
for (const name of versionNames) {
|
|
1839
|
-
if (!centralNames.has(name)) {
|
|
1840
|
-
diff.commands.dangling.push(`${name}.md`);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
else if (agentConfig.format === 'toml') {
|
|
1845
|
-
// Gemini: compare .md files in central vs .toml files in version
|
|
1846
|
-
if (fs.existsSync(centralCommands)) {
|
|
1847
|
-
const centralFiles = fs.readdirSync(centralCommands).filter(f => f.endsWith('.md'));
|
|
1848
|
-
const versionFiles = fs.existsSync(commandsTarget)
|
|
1849
|
-
? fs.readdirSync(commandsTarget).filter(f => f.endsWith('.toml'))
|
|
1850
|
-
: [];
|
|
1851
|
-
const versionNames = new Set(versionFiles.map(f => f.replace('.toml', '')));
|
|
1852
|
-
for (const file of centralFiles) {
|
|
1853
|
-
const name = file.replace('.md', '');
|
|
1854
|
-
if (!versionNames.has(name)) {
|
|
1855
|
-
diff.commands.added.push(file);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
// Check for dangling (toml exists but no md source)
|
|
1859
|
-
const centralNames = new Set(centralFiles.map(f => f.replace('.md', '')));
|
|
1860
|
-
for (const file of versionFiles) {
|
|
1861
|
-
const name = file.replace('.toml', '');
|
|
1862
|
-
if (!centralNames.has(name)) {
|
|
1863
|
-
diff.commands.dangling.push(file);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
else {
|
|
1869
|
-
// Other agents: check directory symlink
|
|
1870
|
-
const status = getSymlinkStatus(commandsTarget);
|
|
1871
|
-
if (status === 'none' && fs.existsSync(centralCommands)) {
|
|
1872
|
-
const files = fs.readdirSync(centralCommands).filter(f => f.endsWith('.md'));
|
|
1873
|
-
diff.commands.added = files;
|
|
1874
|
-
}
|
|
1875
|
-
else if (status === 'dangling') {
|
|
1876
|
-
diff.commands.dangling = ['commands/'];
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
// Skills: check directory symlink (skip if agent natively reads ~/.agents/skills/)
|
|
1880
|
-
if (!agentConfig.nativeAgentsSkillsDir) {
|
|
1881
|
-
const centralSkills = getSkillsDir();
|
|
1882
|
-
const skillsTarget = path.join(agentDir, 'skills');
|
|
1883
|
-
const skillsStatus = getSymlinkStatus(skillsTarget);
|
|
1884
|
-
if (skillsStatus === 'none' && fs.existsSync(centralSkills)) {
|
|
1885
|
-
const dirs = fs.readdirSync(centralSkills).filter(f => {
|
|
1886
|
-
const stat = fs.statSync(path.join(centralSkills, f));
|
|
1887
|
-
return stat.isDirectory() && !f.startsWith('.');
|
|
1888
|
-
});
|
|
1889
|
-
diff.skills.added = dirs;
|
|
1890
|
-
}
|
|
1891
|
-
else if (skillsStatus === 'dangling') {
|
|
1892
|
-
diff.skills.dangling = ['skills/'];
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
// Hooks: check directory symlink (if agent supports hooks)
|
|
1896
|
-
if (agentConfig.supportsHooks) {
|
|
1897
|
-
const centralHooks = getHooksDir();
|
|
1898
|
-
const hooksTarget = path.join(agentDir, 'hooks');
|
|
1899
|
-
const hooksStatus = getSymlinkStatus(hooksTarget);
|
|
1900
|
-
if (hooksStatus === 'none' && fs.existsSync(centralHooks)) {
|
|
1901
|
-
const files = fs.readdirSync(centralHooks).filter(f => !f.startsWith('.'));
|
|
1902
|
-
diff.hooks.added = files;
|
|
1903
|
-
}
|
|
1904
|
-
else if (hooksStatus === 'dangling') {
|
|
1905
|
-
diff.hooks.dangling = ['hooks/'];
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
// Rules: check individual file symlinks
|
|
1909
|
-
const systemRulesDir = getResolvedRulesDir();
|
|
1910
|
-
if (fs.existsSync(systemRulesDir)) {
|
|
1911
|
-
const ruleFiles = fs.readdirSync(systemRulesDir).filter(f => f.endsWith('.md') && f !== RULES_DOC_FILENAME);
|
|
1912
|
-
for (const file of ruleFiles) {
|
|
1913
|
-
const targetName = file === 'AGENTS.md' ? agentConfig.instructionsFile : file;
|
|
1914
|
-
const targetPath = path.join(agentDir, targetName);
|
|
1915
|
-
const status = getSymlinkStatus(targetPath);
|
|
1916
|
-
if (status === 'none') {
|
|
1917
|
-
diff.memory.added.push(file);
|
|
1918
|
-
}
|
|
1919
|
-
else if (status === 'dangling') {
|
|
1920
|
-
diff.memory.dangling.push(targetName);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
// Calculate totals
|
|
1925
|
-
diff.totalAdded = diff.commands.added.length + diff.skills.added.length +
|
|
1926
|
-
diff.hooks.added.length + diff.memory.added.length;
|
|
1927
|
-
diff.totalDangling = diff.commands.dangling.length + diff.skills.dangling.length +
|
|
1928
|
-
diff.hooks.dangling.length + diff.memory.dangling.length;
|
|
1929
|
-
return diff;
|
|
1930
|
-
}
|
|
1931
1829
|
/**
|
|
1932
1830
|
* Enumerate the DotAgent repo names that resources can be scoped to:
|
|
1933
1831
|
* the fixed `project` / `user` / `system` layers plus every enabled extra
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a just-finished run should be checked for stranded work: only
|
|
3
|
+
* writable modes can leave commits (plan is read-only), and only non-interactive
|
|
4
|
+
* runs need the warning (an interactive user sees their own shell). Centralizes
|
|
5
|
+
* the gate so every exit path in the run command applies it identically.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shouldWarnUnpushed(mode: string, interactive: boolean): boolean;
|
|
8
|
+
export interface UnpushedState {
|
|
9
|
+
/** cwd is inside a git work tree. */
|
|
10
|
+
isRepo: boolean;
|
|
11
|
+
/** current branch, or null when detached / not a repo. */
|
|
12
|
+
branch: string | null;
|
|
13
|
+
/** the branch has an upstream tracking ref configured. */
|
|
14
|
+
hasUpstream: boolean;
|
|
15
|
+
/** commits reachable from HEAD but not from any remote-tracking ref. */
|
|
16
|
+
unpushed: {
|
|
17
|
+
sha: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Inspect `cwd` for commits on the current branch that have not reached any
|
|
23
|
+
* remote. Uses `git log --not --remotes` so it is correct even when the branch
|
|
24
|
+
* has no upstream set: commits already present on some `origin/*` ref are NOT
|
|
25
|
+
* reported (no false positive), and a never-pushed branch reports all its
|
|
26
|
+
* commits. Returns an inert result — never throws — for a non-repo cwd, a
|
|
27
|
+
* detached HEAD, or a repo with no remotes (nothing to push to).
|
|
28
|
+
*/
|
|
29
|
+
export declare function getUnpushedState(cwd: string): Promise<UnpushedState>;
|
|
30
|
+
/**
|
|
31
|
+
* Render the warning for an unpushed state, or null when there is nothing to
|
|
32
|
+
* warn about. Split out from the printer so it is directly testable.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatUnpushedWarning(state: UnpushedState, cwd: string): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* If the just-finished run left committed-but-unpushed work in `cwd`, print a
|
|
37
|
+
* loud stderr warning with the exact push / PR commands. Non-fatal by contract:
|
|
38
|
+
* any failure is swallowed so it can never break a run's exit path.
|
|
39
|
+
*/
|
|
40
|
+
export declare function warnUnpushedWork(cwd: string): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-run guard against silently stranded work.
|
|
3
|
+
*
|
|
4
|
+
* A headless `agents run` in a writable mode can end with the agent having
|
|
5
|
+
* committed on a branch but never pushed it — the CLI's exit path does no git
|
|
6
|
+
* work, so those commits sit invisible in a worktree until someone audits the
|
|
7
|
+
* box. This module detects that state and prints a loud stderr warning with the
|
|
8
|
+
* exact push / PR commands. It is advisory only: it never pushes, never mutates
|
|
9
|
+
* the repo, and never throws (a non-repo cwd or any git error yields an inert
|
|
10
|
+
* result), so it can sit on the run's exit path without ever breaking it.
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
// Unit-separator byte (0x1f) as the git-log field delimiter: it cannot occur in
|
|
17
|
+
// a commit subject, so splitting on it never truncates a subject with spaces.
|
|
18
|
+
const SEP = '\x1f';
|
|
19
|
+
/**
|
|
20
|
+
* Whether a just-finished run should be checked for stranded work: only
|
|
21
|
+
* writable modes can leave commits (plan is read-only), and only non-interactive
|
|
22
|
+
* runs need the warning (an interactive user sees their own shell). Centralizes
|
|
23
|
+
* the gate so every exit path in the run command applies it identically.
|
|
24
|
+
*/
|
|
25
|
+
export function shouldWarnUnpushed(mode, interactive) {
|
|
26
|
+
return mode !== 'plan' && !interactive;
|
|
27
|
+
}
|
|
28
|
+
const INERT = { isRepo: false, branch: null, hasUpstream: false, unpushed: [] };
|
|
29
|
+
async function git(args, cwd) {
|
|
30
|
+
// These are all local, non-prompting reads, but the call sits on a run's exit
|
|
31
|
+
// path — a hard timeout guarantees a wedged git can never delay exit unbounded.
|
|
32
|
+
const { stdout } = await execFileAsync('git', args, { cwd, timeout: 5000 });
|
|
33
|
+
return stdout.trim();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Inspect `cwd` for commits on the current branch that have not reached any
|
|
37
|
+
* remote. Uses `git log --not --remotes` so it is correct even when the branch
|
|
38
|
+
* has no upstream set: commits already present on some `origin/*` ref are NOT
|
|
39
|
+
* reported (no false positive), and a never-pushed branch reports all its
|
|
40
|
+
* commits. Returns an inert result — never throws — for a non-repo cwd, a
|
|
41
|
+
* detached HEAD, or a repo with no remotes (nothing to push to).
|
|
42
|
+
*/
|
|
43
|
+
export async function getUnpushedState(cwd) {
|
|
44
|
+
let branch;
|
|
45
|
+
try {
|
|
46
|
+
branch = await git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return INERT; // not a git repo
|
|
50
|
+
}
|
|
51
|
+
// Detached HEAD: agents commit on branches; nothing nameable to push.
|
|
52
|
+
if (!branch || branch === 'HEAD') {
|
|
53
|
+
return { isRepo: true, branch: null, hasUpstream: false, unpushed: [] };
|
|
54
|
+
}
|
|
55
|
+
// No remote configured -> `--remotes` matches nothing and would report the
|
|
56
|
+
// entire history as "unpushed". There's nowhere to push, so stay silent.
|
|
57
|
+
let hasRemote = false;
|
|
58
|
+
try {
|
|
59
|
+
hasRemote = (await git(['remote'], cwd)).length > 0;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
hasRemote = false;
|
|
63
|
+
}
|
|
64
|
+
if (!hasRemote) {
|
|
65
|
+
return { isRepo: true, branch, hasUpstream: false, unpushed: [] };
|
|
66
|
+
}
|
|
67
|
+
let unpushed = [];
|
|
68
|
+
try {
|
|
69
|
+
// HEAD must precede `--not`: everything after `--not` is negated, so
|
|
70
|
+
// `--not --remotes HEAD` would negate HEAD too and always return empty.
|
|
71
|
+
const out = await git(['log', 'HEAD', '--not', '--remotes', `--pretty=format:%h${SEP}%s`], cwd);
|
|
72
|
+
unpushed = out
|
|
73
|
+
? out.split('\n').map((line) => {
|
|
74
|
+
const idx = line.indexOf(SEP);
|
|
75
|
+
return idx === -1
|
|
76
|
+
? { sha: line, subject: '' }
|
|
77
|
+
: { sha: line.slice(0, idx), subject: line.slice(idx + 1) };
|
|
78
|
+
})
|
|
79
|
+
: [];
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { isRepo: true, branch, hasUpstream: false, unpushed: [] };
|
|
83
|
+
}
|
|
84
|
+
let hasUpstream = false;
|
|
85
|
+
try {
|
|
86
|
+
await git(['rev-parse', '--abbrev-ref', '@{u}'], cwd);
|
|
87
|
+
hasUpstream = true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
hasUpstream = false;
|
|
91
|
+
}
|
|
92
|
+
return { isRepo: true, branch, hasUpstream, unpushed };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Render the warning for an unpushed state, or null when there is nothing to
|
|
96
|
+
* warn about. Split out from the printer so it is directly testable.
|
|
97
|
+
*/
|
|
98
|
+
export function formatUnpushedWarning(state, cwd) {
|
|
99
|
+
if (!state.isRepo || !state.branch || state.unpushed.length === 0)
|
|
100
|
+
return null;
|
|
101
|
+
const n = state.unpushed.length;
|
|
102
|
+
const lines = [`\n⚠ agent left ${n} commit${n === 1 ? '' : 's'} on '${state.branch}' not pushed to any remote:`];
|
|
103
|
+
for (const c of state.unpushed.slice(0, 5))
|
|
104
|
+
lines.push(` ${c.sha} ${c.subject}`);
|
|
105
|
+
if (n > 5)
|
|
106
|
+
lines.push(` … and ${n - 5} more`);
|
|
107
|
+
const pushCmd = state.hasUpstream
|
|
108
|
+
? `git -C "${cwd}" push`
|
|
109
|
+
: `git -C "${cwd}" push -u origin ${state.branch}`;
|
|
110
|
+
lines.push(` push them: ${pushCmd}`);
|
|
111
|
+
lines.push(` open a PR: gh pr create --head ${state.branch}`);
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* If the just-finished run left committed-but-unpushed work in `cwd`, print a
|
|
116
|
+
* loud stderr warning with the exact push / PR commands. Non-fatal by contract:
|
|
117
|
+
* any failure is swallowed so it can never break a run's exit path.
|
|
118
|
+
*/
|
|
119
|
+
export async function warnUnpushedWork(cwd) {
|
|
120
|
+
try {
|
|
121
|
+
const warning = formatUnpushedWarning(await getUnpushedState(cwd), cwd);
|
|
122
|
+
if (warning)
|
|
123
|
+
process.stderr.write(chalk.yellow(warning) + '\n');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Advisory only — never break the run over a warning.
|
|
127
|
+
}
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.51",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -98,12 +98,14 @@
|
|
|
98
98
|
"proper-lockfile": "4.1.2",
|
|
99
99
|
"simple-git": "3.36.0",
|
|
100
100
|
"smol-toml": "1.7.0",
|
|
101
|
+
"ws": "^8.21.0",
|
|
101
102
|
"yaml": "2.9.0"
|
|
102
103
|
},
|
|
103
104
|
"devDependencies": {
|
|
104
105
|
"@types/diff": "8.0.0",
|
|
105
106
|
"@types/marked-terminal": "6.1.1",
|
|
106
107
|
"@types/node": "26.1.0",
|
|
108
|
+
"@types/ws": "^8.18.1",
|
|
107
109
|
"tsx": "4.23.0",
|
|
108
110
|
"typescript": "6.0.3",
|
|
109
111
|
"vitest": "4.1.9"
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified resource system - exports all handlers and provides a registry.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* import { handlers, getHandler } from './resources/index.js';
|
|
6
|
-
* const cmds = handlers.commands.listAll('claude');
|
|
7
|
-
*/
|
|
8
|
-
export * from './types.js';
|
|
9
|
-
export { CommandsHandler, commandsHandler, type CommandItem } from './commands.js';
|
|
10
|
-
export { HooksHandler, type HookItem } from './hooks.js';
|
|
11
|
-
export { SkillsHandler, type SkillItem } from './skills.js';
|
|
12
|
-
export { RulesHandler, type RuleItem } from './rules.js';
|
|
13
|
-
export { McpHandler, getMcpConfigPath, type McpItem } from './mcp.js';
|
|
14
|
-
export { PermissionsHandler, type PermissionItem } from './permissions.js';
|
|
15
|
-
export { SubagentsHandler, subagentsHandler, type SubagentItem } from './subagents.js';
|
|
16
|
-
export { WorkflowsHandler, type WorkflowItem } from './workflows.js';
|
|
17
|
-
import type { ResourceKind, ResourceHandler } from './types.js';
|
|
18
|
-
/** All resource handlers keyed by kind. */
|
|
19
|
-
export declare const handlers: {
|
|
20
|
-
readonly command: import("./commands.js").CommandsHandler;
|
|
21
|
-
readonly commands: import("./commands.js").CommandsHandler;
|
|
22
|
-
readonly hook: ResourceHandler<import("../types.js").ManifestHook>;
|
|
23
|
-
readonly hooks: ResourceHandler<import("../types.js").ManifestHook>;
|
|
24
|
-
readonly skill: ResourceHandler<import("./skills.js").SkillItem>;
|
|
25
|
-
readonly skills: ResourceHandler<import("./skills.js").SkillItem>;
|
|
26
|
-
readonly rule: ResourceHandler<import("./rules.js").RuleItem>;
|
|
27
|
-
readonly rules: ResourceHandler<import("./rules.js").RuleItem>;
|
|
28
|
-
readonly mcp: ResourceHandler<import("./mcp.js").McpItem>;
|
|
29
|
-
readonly permission: ResourceHandler<import("../types.js").PermissionSet>;
|
|
30
|
-
readonly permissions: ResourceHandler<import("../types.js").PermissionSet>;
|
|
31
|
-
readonly subagent: import("./subagents.js").SubagentsHandler;
|
|
32
|
-
readonly subagents: import("./subagents.js").SubagentsHandler;
|
|
33
|
-
readonly workflow: {
|
|
34
|
-
readonly kind: "workflow";
|
|
35
|
-
listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
|
|
36
|
-
resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
|
|
37
|
-
sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
|
|
38
|
-
format(_agent: import("./types.js").AgentId): "md";
|
|
39
|
-
targetDir(_agent: import("./types.js").AgentId): string;
|
|
40
|
-
};
|
|
41
|
-
readonly workflows: {
|
|
42
|
-
readonly kind: "workflow";
|
|
43
|
-
listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
|
|
44
|
-
resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
|
|
45
|
-
sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
|
|
46
|
-
format(_agent: import("./types.js").AgentId): "md";
|
|
47
|
-
targetDir(_agent: import("./types.js").AgentId): string;
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
/** Get a handler by resource kind. */
|
|
51
|
-
export declare function getHandler(kind: ResourceKind): ResourceHandler<unknown> | null;
|
|
52
|
-
/** All resource kinds. */
|
|
53
|
-
export declare const RESOURCE_KINDS: ResourceKind[];
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified resource system - exports all handlers and provides a registry.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* import { handlers, getHandler } from './resources/index.js';
|
|
6
|
-
* const cmds = handlers.commands.listAll('claude');
|
|
7
|
-
*/
|
|
8
|
-
export * from './types.js';
|
|
9
|
-
export { CommandsHandler, commandsHandler } from './commands.js';
|
|
10
|
-
export { HooksHandler } from './hooks.js';
|
|
11
|
-
export { SkillsHandler } from './skills.js';
|
|
12
|
-
export { RulesHandler } from './rules.js';
|
|
13
|
-
export { McpHandler, getMcpConfigPath } from './mcp.js';
|
|
14
|
-
export { PermissionsHandler } from './permissions.js';
|
|
15
|
-
export { SubagentsHandler, subagentsHandler } from './subagents.js';
|
|
16
|
-
export { WorkflowsHandler } from './workflows.js';
|
|
17
|
-
import { commandsHandler } from './commands.js';
|
|
18
|
-
import { HooksHandler } from './hooks.js';
|
|
19
|
-
import { SkillsHandler } from './skills.js';
|
|
20
|
-
import { RulesHandler } from './rules.js';
|
|
21
|
-
import { McpHandler } from './mcp.js';
|
|
22
|
-
import { PermissionsHandler } from './permissions.js';
|
|
23
|
-
import { subagentsHandler } from './subagents.js';
|
|
24
|
-
import { WorkflowsHandler } from './workflows.js';
|
|
25
|
-
/** All resource handlers keyed by kind. */
|
|
26
|
-
export const handlers = {
|
|
27
|
-
command: commandsHandler,
|
|
28
|
-
commands: commandsHandler,
|
|
29
|
-
hook: HooksHandler,
|
|
30
|
-
hooks: HooksHandler,
|
|
31
|
-
skill: SkillsHandler,
|
|
32
|
-
skills: SkillsHandler,
|
|
33
|
-
rule: RulesHandler,
|
|
34
|
-
rules: RulesHandler,
|
|
35
|
-
mcp: McpHandler,
|
|
36
|
-
permission: PermissionsHandler,
|
|
37
|
-
permissions: PermissionsHandler,
|
|
38
|
-
subagent: subagentsHandler,
|
|
39
|
-
subagents: subagentsHandler,
|
|
40
|
-
workflow: WorkflowsHandler,
|
|
41
|
-
workflows: WorkflowsHandler,
|
|
42
|
-
};
|
|
43
|
-
/** Get a handler by resource kind. */
|
|
44
|
-
export function getHandler(kind) {
|
|
45
|
-
switch (kind) {
|
|
46
|
-
case 'command':
|
|
47
|
-
return commandsHandler;
|
|
48
|
-
case 'hook':
|
|
49
|
-
return HooksHandler;
|
|
50
|
-
case 'skill':
|
|
51
|
-
return SkillsHandler;
|
|
52
|
-
case 'rule':
|
|
53
|
-
return RulesHandler;
|
|
54
|
-
case 'mcp':
|
|
55
|
-
return McpHandler;
|
|
56
|
-
case 'permission':
|
|
57
|
-
return PermissionsHandler;
|
|
58
|
-
case 'subagent':
|
|
59
|
-
return subagentsHandler;
|
|
60
|
-
case 'workflow':
|
|
61
|
-
return WorkflowsHandler;
|
|
62
|
-
default:
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** All resource kinds. */
|
|
67
|
-
export const RESOURCE_KINDS = [
|
|
68
|
-
'command',
|
|
69
|
-
'hook',
|
|
70
|
-
'skill',
|
|
71
|
-
'rule',
|
|
72
|
-
'mcp',
|
|
73
|
-
'permission',
|
|
74
|
-
'subagent',
|
|
75
|
-
'workflow',
|
|
76
|
-
];
|