create-bunli 0.8.0 → 0.8.2
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 +18 -6
- package/dist/cli.js +589 -560
- package/dist/create-project.d.ts +4 -4
- package/dist/create-project.d.ts.map +1 -1
- package/dist/create.d.ts +3 -3
- package/dist/create.d.ts.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +518 -512
- package/dist/steps.d.ts +5 -5
- package/dist/steps.d.ts.map +1 -1
- package/dist/template-engine.d.ts +2 -2
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/templates/advanced/README.md +8 -4
- package/dist/templates/advanced/bunli.config.ts +19 -19
- package/dist/templates/advanced/package.json +8 -4
- package/dist/templates/advanced/src/commands/config.ts +129 -119
- package/dist/templates/advanced/src/commands/init.ts +53 -60
- package/dist/templates/advanced/src/commands/serve.ts +77 -83
- package/dist/templates/advanced/src/commands/validate.ts +58 -66
- package/dist/templates/advanced/src/index.ts +30 -29
- package/dist/templates/advanced/src/utils/config.ts +48 -47
- package/dist/templates/advanced/src/utils/constants.ts +6 -6
- package/dist/templates/advanced/src/utils/glob.ts +29 -28
- package/dist/templates/advanced/src/utils/validator.ts +60 -61
- package/dist/templates/advanced/template.json +2 -6
- package/dist/templates/advanced/tsconfig.json +1 -1
- package/dist/templates/basic/README.md +1 -1
- package/dist/templates/basic/bunli.config.ts +17 -17
- package/dist/templates/basic/package.json +3 -3
- package/dist/templates/basic/src/commands/hello.ts +20 -26
- package/dist/templates/basic/src/index.ts +9 -8
- package/dist/templates/basic/template.json +2 -6
- package/dist/templates/basic/tsconfig.json +1 -1
- package/dist/templates/monorepo/README.md +1 -1
- package/dist/templates/monorepo/bunli.config.ts +21 -21
- package/dist/templates/monorepo/package.json +3 -3
- package/dist/templates/monorepo/packages/cli/package.json +9 -5
- package/dist/templates/monorepo/packages/cli/src/index.ts +13 -13
- package/dist/templates/monorepo/packages/cli/tsconfig.json +2 -5
- package/dist/templates/monorepo/packages/core/package.json +4 -4
- package/dist/templates/monorepo/packages/core/scripts/build.ts +10 -10
- package/dist/templates/monorepo/packages/core/src/commands/analyze.ts +58 -57
- package/dist/templates/monorepo/packages/core/src/commands/process.ts +39 -47
- package/dist/templates/monorepo/packages/core/src/index.ts +3 -3
- package/dist/templates/monorepo/packages/core/src/types.ts +15 -15
- package/dist/templates/monorepo/packages/core/tsconfig.json +2 -4
- package/dist/templates/monorepo/packages/utils/package.json +4 -4
- package/dist/templates/monorepo/packages/utils/scripts/build.ts +10 -10
- package/dist/templates/monorepo/packages/utils/src/format.ts +19 -19
- package/dist/templates/monorepo/packages/utils/src/index.ts +3 -3
- package/dist/templates/monorepo/packages/utils/src/json.ts +4 -4
- package/dist/templates/monorepo/packages/utils/src/logger.ts +9 -9
- package/dist/templates/monorepo/packages/utils/tsconfig.json +1 -1
- package/dist/templates/monorepo/template.json +2 -6
- package/dist/templates/monorepo/tsconfig.json +1 -1
- package/dist/templates/monorepo/turbo.json +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +28 -28
- package/templates/advanced/README.md +8 -4
- package/templates/advanced/bunli.config.ts +19 -19
- package/templates/advanced/package.json +8 -4
- package/templates/advanced/src/commands/config.ts +129 -119
- package/templates/advanced/src/commands/init.ts +53 -60
- package/templates/advanced/src/commands/serve.ts +77 -83
- package/templates/advanced/src/commands/validate.ts +58 -66
- package/templates/advanced/src/index.ts +30 -29
- package/templates/advanced/src/utils/config.ts +48 -47
- package/templates/advanced/src/utils/constants.ts +6 -6
- package/templates/advanced/src/utils/glob.ts +29 -28
- package/templates/advanced/src/utils/validator.ts +60 -61
- package/templates/advanced/template.json +2 -6
- package/templates/advanced/tsconfig.json +1 -1
- package/templates/basic/README.md +1 -1
- package/templates/basic/bunli.config.ts +17 -17
- package/templates/basic/package.json +3 -3
- package/templates/basic/src/commands/hello.ts +20 -26
- package/templates/basic/src/index.ts +9 -8
- package/templates/basic/template.json +2 -6
- package/templates/basic/tsconfig.json +1 -1
- package/templates/monorepo/README.md +1 -1
- package/templates/monorepo/bunli.config.ts +21 -21
- package/templates/monorepo/package.json +3 -3
- package/templates/monorepo/packages/cli/package.json +9 -5
- package/templates/monorepo/packages/cli/src/index.ts +13 -13
- package/templates/monorepo/packages/cli/tsconfig.json +2 -5
- package/templates/monorepo/packages/core/package.json +4 -4
- package/templates/monorepo/packages/core/scripts/build.ts +10 -10
- package/templates/monorepo/packages/core/src/commands/analyze.ts +58 -57
- package/templates/monorepo/packages/core/src/commands/process.ts +39 -47
- package/templates/monorepo/packages/core/src/index.ts +3 -3
- package/templates/monorepo/packages/core/src/types.ts +15 -15
- package/templates/monorepo/packages/core/tsconfig.json +2 -4
- package/templates/monorepo/packages/utils/package.json +4 -4
- package/templates/monorepo/packages/utils/scripts/build.ts +10 -10
- package/templates/monorepo/packages/utils/src/format.ts +19 -19
- package/templates/monorepo/packages/utils/src/index.ts +3 -3
- package/templates/monorepo/packages/utils/src/json.ts +4 -4
- package/templates/monorepo/packages/utils/src/logger.ts +9 -9
- package/templates/monorepo/packages/utils/tsconfig.json +1 -1
- package/templates/monorepo/template.json +2 -6
- package/templates/monorepo/tsconfig.json +1 -1
- package/templates/monorepo/turbo.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,421 +3,135 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { createCLI, defineCommand, option } from "@bunli/core";
|
|
6
|
-
import { z } from "zod";
|
|
7
6
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const manifest = await loadTemplateManifest(templateDir);
|
|
35
|
-
if (manifest?.files || Object.keys(variables).length > 0) {
|
|
36
|
-
await processTemplateFiles(templateDir, variables, manifest);
|
|
37
|
-
}
|
|
38
|
-
if (manifest?.hooks?.postInstall) {
|
|
39
|
-
await runPostInstallHooks(templateDir, manifest.hooks.postInstall);
|
|
40
|
-
}
|
|
41
|
-
return { dir: templateDir, manifest };
|
|
42
|
-
}
|
|
43
|
-
async function loadTemplateManifest(dir) {
|
|
44
|
-
const possiblePaths = [
|
|
45
|
-
join(dir, "template.json"),
|
|
46
|
-
join(dir, ".template.json"),
|
|
47
|
-
join(dir, "template.yaml"),
|
|
48
|
-
join(dir, ".template.yaml")
|
|
49
|
-
];
|
|
50
|
-
for (const path of possiblePaths) {
|
|
51
|
-
const file = Bun.file(path);
|
|
52
|
-
if (await file.exists()) {
|
|
53
|
-
const content = await file.text();
|
|
54
|
-
if (path.endsWith(".json")) {
|
|
55
|
-
const manifest = JSON.parse(content);
|
|
56
|
-
const removeExitCode = await Bun.spawn(["rm", "-f", path], {
|
|
57
|
-
stdout: "ignore",
|
|
58
|
-
stderr: "ignore"
|
|
59
|
-
}).exited;
|
|
60
|
-
if (removeExitCode !== 0) {
|
|
61
|
-
console.warn(`Warning: failed to remove template manifest ${path}`);
|
|
62
|
-
}
|
|
63
|
-
return manifest;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
7
|
+
// ../../node_modules/.bun/better-result@2.7.0/node_modules/better-result/dist/index.mjs
|
|
8
|
+
function dual(arity, body) {
|
|
9
|
+
if (arity === 2)
|
|
10
|
+
return (...args) => {
|
|
11
|
+
if (args.length >= 2)
|
|
12
|
+
return body(args[0], args[1]);
|
|
13
|
+
return (self) => body(self, args[0]);
|
|
14
|
+
};
|
|
15
|
+
if (arity === 3)
|
|
16
|
+
return (...args) => {
|
|
17
|
+
if (args.length >= 3)
|
|
18
|
+
return body(args[0], args[1], args[2]);
|
|
19
|
+
return (self) => body(self, args[0], args[1]);
|
|
20
|
+
};
|
|
21
|
+
if (arity === 4)
|
|
22
|
+
return (...args) => {
|
|
23
|
+
if (args.length >= 4)
|
|
24
|
+
return body(args[0], args[1], args[2], args[3]);
|
|
25
|
+
return (self) => body(self, args[0], args[1], args[2]);
|
|
26
|
+
};
|
|
27
|
+
return (...args) => {
|
|
28
|
+
if (args.length >= arity)
|
|
29
|
+
return body(...args);
|
|
30
|
+
return (self) => body(self, ...args);
|
|
31
|
+
};
|
|
68
32
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
33
|
+
var serializeCause = (cause) => {
|
|
34
|
+
if (cause instanceof Error)
|
|
35
|
+
return {
|
|
36
|
+
name: cause.name,
|
|
37
|
+
message: cause.message,
|
|
38
|
+
stack: cause.stack
|
|
39
|
+
};
|
|
40
|
+
return cause;
|
|
41
|
+
};
|
|
42
|
+
var isAnyTaggedError = (value) => {
|
|
43
|
+
return value instanceof Error && "_tag" in value && typeof value._tag === "string";
|
|
44
|
+
};
|
|
45
|
+
var TaggedError = Object.assign((tag) => () => {
|
|
46
|
+
|
|
47
|
+
class Base extends Error {
|
|
48
|
+
_tag = tag;
|
|
49
|
+
static is(value) {
|
|
50
|
+
return value instanceof Base;
|
|
85
51
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
await walk(join(currentDir, entry.name), path);
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
if (!path.match(/^(template\.json|\.template\.json|\.DS_Store|Thumbs\.db)$/) || path === ".gitignore") {
|
|
103
|
-
files.push(path);
|
|
104
|
-
}
|
|
52
|
+
constructor(args) {
|
|
53
|
+
const message = args && "message" in args && typeof args.message === "string" ? args.message : undefined;
|
|
54
|
+
const cause = args && "cause" in args ? args.cause : undefined;
|
|
55
|
+
super(message, cause !== undefined ? { cause } : undefined);
|
|
56
|
+
if (args)
|
|
57
|
+
Object.assign(this, args);
|
|
58
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
59
|
+
this.name = tag;
|
|
60
|
+
if (cause instanceof Error && cause.stack) {
|
|
61
|
+
const indented = cause.stack.replace(/\n/g, `
|
|
62
|
+
`);
|
|
63
|
+
this.stack = `${this.stack}
|
|
64
|
+
Caused by: ${indented}`;
|
|
105
65
|
}
|
|
106
66
|
}
|
|
67
|
+
toJSON() {
|
|
68
|
+
return {
|
|
69
|
+
...this,
|
|
70
|
+
_tag: this._tag,
|
|
71
|
+
name: this.name,
|
|
72
|
+
message: this.message,
|
|
73
|
+
cause: serializeCause(this.cause),
|
|
74
|
+
stack: this.stack
|
|
75
|
+
};
|
|
76
|
+
}
|
|
107
77
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
return Base;
|
|
79
|
+
}, { is: isAnyTaggedError });
|
|
80
|
+
var matchError = dual(2, (err$1, handlers) => {
|
|
81
|
+
const handler = handlers[err$1._tag];
|
|
82
|
+
return handler(err$1);
|
|
83
|
+
});
|
|
84
|
+
var matchErrorPartial = dual(3, (err$1, handlers, fallback) => {
|
|
85
|
+
const handler = handlers[err$1._tag];
|
|
86
|
+
if (typeof handler === "function")
|
|
87
|
+
return handler(err$1);
|
|
88
|
+
return fallback(err$1);
|
|
89
|
+
});
|
|
90
|
+
var UnhandledException = class extends TaggedError("UnhandledException")() {
|
|
91
|
+
constructor(args) {
|
|
92
|
+
const message = args.cause instanceof Error ? `Unhandled exception: ${args.cause.message}` : `Unhandled exception: ${String(args.cause)}`;
|
|
93
|
+
super({
|
|
94
|
+
message,
|
|
95
|
+
cause: args.cause
|
|
116
96
|
});
|
|
117
97
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
98
|
+
};
|
|
99
|
+
var Panic = class extends TaggedError("Panic")() {
|
|
100
|
+
};
|
|
101
|
+
var ResultDeserializationError = class extends TaggedError("ResultDeserializationError")() {
|
|
102
|
+
constructor(args) {
|
|
103
|
+
super({
|
|
104
|
+
message: `Failed to deserialize value as Result: expected { status: "ok", value } or { status: "error", error }`,
|
|
105
|
+
value: args.value
|
|
126
106
|
});
|
|
127
|
-
const exitCode = await proc.exited;
|
|
128
|
-
if (exitCode !== 0) {
|
|
129
|
-
throw new Error(`Post-install hook failed: ${hook}`);
|
|
130
|
-
}
|
|
131
107
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
108
|
+
};
|
|
109
|
+
var panic = (message, cause) => {
|
|
110
|
+
throw new Panic({
|
|
111
|
+
message,
|
|
112
|
+
cause
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
var tryOrPanic = (fn, message) => {
|
|
116
|
+
try {
|
|
117
|
+
return fn();
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
throw panic(message, cause);
|
|
141
120
|
}
|
|
142
|
-
|
|
143
|
-
|
|
121
|
+
};
|
|
122
|
+
var tryOrPanicAsync = async (fn, message) => {
|
|
123
|
+
try {
|
|
124
|
+
return await fn();
|
|
125
|
+
} catch (cause) {
|
|
126
|
+
throw panic(message, cause);
|
|
144
127
|
}
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
};
|
|
129
|
+
var Ok = class Ok2 {
|
|
130
|
+
status = "ok";
|
|
131
|
+
constructor(value) {
|
|
132
|
+
this.value = value;
|
|
147
133
|
}
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
function getBundledTemplatePath(name) {
|
|
151
|
-
return join(import.meta.dir, "..", "templates", name);
|
|
152
|
-
}
|
|
153
|
-
async function isLocalTemplate(template) {
|
|
154
|
-
if (template.startsWith("file:") || template.startsWith("./") || template.startsWith("../")) {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
const bundledPath = getBundledTemplatePath(template);
|
|
158
|
-
return await Bun.file(join(bundledPath, "package.json")).exists();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/steps.ts
|
|
162
|
-
import { existsSync } from "fs";
|
|
163
|
-
import { join as join2 } from "path";
|
|
164
|
-
var LOCKFILE_MAP = [
|
|
165
|
-
["bun.lock", "bun"],
|
|
166
|
-
["bun.lockb", "bun"],
|
|
167
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
168
|
-
["yarn.lock", "yarn"],
|
|
169
|
-
["package-lock.json", "npm"]
|
|
170
|
-
];
|
|
171
|
-
function detectPackageManager(cwd) {
|
|
172
|
-
const dir = cwd ?? process.cwd();
|
|
173
|
-
for (const [lockfile, manager] of LOCKFILE_MAP) {
|
|
174
|
-
if (existsSync(join2(dir, lockfile))) {
|
|
175
|
-
return manager;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const userAgent = process.env.npm_config_user_agent;
|
|
179
|
-
if (userAgent) {
|
|
180
|
-
if (userAgent.startsWith("bun"))
|
|
181
|
-
return "bun";
|
|
182
|
-
if (userAgent.startsWith("pnpm"))
|
|
183
|
-
return "pnpm";
|
|
184
|
-
if (userAgent.startsWith("yarn"))
|
|
185
|
-
return "yarn";
|
|
186
|
-
if (userAgent.startsWith("npm"))
|
|
187
|
-
return "npm";
|
|
188
|
-
}
|
|
189
|
-
return "bun";
|
|
190
|
-
}
|
|
191
|
-
function isInGitRepo(cwd) {
|
|
192
|
-
try {
|
|
193
|
-
const result = Bun.spawnSync(["git", "rev-parse", "--is-inside-work-tree"], { cwd: cwd ?? process.cwd(), stdout: "ignore", stderr: "ignore" });
|
|
194
|
-
return result.exitCode === 0;
|
|
195
|
-
} catch {
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async function runSteps(dir, steps) {
|
|
200
|
-
for (const step of steps) {
|
|
201
|
-
switch (step.type) {
|
|
202
|
-
case "install":
|
|
203
|
-
await runInstall(dir);
|
|
204
|
-
break;
|
|
205
|
-
case "git-init":
|
|
206
|
-
await runGitInit(dir, step.commit);
|
|
207
|
-
break;
|
|
208
|
-
case "open-editor":
|
|
209
|
-
await runOpenEditor(dir);
|
|
210
|
-
break;
|
|
211
|
-
case "command":
|
|
212
|
-
await runCommand(step.cmd, step.cwd ?? dir);
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
async function runInstall(cwd) {
|
|
218
|
-
const pm = detectPackageManager(cwd);
|
|
219
|
-
const proc = Bun.spawn([pm, "install"], {
|
|
220
|
-
cwd,
|
|
221
|
-
stdout: "pipe",
|
|
222
|
-
stderr: "pipe"
|
|
223
|
-
});
|
|
224
|
-
const exitCode = await proc.exited;
|
|
225
|
-
if (exitCode !== 0) {
|
|
226
|
-
const stderr = await new Response(proc.stderr).text();
|
|
227
|
-
throw new Error(`"${pm} install" exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
async function runGitInit(cwd, commit) {
|
|
231
|
-
await spawnChecked(["git", "init"], cwd, "git init");
|
|
232
|
-
if (commit) {
|
|
233
|
-
await ensureGitIdentity(cwd);
|
|
234
|
-
await spawnChecked(["git", "add", "."], cwd, "git add");
|
|
235
|
-
await spawnChecked(["git", "commit", "-m", commit], cwd, "git commit");
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async function runOpenEditor(cwd) {
|
|
239
|
-
const editor = process.env.EDITOR || "code";
|
|
240
|
-
try {
|
|
241
|
-
const proc = Bun.spawn([editor, cwd], {
|
|
242
|
-
stdout: "ignore",
|
|
243
|
-
stderr: "ignore"
|
|
244
|
-
});
|
|
245
|
-
const raceResult = await Promise.race([
|
|
246
|
-
proc.exited.then((code) => ({ kind: "exited", code })),
|
|
247
|
-
new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), 500))
|
|
248
|
-
]);
|
|
249
|
-
if (raceResult.kind === "exited" && raceResult.code !== 0) {
|
|
250
|
-
console.warn(`Warning: could not open editor "${editor}" (exit code ${raceResult.code})`);
|
|
251
|
-
}
|
|
252
|
-
} catch {
|
|
253
|
-
console.warn(`Warning: could not open editor "${editor}"`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function getCommandSpawnArgs(cmd, platform = process.platform) {
|
|
257
|
-
return platform === "win32" ? ["cmd", "/d", "/s", "/c", cmd] : ["sh", "-c", cmd];
|
|
258
|
-
}
|
|
259
|
-
async function runCommand(cmd, cwd) {
|
|
260
|
-
const proc = Bun.spawn(getCommandSpawnArgs(cmd), {
|
|
261
|
-
cwd,
|
|
262
|
-
stdout: "inherit",
|
|
263
|
-
stderr: "inherit"
|
|
264
|
-
});
|
|
265
|
-
const exitCode = await proc.exited;
|
|
266
|
-
if (exitCode !== 0) {
|
|
267
|
-
throw new Error(`Command "${cmd}" exited with code ${exitCode}`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
async function ensureGitIdentity(cwd) {
|
|
271
|
-
const hasName = Bun.spawnSync(["git", "config", "user.name"], { cwd }).exitCode === 0;
|
|
272
|
-
const hasEmail = Bun.spawnSync(["git", "config", "user.email"], { cwd }).exitCode === 0;
|
|
273
|
-
if (!hasName) {
|
|
274
|
-
await spawnChecked(["git", "config", "user.name", "Bunli"], cwd, "git config user.name");
|
|
275
|
-
}
|
|
276
|
-
if (!hasEmail) {
|
|
277
|
-
await spawnChecked(["git", "config", "user.email", "bunli@scaffolded.project"], cwd, "git config user.email");
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
async function spawnChecked(cmd, cwd, label) {
|
|
281
|
-
const proc = Bun.spawn(cmd, {
|
|
282
|
-
cwd,
|
|
283
|
-
stdout: "ignore",
|
|
284
|
-
stderr: "pipe"
|
|
285
|
-
});
|
|
286
|
-
const exitCode = await proc.exited;
|
|
287
|
-
if (exitCode !== 0) {
|
|
288
|
-
const stderr = await new Response(proc.stderr).text();
|
|
289
|
-
throw new Error(`"${label}" failed with exit code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ../../node_modules/.bun/better-result@2.7.0/node_modules/better-result/dist/index.mjs
|
|
294
|
-
function dual(arity, body) {
|
|
295
|
-
if (arity === 2)
|
|
296
|
-
return (...args) => {
|
|
297
|
-
if (args.length >= 2)
|
|
298
|
-
return body(args[0], args[1]);
|
|
299
|
-
return (self) => body(self, args[0]);
|
|
300
|
-
};
|
|
301
|
-
if (arity === 3)
|
|
302
|
-
return (...args) => {
|
|
303
|
-
if (args.length >= 3)
|
|
304
|
-
return body(args[0], args[1], args[2]);
|
|
305
|
-
return (self) => body(self, args[0], args[1]);
|
|
306
|
-
};
|
|
307
|
-
if (arity === 4)
|
|
308
|
-
return (...args) => {
|
|
309
|
-
if (args.length >= 4)
|
|
310
|
-
return body(args[0], args[1], args[2], args[3]);
|
|
311
|
-
return (self) => body(self, args[0], args[1], args[2]);
|
|
312
|
-
};
|
|
313
|
-
return (...args) => {
|
|
314
|
-
if (args.length >= arity)
|
|
315
|
-
return body(...args);
|
|
316
|
-
return (self) => body(self, ...args);
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
var serializeCause = (cause) => {
|
|
320
|
-
if (cause instanceof Error)
|
|
321
|
-
return {
|
|
322
|
-
name: cause.name,
|
|
323
|
-
message: cause.message,
|
|
324
|
-
stack: cause.stack
|
|
325
|
-
};
|
|
326
|
-
return cause;
|
|
327
|
-
};
|
|
328
|
-
var isAnyTaggedError = (value) => {
|
|
329
|
-
return value instanceof Error && "_tag" in value && typeof value._tag === "string";
|
|
330
|
-
};
|
|
331
|
-
var TaggedError = Object.assign((tag) => () => {
|
|
332
|
-
|
|
333
|
-
class Base extends Error {
|
|
334
|
-
_tag = tag;
|
|
335
|
-
static is(value) {
|
|
336
|
-
return value instanceof Base;
|
|
337
|
-
}
|
|
338
|
-
constructor(args) {
|
|
339
|
-
const message = args && "message" in args && typeof args.message === "string" ? args.message : undefined;
|
|
340
|
-
const cause = args && "cause" in args ? args.cause : undefined;
|
|
341
|
-
super(message, cause !== undefined ? { cause } : undefined);
|
|
342
|
-
if (args)
|
|
343
|
-
Object.assign(this, args);
|
|
344
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
345
|
-
this.name = tag;
|
|
346
|
-
if (cause instanceof Error && cause.stack) {
|
|
347
|
-
const indented = cause.stack.replace(/\n/g, `
|
|
348
|
-
`);
|
|
349
|
-
this.stack = `${this.stack}
|
|
350
|
-
Caused by: ${indented}`;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
toJSON() {
|
|
354
|
-
return {
|
|
355
|
-
...this,
|
|
356
|
-
_tag: this._tag,
|
|
357
|
-
name: this.name,
|
|
358
|
-
message: this.message,
|
|
359
|
-
cause: serializeCause(this.cause),
|
|
360
|
-
stack: this.stack
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return Base;
|
|
365
|
-
}, { is: isAnyTaggedError });
|
|
366
|
-
var matchError = dual(2, (err$1, handlers) => {
|
|
367
|
-
const handler = handlers[err$1._tag];
|
|
368
|
-
return handler(err$1);
|
|
369
|
-
});
|
|
370
|
-
var matchErrorPartial = dual(3, (err$1, handlers, fallback) => {
|
|
371
|
-
const handler = handlers[err$1._tag];
|
|
372
|
-
if (typeof handler === "function")
|
|
373
|
-
return handler(err$1);
|
|
374
|
-
return fallback(err$1);
|
|
375
|
-
});
|
|
376
|
-
var UnhandledException = class extends TaggedError("UnhandledException")() {
|
|
377
|
-
constructor(args) {
|
|
378
|
-
const message = args.cause instanceof Error ? `Unhandled exception: ${args.cause.message}` : `Unhandled exception: ${String(args.cause)}`;
|
|
379
|
-
super({
|
|
380
|
-
message,
|
|
381
|
-
cause: args.cause
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
var Panic = class extends TaggedError("Panic")() {
|
|
386
|
-
};
|
|
387
|
-
var ResultDeserializationError = class extends TaggedError("ResultDeserializationError")() {
|
|
388
|
-
constructor(args) {
|
|
389
|
-
super({
|
|
390
|
-
message: `Failed to deserialize value as Result: expected { status: "ok", value } or { status: "error", error }`,
|
|
391
|
-
value: args.value
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
var panic = (message, cause) => {
|
|
396
|
-
throw new Panic({
|
|
397
|
-
message,
|
|
398
|
-
cause
|
|
399
|
-
});
|
|
400
|
-
};
|
|
401
|
-
var tryOrPanic = (fn, message) => {
|
|
402
|
-
try {
|
|
403
|
-
return fn();
|
|
404
|
-
} catch (cause) {
|
|
405
|
-
throw panic(message, cause);
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
var tryOrPanicAsync = async (fn, message) => {
|
|
409
|
-
try {
|
|
410
|
-
return await fn();
|
|
411
|
-
} catch (cause) {
|
|
412
|
-
throw panic(message, cause);
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
var Ok = class Ok2 {
|
|
416
|
-
status = "ok";
|
|
417
|
-
constructor(value) {
|
|
418
|
-
this.value = value;
|
|
419
|
-
}
|
|
420
|
-
isOk() {
|
|
134
|
+
isOk() {
|
|
421
135
|
return true;
|
|
422
136
|
}
|
|
423
137
|
isErr() {
|
|
@@ -554,164 +268,459 @@ var tryPromise = async (options, config) => {
|
|
|
554
268
|
throw panic("Result.tryPromise catch handler threw", catchHandlerError);
|
|
555
269
|
}
|
|
556
270
|
}
|
|
557
|
-
};
|
|
558
|
-
const retry = config?.retry;
|
|
559
|
-
if (!retry)
|
|
560
|
-
return execute();
|
|
561
|
-
const getDelay = (retryAttempt) => {
|
|
562
|
-
switch (retry.backoff) {
|
|
563
|
-
case "constant":
|
|
564
|
-
return retry.delayMs;
|
|
565
|
-
case "linear":
|
|
566
|
-
return retry.delayMs * (retryAttempt + 1);
|
|
567
|
-
case "exponential":
|
|
568
|
-
return retry.delayMs * 2 ** retryAttempt;
|
|
271
|
+
};
|
|
272
|
+
const retry = config?.retry;
|
|
273
|
+
if (!retry)
|
|
274
|
+
return execute();
|
|
275
|
+
const getDelay = (retryAttempt) => {
|
|
276
|
+
switch (retry.backoff) {
|
|
277
|
+
case "constant":
|
|
278
|
+
return retry.delayMs;
|
|
279
|
+
case "linear":
|
|
280
|
+
return retry.delayMs * (retryAttempt + 1);
|
|
281
|
+
case "exponential":
|
|
282
|
+
return retry.delayMs * 2 ** retryAttempt;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
+
let result = await execute();
|
|
287
|
+
const shouldRetryFn = retry.shouldRetry ?? (() => true);
|
|
288
|
+
for (let attempt = 0;attempt < retry.times; attempt++) {
|
|
289
|
+
if (result.status !== "error")
|
|
290
|
+
break;
|
|
291
|
+
const error = result.error;
|
|
292
|
+
if (!tryOrPanic(() => shouldRetryFn(error), "shouldRetry predicate threw"))
|
|
293
|
+
break;
|
|
294
|
+
await sleep(getDelay(attempt));
|
|
295
|
+
result = await execute();
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
299
|
+
var map = dual(2, (result, fn) => {
|
|
300
|
+
return result.map(fn);
|
|
301
|
+
});
|
|
302
|
+
var mapError = dual(2, (result, fn) => {
|
|
303
|
+
return result.mapError(fn);
|
|
304
|
+
});
|
|
305
|
+
var andThen = dual(2, (result, fn) => {
|
|
306
|
+
return result.andThen(fn);
|
|
307
|
+
});
|
|
308
|
+
var andThenAsync = dual(2, (result, fn) => {
|
|
309
|
+
return result.andThenAsync(fn);
|
|
310
|
+
});
|
|
311
|
+
var match = dual(2, (result, handlers) => {
|
|
312
|
+
return result.match(handlers);
|
|
313
|
+
});
|
|
314
|
+
var tap = dual(2, (result, fn) => {
|
|
315
|
+
return result.tap(fn);
|
|
316
|
+
});
|
|
317
|
+
var tapAsync = dual(2, (result, fn) => {
|
|
318
|
+
return result.tapAsync(fn);
|
|
319
|
+
});
|
|
320
|
+
var unwrap = (result, message) => {
|
|
321
|
+
return result.unwrap(message);
|
|
322
|
+
};
|
|
323
|
+
function assertIsResult(value) {
|
|
324
|
+
if (value !== null && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "error"))
|
|
325
|
+
return;
|
|
326
|
+
return panic("Result.gen body must return Result.ok() or Result.err(), got: " + (value === null ? "null" : typeof value === "object" ? JSON.stringify(value) : String(value)));
|
|
327
|
+
}
|
|
328
|
+
var unwrapOr = dual(2, (result, fallback) => {
|
|
329
|
+
return result.unwrapOr(fallback);
|
|
330
|
+
});
|
|
331
|
+
var gen = (body, thisArg) => {
|
|
332
|
+
const iterator = body.call(thisArg);
|
|
333
|
+
if (Symbol.asyncIterator in iterator)
|
|
334
|
+
return (async () => {
|
|
335
|
+
const asyncIter = iterator;
|
|
336
|
+
let state$1;
|
|
337
|
+
try {
|
|
338
|
+
state$1 = await asyncIter.next();
|
|
339
|
+
} catch (cause) {
|
|
340
|
+
throw panic("generator body threw", cause);
|
|
341
|
+
}
|
|
342
|
+
assertIsResult(state$1.value);
|
|
343
|
+
if (!state$1.done)
|
|
344
|
+
try {
|
|
345
|
+
await asyncIter.return?.(undefined);
|
|
346
|
+
} catch (cause) {
|
|
347
|
+
throw panic("generator cleanup threw", cause);
|
|
348
|
+
}
|
|
349
|
+
return state$1.value;
|
|
350
|
+
})();
|
|
351
|
+
const syncIter = iterator;
|
|
352
|
+
let state;
|
|
353
|
+
try {
|
|
354
|
+
state = syncIter.next();
|
|
355
|
+
} catch (cause) {
|
|
356
|
+
throw panic("generator body threw", cause);
|
|
357
|
+
}
|
|
358
|
+
assertIsResult(state.value);
|
|
359
|
+
if (!state.done)
|
|
360
|
+
try {
|
|
361
|
+
syncIter.return?.(undefined);
|
|
362
|
+
} catch (cause) {
|
|
363
|
+
throw panic("generator cleanup threw", cause);
|
|
364
|
+
}
|
|
365
|
+
return state.value;
|
|
366
|
+
};
|
|
367
|
+
async function* resultAwait(promise) {
|
|
368
|
+
return yield* await promise;
|
|
369
|
+
}
|
|
370
|
+
function isSerializedResult(obj) {
|
|
371
|
+
return obj !== null && typeof obj === "object" && "status" in obj && (obj.status === "ok" && ("value" in obj) || obj.status === "error" && ("error" in obj));
|
|
372
|
+
}
|
|
373
|
+
var serialize = (result) => {
|
|
374
|
+
return result.status === "ok" ? {
|
|
375
|
+
status: "ok",
|
|
376
|
+
value: result.value
|
|
377
|
+
} : {
|
|
378
|
+
status: "error",
|
|
379
|
+
error: result.error
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
var deserialize = (value) => {
|
|
383
|
+
if (isSerializedResult(value))
|
|
384
|
+
return value.status === "ok" ? new Ok(value.value) : new Err(value.error);
|
|
385
|
+
return err(new ResultDeserializationError({ value }));
|
|
386
|
+
};
|
|
387
|
+
var hydrate = (value) => {
|
|
388
|
+
return deserialize(value);
|
|
389
|
+
};
|
|
390
|
+
var partition = (results) => {
|
|
391
|
+
const oks = [];
|
|
392
|
+
const errs = [];
|
|
393
|
+
for (const r of results)
|
|
394
|
+
if (r.status === "ok")
|
|
395
|
+
oks.push(r.value);
|
|
396
|
+
else
|
|
397
|
+
errs.push(r.error);
|
|
398
|
+
return [oks, errs];
|
|
399
|
+
};
|
|
400
|
+
var flatten = (result) => {
|
|
401
|
+
if (result.status === "ok")
|
|
402
|
+
return result.value;
|
|
403
|
+
return result;
|
|
404
|
+
};
|
|
405
|
+
var Result = {
|
|
406
|
+
ok,
|
|
407
|
+
isOk,
|
|
408
|
+
err,
|
|
409
|
+
isError,
|
|
410
|
+
try: tryFn,
|
|
411
|
+
tryPromise,
|
|
412
|
+
map,
|
|
413
|
+
mapError,
|
|
414
|
+
andThen,
|
|
415
|
+
andThenAsync,
|
|
416
|
+
match,
|
|
417
|
+
tap,
|
|
418
|
+
tapAsync,
|
|
419
|
+
unwrap,
|
|
420
|
+
unwrapOr,
|
|
421
|
+
gen,
|
|
422
|
+
await: resultAwait,
|
|
423
|
+
serialize,
|
|
424
|
+
deserialize,
|
|
425
|
+
hydrate,
|
|
426
|
+
partition,
|
|
427
|
+
flatten
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/cli.ts
|
|
431
|
+
import { z } from "zod";
|
|
432
|
+
|
|
433
|
+
// src/create.ts
|
|
434
|
+
import path from "path";
|
|
435
|
+
|
|
436
|
+
// src/steps.ts
|
|
437
|
+
import { existsSync } from "fs";
|
|
438
|
+
import { join } from "path";
|
|
439
|
+
var LOCKFILE_MAP = [
|
|
440
|
+
["bun.lock", "bun"],
|
|
441
|
+
["bun.lockb", "bun"],
|
|
442
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
443
|
+
["yarn.lock", "yarn"],
|
|
444
|
+
["package-lock.json", "npm"]
|
|
445
|
+
];
|
|
446
|
+
function detectPackageManager(cwd) {
|
|
447
|
+
const dir = cwd ?? process.cwd();
|
|
448
|
+
for (const [lockfile, manager] of LOCKFILE_MAP) {
|
|
449
|
+
if (existsSync(join(dir, lockfile))) {
|
|
450
|
+
return manager;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
454
|
+
if (userAgent) {
|
|
455
|
+
if (userAgent.startsWith("bun"))
|
|
456
|
+
return "bun";
|
|
457
|
+
if (userAgent.startsWith("pnpm"))
|
|
458
|
+
return "pnpm";
|
|
459
|
+
if (userAgent.startsWith("yarn"))
|
|
460
|
+
return "yarn";
|
|
461
|
+
if (userAgent.startsWith("npm"))
|
|
462
|
+
return "npm";
|
|
463
|
+
}
|
|
464
|
+
return "bun";
|
|
465
|
+
}
|
|
466
|
+
function isInGitRepo(cwd) {
|
|
467
|
+
try {
|
|
468
|
+
const result = Bun.spawnSync(["git", "rev-parse", "--is-inside-work-tree"], {
|
|
469
|
+
cwd: cwd ?? process.cwd(),
|
|
470
|
+
stdout: "ignore",
|
|
471
|
+
stderr: "ignore"
|
|
472
|
+
});
|
|
473
|
+
return result.exitCode === 0;
|
|
474
|
+
} catch {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function runSteps(dir, steps) {
|
|
479
|
+
for (const step of steps) {
|
|
480
|
+
switch (step.type) {
|
|
481
|
+
case "install":
|
|
482
|
+
await runInstall(dir);
|
|
483
|
+
break;
|
|
484
|
+
case "git-init":
|
|
485
|
+
await runGitInit(dir, step.commit);
|
|
486
|
+
break;
|
|
487
|
+
case "open-editor":
|
|
488
|
+
await runOpenEditor(dir);
|
|
489
|
+
break;
|
|
490
|
+
case "command":
|
|
491
|
+
await runCommand(step.cmd, step.cwd ?? dir);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function runInstall(cwd) {
|
|
497
|
+
const pm = detectPackageManager(cwd);
|
|
498
|
+
const proc = Bun.spawn([pm, "install"], {
|
|
499
|
+
cwd,
|
|
500
|
+
stdout: "pipe",
|
|
501
|
+
stderr: "pipe"
|
|
502
|
+
});
|
|
503
|
+
const exitCode = await proc.exited;
|
|
504
|
+
if (exitCode !== 0) {
|
|
505
|
+
const stderr = await new Response(proc.stderr).text();
|
|
506
|
+
throw new Error(`"${pm} install" exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function runGitInit(cwd, commit) {
|
|
510
|
+
await spawnChecked(["git", "init"], cwd, "git init");
|
|
511
|
+
if (commit) {
|
|
512
|
+
await ensureGitIdentity(cwd);
|
|
513
|
+
await spawnChecked(["git", "add", "."], cwd, "git add");
|
|
514
|
+
await spawnChecked(["git", "commit", "-m", commit], cwd, "git commit");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function runOpenEditor(cwd) {
|
|
518
|
+
const editor = process.env.EDITOR || "code";
|
|
519
|
+
try {
|
|
520
|
+
const proc = Bun.spawn([editor, cwd], {
|
|
521
|
+
stdout: "ignore",
|
|
522
|
+
stderr: "ignore"
|
|
523
|
+
});
|
|
524
|
+
const raceResult = await Promise.race([
|
|
525
|
+
proc.exited.then((code) => ({ kind: "exited", code })),
|
|
526
|
+
new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), 500))
|
|
527
|
+
]);
|
|
528
|
+
if (raceResult.kind === "exited" && raceResult.code !== 0) {
|
|
529
|
+
console.warn(`Warning: could not open editor "${editor}" (exit code ${raceResult.code})`);
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
console.warn(`Warning: could not open editor "${editor}"`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function getCommandSpawnArgs(cmd, platform = process.platform) {
|
|
536
|
+
return platform === "win32" ? ["cmd", "/d", "/s", "/c", cmd] : ["sh", "-c", cmd];
|
|
537
|
+
}
|
|
538
|
+
async function runCommand(cmd, cwd) {
|
|
539
|
+
const proc = Bun.spawn(getCommandSpawnArgs(cmd), {
|
|
540
|
+
cwd,
|
|
541
|
+
stdout: "inherit",
|
|
542
|
+
stderr: "inherit"
|
|
543
|
+
});
|
|
544
|
+
const exitCode = await proc.exited;
|
|
545
|
+
if (exitCode !== 0) {
|
|
546
|
+
throw new Error(`Command "${cmd}" exited with code ${exitCode}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function ensureGitIdentity(cwd) {
|
|
550
|
+
const hasName = Bun.spawnSync(["git", "config", "user.name"], { cwd }).exitCode === 0;
|
|
551
|
+
const hasEmail = Bun.spawnSync(["git", "config", "user.email"], { cwd }).exitCode === 0;
|
|
552
|
+
if (!hasName) {
|
|
553
|
+
await spawnChecked(["git", "config", "user.name", "Bunli"], cwd, "git config user.name");
|
|
554
|
+
}
|
|
555
|
+
if (!hasEmail) {
|
|
556
|
+
await spawnChecked(["git", "config", "user.email", "bunli@scaffolded.project"], cwd, "git config user.email");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function spawnChecked(cmd, cwd, label) {
|
|
560
|
+
const proc = Bun.spawn(cmd, {
|
|
561
|
+
cwd,
|
|
562
|
+
stdout: "ignore",
|
|
563
|
+
stderr: "pipe"
|
|
564
|
+
});
|
|
565
|
+
const exitCode = await proc.exited;
|
|
566
|
+
if (exitCode !== 0) {
|
|
567
|
+
const stderr = await new Response(proc.stderr).text();
|
|
568
|
+
throw new Error(`"${label}" failed with exit code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/template-engine.ts
|
|
573
|
+
import { readdir } from "fs/promises";
|
|
574
|
+
import { join as join2 } from "path";
|
|
575
|
+
import { downloadTemplate } from "giget";
|
|
576
|
+
async function processTemplate(options) {
|
|
577
|
+
const { source, dir, offline, variables = {} } = options;
|
|
578
|
+
let templateDir;
|
|
579
|
+
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) {
|
|
580
|
+
const sourceDir = source.startsWith("/") ? source : join2(process.cwd(), source);
|
|
581
|
+
const copyExitCode = await Bun.spawn(["cp", "-r", sourceDir + "/.", dir], {
|
|
582
|
+
stdout: "inherit",
|
|
583
|
+
stderr: "inherit"
|
|
584
|
+
}).exited;
|
|
585
|
+
if (copyExitCode !== 0) {
|
|
586
|
+
throw new Error(`Failed to copy local template from ${sourceDir}`);
|
|
587
|
+
}
|
|
588
|
+
templateDir = dir;
|
|
589
|
+
} else {
|
|
590
|
+
const result = await downloadTemplate(source, {
|
|
591
|
+
dir,
|
|
592
|
+
offline,
|
|
593
|
+
preferOffline: true,
|
|
594
|
+
force: true
|
|
595
|
+
});
|
|
596
|
+
templateDir = result.dir;
|
|
597
|
+
}
|
|
598
|
+
const manifest = await loadTemplateManifest(templateDir);
|
|
599
|
+
if (manifest?.files || Object.keys(variables).length > 0) {
|
|
600
|
+
await processTemplateFiles(templateDir, variables, manifest);
|
|
601
|
+
}
|
|
602
|
+
if (manifest?.hooks?.postInstall) {
|
|
603
|
+
await runPostInstallHooks(templateDir, manifest.hooks.postInstall);
|
|
604
|
+
}
|
|
605
|
+
return { dir: templateDir, manifest };
|
|
606
|
+
}
|
|
607
|
+
async function loadTemplateManifest(dir) {
|
|
608
|
+
const possiblePaths = [
|
|
609
|
+
join2(dir, "template.json"),
|
|
610
|
+
join2(dir, ".template.json"),
|
|
611
|
+
join2(dir, "template.yaml"),
|
|
612
|
+
join2(dir, ".template.yaml")
|
|
613
|
+
];
|
|
614
|
+
for (const path of possiblePaths) {
|
|
615
|
+
const file = Bun.file(path);
|
|
616
|
+
if (await file.exists()) {
|
|
617
|
+
const content = await file.text();
|
|
618
|
+
if (path.endsWith(".json")) {
|
|
619
|
+
const manifest = JSON.parse(content);
|
|
620
|
+
const removeExitCode = await Bun.spawn(["rm", "-f", path], {
|
|
621
|
+
stdout: "ignore",
|
|
622
|
+
stderr: "ignore"
|
|
623
|
+
}).exited;
|
|
624
|
+
if (removeExitCode !== 0) {
|
|
625
|
+
console.warn(`Warning: failed to remove template manifest ${path}`);
|
|
626
|
+
}
|
|
627
|
+
return manifest;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
async function processTemplateFiles(dir, variables, manifest) {
|
|
634
|
+
const files = await getFilesToProcess(dir, manifest);
|
|
635
|
+
for (const file of files) {
|
|
636
|
+
const filePath = join2(dir, file);
|
|
637
|
+
const content = await Bun.file(filePath).text();
|
|
638
|
+
let processedContent = content;
|
|
639
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
640
|
+
processedContent = processedContent.replaceAll(`{{${key}}}`, value).replaceAll(`<%= ${key} %>`, value).replaceAll(`$${key}`, value).replaceAll(`__${key}__`, value);
|
|
641
|
+
}
|
|
642
|
+
let newFilePath = filePath;
|
|
643
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
644
|
+
newFilePath = newFilePath.replaceAll(`__${key}__`, value);
|
|
645
|
+
}
|
|
646
|
+
await Bun.write(newFilePath, processedContent);
|
|
647
|
+
if (newFilePath !== filePath) {
|
|
648
|
+
await Bun.spawn(["rm", filePath]).exited;
|
|
569
649
|
}
|
|
570
|
-
};
|
|
571
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
572
|
-
let result = await execute();
|
|
573
|
-
const shouldRetryFn = retry.shouldRetry ?? (() => true);
|
|
574
|
-
for (let attempt = 0;attempt < retry.times; attempt++) {
|
|
575
|
-
if (result.status !== "error")
|
|
576
|
-
break;
|
|
577
|
-
const error = result.error;
|
|
578
|
-
if (!tryOrPanic(() => shouldRetryFn(error), "shouldRetry predicate threw"))
|
|
579
|
-
break;
|
|
580
|
-
await sleep(getDelay(attempt));
|
|
581
|
-
result = await execute();
|
|
582
650
|
}
|
|
583
|
-
return result;
|
|
584
|
-
};
|
|
585
|
-
var map = dual(2, (result, fn) => {
|
|
586
|
-
return result.map(fn);
|
|
587
|
-
});
|
|
588
|
-
var mapError = dual(2, (result, fn) => {
|
|
589
|
-
return result.mapError(fn);
|
|
590
|
-
});
|
|
591
|
-
var andThen = dual(2, (result, fn) => {
|
|
592
|
-
return result.andThen(fn);
|
|
593
|
-
});
|
|
594
|
-
var andThenAsync = dual(2, (result, fn) => {
|
|
595
|
-
return result.andThenAsync(fn);
|
|
596
|
-
});
|
|
597
|
-
var match = dual(2, (result, handlers) => {
|
|
598
|
-
return result.match(handlers);
|
|
599
|
-
});
|
|
600
|
-
var tap = dual(2, (result, fn) => {
|
|
601
|
-
return result.tap(fn);
|
|
602
|
-
});
|
|
603
|
-
var tapAsync = dual(2, (result, fn) => {
|
|
604
|
-
return result.tapAsync(fn);
|
|
605
|
-
});
|
|
606
|
-
var unwrap = (result, message) => {
|
|
607
|
-
return result.unwrap(message);
|
|
608
|
-
};
|
|
609
|
-
function assertIsResult(value) {
|
|
610
|
-
if (value !== null && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "error"))
|
|
611
|
-
return;
|
|
612
|
-
return panic("Result.gen body must return Result.ok() or Result.err(), got: " + (value === null ? "null" : typeof value === "object" ? JSON.stringify(value) : String(value)));
|
|
613
651
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
var gen = (body, thisArg) => {
|
|
618
|
-
const iterator = body.call(thisArg);
|
|
619
|
-
if (Symbol.asyncIterator in iterator)
|
|
620
|
-
return (async () => {
|
|
621
|
-
const asyncIter = iterator;
|
|
622
|
-
let state$1;
|
|
623
|
-
try {
|
|
624
|
-
state$1 = await asyncIter.next();
|
|
625
|
-
} catch (cause) {
|
|
626
|
-
throw panic("generator body threw", cause);
|
|
627
|
-
}
|
|
628
|
-
assertIsResult(state$1.value);
|
|
629
|
-
if (!state$1.done)
|
|
630
|
-
try {
|
|
631
|
-
await asyncIter.return?.(undefined);
|
|
632
|
-
} catch (cause) {
|
|
633
|
-
throw panic("generator cleanup threw", cause);
|
|
634
|
-
}
|
|
635
|
-
return state$1.value;
|
|
636
|
-
})();
|
|
637
|
-
const syncIter = iterator;
|
|
638
|
-
let state;
|
|
639
|
-
try {
|
|
640
|
-
state = syncIter.next();
|
|
641
|
-
} catch (cause) {
|
|
642
|
-
throw panic("generator body threw", cause);
|
|
652
|
+
async function getFilesToProcess(dir, manifest) {
|
|
653
|
+
if (manifest?.files?.include) {
|
|
654
|
+
return manifest.files.include;
|
|
643
655
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
656
|
+
const files = [];
|
|
657
|
+
async function walk(currentDir, prefix = "") {
|
|
658
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
659
|
+
for (const entry of entries) {
|
|
660
|
+
const path = join2(prefix, entry.name);
|
|
661
|
+
if (entry.isDirectory()) {
|
|
662
|
+
if (!["node_modules", ".git", ".next", "dist", "build"].includes(entry.name)) {
|
|
663
|
+
await walk(join2(currentDir, entry.name), path);
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
if (!path.match(/^(template\.json|\.template\.json|\.DS_Store|Thumbs\.db)$/) || path === ".gitignore") {
|
|
667
|
+
files.push(path);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
650
670
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
671
|
+
}
|
|
672
|
+
await walk(dir);
|
|
673
|
+
if (manifest?.files?.exclude) {
|
|
674
|
+
return files.filter((file) => {
|
|
675
|
+
return !manifest.files.exclude.some((pattern) => {
|
|
676
|
+
const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
677
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
678
|
+
return regex.test(file);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
return files;
|
|
655
683
|
}
|
|
656
|
-
function
|
|
657
|
-
|
|
684
|
+
async function runPostInstallHooks(dir, hooks) {
|
|
685
|
+
for (const hook of hooks) {
|
|
686
|
+
const proc = Bun.spawn(hook.split(" "), {
|
|
687
|
+
cwd: dir,
|
|
688
|
+
stdout: "inherit",
|
|
689
|
+
stderr: "inherit"
|
|
690
|
+
});
|
|
691
|
+
const exitCode = await proc.exited;
|
|
692
|
+
if (exitCode !== 0) {
|
|
693
|
+
throw new Error(`Post-install hook failed: ${hook}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
658
696
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
status: "error",
|
|
665
|
-
error: result.error
|
|
697
|
+
function resolveTemplateSource(template) {
|
|
698
|
+
const specialTemplates = {
|
|
699
|
+
basic: "github:bunli/templates/basic",
|
|
700
|
+
advanced: "github:bunli/templates/advanced",
|
|
701
|
+
monorepo: "github:bunli/templates/monorepo"
|
|
666
702
|
};
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return result.value;
|
|
689
|
-
return result;
|
|
690
|
-
};
|
|
691
|
-
var Result = {
|
|
692
|
-
ok,
|
|
693
|
-
isOk,
|
|
694
|
-
err,
|
|
695
|
-
isError,
|
|
696
|
-
try: tryFn,
|
|
697
|
-
tryPromise,
|
|
698
|
-
map,
|
|
699
|
-
mapError,
|
|
700
|
-
andThen,
|
|
701
|
-
andThenAsync,
|
|
702
|
-
match,
|
|
703
|
-
tap,
|
|
704
|
-
tapAsync,
|
|
705
|
-
unwrap,
|
|
706
|
-
unwrapOr,
|
|
707
|
-
gen,
|
|
708
|
-
await: resultAwait,
|
|
709
|
-
serialize,
|
|
710
|
-
deserialize,
|
|
711
|
-
hydrate,
|
|
712
|
-
partition,
|
|
713
|
-
flatten
|
|
714
|
-
};
|
|
703
|
+
if (specialTemplates[template]) {
|
|
704
|
+
return specialTemplates[template];
|
|
705
|
+
}
|
|
706
|
+
if (template.startsWith("npm:")) {
|
|
707
|
+
return template.replace("npm:", "npm:/");
|
|
708
|
+
}
|
|
709
|
+
if (template.includes("/") && !template.includes(":")) {
|
|
710
|
+
return `github:${template}`;
|
|
711
|
+
}
|
|
712
|
+
return template;
|
|
713
|
+
}
|
|
714
|
+
function getBundledTemplatePath(name) {
|
|
715
|
+
return join2(import.meta.dir, "..", "templates", name);
|
|
716
|
+
}
|
|
717
|
+
async function isLocalTemplate(template) {
|
|
718
|
+
if (template.startsWith("file:") || template.startsWith("./") || template.startsWith("../")) {
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
const bundledPath = getBundledTemplatePath(template);
|
|
722
|
+
return await Bun.file(join2(bundledPath, "package.json")).exists();
|
|
723
|
+
}
|
|
715
724
|
|
|
716
725
|
// src/create-project.ts
|
|
717
726
|
var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -770,7 +779,9 @@ async function createProject(options) {
|
|
|
770
779
|
const { name, dir, template, git, install, prompt, spinner, colors, shell, offline } = options;
|
|
771
780
|
const directoryCheck = await shell`test -d ${dir}`.nothrow();
|
|
772
781
|
if (directoryCheck.exitCode === 0) {
|
|
773
|
-
const overwrite = await prompt.confirm(`Directory ${dir} already exists. Overwrite?`, {
|
|
782
|
+
const overwrite = await prompt.confirm(`Directory ${dir} already exists. Overwrite?`, {
|
|
783
|
+
default: false
|
|
784
|
+
});
|
|
774
785
|
if (!overwrite) {
|
|
775
786
|
return Result.err(new UserCancelledError("Cancelled"));
|
|
776
787
|
}
|
|
@@ -844,10 +855,11 @@ async function createProject(options) {
|
|
|
844
855
|
}
|
|
845
856
|
|
|
846
857
|
// src/create.ts
|
|
847
|
-
import path from "path";
|
|
848
858
|
class InvalidProjectNameError extends TaggedError("InvalidProjectNameError")() {
|
|
849
859
|
constructor(name) {
|
|
850
|
-
super({
|
|
860
|
+
super({
|
|
861
|
+
message: `Project name "${name}" must only contain lowercase letters, numbers, and hyphens`
|
|
862
|
+
});
|
|
851
863
|
}
|
|
852
864
|
}
|
|
853
865
|
|
|
@@ -941,11 +953,28 @@ async function run() {
|
|
|
941
953
|
description: "Create a new Bunli CLI project",
|
|
942
954
|
options: {
|
|
943
955
|
name: option(z.string().optional(), { description: "Project name" }),
|
|
944
|
-
template: option(z.string().default("basic"), {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
956
|
+
template: option(z.string().default("basic"), {
|
|
957
|
+
short: "t",
|
|
958
|
+
description: "Project template (basic, advanced, monorepo, or github:user/repo)"
|
|
959
|
+
}),
|
|
960
|
+
dir: option(z.string().optional(), {
|
|
961
|
+
short: "d",
|
|
962
|
+
description: "Directory to create project in"
|
|
963
|
+
}),
|
|
964
|
+
git: option(z.boolean().default(true), {
|
|
965
|
+
short: "g",
|
|
966
|
+
description: "Initialize git repository",
|
|
967
|
+
argumentKind: "flag"
|
|
968
|
+
}),
|
|
969
|
+
install: option(z.boolean().default(true), {
|
|
970
|
+
short: "i",
|
|
971
|
+
description: "Install dependencies",
|
|
972
|
+
argumentKind: "flag"
|
|
973
|
+
}),
|
|
974
|
+
offline: option(z.boolean().default(false), {
|
|
975
|
+
description: "Use cached templates when available",
|
|
976
|
+
argumentKind: "flag"
|
|
977
|
+
})
|
|
949
978
|
},
|
|
950
979
|
handler: async (context) => {
|
|
951
980
|
const result = await create(context);
|