devault-cli 0.1.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 +132 -0
- package/dist/cli.js +266 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# d(evault)
|
|
2
|
+
|
|
3
|
+
we all work on too many projects now. why do we have to remember (and type) dev commands?
|
|
4
|
+
|
|
5
|
+
one project has `bun dev`, the other `pnpm dev`, another `cargo run`, etc.
|
|
6
|
+
|
|
7
|
+
what if all you needed to type was `d`?
|
|
8
|
+
|
|
9
|
+
## installation
|
|
10
|
+
|
|
11
|
+
requires bun.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i -g devault-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## usage
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
d
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
running `d` for the first time in a directory will ask you for the command it should run. future runs will immediately alias to the command you've entered.
|
|
24
|
+
|
|
25
|
+
## commands
|
|
26
|
+
|
|
27
|
+
save a command for the current directory:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
d set "bun dev"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
run it later:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
d
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
see what `d` thinks this directory is:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
d show
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
forget the saved command:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
d forget
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
list everything you've saved:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
d list
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## examples
|
|
58
|
+
|
|
59
|
+
the command can be whatever you would have typed yourself:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
d set "bun dev"
|
|
63
|
+
d set "pnpm dev"
|
|
64
|
+
d set "npm run dev"
|
|
65
|
+
d set "yarn dev"
|
|
66
|
+
d set "mise run dev"
|
|
67
|
+
d set "make dev"
|
|
68
|
+
d set "just dev"
|
|
69
|
+
d set "docker compose up"
|
|
70
|
+
d set "cargo run"
|
|
71
|
+
d set "go run ."
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
devault does not know what a dev server is. it just remembers the command you told it.
|
|
75
|
+
|
|
76
|
+
## first run
|
|
77
|
+
|
|
78
|
+
if nothing is saved yet, `d` will suggest obvious commands from the files in the project.
|
|
79
|
+
|
|
80
|
+
for node projects it looks at lockfiles and package scripts, so a bun project with a `dev` script gets:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
bun dev
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
for other projects it can suggest things like:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
mise run dev
|
|
90
|
+
make dev
|
|
91
|
+
docker compose up
|
|
92
|
+
cargo run
|
|
93
|
+
go run .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
you can always ignore the suggestions and type the command yourself.
|
|
97
|
+
|
|
98
|
+
## non-interactive
|
|
99
|
+
|
|
100
|
+
set `D_NO_PROMPT=1` if you want `d` to fail instead of asking questions:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
D_NO_PROMPT=1 d
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## development
|
|
107
|
+
|
|
108
|
+
run with bun:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
bun run dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
build:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
bun run build
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
the built cli is bundled into:
|
|
121
|
+
|
|
122
|
+
```txt
|
|
123
|
+
dist/cli.js
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## notes
|
|
127
|
+
|
|
128
|
+
this is intentionally not a task runner, process manager, env manager, monorepo thing, shell integration, or runtime. what makes it good is it's everything-agnostic. you tell `d` what to run, and it does so.
|
|
129
|
+
|
|
130
|
+
it is a glorified auto-alias with a memory.
|
|
131
|
+
|
|
132
|
+
that is the whole point.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = import.meta.require;
|
|
19
|
+
|
|
20
|
+
// src/suggest.ts
|
|
21
|
+
var exports_suggest = {};
|
|
22
|
+
__export(exports_suggest, {
|
|
23
|
+
suggestCommands: () => suggestCommands,
|
|
24
|
+
directoryHasFiles: () => directoryHasFiles
|
|
25
|
+
});
|
|
26
|
+
import { readdir, readFile } from "fs/promises";
|
|
27
|
+
import { access } from "fs/promises";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
async function exists(path) {
|
|
30
|
+
try {
|
|
31
|
+
await access(path);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function suggestCommands(root) {
|
|
38
|
+
const suggestions = new Set;
|
|
39
|
+
await addNodeSuggestions(root, suggestions);
|
|
40
|
+
await addGenericSuggestions(root, suggestions);
|
|
41
|
+
return [...suggestions];
|
|
42
|
+
}
|
|
43
|
+
async function addNodeSuggestions(root, suggestions) {
|
|
44
|
+
const packageJsonPath = join(root, "package.json");
|
|
45
|
+
if (!await exists(packageJsonPath))
|
|
46
|
+
return;
|
|
47
|
+
const packageManager = await detectPackageManager(root);
|
|
48
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
49
|
+
for (const script of preferredScripts) {
|
|
50
|
+
if (packageJson.scripts?.[script]) {
|
|
51
|
+
suggestions.add(packageManager === "npm" ? `npm run ${script}` : `${packageManager} ${script}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function detectPackageManager(root) {
|
|
56
|
+
if (await exists(join(root, "bun.lock")) || await exists(join(root, "bun.lockb")))
|
|
57
|
+
return "bun";
|
|
58
|
+
if (await exists(join(root, "pnpm-lock.yaml")))
|
|
59
|
+
return "pnpm";
|
|
60
|
+
if (await exists(join(root, "yarn.lock")))
|
|
61
|
+
return "yarn";
|
|
62
|
+
if (await exists(join(root, "package-lock.json")))
|
|
63
|
+
return "npm";
|
|
64
|
+
return "npm";
|
|
65
|
+
}
|
|
66
|
+
async function addGenericSuggestions(root, suggestions) {
|
|
67
|
+
if (await hasMiseDev(root))
|
|
68
|
+
suggestions.add("mise run dev");
|
|
69
|
+
if (await hasMakeDev(root))
|
|
70
|
+
suggestions.add("make dev");
|
|
71
|
+
if (await exists(join(root, "compose.yaml")) || await exists(join(root, "docker-compose.yml"))) {
|
|
72
|
+
suggestions.add("docker compose up");
|
|
73
|
+
}
|
|
74
|
+
if (await exists(join(root, "Cargo.toml")))
|
|
75
|
+
suggestions.add("cargo run");
|
|
76
|
+
if (await exists(join(root, "go.mod")))
|
|
77
|
+
suggestions.add("go run .");
|
|
78
|
+
}
|
|
79
|
+
async function hasMiseDev(root) {
|
|
80
|
+
for (const file of ["mise.toml", ".mise.toml"]) {
|
|
81
|
+
const path = join(root, file);
|
|
82
|
+
if (await exists(path) && (await readFile(path, "utf8")).includes("[tasks.dev]"))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
async function hasMakeDev(root) {
|
|
88
|
+
for (const file of ["Makefile", "makefile"]) {
|
|
89
|
+
const path = join(root, file);
|
|
90
|
+
if (!await exists(path))
|
|
91
|
+
continue;
|
|
92
|
+
const contents = await readFile(path, "utf8");
|
|
93
|
+
if (/^dev\s*:/m.test(contents))
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
async function directoryHasFiles(root) {
|
|
99
|
+
return (await readdir(root)).length > 0;
|
|
100
|
+
}
|
|
101
|
+
var preferredScripts;
|
|
102
|
+
var init_suggest = __esm(() => {
|
|
103
|
+
preferredScripts = ["dev", "start", "serve", "watch"];
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/cli.ts
|
|
107
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
108
|
+
import { homedir } from "os";
|
|
109
|
+
import { dirname, join as join2, resolve } from "path";
|
|
110
|
+
import { spawn } from "child_process";
|
|
111
|
+
import { stdin as input, stdout as output } from "process";
|
|
112
|
+
var noPrompt = process.env.D_NO_PROMPT === "1";
|
|
113
|
+
var cwd = resolve(process.cwd());
|
|
114
|
+
var configPath = join2(homedir(), ".config", "devault", "config.json");
|
|
115
|
+
var suspicious = /\b(rm|dropdb|reset|prune|destroy|delete)\b|\bmigrate\s+reset\b/;
|
|
116
|
+
async function main() {
|
|
117
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
118
|
+
if (!cmd)
|
|
119
|
+
return runDefault();
|
|
120
|
+
if (cmd === "set")
|
|
121
|
+
return setCommand(args);
|
|
122
|
+
if (cmd === "show")
|
|
123
|
+
return showCommand();
|
|
124
|
+
if (cmd === "forget")
|
|
125
|
+
return forgetCommand();
|
|
126
|
+
if (cmd === "list")
|
|
127
|
+
return listCommands();
|
|
128
|
+
console.error("usage: d [set <command>|show|forget|list]");
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
async function runDefault() {
|
|
132
|
+
const command = commandFor(cwd);
|
|
133
|
+
if (command) {
|
|
134
|
+
await guard(command, "run");
|
|
135
|
+
return run(cwd, command);
|
|
136
|
+
}
|
|
137
|
+
if (noPrompt) {
|
|
138
|
+
console.error(`d: no command set for ${cwd}`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
const selected = await askForCommand(cwd);
|
|
142
|
+
if (!selected)
|
|
143
|
+
return 1;
|
|
144
|
+
await guard(selected, "save and run");
|
|
145
|
+
saveCommand(cwd, selected);
|
|
146
|
+
return run(cwd, selected);
|
|
147
|
+
}
|
|
148
|
+
async function setCommand(parts) {
|
|
149
|
+
const command = parts.join(" ").trim();
|
|
150
|
+
if (!command) {
|
|
151
|
+
console.error("usage: d set <command>");
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
await guard(command, "save");
|
|
155
|
+
saveCommand(cwd, command);
|
|
156
|
+
console.log(`d: ${cwd} -> ${command}`);
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
function showCommand() {
|
|
160
|
+
const command = commandFor(cwd);
|
|
161
|
+
console.log(`d: ${cwd} -> ${command ?? "none"}`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
function forgetCommand() {
|
|
165
|
+
const config = readConfig();
|
|
166
|
+
const hadCommand = Object.hasOwn(config.projects, cwd);
|
|
167
|
+
delete config.projects[cwd];
|
|
168
|
+
writeConfig(config);
|
|
169
|
+
console.log(hadCommand ? `d: forgot ${cwd}` : `d: nothing saved for ${cwd}`);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
function listCommands() {
|
|
173
|
+
const projects = Object.values(readConfig().projects).sort((a, b) => a.root.localeCompare(b.root));
|
|
174
|
+
if (projects.length === 0) {
|
|
175
|
+
console.log("d: no saved commands");
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
for (const project of projects) {
|
|
179
|
+
console.log(`${project.root} -> ${project.command}`);
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
function commandFor(root) {
|
|
184
|
+
return readConfig().projects[root]?.command;
|
|
185
|
+
}
|
|
186
|
+
function readConfig() {
|
|
187
|
+
return readJson(configPath) ?? { version: 1, projects: {} };
|
|
188
|
+
}
|
|
189
|
+
function readJson(path) {
|
|
190
|
+
if (!existsSync(path))
|
|
191
|
+
return;
|
|
192
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
193
|
+
}
|
|
194
|
+
function saveCommand(root, command) {
|
|
195
|
+
const config = readConfig();
|
|
196
|
+
config.projects[root] = { command, root };
|
|
197
|
+
writeConfig(config);
|
|
198
|
+
}
|
|
199
|
+
function writeConfig(config) {
|
|
200
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
201
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
async function askForCommand(root) {
|
|
205
|
+
const [{ createInterface }, { suggestCommands: suggestCommands2 }] = await Promise.all([
|
|
206
|
+
import("readline/promises"),
|
|
207
|
+
Promise.resolve().then(() => (init_suggest(), exports_suggest))
|
|
208
|
+
]);
|
|
209
|
+
const suggestions = await suggestCommands2(root);
|
|
210
|
+
const rl = createInterface({ input, output });
|
|
211
|
+
try {
|
|
212
|
+
console.log(`d: no command set for ${root}`);
|
|
213
|
+
suggestions.forEach((command, index) => console.log(`${index + 1}. ${command}`));
|
|
214
|
+
console.log(`${suggestions.length + 1}. custom`);
|
|
215
|
+
const answer = (await rl.question("command: ")).trim();
|
|
216
|
+
const choice = Number(answer);
|
|
217
|
+
if (Number.isInteger(choice) && choice >= 1 && choice <= suggestions.length)
|
|
218
|
+
return suggestions[choice - 1];
|
|
219
|
+
if (choice === suggestions.length + 1 || answer === "") {
|
|
220
|
+
const custom = (await rl.question("custom: ")).trim();
|
|
221
|
+
return custom || undefined;
|
|
222
|
+
}
|
|
223
|
+
return answer;
|
|
224
|
+
} finally {
|
|
225
|
+
rl.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function guard(command, action) {
|
|
229
|
+
if (!suspicious.test(command))
|
|
230
|
+
return;
|
|
231
|
+
if (noPrompt)
|
|
232
|
+
throw new Error(`refusing to ${action} suspicious command: ${command}`);
|
|
233
|
+
const { createInterface } = await import("readline/promises");
|
|
234
|
+
const rl = createInterface({ input, output });
|
|
235
|
+
try {
|
|
236
|
+
const answer = (await rl.question(`d: suspicious command, ${action} anyway? ${command} [y/N] `)).trim();
|
|
237
|
+
if (!/^y(es)?$/i.test(answer))
|
|
238
|
+
throw new Error("cancelled");
|
|
239
|
+
} finally {
|
|
240
|
+
rl.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function run(root, command) {
|
|
244
|
+
console.log(`d: ${root} -> ${command}`);
|
|
245
|
+
return new Promise((resolveCode) => {
|
|
246
|
+
const child = spawn(command, { cwd: root, shell: true, stdio: "inherit" });
|
|
247
|
+
child.on("error", (error) => {
|
|
248
|
+
console.error(`d: failed to run command: ${error.message}`);
|
|
249
|
+
resolveCode(1);
|
|
250
|
+
});
|
|
251
|
+
child.on("close", (code, signal) => {
|
|
252
|
+
if (signal) {
|
|
253
|
+
console.error(`d: command stopped by ${signal}`);
|
|
254
|
+
resolveCode(1);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
resolveCode(code ?? 0);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
main().then((code) => {
|
|
262
|
+
process.exitCode = code;
|
|
263
|
+
}).catch((error) => {
|
|
264
|
+
console.error(`d: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devault-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A directory-aware default dev command launcher.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"d": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "rm -rf dist && tsgo --noEmit && bun build src/cli.ts --target=bun --outfile=dist/cli.js && chmod +x dist/cli.js",
|
|
14
|
+
"dev": "tsx src/cli.ts",
|
|
15
|
+
"check": "tsgo --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^24.0.0",
|
|
20
|
+
"@typescript/native-preview": "^7.0.0-dev.20260527.2",
|
|
21
|
+
"tsx": "^4.20.0",
|
|
22
|
+
"typescript": "^5.8.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"bun": ">=1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|