@voidlabs.gg/voidshift 0.0.0-stage → 0.4.1
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/LICENSE +202 -0
- package/README.md +136 -2
- package/dist/checks.d.ts +9 -0
- package/dist/checks.js +51 -0
- package/dist/commands/args.d.ts +13 -0
- package/dist/commands/args.js +22 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +181 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +52 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.js +59 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +135 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +60 -0
- package/dist/commands/tools.d.ts +9 -0
- package/dist/commands/tools.js +111 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +79 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +29 -0
- package/dist/dotted.d.ts +9 -0
- package/dist/dotted.js +86 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.js +51 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +93 -0
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +26 -0
- package/dist/tool.d.ts +51 -0
- package/dist/tool.js +4 -0
- package/dist/tui/actions.d.ts +17 -0
- package/dist/tui/actions.js +99 -0
- package/dist/tui/app.d.ts +6 -0
- package/dist/tui/app.js +246 -0
- package/dist/tui/context.d.ts +62 -0
- package/dist/tui/context.js +48 -0
- package/dist/tui/data.d.ts +196 -0
- package/dist/tui/data.js +302 -0
- package/dist/tui/format.d.ts +11 -0
- package/dist/tui/format.js +54 -0
- package/dist/tui/home.d.ts +51 -0
- package/dist/tui/home.js +336 -0
- package/dist/tui/keys.d.ts +2 -0
- package/dist/tui/keys.js +107 -0
- package/dist/tui/nav.d.ts +24 -0
- package/dist/tui/nav.js +32 -0
- package/dist/tui/palette.d.ts +32 -0
- package/dist/tui/palette.js +269 -0
- package/dist/tui/run.d.ts +1 -0
- package/dist/tui/run.js +20 -0
- package/dist/tui/screens/agents.d.ts +6 -0
- package/dist/tui/screens/agents.js +44 -0
- package/dist/tui/screens/awake.d.ts +23 -0
- package/dist/tui/screens/awake.js +209 -0
- package/dist/tui/screens/checks.d.ts +11 -0
- package/dist/tui/screens/checks.js +106 -0
- package/dist/tui/screens/homes.d.ts +14 -0
- package/dist/tui/screens/homes.js +99 -0
- package/dist/tui/screens/list-window.d.ts +6 -0
- package/dist/tui/screens/list-window.js +18 -0
- package/dist/tui/screens/log.d.ts +26 -0
- package/dist/tui/screens/log.js +266 -0
- package/dist/tui/screens/memory.d.ts +24 -0
- package/dist/tui/screens/memory.js +174 -0
- package/dist/tui/screens/project-form.d.ts +14 -0
- package/dist/tui/screens/project-form.js +137 -0
- package/dist/tui/screens/projects.d.ts +17 -0
- package/dist/tui/screens/projects.js +109 -0
- package/dist/tui/screens/runs.d.ts +43 -0
- package/dist/tui/screens/runs.js +420 -0
- package/dist/tui/screens/settings.d.ts +17 -0
- package/dist/tui/screens/settings.js +153 -0
- package/dist/tui/screens/workflow-form.d.ts +58 -0
- package/dist/tui/screens/workflow-form.js +375 -0
- package/dist/tui/screens/workflows.d.ts +41 -0
- package/dist/tui/screens/workflows.js +174 -0
- package/dist/tui/screens.d.ts +30 -0
- package/dist/tui/screens.js +172 -0
- package/dist/tui/theme.d.ts +14 -0
- package/dist/tui/theme.js +18 -0
- package/dist/tui/widgets.d.ts +122 -0
- package/dist/tui/widgets.js +264 -0
- package/dist/ui.d.ts +5 -0
- package/dist/ui.js +82 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +17 -0
- package/package.json +59 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { baseChecks, run } from "../checks.js";
|
|
2
|
+
import { context, readConfig } from "../config.js";
|
|
3
|
+
import { loadTool } from "../loader.js";
|
|
4
|
+
import { findTool } from "../registry.js";
|
|
5
|
+
import { CLI_PKG, cliVersion, isNewer } from "../version.js";
|
|
6
|
+
import { parseFlags } from "./args.js";
|
|
7
|
+
import { usages } from "./help.js";
|
|
8
|
+
import { latestVersion } from "./update.js";
|
|
9
|
+
const mark = { ok: '✓', warn: '!', fail: '✗' };
|
|
10
|
+
export function formatChecks(checks) {
|
|
11
|
+
const width = Math.max(...checks.map((c) => c.name.length));
|
|
12
|
+
return checks
|
|
13
|
+
.map((c) => {
|
|
14
|
+
const line = `${mark[c.status]} ${c.name.padEnd(width)} ${c.detail}`;
|
|
15
|
+
return c.fix && c.status !== 'ok' ? `${line}\n ${' '.repeat(width)} ${c.fix}` : line;
|
|
16
|
+
})
|
|
17
|
+
.join('\n');
|
|
18
|
+
}
|
|
19
|
+
export async function doctor(args = []) {
|
|
20
|
+
const parsed = parseFlags(args, { json: { type: 'boolean' } }, usages.doctor);
|
|
21
|
+
if (!parsed)
|
|
22
|
+
return 2;
|
|
23
|
+
if (parsed.values.help) {
|
|
24
|
+
console.log(usages.doctor);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const checks = baseChecks(process.platform, process.version, run);
|
|
28
|
+
const ctx = context();
|
|
29
|
+
for (const name of readConfig().tools) {
|
|
30
|
+
const entry = findTool(name);
|
|
31
|
+
if (!entry) {
|
|
32
|
+
checks.push({ name: 'config', status: 'warn', detail: `unknown tool ${name} in your config`, fix: `vsf remove ${name}` });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
checks.push(...(await (await loadTool(entry, ctx.configDir)).checks(ctx)));
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
checks.push({ name, status: 'fail', detail: err.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const latest = latestVersion(CLI_PKG);
|
|
43
|
+
if (latest && isNewer(latest, cliVersion())) {
|
|
44
|
+
checks.push({ name: 'VoidShift', status: 'warn', detail: `${latest} is out (you have ${cliVersion()})`, fix: 'vsf update' });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
checks.push({ name: 'VoidShift', status: 'ok', detail: cliVersion() });
|
|
48
|
+
}
|
|
49
|
+
const ok = !checks.some((c) => c.status === 'fail');
|
|
50
|
+
console.log(parsed.values.json ? JSON.stringify({ ok, checks }, null, 2) : formatChecks(checks));
|
|
51
|
+
return ok ? 0 : 1;
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { findTool, registry } from "../registry.js";
|
|
2
|
+
import { toolHelp } from "./tools.js";
|
|
3
|
+
// Usage lines per umbrella command, shared by `vsf help <command>` and each
|
|
4
|
+
// command's own --help.
|
|
5
|
+
export const usages = {
|
|
6
|
+
setup: 'Usage: vsf setup [tool] [--defaults]\n\nPick your tools and configure them, or set up one tool.\n--defaults takes every default and asks only what has none.',
|
|
7
|
+
ui: 'Usage: vsf ui\n\nOpen the dashboard. Plain vsf opens it too. Needs a terminal.',
|
|
8
|
+
status: 'Usage: vsf status [--json]\n\nShow what each added tool is doing.',
|
|
9
|
+
doctor: 'Usage: vsf doctor [--json]\n\nCheck this Mac is ready. Exits 1 when a check fails.',
|
|
10
|
+
config: 'Usage:\n vsf config [tool] [--json] Show saved settings\n vsf config get KEY Print one setting, e.g. awake.battery\n vsf config set KEY VALUE Change a setting (VALUE is JSON, or plain text)\n Negative numbers: vsf config set KEY -- -5\n vsf config unset KEY Remove a setting\n vsf config edit [tool] Edit a tool\'s settings in $EDITOR',
|
|
11
|
+
tools: 'Usage: vsf tools\n\nList available tools.',
|
|
12
|
+
add: 'Usage: vsf add <tool>\n\nInstall and set up a tool.',
|
|
13
|
+
remove: 'Usage: vsf remove <tool>\n\nTake a tool down and uninstall it.',
|
|
14
|
+
update: 'Usage: vsf update\n\nUpdate VoidShift and every tool you added.',
|
|
15
|
+
help: 'Usage: vsf help [command or tool]\n\nShow every command, or the usage of one command or tool.',
|
|
16
|
+
};
|
|
17
|
+
export function mainHelp() {
|
|
18
|
+
const width = Math.max(...registry.map((t) => t.name.length));
|
|
19
|
+
const tools = registry.map((t) => ` ${t.name.padEnd(width)} ${t.summary}`).join('\n');
|
|
20
|
+
return `vsf: one CLI for the Void dev tools
|
|
21
|
+
|
|
22
|
+
Usage
|
|
23
|
+
vsf Open the dashboard (also: vsf ui)
|
|
24
|
+
vsf setup [tool] [--defaults] Pick your tools and configure them
|
|
25
|
+
vsf status [--json] What each tool is doing
|
|
26
|
+
vsf doctor [--json] Check this Mac is ready
|
|
27
|
+
vsf config [tool] [--json] Show saved settings
|
|
28
|
+
vsf config get|set|unset KEY [VALUE]
|
|
29
|
+
vsf config edit [tool] Edit a tool's settings in $EDITOR
|
|
30
|
+
vsf tools List available tools
|
|
31
|
+
vsf add|remove <tool> Install or take down a tool
|
|
32
|
+
vsf update Update VoidShift and every tool you added
|
|
33
|
+
vsf help [command or tool] Show help
|
|
34
|
+
vsf --version Print the version
|
|
35
|
+
|
|
36
|
+
Tools
|
|
37
|
+
${tools}
|
|
38
|
+
|
|
39
|
+
vsf <tool> help lists a tool's commands.
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
export async function help(args) {
|
|
43
|
+
const [name] = args;
|
|
44
|
+
if (!name) {
|
|
45
|
+
console.log(mainHelp());
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (Object.hasOwn(usages, name)) {
|
|
49
|
+
const usage = usages[name];
|
|
50
|
+
console.log(usage);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const entry = findTool(name);
|
|
54
|
+
if (!entry) {
|
|
55
|
+
console.error(`unknown command or tool: ${name}. Run vsf help.`);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
return toolHelp(entry, args.slice(1));
|
|
59
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { baseChecks, run } from "../checks.js";
|
|
3
|
+
import { context, readConfig, writeConfig } from "../config.js";
|
|
4
|
+
import { fetchTool, loadTool } from "../loader.js";
|
|
5
|
+
import { findTool, registry } from "../registry.js";
|
|
6
|
+
import { needsTerminal, parseFlags } from "./args.js";
|
|
7
|
+
import { usages } from "./help.js";
|
|
8
|
+
import { Cancelled } from "../ui.js";
|
|
9
|
+
import { formatChecks } from "./doctor.js";
|
|
10
|
+
// Download, install and record one tool. Shared by `vsf setup` and `vsf setup <tool>`.
|
|
11
|
+
async function installTool(entry, ctx) {
|
|
12
|
+
if (!fetchTool(entry, ctx.configDir)) {
|
|
13
|
+
p.log.error(`Could not download ${entry.pkg}.`);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
await (await loadTool(entry, ctx.configDir)).install(ctx);
|
|
18
|
+
const tools = readConfig().tools;
|
|
19
|
+
if (!tools.includes(entry.name))
|
|
20
|
+
writeConfig({ ...readConfig(), tools: [...tools, entry.name] });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof Cancelled)
|
|
25
|
+
throw err;
|
|
26
|
+
p.log.error(`${entry.name}: ${err.message}`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// `vsf setup <tool>`: set up or reconfigure just one tool.
|
|
31
|
+
async function setupTool(name, defaults) {
|
|
32
|
+
const entry = findTool(name);
|
|
33
|
+
if (!entry) {
|
|
34
|
+
console.error(`unknown tool: ${name}. Run vsf tools to see the list.`);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
if (entry.status === 'planned') {
|
|
38
|
+
console.error(`${entry.name} is not released yet.`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
p.intro(`VoidShift: ${entry.name}`);
|
|
42
|
+
try {
|
|
43
|
+
if (!(await installTool(entry, context(true, defaults))))
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof Cancelled)
|
|
48
|
+
return 1;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
p.outro(`${entry.name} is set up.`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
export async function setupCommand(args) {
|
|
55
|
+
const parsed = parseFlags(args, { defaults: { type: 'boolean' } }, usages.setup);
|
|
56
|
+
if (!parsed)
|
|
57
|
+
return 2;
|
|
58
|
+
const [tool, extra] = parsed.positionals;
|
|
59
|
+
if (extra) {
|
|
60
|
+
console.error(`too many arguments.\n\n${usages.setup}`);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
const defaults = parsed.values.defaults === true;
|
|
64
|
+
if (!defaults && needsTerminal('setup', 'Run it in a terminal, or use vsf setup --defaults.'))
|
|
65
|
+
return 2;
|
|
66
|
+
return tool ? setupTool(tool, defaults) : setup(defaults);
|
|
67
|
+
}
|
|
68
|
+
// `vsf setup` with no tool: check the machine, pick tools, configure
|
|
69
|
+
// each one. Safe to run again; it starts from what is already set up.
|
|
70
|
+
export async function setup(defaults = false) {
|
|
71
|
+
p.intro(defaults ? 'VoidShift (defaults)' : 'VoidShift');
|
|
72
|
+
const base = baseChecks(process.platform, process.version, run);
|
|
73
|
+
const line = base.map((c) => `${c.status === 'ok' ? '✓' : c.status === 'warn' ? '!' : '✗'} ${c.name}`).join(' ');
|
|
74
|
+
if (base.some((c) => c.status === 'fail')) {
|
|
75
|
+
p.log.error(formatChecks(base.filter((c) => c.status !== 'ok')));
|
|
76
|
+
p.outro('Fix the items above, then run vsf setup again.');
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
p.log.info(line);
|
|
80
|
+
for (const c of base.filter((c) => c.status === 'warn'))
|
|
81
|
+
p.log.warn(`${c.name}: ${c.detail}. ${c.fix ?? ''}`);
|
|
82
|
+
const ctx = context(true, defaults);
|
|
83
|
+
const config = readConfig();
|
|
84
|
+
try {
|
|
85
|
+
const available = registry.filter((t) => t.status !== 'planned').map((t) => t.name);
|
|
86
|
+
const picked = defaults
|
|
87
|
+
? [...new Set([...config.tools, ...available])]
|
|
88
|
+
: await p.multiselect({
|
|
89
|
+
message: 'Which tools do you want?',
|
|
90
|
+
options: registry.map((t) => ({
|
|
91
|
+
value: t.name,
|
|
92
|
+
label: t.name,
|
|
93
|
+
hint: t.status === 'planned' ? `${t.summary} (coming soon)` : t.summary,
|
|
94
|
+
disabled: t.status === 'planned',
|
|
95
|
+
})),
|
|
96
|
+
initialValues: config.tools,
|
|
97
|
+
required: false,
|
|
98
|
+
});
|
|
99
|
+
if (p.isCancel(picked))
|
|
100
|
+
throw (p.cancel('Setup cancelled. Nothing was changed.'), new Cancelled());
|
|
101
|
+
if (defaults)
|
|
102
|
+
p.log.info(`Which tools do you want? ${picked.join(', ')}`);
|
|
103
|
+
for (const name of config.tools.filter((t) => !picked.includes(t))) {
|
|
104
|
+
const entry = registry.find((t) => t.name === name);
|
|
105
|
+
if (!entry)
|
|
106
|
+
continue;
|
|
107
|
+
if (!(await ctx.ui.confirm(`Remove ${name}?`, false)))
|
|
108
|
+
continue;
|
|
109
|
+
p.log.step(name);
|
|
110
|
+
try {
|
|
111
|
+
await (await loadTool(entry, ctx.configDir)).uninstall(ctx);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (err instanceof Cancelled)
|
|
115
|
+
throw err;
|
|
116
|
+
p.log.warn(`${name}: ${err.message} Removed it from your tools anyway.`);
|
|
117
|
+
}
|
|
118
|
+
writeConfig({ tools: readConfig().tools.filter((t) => t !== name) });
|
|
119
|
+
}
|
|
120
|
+
for (const name of picked) {
|
|
121
|
+
const entry = registry.find((t) => t.name === name);
|
|
122
|
+
if (!entry)
|
|
123
|
+
continue;
|
|
124
|
+
p.log.step(name);
|
|
125
|
+
await installTool(entry, ctx);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof Cancelled)
|
|
130
|
+
return 1;
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
p.outro('All set. Run vsf setup any time to change this, or vsf doctor to check it.');
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { context, readConfig } from "../config.js";
|
|
2
|
+
import { loadTool } from "../loader.js";
|
|
3
|
+
import { findTool } from "../registry.js";
|
|
4
|
+
import { CLI_PKG, cliVersion, isNewer } from "../version.js";
|
|
5
|
+
import { parseFlags } from "./args.js";
|
|
6
|
+
import { usages } from "./help.js";
|
|
7
|
+
import { latestVersion } from "./update.js";
|
|
8
|
+
// A tool's captured --json output, or its error text when it failed.
|
|
9
|
+
export function toolStatusJson(code, out) {
|
|
10
|
+
return code === 0 ? JSON.parse(out) : { error: out.trim() || `status exited ${code}` };
|
|
11
|
+
}
|
|
12
|
+
function newer() {
|
|
13
|
+
const latest = latestVersion(CLI_PKG, 3_000);
|
|
14
|
+
return latest && isNewer(latest, cliVersion()) ? { current: cliVersion(), latest } : null;
|
|
15
|
+
}
|
|
16
|
+
export async function status(args) {
|
|
17
|
+
const parsed = parseFlags(args, { json: { type: 'boolean' } }, usages.status);
|
|
18
|
+
if (!parsed)
|
|
19
|
+
return 2;
|
|
20
|
+
if (parsed.values.help) {
|
|
21
|
+
console.log(usages.status);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
const json = parsed.values.json === true;
|
|
25
|
+
const base = context();
|
|
26
|
+
const tools = {};
|
|
27
|
+
for (const name of readConfig().tools) {
|
|
28
|
+
const entry = findTool(name);
|
|
29
|
+
if (!entry)
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const tool = await loadTool(entry, base.configDir);
|
|
33
|
+
const command = tool.commands?.status;
|
|
34
|
+
if (!command)
|
|
35
|
+
continue;
|
|
36
|
+
if (json) {
|
|
37
|
+
let out = '';
|
|
38
|
+
const code = await command(['--json'], { ...base, log: (line) => (out += line + '\n') });
|
|
39
|
+
tools[name] = toolStatusJson(code, out);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(name);
|
|
43
|
+
await command([], { ...base, log: (line) => console.log(line.split('\n').map((l) => ` ${l}`).join('\n')) });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const message = err.message;
|
|
48
|
+
if (json)
|
|
49
|
+
tools[name] = { error: message };
|
|
50
|
+
else
|
|
51
|
+
console.log(`${name}: ${message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const update = newer();
|
|
55
|
+
if (json)
|
|
56
|
+
console.log(JSON.stringify({ tools, update }, null, 2));
|
|
57
|
+
else if (update)
|
|
58
|
+
console.log(`Update: ${update.latest} is out (you have ${update.current}). Run vsf update.`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ToolEntry } from '../registry.ts';
|
|
2
|
+
import type { Tool } from '../tool.ts';
|
|
3
|
+
export declare function listTools(): number;
|
|
4
|
+
export declare function addTool(name: string | undefined): Promise<number>;
|
|
5
|
+
export declare function removeTool(name: string | undefined): Promise<number>;
|
|
6
|
+
export declare function toolUsage(name: string, tool: Tool): string;
|
|
7
|
+
export declare function routeTool(sub: string | undefined, commands: Record<string, unknown> | undefined): 'help' | 'unknown' | 'run';
|
|
8
|
+
export declare function toolHelp(entry: ToolEntry, args: string[]): Promise<number>;
|
|
9
|
+
export declare function runToolCommand(entry: ToolEntry, args: string[]): Promise<number>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { context, readConfig, writeConfig } from "../config.js";
|
|
2
|
+
import { fetchTool, loadTool } from "../loader.js";
|
|
3
|
+
import { findTool, registry } from "../registry.js";
|
|
4
|
+
import { needsTerminal, wantsHelp } from "./args.js";
|
|
5
|
+
export function listTools() {
|
|
6
|
+
const installed = new Set(readConfig().tools);
|
|
7
|
+
const width = Math.max(...registry.map((t) => t.name.length));
|
|
8
|
+
for (const t of registry) {
|
|
9
|
+
const tag = installed.has(t.name) ? ' (added)' : t.status === 'planned' ? ' (coming soon)' : '';
|
|
10
|
+
console.log(`${t.name.padEnd(width)} ${t.summary}${tag}`);
|
|
11
|
+
}
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
function lookup(name, verb) {
|
|
15
|
+
if (!name) {
|
|
16
|
+
console.error(`usage: vsf ${verb} <tool>`);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const tool = findTool(name);
|
|
20
|
+
if (!tool)
|
|
21
|
+
console.error(`unknown tool: ${name}. Run vsf tools to see the list.`);
|
|
22
|
+
return tool ?? null;
|
|
23
|
+
}
|
|
24
|
+
export async function addTool(name) {
|
|
25
|
+
if (needsTerminal('add', 'Run it in a terminal.'))
|
|
26
|
+
return 2;
|
|
27
|
+
const entry = lookup(name, 'add');
|
|
28
|
+
if (!entry)
|
|
29
|
+
return 2;
|
|
30
|
+
if (entry.status === 'planned') {
|
|
31
|
+
console.error(`${entry.name} is not released yet.`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const ctx = context();
|
|
35
|
+
if (!fetchTool(entry, ctx.configDir)) {
|
|
36
|
+
console.error(`could not download ${entry.pkg}`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await (await loadTool(entry, ctx.configDir)).install(ctx);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(`${entry.name}: ${err.message}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const config = readConfig();
|
|
47
|
+
if (!config.tools.includes(entry.name))
|
|
48
|
+
writeConfig({ ...config, tools: [...config.tools, entry.name] });
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
export async function removeTool(name) {
|
|
52
|
+
if (needsTerminal('remove', 'Run it in a terminal.'))
|
|
53
|
+
return 2;
|
|
54
|
+
const entry = lookup(name, 'remove');
|
|
55
|
+
if (!entry)
|
|
56
|
+
return 2;
|
|
57
|
+
const ctx = context();
|
|
58
|
+
const config = readConfig();
|
|
59
|
+
if (!config.tools.includes(entry.name)) {
|
|
60
|
+
console.error(`${entry.name} is not added.`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
await (await loadTool(entry, ctx.configDir)).uninstall(ctx);
|
|
64
|
+
writeConfig({ ...config, tools: config.tools.filter((t) => t !== entry.name) });
|
|
65
|
+
console.log(`${entry.name} removed.`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
const INTERNAL = ['tick', 'recall', 'steps'];
|
|
69
|
+
export function toolUsage(name, tool) {
|
|
70
|
+
const names = Object.keys(tool.commands ?? {}).filter((n) => !INTERNAL.includes(n));
|
|
71
|
+
return `Usage: vsf ${name} <${names.join('|')}>\n\nvsf ${name} <command> --help shows a command's usage.`;
|
|
72
|
+
}
|
|
73
|
+
// Decide what `vsf <tool> <sub>` means: help, an unknown command, or run it.
|
|
74
|
+
export function routeTool(sub, commands) {
|
|
75
|
+
if (sub === undefined || sub === 'help' || sub === '--help' || sub === '-h')
|
|
76
|
+
return 'help';
|
|
77
|
+
return commands && Object.hasOwn(commands, sub) ? 'run' : 'unknown';
|
|
78
|
+
}
|
|
79
|
+
// The tool's own help command if it has one, else a generated listing.
|
|
80
|
+
export async function toolHelp(entry, args) {
|
|
81
|
+
const ctx = context();
|
|
82
|
+
const tool = await loadTool(entry, ctx.configDir);
|
|
83
|
+
if (tool.commands && Object.hasOwn(tool.commands, 'help'))
|
|
84
|
+
return tool.commands.help(args, ctx);
|
|
85
|
+
console.log(toolUsage(entry.name, tool));
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
export async function runToolCommand(entry, args) {
|
|
89
|
+
const ctx = context();
|
|
90
|
+
const [sub, ...rest] = args;
|
|
91
|
+
// Help never needs the tool to be added; it is fetched as needed.
|
|
92
|
+
const asksHelp = sub === 'help' || sub === '--help' || sub === '-h' || (sub !== undefined && wantsHelp(rest));
|
|
93
|
+
if (!asksHelp && !readConfig().tools.includes(entry.name)) {
|
|
94
|
+
console.error(`${entry.name} is not added. Run vsf add ${entry.name}`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const tool = await loadTool(entry, ctx.configDir);
|
|
98
|
+
const route = routeTool(sub, tool.commands);
|
|
99
|
+
if (route === 'help')
|
|
100
|
+
return toolHelp(entry, sub === undefined ? [] : rest);
|
|
101
|
+
if (route === 'unknown') {
|
|
102
|
+
console.error(`unknown command: ${entry.name} ${sub}\n\n${toolUsage(entry.name, tool)}`);
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
// Internal commands are run by schedulers, not people: help shows the tool's usage.
|
|
106
|
+
if (INTERNAL.includes(sub) && wantsHelp(rest)) {
|
|
107
|
+
console.log(toolUsage(entry.name, tool));
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
return tool.commands[sub](rest, ctx);
|
|
111
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { configDir, readConfig } from "../config.js";
|
|
6
|
+
import { npm } from "../loader.js";
|
|
7
|
+
import { findTool } from "../registry.js";
|
|
8
|
+
import { CLI_PKG, cliVersion, isNewer } from "../version.js";
|
|
9
|
+
// --prefer-online: right after a release npm's local metadata cache can still
|
|
10
|
+
// name the previous version as latest.
|
|
11
|
+
const QUIET = ['--silent', '--no-audit', '--no-fund', '--prefer-online'];
|
|
12
|
+
export function latestVersion(pkg, timeout = 15_000) {
|
|
13
|
+
const r = spawnSync('npm', ['view', '--prefer-online', pkg, 'version'], { encoding: 'utf8', timeout });
|
|
14
|
+
return r.status === 0 ? r.stdout.trim() || null : null;
|
|
15
|
+
}
|
|
16
|
+
function installedToolVersion(pkg) {
|
|
17
|
+
const path = join(configDir(), 'tools', 'node_modules', pkg, 'package.json');
|
|
18
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')).version : null;
|
|
19
|
+
}
|
|
20
|
+
// Running from a checkout of the repo rather than an npm install.
|
|
21
|
+
const fromSource = () => fileURLToPath(import.meta.url).endsWith('.ts');
|
|
22
|
+
export async function update() {
|
|
23
|
+
let failed = false;
|
|
24
|
+
let changed = false;
|
|
25
|
+
// One install for every outdated tool: they share a folder, so npm may move
|
|
26
|
+
// the others too. Report by comparing versions before and after.
|
|
27
|
+
const tools = readConfig().tools.map((name) => findTool(name)).filter((e) => !!e);
|
|
28
|
+
const before = new Map(tools.map((e) => [e.name, installedToolVersion(e.pkg)]));
|
|
29
|
+
const targets = [];
|
|
30
|
+
for (const entry of tools) {
|
|
31
|
+
const latest = latestVersion(entry.pkg);
|
|
32
|
+
if (!latest) {
|
|
33
|
+
console.log(`${entry.name}: could not check npm`);
|
|
34
|
+
failed = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const current = before.get(entry.name);
|
|
38
|
+
if (!current || isNewer(latest, current))
|
|
39
|
+
targets.push(`${entry.pkg}@${latest}`);
|
|
40
|
+
}
|
|
41
|
+
if (targets.length) {
|
|
42
|
+
if (!npm(['install', ...QUIET, '--prefix', join(configDir(), 'tools'), ...targets])) {
|
|
43
|
+
console.log('Tool update failed.');
|
|
44
|
+
failed = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const entry of tools) {
|
|
48
|
+
const was = before.get(entry.name);
|
|
49
|
+
const now = installedToolVersion(entry.pkg);
|
|
50
|
+
if (now && now !== was) {
|
|
51
|
+
console.log(`${entry.name} ${was ?? 'not downloaded'} → ${now}`);
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const current = cliVersion();
|
|
56
|
+
const latest = latestVersion(CLI_PKG);
|
|
57
|
+
if (!latest) {
|
|
58
|
+
console.log('vsf: could not check npm');
|
|
59
|
+
failed = true;
|
|
60
|
+
}
|
|
61
|
+
else if (isNewer(latest, current)) {
|
|
62
|
+
if (fromSource()) {
|
|
63
|
+
console.log(`vsf ${latest} is out; this copy runs from source, so pull the repo instead.`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (npm(['install', '-g', ...QUIET, `${CLI_PKG}@${latest}`])) {
|
|
67
|
+
console.log(`vsf ${current} → ${latest}`);
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(`vsf: update failed. Run: npm i -g ${CLI_PKG}@latest`);
|
|
72
|
+
failed = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!changed && !failed)
|
|
77
|
+
console.log(`Already up to date (vsf ${current}).`);
|
|
78
|
+
return failed ? 1 : 0;
|
|
79
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolContext } from './tool.ts';
|
|
2
|
+
export declare function configDir(): string;
|
|
3
|
+
interface Config {
|
|
4
|
+
tools: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function readConfig(): Config;
|
|
7
|
+
export declare function writeConfig(config: Config): void;
|
|
8
|
+
export declare function context(quiet?: boolean, defaults?: boolean): ToolContext;
|
|
9
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { defaultsUi, ui } from "./ui.js";
|
|
5
|
+
// ~/.config/voidshift, not ~/.voidshift: the older private daemon owns that.
|
|
6
|
+
export function configDir() {
|
|
7
|
+
return process.env.VOIDSHIFT_HOME ?? join(homedir(), '.config', 'voidshift');
|
|
8
|
+
}
|
|
9
|
+
const configPath = () => join(configDir(), 'config.json');
|
|
10
|
+
export function readConfig() {
|
|
11
|
+
if (!existsSync(configPath()))
|
|
12
|
+
return { tools: [] };
|
|
13
|
+
return JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
export function writeConfig(config) {
|
|
16
|
+
mkdirSync(configDir(), { recursive: true });
|
|
17
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2) + '\n');
|
|
18
|
+
}
|
|
19
|
+
// `quiet` routes tool output through the setup UI instead of plain lines.
|
|
20
|
+
export function context(quiet = false, defaults = false) {
|
|
21
|
+
return {
|
|
22
|
+
defaults,
|
|
23
|
+
home: homedir(),
|
|
24
|
+
configDir: configDir(),
|
|
25
|
+
selfCommand: [process.execPath, realpathSync(process.argv[1] ?? '')],
|
|
26
|
+
log: quiet ? (line) => ui.info(line) : (line) => console.log(line),
|
|
27
|
+
ui: defaults ? defaultsUi : ui,
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/dotted.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type Obj = Record<string, unknown>;
|
|
2
|
+
export declare function getPath(obj: unknown, path: string[]): unknown;
|
|
3
|
+
export declare function setPath(obj: Obj, path: string[], value: unknown): Obj;
|
|
4
|
+
export declare function unsetPath(obj: Obj, path: string[]): Obj;
|
|
5
|
+
export declare function flatten(obj: unknown, prefix?: string): [string, unknown][];
|
|
6
|
+
export declare function parseValue(text: string): unknown;
|
|
7
|
+
export declare function mask(key: string, value: unknown): unknown;
|
|
8
|
+
export declare function maskDeep(value: unknown, key?: string): unknown;
|
|
9
|
+
export {};
|
package/dist/dotted.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Helpers for `vsf config`: address nested settings by a dotted key.
|
|
2
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
3
|
+
const isIndex = (k) => /^\d+$/.test(k);
|
|
4
|
+
const isContainer = (v) => typeof v === 'object' && v !== null;
|
|
5
|
+
export function getPath(obj, path) {
|
|
6
|
+
let cur = obj;
|
|
7
|
+
for (const k of path) {
|
|
8
|
+
if (Array.isArray(cur)) {
|
|
9
|
+
if (!isIndex(k) || Number(k) >= cur.length)
|
|
10
|
+
return undefined;
|
|
11
|
+
cur = cur[Number(k)];
|
|
12
|
+
}
|
|
13
|
+
else if (isObj(cur) && Object.hasOwn(cur, k)) {
|
|
14
|
+
cur = cur[k];
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return cur;
|
|
21
|
+
}
|
|
22
|
+
// Returns a new object; the input is left alone. Non-container values on the way are replaced.
|
|
23
|
+
// A numeric segment into an array addresses that element (memory.homes.0.dir).
|
|
24
|
+
export function setPath(obj, path, value) {
|
|
25
|
+
const out = structuredClone(obj);
|
|
26
|
+
let cur = out;
|
|
27
|
+
const slot = (c, k) => (Array.isArray(c) && isIndex(k) ? Number(k) : k);
|
|
28
|
+
for (const k of path.slice(0, -1)) {
|
|
29
|
+
const at = slot(cur, k);
|
|
30
|
+
const next = cur[at];
|
|
31
|
+
if (!isContainer(next))
|
|
32
|
+
cur[at] = {};
|
|
33
|
+
cur = cur[at];
|
|
34
|
+
}
|
|
35
|
+
const last = path[path.length - 1];
|
|
36
|
+
if (last !== undefined)
|
|
37
|
+
cur[slot(cur, last)] = value;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export function unsetPath(obj, path) {
|
|
41
|
+
const out = structuredClone(obj);
|
|
42
|
+
const parent = path.length > 1 ? getPath(out, path.slice(0, -1)) : out;
|
|
43
|
+
const last = path[path.length - 1];
|
|
44
|
+
if (last === undefined)
|
|
45
|
+
return out;
|
|
46
|
+
if (Array.isArray(parent)) {
|
|
47
|
+
if (isIndex(last) && Number(last) < parent.length)
|
|
48
|
+
parent.splice(Number(last), 1);
|
|
49
|
+
}
|
|
50
|
+
else if (isObj(parent)) {
|
|
51
|
+
delete parent[last];
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
// Arrays and empty objects are leaves.
|
|
56
|
+
export function flatten(obj, prefix = '') {
|
|
57
|
+
if (!isObj(obj) || Object.keys(obj).length === 0)
|
|
58
|
+
return prefix ? [[prefix, obj]] : [];
|
|
59
|
+
return Object.entries(obj).flatMap(([k, v]) => flatten(v, prefix ? `${prefix}.${k}` : k));
|
|
60
|
+
}
|
|
61
|
+
export function parseValue(text) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(text);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Matched against one key segment, anchored so maxTokens, keywords and
|
|
70
|
+
// keyboard stay visible while apiKey, key, accessToken, clientSecret hide.
|
|
71
|
+
const SECRET = /key$|token$|secret|password|passphrase/i;
|
|
72
|
+
const last = (key) => key.split('.').pop() ?? key;
|
|
73
|
+
export function mask(key, value) {
|
|
74
|
+
return SECRET.test(last(key)) ? '****' : value;
|
|
75
|
+
}
|
|
76
|
+
// Mask secrets inside a nested value. A secret-looking key hides its whole
|
|
77
|
+
// subtree; arrays are walked too. `key` is the key the value sits under.
|
|
78
|
+
export function maskDeep(value, key = '') {
|
|
79
|
+
if (key && SECRET.test(last(key)))
|
|
80
|
+
return '****';
|
|
81
|
+
if (Array.isArray(value))
|
|
82
|
+
return value.map((v) => maskDeep(v));
|
|
83
|
+
if (isObj(value))
|
|
84
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskDeep(v, k)]));
|
|
85
|
+
return value;
|
|
86
|
+
}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolEntry } from './registry.ts';
|
|
2
|
+
import type { Tool } from './tool.ts';
|
|
3
|
+
export declare function locate(entry: ToolEntry, configDir: string): string | null;
|
|
4
|
+
export declare function npm(args: string[]): boolean;
|
|
5
|
+
export declare function fetchTool(entry: ToolEntry, configDir: string): boolean;
|
|
6
|
+
export declare function loadTool(entry: ToolEntry, configDir: string): Promise<Tool>;
|