@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/commands/inspect.js
CHANGED
|
@@ -21,6 +21,7 @@ import { truncate, termLink } from '../lib/format.js';
|
|
|
21
21
|
import * as yaml from 'yaml';
|
|
22
22
|
import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
|
|
23
23
|
import { supports } from '../lib/capabilities.js';
|
|
24
|
+
import { resolveConfiguredModel } from '../lib/models.js';
|
|
24
25
|
import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
25
26
|
import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
|
|
26
27
|
import { getVersionHomePath } from '../lib/versions.js';
|
|
@@ -585,8 +586,10 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
585
586
|
console.log(JSON.stringify(json, null, 2));
|
|
586
587
|
return;
|
|
587
588
|
}
|
|
588
|
-
// Plain text
|
|
589
|
-
const
|
|
589
|
+
// Plain text — model sits right beside the version, same priority (no label).
|
|
590
|
+
const configuredModel = resolveConfiguredModel(agent, version)?.model;
|
|
591
|
+
const modelPart = configuredModel ? ' ' + chalk.yellow(configuredModel) : '';
|
|
592
|
+
const head = `${chalk.bold(agent)} ${chalk.gray('@')} ${chalk.cyan(version)}${modelPart}${isDefault ? ' ' + chalk.green('[default]') : ''}`;
|
|
590
593
|
console.log('\n' + head + '\n');
|
|
591
594
|
const rows = [
|
|
592
595
|
['install', versionHome],
|
|
@@ -15,5 +15,25 @@ import { type Preset } from '../lib/profiles-presets.js';
|
|
|
15
15
|
* without mocking @inquirer/prompts.
|
|
16
16
|
*/
|
|
17
17
|
export declare function buildProfileFromCollection(name: string, preset: Preset, collected: Record<string, string>, version?: string): Profile;
|
|
18
|
+
/** Options accepted by {@link addProfile} — shared by `agents profiles add` and `agents harness add`. */
|
|
19
|
+
export interface AddProfileOptions {
|
|
20
|
+
preset?: string;
|
|
21
|
+
host?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
authProvider?: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
keyStdin?: boolean;
|
|
27
|
+
force?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a profile ("custom harness"). Two paths:
|
|
31
|
+
* - `--host <agent> --model <id>`: one-shot custom harness from a host + model
|
|
32
|
+
* (no preset needed). This is what makes a model like Muse Spark a named,
|
|
33
|
+
* runnable harness.
|
|
34
|
+
* - otherwise: apply a built-in preset (existing behavior).
|
|
35
|
+
* `label` only tunes the success wording (Profile vs Harness). Throws on error.
|
|
36
|
+
*/
|
|
37
|
+
export declare function addProfile(name: string, opts: AddProfileOptions, label?: 'Profile' | 'Harness'): Promise<void>;
|
|
18
38
|
/** Register the `agents profiles` command tree. */
|
|
19
39
|
export declare function registerProfilesCommands(program: Command): void;
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { readStdinSync } from '../lib/format.js';
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
|
-
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
|
|
12
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, profileFromHostModel, baseUrlEnvKeyForHost, authEnvKeyForHost, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
|
|
13
13
|
import { getPreset, listPresets, expandPreset } from '../lib/profiles-presets.js';
|
|
14
14
|
import { hasKeychainToken, keychainItemName, setKeychainToken, deleteKeychainToken, } from '../lib/secrets/profiles.js';
|
|
15
15
|
import { isInteractiveTerminal } from './utils.js';
|
|
16
16
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
17
17
|
import { getActiveResourceProfileName, getResourceProfilePreset, listResourceProfileNames, setActiveResourceProfile, upsertResourceProfilePreset, validateResourceProfileName, } from '../lib/resource-profiles.js';
|
|
18
|
-
import { AGENTS } from '../lib/agents.js';
|
|
18
|
+
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
19
19
|
import { listInstalledVersions, syncResourcesToVersion } from '../lib/versions.js';
|
|
20
20
|
/**
|
|
21
21
|
* Pure helper: builds a Profile from collected wizard inputs. Extracted so the
|
|
@@ -31,6 +31,7 @@ export function buildProfileFromCollection(name, preset, collected, version) {
|
|
|
31
31
|
envVar: preset.authEnvVar,
|
|
32
32
|
keychainItem: keychainItemName(preset.provider),
|
|
33
33
|
},
|
|
34
|
+
authOptional: preset.authOptional,
|
|
34
35
|
description: preset.description,
|
|
35
36
|
preset: preset.name,
|
|
36
37
|
provider: preset.provider,
|
|
@@ -65,6 +66,66 @@ async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
|
65
66
|
setKeychainToken(item, token);
|
|
66
67
|
console.log(chalk.green(`Stored in keychain: ${item}`));
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a profile ("custom harness"). Two paths:
|
|
71
|
+
* - `--host <agent> --model <id>`: one-shot custom harness from a host + model
|
|
72
|
+
* (no preset needed). This is what makes a model like Muse Spark a named,
|
|
73
|
+
* runnable harness.
|
|
74
|
+
* - otherwise: apply a built-in preset (existing behavior).
|
|
75
|
+
* `label` only tunes the success wording (Profile vs Harness). Throws on error.
|
|
76
|
+
*/
|
|
77
|
+
export async function addProfile(name, opts, label = 'Profile') {
|
|
78
|
+
validateProfileName(name);
|
|
79
|
+
if (profileExists(name) && !opts.force) {
|
|
80
|
+
throw new Error(`${label} '${name}' already exists. Use --force to overwrite.`);
|
|
81
|
+
}
|
|
82
|
+
// One-shot host + model → custom harness, no preset required.
|
|
83
|
+
if (opts.host || opts.model) {
|
|
84
|
+
if (!opts.host || !opts.model) {
|
|
85
|
+
throw new Error('Both --host <agent> and --model <id> are required to build a harness from a host + model.');
|
|
86
|
+
}
|
|
87
|
+
if (!ALL_AGENT_IDS.includes(opts.host)) {
|
|
88
|
+
throw new Error(`Unknown host '${opts.host}'. Valid hosts: ${ALL_AGENT_IDS.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
const host = opts.host;
|
|
91
|
+
if (opts.baseUrl && !baseUrlEnvKeyForHost(host)) {
|
|
92
|
+
console.error(chalk.yellow(`Note: --base-url has no known env var for host '${host}'; ignoring it.`));
|
|
93
|
+
}
|
|
94
|
+
let authEnvVar;
|
|
95
|
+
if (opts.authProvider) {
|
|
96
|
+
const key = authEnvKeyForHost(host);
|
|
97
|
+
if (!key) {
|
|
98
|
+
throw new Error(`--auth-provider is set but host '${host}' has no known auth env var. Use a preset or a hand-written profile YAML.`);
|
|
99
|
+
}
|
|
100
|
+
authEnvVar = key;
|
|
101
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
102
|
+
}
|
|
103
|
+
const profile = profileFromHostModel(name, host, opts.model, {
|
|
104
|
+
version: opts.version,
|
|
105
|
+
baseUrl: opts.baseUrl,
|
|
106
|
+
provider: opts.authProvider,
|
|
107
|
+
authEnvVar,
|
|
108
|
+
});
|
|
109
|
+
writeProfile(profile);
|
|
110
|
+
console.log(chalk.green(`${label} '${name}' added — ${host} + ${opts.model}.`));
|
|
111
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Preset path.
|
|
115
|
+
const presetName = opts.preset || name;
|
|
116
|
+
const preset = getPreset(presetName);
|
|
117
|
+
if (!preset) {
|
|
118
|
+
throw new Error(`No preset '${presetName}'.\nAvailable presets: ${listPresets().map((p) => p.name).join(', ')}\n` +
|
|
119
|
+
'Or build a custom harness: --host <agent> --model <id>.');
|
|
120
|
+
}
|
|
121
|
+
if (!preset.authOptional) {
|
|
122
|
+
await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
|
|
123
|
+
}
|
|
124
|
+
const profile = profileFromPreset(name, preset, opts.version);
|
|
125
|
+
writeProfile(profile);
|
|
126
|
+
console.log(chalk.green(`${label} '${name}' added.`));
|
|
127
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
128
|
+
}
|
|
68
129
|
/** Format a single profile as a table row for the `profiles list` output. */
|
|
69
130
|
function renderProfileRow(p) {
|
|
70
131
|
const host = p.host.version ? `${p.host.agent}@${p.host.version}` : p.host.agent;
|
|
@@ -462,27 +523,13 @@ Examples:
|
|
|
462
523
|
.option('--version <version>', 'Pin the host CLI version (e.g., 2.1.113)')
|
|
463
524
|
.option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
|
|
464
525
|
.option('--force', 'Overwrite an existing profile with the same name')
|
|
526
|
+
.addHelpText('after', '\nTo build a custom harness from a host CLI + model in one shot, use `agents harness add`.\n')
|
|
465
527
|
.action(async (name, opts) => {
|
|
528
|
+
// Preset-only surface here — `--host` on `profiles` is reserved for remote
|
|
529
|
+
// device routing (see lib/hosts/passthrough.ts). The host+model one-shot
|
|
530
|
+
// lives on `agents harness add`, which owns its own `--host`.
|
|
466
531
|
try {
|
|
467
|
-
|
|
468
|
-
console.error(chalk.red(`Profile '${name}' already exists. Use --force to overwrite.`));
|
|
469
|
-
process.exit(1);
|
|
470
|
-
}
|
|
471
|
-
const presetName = opts.preset || name;
|
|
472
|
-
const preset = getPreset(presetName);
|
|
473
|
-
if (!preset) {
|
|
474
|
-
console.error(chalk.red(`No preset '${presetName}'.`));
|
|
475
|
-
console.error(chalk.gray('Available presets: ' + listPresets().map((p) => p.name).join(', ')));
|
|
476
|
-
console.error(chalk.gray('Or pass --preset <name> to pick explicitly.'));
|
|
477
|
-
process.exit(1);
|
|
478
|
-
}
|
|
479
|
-
if (!preset.authOptional) {
|
|
480
|
-
await ensureProviderToken(preset.provider, preset.signupUrl, opts.keyStdin);
|
|
481
|
-
}
|
|
482
|
-
const profile = profileFromPreset(name, preset, opts.version);
|
|
483
|
-
writeProfile(profile);
|
|
484
|
-
console.log(chalk.green(`Profile '${name}' added.`));
|
|
485
|
-
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
532
|
+
await addProfile(name, { preset: opts.preset, version: opts.version, keyStdin: opts.keyStdin, force: opts.force }, 'Profile');
|
|
486
533
|
}
|
|
487
534
|
catch (err) {
|
|
488
535
|
console.error(chalk.red(err.message));
|
package/dist/commands/repo.js
CHANGED
|
@@ -812,6 +812,7 @@ export function registerRepoCommands(program) {
|
|
|
812
812
|
console.log(chalk.gray('No repos to pull.'));
|
|
813
813
|
return;
|
|
814
814
|
}
|
|
815
|
+
let anyPulled = false;
|
|
815
816
|
for (const t of targets) {
|
|
816
817
|
if (!fs.existsSync(t.dir) || !isGitRepo(t.dir)) {
|
|
817
818
|
// A plain (never-cloned) user repo — setup makes ~/.agents a bare dir and
|
|
@@ -851,11 +852,24 @@ export function registerRepoCommands(program) {
|
|
|
851
852
|
const result = await pullRepo(t.dir);
|
|
852
853
|
if (result.success) {
|
|
853
854
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.commit}`);
|
|
855
|
+
anyPulled = true;
|
|
854
856
|
}
|
|
855
857
|
else {
|
|
856
858
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
857
859
|
}
|
|
858
860
|
}
|
|
861
|
+
// RUSH-1980: a pull rewrites the routine YAML on disk, but the daemon's
|
|
862
|
+
// scheduler froze its JobConfigs (device pins included) at load. Without a
|
|
863
|
+
// reload it keeps firing the pre-pull pins — a routine re-pinned to another
|
|
864
|
+
// host still fires here, double-firing across the fleet. SIGHUP the daemon
|
|
865
|
+
// so scheduler.reloadAll() re-reads the synced YAML and device pins refresh.
|
|
866
|
+
// No-op when the daemon isn't running (or on Windows, which has no SIGHUP).
|
|
867
|
+
if (anyPulled) {
|
|
868
|
+
const { isDaemonRunning, signalDaemonReload } = await import('../lib/daemon.js');
|
|
869
|
+
if (isDaemonRunning() && signalDaemonReload()) {
|
|
870
|
+
console.log(chalk.gray('Reloaded the routines daemon (device pins refreshed).'));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
859
873
|
});
|
|
860
874
|
repoCmd
|
|
861
875
|
.command('push [alias]')
|
|
@@ -12,7 +12,8 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
14
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
15
|
-
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, } from '../lib/routines.js';
|
|
15
|
+
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, placementRequiresFiringPin, HOST_STRATEGIES, } from '../lib/routines.js';
|
|
16
|
+
import { discoverProjectRoutinesAt, enableProjectRoutines, disableProjectRoutines, syncProjectRoutines, syncAllProjectRoutines, listEnabledProjectRoots, resolveProjectRoot, displayProjectPath, listProjectRoutineFiles, } from '../lib/routines-project.js';
|
|
16
17
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
17
18
|
import { getRoutinesDir } from '../lib/state.js';
|
|
18
19
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
@@ -131,6 +132,24 @@ function ensureSchedulerRunning(opts = {}) {
|
|
|
131
132
|
function writeJson(payload) {
|
|
132
133
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
133
134
|
}
|
|
135
|
+
function printSyncResult(sync) {
|
|
136
|
+
console.log(chalk.bold(displayProjectPath(sync.projectRoot)));
|
|
137
|
+
if (sync.synced.length === 0 && sync.skipped.length === 0 && sync.removed.length === 0 && sync.errors.length === 0) {
|
|
138
|
+
console.log(chalk.gray(' (no project routines)'));
|
|
139
|
+
}
|
|
140
|
+
if (sync.synced.length > 0) {
|
|
141
|
+
console.log(chalk.green(` synced: ${sync.synced.join(', ')}`));
|
|
142
|
+
}
|
|
143
|
+
if (sync.removed.length > 0) {
|
|
144
|
+
console.log(chalk.gray(` removed: ${sync.removed.join(', ')}`));
|
|
145
|
+
}
|
|
146
|
+
for (const s of sync.skipped) {
|
|
147
|
+
console.log(chalk.yellow(` skipped ${s.name}: ${s.reason}`));
|
|
148
|
+
}
|
|
149
|
+
for (const e of sync.errors) {
|
|
150
|
+
console.log(chalk.red(` error ${e.name}: ${e.error}`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
134
153
|
function runMetaJson(run) {
|
|
135
154
|
return {
|
|
136
155
|
jobId: run.jobName,
|
|
@@ -233,6 +252,14 @@ export function registerRoutinesCommands(program) {
|
|
|
233
252
|
# Create from YAML (for complex routines with multiple settings)
|
|
234
253
|
agents routines add weekly-report.yml
|
|
235
254
|
|
|
255
|
+
# Place the job body on a fleet device / cloud / named host (not --host)
|
|
256
|
+
agents routines add drain --schedule "0 3 * * *" --agent claude --placement fleet --prompt "Drain queue"
|
|
257
|
+
agents routines add review --schedule "0 9 * * 1" --agent claude --placement cloud --prompt "Review open PRs"
|
|
258
|
+
|
|
259
|
+
# Opt a project's .agents/routines/*.yml into daemon firing (never auto)
|
|
260
|
+
agents routines enable-project --yes
|
|
261
|
+
agents routines sync
|
|
262
|
+
|
|
236
263
|
# List all routines and their next run times
|
|
237
264
|
agents routines list
|
|
238
265
|
|
|
@@ -323,6 +350,10 @@ export function registerRoutinesCommands(program) {
|
|
|
323
350
|
timezone: job.timezone ?? null,
|
|
324
351
|
devices: job.devices ?? [],
|
|
325
352
|
host: job.host ?? null,
|
|
353
|
+
hostStrategy: resolveHostStrategy(job),
|
|
354
|
+
source: job.source ?? null,
|
|
355
|
+
sourceRepo: job.source?.repo ?? job.repo ?? null,
|
|
356
|
+
sourceBranch: job.source?.branch ?? null,
|
|
326
357
|
runsHere: jobRunsOnThisDevice(job),
|
|
327
358
|
enabled: job.enabled,
|
|
328
359
|
overdue: overdueSet.has(job.name),
|
|
@@ -368,7 +399,12 @@ export function registerRoutinesCommands(program) {
|
|
|
368
399
|
}
|
|
369
400
|
const latestRun = getLatestRun(job.name);
|
|
370
401
|
const lastStatus = latestRun?.status || '-';
|
|
371
|
-
|
|
402
|
+
// Prefer project-source repo (with optional @branch) over bare job.repo.
|
|
403
|
+
const sourceRepo = job.source?.repo ?? job.repo;
|
|
404
|
+
const sourceLabel = sourceRepo
|
|
405
|
+
? (job.source?.branch ? `${sourceRepo}@${job.source.branch}` : sourceRepo)
|
|
406
|
+
: null;
|
|
407
|
+
const repoInfo = formatRepoLink(sourceLabel ?? job.repo);
|
|
372
408
|
const repoCell = link(repoInfo.display, repoInfo.href);
|
|
373
409
|
// Pad based on the display string, not the raw cell (which may include escape codes).
|
|
374
410
|
const repoPadding = Math.max(0, REPO_W - repoInfo.display.length);
|
|
@@ -376,8 +412,16 @@ export function registerRoutinesCommands(program) {
|
|
|
376
412
|
// chalk adds escape codes; pad the raw word and let chalk wrap it.
|
|
377
413
|
const enabledWord = job.enabled ? 'yes' : 'no';
|
|
378
414
|
const enabledPad = Math.max(0, ENABLED_W - enabledWord.length);
|
|
379
|
-
// Placement
|
|
380
|
-
const
|
|
415
|
+
// Placement rides in the Devices cell: eligibility →execution strategy.
|
|
416
|
+
const strategy = resolveHostStrategy(job);
|
|
417
|
+
const placementTag = strategy === 'local'
|
|
418
|
+
? (job.host ? `→${job.host}` : '')
|
|
419
|
+
: strategy === 'host'
|
|
420
|
+
? `→${job.host ?? '?'}`
|
|
421
|
+
: strategy === 'fleet'
|
|
422
|
+
? '→fleet'
|
|
423
|
+
: '→cloud';
|
|
424
|
+
const deviceFull = [job.devices?.join(',') ?? '', placementTag]
|
|
381
425
|
.filter(Boolean)
|
|
382
426
|
.join(' ');
|
|
383
427
|
const deviceWord = deviceFull.length === 0
|
|
@@ -423,7 +467,8 @@ export function registerRoutinesCommands(program) {
|
|
|
423
467
|
.option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
|
|
424
468
|
.option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
|
|
425
469
|
.option('--devices <names>', 'Fleet allowlist (comma-separated): only listed devices schedule and fire this routine. Omit for unrestricted.')
|
|
426
|
-
.option('--run-on <name>', 'Execute the job body on this machine over SSH (a registered host, device, capability tag, or user@host). Placement, not eligibility — see --devices. Auto-pins devices to THIS machine unless --devices is given.')
|
|
470
|
+
.option('--run-on <name>', 'Execute the job body on this machine over SSH (a registered host, device, capability tag, or user@host). Sets hostStrategy=host. Placement, not eligibility — see --devices. Auto-pins devices to THIS machine unless --devices is given.')
|
|
471
|
+
.option('--placement <strategy>', `Where the job body runs: ${HOST_STRATEGIES.join('|')} (default: local, or host when --run-on is set). Not the same as --host (which manages routines on a remote machine).`)
|
|
427
472
|
.option('--run-cwd <dir>', 'Working directory on the --run-on host (--remote-cwd is taken by the remote-management passthrough)')
|
|
428
473
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
429
474
|
.option('--on <source:event>', 'Webhook trigger instead of/in addition to a schedule: github:pull_request or linear:Issue')
|
|
@@ -490,12 +535,29 @@ export function registerRoutinesCommands(program) {
|
|
|
490
535
|
if (options.devices !== undefined) {
|
|
491
536
|
devices = await parseAndValidateDevices(options.devices);
|
|
492
537
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
538
|
+
let hostStrategy;
|
|
539
|
+
try {
|
|
540
|
+
hostStrategy = parseHostStrategy(options.placement) ?? undefined;
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
console.error(chalk.red(err.message));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
// --run-on implies host strategy when the user didn't pick one.
|
|
547
|
+
if (options.runOn && !hostStrategy)
|
|
548
|
+
hostStrategy = 'host';
|
|
549
|
+
if (hostStrategy === 'host' && !options.runOn) {
|
|
550
|
+
console.error(chalk.red('--placement host requires --run-on <name>'));
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
// Off-box placement without a --devices pin would fire on EVERY daemon
|
|
554
|
+
// in the fleet, each dispatching once — duplicate runs (RUSH-1980).
|
|
555
|
+
// Pin to this machine unless the user chose an explicit eligibility set.
|
|
556
|
+
const strategyForPin = hostStrategy
|
|
557
|
+
?? (options.runOn ? 'host' : 'local');
|
|
558
|
+
if (placementRequiresFiringPin(strategyForPin) && !devices) {
|
|
497
559
|
devices = [machineId()];
|
|
498
|
-
console.error(chalk.gray(`--
|
|
560
|
+
console.error(chalk.gray(`--placement ${strategyForPin} with no --devices: pinned firing to this machine (${devices[0]}).`));
|
|
499
561
|
}
|
|
500
562
|
const config = {
|
|
501
563
|
name: nameOrPath,
|
|
@@ -512,6 +574,7 @@ export function registerRoutinesCommands(program) {
|
|
|
512
574
|
timezone: options.timezone,
|
|
513
575
|
...(devices ? { devices } : {}),
|
|
514
576
|
...(options.runOn ? { host: options.runOn } : {}),
|
|
577
|
+
...(hostStrategy ? { hostStrategy } : {}),
|
|
515
578
|
...(options.runCwd ? { remoteCwd: options.runCwd } : {}),
|
|
516
579
|
...(runOnce ? { runOnce: true } : {}),
|
|
517
580
|
...(options.endAt ? { endAt: options.endAt } : {}),
|
|
@@ -586,11 +649,12 @@ export function registerRoutinesCommands(program) {
|
|
|
586
649
|
enabled: true,
|
|
587
650
|
...parsed,
|
|
588
651
|
};
|
|
589
|
-
// Same duplicate-fire guard as
|
|
652
|
+
// Same duplicate-fire guard as --placement/--run-on: off-box placement
|
|
590
653
|
// with no eligibility pin would fire from every daemon in the fleet.
|
|
591
|
-
|
|
654
|
+
const fileStrategy = resolveHostStrategy(config);
|
|
655
|
+
if (placementRequiresFiringPin(fileStrategy) && (!config.devices || config.devices.length === 0)) {
|
|
592
656
|
config.devices = [machineId()];
|
|
593
|
-
console.error(chalk.gray(
|
|
657
|
+
console.error(chalk.gray(`${fileStrategy} placement with no devices pin: pinned firing to this machine (${config.devices[0]}).`));
|
|
594
658
|
}
|
|
595
659
|
writeJob(config);
|
|
596
660
|
if (options.json) {
|
|
@@ -783,18 +847,26 @@ export function registerRoutinesCommands(program) {
|
|
|
783
847
|
try {
|
|
784
848
|
const result = await executeJob(job);
|
|
785
849
|
const logPath = `${getRunDir(name, result.meta.runId)}/stdout.log`;
|
|
850
|
+
const succeeded = result.meta.status === 'completed';
|
|
786
851
|
if (options.json) {
|
|
787
852
|
writeJson({
|
|
788
|
-
ok:
|
|
853
|
+
ok: succeeded,
|
|
789
854
|
job: name,
|
|
790
855
|
logDir: getRunDir(name, result.meta.runId),
|
|
791
856
|
...runMetaJson(result.meta),
|
|
792
857
|
logPath,
|
|
793
858
|
reportPath: result.reportPath ?? null,
|
|
794
859
|
});
|
|
860
|
+
// A failed run must exit non-zero so cron wrappers, `&&` chains, and
|
|
861
|
+
// `--json` consumers actually see the failure (a logged-out agent used
|
|
862
|
+
// to exit 0 with ok:true, hiding the whole auth-failure epidemic). Set
|
|
863
|
+
// exitCode rather than process.exit() so the JSON payload is fully
|
|
864
|
+
// flushed to a pipe before the process ends.
|
|
865
|
+
if (!succeeded)
|
|
866
|
+
process.exitCode = 1;
|
|
795
867
|
return;
|
|
796
868
|
}
|
|
797
|
-
if (
|
|
869
|
+
if (succeeded) {
|
|
798
870
|
spinner.succeed(`Job completed (exit code: ${result.meta.exitCode})`);
|
|
799
871
|
}
|
|
800
872
|
else if (result.meta.status === 'timeout') {
|
|
@@ -805,10 +877,15 @@ export function registerRoutinesCommands(program) {
|
|
|
805
877
|
}
|
|
806
878
|
console.log(chalk.gray(` Run: ${result.meta.runId}`));
|
|
807
879
|
console.log(chalk.gray(` Log: ${logPath}`));
|
|
880
|
+
if (result.meta.errorMessage) {
|
|
881
|
+
console.log(chalk.gray(` Reason: ${result.meta.errorMessage}`));
|
|
882
|
+
}
|
|
808
883
|
if (result.reportPath) {
|
|
809
884
|
console.log(chalk.bold('\nReport:\n'));
|
|
810
885
|
console.log(fs.readFileSync(result.reportPath, 'utf-8'));
|
|
811
886
|
}
|
|
887
|
+
if (!succeeded)
|
|
888
|
+
process.exitCode = 1;
|
|
812
889
|
}
|
|
813
890
|
catch (err) {
|
|
814
891
|
if (options.json) {
|
|
@@ -1107,6 +1184,12 @@ export function registerRoutinesCommands(program) {
|
|
|
1107
1184
|
process.exit(1);
|
|
1108
1185
|
}
|
|
1109
1186
|
if (options.clear) {
|
|
1187
|
+
const strategy = resolveHostStrategy(job);
|
|
1188
|
+
if (placementRequiresFiringPin(strategy)) {
|
|
1189
|
+
console.error(chalk.red(`Cannot clear devices for hostStrategy: ${strategy} — without a pin every fleet daemon would dispatch once.`));
|
|
1190
|
+
console.error(chalk.gray(`Keep a single-machine pin (e.g. --set ${machineId()}) or switch to --placement local.`));
|
|
1191
|
+
process.exit(1);
|
|
1192
|
+
}
|
|
1110
1193
|
job.devices = undefined;
|
|
1111
1194
|
writeJob(job);
|
|
1112
1195
|
console.log(chalk.green(`Devices cleared for '${name}' — runs on all devices`));
|
|
@@ -1117,6 +1200,12 @@ export function registerRoutinesCommands(program) {
|
|
|
1117
1200
|
if (hasSet) {
|
|
1118
1201
|
const devices = await parseAndValidateDevices(options.set);
|
|
1119
1202
|
job.devices = devices;
|
|
1203
|
+
// Re-validate so host/fleet/cloud can't be left with an empty set via --set ""
|
|
1204
|
+
const errs = validateJob(job);
|
|
1205
|
+
if (errs.length > 0) {
|
|
1206
|
+
console.error(chalk.red(errs.join('\n')));
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1120
1209
|
writeJob(job);
|
|
1121
1210
|
console.log(chalk.green(`Devices for '${name}' set to: ${devices.join(', ')}`));
|
|
1122
1211
|
if (isDaemonRunning())
|
|
@@ -1145,6 +1234,11 @@ export function registerRoutinesCommands(program) {
|
|
|
1145
1234
|
})),
|
|
1146
1235
|
});
|
|
1147
1236
|
if (selected.length === 0) {
|
|
1237
|
+
const strategy = resolveHostStrategy(job);
|
|
1238
|
+
if (placementRequiresFiringPin(strategy)) {
|
|
1239
|
+
console.error(chalk.red(`Cannot clear devices for hostStrategy: ${strategy} — without a pin every fleet daemon would dispatch once.`));
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1148
1242
|
job.devices = undefined;
|
|
1149
1243
|
writeJob(job);
|
|
1150
1244
|
console.log(chalk.green(`Devices cleared for '${name}' — runs on all devices`));
|
|
@@ -1237,6 +1331,183 @@ export function registerRoutinesCommands(program) {
|
|
|
1237
1331
|
console.log(chalk.gray('\n Start the scheduler to begin firing routines: agents routines start'));
|
|
1238
1332
|
}
|
|
1239
1333
|
});
|
|
1334
|
+
routinesCmd
|
|
1335
|
+
.command('enable-project [path]')
|
|
1336
|
+
.description('Opt a project\'s .agents/routines/*.yml into daemon firing. Requires explicit approval — project routines never auto-fire from a cloned repo. Materialises copies into ~/.agents/routines/ with source provenance.')
|
|
1337
|
+
.option('--yes', 'Skip the interactive confirmation prompt')
|
|
1338
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1339
|
+
.action(async (projectPath, options) => {
|
|
1340
|
+
const root = projectPath
|
|
1341
|
+
? path.resolve(projectPath)
|
|
1342
|
+
: resolveProjectRoot(process.cwd());
|
|
1343
|
+
if (!root) {
|
|
1344
|
+
console.error(chalk.red('No project .agents/ directory found from the current directory.'));
|
|
1345
|
+
console.error(chalk.gray('Run from inside a project, or pass the project path: agents routines enable-project /path/to/repo'));
|
|
1346
|
+
process.exit(1);
|
|
1347
|
+
}
|
|
1348
|
+
const files = listProjectRoutineFiles(root);
|
|
1349
|
+
if (files.length === 0) {
|
|
1350
|
+
console.error(chalk.red(`No routines found under ${path.join(root, '.agents', 'routines')}`));
|
|
1351
|
+
process.exit(1);
|
|
1352
|
+
}
|
|
1353
|
+
if (!options.yes) {
|
|
1354
|
+
if (!isInteractiveTerminal()) {
|
|
1355
|
+
console.error(chalk.red('Refusing to enable project routines non-interactively without --yes.'));
|
|
1356
|
+
console.error(chalk.gray(`Found ${files.length} routine(s) in ${displayProjectPath(root)}. Re-run with --yes to confirm.`));
|
|
1357
|
+
process.exit(1);
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
1361
|
+
const ok = await confirm({
|
|
1362
|
+
message: `Enable daemon firing for ${files.length} project routine(s) in ${displayProjectPath(root)}?`,
|
|
1363
|
+
default: false,
|
|
1364
|
+
});
|
|
1365
|
+
if (!ok) {
|
|
1366
|
+
console.log(chalk.gray('Cancelled'));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
catch (err) {
|
|
1371
|
+
if (isPromptCancelled(err)) {
|
|
1372
|
+
console.log(chalk.gray('Cancelled'));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
throw err;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const newly = enableProjectRoutines(root);
|
|
1379
|
+
const sync = syncProjectRoutines(root);
|
|
1380
|
+
if (isDaemonRunning())
|
|
1381
|
+
signalDaemonReload();
|
|
1382
|
+
if (options.json) {
|
|
1383
|
+
writeJson({
|
|
1384
|
+
ok: true,
|
|
1385
|
+
projectRoot: root,
|
|
1386
|
+
newlyEnabled: newly,
|
|
1387
|
+
synced: sync.synced,
|
|
1388
|
+
skipped: sync.skipped,
|
|
1389
|
+
removed: sync.removed,
|
|
1390
|
+
errors: sync.errors,
|
|
1391
|
+
});
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
console.log(chalk.green(newly
|
|
1395
|
+
? `Enabled project routines for ${displayProjectPath(root)}`
|
|
1396
|
+
: `Project routines already enabled for ${displayProjectPath(root)}`));
|
|
1397
|
+
if (sync.synced.length > 0) {
|
|
1398
|
+
console.log(chalk.gray(` Synced: ${sync.synced.join(', ')}`));
|
|
1399
|
+
}
|
|
1400
|
+
for (const s of sync.skipped) {
|
|
1401
|
+
console.log(chalk.yellow(` Skipped ${s.name}: ${s.reason}`));
|
|
1402
|
+
}
|
|
1403
|
+
for (const e of sync.errors) {
|
|
1404
|
+
console.log(chalk.red(` Error ${e.name}: ${e.error}`));
|
|
1405
|
+
}
|
|
1406
|
+
console.log(chalk.gray('Daemon will fire these after reload. Re-sync later with: agents routines sync'));
|
|
1407
|
+
});
|
|
1408
|
+
routinesCmd
|
|
1409
|
+
.command('disable-project [path]')
|
|
1410
|
+
.description('Remove a project from the project-routines allowlist. Use --remove-synced to also delete the user-layer copies.')
|
|
1411
|
+
.option('--remove-synced', 'Delete user-layer routines that were materialised from this project')
|
|
1412
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1413
|
+
.action(async (projectPath, options) => {
|
|
1414
|
+
const root = projectPath
|
|
1415
|
+
? path.resolve(projectPath)
|
|
1416
|
+
: resolveProjectRoot(process.cwd());
|
|
1417
|
+
if (!root) {
|
|
1418
|
+
console.error(chalk.red('No project .agents/ directory found from the current directory.'));
|
|
1419
|
+
process.exit(1);
|
|
1420
|
+
}
|
|
1421
|
+
const result = disableProjectRoutines(root, { removeSynced: options.removeSynced });
|
|
1422
|
+
if (isDaemonRunning())
|
|
1423
|
+
signalDaemonReload();
|
|
1424
|
+
if (options.json) {
|
|
1425
|
+
writeJson({ ok: true, projectRoot: root, ...result });
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (!result.removed) {
|
|
1429
|
+
console.log(chalk.gray(`Project ${displayProjectPath(root)} was not on the allowlist`));
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
console.log(chalk.green(`Disabled project routines for ${displayProjectPath(root)}`));
|
|
1433
|
+
}
|
|
1434
|
+
if (result.deletedJobs.length > 0) {
|
|
1435
|
+
console.log(chalk.gray(` Removed user-layer copies: ${result.deletedJobs.join(', ')}`));
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
routinesCmd
|
|
1439
|
+
.command('sync [path]')
|
|
1440
|
+
.description('Refresh user-layer copies of opted-in project routines from their .agents/routines/*.yml sources. With no path, syncs every enabled project. Also runs automatically on daemon reload (SIGHUP).')
|
|
1441
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1442
|
+
.action(async (projectPath, options) => {
|
|
1443
|
+
if (projectPath) {
|
|
1444
|
+
const root = path.resolve(projectPath);
|
|
1445
|
+
const isEnabled = listEnabledProjectRoots().some((p) => p === root);
|
|
1446
|
+
if (!isEnabled) {
|
|
1447
|
+
console.error(chalk.red(`Project ${displayProjectPath(root)} is not enabled. Run: agents routines enable-project ${root}`));
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
}
|
|
1450
|
+
const sync = syncProjectRoutines(root);
|
|
1451
|
+
if (isDaemonRunning())
|
|
1452
|
+
signalDaemonReload();
|
|
1453
|
+
if (options.json) {
|
|
1454
|
+
writeJson({ ok: true, ...sync });
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
printSyncResult(sync);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const all = syncAllProjectRoutines();
|
|
1461
|
+
if (isDaemonRunning())
|
|
1462
|
+
signalDaemonReload();
|
|
1463
|
+
if (options.json) {
|
|
1464
|
+
writeJson({ ok: true, ...all });
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (all.projects.length === 0 && all.missing.length === 0) {
|
|
1468
|
+
console.log(chalk.gray('No project roots on the routines allowlist.'));
|
|
1469
|
+
console.log(chalk.gray(' Enable one with: agents routines enable-project'));
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
for (const p of all.projects)
|
|
1473
|
+
printSyncResult(p);
|
|
1474
|
+
for (const m of all.missing) {
|
|
1475
|
+
console.log(chalk.yellow(`Missing project root (still on allowlist): ${displayProjectPath(m)}`));
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
routinesCmd
|
|
1479
|
+
.command('projects')
|
|
1480
|
+
.description('List project roots opted into daemon-fired project routines')
|
|
1481
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1482
|
+
.action((options) => {
|
|
1483
|
+
const roots = listEnabledProjectRoots();
|
|
1484
|
+
if (options.json) {
|
|
1485
|
+
writeJson(roots.map((r) => ({
|
|
1486
|
+
path: r,
|
|
1487
|
+
display: displayProjectPath(r),
|
|
1488
|
+
routines: listProjectRoutineFiles(r).map((f) => f.name),
|
|
1489
|
+
})));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (roots.length === 0) {
|
|
1493
|
+
console.log(chalk.gray('No projects enabled. Use: agents routines enable-project'));
|
|
1494
|
+
// Offer a discovery hint for the current project.
|
|
1495
|
+
const discovered = discoverProjectRoutinesAt(process.cwd());
|
|
1496
|
+
if (discovered) {
|
|
1497
|
+
console.log(chalk.gray(` Found ${discovered.files.length} routine(s) in ${displayProjectPath(discovered.projectRoot)} — enable with: agents routines enable-project`));
|
|
1498
|
+
}
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
console.log(chalk.bold('Enabled project routines\n'));
|
|
1502
|
+
for (const r of roots) {
|
|
1503
|
+
const files = listProjectRoutineFiles(r);
|
|
1504
|
+
console.log(` ${chalk.cyan(displayProjectPath(r))} ${chalk.gray(`(${files.length} routine${files.length === 1 ? '' : 's'})`)}`);
|
|
1505
|
+
for (const f of files) {
|
|
1506
|
+
console.log(chalk.gray(` - ${f.name}`));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
console.log();
|
|
1510
|
+
});
|
|
1240
1511
|
routinesCmd
|
|
1241
1512
|
.command('scheduler-logs')
|
|
1242
1513
|
.description('Read scheduler log output (for debugging why a routine did not fire). Use --follow to stream.')
|