mick-templates 1.1.2 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mick-templates",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "CLI tool for creating projects from templates",
5
5
  "bin": {
6
6
  "mick-templates": "bin/cli.js"
@@ -8,8 +8,7 @@
8
8
  "files": [
9
9
  "templates",
10
10
  "src",
11
- "bin",
12
- "ts-to-js-codemod.js"
11
+ "bin"
13
12
  ],
14
13
  "type": "module",
15
14
  "scripts": {
@@ -25,12 +24,11 @@
25
24
  "author": "Mick",
26
25
  "license": "MIT",
27
26
  "dependencies": {
28
- "execa": "^9.5.2",
29
27
  "glob": "^10.3.10",
30
28
  "ink": "^4.4.1",
31
29
  "ink-select-input": "^5.0.0",
32
30
  "jscodeshift": "^17.3.0",
33
- "picocolors": "^1.1.1",
31
+ "package-manager-detector": "^1.6.0",
34
32
  "react": "^18.2.0",
35
33
  "tsx": "^4.21.0"
36
34
  },
package/src/app.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React, { useState, useEffect, useCallback } from "react";
2
2
  import { readdirSync, existsSync } from "fs";
3
3
  import { LanguageSelect } from "./components/LanguageSelect.js";
4
4
  import { OverwritePrompt } from "./components/OverwritePrompt.js";
@@ -14,13 +14,47 @@ interface Props {
14
14
  skipInstall?: boolean;
15
15
  }
16
16
 
17
- export function App({ template, projectName, language: initialLang, skipInstall = false }: Props) {
17
+ export function App({
18
+ template,
19
+ projectName,
20
+ language: initialLang,
21
+ skipInstall = false,
22
+ }: Props) {
18
23
  const [step, setStep] = useState<Step>("processing");
19
24
  const [language, setLanguage] = useState<Language | undefined>(initialLang);
25
+ const [isCreating, setIsCreating] = useState(false);
20
26
  const [targetDir, setTargetDir] = useState("");
21
27
  const [error, setError] = useState("");
22
28
 
23
- const needsOverwrite = !projectName && existsSync(process.cwd()) && readdirSync(process.cwd()).length > 0;
29
+ const needsOverwrite =
30
+ !projectName &&
31
+ existsSync(process.cwd()) &&
32
+ readdirSync(process.cwd()).length > 0;
33
+
34
+ const runCreate = useCallback(
35
+ async (lang: Language, overwrite: boolean) => {
36
+ if (isCreating) return;
37
+ setIsCreating(true);
38
+ setStep("processing");
39
+ try {
40
+ const dir = await createProject({
41
+ template,
42
+ projectName,
43
+ language: lang,
44
+ overwrite,
45
+ skipInstall,
46
+ });
47
+ setTargetDir(dir);
48
+ setStep("done");
49
+ } catch (e) {
50
+ setError(e instanceof Error ? e.message : String(e));
51
+ setStep("error");
52
+ } finally {
53
+ setIsCreating(false);
54
+ }
55
+ },
56
+ [isCreating, template, projectName, skipInstall]
57
+ );
24
58
 
25
59
  useEffect(() => {
26
60
  // Determine initial step
@@ -34,37 +68,26 @@ export function App({ template, projectName, language: initialLang, skipInstall
34
68
  return;
35
69
  }
36
70
 
37
- // Don't run if we're waiting for user input
38
- if (step === "language" || step === "overwrite") return;
71
+ // Don't run if we're waiting for user input or already creating
72
+ if (
73
+ step === "language" ||
74
+ step === "overwrite" ||
75
+ isCreating ||
76
+ step === "done" ||
77
+ step === "error"
78
+ )
79
+ return;
39
80
 
40
81
  // Create project
41
82
  runCreate(language, false);
42
- }, [language]);
43
-
44
- const runCreate = async (lang: Language, overwrite: boolean) => {
45
- setStep("processing");
46
- try {
47
- const dir = await createProject({
48
- template,
49
- projectName,
50
- language: lang,
51
- overwrite,
52
- skipInstall,
53
- });
54
- setTargetDir(dir);
55
- setStep("done");
56
- } catch (e) {
57
- setError(e instanceof Error ? e.message : String(e));
58
- setStep("error");
59
- }
60
- };
83
+ }, [language, step, isCreating, needsOverwrite, runCreate]);
61
84
 
62
85
  const handleLanguageSelect = (lang: Language) => {
63
86
  setLanguage(lang);
64
87
  if (needsOverwrite) {
65
88
  setStep("overwrite");
66
89
  } else {
67
- runCreate(lang, false);
90
+ setStep("processing");
68
91
  }
69
92
  };
70
93
 
@@ -85,4 +108,3 @@ export function App({ template, projectName, language: initialLang, skipInstall
85
108
 
86
109
  return <Status status={step} targetDir={targetDir} error={error} />;
87
110
  }
88
-
@@ -2,8 +2,7 @@ import { readFile, writeFile, rename, rm } from "fs/promises";
2
2
  import { glob } from "glob";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { execa } from "execa";
6
- import { getPMX, detectPM } from "./pm.js";
5
+ import { execute } from "./pm.js";
7
6
 
8
7
  const TS_DEPS = [
9
8
  "typescript",
@@ -18,38 +17,27 @@ const TS_DEPS = [
18
17
  * Convert a TypeScript project to JavaScript.
19
18
  */
20
19
  export async function convertToJS(targetDir: string): Promise<void> {
21
- // Remove tsconfig.json
22
20
  await rm(join(targetDir, "tsconfig.json"), { force: true });
23
21
 
24
- // Rename vite.config.ts -> vite.config.js
25
22
  await renameIfExists(
26
23
  join(targetDir, "vite.config.ts"),
27
24
  join(targetDir, "vite.config.js")
28
25
  );
29
26
 
30
- // Rename all .tsx -> .jsx
31
27
  const tsxFiles = await glob("**/*.tsx", { cwd: targetDir });
32
28
  for (const file of tsxFiles) {
33
29
  const oldPath = join(targetDir, file);
34
- const newPath = oldPath.replace(/\.tsx$/, ".jsx");
35
- await rename(oldPath, newPath);
30
+ await rename(oldPath, oldPath.replace(/\.tsx$/, ".jsx"));
36
31
  }
37
32
 
38
- // Rename all .ts -> .js
39
33
  const tsFiles = await glob("**/*.ts", { cwd: targetDir });
40
34
  for (const file of tsFiles) {
41
35
  const oldPath = join(targetDir, file);
42
- const newPath = oldPath.replace(/\.ts$/, ".js");
43
- await rename(oldPath, newPath);
36
+ await rename(oldPath, oldPath.replace(/\.ts$/, ".js"));
44
37
  }
45
38
 
46
- // Update HTML files to reference .jsx instead of .tsx
47
39
  await updateHTMLScripts(targetDir);
48
-
49
- // Run jscodeshift on all JS/JSX files to strip types
50
40
  await runCodemod(targetDir);
51
-
52
- // Clean up package.json
53
41
  await cleanPackageJSON(targetDir);
54
42
  }
55
43
 
@@ -57,7 +45,7 @@ async function renameIfExists(from: string, to: string): Promise<void> {
57
45
  try {
58
46
  await rename(from, to);
59
47
  } catch {
60
- // File doesn't exist, ignore
48
+ // File doesn't exist
61
49
  }
62
50
  }
63
51
 
@@ -77,20 +65,11 @@ async function runCodemod(targetDir: string): Promise<void> {
77
65
  if (jsFiles.length === 0) return;
78
66
 
79
67
  const __dirname = dirname(fileURLToPath(import.meta.url));
80
- const codemodPath = join(__dirname, "..", "..", "ts-to-js-codemod.js");
81
- const pmx = getPMX(detectPM());
82
- const [cmd, ...baseArgs] = pmx.split(" ");
68
+ const codemodPath = join(__dirname, "..", "ts-to-js-codemod.mjs");
83
69
 
84
70
  for (const file of jsFiles) {
85
71
  const filePath = join(targetDir, file);
86
- await execa(
87
- cmd,
88
- [...baseArgs, "jscodeshift", "-t", codemodPath, "--parser=tsx", filePath],
89
- {
90
- cwd: targetDir,
91
- stdio: "pipe",
92
- }
93
- );
72
+ await execute(targetDir, "jscodeshift", ["-t", codemodPath, "--parser=tsx", filePath]);
94
73
  }
95
74
  }
96
75
 
@@ -99,19 +78,17 @@ async function cleanPackageJSON(targetDir: string): Promise<void> {
99
78
  try {
100
79
  const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
101
80
 
102
- // Remove TS dependencies
103
81
  for (const dep of TS_DEPS) {
104
82
  delete pkg.dependencies?.[dep];
105
83
  delete pkg.devDependencies?.[dep];
106
84
  }
107
85
 
108
- // Update build script to remove tsc
109
86
  if (pkg.scripts?.build) {
110
87
  pkg.scripts.build = pkg.scripts.build.replace("tsc -b && ", "");
111
88
  }
112
89
 
113
90
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
114
91
  } catch {
115
- // No package.json, ignore
92
+ // No package.json
116
93
  }
117
94
  }
package/src/lib/pm.ts CHANGED
@@ -1,95 +1,44 @@
1
- import { execa } from "execa";
2
- import { dirname, join } from "path";
3
-
4
- export type PackageManager = "npm" | "yarn" | "pnpm" | "bun" | "deno";
1
+ import { getUserAgent, resolveCommand } from "package-manager-detector";
2
+ import type { AgentName } from "package-manager-detector";
3
+ import { spawn } from "child_process";
5
4
 
6
5
  /**
7
- * Detect the package manager based on how the CLI was invoked.
8
- * Checks npm_config_user_agent first (set by npx/bunx/etc),
9
- * then falls back to checking the runtime executable.
6
+ * Get the current package manager agent.
10
7
  */
11
- export function detectPM(): PackageManager {
12
- const ua = process.env.npm_config_user_agent || "";
13
-
14
- if (ua.startsWith("bun/")) return "bun";
15
- if (ua.startsWith("yarn/")) return "yarn";
16
- if (ua.startsWith("pnpm/")) return "pnpm";
17
- if (ua.startsWith("deno/")) return "deno";
18
- if (ua.startsWith("npm/")) return "npm";
19
-
20
- // Fallback: check runtime executable
21
- const runtime = process.argv0.toLowerCase();
22
- if (runtime.includes("bun")) return "bun";
23
- if (runtime.includes("deno")) return "deno";
24
-
25
- return "npm";
8
+ export function getAgent(): AgentName {
9
+ const ua = getUserAgent();
10
+ return ua?.name ?? "npm";
26
11
  }
27
12
 
28
13
  /**
29
- * Get the executable path for the package manager.
30
- * Uses the runtime path if available for better reliability.
14
+ * Run a command with the detected package manager.
31
15
  */
32
- function getPMExecutable(pm: PackageManager): string {
33
- const runtime = process.argv0;
34
-
35
- // If the runtime matches the PM, use it directly (handles custom paths like ~/.bun/bin/bun)
36
- if (pm === "bun" && runtime.toLowerCase().includes("bun")) return runtime;
37
- if (pm === "deno" && runtime.toLowerCase().includes("deno")) return runtime;
38
-
39
- return pm;
16
+ function run(cmd: string, args: string[], cwd: string): Promise<void> {
17
+ return new Promise((resolve, reject) => {
18
+ spawn(cmd, args, { cwd, stdio: "inherit" })
19
+ .on("close", (code) => (code === 0 ? resolve() : reject(new Error(`Exit ${code}`))))
20
+ .on("error", reject);
21
+ });
40
22
  }
41
23
 
42
24
  /**
43
- * Get the install command for a package manager.
25
+ * Install dependencies in a directory.
44
26
  */
45
- export function getInstallCmd(pm: PackageManager): string[] {
46
- const exe = getPMExecutable(pm);
47
-
48
- switch (pm) {
49
- case "bun":
50
- return [exe, "install"];
51
- case "yarn":
52
- return [exe, "install"];
53
- case "pnpm":
54
- return [exe, "install"];
55
- case "deno":
56
- return [exe, "install"];
57
- default:
58
- return ["npm", "install"];
59
- }
60
- }
61
-
62
- /**
63
- * Get the package runner (npx, bunx, etc.) for a package manager.
64
- */
65
- export function getPMX(pm: PackageManager): string {
66
- const runtime = process.argv0;
67
-
68
- // For bun, use bunx from the same directory as the runtime
69
- if (pm === "bun" && runtime.toLowerCase().includes("bun")) {
70
- const bunDir = dirname(runtime);
71
- return join(bunDir, "bunx");
72
- }
73
-
74
- switch (pm) {
75
- case "bun":
76
- return "bunx";
77
- case "yarn":
78
- return "yarn dlx";
79
- case "pnpm":
80
- return "pnpm dlx";
81
- case "deno":
82
- return "deno run -A npm:";
83
- default:
84
- return "npx";
85
- }
27
+ export async function install(cwd: string): Promise<void> {
28
+ const agent = getAgent();
29
+ const resolved = resolveCommand(agent, "install", []);
30
+ if (!resolved) throw new Error(`Cannot resolve install command for ${agent}`);
31
+
32
+ await run(resolved.command, resolved.args, cwd);
86
33
  }
87
34
 
88
35
  /**
89
- * Run the install command in a directory.
36
+ * Execute a package binary (npx/bunx/etc).
90
37
  */
91
- export async function install(cwd: string): Promise<void> {
92
- const pm = detectPM();
93
- const [cmd, ...args] = getInstallCmd(pm);
94
- await execa(cmd, args, { cwd, stdio: "inherit" });
38
+ export async function execute(cwd: string, pkg: string, args: string[]): Promise<void> {
39
+ const agent = getAgent();
40
+ const resolved = resolveCommand(agent, "execute", [pkg, ...args]);
41
+ if (!resolved) throw new Error(`Cannot resolve execute command for ${agent}`);
42
+
43
+ await run(resolved.command, resolved.args, cwd);
95
44
  }
@@ -1,5 +1,5 @@
1
1
  // Transform TypeScript files to JavaScript
2
- module.exports = function (fileInfo, api) {
2
+ export default function (fileInfo, api) {
3
3
  const j = api.jscodeshift;
4
4
  const root = j(fileInfo.source);
5
5
 
@@ -16,4 +16,5 @@ module.exports = function (fileInfo, api) {
16
16
  });
17
17
 
18
18
  return root.toSource();
19
- };
19
+ }
20
+
@@ -21,7 +21,13 @@
21
21
  "noUnusedParameters": true,
22
22
  "erasableSyntaxOnly": true,
23
23
  "noFallthroughCasesInSwitch": true,
24
- "noUncheckedSideEffectImports": true
24
+ "noUncheckedSideEffectImports": true,
25
+
26
+ // Paths
27
+ "baseUrl": ".",
28
+ "paths": {
29
+ "@/*": ["./src/*"],
30
+ },
25
31
  },
26
- "include": ["src", "vite.config.ts"]
32
+ "include": ["src", "vite.config.ts"],
27
33
  }
@@ -1,7 +1,13 @@
1
+ import path from "node:path";
1
2
  import react from "@vitejs/plugin-react";
2
3
  import { defineConfig } from "vite";
3
4
 
4
5
  // https://vite.dev/config/
5
6
  export default defineConfig({
6
7
  plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
7
13
  });