create-kide-app 0.3.3 → 0.3.5
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/index.js +48 -79
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import {
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import {
|
|
6
6
|
cpSync,
|
|
7
7
|
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
8
|
readdirSync,
|
|
10
9
|
readFileSync,
|
|
11
10
|
rmSync,
|
|
@@ -32,6 +31,27 @@ const runAsync = (cmd, cwd) =>
|
|
|
32
31
|
});
|
|
33
32
|
});
|
|
34
33
|
|
|
34
|
+
// Argv-based variant: no shell, so untrusted values (like the project directory)
|
|
35
|
+
// are passed as single arguments and can never be reinterpreted as commands.
|
|
36
|
+
const runFileAsync = (file, args, cwd) =>
|
|
37
|
+
new Promise((resolve, reject) => {
|
|
38
|
+
const child = spawn(file, args, { cwd });
|
|
39
|
+
let stdout = "";
|
|
40
|
+
let stderr = "";
|
|
41
|
+
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
42
|
+
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
43
|
+
child.on("error", reject);
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
if (code === 0) resolve(stdout);
|
|
46
|
+
else {
|
|
47
|
+
const err = new Error(`Command failed: ${file} ${args.join(" ")}`);
|
|
48
|
+
err.stderr = stderr;
|
|
49
|
+
err.stdout = stdout;
|
|
50
|
+
reject(err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
35
55
|
// --- Package manager detection ---
|
|
36
56
|
|
|
37
57
|
const pm = {
|
|
@@ -97,44 +117,28 @@ const validateProjectName = (value) => {
|
|
|
97
117
|
};
|
|
98
118
|
|
|
99
119
|
// Resolve the latest release tag (v-prefixed semver) so scaffolds pin to a
|
|
100
|
-
// deliberate release instead of whatever HEAD happens to be.
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
const resolveLatestTag = () => {
|
|
120
|
+
// deliberate release instead of whatever HEAD happens to be. Returns null when
|
|
121
|
+
// the repo has no tags (falls back to the default branch). Async so the
|
|
122
|
+
// "Downloading template" spinner keeps animating during the network call.
|
|
123
|
+
const resolveLatestTag = async () => {
|
|
105
124
|
try {
|
|
106
|
-
const output =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
const output = await runFileAsync("git", [
|
|
126
|
+
"ls-remote",
|
|
127
|
+
"--tags",
|
|
128
|
+
"--sort=-v:refname",
|
|
129
|
+
REPO,
|
|
130
|
+
"v*",
|
|
131
|
+
]);
|
|
112
132
|
for (const line of output.split("\n")) {
|
|
113
|
-
const match = line.match(
|
|
114
|
-
if (
|
|
115
|
-
if (!tag) {
|
|
116
|
-
tag = match[2];
|
|
117
|
-
commit = match[1];
|
|
118
|
-
}
|
|
119
|
-
if (match[2] === tag && match[3]) commit = match[1];
|
|
133
|
+
const match = line.match(/refs\/tags\/(v[0-9][^^\s]*)$/);
|
|
134
|
+
if (match) return match[1];
|
|
120
135
|
}
|
|
121
|
-
if (tag) return { tag, commit };
|
|
122
136
|
} catch {
|
|
123
137
|
// Network/git hiccup — fall back to default branch
|
|
124
138
|
}
|
|
125
139
|
return null;
|
|
126
140
|
};
|
|
127
141
|
|
|
128
|
-
// GitHub serves any tag as a single gzipped tarball — one HTTP request instead of
|
|
129
|
-
// git protocol negotiation, and no .git dir to delete afterwards. Only applicable
|
|
130
|
-
// for github.com remotes (KIDE_TEMPLATE_REPO may point anywhere, incl. file://).
|
|
131
|
-
const githubTarballUrl = (tag) => {
|
|
132
|
-
const match = REPO.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
133
|
-
return match && tag
|
|
134
|
-
? `https://codeload.github.com/${match[1]}/${match[2]}/tar.gz/refs/tags/${tag}`
|
|
135
|
-
: null;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
142
|
// --- CLI flags (non-interactive use: CI, agents, testing) ---
|
|
139
143
|
// Any prompt whose answer is supplied by a flag is skipped. Example:
|
|
140
144
|
// create-kide-app my-app --starter=marketing --seed --mode=embedded --target=local --no-github --no-dev
|
|
@@ -216,58 +220,23 @@ async function main() {
|
|
|
216
220
|
|
|
217
221
|
s.start("Downloading template");
|
|
218
222
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
let templateCommit = resolved?.commit ?? null;
|
|
222
|
-
|
|
223
|
-
// Fast path: single tarball request. Falls back to git clone on any failure.
|
|
224
|
-
let downloaded = false;
|
|
225
|
-
const tarballUrl = githubTarballUrl(templateRef);
|
|
226
|
-
if (tarballUrl) {
|
|
227
|
-
try {
|
|
228
|
-
const response = await fetch(tarballUrl);
|
|
229
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
230
|
-
const tarball = Buffer.from(await response.arrayBuffer());
|
|
231
|
-
mkdirSync(projectDir, { recursive: true });
|
|
232
|
-
execFileSync("tar", ["-xzf", "-", "--strip-components=1", "-C", projectDir], {
|
|
233
|
-
input: tarball,
|
|
234
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
235
|
-
});
|
|
236
|
-
downloaded = true;
|
|
237
|
-
} catch {
|
|
238
|
-
rmSync(projectDir, { recursive: true, force: true });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
223
|
+
const templateRef = await resolveLatestTag();
|
|
224
|
+
let templateCommit = null;
|
|
241
225
|
|
|
242
|
-
|
|
226
|
+
try {
|
|
227
|
+
const branchArgs = templateRef ? ["--branch", templateRef] : [];
|
|
228
|
+
await runFileAsync("git", ["clone", "--depth", "1", ...branchArgs, REPO, projectDir]);
|
|
243
229
|
try {
|
|
244
|
-
|
|
245
|
-
// execFileSync, not execSync: no shell, so projectDir is passed as one argv entry
|
|
246
|
-
// and can never be reinterpreted as a command regardless of what it contains.
|
|
247
|
-
execFileSync(
|
|
248
|
-
"git",
|
|
249
|
-
["clone", "--depth", "1", ...branchArgs, REPO, projectDir],
|
|
250
|
-
{
|
|
251
|
-
stdio: "pipe",
|
|
252
|
-
},
|
|
253
|
-
);
|
|
254
|
-
try {
|
|
255
|
-
templateCommit = execSync("git rev-parse HEAD", {
|
|
256
|
-
cwd: projectDir,
|
|
257
|
-
stdio: "pipe",
|
|
258
|
-
})
|
|
259
|
-
.toString()
|
|
260
|
-
.trim();
|
|
261
|
-
} catch {
|
|
262
|
-
// best-effort — stamp without a commit hash
|
|
263
|
-
}
|
|
264
|
-
rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
230
|
+
templateCommit = (await runFileAsync("git", ["rev-parse", "HEAD"], projectDir)).trim();
|
|
265
231
|
} catch {
|
|
266
|
-
|
|
267
|
-
s.stop("Failed to download template.");
|
|
268
|
-
p.cancel("Check your network connection.");
|
|
269
|
-
process.exit(1);
|
|
232
|
+
// best-effort — stamp without a commit hash
|
|
270
233
|
}
|
|
234
|
+
rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
235
|
+
} catch {
|
|
236
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
237
|
+
s.stop("Failed to download template.");
|
|
238
|
+
p.cancel("Check your network connection.");
|
|
239
|
+
process.exit(1);
|
|
271
240
|
}
|
|
272
241
|
|
|
273
242
|
// Remove files that shouldn't be in the scaffold
|