@voidlabs.gg/voidshift 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/checks.js +6 -5
- package/dist/commands/setup.js +6 -6
- package/dist/commands/tools.js +7 -2
- package/dist/registry.d.ts +3 -0
- package/dist/registry.js +8 -0
- package/dist/tui/data.d.ts +4 -2
- package/dist/tui/screens/workflow-form.js +11 -9
- package/dist/tui/screens/workflows.d.ts +6 -2
- package/dist/tui/screens/workflows.js +4 -2
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ One CLI for the Void dev tools. Install it once, run `vsf` (short for
|
|
|
5
5
|
defaults you can change. Everything setup does can be seen and changed later
|
|
6
6
|
from the command line.
|
|
7
7
|
|
|
8
|
-
> Early release. macOS
|
|
8
|
+
> Early release. macOS and Linux (awake is macOS only); Windows is next.
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
11
|
npm i -g @voidlabs.gg/voidshift
|
|
@@ -50,15 +50,24 @@ Five agents, installed into Claude Code (and Codex if you use it):
|
|
|
50
50
|
| QA | CI, then code review, then a usability check in a headless browser |
|
|
51
51
|
|
|
52
52
|
Use them yourself (`/vs-plan ENG-123` in Claude Code), or let a **workflow**
|
|
53
|
-
run them. A workflow starts when one of your Linear cards gets a
|
|
54
|
-
agents in order in a project (a repo on your
|
|
55
|
-
step that made it when a check fails:
|
|
53
|
+
run them. A workflow starts when one of your Linear or VoidTrack cards gets a
|
|
54
|
+
label, or on a schedule, runs agents in order in a project (a repo on your
|
|
55
|
+
machine), and sends work back to the step that made it when a check fails:
|
|
56
56
|
|
|
57
57
|
```
|
|
58
58
|
when ENG + vs-plan: Dev → QA in tether-relay
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Triggers, as `--trigger` takes them:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ENG:vs-plan # a Linear card in ENG assigned to you gets vs-plan
|
|
65
|
+
voidtrack:ORG:vs-start # the same for VoidTrack (vsf agents key --voidtrack)
|
|
66
|
+
"cron:0 5 * * 1:Update dependencies" # every Monday 05:00, no card, on its own branch
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A schedule fires once when due (a missed one is made up within a day) and
|
|
70
|
+
`vsf agents run --workflow NAME` fires it now. The runner checks every minute, works in its own copy of the repo so
|
|
62
71
|
your checkout is never touched, and keeps two labels on the card as it goes: the
|
|
63
72
|
stage (`vs-planning`, `vs-plan-review`, `vs-dev`, `vs-qa`, then `vs-qa-verified`
|
|
64
73
|
or `vs-parked`) and the round (`vs-v1`, `vs-v2`). On Linear it posts the plan and
|
|
@@ -104,10 +113,13 @@ to trust the recall hook once: run `/hooks` in Codex.
|
|
|
104
113
|
- **Your own logins.** Agents run the official `claude` and `codex` CLIs you
|
|
105
114
|
already use, signed in with your own account. VoidShift never reads or
|
|
106
115
|
stores those credentials. The Linear API key for the runner lives in your
|
|
107
|
-
macOS Keychain.
|
|
116
|
+
macOS Keychain, or on Linux in `~/.config/voidshift/agents/secrets.json`
|
|
117
|
+
(mode 0600).
|
|
108
118
|
- **Trust.** The Dev agent can run shell commands inside its copy of the repo
|
|
109
119
|
so it can install and test. Review its pull requests like any other.
|
|
110
120
|
- **Your config** lives in `~/.config/voidshift`.
|
|
121
|
+
- **Linux.** The runner is a systemd user timer. Run
|
|
122
|
+
`sudo loginctl enable-linger $USER` so it keeps going after you log out.
|
|
111
123
|
|
|
112
124
|
```sh
|
|
113
125
|
vsf doctor # check everything is set up, and whether an update is out
|
package/dist/checks.js
CHANGED
|
@@ -19,23 +19,24 @@ function firstLine(s) {
|
|
|
19
19
|
}
|
|
20
20
|
export function baseChecks(platform, nodeVersion, exec) {
|
|
21
21
|
const checks = [];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const linux = platform === 'linux';
|
|
23
|
+
checks.push(platform === 'darwin' || linux
|
|
24
|
+
? { name: 'platform', status: 'ok', detail: linux ? 'Linux, supported' : 'macOS, supported' }
|
|
25
|
+
: { name: 'platform', status: 'fail', detail: `${platform} is not supported yet`, fix: 'Run on macOS or Linux' });
|
|
25
26
|
checks.push(atLeast(nodeVersion, '22.18')
|
|
26
27
|
? { name: 'Node', status: 'ok', detail: nodeVersion }
|
|
27
28
|
: { name: 'Node', status: 'fail', detail: `${nodeVersion} is too old`, fix: 'Install Node 22.18 or newer' });
|
|
28
29
|
const git = exec('git', ['--version']);
|
|
29
30
|
checks.push(git.ok
|
|
30
31
|
? { name: 'git', status: 'ok', detail: git.stdout }
|
|
31
|
-
: { name: 'git', status: 'fail', detail: 'not found', fix: 'xcode-select --install' });
|
|
32
|
+
: { name: 'git', status: 'fail', detail: 'not found', fix: linux ? 'Install git with your package manager' : 'xcode-select --install' });
|
|
32
33
|
const gh = exec('gh', ['auth', 'token']);
|
|
33
34
|
const ghInstalled = gh.ok || exec('gh', ['--version']).ok;
|
|
34
35
|
checks.push(gh.ok
|
|
35
36
|
? { name: 'GitHub CLI', status: 'ok', detail: 'logged in' }
|
|
36
37
|
: ghInstalled
|
|
37
38
|
? { name: 'GitHub CLI', status: 'warn', detail: 'not logged in', fix: 'gh auth login' }
|
|
38
|
-
: { name: 'GitHub CLI', status: 'warn', detail: 'not found', fix: 'brew install gh' });
|
|
39
|
+
: { name: 'GitHub CLI', status: 'warn', detail: 'not found', fix: linux ? 'https://cli.github.com' : 'brew install gh' });
|
|
39
40
|
const claude = exec('claude', ['--version']);
|
|
40
41
|
const codex = exec('codex', ['--version']);
|
|
41
42
|
checks.push(claude.ok
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import { baseChecks, run } from "../checks.js";
|
|
3
3
|
import { context, readConfig, writeConfig } from "../config.js";
|
|
4
4
|
import { fetchTool, loadTool } from "../loader.js";
|
|
5
|
-
import { findTool, registry } from "../registry.js";
|
|
5
|
+
import { findTool, registry, unavailable } from "../registry.js";
|
|
6
6
|
import { needsTerminal, parseFlags } from "./args.js";
|
|
7
7
|
import { usages } from "./help.js";
|
|
8
8
|
import { Cancelled } from "../ui.js";
|
|
@@ -82,18 +82,18 @@ export async function setup(defaults = false) {
|
|
|
82
82
|
const ctx = context(true, defaults);
|
|
83
83
|
const config = readConfig();
|
|
84
84
|
try {
|
|
85
|
-
const available = registry.filter((t) => t
|
|
85
|
+
const available = registry.filter((t) => !unavailable(t)).map((t) => t.name);
|
|
86
86
|
const picked = defaults
|
|
87
|
-
? [...new Set([...config.tools, ...available])]
|
|
87
|
+
? [...new Set([...config.tools.filter((n) => available.includes(n)), ...available])]
|
|
88
88
|
: await p.multiselect({
|
|
89
89
|
message: 'Which tools do you want?',
|
|
90
90
|
options: registry.map((t) => ({
|
|
91
91
|
value: t.name,
|
|
92
92
|
label: t.name,
|
|
93
|
-
hint: t
|
|
94
|
-
disabled: t
|
|
93
|
+
hint: unavailable(t) ? `${t.summary} (${unavailable(t)})` : t.summary,
|
|
94
|
+
disabled: !!unavailable(t),
|
|
95
95
|
})),
|
|
96
|
-
initialValues: config.tools,
|
|
96
|
+
initialValues: config.tools.filter((n) => available.includes(n)),
|
|
97
97
|
required: false,
|
|
98
98
|
});
|
|
99
99
|
if (p.isCancel(picked))
|
package/dist/commands/tools.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { context, readConfig, writeConfig } from "../config.js";
|
|
2
2
|
import { fetchTool, loadTool } from "../loader.js";
|
|
3
|
-
import { findTool, registry } from "../registry.js";
|
|
3
|
+
import { findTool, registry, unavailable } from "../registry.js";
|
|
4
4
|
import { needsTerminal, wantsHelp } from "./args.js";
|
|
5
5
|
export function listTools() {
|
|
6
6
|
const installed = new Set(readConfig().tools);
|
|
7
7
|
const width = Math.max(...registry.map((t) => t.name.length));
|
|
8
8
|
for (const t of registry) {
|
|
9
|
-
const
|
|
9
|
+
const why = unavailable(t);
|
|
10
|
+
const tag = installed.has(t.name) ? ' (added)' : why ? ` (${why})` : '';
|
|
10
11
|
console.log(`${t.name.padEnd(width)} ${t.summary}${tag}`);
|
|
11
12
|
}
|
|
12
13
|
return 0;
|
|
@@ -31,6 +32,10 @@ export async function addTool(name) {
|
|
|
31
32
|
console.error(`${entry.name} is not released yet.`);
|
|
32
33
|
return 1;
|
|
33
34
|
}
|
|
35
|
+
if (unavailable(entry)) {
|
|
36
|
+
console.error(`${entry.name} runs on macOS only.`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
34
39
|
const ctx = context();
|
|
35
40
|
if (!fetchTool(entry, ctx.configDir)) {
|
|
36
41
|
console.error(`could not download ${entry.pkg}`);
|
package/dist/registry.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export interface ToolEntry {
|
|
|
3
3
|
summary: string;
|
|
4
4
|
pkg: string;
|
|
5
5
|
status: 'planned' | 'available';
|
|
6
|
+
platforms?: NodeJS.Platform[];
|
|
6
7
|
}
|
|
7
8
|
export declare const registry: ToolEntry[];
|
|
9
|
+
export declare const runsHere: (t: ToolEntry, platform?: NodeJS.Platform) => boolean;
|
|
10
|
+
export declare function unavailable(t: ToolEntry, platform?: NodeJS.Platform): string | null;
|
|
8
11
|
export declare function findTool(name: string): ToolEntry | undefined;
|
package/dist/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ export const registry = [
|
|
|
7
7
|
summary: 'Keep the Mac running on battery with the lid closed, with guardrails',
|
|
8
8
|
pkg: '@voidlabs.gg/voidshift-awake',
|
|
9
9
|
status: 'available',
|
|
10
|
+
platforms: ['darwin'],
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
name: 'agents',
|
|
@@ -21,6 +22,13 @@ export const registry = [
|
|
|
21
22
|
status: 'available',
|
|
22
23
|
},
|
|
23
24
|
];
|
|
25
|
+
export const runsHere = (t, platform = process.platform) => !t.platforms || t.platforms.includes(platform);
|
|
26
|
+
// Why a tool cannot be added here, or null when it can.
|
|
27
|
+
export function unavailable(t, platform = process.platform) {
|
|
28
|
+
if (t.status === 'planned')
|
|
29
|
+
return 'coming soon';
|
|
30
|
+
return runsHere(t, platform) ? null : 'macOS only';
|
|
31
|
+
}
|
|
24
32
|
export function findTool(name) {
|
|
25
33
|
return registry.find((t) => t.name === name);
|
|
26
34
|
}
|
package/dist/tui/data.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { useBackGuard, useEquivalent, useHints, useKeys, useScreen } from "../co
|
|
|
8
8
|
import { theme } from "../theme.js";
|
|
9
9
|
import { Panel, Select, TextInput } from "../widgets.js";
|
|
10
10
|
import { tilde } from "../format.js";
|
|
11
|
-
import { matchWarnings, stepLabel, stepName, triggerText, warningText, sameTriggerLike } from "./workflows.js";
|
|
11
|
+
import { matchWarnings, stepLabel, stepName, triggerSpec, triggerText, warningText, sameTriggerLike } from "./workflows.js";
|
|
12
12
|
const h = React.createElement;
|
|
13
13
|
export function formReducer(s, a) {
|
|
14
14
|
switch (a.type) {
|
|
@@ -38,7 +38,7 @@ export function formReducer(s, a) {
|
|
|
38
38
|
}
|
|
39
39
|
export function initialForm(w, name, projects) {
|
|
40
40
|
if (w)
|
|
41
|
-
return { name, triggers: w.triggers.map((t) => ({
|
|
41
|
+
return { name, triggers: w.triggers.map((t) => ({ ...t })), steps: [...w.steps], project: w.project, rounds: w.maxRounds };
|
|
42
42
|
return { name: '', triggers: [], steps: [], project: projects.length === 1 ? projects[0] : '', rounds: 3 };
|
|
43
43
|
}
|
|
44
44
|
const FALLBACK_PRODUCERS = ['vs-plan', 'vs-dev'];
|
|
@@ -95,7 +95,7 @@ export function saveArgs(s, isNew, catalog) {
|
|
|
95
95
|
'workflow',
|
|
96
96
|
isNew ? 'add' : 'edit',
|
|
97
97
|
s.name.trim(),
|
|
98
|
-
...s.triggers.flatMap((t) => ['--trigger',
|
|
98
|
+
...s.triggers.flatMap((t) => ['--trigger', triggerSpec(t)]),
|
|
99
99
|
'--steps',
|
|
100
100
|
s.steps.map((id) => stepName(catalog, id)).join(','),
|
|
101
101
|
'--project',
|
|
@@ -105,17 +105,19 @@ export function saveArgs(s, isNew, catalog) {
|
|
|
105
105
|
];
|
|
106
106
|
}
|
|
107
107
|
export const renameArgs = (from, to) => ['agents', 'workflow', 'rename', from, to];
|
|
108
|
-
// A trigger from the two text fields, or a complaint.
|
|
108
|
+
// A trigger from the two text fields, or a complaint. A team written as
|
|
109
|
+
// voidtrack:ORG is a VoidTrack team; schedules are added with the CLI.
|
|
109
110
|
export function makeTrigger(team, label) {
|
|
110
|
-
const
|
|
111
|
+
const voidtrack = /^voidtrack:/i.test(team.trim());
|
|
112
|
+
const t = team.trim().replace(/^voidtrack:/i, '').toUpperCase();
|
|
111
113
|
const l = label.trim();
|
|
112
114
|
if (!t)
|
|
113
|
-
return { error: 'Give the
|
|
115
|
+
return { error: 'Give the team, like ENG, or voidtrack:ORG for VoidTrack.' };
|
|
114
116
|
if (t.includes(':') || /\s/.test(t))
|
|
115
|
-
return { error: 'The team is a short key like ENG.' };
|
|
117
|
+
return { error: 'The team is a short key like ENG, or voidtrack:ORG.' };
|
|
116
118
|
if (!l)
|
|
117
119
|
return { error: 'Give the label, like vs-plan.' };
|
|
118
|
-
return { trigger: { team: t, label: l } };
|
|
120
|
+
return { trigger: { source: voidtrack ? 'voidtrack' : 'linear', team: t, label: l } };
|
|
119
121
|
}
|
|
120
122
|
// Warnings for triggers this workflow shares with another. In place when
|
|
121
123
|
// editing, last in order when new.
|
|
@@ -348,7 +350,7 @@ function WorkflowForm(props) {
|
|
|
348
350
|
// Triggers
|
|
349
351
|
h(Box, null, h(Label, { text: 'Starts when', focused: where === 'triggers' }), h(Box, { flexDirection: 'column', flexGrow: 1 }, form.triggers.length === 0 ? h(Text, { color: theme.dim }, ' no trigger yet') : null, ...form.triggers.map((t, i) => {
|
|
350
352
|
const on = where === 'triggers' && i === tCursor && adding === null;
|
|
351
|
-
return h(Text, { key:
|
|
353
|
+
return h(Text, { key: triggerSpec(t), backgroundColor: on ? theme.selected : undefined, bold: on, color: on ? theme.bold : undefined }, ` ${triggerText(t)} `);
|
|
352
354
|
}), adding !== null
|
|
353
355
|
? h(Box, { flexDirection: 'column' }, h(Box, null, h(Text, { color: theme.dim }, ' team '), h(TextInput, { value: newTeam, onChange: setNewTeam, focus: adding === 'team', placeholder: 'ENG', onSubmit: () => setAdding('label') }), h(Text, { color: theme.dim }, ' label '), h(TextInput, { value: newLabel, onChange: setNewLabel, focus: adding === 'label', placeholder: 'vs-plan', onSubmit: commitTrigger })), addError ? h(Text, { color: theme.red }, ` ${addError}`) : null)
|
|
354
356
|
: null), h(Box, { flexShrink: 0, marginLeft: 2 }, h(Text, { color: theme.dim }, where === 'triggers' ? 'a add trigger · x remove' : ''))), h(Text, null, ' '),
|
|
@@ -4,10 +4,14 @@ import type { Screen } from '../screens.ts';
|
|
|
4
4
|
export declare function stepLabel(catalog: StepsCatalog | null, id: string): string;
|
|
5
5
|
export declare function stepName(catalog: StepsCatalog | null, id: string): string;
|
|
6
6
|
export interface TriggerLike {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
source?: string;
|
|
8
|
+
team?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
schedule?: string;
|
|
11
|
+
task?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare const triggerText: (t: TriggerLike) => string;
|
|
14
|
+
export declare const triggerSpec: (t: TriggerLike) => string;
|
|
11
15
|
export declare const triggersText: (triggers: TriggerLike[]) => string;
|
|
12
16
|
export declare const sameTriggerLike: (a: TriggerLike, b: TriggerLike) => boolean;
|
|
13
17
|
export declare const stepsText: (catalog: StepsCatalog | null, steps: string[]) => string;
|
|
@@ -28,9 +28,11 @@ export function stepLabel(catalog, id) {
|
|
|
28
28
|
export function stepName(catalog, id) {
|
|
29
29
|
return catalog?.steps.find((s) => s.id === id)?.name ?? id.replace(/^vs-/, '');
|
|
30
30
|
}
|
|
31
|
-
export const triggerText = (t) => `${t.team} + ${t.label}`;
|
|
31
|
+
export const triggerText = (t) => t.source === 'cron' ? `cron ${t.schedule}` : `${t.source === 'voidtrack' ? 'VoidTrack ' : ''}${t.team} + ${t.label}`;
|
|
32
|
+
// The form `--trigger` takes.
|
|
33
|
+
export const triggerSpec = (t) => t.source === 'cron' ? `cron:${t.schedule}:${t.task}` : t.source === 'voidtrack' ? `voidtrack:${t.team}:${t.label}` : `${t.team}:${t.label}`;
|
|
32
34
|
export const triggersText = (triggers) => triggers.map(triggerText).join(', ');
|
|
33
|
-
export const sameTriggerLike = (a, b) => a
|
|
35
|
+
export const sameTriggerLike = (a, b) => triggerSpec(a) === triggerSpec(b);
|
|
34
36
|
export const stepsText = (catalog, steps) => steps.map((s) => stepLabel(catalog, s)).join(' → ');
|
|
35
37
|
// Two workflows that start on the same TEAM:LABEL: only the first in order
|
|
36
38
|
// starts the card. One entry per (loser, winner) pair.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidlabs.gg/voidshift",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "One CLI for the Void dev tools: keep your Mac awake, run agents in the background, shared skills and memory for Claude Code and Codex.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"agents",
|
|
19
19
|
"linear",
|
|
20
20
|
"cli",
|
|
21
|
-
"macos"
|
|
21
|
+
"macos",
|
|
22
|
+
"linux"
|
|
22
23
|
],
|
|
23
24
|
"type": "module",
|
|
24
25
|
"bin": {
|
|
@@ -30,9 +31,6 @@
|
|
|
30
31
|
"LICENSE",
|
|
31
32
|
"README.md"
|
|
32
33
|
],
|
|
33
|
-
"os": [
|
|
34
|
-
"darwin"
|
|
35
|
-
],
|
|
36
34
|
"engines": {
|
|
37
35
|
"node": ">=22.18"
|
|
38
36
|
},
|