@voidlabs.gg/voidshift-agents 0.5.1 → 0.7.0
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 +16 -2
- package/dist/config.d.ts +19 -0
- package/dist/config.js +43 -0
- package/dist/control.d.ts +7 -2
- package/dist/control.js +82 -38
- package/dist/index.js +186 -11
- package/dist/manage.d.ts +2 -1
- package/dist/manage.js +46 -1
- package/dist/notify.d.ts +30 -0
- package/dist/notify.js +233 -0
- package/dist/runner.d.ts +28 -2
- package/dist/runner.js +223 -60
- package/dist/secrets.d.ts +1 -0
- package/dist/secrets.js +12 -1
- package/dist/state.d.ts +11 -1
- package/dist/state.js +57 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,17 +4,18 @@ 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, AGENT_FILE, STEPS, TEMPLATES, isProducer, agentFolders, agentsDir, describeSteps, describeTrigger, describeTriggers, describeWorkflow, stepEnabled, stepId, visibleAgents, readAgentsConfig, writeAgentsConfig, DEFAULTS } from "./config.js";
|
|
7
|
+
import { AGENTS, AGENT_FILE, STEPS, TEMPLATES, isProducer, agentFolders, agentsDir, describeSteps, describeTrigger, describeTriggers, describeWorkflow, stepEnabled, stepId, visibleAgents, readAgentsConfig, writeAgentsConfig, workspaceOf, DEFAULTS, DEFAULT_WORKSPACE, WORKSPACE_NAME } from "./config.js";
|
|
8
8
|
import { controlChecks, controlCommands, controlUsage } from "./control.js";
|
|
9
9
|
import { keyStore, looksLikeLinearKey, readLinearKey, readSecret, storeLinearKey, storeSecret } from "./secrets.js";
|
|
10
10
|
import { TRACKERS } from "./tracker.js";
|
|
11
11
|
import { createVoidTrack } from "./voidtrack.js";
|
|
12
|
+
import { addWebhook, dropWebhook, looksLikeWebhook, moveWebhook, removeWebhook, testWebhook, webhookFor } from "./notify.js";
|
|
12
13
|
import { parseCron } from "./cron.js";
|
|
13
14
|
import { trackersInUse } from "./control.js";
|
|
14
15
|
import { runnerService } from "./service.js";
|
|
15
16
|
import { initProfile, profileChecks, profileDir, readRules, renderClaudeSettings, CLAUDE_SETTINGS, RULES_FILE } from "./profile.js";
|
|
16
17
|
import { createLinear } from "./linear.js";
|
|
17
|
-
import { TRIGGER_FORMS, duplicateTriggerWarnings, moveKey, parseTrigger, renameKey, sameTrigger, validateAgentFolder, validateConfig, validateWorkflow, workflowsUsing } from "./manage.js";
|
|
18
|
+
import { TRIGGER_FORMS, duplicateTriggerWarnings, moveKey, parseTrigger, projectProblems, renameKey, sameTrigger, validateAgentFolder, validateConfig, validateWorkflow, workflowsUsing } from "./manage.js";
|
|
18
19
|
import { parseStep, resolveName, stepNames } from "./names.js";
|
|
19
20
|
import { findRepos } from "./repos.js";
|
|
20
21
|
import { defaultBranch, sh } from "./git.js";
|
|
@@ -239,7 +240,8 @@ async function promptProject(ui, config, current = {}, edit = false, allowExisti
|
|
|
239
240
|
const describeProject = (config, name) => {
|
|
240
241
|
const p = config.projects[name];
|
|
241
242
|
const used = workflowsUsing(config, name);
|
|
242
|
-
|
|
243
|
+
const where = [p.workspace ? `workspace ${p.workspace}` : '', p.match?.length ? `matches ${p.match.join(', ')}` : ''].filter(Boolean);
|
|
244
|
+
return `${name} ${tilde(p.path)}, PRs into ${p.base ?? 'the default branch'}, ${where.map((w) => `${w}, `).join('')}${used.length ? `used by ${used.join(', ')}` : 'not used by any workflow'}`;
|
|
243
245
|
};
|
|
244
246
|
// Named local repos the workflows run in. Triggers point at them by name.
|
|
245
247
|
async function setupProjects(ui, config, log) {
|
|
@@ -366,15 +368,19 @@ const USAGE = [
|
|
|
366
368
|
['workflow rm NAME [--yes]', 'Delete a workflow'],
|
|
367
369
|
['workflow move NAME (--first | --last | --before OTHER | --after OTHER)', 'Change the match order'],
|
|
368
370
|
['projects [--json]', 'List projects'],
|
|
369
|
-
['project add [NAME] [--path P] [--base B]', 'Add a project; prompts for what is missing'],
|
|
371
|
+
['project add [NAME] [--path P] [--base B] [--workspace W] [--match NAME]...', 'Add a project; prompts for what is missing. --match routes cards of that tracker project here, whatever the workflow says'],
|
|
370
372
|
['project show NAME [--json]', 'Show one project'],
|
|
371
|
-
['project edit NAME [--path P] [--base B]', 'Change a project; no flags asks'],
|
|
373
|
+
['project edit NAME [--path P] [--base B] [--workspace W] [--match NAME]...', 'Change a project; no flags asks. An empty --workspace or --match clears it'],
|
|
372
374
|
['project rename OLD NEW', 'Rename a project and the workflows that use it'],
|
|
373
375
|
['project rm NAME [--yes]', 'Delete a project nothing uses'],
|
|
376
|
+
['workspace add NAME --root DIR [--context FILE]', 'Add a workspace: a folder of repos with its own run queue. Steps carry FILE, or DIR/CONTEXT.md'],
|
|
377
|
+
['workspace list [--json]', 'List workspaces and their projects'],
|
|
378
|
+
['workspace rm NAME', 'Delete a workspace no project uses'],
|
|
374
379
|
['homes [--json]', 'Show where the agents are linked'],
|
|
375
380
|
['link DIR [--claude | --codex]', 'Link the agents into a config folder'],
|
|
376
381
|
['unlink DIR', 'Remove the agents from a config folder'],
|
|
377
382
|
['key [--voidtrack]', 'Store or replace the Linear or VoidTrack API key (reads stdin without a terminal)'],
|
|
383
|
+
['discord [--workflow W] [--test] [--rm]', 'Store, test or remove the Discord webhook for run pings, global or for one workflow; it is stored only after a test message goes through (reads stdin without a terminal)'],
|
|
378
384
|
['profile [init]', `Your profile: rules (${RULES_FILE}) and Claude settings (${CLAUDE_SETTINGS}) every agent run carries; init creates it`],
|
|
379
385
|
['validate [DIR]', `Check an agent folder (DIR/${AGENT_FILE} and its prompts), or the config and every folder under agents/defs`],
|
|
380
386
|
['help [COMMAND]', 'Show this list, or one command'],
|
|
@@ -658,8 +664,14 @@ const workflowSubs = {
|
|
|
658
664
|
if (!teams)
|
|
659
665
|
return 1;
|
|
660
666
|
const r = await promptWorkflow(ctx.ui, config, teams, { name, workflow: current }, true);
|
|
661
|
-
if (r.name !== name)
|
|
667
|
+
if (r.name !== name) {
|
|
668
|
+
const refused = moveWebhook(ctx.configDir, name, r.name);
|
|
669
|
+
if (refused) {
|
|
670
|
+
ctx.log(refused);
|
|
671
|
+
return 1;
|
|
672
|
+
}
|
|
662
673
|
config.workflows = renameKey(config.workflows, name, r.name);
|
|
674
|
+
}
|
|
663
675
|
config.workflows[r.name] = r.workflow;
|
|
664
676
|
return saveWorkflow(config, r.name, r.workflow, ctx);
|
|
665
677
|
}
|
|
@@ -712,6 +724,11 @@ const workflowSubs = {
|
|
|
712
724
|
ctx.log(`A workflow named ${next} already exists.`);
|
|
713
725
|
return 1;
|
|
714
726
|
}
|
|
727
|
+
const refused = moveWebhook(ctx.configDir, old, next);
|
|
728
|
+
if (refused) {
|
|
729
|
+
ctx.log(refused);
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
715
732
|
config.workflows = renameKey(config.workflows, old, next);
|
|
716
733
|
save(ctx, config);
|
|
717
734
|
ctx.log(`Workflow ${old} is now ${next}.`);
|
|
@@ -738,6 +755,7 @@ const workflowSubs = {
|
|
|
738
755
|
}
|
|
739
756
|
delete config.workflows[name];
|
|
740
757
|
save(ctx, config);
|
|
758
|
+
dropWebhook(ctx.configDir, name);
|
|
741
759
|
ctx.log(`Deleted workflow ${name}.`);
|
|
742
760
|
return 0;
|
|
743
761
|
},
|
|
@@ -789,9 +807,31 @@ function pickProject(config, input, ctx) {
|
|
|
789
807
|
}
|
|
790
808
|
return r.name;
|
|
791
809
|
}
|
|
810
|
+
const PROJECT_FLAGS = { path: { type: 'string' }, base: { type: 'string' }, workspace: { type: 'string' }, match: { type: 'string', multiple: true } };
|
|
811
|
+
// Applies --workspace and --match to a project. An empty value clears either.
|
|
812
|
+
// Returns a complaint when the workspace or a match does not fit.
|
|
813
|
+
function applyWorkspaceFlags(config, name, project, v) {
|
|
814
|
+
if (v.workspace !== undefined) {
|
|
815
|
+
const ws = String(v.workspace).trim();
|
|
816
|
+
if (ws && ws !== DEFAULT_WORKSPACE && !config.workspaces[ws])
|
|
817
|
+
return `No workspace named ${ws}. vsf agents workspace add creates it.`;
|
|
818
|
+
if (ws && ws !== DEFAULT_WORKSPACE)
|
|
819
|
+
project.workspace = ws;
|
|
820
|
+
else
|
|
821
|
+
delete project.workspace;
|
|
822
|
+
}
|
|
823
|
+
if (v.match !== undefined) {
|
|
824
|
+
const match = v.match.map((m) => m.trim()).filter(Boolean);
|
|
825
|
+
if (match.length)
|
|
826
|
+
project.match = match;
|
|
827
|
+
else
|
|
828
|
+
delete project.match;
|
|
829
|
+
}
|
|
830
|
+
return projectProblems({ ...config.projects, [name]: project }, config.workspaces)[0] ?? null;
|
|
831
|
+
}
|
|
792
832
|
const projectSubs = {
|
|
793
833
|
async add(args, ctx) {
|
|
794
|
-
const p = begin('project add', args,
|
|
834
|
+
const p = begin('project add', args, PROJECT_FLAGS, ctx);
|
|
795
835
|
if (typeof p === 'number')
|
|
796
836
|
return p;
|
|
797
837
|
if (p.positionals.length > 1) {
|
|
@@ -838,6 +878,11 @@ const projectSubs = {
|
|
|
838
878
|
name = r.name;
|
|
839
879
|
project = r.project;
|
|
840
880
|
}
|
|
881
|
+
const bad = applyWorkspaceFlags(config, name, project, p.values);
|
|
882
|
+
if (bad) {
|
|
883
|
+
ctx.log(bad);
|
|
884
|
+
return 1;
|
|
885
|
+
}
|
|
841
886
|
config.projects[name] = project;
|
|
842
887
|
save(ctx, config);
|
|
843
888
|
ctx.log(`Project ${name}: ${tilde(project.path)}${project.base ? `, PRs into ${project.base}` : ''}`);
|
|
@@ -861,12 +906,15 @@ const projectSubs = {
|
|
|
861
906
|
ctx.log(`Project ${name}`);
|
|
862
907
|
ctx.log(`Path: ${tilde(proj.path)}`);
|
|
863
908
|
ctx.log(`PRs into: ${proj.base ?? 'the default branch'}`);
|
|
909
|
+
ctx.log(`Workspace: ${workspaceOf(proj)}`);
|
|
910
|
+
if (proj.match?.length)
|
|
911
|
+
ctx.log(`Matches: ${proj.match.join(', ')}`);
|
|
864
912
|
ctx.log(`Used by: ${used.length ? used.join(', ') : 'no workflows'}`);
|
|
865
913
|
}
|
|
866
914
|
return 0;
|
|
867
915
|
},
|
|
868
916
|
async edit(args, ctx) {
|
|
869
|
-
const p = begin('project edit', args,
|
|
917
|
+
const p = begin('project edit', args, PROJECT_FLAGS, ctx);
|
|
870
918
|
if (typeof p === 'number')
|
|
871
919
|
return p;
|
|
872
920
|
if (!needArgs(p, 1, 'project edit', ctx))
|
|
@@ -877,9 +925,9 @@ const projectSubs = {
|
|
|
877
925
|
return 1;
|
|
878
926
|
const current = config.projects[name];
|
|
879
927
|
const v = p.values;
|
|
880
|
-
if (v.path === undefined && v.base === undefined) {
|
|
928
|
+
if (v.path === undefined && v.base === undefined && v.workspace === undefined && v.match === undefined) {
|
|
881
929
|
if (!interactive()) {
|
|
882
|
-
ctx.log('Nothing to change. Give --path or --
|
|
930
|
+
ctx.log('Nothing to change. Give --path, --base, --workspace or --match, or run this in a terminal to be asked.');
|
|
883
931
|
ctx.log(usageOf('project edit'));
|
|
884
932
|
return 2;
|
|
885
933
|
}
|
|
@@ -887,7 +935,8 @@ const projectSubs = {
|
|
|
887
935
|
}
|
|
888
936
|
else {
|
|
889
937
|
const path = v.path !== undefined ? resolve(expandHome(v.path)) : current.path;
|
|
890
|
-
|
|
938
|
+
// Only a new path needs checking; a workspace or match change keeps the repo.
|
|
939
|
+
if (v.path !== undefined && !isRepo(path)) {
|
|
891
940
|
ctx.log(`${tilde(path)} is not a git repository.`);
|
|
892
941
|
return 1;
|
|
893
942
|
}
|
|
@@ -899,6 +948,11 @@ const projectSubs = {
|
|
|
899
948
|
else
|
|
900
949
|
delete next.base;
|
|
901
950
|
}
|
|
951
|
+
const bad = applyWorkspaceFlags(config, name, next, v);
|
|
952
|
+
if (bad) {
|
|
953
|
+
ctx.log(bad);
|
|
954
|
+
return 1;
|
|
955
|
+
}
|
|
902
956
|
config.projects[name] = next;
|
|
903
957
|
}
|
|
904
958
|
save(ctx, config);
|
|
@@ -960,6 +1014,87 @@ const projectSubs = {
|
|
|
960
1014
|
return 0;
|
|
961
1015
|
},
|
|
962
1016
|
};
|
|
1017
|
+
// ---- workspaces ----
|
|
1018
|
+
const projectsIn = (config, ws) => Object.entries(config.projects).filter(([, p]) => workspaceOf(p) === ws).map(([n]) => n);
|
|
1019
|
+
const workspaceSubs = {
|
|
1020
|
+
async add(args, ctx) {
|
|
1021
|
+
const p = begin('workspace add', args, { root: { type: 'string' }, context: { type: 'string' } }, ctx);
|
|
1022
|
+
if (typeof p === 'number')
|
|
1023
|
+
return p;
|
|
1024
|
+
if (!needArgs(p, 1, 'workspace add', ctx))
|
|
1025
|
+
return 2;
|
|
1026
|
+
const name = p.positionals[0].trim();
|
|
1027
|
+
if (!WORKSPACE_NAME.test(name)) {
|
|
1028
|
+
ctx.log('A workspace name is letters, digits, dashes and underscores.');
|
|
1029
|
+
return 2;
|
|
1030
|
+
}
|
|
1031
|
+
if (!p.values.root) {
|
|
1032
|
+
ctx.log('Missing --root.');
|
|
1033
|
+
ctx.log(usageOf('workspace add'));
|
|
1034
|
+
return 2;
|
|
1035
|
+
}
|
|
1036
|
+
const config = readAgentsConfig(ctx.configDir);
|
|
1037
|
+
if (config.workspaces[name]) {
|
|
1038
|
+
ctx.log(`A workspace named ${name} already exists.`);
|
|
1039
|
+
return 1;
|
|
1040
|
+
}
|
|
1041
|
+
const root = resolve(expandHome(p.values.root));
|
|
1042
|
+
if (!existsSync(root)) {
|
|
1043
|
+
ctx.log(`${tilde(root)} does not exist.`);
|
|
1044
|
+
return 1;
|
|
1045
|
+
}
|
|
1046
|
+
const context = p.values.context ? resolve(root, expandHome(p.values.context)) : undefined;
|
|
1047
|
+
if (context && !existsSync(context)) {
|
|
1048
|
+
ctx.log(`${tilde(context)} does not exist.`);
|
|
1049
|
+
return 1;
|
|
1050
|
+
}
|
|
1051
|
+
config.workspaces[name] = { root, ...(context ? { context } : {}) };
|
|
1052
|
+
save(ctx, config);
|
|
1053
|
+
ctx.log(`Workspace ${name}: ${tilde(root)}. vsf agents project edit P --workspace ${name} puts a project in it.`);
|
|
1054
|
+
return 0;
|
|
1055
|
+
},
|
|
1056
|
+
async list(args, ctx) {
|
|
1057
|
+
const p = begin('workspace list', args, { json: { type: 'boolean' } }, ctx);
|
|
1058
|
+
if (typeof p === 'number')
|
|
1059
|
+
return p;
|
|
1060
|
+
const config = readAgentsConfig(ctx.configDir);
|
|
1061
|
+
const names = [...new Set([...Object.keys(config.workspaces), ...Object.values(config.projects).map(workspaceOf)])];
|
|
1062
|
+
if (p.values.json) {
|
|
1063
|
+
out(ctx, names.map((name) => ({ name, ...config.workspaces[name], projects: projectsIn(config, name) })));
|
|
1064
|
+
return 0;
|
|
1065
|
+
}
|
|
1066
|
+
if (!names.length)
|
|
1067
|
+
ctx.log('No workspaces yet. Every project runs in the default one; vsf agents workspace add creates another.');
|
|
1068
|
+
for (const name of names) {
|
|
1069
|
+
const w = config.workspaces[name];
|
|
1070
|
+
const projects = projectsIn(config, name);
|
|
1071
|
+
ctx.log(`${name} ${w ? tilde(w.root) : 'no root'}${w?.context ? `, context ${tilde(w.context)}` : ''}, ${projects.length ? projects.join(', ') : 'no projects'}`);
|
|
1072
|
+
}
|
|
1073
|
+
return 0;
|
|
1074
|
+
},
|
|
1075
|
+
async rm(args, ctx) {
|
|
1076
|
+
const p = begin('workspace rm', args, {}, ctx);
|
|
1077
|
+
if (typeof p === 'number')
|
|
1078
|
+
return p;
|
|
1079
|
+
if (!needArgs(p, 1, 'workspace rm', ctx))
|
|
1080
|
+
return 2;
|
|
1081
|
+
const config = readAgentsConfig(ctx.configDir);
|
|
1082
|
+
const name = p.positionals[0].trim();
|
|
1083
|
+
if (!config.workspaces[name]) {
|
|
1084
|
+
ctx.log(`No workspace named ${name}.`);
|
|
1085
|
+
return 1;
|
|
1086
|
+
}
|
|
1087
|
+
const used = projectsIn(config, name);
|
|
1088
|
+
if (used.length) {
|
|
1089
|
+
ctx.log(`Used by ${used.join(', ')}. Move those projects first: vsf agents project edit P --workspace OTHER.`);
|
|
1090
|
+
return 1;
|
|
1091
|
+
}
|
|
1092
|
+
delete config.workspaces[name];
|
|
1093
|
+
save(ctx, config);
|
|
1094
|
+
ctx.log(`Deleted workspace ${name}.`);
|
|
1095
|
+
return 0;
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
963
1098
|
// ---- homes ----
|
|
964
1099
|
// Per enabled agent: ours, taken by something else, or missing.
|
|
965
1100
|
function homeReport(config, h) {
|
|
@@ -1099,6 +1234,11 @@ const agents = {
|
|
|
1099
1234
|
if (!(await setupKey(ui, ctx.configDir, 'voidtrack')))
|
|
1100
1235
|
ctx.log('VoidTrack is not connected yet. Run vsf agents key --voidtrack.');
|
|
1101
1236
|
}
|
|
1237
|
+
if (!webhookFor(ctx.configDir) && (await ui.confirm('Send run pings to Discord?', false))) {
|
|
1238
|
+
const url = await ui.secret('Paste the Discord webhook URL', (v) => (looksLikeWebhook(v.trim()) ? undefined : 'Not a Discord webhook URL'));
|
|
1239
|
+
if (!(await addWebhook(ctx.configDir, url.trim(), undefined, ui.info)))
|
|
1240
|
+
ctx.log('Discord is not connected. Run vsf agents discord to retry.');
|
|
1241
|
+
}
|
|
1102
1242
|
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.'))) {
|
|
1103
1243
|
const ok = runnerService().start(ctx.home, [...ctx.selfCommand, 'agents', 'tick'], join(agentsDir(ctx.configDir), 'runner.log'));
|
|
1104
1244
|
ctx.log(ok ? 'Runner started.' : 'Could not start the runner. Try vsf agents start.');
|
|
@@ -1137,6 +1277,8 @@ const agents = {
|
|
|
1137
1277
|
checks.push({ name, status: 'fail', detail: err.message, fix: `vsf agents key${kind === 'linear' ? '' : ` --${kind}`}` });
|
|
1138
1278
|
}
|
|
1139
1279
|
}
|
|
1280
|
+
if (webhookFor(ctx.configDir))
|
|
1281
|
+
checks.push({ name: 'discord', status: 'ok', detail: 'webhook set' });
|
|
1140
1282
|
const workflows = Object.entries(saved.workflows);
|
|
1141
1283
|
const broken = workflows.filter(([, c]) => !saved.projects[c.project] || !isRepo(saved.projects[c.project].path)).map(([n]) => n);
|
|
1142
1284
|
checks.push(!workflows.length
|
|
@@ -1242,6 +1384,7 @@ const agents = {
|
|
|
1242
1384
|
return 0;
|
|
1243
1385
|
},
|
|
1244
1386
|
project: (args, ctx) => group('project', projectSubs, args, ctx),
|
|
1387
|
+
workspace: (args, ctx) => group('workspace', workspaceSubs, args, ctx),
|
|
1245
1388
|
homes: homeCommands.homes,
|
|
1246
1389
|
link: homeCommands.link,
|
|
1247
1390
|
unlink: homeCommands.unlink,
|
|
@@ -1267,6 +1410,38 @@ const agents = {
|
|
|
1267
1410
|
}
|
|
1268
1411
|
return (await verifyAndStore(ctx.configDir, key, ctx.log, ctx.log, kind)) ? 0 : 1;
|
|
1269
1412
|
},
|
|
1413
|
+
async discord(args, ctx) {
|
|
1414
|
+
const p = begin('discord', args, { workflow: { type: 'string' }, test: { type: 'boolean' }, rm: { type: 'boolean' } }, ctx);
|
|
1415
|
+
if (typeof p === 'number')
|
|
1416
|
+
return p;
|
|
1417
|
+
if (p.positionals.length) {
|
|
1418
|
+
ctx.log('Webhooks are not accepted on the command line. Pipe the URL in instead.');
|
|
1419
|
+
return 2;
|
|
1420
|
+
}
|
|
1421
|
+
if (p.values.test && p.values.rm) {
|
|
1422
|
+
ctx.log('Give --test or --rm, not both.');
|
|
1423
|
+
return 2;
|
|
1424
|
+
}
|
|
1425
|
+
let workflow;
|
|
1426
|
+
if (p.values.workflow) {
|
|
1427
|
+
const name = pickWorkflow(readAgentsConfig(ctx.configDir), p.values.workflow, ctx);
|
|
1428
|
+
if (!name)
|
|
1429
|
+
return 1;
|
|
1430
|
+
workflow = name;
|
|
1431
|
+
}
|
|
1432
|
+
if (p.values.test)
|
|
1433
|
+
return (await testWebhook(ctx.configDir, workflow, ctx.log)) ? 0 : 1;
|
|
1434
|
+
if (p.values.rm)
|
|
1435
|
+
return removeWebhook(ctx.configDir, workflow, ctx.log) ? 0 : 1;
|
|
1436
|
+
const url = interactive()
|
|
1437
|
+
? await ctx.ui.secret('Paste the Discord webhook URL', (v) => (looksLikeWebhook(v.trim()) ? undefined : 'Not a Discord webhook URL'))
|
|
1438
|
+
: readFileSync(0, 'utf8');
|
|
1439
|
+
if (!url.trim()) {
|
|
1440
|
+
ctx.log('No webhook on stdin.');
|
|
1441
|
+
return 2;
|
|
1442
|
+
}
|
|
1443
|
+
return (await addWebhook(ctx.configDir, url.trim(), workflow, ctx.log)) ? 0 : 1;
|
|
1444
|
+
},
|
|
1270
1445
|
async help(args, ctx) {
|
|
1271
1446
|
const want = args.join(' ').trim();
|
|
1272
1447
|
if (!want) {
|
package/dist/manage.d.ts
CHANGED
|
@@ -18,4 +18,5 @@ export type Place = {
|
|
|
18
18
|
};
|
|
19
19
|
export declare function moveKey<T>(record: Record<string, T>, name: string, place: Place): Record<string, T>;
|
|
20
20
|
export declare const workflowsUsing: (config: Pick<AgentsConfig, "workflows">, project: string) => string[];
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function projectProblems(projects: AgentsConfig['projects'], workspaces: AgentsConfig['workspaces']): string[];
|
|
22
|
+
export declare function validateConfig(value: unknown, saved?: Pick<AgentsConfig, 'projects' | 'workflows'> & Partial<Pick<AgentsConfig, 'workspaces'>>): string[];
|
package/dist/manage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, join, resolve } from 'node:path';
|
|
3
|
-
import { AGENTS, AGENT_FILE, OUTPUTS, STEP_ID, isBuiltin, readAgentFolder, stepFile, describeTrigger, triggerSpec } from "./config.js";
|
|
3
|
+
import { AGENTS, AGENT_FILE, DEFAULT_WORKSPACE, OUTPUTS, STEP_ID, WORKSPACE_NAME, isBuiltin, readAgentFolder, stepFile, describeTrigger, triggerSpec } from "./config.js";
|
|
4
4
|
import { parseCron } from "./cron.js";
|
|
5
5
|
const isInt = (v, min, max) => typeof v === 'number' && Number.isInteger(v) && v >= min && v <= max;
|
|
6
6
|
// The line a step's prompt must ask for, so the runner can read the result.
|
|
@@ -185,6 +185,34 @@ export function moveKey(record, name, place) {
|
|
|
185
185
|
}
|
|
186
186
|
export const workflowsUsing = (config, project) => Object.entries(config.workflows).filter(([, w]) => w.project === project).map(([n]) => n);
|
|
187
187
|
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
188
|
+
// What is wrong with the projects' workspaces and matches: a workspace that
|
|
189
|
+
// does not exist, or a tracker project name two projects both claim.
|
|
190
|
+
export function projectProblems(projects, workspaces) {
|
|
191
|
+
const problems = [];
|
|
192
|
+
const claimed = new Map();
|
|
193
|
+
for (const [name, p] of Object.entries(projects)) {
|
|
194
|
+
if (!isObj(p))
|
|
195
|
+
continue;
|
|
196
|
+
if (p.workspace !== undefined && (typeof p.workspace !== 'string' || (p.workspace !== DEFAULT_WORKSPACE && !Object.hasOwn(workspaces, p.workspace)))) {
|
|
197
|
+
problems.push(`Project ${name}: workspace ${String(p.workspace)} does not exist. vsf agents workspace add creates it.`);
|
|
198
|
+
}
|
|
199
|
+
if (p.match === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
if (!Array.isArray(p.match) || p.match.some((m) => typeof m !== 'string' || !m.trim())) {
|
|
202
|
+
problems.push(`Project ${name}: match must list tracker project names.`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
for (const m of p.match) {
|
|
206
|
+
const key = m.trim().toLowerCase();
|
|
207
|
+
const other = claimed.get(key);
|
|
208
|
+
if (other && other !== name)
|
|
209
|
+
problems.push(`Projects ${other} and ${name} both match ${m}.`);
|
|
210
|
+
else
|
|
211
|
+
claimed.set(key, name);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return problems;
|
|
215
|
+
}
|
|
188
216
|
// Everything wrong with a whole agents config, e.g. one edited by hand.
|
|
189
217
|
// `saved` is the config on disk: only what the write changes is checked, so a
|
|
190
218
|
// moved folder or a retired agent elsewhere does not block an unrelated edit.
|
|
@@ -209,6 +237,23 @@ export function validateConfig(value, saved) {
|
|
|
209
237
|
else if (saved?.projects?.[name]?.path !== p.path && !existsSync(join(p.path, '.git')))
|
|
210
238
|
problems.push(`Project ${name}: ${p.path} is not a git repository.`);
|
|
211
239
|
}
|
|
240
|
+
// Older configs have no workspaces: every project is in the default one.
|
|
241
|
+
const workspaces = value.workspaces === undefined ? {} : isObj(value.workspaces) ? value.workspaces : null;
|
|
242
|
+
if (!workspaces)
|
|
243
|
+
problems.push('workspaces must be an object.');
|
|
244
|
+
for (const [name, w] of Object.entries(workspaces ?? {})) {
|
|
245
|
+
if (!WORKSPACE_NAME.test(name))
|
|
246
|
+
problems.push(`Workspace ${name} must be letters, digits, dashes and underscores.`);
|
|
247
|
+
if (!isObj(w) || typeof w.root !== 'string' || !w.root)
|
|
248
|
+
problems.push(`Workspace ${name} needs a root folder.`);
|
|
249
|
+
else {
|
|
250
|
+
if (saved?.workspaces?.[name]?.root !== w.root && !existsSync(w.root))
|
|
251
|
+
problems.push(`Workspace ${name}: ${w.root} does not exist.`);
|
|
252
|
+
if (w.context !== undefined && typeof w.context !== 'string')
|
|
253
|
+
problems.push(`Workspace ${name} context must be a file name.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
problems.push(...projectProblems((projects ?? {}), (workspaces ?? {})));
|
|
212
257
|
if (!Array.isArray(value.enabled) || value.enabled.some((a) => typeof a !== 'string' || !(a in AGENTS)))
|
|
213
258
|
problems.push(`enabled must list agents from: ${Object.keys(AGENTS).join(', ')}.`);
|
|
214
259
|
if (value.commits !== undefined && (!isObj(value.commits) || Object.entries(value.commits).some(([k, v]) => !['signOff', 'dropAiTrailers'].includes(k) || typeof v !== 'boolean')))
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const looksLikeWebhook: (url: string) => boolean;
|
|
2
|
+
export declare const slug: (name: string) => string;
|
|
3
|
+
export declare const webhookFor: (configDir: string, workflow?: string, platform?: NodeJS.Platform) => string | null;
|
|
4
|
+
export declare function postDiscord(url: string, content: string, fetcher?: typeof fetch, timeoutMs?: number): Promise<string | null>;
|
|
5
|
+
export type RunKind = 'ready' | 'passed' | 'parked' | 'failed';
|
|
6
|
+
export interface RunEvent {
|
|
7
|
+
kind: RunKind;
|
|
8
|
+
card: string;
|
|
9
|
+
title: string;
|
|
10
|
+
workflow: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
prUrl?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function runMessage(e: RunEvent): string;
|
|
16
|
+
export declare function notifyRun(configDir: string, e: RunEvent, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
17
|
+
export declare function notifyRunner(configDir: string, paused: boolean, reason: string, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
18
|
+
export interface NotifyState {
|
|
19
|
+
summaryDay?: string;
|
|
20
|
+
summaryAt?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function readNotifyState(configDir: string): NotifyState;
|
|
23
|
+
export declare function writeNotifyState(configDir: string, state: NotifyState): void;
|
|
24
|
+
export declare function summaryMessage(configDir: string, since: Date, paused: boolean): string | null;
|
|
25
|
+
export declare function sendSummary(configDir: string, paused: boolean, log: (line: string) => void, now?: Date, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
|
|
26
|
+
export declare function addWebhook(configDir: string, url: string, workflow: string | undefined, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<boolean>;
|
|
27
|
+
export declare function testWebhook(configDir: string, workflow: string | undefined, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<boolean>;
|
|
28
|
+
export declare function removeWebhook(configDir: string, workflow: string | undefined, log: (line: string) => void, platform?: NodeJS.Platform): boolean;
|
|
29
|
+
export declare function moveWebhook(configDir: string, from: string, to: string, platform?: NodeJS.Platform): string | null;
|
|
30
|
+
export declare function dropWebhook(configDir: string, workflow: string, platform?: NodeJS.Platform): void;
|