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