@voidlabs.gg/voidshift-agents 0.4.2 → 0.4.4
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/README.md +18 -6
- package/dist/config.d.ts +12 -2
- package/dist/config.js +14 -1
- package/dist/control.d.ts +4 -2
- package/dist/control.js +79 -52
- package/dist/cron.d.ts +9 -0
- package/dist/cron.js +66 -0
- package/dist/index.js +77 -44
- package/dist/launchd.d.ts +1 -0
- package/dist/launchd.js +6 -0
- package/dist/linear.d.ts +1 -1
- package/dist/linear.js +3 -3
- package/dist/manage.d.ts +1 -0
- package/dist/manage.js +35 -13
- package/dist/runner.d.ts +12 -5
- package/dist/runner.js +130 -47
- package/dist/secrets.d.ts +9 -0
- package/dist/secrets.js +55 -0
- package/dist/service.d.ts +9 -0
- package/dist/service.js +18 -0
- package/dist/state.d.ts +6 -1
- package/dist/state.js +15 -0
- package/dist/systemd.d.ts +14 -0
- package/dist/systemd.js +85 -0
- package/dist/tracker.d.ts +24 -0
- package/dist/tracker.js +36 -0
- package/dist/voidtrack.d.ts +18 -0
- package/dist/voidtrack.js +89 -0
- package/package.json +3 -5
- package/dist/keychain.d.ts +0 -3
- package/dist/keychain.js +0 -19
package/dist/index.js
CHANGED
|
@@ -4,12 +4,16 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { AGENTS, STEPS, TEMPLATES, isProducer, agentsDir, describeSteps, describeTriggers, describeWorkflow, stepEnabled, visibleAgents, readAgentsConfig, writeAgentsConfig, DEFAULTS } from "./config.js";
|
|
7
|
+
import { AGENTS, STEPS, TEMPLATES, isProducer, agentsDir, describeSteps, describeTrigger, describeTriggers, describeWorkflow, stepEnabled, visibleAgents, readAgentsConfig, writeAgentsConfig, DEFAULTS } from "./config.js";
|
|
8
8
|
import { controlChecks, controlCommands, controlUsage } from "./control.js";
|
|
9
|
-
import { looksLikeLinearKey, readLinearKey, storeLinearKey } from "./
|
|
10
|
-
import {
|
|
9
|
+
import { keyStore, looksLikeLinearKey, readLinearKey, readSecret, storeLinearKey, storeSecret } from "./secrets.js";
|
|
10
|
+
import { TRACKERS } from "./tracker.js";
|
|
11
|
+
import { createVoidTrack } from "./voidtrack.js";
|
|
12
|
+
import { parseCron } from "./cron.js";
|
|
13
|
+
import { trackersInUse } from "./control.js";
|
|
14
|
+
import { runnerService } from "./service.js";
|
|
11
15
|
import { createLinear } from "./linear.js";
|
|
12
|
-
import { duplicateTriggerWarnings, moveKey, parseTrigger, renameKey, sameTrigger, validateConfig, validateWorkflow, workflowsUsing } from "./manage.js";
|
|
16
|
+
import { TRIGGER_FORMS, duplicateTriggerWarnings, moveKey, parseTrigger, renameKey, sameTrigger, validateConfig, validateWorkflow, workflowsUsing } from "./manage.js";
|
|
13
17
|
import { parseStep, resolveName, stepNames } from "./names.js";
|
|
14
18
|
import { findRepos } from "./repos.js";
|
|
15
19
|
import { defaultBranch, sh } from "./git.js";
|
|
@@ -81,30 +85,32 @@ async function chooseHomes(ui, configDir, saved) {
|
|
|
81
85
|
return homes;
|
|
82
86
|
}
|
|
83
87
|
// Checks the key against Linear, and only then stores it.
|
|
84
|
-
async function verifyAndStore(key, info, warn) {
|
|
88
|
+
async function verifyAndStore(configDir, key, info, warn, kind = 'linear') {
|
|
89
|
+
const name = TRACKERS[kind].name;
|
|
85
90
|
try {
|
|
86
|
-
const me = await createLinear(key).viewer();
|
|
87
|
-
if (!storeLinearKey(key)) {
|
|
88
|
-
warn(
|
|
91
|
+
const me = await (kind === 'voidtrack' ? createVoidTrack(key) : createLinear(key)).viewer();
|
|
92
|
+
if (!(kind === 'voidtrack' ? storeSecret(configDir, 'voidtrack', key) : storeLinearKey(configDir, key))) {
|
|
93
|
+
warn(`${name} accepted the key but it could not be stored in ${keyStore()}.`);
|
|
89
94
|
return null;
|
|
90
95
|
}
|
|
91
|
-
info(
|
|
96
|
+
info(`${name} connected as ${me.name}. Key stored in ${keyStore()}.`);
|
|
92
97
|
return me.name;
|
|
93
98
|
}
|
|
94
99
|
catch (err) {
|
|
95
|
-
warn(
|
|
100
|
+
warn(`${name} did not accept that key: ${err.message}. Nothing was stored.`);
|
|
96
101
|
return null;
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
// Asks for the key, checks it against Linear, and only then stores it.
|
|
100
|
-
async function setupKey(ui) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
async function setupKey(ui, configDir, kind = 'linear') {
|
|
106
|
+
const { name, keyHint } = TRACKERS[kind];
|
|
107
|
+
ui.info(`Agents need a ${name} API key (${keyHint}).`);
|
|
108
|
+
const key = await ui.secret(`Paste your ${name} API key`, (v) => kind === 'linear' ? (looksLikeLinearKey(v.trim()) ? undefined : 'Linear keys start with lin_api_') : v.trim() ? undefined : 'Required');
|
|
109
|
+
return verifyAndStore(configDir, key.trim(), ui.info, ui.warn, kind);
|
|
104
110
|
}
|
|
105
|
-
async function currentLinearUser() {
|
|
111
|
+
async function currentLinearUser(configDir) {
|
|
106
112
|
try {
|
|
107
|
-
return readLinearKey() ? (await linear().viewer()).name : null;
|
|
113
|
+
return readLinearKey(configDir) ? (await linear(configDir).viewer()).name : null;
|
|
108
114
|
}
|
|
109
115
|
catch {
|
|
110
116
|
return null;
|
|
@@ -181,8 +187,8 @@ function syncLinks(chosen, all, enabled, log) {
|
|
|
181
187
|
log(`Agents linked in ${homeLabel(home)}: ${linked} linked${skipped ? `, ${skipped} skipped (something else is already there)` : ''}`);
|
|
182
188
|
}
|
|
183
189
|
}
|
|
184
|
-
function linear() {
|
|
185
|
-
const key = readLinearKey();
|
|
190
|
+
function linear(configDir) {
|
|
191
|
+
const key = readLinearKey(configDir);
|
|
186
192
|
if (!key)
|
|
187
193
|
throw new Error('No Linear API key. Run vsf agents key');
|
|
188
194
|
return createLinear(key);
|
|
@@ -277,11 +283,22 @@ async function promptWorkflow(ui, config, teams, current = {}, edit = false) {
|
|
|
277
283
|
if (!triggers) {
|
|
278
284
|
triggers = [];
|
|
279
285
|
do {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
:
|
|
286
|
+
const source = await ui.select(triggers.length ? 'Also start it when...' : 'Starts when...', [
|
|
287
|
+
{ value: 'linear', label: 'A Linear card gets a label' },
|
|
288
|
+
{ value: 'voidtrack', label: 'A VoidTrack card gets a label' },
|
|
289
|
+
{ value: 'cron', label: 'On a schedule', hint: 'runs on a standing task, no card' },
|
|
290
|
+
], 'linear');
|
|
291
|
+
if (source === 'cron') {
|
|
292
|
+
const schedule = (await ui.text('Cron schedule (minute hour day month weekday)', { initial: '0 5 * * 1', validate: (v) => (parseCron(v) ? undefined : 'Five fields, like 0 5 * * 1') })).trim();
|
|
293
|
+
const task = (await ui.text('What should each run work on?', { placeholder: 'Update the dependencies and fix what breaks', validate: (v) => (v.trim() ? undefined : 'Required') })).trim();
|
|
294
|
+
triggers.push({ source: 'cron', schedule, task });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const team = source === 'linear' && teams.length
|
|
298
|
+
? await ui.select('A card in which Linear team...', teams.map((t) => ({ value: t.key, label: `${t.key} ${t.name}`, hint: t.mine ? 'your team' : undefined })), myTeams.length === 1 ? myTeams[0].key : undefined)
|
|
299
|
+
: (await ui.text(`A card in which ${TRACKERS[source].name} team key...`, { validate: (v) => (v.trim() ? undefined : 'Required') })).trim().toUpperCase();
|
|
283
300
|
const label = (await ui.text('...gets which label?', { initial: 'vs-plan', validate: (v) => (v.trim() ? undefined : 'Required') })).trim();
|
|
284
|
-
triggers.push({ source
|
|
301
|
+
triggers.push({ source, team, label });
|
|
285
302
|
} while (await ui.confirm('Add another way to start this workflow?', false));
|
|
286
303
|
}
|
|
287
304
|
let steps = w.steps?.length && (await keep('steps', describeSteps(w.steps))) ? w.steps : undefined;
|
|
@@ -341,7 +358,7 @@ async function setupWorkflows(ui, config, teams, log) {
|
|
|
341
358
|
// One entry per command, in the order help lists them.
|
|
342
359
|
const USAGE = [
|
|
343
360
|
['workflows [--json]', 'List workflows in the order they match'],
|
|
344
|
-
['workflow add [NAME] [--trigger TEAM:LABEL]... [--steps dev,qa | --template build] [--project P] [--rounds N]', 'Create a workflow; prompts for what is missing'],
|
|
361
|
+
['workflow add [NAME] [--trigger TEAM:LABEL]... [--steps dev,qa | --template build] [--project P] [--rounds N]', 'Create a workflow; prompts for what is missing. A trigger is TEAM:LABEL (Linear), voidtrack:TEAM:LABEL or "cron:SCHEDULE:TASK"'],
|
|
345
362
|
['workflow show NAME [--json]', 'Show one workflow'],
|
|
346
363
|
['workflow edit NAME [--trigger T:L]... [--add-trigger T:L] [--remove-trigger T:L] [--steps S | --template T] [--project P] [--rounds N]', 'Change a workflow; no flags asks'],
|
|
347
364
|
['workflow rename OLD NEW', 'Rename a workflow, keeping its place'],
|
|
@@ -356,7 +373,7 @@ const USAGE = [
|
|
|
356
373
|
['homes [--json]', 'Show where the agents are linked'],
|
|
357
374
|
['link DIR [--claude | --codex]', 'Link the agents into a config folder'],
|
|
358
375
|
['unlink DIR', 'Remove the agents from a config folder'],
|
|
359
|
-
['key', 'Store or replace the Linear API key (reads stdin without a terminal)'],
|
|
376
|
+
['key [--voidtrack]', 'Store or replace the Linear or VoidTrack API key (reads stdin without a terminal)'],
|
|
360
377
|
['help [COMMAND]', 'Show this list, or one command'],
|
|
361
378
|
];
|
|
362
379
|
const usageOf = (name) => USAGE.filter(([c]) => c === name || c.startsWith(`${name} `))
|
|
@@ -441,7 +458,7 @@ function readWorkflowFlags(v, config) {
|
|
|
441
458
|
for (const t of v.trigger) {
|
|
442
459
|
const parsed = parseTrigger(t);
|
|
443
460
|
if (!parsed)
|
|
444
|
-
return { error: `Bad trigger ${t}. Use
|
|
461
|
+
return { error: `Bad trigger ${t}. Use ${TRIGGER_FORMS}.`, code: 2 };
|
|
445
462
|
flags.triggers.push(parsed);
|
|
446
463
|
}
|
|
447
464
|
}
|
|
@@ -489,7 +506,7 @@ function pickWorkflow(config, input, ctx) {
|
|
|
489
506
|
}
|
|
490
507
|
async function teamsOrNull(ctx) {
|
|
491
508
|
try {
|
|
492
|
-
return await linear().teams();
|
|
509
|
+
return await linear(ctx.configDir).teams();
|
|
493
510
|
}
|
|
494
511
|
catch (err) {
|
|
495
512
|
ctx.log(`Could not list Linear teams: ${err.message}`);
|
|
@@ -590,7 +607,7 @@ const workflowSubs = {
|
|
|
590
607
|
else {
|
|
591
608
|
ctx.log(`Workflow ${name}: ${describeWorkflow(w)}`);
|
|
592
609
|
for (const t of w.triggers)
|
|
593
|
-
ctx.log(`Trigger: ${t.
|
|
610
|
+
ctx.log(`Trigger: ${describeTrigger(t)}${t.source === 'cron' ? `, task: ${t.task}` : ''}`);
|
|
594
611
|
ctx.log(`Steps: ${describeSteps(w.steps)}`);
|
|
595
612
|
ctx.log(`Project: ${w.project}`);
|
|
596
613
|
ctx.log(`Rounds: ${w.maxRounds}`);
|
|
@@ -637,7 +654,7 @@ const workflowSubs = {
|
|
|
637
654
|
for (const t of (v['add-trigger'] ?? [])) {
|
|
638
655
|
const parsed = parseTrigger(t);
|
|
639
656
|
if (!parsed) {
|
|
640
|
-
ctx.log(`Bad trigger ${t}. Use
|
|
657
|
+
ctx.log(`Bad trigger ${t}. Use ${TRIGGER_FORMS}.`);
|
|
641
658
|
return 2;
|
|
642
659
|
}
|
|
643
660
|
if (!next.triggers.some((x) => sameTrigger(x, parsed)))
|
|
@@ -1045,22 +1062,26 @@ const agents = {
|
|
|
1045
1062
|
config.models = { ...DEFAULTS.models };
|
|
1046
1063
|
}
|
|
1047
1064
|
config.engine = engine;
|
|
1048
|
-
const name = await currentLinearUser();
|
|
1065
|
+
const name = await currentLinearUser(ctx.configDir);
|
|
1049
1066
|
if (!name || !(await ui.confirm(`Keep using Linear as ${name}?`))) {
|
|
1050
|
-
if (!(await setupKey(ui)))
|
|
1067
|
+
if (!(await setupKey(ui, ctx.configDir)))
|
|
1051
1068
|
throw new Error('Linear is not connected. Run vsf setup agents to retry.');
|
|
1052
1069
|
}
|
|
1053
1070
|
await setupProjects(ui, config, ctx.log);
|
|
1054
|
-
await setupWorkflows(ui, config, await linear().teams(), ctx.log);
|
|
1071
|
+
await setupWorkflows(ui, config, await linear(ctx.configDir).teams(), ctx.log);
|
|
1055
1072
|
writeAgentsConfig(ctx.configDir, config);
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1073
|
+
if (trackersInUse(config).includes('voidtrack') && !readSecret(ctx.configDir, 'voidtrack')) {
|
|
1074
|
+
if (!(await setupKey(ui, ctx.configDir, 'voidtrack')))
|
|
1075
|
+
ctx.log('VoidTrack is not connected yet. Run vsf agents key --voidtrack.');
|
|
1076
|
+
}
|
|
1077
|
+
if (Object.keys(config.workflows).length && (await ui.confirm('Start the background runner now? Every minute it checks for cards and schedules that start your workflows.'))) {
|
|
1078
|
+
const ok = runnerService().start(ctx.home, [...ctx.selfCommand, 'agents', 'tick'], join(agentsDir(ctx.configDir), 'runner.log'));
|
|
1058
1079
|
ctx.log(ok ? 'Runner started.' : 'Could not start the runner. Try vsf agents start.');
|
|
1059
1080
|
}
|
|
1060
1081
|
ctx.log('In Claude Code or Codex, connect Linear too, then use /vs-plan, /vs-plan-review and /vs-publish on a card.');
|
|
1061
1082
|
},
|
|
1062
1083
|
async uninstall(ctx) {
|
|
1063
|
-
|
|
1084
|
+
runnerService().stop(ctx.home);
|
|
1064
1085
|
syncLinks([], candidateHomes(ctx.configDir, readAgentsConfig(ctx.configDir).homes), [], ctx.log);
|
|
1065
1086
|
ctx.log('Agents unlinked and the runner stopped. Config and the Linear key were kept.');
|
|
1066
1087
|
},
|
|
@@ -1077,12 +1098,19 @@ const agents = {
|
|
|
1077
1098
|
? { name: `agents in ${homeLabel(h)}`, status: 'ok', detail: `${wanted.length} linked` }
|
|
1078
1099
|
: { name: `agents in ${homeLabel(h)}`, status: 'warn', detail: `missing ${missing.join(', ')}`, fix: `vsf agents link ${tilde(h.dir)}` });
|
|
1079
1100
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1101
|
+
const inUse = trackersInUse(saved);
|
|
1102
|
+
for (const kind of inUse.length ? inUse : ['linear']) {
|
|
1103
|
+
const { name } = TRACKERS[kind];
|
|
1104
|
+
try {
|
|
1105
|
+
const key = readSecret(ctx.configDir, kind);
|
|
1106
|
+
if (!key)
|
|
1107
|
+
throw new Error(`No ${name} API key`);
|
|
1108
|
+
const me = await (kind === 'voidtrack' ? createVoidTrack(key) : createLinear(key)).viewer();
|
|
1109
|
+
checks.push({ name, status: 'ok', detail: `connected as ${me.name}` });
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
checks.push({ name, status: 'fail', detail: err.message, fix: `vsf agents key${kind === 'linear' ? '' : ` --${kind}`}` });
|
|
1113
|
+
}
|
|
1086
1114
|
}
|
|
1087
1115
|
const workflows = Object.entries(saved.workflows);
|
|
1088
1116
|
const broken = workflows.filter(([, c]) => !saved.projects[c.project] || !isRepo(saved.projects[c.project].path)).map(([n]) => n);
|
|
@@ -1134,21 +1162,26 @@ const agents = {
|
|
|
1134
1162
|
link: homeCommands.link,
|
|
1135
1163
|
unlink: homeCommands.unlink,
|
|
1136
1164
|
async key(args, ctx) {
|
|
1137
|
-
const p = begin('key', args, {}, ctx);
|
|
1165
|
+
const p = begin('key', args, { voidtrack: { type: 'boolean' } }, ctx);
|
|
1138
1166
|
if (typeof p === 'number')
|
|
1139
1167
|
return p;
|
|
1140
1168
|
if (p.positionals.length) {
|
|
1141
1169
|
ctx.log('Keys are not accepted on the command line. Pipe the key in instead.');
|
|
1142
1170
|
return 2;
|
|
1143
1171
|
}
|
|
1172
|
+
const kind = p.values.voidtrack ? 'voidtrack' : 'linear';
|
|
1144
1173
|
if (interactive())
|
|
1145
|
-
return (await setupKey(ctx.ui)) ? 0 : 1;
|
|
1174
|
+
return (await setupKey(ctx.ui, ctx.configDir, kind)) ? 0 : 1;
|
|
1146
1175
|
const key = readFileSync(0, 'utf8').trim();
|
|
1147
|
-
if (!looksLikeLinearKey(key)) {
|
|
1176
|
+
if (kind === 'linear' && !looksLikeLinearKey(key)) {
|
|
1148
1177
|
ctx.log('Linear keys start with lin_api_');
|
|
1149
1178
|
return 2;
|
|
1150
1179
|
}
|
|
1151
|
-
|
|
1180
|
+
if (!key) {
|
|
1181
|
+
ctx.log('No key on stdin.');
|
|
1182
|
+
return 2;
|
|
1183
|
+
}
|
|
1184
|
+
return (await verifyAndStore(ctx.configDir, key, ctx.log, ctx.log, kind)) ? 0 : 1;
|
|
1152
1185
|
},
|
|
1153
1186
|
async help(args, ctx) {
|
|
1154
1187
|
const want = args.join(' ').trim();
|
package/dist/launchd.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare function runnerEnv(): Record<string, string>;
|
|
|
5
5
|
export declare const runnerLoaded: () => boolean;
|
|
6
6
|
export declare function startRunner(home: string, program: string[], logPath: string): boolean;
|
|
7
7
|
export declare function stopRunner(home: string): void;
|
|
8
|
+
export declare function parsePlistArgs(plist: string): string[];
|
package/dist/launchd.js
CHANGED
|
@@ -66,3 +66,9 @@ export function stopRunner(home) {
|
|
|
66
66
|
launchctl(['bootout', `${domain()}/${LABEL}`]);
|
|
67
67
|
rmSync(plistPath(home), { force: true });
|
|
68
68
|
}
|
|
69
|
+
// The <string> values of a launchd plist's ProgramArguments.
|
|
70
|
+
export function parsePlistArgs(plist) {
|
|
71
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(plist)?.[1] ?? '';
|
|
72
|
+
const unxml = (s) => s.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
73
|
+
return [...block.matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unxml(m[1]));
|
|
74
|
+
}
|
package/dist/linear.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function createLinear(apiKey: string): {
|
|
|
20
20
|
name: string;
|
|
21
21
|
mine: boolean;
|
|
22
22
|
}[]>;
|
|
23
|
-
myCardsWithLabel(label: string): Promise<Card[]>;
|
|
23
|
+
myCardsWithLabel(label: string, team: string): Promise<Card[]>;
|
|
24
24
|
card(identifier: string): Promise<Card>;
|
|
25
25
|
comments(identifier: string): Promise<{
|
|
26
26
|
body: string;
|
package/dist/linear.js
CHANGED
|
@@ -79,14 +79,14 @@ export function createLinear(apiKey) {
|
|
|
79
79
|
const all = d?.teams?.nodes ?? [];
|
|
80
80
|
return all.map((t) => ({ ...t, mine: mine.has(t.key) })).sort((a, b) => Number(b.mine) - Number(a.mine));
|
|
81
81
|
},
|
|
82
|
-
// My cards carrying the trigger label.
|
|
83
|
-
async myCardsWithLabel(label) {
|
|
82
|
+
// My cards in the team carrying the trigger label.
|
|
83
|
+
async myCardsWithLabel(label, team) {
|
|
84
84
|
const d = await gql('myCards', `query MyCards($label: String!) {
|
|
85
85
|
issues(first: 20, orderBy: createdAt, filter: { assignee: { isMe: { eq: true } }, labels: { name: { eq: $label } } }) {
|
|
86
86
|
nodes { ${CARD_FIELDS} }
|
|
87
87
|
}
|
|
88
88
|
}`, { label });
|
|
89
|
-
return (d?.issues?.nodes ?? []).map(mapCard);
|
|
89
|
+
return (d?.issues?.nodes ?? []).map(mapCard).filter((c) => c.teamKey === team);
|
|
90
90
|
},
|
|
91
91
|
async card(identifier) {
|
|
92
92
|
const d = await gql('card', `query Card($id: String!) { issue(id: $id) { ${CARD_FIELDS} } }`, { id: identifier });
|
package/dist/manage.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AgentsConfig, type Trigger, type Workflow } from './config.ts';
|
|
2
2
|
export declare function validateWorkflow(config: Pick<AgentsConfig, 'enabled' | 'projects'>, workflow: Workflow): string[];
|
|
3
|
+
export declare const TRIGGER_FORMS = "TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK";
|
|
3
4
|
export declare function parseTrigger(input: string): Trigger | null;
|
|
4
5
|
export declare const sameTrigger: (a: Trigger, b: Trigger) => boolean;
|
|
5
6
|
export declare function duplicateTriggerWarnings(workflows: Record<string, Workflow>, name: string, workflow: Workflow): string[];
|
package/dist/manage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { AGENTS, STEPS } from "./config.js";
|
|
3
|
+
import { AGENTS, STEPS, describeTrigger, triggerSpec } from "./config.js";
|
|
4
|
+
import { parseCron } from "./cron.js";
|
|
4
5
|
const isInt = (v, min, max) => typeof v === 'number' && Number.isInteger(v) && v >= min && v <= max;
|
|
5
6
|
// Everything wrong with one workflow, in plain words. Empty means it is fine.
|
|
6
7
|
export function validateWorkflow(config, workflow) {
|
|
@@ -8,10 +9,20 @@ export function validateWorkflow(config, workflow) {
|
|
|
8
9
|
if (!workflow.triggers?.length)
|
|
9
10
|
problems.push('Needs at least one trigger.');
|
|
10
11
|
for (const t of workflow.triggers ?? []) {
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (t.source === 'cron') {
|
|
13
|
+
if (!parseCron(t.schedule ?? ''))
|
|
14
|
+
problems.push(`Schedule ${t.schedule} is not a five field cron schedule, like 0 5 * * 1.`);
|
|
15
|
+
if (!t.task?.trim())
|
|
16
|
+
problems.push('A schedule has no task.');
|
|
17
|
+
}
|
|
18
|
+
else if (t.source !== 'linear' && t.source !== 'voidtrack')
|
|
19
|
+
problems.push(`Unknown trigger source ${t.source}.`);
|
|
20
|
+
else {
|
|
21
|
+
if (!t.team?.trim())
|
|
22
|
+
problems.push('A trigger has no team.');
|
|
23
|
+
if (!t.label?.trim())
|
|
24
|
+
problems.push('A trigger has no label.');
|
|
25
|
+
}
|
|
15
26
|
}
|
|
16
27
|
if (!workflow.steps?.length)
|
|
17
28
|
problems.push('Needs at least one step.');
|
|
@@ -27,16 +38,27 @@ export function validateWorkflow(config, workflow) {
|
|
|
27
38
|
problems.push('Rounds must be a whole number from 1 to 9.');
|
|
28
39
|
return problems;
|
|
29
40
|
}
|
|
30
|
-
|
|
41
|
+
export const TRIGGER_FORMS = 'TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK';
|
|
42
|
+
// "ENG:vs-plan" (Linear, also "linear:ENG:vs-plan"), "voidtrack:ORG:vs-start"
|
|
43
|
+
// or "cron:0 5 * * 1:Update the dependencies" into a trigger. Teams are
|
|
44
|
+
// upper-cased; a schedule must parse.
|
|
31
45
|
export function parseTrigger(input) {
|
|
32
|
-
const
|
|
33
|
-
|
|
46
|
+
const text = input.trim();
|
|
47
|
+
// A source prefix decides the form; a bare TEAM:LABEL is Linear.
|
|
48
|
+
if (text.startsWith('cron:')) {
|
|
49
|
+
const cron = /^cron:([^:]+):([\s\S]+)$/.exec(text);
|
|
50
|
+
const schedule = cron?.[1].trim().replace(/\s+/g, ' ') ?? '';
|
|
51
|
+
const task = cron?.[2].trim() ?? '';
|
|
52
|
+
return parseCron(schedule) && task ? { source: 'cron', schedule, task } : null;
|
|
53
|
+
}
|
|
54
|
+
const m = /^(linear|voidtrack):/.test(text) ? /^(linear|voidtrack):([^:]+):(.+)$/.exec(text) : /^()([^:]+):(.+)$/.exec(text);
|
|
55
|
+
if (!m)
|
|
34
56
|
return null;
|
|
35
|
-
const team =
|
|
36
|
-
const label =
|
|
37
|
-
return team && label ? { source: 'linear', team, label } : null;
|
|
57
|
+
const team = m[2].trim().toUpperCase();
|
|
58
|
+
const label = m[3].trim();
|
|
59
|
+
return team && label ? { source: (m[1] || 'linear'), team, label } : null;
|
|
38
60
|
}
|
|
39
|
-
export const sameTrigger = (a, b) => a
|
|
61
|
+
export const sameTrigger = (a, b) => triggerSpec(a) === triggerSpec(b);
|
|
40
62
|
// Warning lines for triggers this workflow shares with another one. Only the
|
|
41
63
|
// first workflow in order starts a card, so the rest never get it.
|
|
42
64
|
export function duplicateTriggerWarnings(workflows, name, workflow) {
|
|
@@ -47,7 +69,7 @@ export function duplicateTriggerWarnings(workflows, name, workflow) {
|
|
|
47
69
|
for (const other of owners) {
|
|
48
70
|
if (other === name)
|
|
49
71
|
continue;
|
|
50
|
-
lines.push(`Warning: ${other} also starts on ${t
|
|
72
|
+
lines.push(`Warning: ${other} also starts on ${describeTrigger(t)}, and ${first} wins. vsf agents workflow move changes the order.`);
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
return lines;
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type AgentsConfig, type Workflow, type Project, type StepId, type Trigger } from './config.ts';
|
|
2
|
-
import type { Card
|
|
2
|
+
import type { Card } from './linear.ts';
|
|
3
|
+
import { type Tracker, type Trackers } from './tracker.ts';
|
|
3
4
|
export interface RunContext {
|
|
4
5
|
configDir: string;
|
|
5
6
|
config: AgentsConfig;
|
|
6
|
-
|
|
7
|
+
trackers: Trackers;
|
|
7
8
|
log: (line: string) => void;
|
|
8
9
|
selfCommand: string[];
|
|
9
10
|
}
|
|
@@ -11,13 +12,19 @@ export declare function summarize(output: string): string;
|
|
|
11
12
|
export declare function runCard(card: Card, trigger: Trigger, workflowName: string, workflow: Workflow, project: Project, rc: RunContext, opts?: {
|
|
12
13
|
from?: StepId;
|
|
13
14
|
}): Promise<'verified' | 'changes' | 'parked'>;
|
|
14
|
-
|
|
15
|
+
type Job = {
|
|
15
16
|
card: Card;
|
|
16
17
|
trigger: Trigger;
|
|
17
18
|
workflow: string;
|
|
18
|
-
}
|
|
19
|
-
export declare function
|
|
19
|
+
};
|
|
20
|
+
export declare function cronCard(workflow: string, task: string, at: Date): Card;
|
|
21
|
+
export declare function nextJob(rc: RunContext, only?: string, workflowName?: string, opts?: {
|
|
22
|
+
force?: boolean;
|
|
23
|
+
now?: Date;
|
|
24
|
+
}): Promise<Job | null>;
|
|
25
|
+
export declare function cancelCard(tracker: Tracker, configDir: string, cardId: string, label: string, source?: 'linear' | 'voidtrack'): Promise<boolean>;
|
|
20
26
|
export declare function tick(rc: RunContext, only?: string, opts?: {
|
|
21
27
|
workflow?: string;
|
|
22
28
|
from?: StepId;
|
|
23
29
|
}): Promise<number>;
|
|
30
|
+
export {};
|