@phnx-labs/agents-cli 1.22.14 → 1.22.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/routines.js +53 -71
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +4 -3
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +17 -0
- package/dist/lib/projects.js +17 -1
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- 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/state.js +17 -4
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.15
|
|
4
|
+
|
|
5
|
+
- **Separate routine definitions from device activation (#2023).** Enable a routine by listing its name in `~/.agents/devices/<hostname>/agents.yaml`; built-in Watchdog setup and `watchdog on|off` now update that host-owned manifest without rewriting the routine definition. Source: `apps/cli/src/lib/routine-activation.ts`, `apps/cli/src/commands/setup-watchdog.ts`.
|
|
6
|
+
|
|
7
|
+
- **`agents devices list` no longer shows the "Leased boxes" section by default — it moves behind a new `--all` flag (RUSH-2190).** Loading the section routes through crabbox's bundle auto-detect, which scans the keychain and can raise a macOS Touch ID sheet *after* the device table has printed, hanging non-interactive callers (observed: the `.agents-system` SessionStart topology hook). The default list now renders only registered devices, which are reachable without any secrets; the load/mem/headroom columns are unchanged (the stats probe was already broker-only). `agents devices list --all` restores the section; `--no-stats` remains a hard "instant, no provider calls" opt-out even with `--all`. Source: `apps/cli/src/commands/ssh.ts` (`showLeasedBoxesSection`), `apps/cli/src/commands/ssh.test.ts`.
|
|
8
|
+
|
|
9
|
+
- **Install: `plugin:` prefix on the unified path (Phase 5 packaging).**
|
|
10
|
+
`agents install plugin:<spec>` uses the same grammar and trust gate as
|
|
11
|
+
`agents plugins install` (`name@url`, local path, `--allow-exec-surfaces`).
|
|
12
|
+
Specialized verbs still work; `agents install` is the one add path for mcp,
|
|
13
|
+
skill, plugin, and GitHub sources. Source: `apps/cli/src/commands/packages.ts`,
|
|
14
|
+
`apps/cli/src/lib/registry.ts`.
|
|
15
|
+
|
|
3
16
|
## 1.22.14
|
|
4
17
|
|
|
5
18
|
- **`agents secrets view <bundle> --reveal` now resolves a locked keychain bundle
|
package/README.md
CHANGED
|
@@ -396,7 +396,7 @@ agents watchdog --nudge # actually inject "Continue." into the stalled split
|
|
|
396
396
|
agents watchdog --watch # daemon loop: a tick every --interval
|
|
397
397
|
```
|
|
398
398
|
|
|
399
|
-
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog
|
|
399
|
+
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog on|off` enables or disables the built-in routine on this device. `agents setup watchdog` chooses devices. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
|
|
400
400
|
|
|
401
401
|
A stalled session whose tail shows a hard account limit ("You've hit your weekly limit · resets …") is **rotated in place** instead of nudged: the watchdog gates on the same healthy-account selection `agents run auto` makes (zero healthy → one skip event per cooldown window, terminal untouched), injects the harness's exit sequence, relaunches `agents run auto --interactive --session-id <uuid>` in the *same* tab, then replays the old session's resume once the new TUI is live. Default on; `agents watchdog rotate off` disables it (nudging stays on).
|
|
402
402
|
|
|
@@ -921,11 +921,11 @@ agents routines run daily-digest # Test it now, ignore the schedule
|
|
|
921
921
|
agents routines logs daily-digest # Last execution — status + report (add --full for raw stdout)
|
|
922
922
|
agents routines stats # Run count, failed, missed, avg/p50/p95 duration — per job or all
|
|
923
923
|
|
|
924
|
-
#
|
|
924
|
+
# Definitions sync to every device; activation is stored per hostname
|
|
925
925
|
agents routines add nightly-drain --schedule "0 3 * * *" --agent claude \
|
|
926
|
-
--
|
|
926
|
+
--prompt "Drain the local work queue"
|
|
927
927
|
|
|
928
|
-
agents routines devices nightly-drain --set yosemite-s0,mac-mini #
|
|
928
|
+
agents routines devices nightly-drain --set yosemite-s0,mac-mini # enable on both hosts
|
|
929
929
|
agents routines list --host yosemite-s0 # query another device
|
|
930
930
|
|
|
931
931
|
# Signed webhook trigger: Linear issue labeled "agent" fires a routine
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/cloud.js
CHANGED
|
@@ -7,8 +7,7 @@ import { resolveProvider, getAllProviders, getDefaultProviderId } from '../lib/c
|
|
|
7
7
|
import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks, listActiveTasks } from '../lib/cloud/store.js';
|
|
8
8
|
import { renderStream } from '../lib/cloud/stream.js';
|
|
9
9
|
import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
|
|
10
|
-
import { normalizeTriggerEvent, validateTrigger, writeJob, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
|
|
11
|
-
import { machineId } from '../lib/machine-id.js';
|
|
10
|
+
import { normalizeTriggerEvent, validateTrigger, writeJob, setJobEnabled, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
|
|
12
11
|
import { emit } from '../lib/events.js';
|
|
13
12
|
import { shareRuntimeEnv } from '../lib/share/config.js';
|
|
14
13
|
/** Map a supported image file extension to its wire mimeType. Rejects anything else. */
|
|
@@ -310,16 +309,15 @@ Examples:
|
|
|
310
309
|
};
|
|
311
310
|
if (repoValues[0])
|
|
312
311
|
routine.repo = repoValues[0];
|
|
313
|
-
// --host
|
|
314
|
-
//
|
|
315
|
-
// fleet of receivers can't each dispatch a duplicate).
|
|
312
|
+
// --host controls where the enabled local receiver dispatches the run.
|
|
313
|
+
// Device activation remains in this receiver's device manifest.
|
|
316
314
|
if (options.host) {
|
|
317
315
|
routine.host = options.host;
|
|
318
316
|
if (options.remoteCwd)
|
|
319
317
|
routine.remoteCwd = options.remoteCwd;
|
|
320
|
-
routine.devices = [machineId()];
|
|
321
318
|
}
|
|
322
319
|
writeJob(routine);
|
|
320
|
+
setJobEnabled(routine.name, true);
|
|
323
321
|
if (json) {
|
|
324
322
|
console.log(JSON.stringify({ ok: true, registered: routineName, trigger }, null, 2));
|
|
325
323
|
}
|
|
@@ -22,7 +22,7 @@ import { discoverWorkflowsFromRepo, installWorkflowCentrally, } from '../lib/wor
|
|
|
22
22
|
import { discoverSubagentsFromRepo, installSubagentCentrally, } from '../lib/subagents.js';
|
|
23
23
|
import { discoverPermissionsFromRepo, installPermissionSet, } from '../lib/permissions.js';
|
|
24
24
|
import { listInstalledVersions, resolveConfiguredAgentTargets, syncResourcesToVersion, } from '../lib/versions.js';
|
|
25
|
-
import { isInteractiveTerminal, isPromptCancelled, parseCommaSeparatedList, requireDestructiveArg, requireInteractiveSelection, resolveInstalledAgentTargetsAutoInstalling, } from './utils.js';
|
|
25
|
+
import { formatPath, isInteractiveTerminal, isPromptCancelled, parseCommaSeparatedList, requireDestructiveArg, requireInteractiveSelection, resolveInstalledAgentTargetsAutoInstalling, } from './utils.js';
|
|
26
26
|
import { itemPicker } from '../lib/picker.js';
|
|
27
27
|
import { registerMcpCommandToTargets, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
|
|
28
28
|
export function buildMcpPackageCommand(pkg) {
|
|
@@ -435,37 +435,30 @@ the sha256 in the index and abort on mismatch.
|
|
|
435
435
|
// ==========================================================================
|
|
436
436
|
program
|
|
437
437
|
.command('install <identifier>')
|
|
438
|
-
.description('Install a package
|
|
438
|
+
.description('Install a package: mcp:, skill:, plugin:, or GitHub (gh:user/repo) — one install path (Phase 5)')
|
|
439
439
|
.option('-a, --agents <list>', 'Targets: claude, codex@0.116.0, or cursor@default')
|
|
440
440
|
.option('--types <list>', 'When source is a repo: comma-separated resource types to install (skills,workflows,commands,hooks,permissions,subagents,mcp)')
|
|
441
441
|
.option('--names <list>', 'When source is a repo: comma-separated resource names within the selected types')
|
|
442
442
|
.option('-y, --yes', 'Auto-install any missing agent versions without prompting')
|
|
443
|
+
.option('--allow-exec-surfaces', 'With plugin: allow plugins that ship hooks/mcp/bin (same as agents plugins install)')
|
|
443
444
|
.addHelpText('after', `
|
|
444
|
-
Install
|
|
445
|
+
Install is the unified add path (Phase 5 packaging). Prefix the identifier:
|
|
446
|
+
|
|
447
|
+
mcp:<name> MCP server from a registry
|
|
448
|
+
skill:<name> skill from a registry (or gh: fallback)
|
|
449
|
+
plugin:<spec> plugin — same grammar as agents plugins install (name@url or path)
|
|
450
|
+
gh:user/repo clone a DotAgents / multi-resource repo and install selected types
|
|
445
451
|
|
|
446
452
|
Examples:
|
|
447
|
-
# Install an MCP server from a registry
|
|
448
453
|
agents install mcp:notion --agents claude
|
|
449
|
-
|
|
450
|
-
|
|
454
|
+
agents install skill:animator --agents claude,codex
|
|
455
|
+
agents install plugin:my-plugin@https://github.com/user/my-plugin.git
|
|
456
|
+
agents install plugin:~/Projects/rush-toolkit
|
|
451
457
|
agents install gh:anthropics/skills --agents codex,claude
|
|
452
|
-
|
|
453
|
-
# Install using GitHub shorthand
|
|
454
|
-
agents install gh:user/repo --agents claude@2.1.112
|
|
455
|
-
|
|
456
|
-
# Install only specific resource types from a multi-resource repo
|
|
457
458
|
agents install gh:phnx-labs/.agents-system --types skills,workflows --agents claude@all
|
|
458
459
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
# Install to all installed agents (uses defaults or prompts)
|
|
463
|
-
agents install mcp:postgres
|
|
464
|
-
|
|
465
|
-
When to use:
|
|
466
|
-
- After search: 'agents search notion' then 'agents install mcp:notion'
|
|
467
|
-
- Team setup: 'agents install gh:team/resources' to sync everyone's tooling
|
|
468
|
-
- Quick MCP add: 'agents install mcp:<name>' when you know the package name
|
|
460
|
+
Specialized verbs still work (agents plugins install, agents skills add) and
|
|
461
|
+
delegate to the same underlying installers.
|
|
469
462
|
`)
|
|
470
463
|
.action(async (identifier, options) => {
|
|
471
464
|
const spinner = ora('Resolving package...').start();
|
|
@@ -473,10 +466,85 @@ When to use:
|
|
|
473
466
|
const resolved = await resolvePackage(identifier);
|
|
474
467
|
if (!resolved) {
|
|
475
468
|
spinner.fail('Package not found');
|
|
476
|
-
console.log(chalk.gray('\nTip: Use explicit prefix (mcp:, skill:, gh:) or check the identifier.'));
|
|
469
|
+
console.log(chalk.gray('\nTip: Use explicit prefix (mcp:, skill:, plugin:, gh:) or check the identifier.'));
|
|
477
470
|
process.exit(1);
|
|
478
471
|
}
|
|
479
472
|
spinner.succeed(`Found ${resolved.type} package`);
|
|
473
|
+
if (resolved.type === 'plugin') {
|
|
474
|
+
spinner.stop();
|
|
475
|
+
const spec = resolved.pluginSpec ?? resolved.source;
|
|
476
|
+
console.log(chalk.gray(`Installing plugin from: ${spec}`));
|
|
477
|
+
const { installPlugin, getPlugin, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, checkPluginDependencies, hasPluginExecSurfaces, pluginSupportsAgent, syncPluginToVersion, pluginResourceGroups, } = await import('../lib/plugins.js');
|
|
478
|
+
const { listInstalledVersions, getGlobalDefault, getVersionHomePath, syncResourcesToVersion } = await import('../lib/versions.js');
|
|
479
|
+
const { agentLabel } = await import('../lib/agents.js');
|
|
480
|
+
let name;
|
|
481
|
+
let root;
|
|
482
|
+
try {
|
|
483
|
+
const result = await installPlugin(spec);
|
|
484
|
+
name = result.name;
|
|
485
|
+
root = result.root;
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
console.log(chalk.red(`Install failed: ${err.message}`));
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
const plugin = getPlugin(name);
|
|
492
|
+
if (!plugin) {
|
|
493
|
+
console.log(chalk.red(`Installed but could not load plugin '${name}'`));
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
const capabilities = inspectPluginCapabilities(root);
|
|
497
|
+
const allowExec = options.allowExecSurfaces === true;
|
|
498
|
+
if (hasPluginExecSurfaces(capabilities) && !allowExec) {
|
|
499
|
+
const source = parseInstallSpec(spec).source;
|
|
500
|
+
console.error(chalk.red('Install refused: plugin ships executable surfaces:'));
|
|
501
|
+
for (const label of pluginCapabilityLabels(capabilities)) {
|
|
502
|
+
console.error(` ${label}`);
|
|
503
|
+
}
|
|
504
|
+
console.error(`Re-run with --allow-exec-surfaces if you trust the source: ${source}@HEAD`);
|
|
505
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
const missingDeps = checkPluginDependencies(plugin.manifest);
|
|
509
|
+
if (missingDeps.length > 0) {
|
|
510
|
+
console.log(chalk.yellow(`Warning: missing dependencies: ${missingDeps.join(', ')}`));
|
|
511
|
+
console.log(chalk.gray('Install them with: agents install plugin:<name>@<source>'));
|
|
512
|
+
}
|
|
513
|
+
// Same sync loop as `agents plugins install` (default version per harness).
|
|
514
|
+
console.log();
|
|
515
|
+
let synced = 0;
|
|
516
|
+
for (const agentId of capableAgents('plugins')) {
|
|
517
|
+
if (!pluginSupportsAgent(plugin, agentId))
|
|
518
|
+
continue;
|
|
519
|
+
const versions = listInstalledVersions(agentId);
|
|
520
|
+
if (versions.length === 0)
|
|
521
|
+
continue;
|
|
522
|
+
const defaultVer = getGlobalDefault(agentId);
|
|
523
|
+
const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
|
|
524
|
+
for (const version of targetVersions) {
|
|
525
|
+
const didSync = allowExec
|
|
526
|
+
? syncPluginToVersion(plugin, agentId, getVersionHomePath(agentId, version), {
|
|
527
|
+
allowExecSurfaces: true,
|
|
528
|
+
}).success
|
|
529
|
+
: syncResourcesToVersion(agentId, version, { plugins: [name] }).plugins.length > 0;
|
|
530
|
+
if (didSync) {
|
|
531
|
+
console.log(chalk.green(` Synced to ${agentLabel(agentId)}@${version}`));
|
|
532
|
+
synced++;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (synced === 0) {
|
|
537
|
+
console.log(chalk.gray(' No supported agent versions installed — run "agents use <agent>@<version>" to sync.'));
|
|
538
|
+
}
|
|
539
|
+
console.log(chalk.bold(`\nInstalled ${plugin.name} v${plugin.manifest.version} to ${formatPath(root)}`));
|
|
540
|
+
const groups = pluginResourceGroups(plugin);
|
|
541
|
+
if (groups.length > 0) {
|
|
542
|
+
for (const g of groups) {
|
|
543
|
+
console.log(chalk.gray(` ${g.label}: ${g.items.slice(0, 8).join(', ')}${g.items.length > 8 ? '…' : ''}`));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
480
548
|
if (resolved.type === 'mcp') {
|
|
481
549
|
// Install MCP server
|
|
482
550
|
const entry = resolved.mcpEntry;
|
|
@@ -24,11 +24,8 @@ export declare function formatFleetSkippedNote(skipped: string[]): string;
|
|
|
24
24
|
*
|
|
25
25
|
* `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
|
|
26
26
|
* makes git print `error: No such remote 'origin'` on ITS stderr, which is the
|
|
27
|
-
* terminal's — the catch below never sees it.
|
|
28
|
-
*
|
|
29
|
-
* `import --from-factory` started calling it per row: importing 12 registry
|
|
30
|
-
* rows printed two raw git errors between the progress lines. Absence of a
|
|
31
|
-
* remote is an expected answer here (`undefined`), not something to report.
|
|
27
|
+
* terminal's — the catch below never sees it. Absence of a remote is an
|
|
28
|
+
* expected answer here (`undefined`), not something to report.
|
|
32
29
|
*/
|
|
33
30
|
export declare function originSlug(cwd: string): string | undefined;
|
|
34
31
|
/** One `projects list` row, pre-render. */
|
|
@@ -20,16 +20,15 @@ import { machineId } from '../lib/machine-id.js';
|
|
|
20
20
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
21
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
22
22
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
23
|
-
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
24
23
|
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, workspaceWarnings, } from '../lib/project-probe.js';
|
|
25
|
-
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
24
|
+
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, validateProjectDef, } from '../lib/projects.js';
|
|
26
25
|
import { rollupSessionsByProject, withDefaultMachine, isDeadStatus, liveDeadSplit, enrichProjectSignals, formatProjectMembersByHost, formatProjectWarnings, } from '../lib/project-status.js';
|
|
27
26
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
28
27
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
29
28
|
import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
30
29
|
import { formatFocusAreas, readFocusAreas } from '../lib/project-focus.js';
|
|
31
30
|
import { formatVerdict, scheduleVerdict } from '../lib/project-schedule.js';
|
|
32
|
-
import {
|
|
31
|
+
import { buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
33
32
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
34
33
|
export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
35
34
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
@@ -73,11 +72,8 @@ function parseGoalFlag(raw) {
|
|
|
73
72
|
*
|
|
74
73
|
* `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
|
|
75
74
|
* makes git print `error: No such remote 'origin'` on ITS stderr, which is the
|
|
76
|
-
* terminal's — the catch below never sees it.
|
|
77
|
-
*
|
|
78
|
-
* `import --from-factory` started calling it per row: importing 12 registry
|
|
79
|
-
* rows printed two raw git errors between the progress lines. Absence of a
|
|
80
|
-
* remote is an expected answer here (`undefined`), not something to report.
|
|
75
|
+
* terminal's — the catch below never sees it. Absence of a remote is an
|
|
76
|
+
* expected answer here (`undefined`), not something to report.
|
|
81
77
|
*/
|
|
82
78
|
export function originSlug(cwd) {
|
|
83
79
|
try {
|
|
@@ -92,38 +88,6 @@ export function originSlug(cwd) {
|
|
|
92
88
|
return undefined;
|
|
93
89
|
}
|
|
94
90
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Read the Factory registry off disk and plan its import. Every read failure is
|
|
97
|
-
* fatal and named — an unreadable or malformed registry must not read as "0
|
|
98
|
-
* projects to import".
|
|
99
|
-
*/
|
|
100
|
-
function runFactoryImport(existing, opts) {
|
|
101
|
-
const src = factoryProjectsPath();
|
|
102
|
-
let rawText;
|
|
103
|
-
try {
|
|
104
|
-
rawText = fs.readFileSync(src, 'utf8');
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
console.error(chalk.red(`No Factory registry at ${src}`));
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
let parsed;
|
|
111
|
-
try {
|
|
112
|
-
parsed = JSON.parse(rawText);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
console.error(chalk.red(`Factory registry at ${src} is not valid JSON`));
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
const rows = Array.isArray(parsed)
|
|
119
|
-
? parsed
|
|
120
|
-
: Array.isArray(parsed?.projects)
|
|
121
|
-
? parsed.projects
|
|
122
|
-
: [];
|
|
123
|
-
// The checkout's real `origin` overrides the registry's path-derived slug —
|
|
124
|
-
// see buildFactoryImportCandidates for why the path guess is not trustworthy.
|
|
125
|
-
return buildFactoryImportCandidates(rows, existing, opts, (root) => originSlug(expandLocalHome(root)));
|
|
126
|
-
}
|
|
127
91
|
/**
|
|
128
92
|
* List the workspace's Linear projects and plan their import, binding each to a
|
|
129
93
|
* local checkout under the configured projects root when the names match
|
|
@@ -442,7 +406,11 @@ export function registerProjectsCommands(program) {
|
|
|
442
406
|
examples: `
|
|
443
407
|
agents projects import --from-linear # the projects you actually track
|
|
444
408
|
agents projects add rush --repo phnx-labs/rush --path apps/web
|
|
445
|
-
agents projects list
|
|
409
|
+
agents projects list # definitions only (no session scan)
|
|
410
|
+
agents projects list --with-agents # opt-in local active counts
|
|
411
|
+
agents projects list --json # machine-readable defs (Factory uses this)
|
|
412
|
+
echo '{...}' | agents projects save --json # create/update one def from stdin
|
|
413
|
+
agents projects rm rush --json # machine-readable delete
|
|
446
414
|
agents projects status # every project, across the whole fleet
|
|
447
415
|
agents projects status rush # one project (same body as view/show)
|
|
448
416
|
agents projects view rush # alias of status <name>
|
|
@@ -452,31 +420,34 @@ export function registerProjectsCommands(program) {
|
|
|
452
420
|
`,
|
|
453
421
|
notes: `
|
|
454
422
|
Definitions are hand-editable YAML in ~/.agents/projects/ and sync across
|
|
455
|
-
machines with 'agents push/pull'.
|
|
456
|
-
|
|
457
|
-
'import --from-factory' reads Factory's auto-detected registry, which
|
|
458
|
-
guesses from checkouts on disk — it imports only 'high' confidence rows by
|
|
459
|
-
default. Widen with --min-confidence medium or --all, and drop a bad guess
|
|
460
|
-
with 'agents projects rm <name>'.
|
|
423
|
+
machines with 'agents push/pull'. Factory reads and writes only through
|
|
424
|
+
these commands — never ~/.agents/factory/projects.json.
|
|
461
425
|
`,
|
|
462
426
|
});
|
|
463
427
|
// ---- list ----
|
|
464
428
|
projects
|
|
465
429
|
.command('list')
|
|
466
|
-
.description('List defined projects
|
|
430
|
+
.description('List defined projects (definitions only by default; no session scan).')
|
|
467
431
|
.option('--json', 'Machine-readable output')
|
|
432
|
+
.option('--with-agents', 'Include local active agent counts (opt-in; never SSH)')
|
|
468
433
|
.action(async (opts) => {
|
|
469
434
|
const defs = listProjectDefs();
|
|
435
|
+
// Definitions-only by default: zero session scan / SSH. --with-agents is
|
|
436
|
+
// an explicit opt-in for local active counts only (getActiveSessions is
|
|
437
|
+
// local; it never fans out).
|
|
438
|
+
const roll = opts.withAgents
|
|
439
|
+
? rollupSessionsByProject(defs, await getActiveSessions())
|
|
440
|
+
: undefined;
|
|
470
441
|
if (opts.json) {
|
|
471
|
-
|
|
472
|
-
|
|
442
|
+
console.log(JSON.stringify(opts.withAgents
|
|
443
|
+
? defs.map((d) => ({ ...d, agents: roll.get(d.name)?.agents ?? 0 }))
|
|
444
|
+
: defs, null, 2));
|
|
473
445
|
return;
|
|
474
446
|
}
|
|
475
447
|
if (!defs.length) {
|
|
476
448
|
console.log(chalk.gray('No projects defined. Add one: agents projects add <name>'));
|
|
477
449
|
return;
|
|
478
450
|
}
|
|
479
|
-
const roll = rollupSessionsByProject(defs, await getActiveSessions());
|
|
480
451
|
const rows = defs.map((d) => ({
|
|
481
452
|
name: d.name,
|
|
482
453
|
path: truncate(d.root ?? d.defaultPath ?? '', LIST_PATH_MAX),
|
|
@@ -484,9 +455,9 @@ export function registerProjectsCommands(program) {
|
|
|
484
455
|
}));
|
|
485
456
|
const w = computeProjectListWidths(rows);
|
|
486
457
|
for (const [i, d] of defs.entries()) {
|
|
487
|
-
const agents = roll.get(d.name)?.agents ?? 0;
|
|
488
458
|
const row = rows[i];
|
|
489
|
-
|
|
459
|
+
const agentsSuffix = opts.withAgents && roll ? ` ${roll.get(d.name)?.agents ?? 0} agents` : '';
|
|
460
|
+
console.log(` ${chalk.bold(row.name.padEnd(w.name))} ${chalk.dim(row.path.padEnd(w.path))} ${chalk.cyan(row.repo.padEnd(w.repo))}${agentsSuffix}`);
|
|
490
461
|
}
|
|
491
462
|
});
|
|
492
463
|
// ---- add ----
|
|
@@ -732,11 +703,8 @@ export function registerProjectsCommands(program) {
|
|
|
732
703
|
// ---- import ----
|
|
733
704
|
projects
|
|
734
705
|
.command('import')
|
|
735
|
-
.description('Import project definitions from Linear (
|
|
736
|
-
.option('--from-linear', 'Import the workspace\'s Linear projects
|
|
737
|
-
.option('--from-factory', 'Import the Factory registry (~/.agents/factory/projects.json)')
|
|
738
|
-
.option('--min-confidence <level>', '--from-factory only: lowest detection confidence to import — low|medium|high (default: high)')
|
|
739
|
-
.option('--all', '--from-factory only: import every row regardless of confidence')
|
|
706
|
+
.description('Import project definitions from Linear (via the `linear` CLI).')
|
|
707
|
+
.option('--from-linear', 'Import the workspace\'s Linear projects')
|
|
740
708
|
.option('--force', 'Overwrite existing definitions')
|
|
741
709
|
.action((raw) => {
|
|
742
710
|
let opts;
|
|
@@ -748,7 +716,7 @@ export function registerProjectsCommands(program) {
|
|
|
748
716
|
process.exit(1);
|
|
749
717
|
}
|
|
750
718
|
const existing = new Map(listProjectDefs().map((d) => [d.name, d]));
|
|
751
|
-
const result =
|
|
719
|
+
const result = runLinearImport(existing, opts);
|
|
752
720
|
for (const def of result.defs)
|
|
753
721
|
writeProjectDef(def);
|
|
754
722
|
const n = result.defs.length;
|
|
@@ -756,9 +724,6 @@ export function registerProjectsCommands(program) {
|
|
|
756
724
|
console.log(chalk.green(`Imported ${n} project${n === 1 ? '' : 's'}${s ? chalk.gray(` (${s} skipped)`) : ''}`));
|
|
757
725
|
for (const skip of result.skipped)
|
|
758
726
|
console.log(chalk.gray(` skip ${skip.name}: ${skip.reason}`));
|
|
759
|
-
if (opts.source === 'factory' && s > 0 && opts.minConfidence === 'high') {
|
|
760
|
-
console.log(chalk.gray(' (widen with --min-confidence medium or --all)'));
|
|
761
|
-
}
|
|
762
727
|
});
|
|
763
728
|
// ---- set ----
|
|
764
729
|
projects
|
|
@@ -883,17 +848,79 @@ export function registerProjectsCommands(program) {
|
|
|
883
848
|
writeProjectDef(def);
|
|
884
849
|
console.log(chalk.green(`${def.name} → Linear project "${p.name}" (${p.id})${p.url ? ` ${p.url}` : ''}`));
|
|
885
850
|
});
|
|
851
|
+
// ---- save ----
|
|
852
|
+
projects
|
|
853
|
+
.command('save')
|
|
854
|
+
.description('Create or update one project from a complete ProjectDef JSON object on stdin.')
|
|
855
|
+
.option('--json', 'Required: read ProjectDef JSON from stdin; print the saved definition as JSON')
|
|
856
|
+
.action(async (opts) => {
|
|
857
|
+
if (!opts.json) {
|
|
858
|
+
console.error(chalk.red('projects save requires --json (pipe one complete ProjectDef JSON object on stdin).'));
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
const chunks = [];
|
|
862
|
+
for await (const c of process.stdin)
|
|
863
|
+
chunks.push(c);
|
|
864
|
+
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
865
|
+
if (!raw) {
|
|
866
|
+
console.error(chalk.red('projects save --json: empty stdin (expected one ProjectDef JSON object).'));
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
let parsed;
|
|
870
|
+
try {
|
|
871
|
+
parsed = JSON.parse(raw);
|
|
872
|
+
}
|
|
873
|
+
catch (e) {
|
|
874
|
+
console.error(chalk.red(`projects save --json: invalid JSON: ${e instanceof Error ? e.message : String(e)}`));
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
let def;
|
|
878
|
+
try {
|
|
879
|
+
def = validateProjectDef(parsed);
|
|
880
|
+
}
|
|
881
|
+
catch (e) {
|
|
882
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
writeProjectDef(def);
|
|
886
|
+
const saved = loadProjectDef(def.name);
|
|
887
|
+
if (!saved) {
|
|
888
|
+
console.error(chalk.red(`projects save: wrote "${def.name}" but could not reload it`));
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
console.log(JSON.stringify(saved, null, 2));
|
|
892
|
+
});
|
|
886
893
|
// ---- rm ----
|
|
887
894
|
projects
|
|
888
895
|
.command('rm <name>')
|
|
889
896
|
.alias('remove')
|
|
890
897
|
.description('Delete a project definition. Never touches the repo.')
|
|
891
|
-
.
|
|
898
|
+
.option('--json', 'Machine-readable success / error')
|
|
899
|
+
.action((name, opts) => {
|
|
900
|
+
if (!isSafeProjectName(name)) {
|
|
901
|
+
if (opts.json) {
|
|
902
|
+
console.log(JSON.stringify({ ok: false, name, error: `Invalid project name: "${name}"` }));
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
console.error(chalk.red(`Invalid project name: "${name}"`));
|
|
906
|
+
}
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
892
909
|
if (removeProjectDef(name)) {
|
|
893
|
-
|
|
910
|
+
if (opts.json) {
|
|
911
|
+
console.log(JSON.stringify({ ok: true, name, removed: true }));
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
console.log(chalk.green(`Removed project "${name}"`));
|
|
915
|
+
}
|
|
894
916
|
}
|
|
895
917
|
else {
|
|
896
|
-
|
|
918
|
+
if (opts.json) {
|
|
919
|
+
console.log(JSON.stringify({ ok: false, name, error: `No project named "${name}"` }));
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
console.error(chalk.red(`No project named "${name}".`));
|
|
923
|
+
}
|
|
897
924
|
process.exit(1);
|
|
898
925
|
}
|
|
899
926
|
});
|