@crustjs/create 0.0.3 → 0.0.4

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/dist/index.d.ts CHANGED
@@ -161,6 +161,24 @@ type PackageManager = "npm" | "pnpm" | "bun" | "yarn";
161
161
  */
162
162
  declare function detectPackageManager(cwd?: string): PackageManager;
163
163
  /**
164
+ * Check whether a directory is inside an existing git repository.
165
+ *
166
+ * Uses `git rev-parse --is-inside-work-tree` to detect any enclosing repo,
167
+ * including parent directories. Useful for skipping `git init` prompts when
168
+ * scaffolding inside a monorepo or existing project.
169
+ *
170
+ * @param cwd - The directory to check. Defaults to `process.cwd()`.
171
+ * @returns `true` if the directory is inside a git work tree, `false` otherwise.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * if (isInGitRepo("./my-project")) {
176
+ * // Skip git init — already inside a repo
177
+ * }
178
+ * ```
179
+ */
180
+ declare function isInGitRepo(cwd?: string): boolean;
181
+ /**
164
182
  * Check whether `git` is installed and available on the system PATH.
165
183
  *
166
184
  * Runs `git --version` and returns `true` if it exits successfully.
@@ -193,4 +211,4 @@ declare function getGitUser(): {
193
211
  name: string | null;
194
212
  email: string | null;
195
213
  };
196
- export { scaffold, runSteps, isGitInstalled, interpolate, getGitUser, detectPackageManager, ScaffoldResult, ScaffoldOptions, PostScaffoldStep, PackageManager };
214
+ export { scaffold, runSteps, isInGitRepo, isGitInstalled, interpolate, getGitUser, detectPackageManager, ScaffoldResult, ScaffoldOptions, PostScaffoldStep, PackageManager };
package/dist/index.js CHANGED
@@ -1,294 +1,2 @@
1
1
  // @bun
2
- // src/interpolate.ts
3
- function interpolate(content, context) {
4
- return content.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
5
- if (key in context) {
6
- return context[key];
7
- }
8
- return match;
9
- });
10
- }
11
- // src/scaffold.ts
12
- import {
13
- existsSync,
14
- mkdirSync,
15
- readdirSync,
16
- readFileSync,
17
- statSync,
18
- writeFileSync
19
- } from "fs";
20
- import { dirname, isAbsolute, join, relative, resolve } from "path";
21
- import { fileURLToPath } from "url";
22
-
23
- // src/isBinary.ts
24
- function isBinary(buffer) {
25
- const length = Math.min(buffer.length, 8192);
26
- for (let i = 0;i < length; i++) {
27
- if (buffer[i] === 0) {
28
- return true;
29
- }
30
- }
31
- return false;
32
- }
33
-
34
- // src/scaffold.ts
35
- function walkDir(dir) {
36
- const files = [];
37
- const entries = readdirSync(dir);
38
- for (const entry of entries) {
39
- const fullPath = join(dir, entry);
40
- const stat = statSync(fullPath);
41
- if (stat.isDirectory()) {
42
- files.push(...walkDir(fullPath));
43
- } else {
44
- files.push(fullPath);
45
- }
46
- }
47
- return files;
48
- }
49
- function renameDotfile(relativePath) {
50
- const dir = dirname(relativePath);
51
- const base = relativePath.slice(dir === "." ? 0 : dir.length + 1);
52
- if (base.startsWith("_") && !base.startsWith("__")) {
53
- const renamed = `.${base.slice(1)}`;
54
- return dir === "." ? renamed : join(dir, renamed);
55
- }
56
- return relativePath;
57
- }
58
- function isNonEmptyDir(dirPath) {
59
- if (!existsSync(dirPath)) {
60
- return false;
61
- }
62
- const stat = statSync(dirPath);
63
- if (!stat.isDirectory()) {
64
- return false;
65
- }
66
- const entries = readdirSync(dirPath);
67
- return entries.length > 0;
68
- }
69
- function findNearestPackageRoot(startPath) {
70
- let current = resolve(startPath);
71
- if (existsSync(current) && !statSync(current).isDirectory()) {
72
- current = dirname(current);
73
- }
74
- while (true) {
75
- if (existsSync(join(current, "package.json"))) {
76
- return current;
77
- }
78
- const parent = dirname(current);
79
- if (parent === current) {
80
- return null;
81
- }
82
- current = parent;
83
- }
84
- }
85
- function resolveTemplateDir(template) {
86
- if (template instanceof URL) {
87
- if (template.protocol !== "file:") {
88
- throw new Error(`Template URL must use file: protocol, got "${template.protocol}".`);
89
- }
90
- return fileURLToPath(template);
91
- }
92
- if (isAbsolute(template)) {
93
- return resolve(template);
94
- }
95
- const entrypoint = process.argv[1];
96
- if (!entrypoint) {
97
- throw new Error(`Could not resolve relative template path "${template}" because process.argv[1] is not set. Pass an absolute path or a file: URL template.`);
98
- }
99
- const packageRoot = findNearestPackageRoot(resolve(entrypoint));
100
- if (!packageRoot) {
101
- throw new Error(`Could not resolve relative template path "${template}" from entrypoint "${entrypoint}" because no package.json was found in its parent directories. Pass an absolute path or a file: URL template.`);
102
- }
103
- return resolve(packageRoot, template);
104
- }
105
- function formatTemplateInput(template) {
106
- return typeof template === "string" ? template : template.href;
107
- }
108
- async function scaffold(options) {
109
- const { template, dest, context, conflict = "abort" } = options;
110
- const templateDir = resolveTemplateDir(template);
111
- const destDir = resolve(dest);
112
- if (!existsSync(templateDir)) {
113
- throw new Error(`Template directory "${templateDir}" does not exist (from template: "${formatTemplateInput(template)}").`);
114
- }
115
- if (!statSync(templateDir).isDirectory()) {
116
- throw new Error(`Template path "${templateDir}" is not a directory (from template: "${formatTemplateInput(template)}").`);
117
- }
118
- if (conflict === "abort" && isNonEmptyDir(destDir)) {
119
- throw new Error(`Destination directory "${destDir}" already exists and is non-empty. Use conflict: "overwrite" to proceed.`);
120
- }
121
- const templateFiles = walkDir(templateDir);
122
- const writtenFiles = [];
123
- for (const absolutePath of templateFiles) {
124
- const relFromTemplate = relative(templateDir, absolutePath);
125
- const destRelPath = renameDotfile(relFromTemplate);
126
- const destFilePath = join(destDir, destRelPath);
127
- mkdirSync(dirname(destFilePath), { recursive: true });
128
- const buffer = readFileSync(absolutePath);
129
- if (isBinary(buffer)) {
130
- writeFileSync(destFilePath, buffer);
131
- } else {
132
- const content = buffer.toString("utf-8");
133
- const interpolated = interpolate(content, context);
134
- writeFileSync(destFilePath, interpolated, "utf-8");
135
- }
136
- writtenFiles.push(destRelPath);
137
- }
138
- return { files: writtenFiles };
139
- }
140
- // src/utils.ts
141
- import { existsSync as existsSync2 } from "fs";
142
- import { join as join2 } from "path";
143
- var LOCKFILE_MAP = [
144
- ["bun.lock", "bun"],
145
- ["bun.lockb", "bun"],
146
- ["pnpm-lock.yaml", "pnpm"],
147
- ["yarn.lock", "yarn"],
148
- ["package-lock.json", "npm"]
149
- ];
150
- function detectPackageManager(cwd) {
151
- const dir = cwd ?? process.cwd();
152
- for (const [lockfile, manager] of LOCKFILE_MAP) {
153
- if (existsSync2(join2(dir, lockfile))) {
154
- return manager;
155
- }
156
- }
157
- const userAgent = process.env.npm_config_user_agent;
158
- if (userAgent) {
159
- if (userAgent.startsWith("bun"))
160
- return "bun";
161
- if (userAgent.startsWith("pnpm"))
162
- return "pnpm";
163
- if (userAgent.startsWith("yarn"))
164
- return "yarn";
165
- if (userAgent.startsWith("npm"))
166
- return "npm";
167
- }
168
- return "npm";
169
- }
170
- function isGitInstalled() {
171
- try {
172
- const result = Bun.spawnSync(["git", "--version"]);
173
- return result.exitCode === 0;
174
- } catch {
175
- return false;
176
- }
177
- }
178
- function getGitUser() {
179
- return {
180
- name: readGitConfig("user.name"),
181
- email: readGitConfig("user.email")
182
- };
183
- }
184
- function readGitConfig(key) {
185
- try {
186
- const result = Bun.spawnSync(["git", "config", key]);
187
- if (result.exitCode !== 0) {
188
- return null;
189
- }
190
- const value = result.stdout.toString().trim();
191
- return value.length > 0 ? value : null;
192
- } catch {
193
- return null;
194
- }
195
- }
196
-
197
- // src/steps.ts
198
- async function runSteps(steps, cwd) {
199
- for (const step of steps) {
200
- switch (step.type) {
201
- case "install":
202
- await runInstall(cwd);
203
- break;
204
- case "git-init":
205
- await runGitInit(cwd, step.commit);
206
- break;
207
- case "open-editor":
208
- await runOpenEditor(cwd);
209
- break;
210
- case "command":
211
- await runCommand(step.cmd, step.cwd ?? cwd);
212
- break;
213
- }
214
- }
215
- }
216
- async function runInstall(cwd) {
217
- const pm = detectPackageManager(cwd);
218
- const proc = Bun.spawn([pm, "install"], {
219
- cwd,
220
- stdout: "inherit",
221
- stderr: "inherit"
222
- });
223
- const exitCode = await proc.exited;
224
- if (exitCode !== 0) {
225
- throw new Error(`"${pm} install" exited with code ${exitCode}`);
226
- }
227
- }
228
- async function runGitInit(cwd, commit) {
229
- await spawnChecked(["git", "init"], cwd, "git init");
230
- if (commit) {
231
- await ensureGitIdentity(cwd);
232
- await spawnChecked(["git", "add", "."], cwd, "git add");
233
- await spawnChecked(["git", "commit", "-m", commit], cwd, "git commit");
234
- }
235
- }
236
- async function runOpenEditor(cwd) {
237
- const editor = process.env.EDITOR || "code";
238
- try {
239
- const proc = Bun.spawn([editor, cwd], {
240
- stdout: "ignore",
241
- stderr: "ignore"
242
- });
243
- const raceResult = await Promise.race([
244
- proc.exited.then((code) => ({ kind: "exited", code })),
245
- new Promise((resolve2) => setTimeout(() => resolve2({ kind: "timeout" }), 500))
246
- ]);
247
- if (raceResult.kind === "exited" && raceResult.code !== 0) {
248
- console.warn(`Warning: could not open editor "${editor}" (exit code ${raceResult.code})`);
249
- }
250
- } catch {
251
- console.warn(`Warning: could not open editor "${editor}"`);
252
- }
253
- }
254
- async function runCommand(cmd, cwd) {
255
- const proc = Bun.spawn(["sh", "-c", cmd], {
256
- cwd,
257
- stdout: "inherit",
258
- stderr: "inherit"
259
- });
260
- const exitCode = await proc.exited;
261
- if (exitCode !== 0) {
262
- throw new Error(`Command "${cmd}" exited with code ${exitCode}`);
263
- }
264
- }
265
- async function ensureGitIdentity(cwd) {
266
- const hasName = Bun.spawnSync(["git", "config", "user.name"], { cwd }).exitCode === 0;
267
- const hasEmail = Bun.spawnSync(["git", "config", "user.email"], { cwd }).exitCode === 0;
268
- if (!hasName) {
269
- await spawnChecked(["git", "config", "user.name", "Crust"], cwd, "git config user.name");
270
- }
271
- if (!hasEmail) {
272
- await spawnChecked(["git", "config", "user.email", "crust@scaffolded.project"], cwd, "git config user.email");
273
- }
274
- }
275
- async function spawnChecked(cmd, cwd, label) {
276
- const proc = Bun.spawn(cmd, {
277
- cwd,
278
- stdout: "ignore",
279
- stderr: "pipe"
280
- });
281
- const exitCode = await proc.exited;
282
- if (exitCode !== 0) {
283
- const stderr = await new Response(proc.stderr).text();
284
- throw new Error(`"${label}" failed with exit code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`);
285
- }
286
- }
287
- export {
288
- scaffold,
289
- runSteps,
290
- isGitInstalled,
291
- interpolate,
292
- getGitUser,
293
- detectPackageManager
294
- };
2
+ function K(b,H){return b.replace(/\{\{\s*(\w+)\s*\}\}/g,(W,q)=>{if(q in H)return H[q];return W})}import{existsSync as V,mkdirSync as F,readdirSync as j,readFileSync as v,statSync as Y,writeFileSync as N}from"fs";import{dirname as X,isAbsolute as D,join as Z,relative as C,resolve as M}from"path";import{fileURLToPath as k}from"url";function L(b){let H=Math.min(b.length,8192);for(let W=0;W<H;W++)if(b[W]===0)return!0;return!1}function E(b){let H=[],W=j(b);for(let q of W){let B=Z(b,q);if(Y(B).isDirectory())H.push(...E(B));else H.push(B)}return H}function S(b){let H=X(b),W=b.slice(H==="."?0:H.length+1);if(W.startsWith("_")&&!W.startsWith("__")){let q=`.${W.slice(1)}`;return H==="."?q:Z(H,q)}return b}function h(b){if(!V(b))return!1;if(!Y(b).isDirectory())return!1;return j(b).length>0}function y(b){let H=M(b);if(V(H)&&!Y(H).isDirectory())H=X(H);while(!0){if(V(Z(H,"package.json")))return H;let W=X(H);if(W===H)return null;H=W}}function u(b){if(b instanceof URL){if(b.protocol!=="file:")throw Error(`Template URL must use file: protocol, got "${b.protocol}".`);return k(b)}if(D(b))return M(b);let H=process.argv[1];if(!H)throw Error(`Could not resolve relative template path "${b}" because process.argv[1] is not set. Pass an absolute path or a file: URL template.`);let W=y(M(H));if(!W)throw Error(`Could not resolve relative template path "${b}" from entrypoint "${H}" because no package.json was found in its parent directories. Pass an absolute path or a file: URL template.`);return M(W,b)}function f(b){return typeof b==="string"?b:b.href}async function P(b){let{template:H,dest:W,context:q,conflict:B="abort"}=b,J=u(H),$=M(W);if(!V(J))throw Error(`Template directory "${J}" does not exist (from template: "${f(H)}").`);if(!Y(J).isDirectory())throw Error(`Template path "${J}" is not a directory (from template: "${f(H)}").`);if(B==="abort"&&h($))throw Error(`Destination directory "${$}" already exists and is non-empty. Use conflict: "overwrite" to proceed.`);let I=E(J),U=[];for(let _ of I){let A=C(J,_),T=S(A),g=Z($,T);F(X(g),{recursive:!0});let z=v(_);if(L(z))N(g,z);else{let x=z.toString("utf-8"),R=K(x,q);N(g,R,"utf-8")}U.push(T)}return{files:U}}import{existsSync as n}from"fs";import{join as o}from"path";var w=[["bun.lock","bun"],["bun.lockb","bun"],["pnpm-lock.yaml","pnpm"],["yarn.lock","yarn"],["package-lock.json","npm"]];function O(b){let H=b??process.cwd();for(let[q,B]of w)if(n(o(H,q)))return B;let W=process.env.npm_config_user_agent;if(W){if(W.startsWith("bun"))return"bun";if(W.startsWith("pnpm"))return"pnpm";if(W.startsWith("yarn"))return"yarn";if(W.startsWith("npm"))return"npm"}return"npm"}function p(b){try{return Bun.spawnSync(["git","rev-parse","--is-inside-work-tree"],{cwd:b??process.cwd(),stdout:"ignore",stderr:"ignore"}).exitCode===0}catch{return!1}}function m(){try{return Bun.spawnSync(["git","--version"]).exitCode===0}catch{return!1}}function s(){return{name:G("user.name"),email:G("user.email")}}function G(b){try{let H=Bun.spawnSync(["git","config",b]);if(H.exitCode!==0)return null;let W=H.stdout.toString().trim();return W.length>0?W:null}catch{return null}}async function r(b,H){for(let W of b)switch(W.type){case"install":await c(H);break;case"git-init":await l(H,W.commit);break;case"open-editor":await i(H);break;case"command":await a(W.cmd,W.cwd??H);break}}async function c(b){let H=O(b),q=await Bun.spawn([H,"install"],{cwd:b,stdout:"inherit",stderr:"inherit"}).exited;if(q!==0)throw Error(`"${H} install" exited with code ${q}`)}async function l(b,H){if(await Q(["git","init"],b,"git init"),H)await d(b),await Q(["git","add","."],b,"git add"),await Q(["git","commit","-m",H],b,"git commit")}async function i(b){let H=process.env.EDITOR||"code";try{let W=Bun.spawn([H,b],{stdout:"ignore",stderr:"ignore"}),q=await Promise.race([W.exited.then((B)=>({kind:"exited",code:B})),new Promise((B)=>setTimeout(()=>B({kind:"timeout"}),500))]);if(q.kind==="exited"&&q.code!==0)console.warn(`Warning: could not open editor "${H}" (exit code ${q.code})`)}catch{console.warn(`Warning: could not open editor "${H}"`)}}async function a(b,H){let q=await Bun.spawn(["sh","-c",b],{cwd:H,stdout:"inherit",stderr:"inherit"}).exited;if(q!==0)throw Error(`Command "${b}" exited with code ${q}`)}async function d(b){let H=Bun.spawnSync(["git","config","user.name"],{cwd:b}).exitCode===0,W=Bun.spawnSync(["git","config","user.email"],{cwd:b}).exitCode===0;if(!H)await Q(["git","config","user.name","Crust"],b,"git config user.name");if(!W)await Q(["git","config","user.email","crust@scaffolded.project"],b,"git config user.email")}async function Q(b,H,W){let q=Bun.spawn(b,{cwd:H,stdout:"ignore",stderr:"pipe"}),B=await q.exited;if(B!==0){let J=await new Response(q.stderr).text();throw Error(`"${W}" failed with exit code ${B}${J?`: ${J.trim()}`:""}`)}}export{P as scaffold,r as runSteps,p as isInGitRepo,m as isGitInstalled,K as interpolate,s as getGitUser,O as detectPackageManager};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/create",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Headless scaffolding engine for building create-xxx tools",
5
5
  "type": "module",
6
6
  "license": "MIT",