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.
Files changed (2) hide show
  1. package/index.js +48 -79
  2. 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 { execFileSync, execSync, spawn } from "node:child_process";
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. Also captures the
101
- // tag's commit SHA from the same listing (peeled `^{}` entries win, so annotated
102
- // tags resolve to the commit, not the tag object). Returns null when the repo
103
- // has no tags (falls back to the default branch).
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 = execSync(
107
- `git ls-remote --tags --sort=-v:refname ${REPO} "v*"`,
108
- { stdio: "pipe" },
109
- ).toString();
110
- let tag = null;
111
- let commit = null;
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(/^([0-9a-f]{40})\trefs\/tags\/(v[0-9][^\s^]*)(\^\{\})?$/);
114
- if (!match) continue;
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 resolved = resolveLatestTag();
220
- const templateRef = resolved?.tag ?? null;
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
- if (!downloaded) {
226
+ try {
227
+ const branchArgs = templateRef ? ["--branch", templateRef] : [];
228
+ await runFileAsync("git", ["clone", "--depth", "1", ...branchArgs, REPO, projectDir]);
243
229
  try {
244
- const branchArgs = templateRef ? ["--branch", templateRef] : [];
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
- rmSync(projectDir, { recursive: true, force: true });
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kide-app",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Scaffold a new Kide CMS project",
5
5
  "author": "Matti Hernesniemi",
6
6
  "license": "MIT",