create-next-hydra 0.1.0
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/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +19 -0
- package/dist/clone.d.ts +9 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +19 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/fs-utils.d.ts +9 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +64 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +107 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +22 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +24 -0
- package/dist/sanitize.d.ts +9 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +14 -0
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +149 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cancel } from "@clack/prompts";
|
|
3
|
+
import { runCli } from "./index.js";
|
|
4
|
+
import { UserCancelledError } from "./prompts.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
try {
|
|
7
|
+
await runCli(process.argv);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof UserCancelledError) {
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
15
|
+
cancel(message);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
main();
|
package/dist/clone.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AAEA,KAAK,mBAAmB,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
package/dist/clone.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runGit } from "./git.js";
|
|
2
|
+
export async function cloneStarter(options) {
|
|
3
|
+
const verbose = options.verbose ?? false;
|
|
4
|
+
const cloneArgs = ["clone"];
|
|
5
|
+
if (verbose) {
|
|
6
|
+
cloneArgs.push("--progress");
|
|
7
|
+
}
|
|
8
|
+
if (options.ref) {
|
|
9
|
+
cloneArgs.push(options.repoUrl, options.targetPath);
|
|
10
|
+
await runGit(cloneArgs, { verbose });
|
|
11
|
+
await runGit(["checkout", options.ref], {
|
|
12
|
+
cwd: options.targetPath,
|
|
13
|
+
verbose,
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
cloneArgs.push("--depth", "1", options.repoUrl, options.targetPath);
|
|
18
|
+
await runGit(cloneArgs, { verbose });
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StarterDefinition } from "./types.js";
|
|
2
|
+
export declare const CLI_NAME = "create-next-hydra";
|
|
3
|
+
export declare const DEFAULT_REPO_URL = "https://github.com/jakala-na/next-hydra.git";
|
|
4
|
+
export declare const DEFAULT_COMMIT_MESSAGE = "Initial commit";
|
|
5
|
+
export declare const DEFAULT_PACKAGE_MANAGER = "pnpm";
|
|
6
|
+
export declare const DEFAULT_STARTER: StarterDefinition;
|
|
7
|
+
export declare const SANITIZE_REMOVE_PATHS: readonly [".git", "apps/docs", "pnpm-lock.yaml"];
|
|
8
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,QAAQ,sBAAsB,CAAC;AAC5C,eAAO,MAAM,gBAAgB,gDAAgD,CAAC;AAC9E,eAAO,MAAM,sBAAsB,mBAAmB,CAAC;AACvD,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAE9C,eAAO,MAAM,eAAe,EAAE,iBAG7B,CAAC;AAEF,eAAO,MAAM,qBAAqB,kDAIxB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const CLI_NAME = "create-next-hydra";
|
|
2
|
+
export const DEFAULT_REPO_URL = "https://github.com/jakala-na/next-hydra.git";
|
|
3
|
+
export const DEFAULT_COMMIT_MESSAGE = "Initial commit";
|
|
4
|
+
export const DEFAULT_PACKAGE_MANAGER = "pnpm";
|
|
5
|
+
export const DEFAULT_STARTER = {
|
|
6
|
+
id: "default",
|
|
7
|
+
repoUrl: DEFAULT_REPO_URL,
|
|
8
|
+
};
|
|
9
|
+
export const SANITIZE_REMOVE_PATHS = [
|
|
10
|
+
".git",
|
|
11
|
+
"apps/docs",
|
|
12
|
+
"pnpm-lock.yaml",
|
|
13
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function pathExists(targetPath: string): Promise<boolean>;
|
|
2
|
+
export declare function isDirectoryEmpty(targetPath: string): Promise<boolean>;
|
|
3
|
+
export declare function ensureParentDirectory(targetPath: string): Promise<void>;
|
|
4
|
+
export declare function removePath(targetPath: string): Promise<void>;
|
|
5
|
+
export declare function readJsonFile<T>(filePath: string): Promise<T>;
|
|
6
|
+
export declare function writeJsonFile(filePath: string, value: unknown): Promise<void>;
|
|
7
|
+
export declare function toDisplayPath(absolutePath: string, cwd?: string): string;
|
|
8
|
+
export declare function normalizePackageName(input: string): string;
|
|
9
|
+
//# sourceMappingURL=fs-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-utils.d.ts","sourceRoot":"","sources":["../src/fs-utils.ts"],"names":[],"mappings":"AAkBA,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;AAED,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ3E;AAED,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7E;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGlE;AAED,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,GAAG,SAAgB,GAClB,MAAM,CAYR;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB1D"}
|
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { access, mkdir, readdir, readFile, rm, stat, writeFile, } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const INVALID_PACKAGE_CHARS_REGEX = /[^a-z0-9._-]+/g;
|
|
4
|
+
const DUPLICATE_DASHES_REGEX = /-+/g;
|
|
5
|
+
const LEADING_PUNCTUATION_REGEX = /^[._-]+/;
|
|
6
|
+
const TRAILING_PUNCTUATION_REGEX = /[._-]+$/;
|
|
7
|
+
const PACKAGE_START_CHAR_REGEX = /^[a-z0-9]/;
|
|
8
|
+
const MAX_UNSCOPED_PACKAGE_NAME_LENGTH = 214;
|
|
9
|
+
export async function pathExists(targetPath) {
|
|
10
|
+
try {
|
|
11
|
+
await stat(targetPath);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function isDirectoryEmpty(targetPath) {
|
|
19
|
+
const targetStat = await stat(targetPath);
|
|
20
|
+
if (!targetStat.isDirectory()) {
|
|
21
|
+
throw new Error(`Target path exists and is not a directory: ${targetPath}`);
|
|
22
|
+
}
|
|
23
|
+
const entries = await readdir(targetPath);
|
|
24
|
+
return entries.length === 0;
|
|
25
|
+
}
|
|
26
|
+
export async function ensureParentDirectory(targetPath) {
|
|
27
|
+
const parentDir = path.dirname(targetPath);
|
|
28
|
+
await mkdir(parentDir, { recursive: true });
|
|
29
|
+
await access(parentDir);
|
|
30
|
+
}
|
|
31
|
+
export async function removePath(targetPath) {
|
|
32
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
export async function readJsonFile(filePath) {
|
|
35
|
+
const raw = await readFile(filePath, "utf8");
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
export async function writeJsonFile(filePath, value) {
|
|
39
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
export function toDisplayPath(absolutePath, cwd = process.cwd()) {
|
|
42
|
+
const relative = path.relative(cwd, absolutePath);
|
|
43
|
+
if (!relative || relative === "") {
|
|
44
|
+
return ".";
|
|
45
|
+
}
|
|
46
|
+
if (relative.startsWith("..")) {
|
|
47
|
+
return absolutePath;
|
|
48
|
+
}
|
|
49
|
+
return relative;
|
|
50
|
+
}
|
|
51
|
+
export function normalizePackageName(input) {
|
|
52
|
+
let value = input.trim().toLowerCase();
|
|
53
|
+
value = value.replace(INVALID_PACKAGE_CHARS_REGEX, "-");
|
|
54
|
+
value = value.replace(DUPLICATE_DASHES_REGEX, "-");
|
|
55
|
+
value = value.replace(LEADING_PUNCTUATION_REGEX, "");
|
|
56
|
+
value = value.replace(TRAILING_PUNCTUATION_REGEX, "");
|
|
57
|
+
if (!value) {
|
|
58
|
+
return "next-hydra-app";
|
|
59
|
+
}
|
|
60
|
+
if (!PACKAGE_START_CHAR_REGEX.test(value)) {
|
|
61
|
+
value = `app-${value}`;
|
|
62
|
+
}
|
|
63
|
+
return value.slice(0, MAX_UNSCOPED_PACKAGE_NAME_LENGTH);
|
|
64
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GitInitResult, RunCommandResult } from "./types.js";
|
|
2
|
+
type RunCommandOptions = {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
verbose?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare class CommandExecutionError extends Error {
|
|
8
|
+
readonly command: string;
|
|
9
|
+
readonly code: number | null;
|
|
10
|
+
readonly stdout: string;
|
|
11
|
+
readonly stderr: string;
|
|
12
|
+
constructor(params: {
|
|
13
|
+
command: string;
|
|
14
|
+
code: number | null;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export declare function runCommand(command: string, args: string[], options?: RunCommandOptions): Promise<RunCommandResult>;
|
|
21
|
+
export declare function runGit(args: string[], options?: RunCommandOptions): Promise<RunCommandResult>;
|
|
22
|
+
export declare function ensureGitInstalled(verbose?: boolean): Promise<void>;
|
|
23
|
+
export declare function initializeGitRepository(targetPath: string, options: {
|
|
24
|
+
commit: boolean;
|
|
25
|
+
verbose?: boolean;
|
|
26
|
+
}): Promise<GitInitResult>;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAElE,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB;CAQF;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAqD3B;AAED,wBAAgB,MAAM,CACpB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAQ3B;AAED,wBAAsB,kBAAkB,CAAC,OAAO,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9C,OAAO,CAAC,aAAa,CAAC,CAoCxB"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { DEFAULT_COMMIT_MESSAGE } from "./constants.js";
|
|
3
|
+
import { formatCommand } from "./logger.js";
|
|
4
|
+
export class CommandExecutionError extends Error {
|
|
5
|
+
command;
|
|
6
|
+
code;
|
|
7
|
+
stdout;
|
|
8
|
+
stderr;
|
|
9
|
+
constructor(params) {
|
|
10
|
+
super(params.message ?? `Command failed: ${params.command}`);
|
|
11
|
+
this.name = "CommandExecutionError";
|
|
12
|
+
this.command = params.command;
|
|
13
|
+
this.code = params.code;
|
|
14
|
+
this.stdout = params.stdout;
|
|
15
|
+
this.stderr = params.stderr;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function runCommand(command, args, options = {}) {
|
|
19
|
+
const child = spawn(command, args, {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
...options.env,
|
|
24
|
+
},
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
});
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
child.stdout.on("data", (chunk) => {
|
|
30
|
+
const text = chunk.toString();
|
|
31
|
+
stdout += text;
|
|
32
|
+
if (options.verbose) {
|
|
33
|
+
process.stdout.write(text);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
child.stderr.on("data", (chunk) => {
|
|
37
|
+
const text = chunk.toString();
|
|
38
|
+
stderr += text;
|
|
39
|
+
if (options.verbose) {
|
|
40
|
+
process.stderr.write(text);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
44
|
+
child.on("error", reject);
|
|
45
|
+
child.on("close", resolve);
|
|
46
|
+
}).catch((error) => {
|
|
47
|
+
throw new CommandExecutionError({
|
|
48
|
+
command: formatCommand(command, args),
|
|
49
|
+
code: null,
|
|
50
|
+
stdout,
|
|
51
|
+
stderr,
|
|
52
|
+
message: error instanceof Error ? error.message : "Failed to start command",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
if (exitCode !== 0) {
|
|
56
|
+
throw new CommandExecutionError({
|
|
57
|
+
command: formatCommand(command, args),
|
|
58
|
+
code: exitCode,
|
|
59
|
+
stdout,
|
|
60
|
+
stderr,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return { stdout, stderr };
|
|
64
|
+
}
|
|
65
|
+
export function runGit(args, options = {}) {
|
|
66
|
+
return runCommand("git", args, {
|
|
67
|
+
...options,
|
|
68
|
+
env: {
|
|
69
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
70
|
+
...options.env,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export async function ensureGitInstalled(verbose = false) {
|
|
75
|
+
await runGit(["--version"], { verbose });
|
|
76
|
+
}
|
|
77
|
+
export async function initializeGitRepository(targetPath, options) {
|
|
78
|
+
const verbose = options.verbose ?? false;
|
|
79
|
+
await runGit(["init"], { cwd: targetPath, verbose });
|
|
80
|
+
if (!options.commit) {
|
|
81
|
+
return {
|
|
82
|
+
gitInitialized: true,
|
|
83
|
+
committed: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
await runGit(["add", "-A"], { cwd: targetPath, verbose });
|
|
87
|
+
try {
|
|
88
|
+
await runGit(["commit", "-m", DEFAULT_COMMIT_MESSAGE], {
|
|
89
|
+
cwd: targetPath,
|
|
90
|
+
verbose,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
gitInitialized: true,
|
|
94
|
+
committed: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof CommandExecutionError) {
|
|
99
|
+
return {
|
|
100
|
+
gitInitialized: true,
|
|
101
|
+
committed: false,
|
|
102
|
+
commitError: error.stderr.trim() || error.stdout.trim() || error.message,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA+BA,wBAAsB,MAAM,CAAC,IAAI,WAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC/D;AACD,YAAY,EACV,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { CLI_NAME, DEFAULT_REPO_URL } from "./constants.js";
|
|
3
|
+
import { promptForTargetDirectory } from "./prompts.js";
|
|
4
|
+
import { scaffoldProject } from "./scaffold.js";
|
|
5
|
+
function buildCreateOptions(targetDir, rawOptions) {
|
|
6
|
+
return {
|
|
7
|
+
targetDir,
|
|
8
|
+
yes: rawOptions.yes ?? false,
|
|
9
|
+
skipGit: rawOptions.skipGit ?? false,
|
|
10
|
+
commit: rawOptions.commit ?? true,
|
|
11
|
+
ref: rawOptions.ref,
|
|
12
|
+
repoUrl: rawOptions.repoUrl ?? DEFAULT_REPO_URL,
|
|
13
|
+
verbose: rawOptions.verbose ?? false,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export async function runCli(argv = process.argv) {
|
|
17
|
+
const program = new Command();
|
|
18
|
+
program
|
|
19
|
+
.name(CLI_NAME)
|
|
20
|
+
.description("Scaffold a new next-hydra project")
|
|
21
|
+
.argument("[project-directory]", "Target directory")
|
|
22
|
+
.option("-y, --yes", "Skip prompts (requires [project-directory])")
|
|
23
|
+
.option("--skip-git", "Skip git initialization")
|
|
24
|
+
.option("--no-commit", "Initialize git but skip initial commit")
|
|
25
|
+
.option("--ref <git-ref>", "Clone and checkout a specific git ref")
|
|
26
|
+
.option("--repo-url <url>", "Override the starter repo URL")
|
|
27
|
+
.option("--verbose", "Print git command output")
|
|
28
|
+
.version("0.1.0")
|
|
29
|
+
.action(async (projectDirectory, rawOptions) => {
|
|
30
|
+
let targetDir = projectDirectory?.trim();
|
|
31
|
+
if (!targetDir) {
|
|
32
|
+
if (rawOptions.yes) {
|
|
33
|
+
throw new Error("`--yes` requires a target directory, e.g. `create-next-hydra my-app --yes`.");
|
|
34
|
+
}
|
|
35
|
+
targetDir = await promptForTargetDirectory();
|
|
36
|
+
}
|
|
37
|
+
await scaffoldProject(buildCreateOptions(targetDir, rawOptions));
|
|
38
|
+
});
|
|
39
|
+
await program.parseAsync(argv);
|
|
40
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function info(message: string): void;
|
|
2
|
+
export declare function warn(message: string): void;
|
|
3
|
+
export declare function success(message: string): void;
|
|
4
|
+
export declare function printNextSteps(message: string, title?: string): void;
|
|
5
|
+
export declare function finish(message: string): void;
|
|
6
|
+
export declare function createSpinner(): {
|
|
7
|
+
start: (msg?: string) => void;
|
|
8
|
+
stop: (msg?: string, code?: number) => void;
|
|
9
|
+
message: (msg?: string) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare function formatCommand(command: string, args: string[]): string;
|
|
12
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAe,GAAG,IAAI,CAE1E;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,aAAa;eAO4uG,CAAC;cAAiC,CAAC,cAAc,CAAC;iBAAoC,CAAC;EAL/1G;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAErE"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { log, note, outro, spinner } from "@clack/prompts";
|
|
2
|
+
export function info(message) {
|
|
3
|
+
log.info(message);
|
|
4
|
+
}
|
|
5
|
+
export function warn(message) {
|
|
6
|
+
log.warn(message);
|
|
7
|
+
}
|
|
8
|
+
export function success(message) {
|
|
9
|
+
log.success(message);
|
|
10
|
+
}
|
|
11
|
+
export function printNextSteps(message, title = "Next steps") {
|
|
12
|
+
note(message, title);
|
|
13
|
+
}
|
|
14
|
+
export function finish(message) {
|
|
15
|
+
outro(message);
|
|
16
|
+
}
|
|
17
|
+
export function createSpinner() {
|
|
18
|
+
return spinner();
|
|
19
|
+
}
|
|
20
|
+
export function formatCommand(command, args) {
|
|
21
|
+
return [command, ...args].join(" ");
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAEA,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,CAAC,CAmBhE"}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cancel, isCancel, text } from "@clack/prompts";
|
|
2
|
+
export class UserCancelledError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("Operation cancelled by user.");
|
|
5
|
+
this.name = "UserCancelledError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export async function promptForTargetDirectory() {
|
|
9
|
+
const result = await text({
|
|
10
|
+
message: "Where should the project be created?",
|
|
11
|
+
placeholder: "my-next-hydra-app",
|
|
12
|
+
validate(value) {
|
|
13
|
+
if (!value?.trim()) {
|
|
14
|
+
return "Please enter a folder name.";
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (isCancel(result)) {
|
|
20
|
+
cancel("Cancelled.");
|
|
21
|
+
throw new UserCancelledError();
|
|
22
|
+
}
|
|
23
|
+
return result.trim();
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type SanitizeStarterOptions = {
|
|
2
|
+
targetPath: string;
|
|
3
|
+
targetName: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function sanitizeStarter({ targetPath, targetName, }: SanitizeStarterOptions): Promise<{
|
|
6
|
+
packageName: string;
|
|
7
|
+
}>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AASA,KAAK,sBAAsB,GAAG;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAOF,wBAAsB,eAAe,CAAC,EACpC,UAAU,EACV,UAAU,GACX,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAc3D"}
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { SANITIZE_REMOVE_PATHS } from "./constants.js";
|
|
3
|
+
import { normalizePackageName, readJsonFile, removePath, writeJsonFile, } from "./fs-utils.js";
|
|
4
|
+
export async function sanitizeStarter({ targetPath, targetName, }) {
|
|
5
|
+
for (const relativePath of SANITIZE_REMOVE_PATHS) {
|
|
6
|
+
await removePath(path.join(targetPath, relativePath));
|
|
7
|
+
}
|
|
8
|
+
const packageJsonPath = path.join(targetPath, "package.json");
|
|
9
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
10
|
+
const packageName = normalizePackageName(targetName);
|
|
11
|
+
packageJson.name = packageName;
|
|
12
|
+
await writeJsonFile(packageJsonPath, packageJson);
|
|
13
|
+
return { packageName };
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EACV,aAAa,EAEb,cAAc,EACf,MAAM,YAAY,CAAC;AA8EpB,wBAAsB,eAAe,CACnC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,cAAc,CAAC,CAyGzB"}
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { intro } from "@clack/prompts";
|
|
3
|
+
import { cloneStarter } from "./clone.js";
|
|
4
|
+
import { DEFAULT_PACKAGE_MANAGER } from "./constants.js";
|
|
5
|
+
import { ensureParentDirectory, isDirectoryEmpty, pathExists, removePath, toDisplayPath, } from "./fs-utils.js";
|
|
6
|
+
import { CommandExecutionError, ensureGitInstalled, initializeGitRepository, } from "./git.js";
|
|
7
|
+
import { createSpinner, finish, info, printNextSteps, success, warn, } from "./logger.js";
|
|
8
|
+
import { sanitizeStarter } from "./sanitize.js";
|
|
9
|
+
const SHELL_NEEDS_QUOTING_REGEX = /[\s"'\\]/;
|
|
10
|
+
function quotePathForShell(value) {
|
|
11
|
+
if (!SHELL_NEEDS_QUOTING_REGEX.test(value)) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
15
|
+
}
|
|
16
|
+
async function resolveAndValidateTarget(inputTargetDir) {
|
|
17
|
+
const targetDir = inputTargetDir.trim();
|
|
18
|
+
const targetPath = path.resolve(process.cwd(), targetDir);
|
|
19
|
+
const targetName = path.basename(targetPath);
|
|
20
|
+
if (!targetName) {
|
|
21
|
+
throw new Error("Please provide a valid target folder.");
|
|
22
|
+
}
|
|
23
|
+
const existedBefore = await pathExists(targetPath);
|
|
24
|
+
if (existedBefore) {
|
|
25
|
+
const isEmpty = await isDirectoryEmpty(targetPath);
|
|
26
|
+
if (!isEmpty) {
|
|
27
|
+
throw new Error(`Target directory is not empty: ${toDisplayPath(targetPath)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await ensureParentDirectory(targetPath);
|
|
32
|
+
}
|
|
33
|
+
return { targetDir, targetName, targetPath, existedBefore };
|
|
34
|
+
}
|
|
35
|
+
function formatGitError(error) {
|
|
36
|
+
const isCloneCommand = error.command.startsWith("git clone ");
|
|
37
|
+
const stderr = error.stderr.trim();
|
|
38
|
+
if (stderr) {
|
|
39
|
+
if (!isCloneCommand) {
|
|
40
|
+
return `${error.command}\n\n${stderr}`;
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
error.command,
|
|
44
|
+
"",
|
|
45
|
+
stderr,
|
|
46
|
+
"",
|
|
47
|
+
"Hint: If the starter repo is private or you are testing locally, use `--repo-url`.",
|
|
48
|
+
"Example: create-next-hydra my-app --repo-url /path/to/next-hydra",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
const stdout = error.stdout.trim();
|
|
52
|
+
if (stdout) {
|
|
53
|
+
if (!isCloneCommand) {
|
|
54
|
+
return `${error.command}\n\n${stdout}`;
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
error.command,
|
|
58
|
+
"",
|
|
59
|
+
stdout,
|
|
60
|
+
"",
|
|
61
|
+
"Hint: If the starter repo is private or you are testing locally, use `--repo-url`.",
|
|
62
|
+
"Example: create-next-hydra my-app --repo-url /path/to/next-hydra",
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
return `${error.command}\n\n${error.message}`;
|
|
66
|
+
}
|
|
67
|
+
export async function scaffoldProject(options) {
|
|
68
|
+
intro("create-next-hydra");
|
|
69
|
+
const spin = createSpinner();
|
|
70
|
+
const { repoUrl, ref, skipGit, commit, verbose } = options;
|
|
71
|
+
const targetDir = options.targetDir;
|
|
72
|
+
if (!targetDir) {
|
|
73
|
+
throw new Error("Missing target directory.");
|
|
74
|
+
}
|
|
75
|
+
spin.start("Checking git availability");
|
|
76
|
+
try {
|
|
77
|
+
await ensureGitInstalled(verbose);
|
|
78
|
+
spin.stop("Git detected");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
spin.stop("Git not available");
|
|
82
|
+
throw new Error("Git is required to scaffold a project. Please install git and try again.");
|
|
83
|
+
}
|
|
84
|
+
const { targetPath, targetName, existedBefore } = await resolveAndValidateTarget(targetDir);
|
|
85
|
+
let cloneCompleted = false;
|
|
86
|
+
try {
|
|
87
|
+
spin.start("Cloning next-hydra starter");
|
|
88
|
+
await cloneStarter({
|
|
89
|
+
repoUrl,
|
|
90
|
+
targetPath,
|
|
91
|
+
ref,
|
|
92
|
+
verbose,
|
|
93
|
+
});
|
|
94
|
+
cloneCompleted = true;
|
|
95
|
+
spin.stop("Starter cloned");
|
|
96
|
+
spin.start("Sanitizing starter");
|
|
97
|
+
const { packageName } = await sanitizeStarter({ targetPath, targetName });
|
|
98
|
+
spin.stop("Starter sanitized");
|
|
99
|
+
let gitInitialized = false;
|
|
100
|
+
let committed = false;
|
|
101
|
+
if (skipGit) {
|
|
102
|
+
info("Skipped git initialization (--skip-git).");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
spin.start(commit
|
|
106
|
+
? "Initializing git repo and creating initial commit"
|
|
107
|
+
: "Initializing git repo");
|
|
108
|
+
const gitResult = await initializeGitRepository(targetPath, {
|
|
109
|
+
commit,
|
|
110
|
+
verbose,
|
|
111
|
+
});
|
|
112
|
+
gitInitialized = gitResult.gitInitialized;
|
|
113
|
+
committed = gitResult.committed;
|
|
114
|
+
spin.stop(committed
|
|
115
|
+
? "Git repo initialized with initial commit"
|
|
116
|
+
: "Git repo initialized");
|
|
117
|
+
if (gitResult.commitError) {
|
|
118
|
+
warn(`Project was scaffolded, but the initial commit failed. You can commit manually.\n${gitResult.commitError}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const displayTarget = toDisplayPath(targetPath);
|
|
122
|
+
const cdTarget = quotePathForShell(displayTarget);
|
|
123
|
+
printNextSteps([
|
|
124
|
+
`cd ${cdTarget}`,
|
|
125
|
+
`${DEFAULT_PACKAGE_MANAGER} install`,
|
|
126
|
+
`${DEFAULT_PACKAGE_MANAGER} dev`,
|
|
127
|
+
].join("\n"));
|
|
128
|
+
success(`Created project in ${displayTarget}`);
|
|
129
|
+
finish("Scaffold complete.");
|
|
130
|
+
return {
|
|
131
|
+
projectPath: targetPath,
|
|
132
|
+
packageName,
|
|
133
|
+
gitInitialized,
|
|
134
|
+
committed,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (cloneCompleted === false && existedBefore === false) {
|
|
139
|
+
await removePath(targetPath);
|
|
140
|
+
}
|
|
141
|
+
if (error instanceof CommandExecutionError) {
|
|
142
|
+
throw new Error(formatGitError(error));
|
|
143
|
+
}
|
|
144
|
+
if (error instanceof Error) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
throw new Error("Failed to scaffold project.");
|
|
148
|
+
}
|
|
149
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type CreateOptions = {
|
|
2
|
+
targetDir?: string;
|
|
3
|
+
yes: boolean;
|
|
4
|
+
skipGit: boolean;
|
|
5
|
+
commit: boolean;
|
|
6
|
+
ref?: string;
|
|
7
|
+
repoUrl: string;
|
|
8
|
+
verbose: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ResolvedCreateOptions = CreateOptions & {
|
|
11
|
+
targetDir: string;
|
|
12
|
+
targetPath: string;
|
|
13
|
+
targetName: string;
|
|
14
|
+
};
|
|
15
|
+
export type StarterDefinition = {
|
|
16
|
+
id: string;
|
|
17
|
+
repoUrl: string;
|
|
18
|
+
defaultRef?: string;
|
|
19
|
+
};
|
|
20
|
+
export type ScaffoldResult = {
|
|
21
|
+
projectPath: string;
|
|
22
|
+
packageName: string;
|
|
23
|
+
gitInitialized: boolean;
|
|
24
|
+
committed: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type GitInitResult = {
|
|
27
|
+
gitInitialized: boolean;
|
|
28
|
+
committed: boolean;
|
|
29
|
+
commitError?: string;
|
|
30
|
+
};
|
|
31
|
+
export type RunCommandResult = {
|
|
32
|
+
stdout: string;
|
|
33
|
+
stderr: string;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,aAAa,GAAG;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-next-hydra",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold a new next-hydra project",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"create-next-app",
|
|
8
|
+
"create-next-hydra",
|
|
9
|
+
"next-hydra",
|
|
10
|
+
"scaffold",
|
|
11
|
+
"starter",
|
|
12
|
+
"turborepo"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"create-next-hydra": "dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./cli": {
|
|
30
|
+
"import": "./dist/cli.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
42
|
+
"pack": "npm pack",
|
|
43
|
+
"publish:dry-run": "npm publish --access public --dry-run",
|
|
44
|
+
"publish:public": "npm publish --access public",
|
|
45
|
+
"prepublishOnly": "pnpm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@clack/prompts": "^0.11.0",
|
|
49
|
+
"commander": "^14.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@repo/typescript-config": "workspace:*",
|
|
53
|
+
"@types/node": "^22.15.21",
|
|
54
|
+
"typescript": "^5.8.3"
|
|
55
|
+
}
|
|
56
|
+
}
|