@crustjs/create 0.0.2 → 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 +23 -3
- package/dist/index.js +1 -266
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,8 @@ interface ScaffoldOptions {
|
|
|
33
33
|
/**
|
|
34
34
|
* Template directory source.
|
|
35
35
|
*
|
|
36
|
-
* - `string
|
|
36
|
+
* - `string` absolute path: used as-is
|
|
37
|
+
* - `string` relative path: resolved from the nearest package root of `process.argv[1]`
|
|
37
38
|
* - `URL`: must be a `file:` URL (for module-relative templates)
|
|
38
39
|
*/
|
|
39
40
|
readonly template: string | URL;
|
|
@@ -92,7 +93,8 @@ type PostScaffoldStep = {
|
|
|
92
93
|
* and dotfile renaming.
|
|
93
94
|
*
|
|
94
95
|
* Template resolution:
|
|
95
|
-
* - `string`
|
|
96
|
+
* - `string` absolute paths are used as-is
|
|
97
|
+
* - `string` relative paths resolve from the nearest package root of `process.argv[1]`
|
|
96
98
|
* - `URL` templates must be `file:` URLs (for module-relative templates)
|
|
97
99
|
*
|
|
98
100
|
* Call `scaffold()` multiple times to layer/compose templates — for example,
|
|
@@ -159,6 +161,24 @@ type PackageManager = "npm" | "pnpm" | "bun" | "yarn";
|
|
|
159
161
|
*/
|
|
160
162
|
declare function detectPackageManager(cwd?: string): PackageManager;
|
|
161
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
|
+
/**
|
|
162
182
|
* Check whether `git` is installed and available on the system PATH.
|
|
163
183
|
*
|
|
164
184
|
* Runs `git --version` and returns `true` if it exits successfully.
|
|
@@ -191,4 +211,4 @@ declare function getGitUser(): {
|
|
|
191
211
|
name: string | null;
|
|
192
212
|
email: string | null;
|
|
193
213
|
};
|
|
194
|
-
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,267 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
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, 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 resolveTemplateDir(template) {
|
|
70
|
-
if (template instanceof URL) {
|
|
71
|
-
if (template.protocol !== "file:") {
|
|
72
|
-
throw new Error(`Template URL must use file: protocol, got "${template.protocol}".`);
|
|
73
|
-
}
|
|
74
|
-
return fileURLToPath(template);
|
|
75
|
-
}
|
|
76
|
-
return resolve(process.cwd(), template);
|
|
77
|
-
}
|
|
78
|
-
function formatTemplateInput(template) {
|
|
79
|
-
return typeof template === "string" ? template : template.href;
|
|
80
|
-
}
|
|
81
|
-
async function scaffold(options) {
|
|
82
|
-
const { template, dest, context, conflict = "abort" } = options;
|
|
83
|
-
const templateDir = resolveTemplateDir(template);
|
|
84
|
-
const destDir = resolve(dest);
|
|
85
|
-
if (!existsSync(templateDir)) {
|
|
86
|
-
throw new Error(`Template directory "${templateDir}" does not exist (from template: "${formatTemplateInput(template)}").`);
|
|
87
|
-
}
|
|
88
|
-
if (!statSync(templateDir).isDirectory()) {
|
|
89
|
-
throw new Error(`Template path "${templateDir}" is not a directory (from template: "${formatTemplateInput(template)}").`);
|
|
90
|
-
}
|
|
91
|
-
if (conflict === "abort" && isNonEmptyDir(destDir)) {
|
|
92
|
-
throw new Error(`Destination directory "${destDir}" already exists and is non-empty. Use conflict: "overwrite" to proceed.`);
|
|
93
|
-
}
|
|
94
|
-
const templateFiles = walkDir(templateDir);
|
|
95
|
-
const writtenFiles = [];
|
|
96
|
-
for (const absolutePath of templateFiles) {
|
|
97
|
-
const relFromTemplate = relative(templateDir, absolutePath);
|
|
98
|
-
const destRelPath = renameDotfile(relFromTemplate);
|
|
99
|
-
const destFilePath = join(destDir, destRelPath);
|
|
100
|
-
mkdirSync(dirname(destFilePath), { recursive: true });
|
|
101
|
-
const buffer = readFileSync(absolutePath);
|
|
102
|
-
if (isBinary(buffer)) {
|
|
103
|
-
writeFileSync(destFilePath, buffer);
|
|
104
|
-
} else {
|
|
105
|
-
const content = buffer.toString("utf-8");
|
|
106
|
-
const interpolated = interpolate(content, context);
|
|
107
|
-
writeFileSync(destFilePath, interpolated, "utf-8");
|
|
108
|
-
}
|
|
109
|
-
writtenFiles.push(destRelPath);
|
|
110
|
-
}
|
|
111
|
-
return { files: writtenFiles };
|
|
112
|
-
}
|
|
113
|
-
// src/utils.ts
|
|
114
|
-
import { existsSync as existsSync2 } from "fs";
|
|
115
|
-
import { join as join2 } from "path";
|
|
116
|
-
var LOCKFILE_MAP = [
|
|
117
|
-
["bun.lock", "bun"],
|
|
118
|
-
["bun.lockb", "bun"],
|
|
119
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
120
|
-
["yarn.lock", "yarn"],
|
|
121
|
-
["package-lock.json", "npm"]
|
|
122
|
-
];
|
|
123
|
-
function detectPackageManager(cwd) {
|
|
124
|
-
const dir = cwd ?? process.cwd();
|
|
125
|
-
for (const [lockfile, manager] of LOCKFILE_MAP) {
|
|
126
|
-
if (existsSync2(join2(dir, lockfile))) {
|
|
127
|
-
return manager;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const userAgent = process.env.npm_config_user_agent;
|
|
131
|
-
if (userAgent) {
|
|
132
|
-
if (userAgent.startsWith("bun"))
|
|
133
|
-
return "bun";
|
|
134
|
-
if (userAgent.startsWith("pnpm"))
|
|
135
|
-
return "pnpm";
|
|
136
|
-
if (userAgent.startsWith("yarn"))
|
|
137
|
-
return "yarn";
|
|
138
|
-
if (userAgent.startsWith("npm"))
|
|
139
|
-
return "npm";
|
|
140
|
-
}
|
|
141
|
-
return "npm";
|
|
142
|
-
}
|
|
143
|
-
function isGitInstalled() {
|
|
144
|
-
try {
|
|
145
|
-
const result = Bun.spawnSync(["git", "--version"]);
|
|
146
|
-
return result.exitCode === 0;
|
|
147
|
-
} catch {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function getGitUser() {
|
|
152
|
-
return {
|
|
153
|
-
name: readGitConfig("user.name"),
|
|
154
|
-
email: readGitConfig("user.email")
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function readGitConfig(key) {
|
|
158
|
-
try {
|
|
159
|
-
const result = Bun.spawnSync(["git", "config", key]);
|
|
160
|
-
if (result.exitCode !== 0) {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
const value = result.stdout.toString().trim();
|
|
164
|
-
return value.length > 0 ? value : null;
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// src/steps.ts
|
|
171
|
-
async function runSteps(steps, cwd) {
|
|
172
|
-
for (const step of steps) {
|
|
173
|
-
switch (step.type) {
|
|
174
|
-
case "install":
|
|
175
|
-
await runInstall(cwd);
|
|
176
|
-
break;
|
|
177
|
-
case "git-init":
|
|
178
|
-
await runGitInit(cwd, step.commit);
|
|
179
|
-
break;
|
|
180
|
-
case "open-editor":
|
|
181
|
-
await runOpenEditor(cwd);
|
|
182
|
-
break;
|
|
183
|
-
case "command":
|
|
184
|
-
await runCommand(step.cmd, step.cwd ?? cwd);
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async function runInstall(cwd) {
|
|
190
|
-
const pm = detectPackageManager(cwd);
|
|
191
|
-
const proc = Bun.spawn([pm, "install"], {
|
|
192
|
-
cwd,
|
|
193
|
-
stdout: "inherit",
|
|
194
|
-
stderr: "inherit"
|
|
195
|
-
});
|
|
196
|
-
const exitCode = await proc.exited;
|
|
197
|
-
if (exitCode !== 0) {
|
|
198
|
-
throw new Error(`"${pm} install" exited with code ${exitCode}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async function runGitInit(cwd, commit) {
|
|
202
|
-
await spawnChecked(["git", "init"], cwd, "git init");
|
|
203
|
-
if (commit) {
|
|
204
|
-
await ensureGitIdentity(cwd);
|
|
205
|
-
await spawnChecked(["git", "add", "."], cwd, "git add");
|
|
206
|
-
await spawnChecked(["git", "commit", "-m", commit], cwd, "git commit");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
async function runOpenEditor(cwd) {
|
|
210
|
-
const editor = process.env.EDITOR || "code";
|
|
211
|
-
try {
|
|
212
|
-
const proc = Bun.spawn([editor, cwd], {
|
|
213
|
-
stdout: "ignore",
|
|
214
|
-
stderr: "ignore"
|
|
215
|
-
});
|
|
216
|
-
const raceResult = await Promise.race([
|
|
217
|
-
proc.exited.then((code) => ({ kind: "exited", code })),
|
|
218
|
-
new Promise((resolve2) => setTimeout(() => resolve2({ kind: "timeout" }), 500))
|
|
219
|
-
]);
|
|
220
|
-
if (raceResult.kind === "exited" && raceResult.code !== 0) {
|
|
221
|
-
console.warn(`Warning: could not open editor "${editor}" (exit code ${raceResult.code})`);
|
|
222
|
-
}
|
|
223
|
-
} catch {
|
|
224
|
-
console.warn(`Warning: could not open editor "${editor}"`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function runCommand(cmd, cwd) {
|
|
228
|
-
const proc = Bun.spawn(["sh", "-c", cmd], {
|
|
229
|
-
cwd,
|
|
230
|
-
stdout: "inherit",
|
|
231
|
-
stderr: "inherit"
|
|
232
|
-
});
|
|
233
|
-
const exitCode = await proc.exited;
|
|
234
|
-
if (exitCode !== 0) {
|
|
235
|
-
throw new Error(`Command "${cmd}" exited with code ${exitCode}`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async function ensureGitIdentity(cwd) {
|
|
239
|
-
const hasName = Bun.spawnSync(["git", "config", "user.name"], { cwd }).exitCode === 0;
|
|
240
|
-
const hasEmail = Bun.spawnSync(["git", "config", "user.email"], { cwd }).exitCode === 0;
|
|
241
|
-
if (!hasName) {
|
|
242
|
-
await spawnChecked(["git", "config", "user.name", "Crust"], cwd, "git config user.name");
|
|
243
|
-
}
|
|
244
|
-
if (!hasEmail) {
|
|
245
|
-
await spawnChecked(["git", "config", "user.email", "crust@scaffolded.project"], cwd, "git config user.email");
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
async function spawnChecked(cmd, cwd, label) {
|
|
249
|
-
const proc = Bun.spawn(cmd, {
|
|
250
|
-
cwd,
|
|
251
|
-
stdout: "ignore",
|
|
252
|
-
stderr: "pipe"
|
|
253
|
-
});
|
|
254
|
-
const exitCode = await proc.exited;
|
|
255
|
-
if (exitCode !== 0) {
|
|
256
|
-
const stderr = await new Response(proc.stderr).text();
|
|
257
|
-
throw new Error(`"${label}" failed with exit code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
export {
|
|
261
|
-
scaffold,
|
|
262
|
-
runSteps,
|
|
263
|
-
isGitInstalled,
|
|
264
|
-
interpolate,
|
|
265
|
-
getGitUser,
|
|
266
|
-
detectPackageManager
|
|
267
|
-
};
|
|
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};
|