@ycs77/devbox 0.1.0 → 0.3.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 +116 -0
- package/bin/devbox.mjs +2 -0
- package/dist/cli.mjs +1998 -0
- package/package.json +32 -11
- package/dist/cli.js +0 -293
package/package.json
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ycs77/devbox",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A containerized devbox for AI agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"agentic",
|
|
8
|
+
"ai",
|
|
9
|
+
"ai-agents",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"cli",
|
|
12
|
+
"codex",
|
|
13
|
+
"container",
|
|
14
|
+
"devcontainer",
|
|
15
|
+
"development",
|
|
16
|
+
"docker",
|
|
17
|
+
"nodejs"
|
|
18
|
+
],
|
|
5
19
|
"homepage": "https://github.com/ycs77/devbox",
|
|
6
20
|
"license": "MIT",
|
|
7
21
|
"repository": {
|
|
8
22
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/ycs77/devbox.git"
|
|
10
|
-
"directory": "packages/devbox"
|
|
23
|
+
"url": "git+https://github.com/ycs77/devbox.git"
|
|
11
24
|
},
|
|
12
25
|
"bin": {
|
|
13
|
-
"devbox": "./
|
|
26
|
+
"devbox": "./bin/devbox.mjs"
|
|
14
27
|
},
|
|
15
28
|
"files": [
|
|
16
29
|
"dist"
|
|
@@ -20,14 +33,20 @@
|
|
|
20
33
|
"access": "public"
|
|
21
34
|
},
|
|
22
35
|
"dependencies": {
|
|
23
|
-
"
|
|
36
|
+
"@clack/prompts": "^1.7.0",
|
|
37
|
+
"cac": "^7.0.0",
|
|
38
|
+
"picocolors": "^1.1.1",
|
|
39
|
+
"yaml": "^2.9.0"
|
|
24
40
|
},
|
|
25
41
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^26.0
|
|
27
|
-
"
|
|
28
|
-
"
|
|
42
|
+
"@types/node": "^26.2.0",
|
|
43
|
+
"bumpp": "^12.2.0",
|
|
44
|
+
"memfs": "^4.68.1",
|
|
45
|
+
"oxfmt": "^0.63.0",
|
|
46
|
+
"oxlint": "^1.78.0",
|
|
29
47
|
"oxlint-tsgolint": "^7.0.2001",
|
|
30
|
-
"tsdown": "^0.
|
|
48
|
+
"tsdown": "^0.22.14",
|
|
49
|
+
"tsx": "^4.23.13",
|
|
31
50
|
"typescript": "^7.0.2",
|
|
32
51
|
"vitest": "^4.1.10"
|
|
33
52
|
},
|
|
@@ -40,6 +59,8 @@
|
|
|
40
59
|
"fmt": "oxfmt",
|
|
41
60
|
"typecheck": "tsc --noEmit",
|
|
42
61
|
"test": "vitest",
|
|
43
|
-
"check": "pnpm lint && pnpm fmt --check && pnpm typecheck && pnpm test --run"
|
|
62
|
+
"check": "pnpm lint && pnpm fmt --check && pnpm typecheck && pnpm test --run",
|
|
63
|
+
"smoke:cli": "node scripts/package-devbox-smoke.mjs",
|
|
64
|
+
"release": "bumpp --commit \"Release v%s\""
|
|
44
65
|
}
|
|
45
66
|
}
|
package/dist/cli.js
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
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 };
|