@ycs77/devbox 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cli.js +293 -0
  4. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Lucas Yang <yangchenshin77@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @ycs77/devbox
2
+
3
+ ## License
4
+
5
+ [MIT LICENSE](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { defineCommand, runCommand } from "citty";
5
+ import { randomUUID } from "node:crypto";
6
+ import { link, lstat, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { isAbsolute, join, parse, relative, sep } from "node:path";
9
+ import { execFile } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+
12
+ //#region src/result.ts
13
+ function success(value) {
14
+ return {
15
+ ok: true,
16
+ value
17
+ };
18
+ }
19
+ function failure(error) {
20
+ return {
21
+ ok: false,
22
+ error
23
+ };
24
+ }
25
+
26
+ //#endregion
27
+ //#region src/host.ts
28
+ const execFile$1 = promisify(execFile);
29
+ const supportedHostAction = "Run Devbox from 64-bit WSL2 with Docker Desktop and Docker Compose available.";
30
+ function currentHostEnvironment() {
31
+ return {
32
+ platform: process.platform,
33
+ arch: process.arch,
34
+ procVersion: () => readFile("/proc/version", "utf8"),
35
+ run: async (file, args) => {
36
+ await execFile$1(file, args);
37
+ }
38
+ };
39
+ }
40
+ async function validateSupportedHost(environment = currentHostEnvironment()) {
41
+ if (environment.platform !== "linux" || environment.arch !== "x64") return failure({
42
+ kind: "validation",
43
+ code: "unsupported-host",
44
+ observed: `Devbox supports only WSL2 linux/amd64; detected ${environment.platform}/${environment.arch}.`,
45
+ nextAction: supportedHostAction
46
+ });
47
+ let procVersion;
48
+ try {
49
+ procVersion = await environment.procVersion();
50
+ } catch {
51
+ return failure({
52
+ kind: "validation",
53
+ code: "unsupported-host",
54
+ observed: "Devbox could not verify that this Linux host is WSL2.",
55
+ nextAction: supportedHostAction
56
+ });
57
+ }
58
+ if (!/microsoft|wsl/i.test(procVersion)) return failure({
59
+ kind: "validation",
60
+ code: "unsupported-host",
61
+ observed: "Devbox supports WSL2, but this Linux host is not WSL2.",
62
+ nextAction: supportedHostAction
63
+ });
64
+ for (const [file, args, description] of [[
65
+ "docker",
66
+ ["--version"],
67
+ "Docker"
68
+ ], [
69
+ "docker",
70
+ ["compose", "version"],
71
+ "Docker Compose"
72
+ ]]) try {
73
+ await environment.run(file, args);
74
+ } catch {
75
+ return failure({
76
+ kind: "validation",
77
+ code: "missing-host-prerequisite",
78
+ observed: `${description} is unavailable.`,
79
+ nextAction: supportedHostAction
80
+ });
81
+ }
82
+ return success(void 0);
83
+ }
84
+
85
+ //#endregion
86
+ //#region src/project.ts
87
+ function projectStateDirectory(projectRoot, devboxHome) {
88
+ const root = parse(projectRoot).root;
89
+ const pathWithinRoot = relative(root, projectRoot);
90
+ if (!isAbsolute(projectRoot) || pathWithinRoot === ".." || pathWithinRoot.startsWith(`..${sep}`)) throw new TypeError("Project root must be an absolute path.");
91
+ return join(devboxHome, "projects", pathWithinRoot === "" ? "root" : pathWithinRoot.split(sep).map(escapePathSegment).join("-"));
92
+ }
93
+ function escapePathSegment(segment) {
94
+ const bytes = new TextEncoder().encode(segment);
95
+ let escaped = "";
96
+ for (const byte of bytes) if (byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 || byte >= 48 && byte <= 57 || byte === 45 || byte === 95) escaped += String.fromCharCode(byte);
97
+ else escaped += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
98
+ return escaped;
99
+ }
100
+ async function initializeProject(input = {}) {
101
+ const projectRoot = input.root ?? process.cwd();
102
+ const rootCheck = await validateProjectRoot(projectRoot);
103
+ if (!rootCheck.ok) return rootCheck;
104
+ if (input.signal?.aborted) throw new InterruptedError();
105
+ const hostCheck = await (input.validateHost ?? validateSupportedHost)();
106
+ if (!hostCheck.ok) return hostCheck;
107
+ if (input.signal?.aborted) throw new InterruptedError();
108
+ const stateDirectory = projectStateDirectory(projectRoot, input.devboxHome ?? join(homedir(), ".devbox"));
109
+ const configurationPath = join(stateDirectory, "config.yaml");
110
+ try {
111
+ await mkdir(stateDirectory, {
112
+ recursive: true,
113
+ mode: 448
114
+ });
115
+ } catch {
116
+ return failure({
117
+ kind: "operational",
118
+ code: "state-directory-unavailable",
119
+ observed: `Devbox could not create its Project state directory: ${stateDirectory}.`,
120
+ nextAction: "Check write access to ~/.devbox and run devbox init again."
121
+ });
122
+ }
123
+ if (input.signal?.aborted) throw new InterruptedError();
124
+ const temporaryPath = join(stateDirectory, `.config-${process.pid}-${randomUUID()}.tmp`);
125
+ const content = `version: 1\nprojectRoot: ${JSON.stringify(projectRoot)}\n`;
126
+ try {
127
+ await writeFile(temporaryPath, content, {
128
+ encoding: "utf8",
129
+ flag: "wx",
130
+ mode: 384
131
+ });
132
+ await link(temporaryPath, configurationPath);
133
+ await unlink(temporaryPath);
134
+ return success({
135
+ root: projectRoot,
136
+ stateDirectory,
137
+ created: true
138
+ });
139
+ } catch (cause) {
140
+ await unlink(temporaryPath).catch(() => void 0);
141
+ if (isFileExistsError(cause)) return success({
142
+ root: projectRoot,
143
+ stateDirectory,
144
+ created: false
145
+ });
146
+ return failure({
147
+ kind: "operational",
148
+ code: "registration-write-failed",
149
+ observed: `Devbox could not register this Project in ${stateDirectory}.`,
150
+ nextAction: "Check write access to ~/.devbox and run devbox init again."
151
+ });
152
+ }
153
+ }
154
+ async function validateProjectRoot(projectRoot) {
155
+ if (!isAbsolute(projectRoot)) return failure({
156
+ kind: "validation",
157
+ code: "invalid-project-root",
158
+ observed: `The current Project directory is not absolute: ${projectRoot}.`,
159
+ nextAction: "Run devbox init from an existing directory."
160
+ });
161
+ try {
162
+ if (!(await lstat(projectRoot)).isDirectory()) return failure({
163
+ kind: "validation",
164
+ code: "invalid-project-root",
165
+ observed: `The current Project path is not a directory: ${projectRoot}.`,
166
+ nextAction: "Run devbox init from an existing directory."
167
+ });
168
+ } catch {
169
+ return failure({
170
+ kind: "validation",
171
+ code: "missing-project-root",
172
+ observed: `The current Project directory does not exist: ${projectRoot}.`,
173
+ nextAction: "Change to an existing directory and run devbox init again."
174
+ });
175
+ }
176
+ return success(void 0);
177
+ }
178
+ function isFileExistsError(error) {
179
+ return typeof error === "object" && error !== null && error.code === "EEXIST";
180
+ }
181
+ var InterruptedError = class extends Error {
182
+ constructor() {
183
+ super("Devbox command interrupted.");
184
+ }
185
+ };
186
+
187
+ //#endregion
188
+ //#region src/cli.ts
189
+ const initCommand = defineCommand({
190
+ meta: {
191
+ name: "init",
192
+ description: "Register the current directory as a Devbox Project."
193
+ },
194
+ run: async (context) => {
195
+ if (context.rawArgs.length > 0) return failure({
196
+ kind: "usage",
197
+ code: "unexpected-init-arguments",
198
+ observed: "devbox init does not accept arguments.",
199
+ nextAction: "Run devbox init without arguments."
200
+ });
201
+ const registration = await context.data.initializeProject();
202
+ if (!registration.ok) return registration;
203
+ return {
204
+ ok: true,
205
+ value: {
206
+ projectRoot: registration.value.root,
207
+ created: registration.value.created
208
+ }
209
+ };
210
+ }
211
+ });
212
+ const rootCommand = defineCommand({
213
+ meta: {
214
+ name: "devbox",
215
+ description: "Prepare and run the Devbox Node Toolchain."
216
+ },
217
+ args: { command: {
218
+ type: "positional",
219
+ required: true
220
+ } },
221
+ run: async (context) => {
222
+ if (context.args.command !== "init") return failure({
223
+ kind: "usage",
224
+ code: "unknown-command",
225
+ observed: `Unknown command: ${context.args.command}.`,
226
+ nextAction: "Run devbox --help to see available commands."
227
+ });
228
+ const { result } = await runCommand(initCommand, {
229
+ rawArgs: context.rawArgs.slice(1),
230
+ data: context.data
231
+ });
232
+ return result;
233
+ }
234
+ });
235
+ async function runCli(rawArgs, dependencies = { initializeProject }) {
236
+ if (rawArgs.length === 1 && (rawArgs[0] === "--help" || rawArgs[0] === "-h")) return {
237
+ ok: true,
238
+ value: {
239
+ projectRoot: "",
240
+ created: false
241
+ }
242
+ };
243
+ try {
244
+ const { result } = await runCommand(rootCommand, {
245
+ rawArgs: [...rawArgs],
246
+ data: dependencies
247
+ });
248
+ return result;
249
+ } catch (error) {
250
+ if (error instanceof InterruptedError) throw error;
251
+ return failure({
252
+ kind: "usage",
253
+ code: "invalid-command-usage",
254
+ observed: error instanceof Error ? error.message : "Invalid Devbox command usage.",
255
+ nextAction: "Run devbox --help to see available commands."
256
+ });
257
+ }
258
+ }
259
+ function present(result, output = process) {
260
+ if (!result.ok) {
261
+ output.stderr.write(`${result.error.observed}\n${result.error.nextAction}\n`);
262
+ return result.error.kind === "usage" ? 2 : 1;
263
+ }
264
+ if (result.value.projectRoot === "") {
265
+ output.stdout.write("Usage: devbox init\n\nRegister the current directory as a Devbox Project.\n");
266
+ return 0;
267
+ }
268
+ output.stdout.write(result.value.created ? `Registered Project: ${result.value.projectRoot}\n` : `Project is already registered: ${result.value.projectRoot}\n`);
269
+ return 0;
270
+ }
271
+ async function main() {
272
+ const abortController = new AbortController();
273
+ const interrupt = () => abortController.abort();
274
+ process.once("SIGINT", interrupt);
275
+ try {
276
+ const result = await runCli(process.argv.slice(2), { initializeProject: () => initializeProject({ signal: abortController.signal }) });
277
+ process.exitCode = present(result);
278
+ } catch (error) {
279
+ if (error instanceof InterruptedError || abortController.signal.aborted) {
280
+ process.stderr.write("Devbox command interrupted.\n");
281
+ process.exitCode = 130;
282
+ return;
283
+ }
284
+ process.stderr.write("Devbox encountered an unexpected failure. Run devbox init again.\n");
285
+ process.exitCode = 1;
286
+ } finally {
287
+ process.off("SIGINT", interrupt);
288
+ }
289
+ }
290
+ if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) await main();
291
+
292
+ //#endregion
293
+ export { present, runCli };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@ycs77/devbox",
3
+ "version": "0.1.0",
4
+ "description": "Devbox Node-only MVP CLI",
5
+ "homepage": "https://github.com/ycs77/devbox",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ycs77/devbox.git",
10
+ "directory": "packages/devbox"
11
+ },
12
+ "bin": {
13
+ "devbox": "./dist/cli.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "citty": "^0.2.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^26.0.1",
27
+ "oxfmt": "^0.62.0",
28
+ "oxlint": "^1.77.0",
29
+ "oxlint-tsgolint": "^7.0.2001",
30
+ "tsdown": "^0.15.4",
31
+ "typescript": "^7.0.2",
32
+ "vitest": "^4.1.10"
33
+ },
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "lint": "oxlint",
40
+ "fmt": "oxfmt",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "vitest",
43
+ "check": "pnpm lint && pnpm fmt --check && pnpm typecheck && pnpm test --run"
44
+ }
45
+ }