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.
Files changed (3) hide show
  1. package/README.md +132 -0
  2. package/dist/cli.js +266 -0
  3. 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
+ }