@phnx-labs/agents-cli 1.20.88 → 1.20.90
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 +503 -0
- package/README.md +15 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- package/dist/commands/exec.js +7 -1
- package/dist/commands/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/harness.d.ts +27 -0
- package/dist/commands/harness.js +120 -13
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/routines.d.ts +19 -0
- package/dist/commands/routines.js +51 -7
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.d.ts +10 -1
- package/dist/commands/secrets.js +20 -6
- package/dist/commands/sessions-browser.d.ts +4 -0
- package/dist/commands/sessions-browser.js +51 -9
- package/dist/commands/sessions-favorite.d.ts +20 -0
- package/dist/commands/sessions-favorite.js +120 -0
- package/dist/commands/sessions.d.ts +110 -21
- package/dist/commands/sessions.js +395 -74
- package/dist/commands/setup-secrets.d.ts +7 -0
- package/dist/commands/setup-secrets.js +12 -9
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.d.ts +14 -1
- package/dist/commands/view.js +103 -128
- package/dist/commands/webhook.js +7 -2
- package/dist/lib/agents.d.ts +4 -2
- package/dist/lib/agents.js +21 -6
- package/dist/lib/commands.js +9 -1
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +58 -4
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -0
- package/dist/lib/hosts/dispatch.js +19 -1
- package/dist/lib/hq/floor.js +12 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/picker.d.ts +27 -2
- package/dist/lib/picker.js +71 -7
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/profiles.d.ts +48 -0
- package/dist/lib/profiles.js +67 -0
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/rotate.d.ts +24 -2
- package/dist/lib/rotate.js +63 -6
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +2 -0
- package/dist/lib/runner.js +21 -8
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- package/dist/lib/session/active.d.ts +109 -3
- package/dist/lib/session/active.js +269 -13
- package/dist/lib/session/db.d.ts +14 -0
- package/dist/lib/session/db.js +35 -0
- package/dist/lib/session/favorites.d.ts +39 -0
- package/dist/lib/session/favorites.js +101 -0
- package/dist/lib/session/host-link.d.ts +68 -0
- package/dist/lib/session/host-link.js +64 -0
- package/dist/lib/session/presence.d.ts +85 -0
- package/dist/lib/session/presence.js +150 -0
- package/dist/lib/session/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- package/dist/lib/session/remote-list.d.ts +10 -0
- package/dist/lib/session/remote-list.js +47 -9
- package/dist/lib/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- package/dist/lib/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/tmux/binary.d.ts +7 -0
- package/dist/lib/tmux/binary.js +11 -1
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/usage-backoff.d.ts +29 -0
- package/dist/lib/usage-backoff.js +165 -0
- package/dist/lib/usage.d.ts +112 -5
- package/dist/lib/usage.js +464 -46
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +16 -1
- package/package.json +1 -1
package/dist/commands/factory.js
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir } from 'os';
|
|
|
6
6
|
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
7
7
|
import { insertTask } from '../lib/cloud/store.js';
|
|
8
8
|
import { emit } from '../lib/events.js';
|
|
9
|
+
import { buildFactorySnapshot } from '../lib/factory/snapshot.js';
|
|
9
10
|
function requireFactoryUrl() {
|
|
10
11
|
const url = process.env.FACTORY_FLOOR_URL;
|
|
11
12
|
if (!url) {
|
|
@@ -51,14 +52,28 @@ export function registerFactoryCommands(program) {
|
|
|
51
52
|
Examples:
|
|
52
53
|
agents factory submit PROJ-123
|
|
53
54
|
agents factory submit https://linear.app/example/issue/PROJ-123
|
|
55
|
+
agents factory snapshot --json
|
|
54
56
|
`);
|
|
55
|
-
factory.hook('preAction', () => {
|
|
56
|
-
|
|
57
|
+
factory.hook('preAction', (_thisCommand, actionCommand) => {
|
|
58
|
+
// Foreman must be able to read its tick input before any beta-gated action.
|
|
59
|
+
if (enabled || actionCommand.name() === 'snapshot')
|
|
57
60
|
return;
|
|
58
61
|
console.error(chalk.red('agents factory is in beta.'));
|
|
59
62
|
console.error(chalk.gray(betaEnableHint('factory')));
|
|
60
63
|
process.exit(1);
|
|
61
64
|
});
|
|
65
|
+
factory
|
|
66
|
+
.command('snapshot')
|
|
67
|
+
.description('Read the complete Software Factory state without dispatching or changing it.')
|
|
68
|
+
.option('--json', 'Output the stable machine-readable snapshot')
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
const snapshot = await buildFactorySnapshot();
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
renderSnapshot(snapshot);
|
|
76
|
+
});
|
|
62
77
|
factory
|
|
63
78
|
.command('submit <linear-ref>')
|
|
64
79
|
.description('Submit a Linear issue (PROJ-123 or URL) to the Software Factory.')
|
|
@@ -93,3 +108,12 @@ Examples:
|
|
|
93
108
|
console.log(` tail output agents cloud logs ${result.cloud_execution_id}`);
|
|
94
109
|
});
|
|
95
110
|
}
|
|
111
|
+
function renderSnapshot(snapshot) {
|
|
112
|
+
console.log(chalk.bold(`Factory snapshot ${snapshot.generatedAt}`));
|
|
113
|
+
console.log(` sessions ${snapshot.sessions.length}`);
|
|
114
|
+
console.log(` open PRs ${snapshot.prs.length}`);
|
|
115
|
+
console.log(` devices ${snapshot.devices.length}`);
|
|
116
|
+
for (const [project, queue] of Object.entries(snapshot.queues)) {
|
|
117
|
+
console.log(` ${project.padEnd(12)} todo ${queue.todo} · in progress ${queue.inProgress} · blocked ${queue.blocked}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
package/dist/commands/funnel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
|
|
2
|
+
import { buildFunnelDownCommand, buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
|
|
3
3
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
4
4
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
5
5
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
@@ -59,4 +59,19 @@ export function registerFunnelCommand(program) {
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
|
+
funnel
|
|
63
|
+
.command('down <host>')
|
|
64
|
+
.description('Disable Tailscale Funnel exposure for a public HTTPS port.')
|
|
65
|
+
.option('--port <n>', 'Public Funnel port: 443, 8443, or 10000', '443')
|
|
66
|
+
.action(async (host, opts) => {
|
|
67
|
+
try {
|
|
68
|
+
const publicPort = parseFunnelPort(opts.port ?? '443');
|
|
69
|
+
await runOnHost(host, buildFunnelDownCommand(publicPort));
|
|
70
|
+
console.log(chalk.green(`Funnel disabled on ${host}: public :${publicPort}`));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(chalk.red(err.message));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
62
77
|
}
|
|
@@ -11,4 +11,31 @@
|
|
|
11
11
|
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import type { Command } from 'commander';
|
|
14
|
+
import { type Profile } from '../lib/profiles.js';
|
|
15
|
+
/**
|
|
16
|
+
* Print one custom harness. Shared by `agents harness view <name>` and by
|
|
17
|
+
* `agents view <name>` — a custom harness resolves as an agent type there, so
|
|
18
|
+
* both entry points must describe it identically.
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderHarnessDetail(name: string): void;
|
|
21
|
+
/** Options accepted by `agents harness fork`. */
|
|
22
|
+
export interface ForkOptions {
|
|
23
|
+
model?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
authProvider?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
keyStdin?: boolean;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the new harness for `agents harness fork <source> <name>`.
|
|
34
|
+
*
|
|
35
|
+
* Two sources, one verb: an existing custom harness is copied and overridden;
|
|
36
|
+
* a native agent id is turned into a harness pinned to `--model` on that host.
|
|
37
|
+
* Forking a native harness therefore requires `--model` — there is nothing to
|
|
38
|
+
* copy a model from.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildFork(source: string, name: string, opts: ForkOptions): Profile;
|
|
14
41
|
export declare function registerHarnessCommands(program: Command): void;
|
package/dist/commands/harness.js
CHANGED
|
@@ -11,15 +11,82 @@
|
|
|
11
11
|
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import { addProfile } from './profiles.js';
|
|
15
|
-
import { listProfiles, readProfile, deleteProfile, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, getProfilePath, } from '../lib/profiles.js';
|
|
14
|
+
import { addProfile, ensureProviderToken } from './profiles.js';
|
|
15
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, profileFromHostModel, authEnvKeyForHost, getProfilePath, validateProfileName, } from '../lib/profiles.js';
|
|
16
16
|
import { listPresets } from '../lib/profiles-presets.js';
|
|
17
|
-
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
17
|
+
import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
|
|
18
18
|
/** Short capability summary for a native harness — its supported run modes. */
|
|
19
19
|
function nativeModes(id) {
|
|
20
20
|
const modes = AGENTS[id]?.capabilities?.modes ?? [];
|
|
21
21
|
return modes.length ? modes.join('/') : '-';
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Print one custom harness. Shared by `agents harness view <name>` and by
|
|
25
|
+
* `agents view <name>` — a custom harness resolves as an agent type there, so
|
|
26
|
+
* both entry points must describe it identically.
|
|
27
|
+
*/
|
|
28
|
+
export function renderHarnessDetail(name) {
|
|
29
|
+
const p = readProfile(name);
|
|
30
|
+
console.log(chalk.bold(profileLabel(p)) + chalk.gray(' (custom harness)'));
|
|
31
|
+
if (p.description)
|
|
32
|
+
console.log(chalk.gray(p.description));
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(`Host: ${profileHostLabel(p)}`);
|
|
35
|
+
console.log(`Model: ${profileModelLabel(p)}`);
|
|
36
|
+
if (p.fallback_model)
|
|
37
|
+
console.log(`Fallback: ${p.fallback_model}`);
|
|
38
|
+
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
39
|
+
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
40
|
+
if (p.forkedFrom)
|
|
41
|
+
console.log(`Forked: from ${p.forkedFrom}`);
|
|
42
|
+
console.log(chalk.gray(getProfilePath(p.name)));
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.gray(`Run: agents run ${p.name} "hello"`));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the new harness for `agents harness fork <source> <name>`.
|
|
48
|
+
*
|
|
49
|
+
* Two sources, one verb: an existing custom harness is copied and overridden;
|
|
50
|
+
* a native agent id is turned into a harness pinned to `--model` on that host.
|
|
51
|
+
* Forking a native harness therefore requires `--model` — there is nothing to
|
|
52
|
+
* copy a model from.
|
|
53
|
+
*/
|
|
54
|
+
export function buildFork(source, name, opts) {
|
|
55
|
+
if (profileExists(source)) {
|
|
56
|
+
return forkProfile(readProfile(source), name, {
|
|
57
|
+
model: opts.model,
|
|
58
|
+
baseUrl: opts.baseUrl,
|
|
59
|
+
provider: opts.authProvider,
|
|
60
|
+
version: opts.version,
|
|
61
|
+
label: opts.label,
|
|
62
|
+
description: opts.description,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const host = resolveAgentName(source);
|
|
66
|
+
if (!host) {
|
|
67
|
+
throw new Error(`No harness or agent named '${source}'.\n` +
|
|
68
|
+
`Fork from a custom harness (agents harness list) or a native one: ${ALL_AGENT_IDS.join(', ')}.`);
|
|
69
|
+
}
|
|
70
|
+
if (!opts.model) {
|
|
71
|
+
throw new Error(`--model <id> is required when forking the native '${host}' harness (there is no model to inherit).`);
|
|
72
|
+
}
|
|
73
|
+
return profileFromHostModel(name, host, opts.model, {
|
|
74
|
+
version: opts.version,
|
|
75
|
+
baseUrl: opts.baseUrl,
|
|
76
|
+
provider: opts.authProvider,
|
|
77
|
+
authEnvVar: opts.authProvider ? authEnvKeyForHostOrThrow(host) : undefined,
|
|
78
|
+
label: opts.label,
|
|
79
|
+
description: opts.description ?? `Forked from ${host}: ${opts.model}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Auth env var for a host, as a hard error when the host declares none. */
|
|
83
|
+
function authEnvKeyForHostOrThrow(host) {
|
|
84
|
+
const key = authEnvKeyForHost(host);
|
|
85
|
+
if (!key) {
|
|
86
|
+
throw new Error(`--auth-provider is set but host '${host}' has no known auth env var; it manages its own login.`);
|
|
87
|
+
}
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
23
90
|
export function registerHarnessCommands(program) {
|
|
24
91
|
const cmd = program
|
|
25
92
|
.command('harness')
|
|
@@ -30,11 +97,18 @@ A custom harness pins a host CLI (opencode, claude, codex, grok, antigravity, ..
|
|
|
30
97
|
model and gives it a name. 'agents run <name>' then behaves like a native agent
|
|
31
98
|
type, and 'agents repo push user' syncs it to every device.
|
|
32
99
|
|
|
100
|
+
A custom harness is its own agent type in 'agents view' — its own block beside Claude
|
|
101
|
+
and Codex, not a row indented under the host CLI that executes it.
|
|
102
|
+
|
|
33
103
|
Examples:
|
|
34
104
|
# Meta Muse Spark 1.1 through OpenCode, called 'spark'
|
|
35
105
|
agents harness add spark --host opencode --model meta/muse-spark-1.1
|
|
36
106
|
agents run spark "refactor api/handlers/checkout.py"
|
|
37
107
|
|
|
108
|
+
# Fork a native harness, or copy one of your own and swap the model
|
|
109
|
+
agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter
|
|
110
|
+
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
111
|
+
|
|
38
112
|
# Per-run model override still wins
|
|
39
113
|
agents run spark --model opencode/big-pickle "quick pass"
|
|
40
114
|
|
|
@@ -66,6 +140,48 @@ Examples:
|
|
|
66
140
|
process.exit(1);
|
|
67
141
|
}
|
|
68
142
|
});
|
|
143
|
+
cmd
|
|
144
|
+
.command('fork <source> <name>')
|
|
145
|
+
.description('Fork a native harness (claude, opencode, ...) or an existing custom one into a new named harness.')
|
|
146
|
+
.option('--model <id>', 'Model to pin on the fork (required when forking a native harness)')
|
|
147
|
+
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
148
|
+
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider')
|
|
149
|
+
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
150
|
+
.option('--label <text>', 'Human-facing name shown by `agents view` (defaults to <name>)')
|
|
151
|
+
.option('--description <text>', 'One-line description')
|
|
152
|
+
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
153
|
+
.option('--force', 'Overwrite an existing harness with the same name')
|
|
154
|
+
.addHelpText('after', `
|
|
155
|
+
Examples:
|
|
156
|
+
# Fork OpenCode into a harness pinned to a DeepSeek model on OpenRouter
|
|
157
|
+
agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter
|
|
158
|
+
|
|
159
|
+
# Fork Claude Code onto a private gateway
|
|
160
|
+
agents harness fork claude corp --model gpt-x --base-url https://gw.corp/v1 --auth-provider corp
|
|
161
|
+
|
|
162
|
+
# Copy an existing harness and swap only the model
|
|
163
|
+
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
164
|
+
`)
|
|
165
|
+
.action(async (source, name, opts) => {
|
|
166
|
+
try {
|
|
167
|
+
validateProfileName(name);
|
|
168
|
+
if (profileExists(name) && !opts.force) {
|
|
169
|
+
throw new Error(`Harness '${name}' already exists. Use --force to overwrite.`);
|
|
170
|
+
}
|
|
171
|
+
// Build first so a bad source/flag combination fails before prompting
|
|
172
|
+
// for a key the user would then have stored for nothing.
|
|
173
|
+
const forked = buildFork(source, name, opts);
|
|
174
|
+
if (opts.authProvider)
|
|
175
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
176
|
+
writeProfile(forked);
|
|
177
|
+
console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
|
|
178
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(chalk.red(err.message));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
69
185
|
cmd
|
|
70
186
|
.command('list')
|
|
71
187
|
.alias('ls')
|
|
@@ -114,16 +230,7 @@ Examples:
|
|
|
114
230
|
.description('Show one custom harness (host, model, provider, auth, path).')
|
|
115
231
|
.action((name) => {
|
|
116
232
|
try {
|
|
117
|
-
|
|
118
|
-
console.log(chalk.bold(p.name));
|
|
119
|
-
if (p.description)
|
|
120
|
-
console.log(chalk.gray(p.description));
|
|
121
|
-
console.log('');
|
|
122
|
-
console.log(`Host: ${profileHostLabel(p)}`);
|
|
123
|
-
console.log(`Model: ${profileModelLabel(p)}`);
|
|
124
|
-
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
125
|
-
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
126
|
-
console.log(chalk.gray(getProfilePath(p.name)));
|
|
233
|
+
renderHarnessDetail(name);
|
|
127
234
|
}
|
|
128
235
|
catch (err) {
|
|
129
236
|
console.error(chalk.red(err.message));
|
package/dist/commands/menubar.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* user; these commands are the manual override.
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import {
|
|
10
|
+
import { setHelpSections } from '../lib/help.js';
|
|
11
|
+
import { enableMenubarService, disableMenubarService, getMenubarStatus, runMenubarSetup, } from '../lib/menubar/install-menubar.js';
|
|
11
12
|
function notMac() {
|
|
12
13
|
if (process.platform !== 'darwin') {
|
|
13
14
|
console.log(chalk.yellow('The menu bar helper is macOS only.'));
|
|
@@ -15,10 +16,122 @@ function notMac() {
|
|
|
15
16
|
}
|
|
16
17
|
return false;
|
|
17
18
|
}
|
|
19
|
+
/** Shared status readout — `status`, bare `menubar`, and `setup --check` all end here. */
|
|
20
|
+
function printStatus(s, opts = {}) {
|
|
21
|
+
const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
|
|
22
|
+
console.log(chalk.bold('Menu bar helper\n'));
|
|
23
|
+
console.log(` running ${yn(s.running)}`);
|
|
24
|
+
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
25
|
+
if (opts.brief) {
|
|
26
|
+
console.log(chalk.gray('\n setup | enable | disable | status'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
30
|
+
console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
31
|
+
console.log(` current version ${chalk.gray(s.currentVersion)}`);
|
|
32
|
+
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
33
|
+
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
34
|
+
// Two copies of the INSTALLED bundle is the duplicate the user sees as two
|
|
35
|
+
// agents marks in the menu bar. It used to read as a healthy `running: yes`.
|
|
36
|
+
if (s.instances.length > 1) {
|
|
37
|
+
console.log(chalk.yellow(`\n ${s.instances.length} copies of the installed helper are running — that is the duplicate menu-bar icon:`));
|
|
38
|
+
for (const p of s.instances)
|
|
39
|
+
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
40
|
+
console.log(chalk.gray(' Fix it with `agents menubar setup`.'));
|
|
41
|
+
}
|
|
42
|
+
if (s.foreignInstances.length > 0) {
|
|
43
|
+
// RegisterEventHotKey is first-come, so the helper that registered the
|
|
44
|
+
// chord first owns Cmd-Shift-V/O. A process list cannot say which that
|
|
45
|
+
// was — only that a rival exists — so report the conflict, not a winner.
|
|
46
|
+
// The loser has no other symptom: its chords simply never fire.
|
|
47
|
+
const n = s.foreignInstances.length;
|
|
48
|
+
console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
|
|
49
|
+
for (const p of s.foreignInstances)
|
|
50
|
+
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
51
|
+
console.log(chalk.gray(' End them with `agents menubar setup`.'));
|
|
52
|
+
}
|
|
53
|
+
if (s.stale) {
|
|
54
|
+
console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar setup` now.'));
|
|
55
|
+
}
|
|
56
|
+
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
57
|
+
console.log(chalk.gray('\n Set it up with `agents menubar setup`.'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function printSetupResult(r) {
|
|
61
|
+
console.log(chalk.bold('Menu bar setup\n'));
|
|
62
|
+
for (const step of r.steps) {
|
|
63
|
+
const mark = step.outcome === 'failed' ? chalk.red('✗')
|
|
64
|
+
: step.outcome === 'changed' ? chalk.green('+') : chalk.green('✓');
|
|
65
|
+
console.log(` ${mark} ${step.name.padEnd(15)} ${chalk.gray(step.detail)}`);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
if (r.configured) {
|
|
69
|
+
console.log(chalk.green('Menu bar configured.') + chalk.gray(' One agents mark, started at login.'));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(chalk.red('Menu bar not fully configured.') + chalk.gray(' See the failed step above.'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
18
75
|
export function registerMenubarCommands(program) {
|
|
19
76
|
const menubar = program
|
|
20
77
|
.command('menubar')
|
|
21
78
|
.description('Manage the macOS menu-bar helper (running sessions, agents awaiting input, routines)');
|
|
79
|
+
// `setup` is the one command that gets a machine to the intended state:
|
|
80
|
+
// exactly one status item, started at login. `enable` stays the narrow
|
|
81
|
+
// install+start; setup adds duplicate cleanup and verifies the end state.
|
|
82
|
+
const setup = menubar
|
|
83
|
+
.command('setup')
|
|
84
|
+
.description('Configure the menu bar end-to-end: one instance, started at login')
|
|
85
|
+
.option('--check', 'Report the current state, change nothing')
|
|
86
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
87
|
+
.action((options) => {
|
|
88
|
+
if (options.check) {
|
|
89
|
+
const s = getMenubarStatus();
|
|
90
|
+
if (options.json) {
|
|
91
|
+
process.stdout.write(JSON.stringify(s) + '\n');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (notMac())
|
|
95
|
+
return;
|
|
96
|
+
printStatus(s);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!options.json && notMac())
|
|
100
|
+
return;
|
|
101
|
+
const r = runMenubarSetup();
|
|
102
|
+
if (options.json) {
|
|
103
|
+
process.stdout.write(JSON.stringify(r) + '\n');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
printSetupResult(r);
|
|
107
|
+
}
|
|
108
|
+
if (!r.configured)
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
});
|
|
111
|
+
setHelpSections(setup, {
|
|
112
|
+
examples: `
|
|
113
|
+
# Configure the menu bar end-to-end (idempotent — safe to re-run)
|
|
114
|
+
agents menubar setup
|
|
115
|
+
|
|
116
|
+
# Two agents marks in the menu bar? This ends the duplicate.
|
|
117
|
+
agents menubar setup
|
|
118
|
+
|
|
119
|
+
# See the current state without changing anything
|
|
120
|
+
agents menubar setup --check
|
|
121
|
+
`,
|
|
122
|
+
notes: `
|
|
123
|
+
Configures, in order: every running helper ended, the helper bundle at
|
|
124
|
+
~/Library/Application Support/agents-cli, its code signature, the launchd
|
|
125
|
+
login item (com.phnx-labs.agents-menubar — RunAtLoad + KeepAlive), then
|
|
126
|
+
verifies exactly one helper came back up.
|
|
127
|
+
|
|
128
|
+
Every running helper is ended and launchd restarts one, so the survivor is
|
|
129
|
+
always the login-managed copy. Exits nonzero if it cannot reach that state.
|
|
130
|
+
|
|
131
|
+
Setup clears a previous \`agents menubar disable\`. To turn the menu bar off
|
|
132
|
+
again, run \`agents menubar disable\`.
|
|
133
|
+
`,
|
|
134
|
+
});
|
|
22
135
|
menubar
|
|
23
136
|
.command('enable')
|
|
24
137
|
.description('Install and start the menu-bar helper (launches at login)')
|
|
@@ -40,7 +153,7 @@ export function registerMenubarCommands(program) {
|
|
|
40
153
|
if (notMac())
|
|
41
154
|
return;
|
|
42
155
|
disableMenubarService();
|
|
43
|
-
console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar
|
|
156
|
+
console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar setup`.'));
|
|
44
157
|
});
|
|
45
158
|
menubar
|
|
46
159
|
.command('status')
|
|
@@ -56,33 +169,7 @@ export function registerMenubarCommands(program) {
|
|
|
56
169
|
console.log(chalk.yellow('The menu bar helper is macOS only.'));
|
|
57
170
|
return;
|
|
58
171
|
}
|
|
59
|
-
|
|
60
|
-
console.log(chalk.bold('Menu bar helper\n'));
|
|
61
|
-
console.log(` running ${yn(s.running)}`);
|
|
62
|
-
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
63
|
-
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
64
|
-
console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
65
|
-
console.log(` current version ${chalk.gray(s.currentVersion)}`);
|
|
66
|
-
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
67
|
-
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
68
|
-
if (s.foreignInstances.length > 0) {
|
|
69
|
-
// RegisterEventHotKey is first-come, so the helper that registered the
|
|
70
|
-
// chord first owns Cmd-Shift-V/O. A process list cannot say which that
|
|
71
|
-
// was — only that a rival exists — so report the conflict, not a
|
|
72
|
-
// winner. The loser
|
|
73
|
-
// has no other symptom: its chords simply never fire.
|
|
74
|
-
const n = s.foreignInstances.length;
|
|
75
|
-
console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
|
|
76
|
-
for (const p of s.foreignInstances)
|
|
77
|
-
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
78
|
-
console.log(chalk.gray(' End it, then `agents menubar enable` to restart the installed helper.'));
|
|
79
|
-
}
|
|
80
|
-
if (s.stale) {
|
|
81
|
-
console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar enable` now.'));
|
|
82
|
-
}
|
|
83
|
-
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
84
|
-
console.log(chalk.gray('\n Enable it with `agents menubar enable`.'));
|
|
85
|
-
}
|
|
172
|
+
printStatus(s);
|
|
86
173
|
});
|
|
87
174
|
// Bare `agents menubar` -> status.
|
|
88
175
|
menubar.action(() => {
|
|
@@ -91,10 +178,6 @@ export function registerMenubarCommands(program) {
|
|
|
91
178
|
console.log(chalk.yellow('The menu bar helper is macOS only.'));
|
|
92
179
|
return;
|
|
93
180
|
}
|
|
94
|
-
|
|
95
|
-
console.log(chalk.bold('Menu bar helper\n'));
|
|
96
|
-
console.log(` running ${yn(s.running)}`);
|
|
97
|
-
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
98
|
-
console.log(chalk.gray('\n enable | disable | status'));
|
|
181
|
+
printStatus(s, { brief: true });
|
|
99
182
|
});
|
|
100
183
|
}
|
|
@@ -15,6 +15,9 @@ 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
|
+
/** Read all available data from stdin synchronously, trimmed. */
|
|
19
|
+
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
20
|
+
export declare function ensureProviderToken(provider: string, signupUrl?: string, fromStdin?: boolean): Promise<void>;
|
|
18
21
|
/** Options accepted by {@link addProfile} — shared by `agents profiles add` and `agents harness add`. */
|
|
19
22
|
export interface AddProfileOptions {
|
|
20
23
|
preset?: string;
|
|
@@ -47,7 +47,7 @@ async function promptForSecret(message) {
|
|
|
47
47
|
}
|
|
48
48
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
49
49
|
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
50
|
-
async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
50
|
+
export async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
51
51
|
const item = keychainItemName(provider);
|
|
52
52
|
if (hasKeychainToken(item)) {
|
|
53
53
|
return;
|
|
@@ -14,10 +14,29 @@ import type { DeviceRegistry } from '../lib/devices/registry.js';
|
|
|
14
14
|
* callers drop it straight into a status line.
|
|
15
15
|
*/
|
|
16
16
|
export declare function formatRunDuration(startedAt: string, completedAt: string | null): string;
|
|
17
|
+
/**
|
|
18
|
+
* The last run THIS device can speak for.
|
|
19
|
+
*
|
|
20
|
+
* A run record is written by whichever daemon fired the routine, into that
|
|
21
|
+
* machine's own runs dir — records carry no device attribution, so a record
|
|
22
|
+
* found here only ever describes this device's history. When a routine is
|
|
23
|
+
* pinned away from this machine (`devices:` excludes it) any local record is a
|
|
24
|
+
* leftover from before the pin, and reporting it as the routine's status paints
|
|
25
|
+
* another device's healthy routine red. Report nothing instead; the local
|
|
26
|
+
* history stays readable via `agents routines runs <name>`, and the owning
|
|
27
|
+
* device's status via `agents routines list --device <name>`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function localLatestRun(job: JobConfig): RunMeta | null;
|
|
17
30
|
export interface RoutineListGroup {
|
|
18
31
|
key: string;
|
|
19
32
|
title: string;
|
|
20
33
|
jobs: JobConfig[];
|
|
34
|
+
/**
|
|
35
|
+
* Whether this device's run records describe the group. False for a
|
|
36
|
+
* `Device: <peer>` group — the rows are the same routine seen from a machine
|
|
37
|
+
* that does not fire it there, so Last Status is not ours to report.
|
|
38
|
+
*/
|
|
39
|
+
local: boolean;
|
|
21
40
|
}
|
|
22
41
|
export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: DeviceRegistry, self?: string): RoutineListGroup[];
|
|
23
42
|
export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
|
|
@@ -70,6 +70,8 @@ function fireConditionLabel(job) {
|
|
|
70
70
|
job.trigger.action ? `action=${job.trigger.action}` : null,
|
|
71
71
|
job.trigger.teamKey ? `team=${job.trigger.teamKey}` : null,
|
|
72
72
|
job.trigger.label ? `label=${job.trigger.label}` : null,
|
|
73
|
+
job.trigger.stateTo ? `stateTo=${job.trigger.stateTo}` : null,
|
|
74
|
+
job.trigger.stateFrom ? `stateFrom=${job.trigger.stateFrom}` : null,
|
|
73
75
|
].filter(Boolean).join(', ');
|
|
74
76
|
return `on linear:${job.trigger.event}${filters ? ` (${filters})` : ''}`;
|
|
75
77
|
}
|
|
@@ -136,6 +138,21 @@ function deviceLabel(job, width) {
|
|
|
136
138
|
: raw;
|
|
137
139
|
return { raw, display, dim: full.length === 0 || !jobRunsOnThisDevice(job) };
|
|
138
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The last run THIS device can speak for.
|
|
143
|
+
*
|
|
144
|
+
* A run record is written by whichever daemon fired the routine, into that
|
|
145
|
+
* machine's own runs dir — records carry no device attribution, so a record
|
|
146
|
+
* found here only ever describes this device's history. When a routine is
|
|
147
|
+
* pinned away from this machine (`devices:` excludes it) any local record is a
|
|
148
|
+
* leftover from before the pin, and reporting it as the routine's status paints
|
|
149
|
+
* another device's healthy routine red. Report nothing instead; the local
|
|
150
|
+
* history stays readable via `agents routines runs <name>`, and the owning
|
|
151
|
+
* device's status via `agents routines list --device <name>`.
|
|
152
|
+
*/
|
|
153
|
+
export function localLatestRun(job) {
|
|
154
|
+
return jobRunsOnThisDevice(job) ? getLatestRun(job.name) : null;
|
|
155
|
+
}
|
|
139
156
|
export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
140
157
|
const groups = new Map();
|
|
141
158
|
const add = (key, title, job) => {
|
|
@@ -144,7 +161,9 @@ export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
|
144
161
|
existing.jobs.push(job);
|
|
145
162
|
return;
|
|
146
163
|
}
|
|
147
|
-
|
|
164
|
+
// `device:` is the only group that describes a machine other than this one,
|
|
165
|
+
// so it is the only one whose rows this device cannot report a status for.
|
|
166
|
+
groups.set(key, { key, title, jobs: [job], local: !key.startsWith('device:') });
|
|
148
167
|
};
|
|
149
168
|
for (const job of jobs) {
|
|
150
169
|
const strategy = resolveHostStrategy(job);
|
|
@@ -191,7 +210,7 @@ export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
|
191
210
|
};
|
|
192
211
|
return [...groups.values()].sort((a, b) => order(a) - order(b) || a.title.localeCompare(b.title));
|
|
193
212
|
}
|
|
194
|
-
function renderRoutineRows({ jobs, scheduler, overdueSet, link, now }) {
|
|
213
|
+
function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = true }) {
|
|
195
214
|
const NAME_W = 24;
|
|
196
215
|
const AGENT_W = 10;
|
|
197
216
|
const REPO_W = REPO_DISPLAY_MAX;
|
|
@@ -205,7 +224,7 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now }) {
|
|
|
205
224
|
for (const job of jobs) {
|
|
206
225
|
const nextStr = nextRunLabel(job, scheduler, now);
|
|
207
226
|
const schedStr = scheduleLabel(job);
|
|
208
|
-
const latestRun =
|
|
227
|
+
const latestRun = local ? localLatestRun(job) : null;
|
|
209
228
|
const lastStatus = latestRun?.status || '-';
|
|
210
229
|
const sourceRepo = job.source?.repo ?? job.repo;
|
|
211
230
|
const sourceLabel = sourceRepo
|
|
@@ -261,6 +280,10 @@ function parseRoutineTrigger(options) {
|
|
|
261
280
|
trigger.teamKey = options.teamKey;
|
|
262
281
|
if (typeof options.label === 'string')
|
|
263
282
|
trigger.label = options.label;
|
|
283
|
+
if (typeof options.stateTo === 'string')
|
|
284
|
+
trigger.stateTo = options.stateTo;
|
|
285
|
+
if (typeof options.stateFrom === 'string')
|
|
286
|
+
trigger.stateFrom = options.stateFrom;
|
|
264
287
|
return trigger;
|
|
265
288
|
}
|
|
266
289
|
throw new Error('--on source must be github or linear');
|
|
@@ -503,7 +526,7 @@ export function registerRoutinesCommands(program) {
|
|
|
503
526
|
if (options.json) {
|
|
504
527
|
const nowJson = new Date();
|
|
505
528
|
const payload = jobs.map((job) => {
|
|
506
|
-
const latestRun =
|
|
529
|
+
const latestRun = localLatestRun(job);
|
|
507
530
|
return {
|
|
508
531
|
name: job.name,
|
|
509
532
|
agent: job.agent ?? null,
|
|
@@ -556,9 +579,14 @@ export function registerRoutinesCommands(program) {
|
|
|
556
579
|
catch (err) {
|
|
557
580
|
console.error(chalk.yellow(`Could not read device registry: ${err.message}`));
|
|
558
581
|
}
|
|
559
|
-
|
|
582
|
+
const groups = groupRoutineJobsByDevice(jobs, registry);
|
|
583
|
+
for (const group of groups) {
|
|
560
584
|
console.log(chalk.bold(`\n${group.title}`));
|
|
561
|
-
renderRoutineRows({ jobs: group.jobs, scheduler, overdueSet, link, now });
|
|
585
|
+
renderRoutineRows({ jobs: group.jobs, scheduler, overdueSet, link, now, local: group.local });
|
|
586
|
+
}
|
|
587
|
+
if (groups.some((group) => !group.local)) {
|
|
588
|
+
console.log();
|
|
589
|
+
console.log(chalk.gray(' Last Status is per-device: rows under another device show "-" — read it there with: agents routines list --device <name>'));
|
|
562
590
|
}
|
|
563
591
|
}
|
|
564
592
|
if (overdueSet.size > 0) {
|
|
@@ -591,6 +619,8 @@ export function registerRoutinesCommands(program) {
|
|
|
591
619
|
.option('--action <name>', 'Webhook action filter for --on triggers (GitHub: labeled/opened; Linear: update)')
|
|
592
620
|
.option('--team-key <key>', 'Linear team key filter for --on linear:<event> (e.g. RUSH)')
|
|
593
621
|
.option('--label <name>', 'Label filter for --on triggers (GitHub label name or Linear issue label)')
|
|
622
|
+
.option('--state-to <name>', 'Linear current-state filter for --on linear:<event> (e.g. Plan)')
|
|
623
|
+
.option('--state-from <name>', 'Linear previous-state filter for --on linear:<event> (e.g. Triage)')
|
|
594
624
|
.option('--end-at <iso>', 'Stop firing on or after this ISO 8601 timestamp (e.g., "2026-12-31T23:59:00Z"); routine auto-disables.')
|
|
595
625
|
.option('--disabled', 'Create the routine but keep it paused (enable later with resume)')
|
|
596
626
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
@@ -884,12 +914,26 @@ export function registerRoutinesCommands(program) {
|
|
|
884
914
|
routinesCmd
|
|
885
915
|
.command('edit [name]')
|
|
886
916
|
.description('Open a routine in $EDITOR. Creates a new YAML template if the routine does not exist.')
|
|
887
|
-
.
|
|
917
|
+
.option('--state-to <name>', 'Update the Linear current-state filter before opening the editor')
|
|
918
|
+
.option('--state-from <name>', 'Update the Linear previous-state filter before opening the editor')
|
|
919
|
+
.action(async (name, options) => {
|
|
888
920
|
if (!name) {
|
|
889
921
|
name = await pickJob('Select job to edit', undefined, ['agents routines edit <name>']) ?? undefined;
|
|
890
922
|
if (!name)
|
|
891
923
|
return;
|
|
892
924
|
}
|
|
925
|
+
const existing = readJob(name);
|
|
926
|
+
if (existing && (options.stateTo !== undefined || options.stateFrom !== undefined)) {
|
|
927
|
+
if (!existing.trigger || existing.trigger.type !== 'linear_event') {
|
|
928
|
+
console.error(chalk.red(`'${name}' does not have a Linear trigger; --state-to/--state-from only apply to linear triggers`));
|
|
929
|
+
process.exit(1);
|
|
930
|
+
}
|
|
931
|
+
if (options.stateTo !== undefined)
|
|
932
|
+
existing.trigger.stateTo = options.stateTo || undefined;
|
|
933
|
+
if (options.stateFrom !== undefined)
|
|
934
|
+
existing.trigger.stateFrom = options.stateFrom || undefined;
|
|
935
|
+
writeJob(existing);
|
|
936
|
+
}
|
|
893
937
|
const jobPath = getJobPath(name);
|
|
894
938
|
if (!jobPath) {
|
|
895
939
|
// Job doesn't exist - create a new one
|