yehle 0.0.8
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/LICENSE +21 -0
- package/README.md +183 -0
- package/bin/cli.js +8 -0
- package/dist/cli/animated-intro.js +135 -0
- package/dist/cli/logger.js +48 -0
- package/dist/cli/prompts.js +95 -0
- package/dist/cli/tasks.js +68 -0
- package/dist/core/constants.js +5 -0
- package/dist/core/fs.js +185 -0
- package/dist/core/git.js +90 -0
- package/dist/core/pkg-manager.js +65 -0
- package/dist/core/shell.js +105 -0
- package/dist/core/template-registry.js +229 -0
- package/dist/core/utils.js +57 -0
- package/dist/index.js +37 -0
- package/dist/resources/index.js +33 -0
- package/dist/resources/package/command.js +148 -0
- package/dist/resources/package/config.js +159 -0
- package/dist/resources/package/setup.js +106 -0
- package/dist/resources/package/typescript.js +19 -0
- package/package.json +63 -0
package/dist/core/fs.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isDirAsync = isDirAsync;
|
|
7
|
+
exports.ensureDirAsync = ensureDirAsync;
|
|
8
|
+
exports.writeFileAsync = writeFileAsync;
|
|
9
|
+
exports.copyFileSafeAsync = copyFileSafeAsync;
|
|
10
|
+
exports.copyDirSafeAsync = copyDirSafeAsync;
|
|
11
|
+
exports.removeMatchingFilesRecursively = removeMatchingFilesRecursively;
|
|
12
|
+
exports.removeFilesByBasename = removeFilesByBasename;
|
|
13
|
+
exports.renderMustacheTemplates = renderMustacheTemplates;
|
|
14
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const mustache_1 = __importDefault(require("mustache"));
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a path exists and is a directory.
|
|
19
|
+
* @param dirPath - Directory path to check.
|
|
20
|
+
* @returns True if the path exists and is a directory, false otherwise.
|
|
21
|
+
*/
|
|
22
|
+
async function isDirAsync(dirPath) {
|
|
23
|
+
try {
|
|
24
|
+
const st = await node_fs_1.default.promises.stat(dirPath);
|
|
25
|
+
return st.isDirectory();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Ensure a directory exists (mkdir -p).
|
|
33
|
+
* @param dirPath - Directory to create if missing.
|
|
34
|
+
*/
|
|
35
|
+
async function ensureDirAsync(dirPath) {
|
|
36
|
+
await node_fs_1.default.promises.mkdir(dirPath, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write data to a file, ensuring parent directories exist.
|
|
40
|
+
* @param filePath - Absolute or relative path to the file.
|
|
41
|
+
* @param data - File contents.
|
|
42
|
+
*/
|
|
43
|
+
async function writeFileAsync(filePath, data) {
|
|
44
|
+
const dir = node_path_1.default.dirname(filePath);
|
|
45
|
+
await ensureDirAsync(dir);
|
|
46
|
+
await node_fs_1.default.promises.writeFile(filePath, data, "utf8");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Copy a file if it exists, ensuring destination directory exists.
|
|
50
|
+
* No-ops when source is missing or is not a regular file.
|
|
51
|
+
* @param src - Source file path.
|
|
52
|
+
* @param dest - Destination file path.
|
|
53
|
+
*/
|
|
54
|
+
async function copyFileSafeAsync(src, dest) {
|
|
55
|
+
try {
|
|
56
|
+
const stat = await node_fs_1.default.promises.stat(src);
|
|
57
|
+
if (!stat.isFile())
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await ensureDirAsync(node_path_1.default.dirname(dest));
|
|
64
|
+
await node_fs_1.default.promises.copyFile(src, dest);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recursively copy a directory tree. If the source directory does not exist, it no-ops.
|
|
68
|
+
* @param srcDir - Source directory path.
|
|
69
|
+
* @param destDir - Destination directory path.
|
|
70
|
+
*/
|
|
71
|
+
async function copyDirSafeAsync(srcDir, destDir) {
|
|
72
|
+
try {
|
|
73
|
+
const st = await node_fs_1.default.promises.stat(srcDir);
|
|
74
|
+
if (!st.isDirectory())
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await ensureDirAsync(destDir);
|
|
81
|
+
const entries = await node_fs_1.default.promises.readdir(srcDir, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const srcPath = node_path_1.default.join(srcDir, entry.name);
|
|
84
|
+
const destPath = node_path_1.default.join(destDir, entry.name);
|
|
85
|
+
if (entry.isDirectory())
|
|
86
|
+
await copyDirSafeAsync(srcPath, destPath);
|
|
87
|
+
else if (entry.isFile())
|
|
88
|
+
await copyFileSafeAsync(srcPath, destPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Recursively remove files or directories in a directory tree that match a predicate.
|
|
93
|
+
* The predicate is called with: (basename, fullPath, dirent).
|
|
94
|
+
* Directories that match are deleted (recursively); non-matching directories are traversed.
|
|
95
|
+
* @param rootDir - Root directory to traverse.
|
|
96
|
+
* @param predicate - Function returning true when the entry should be removed.
|
|
97
|
+
*/
|
|
98
|
+
async function removeMatchingFilesRecursively(rootDir, predicate) {
|
|
99
|
+
let entries = [];
|
|
100
|
+
try {
|
|
101
|
+
entries = await node_fs_1.default.promises.readdir(rootDir, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const full = node_path_1.default.join(rootDir, entry.name);
|
|
108
|
+
// If the directory itself matches, remove it entirely; otherwise traverse it.
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
if (predicate(entry.name, full, entry)) {
|
|
111
|
+
await node_fs_1.default.promises.rm(full, { recursive: true, force: true });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
await removeMatchingFilesRecursively(full, predicate);
|
|
115
|
+
}
|
|
116
|
+
// If the file matches, remove the file
|
|
117
|
+
else if (entry.isFile()) {
|
|
118
|
+
if (predicate(entry.name, full, entry)) {
|
|
119
|
+
await node_fs_1.default.promises.rm(full, { force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Convenience wrapper to remove any files or directories whose basename is in the provided list,
|
|
126
|
+
* regardless of which subfolder they are in.
|
|
127
|
+
* @param rootDir - Root directory to traverse.
|
|
128
|
+
* @param fileNames - Iterable of basenames to remove.
|
|
129
|
+
*/
|
|
130
|
+
async function removeFilesByBasename(rootDir, fileNames) {
|
|
131
|
+
const set = new Set(fileNames);
|
|
132
|
+
await removeMatchingFilesRecursively(rootDir, (name) => set.has(name));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Recursively find all *.mustache.* files in targetDir, render them using the provided data,
|
|
136
|
+
* write the rendered content to the same path with ".mustache." removed, and remove the original.
|
|
137
|
+
* Example: package.mustache.json -> package.json, config.mustache.ts -> config.ts
|
|
138
|
+
* @param targetDir - Root directory to search.
|
|
139
|
+
* @param data - Key/value pairs used for mustache interpolation.
|
|
140
|
+
*/
|
|
141
|
+
async function renderMustacheTemplates(targetDir, data) {
|
|
142
|
+
let entries = [];
|
|
143
|
+
try {
|
|
144
|
+
entries = await node_fs_1.default.promises.readdir(targetDir, { withFileTypes: true });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const full = node_path_1.default.join(targetDir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
await renderMustacheTemplates(full, data);
|
|
153
|
+
}
|
|
154
|
+
else if (entry.isFile() && /\.mustache\./i.test(entry.name)) {
|
|
155
|
+
const raw = await node_fs_1.default.promises.readFile(full, "utf8");
|
|
156
|
+
// Preserve GitHub Actions expressions like ${{ secrets.X }} by masking them before rendering.
|
|
157
|
+
const ghExprPattern = /\$\{\{[\s\S]*?\}\}/g;
|
|
158
|
+
const ghExprs = [];
|
|
159
|
+
const masked = raw.replaceAll(ghExprPattern, (m) => {
|
|
160
|
+
const token = `__GH_EXPR_${ghExprs.length}__`;
|
|
161
|
+
ghExprs.push(m);
|
|
162
|
+
return token;
|
|
163
|
+
});
|
|
164
|
+
const previousEscape = mustache_1.default.escape;
|
|
165
|
+
try {
|
|
166
|
+
// Disable HTML escaping to preserve literal "/" and other characters during render
|
|
167
|
+
// This is safe because the rendered content is written to files, not directly to HTML output
|
|
168
|
+
mustache_1.default.escape = (s) => s;
|
|
169
|
+
let rendered = mustache_1.default.render(masked, data);
|
|
170
|
+
// Restore masked GitHub Actions expressions
|
|
171
|
+
ghExprs.forEach((expr, i) => {
|
|
172
|
+
const token = `__GH_EXPR_${i}__`;
|
|
173
|
+
rendered = rendered.split(token).join(expr);
|
|
174
|
+
});
|
|
175
|
+
const dest = node_path_1.default.join(node_path_1.default.dirname(full), entry.name.replace(/\.mustache\./i, "."));
|
|
176
|
+
await node_fs_1.default.promises.writeFile(dest, rendered, "utf8");
|
|
177
|
+
await node_fs_1.default.promises.rm(full, { force: true });
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
// Restore original Mustache escape behavior
|
|
181
|
+
mustache_1.default.escape = previousEscape;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getGitUsername = getGitUsername;
|
|
7
|
+
exports.getGitEmail = getGitEmail;
|
|
8
|
+
exports.isGitRepo = isGitRepo;
|
|
9
|
+
exports.initGitRepo = initGitRepo;
|
|
10
|
+
exports.makeInitialCommit = makeInitialCommit;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const shell_1 = require("./shell");
|
|
14
|
+
/**
|
|
15
|
+
* Read a git config value for a given key from the current environment.
|
|
16
|
+
* This will look up git configuration (local/global/system) based on how git resolves it.
|
|
17
|
+
* @param key - Fully qualified git config key (e.g., "user.name", "user.email")
|
|
18
|
+
* @returns The config value if set; undefined otherwise
|
|
19
|
+
*/
|
|
20
|
+
async function readGitConfig(key) {
|
|
21
|
+
try {
|
|
22
|
+
const out = await (0, shell_1.runAsync)(`git config --get ${key}`, { stdio: "pipe" });
|
|
23
|
+
const s = out.trim();
|
|
24
|
+
return s.length ? s : undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get git user.name from current git configuration.
|
|
32
|
+
* @returns The configured git user.name, or undefined if not available.
|
|
33
|
+
*/
|
|
34
|
+
async function getGitUsername() {
|
|
35
|
+
return await readGitConfig("user.name");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get git user.email from current git configuration.
|
|
39
|
+
* @returns The configured git user.email, or undefined if not available.
|
|
40
|
+
*/
|
|
41
|
+
async function getGitEmail() {
|
|
42
|
+
return await readGitConfig("user.email");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Determine whether a directory already contains a git repository.
|
|
46
|
+
* @param cwd - Absolute path to the target directory
|
|
47
|
+
* @returns true if a .git directory exists inside cwd; false otherwise
|
|
48
|
+
*/
|
|
49
|
+
function isGitRepo(cwd) {
|
|
50
|
+
return node_fs_1.default.existsSync(node_path_1.default.join(cwd, ".git"));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Initialize a git repository in the provided directory.
|
|
54
|
+
* @param cwd - Absolute path to the target directory where git should be initialized
|
|
55
|
+
* @throws Error when initialization fails due to underlying git issues
|
|
56
|
+
*/
|
|
57
|
+
async function initGitRepo(cwd) {
|
|
58
|
+
if (isGitRepo(cwd))
|
|
59
|
+
return;
|
|
60
|
+
const defaultBranch = "main";
|
|
61
|
+
try {
|
|
62
|
+
await (0, shell_1.runAsync)(`git init -b ${defaultBranch}`, { cwd, stdio: "ignore" });
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
66
|
+
throw new Error(`Failed to initialize git repository in ${cwd}: ${msg}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Initialize a git repository in the provided directory.
|
|
71
|
+
* @param cwd - Absolute path to the target directory where git should be initialized
|
|
72
|
+
* @throws Error when initialization fails due to underlying git issues
|
|
73
|
+
*/
|
|
74
|
+
async function makeInitialCommit(cwd) {
|
|
75
|
+
try {
|
|
76
|
+
// Ensure a repository exists
|
|
77
|
+
if (!isGitRepo(cwd))
|
|
78
|
+
await initGitRepo(cwd);
|
|
79
|
+
// Stage all files and create an initial commit
|
|
80
|
+
await (0, shell_1.runAsync)("git add -A", { cwd, stdio: "ignore" });
|
|
81
|
+
await (0, shell_1.runAsync)('git commit -m "chore: initial commit"', {
|
|
82
|
+
cwd,
|
|
83
|
+
stdio: "ignore",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
88
|
+
throw new Error(`Failed to create initial git commit in ${cwd}: ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LANGUAGE_PACKAGE_REGISTRY = exports.LANGUAGE_PACKAGE_MANAGER = void 0;
|
|
4
|
+
exports.validatePackageName = validatePackageName;
|
|
5
|
+
exports.ensurePackageManager = ensurePackageManager;
|
|
6
|
+
exports.getInstallScript = getInstallScript;
|
|
7
|
+
const config_1 = require("../resources/package/config");
|
|
8
|
+
const typescript_1 = require("../resources/package/typescript");
|
|
9
|
+
const shell_1 = require("./shell");
|
|
10
|
+
/** Maps each supported language to its default package manager. */
|
|
11
|
+
exports.LANGUAGE_PACKAGE_MANAGER = {
|
|
12
|
+
[config_1.Language.TYPESCRIPT]: "pnpm",
|
|
13
|
+
};
|
|
14
|
+
/** Maps each supported language to its package registry. */
|
|
15
|
+
exports.LANGUAGE_PACKAGE_REGISTRY = {
|
|
16
|
+
[config_1.Language.TYPESCRIPT]: "NPM",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Validates the given package name based on the language type.
|
|
20
|
+
* @param name - The name of the package to validate.
|
|
21
|
+
* @param language - The programming language context to validate against.
|
|
22
|
+
* @throws Will throw an error if the package name is invalid for TypeScript.
|
|
23
|
+
*/
|
|
24
|
+
function validatePackageName(name, language) {
|
|
25
|
+
switch (language) {
|
|
26
|
+
case config_1.Language.TYPESCRIPT: {
|
|
27
|
+
const validation = (0, typescript_1.validateTypescriptPackageName)(name);
|
|
28
|
+
if (validation !== true)
|
|
29
|
+
throw new Error(typeof validation === "string" ? validation : "Invalid package name");
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`Unsupported language: ${language}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Ensure the selected package manager is available on the system
|
|
38
|
+
* and return a packageManager identifier string (e.g., "bun@1.2.0")
|
|
39
|
+
* @param pm - The package manager to verify/resolve.
|
|
40
|
+
*/
|
|
41
|
+
async function ensurePackageManager(pm) {
|
|
42
|
+
switch (pm) {
|
|
43
|
+
case "pnpm": {
|
|
44
|
+
if (!(await (0, shell_1.commandExistsAsync)("pnpm")))
|
|
45
|
+
throw new Error("pnpm is not installed. Please install PNPM and re-run.");
|
|
46
|
+
const version = await (0, shell_1.runAsync)("pnpm --version");
|
|
47
|
+
return `pnpm@${version}`;
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unsupported package manager: ${pm}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the install command for the specified package manager to display in the next-steps box.
|
|
55
|
+
* @param pm - The package manager to use.
|
|
56
|
+
* @returns The shell command to install dependencies (e.g., "pnpm install").
|
|
57
|
+
*/
|
|
58
|
+
function getInstallScript(pm) {
|
|
59
|
+
switch (pm) {
|
|
60
|
+
case "pnpm":
|
|
61
|
+
return "pnpm install";
|
|
62
|
+
default:
|
|
63
|
+
throw new Error(`Unsupported package manager: ${pm}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAsync = runAsync;
|
|
4
|
+
exports.commandExistsAsync = commandExistsAsync;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
/**
|
|
7
|
+
* Parses a command string into a command name and an array of arguments.
|
|
8
|
+
* Supports quoted arguments and spaces within arguments.
|
|
9
|
+
* @param cmd - The command string to parse.
|
|
10
|
+
* @returns An object containing the command name and an array of arguments.
|
|
11
|
+
*/
|
|
12
|
+
function parseCommand(cmd) {
|
|
13
|
+
const tokens = [];
|
|
14
|
+
let current = "";
|
|
15
|
+
let inQuotes = false;
|
|
16
|
+
for (const char of cmd) {
|
|
17
|
+
if (char === '"' && !inQuotes) {
|
|
18
|
+
inQuotes = true;
|
|
19
|
+
}
|
|
20
|
+
else if (char === '"' && inQuotes) {
|
|
21
|
+
inQuotes = false;
|
|
22
|
+
}
|
|
23
|
+
else if (char === " " && !inQuotes) {
|
|
24
|
+
if (current) {
|
|
25
|
+
tokens.push(current);
|
|
26
|
+
current = "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
current += char;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (current)
|
|
34
|
+
tokens.push(current);
|
|
35
|
+
const [command, ...args] = tokens;
|
|
36
|
+
return { command, args };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run a command asynchronously and resolve with stdout (trimmed) for "pipe" stdio, or an empty string for "inherit" or "ignore" stdio.
|
|
40
|
+
* The command string is parsed into command and arguments to avoid shell interpretation.
|
|
41
|
+
* @param cmd - The command string to run (will be parsed into command and args).
|
|
42
|
+
* @param opts - Optional run options to customize execution.
|
|
43
|
+
* @returns Promise resolving to the trimmed stdout for "pipe" stdio, or empty string for others.
|
|
44
|
+
*/
|
|
45
|
+
function runAsync(cmd, opts = {}) {
|
|
46
|
+
const { cwd, stdio = "pipe", env, timeoutMs } = opts;
|
|
47
|
+
const { command, args } = parseCommand(cmd);
|
|
48
|
+
if (stdio === "inherit") {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
51
|
+
cwd,
|
|
52
|
+
env: { ...process.env, ...env },
|
|
53
|
+
stdio: "inherit",
|
|
54
|
+
timeout: timeoutMs,
|
|
55
|
+
});
|
|
56
|
+
child.on("error", reject);
|
|
57
|
+
child.on("close", (code) => {
|
|
58
|
+
if (code === 0)
|
|
59
|
+
resolve("");
|
|
60
|
+
else
|
|
61
|
+
reject(new Error(`Command failed: ${cmd} (exit ${code})`));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Default: capture stdout using spawn
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
68
|
+
cwd,
|
|
69
|
+
env: { ...process.env, ...env },
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
+
timeout: timeoutMs,
|
|
72
|
+
});
|
|
73
|
+
let stdout = "";
|
|
74
|
+
child.stdout.on("data", (data) => {
|
|
75
|
+
stdout += data;
|
|
76
|
+
});
|
|
77
|
+
child.on("error", reject);
|
|
78
|
+
child.on("close", (code) => {
|
|
79
|
+
if (code === 0)
|
|
80
|
+
resolve(stdout.trim());
|
|
81
|
+
else
|
|
82
|
+
reject(new Error(`Command failed: ${cmd} (exit ${code})`));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Checks asynchronously if a command exists on the system's PATH.
|
|
88
|
+
* Uses 'where' on Windows and 'command -v' on other platforms.
|
|
89
|
+
* @param command - The command name to check.
|
|
90
|
+
* @returns Promise resolving to true if the command exists, false otherwise.
|
|
91
|
+
*/
|
|
92
|
+
async function commandExistsAsync(command) {
|
|
93
|
+
try {
|
|
94
|
+
if (process.platform === "win32") {
|
|
95
|
+
await runAsync(`where ${command}`, { stdio: "ignore" });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await runAsync(`command -v ${command}`, { stdio: "ignore" });
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveTemplatesDir = resolveTemplatesDir;
|
|
7
|
+
exports.listAvailableTemplates = listAvailableTemplates;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const giget_1 = require("giget");
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
const fs_1 = require("./fs");
|
|
14
|
+
/** Name of the shared templates directory that may be filtered out from listings. */
|
|
15
|
+
const SHARED_DIR_NAME = "shared";
|
|
16
|
+
/** Default GitHub owner to fetch templates from in remote mode.*/
|
|
17
|
+
const DEFAULT_GITHUB_OWNER = "agrawal-rohit";
|
|
18
|
+
/** Default GitHub repository to fetch templates from in remote mode. */
|
|
19
|
+
const DEFAULT_GITHUB_REPO = "yehle";
|
|
20
|
+
/** HTTP headers used when communicating with the GitHub API. */
|
|
21
|
+
const GITHUB_HEADERS = {
|
|
22
|
+
"User-Agent": "yehle-cli",
|
|
23
|
+
Accept: "application/vnd.github.v3+json",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the absolute path to the local templates root directory.
|
|
27
|
+
* @returns The absolute path if the directory exists at `./templates`; otherwise null.
|
|
28
|
+
*/
|
|
29
|
+
async function getLocalTemplatesRoot() {
|
|
30
|
+
const localTemplatesPath = node_path_1.default.resolve(process.cwd(), "templates");
|
|
31
|
+
if (await (0, fs_1.isDirAsync)(localTemplatesPath))
|
|
32
|
+
return localTemplatesPath;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolves the absolute path to the local templates subdirectory for a given language and resource.
|
|
37
|
+
* @param language - The programming language for the templates.
|
|
38
|
+
* @param resource - Optional resource within the language.
|
|
39
|
+
* @returns The path to the language subdirectory or resource directory if it exists; otherwise null.
|
|
40
|
+
*/
|
|
41
|
+
async function getLocalTemplatesSubdir(language, resource) {
|
|
42
|
+
const root = await getLocalTemplatesRoot();
|
|
43
|
+
if (!root)
|
|
44
|
+
return null;
|
|
45
|
+
const langRoot = node_path_1.default.join(root, language);
|
|
46
|
+
if (!(await (0, fs_1.isDirAsync)(langRoot)))
|
|
47
|
+
return null;
|
|
48
|
+
if (resource) {
|
|
49
|
+
const resourceDir = node_path_1.default.join(langRoot, resource);
|
|
50
|
+
if (await (0, fs_1.isDirAsync)(resourceDir))
|
|
51
|
+
return resourceDir;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return langRoot;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @param dir The directory to list child directories from.
|
|
58
|
+
* @returns An array of child directory names, excluding shared if not included.
|
|
59
|
+
*/
|
|
60
|
+
async function listChildDirs(dir) {
|
|
61
|
+
if (!(await (0, fs_1.isDirAsync)(dir)))
|
|
62
|
+
return [];
|
|
63
|
+
const entries = await node_fs_1.default.promises.readdir(dir, { withFileTypes: true });
|
|
64
|
+
const names = entries
|
|
65
|
+
.filter((e) => e.isDirectory())
|
|
66
|
+
.map((e) => e.name)
|
|
67
|
+
.filter((n) => n.toLowerCase() !== SHARED_DIR_NAME);
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a GitHub API Contents URL for a templates subtree.
|
|
72
|
+
* @param language - The programming language for the templates.
|
|
73
|
+
* @param resource - Optional resource within the language.
|
|
74
|
+
* @returns The constructed API URL.
|
|
75
|
+
*/
|
|
76
|
+
function buildContentsURL(language, resource) {
|
|
77
|
+
const subpath = ["templates", language, resource].filter(Boolean).join("/");
|
|
78
|
+
return `https://api.github.com/repos/${DEFAULT_GITHUB_OWNER}/${DEFAULT_GITHUB_REPO}/contents/${subpath}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a giget specification for a subtree within the default repo.
|
|
82
|
+
* @param language - The programming language for the templates.
|
|
83
|
+
* @param resource - Optional resource within the language.
|
|
84
|
+
* @returns The constructed giget spec string.
|
|
85
|
+
*/
|
|
86
|
+
function buildGigetSpec(language, resource) {
|
|
87
|
+
const subpath = ["templates", language, resource].filter(Boolean).join("/");
|
|
88
|
+
return `github:${DEFAULT_GITHUB_OWNER}/${DEFAULT_GITHUB_REPO}/${subpath}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check whether a remote templates subtree exists in GitHub.
|
|
92
|
+
* @param language - The programming language for the templates.
|
|
93
|
+
* @param resource - Optional resource within the language.
|
|
94
|
+
* @returns True if the subtree exists or is uncertain, false if definitely not.
|
|
95
|
+
*/
|
|
96
|
+
async function subtreeExistsRemote(language, resource) {
|
|
97
|
+
try {
|
|
98
|
+
const url = buildContentsURL(language, resource);
|
|
99
|
+
const res = await fetch(url, { headers: GITHUB_HEADERS });
|
|
100
|
+
if (res.status === 404)
|
|
101
|
+
return false; // definitely not there
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
return true; // uncertain; assume exists and let giget verify
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
if (Array.isArray(data))
|
|
106
|
+
return true;
|
|
107
|
+
if (data &&
|
|
108
|
+
typeof data === "object" &&
|
|
109
|
+
data.type === "dir")
|
|
110
|
+
return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Attempt to normalize the downloaded directory to the expected subtree.
|
|
119
|
+
* @param downloadedDir - The path to the downloaded directory.
|
|
120
|
+
* @param language - The programming language for the templates.
|
|
121
|
+
* @param resource - Optional resource within the language.
|
|
122
|
+
* @returns The normalized directory path.
|
|
123
|
+
*/
|
|
124
|
+
async function normalizeDownloadedDir(downloadedDir, language, resource) {
|
|
125
|
+
const candidates = resource
|
|
126
|
+
? [
|
|
127
|
+
node_path_1.default.join(downloadedDir, "templates", language, resource),
|
|
128
|
+
node_path_1.default.join(downloadedDir, language, resource),
|
|
129
|
+
node_path_1.default.join(downloadedDir, resource),
|
|
130
|
+
downloadedDir,
|
|
131
|
+
]
|
|
132
|
+
: [
|
|
133
|
+
node_path_1.default.join(downloadedDir, "templates", language),
|
|
134
|
+
node_path_1.default.join(downloadedDir, language),
|
|
135
|
+
downloadedDir,
|
|
136
|
+
];
|
|
137
|
+
for (const cand of candidates) {
|
|
138
|
+
if (await (0, fs_1.isDirAsync)(cand))
|
|
139
|
+
return cand;
|
|
140
|
+
}
|
|
141
|
+
return downloadedDir;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Download a remote templates subtree to a temporary directory and return the normalized path.
|
|
145
|
+
* @param language - The programming language for the templates.
|
|
146
|
+
* @param resource - Optional resource within the language.
|
|
147
|
+
* @returns The path to the downloaded and normalized directory.
|
|
148
|
+
*/
|
|
149
|
+
async function downloadRemoteTemplatesSubdir(language, resource) {
|
|
150
|
+
const spec = buildGigetSpec(language, resource);
|
|
151
|
+
const promise = (async () => {
|
|
152
|
+
const exists = await subtreeExistsRemote(language, resource);
|
|
153
|
+
if (!exists) {
|
|
154
|
+
const resourcePart = resource ? `/${resource}` : "";
|
|
155
|
+
throw new Error(`Remote templates path does not exist: templates/${language}${resourcePart} (repo: ${DEFAULT_GITHUB_OWNER}/${DEFAULT_GITHUB_REPO}).`);
|
|
156
|
+
}
|
|
157
|
+
const tmpRoot = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "yehle-templates-"));
|
|
158
|
+
try {
|
|
159
|
+
const res = await (0, giget_1.downloadTemplate)(spec, { dir: tmpRoot, force: true });
|
|
160
|
+
const normalized = await normalizeDownloadedDir(res.dir, language, resource);
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
165
|
+
throw new Error(`Failed to download templates from "${spec}". ${msg}. ` +
|
|
166
|
+
`Ensure the path exists and that network/GitHub access are available.`);
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
return promise;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* List child directories from the GitHub Contents API without downloading the subtree.
|
|
173
|
+
* @param language - The programming language for the templates.
|
|
174
|
+
* @param resource - Optional resource within the language.
|
|
175
|
+
* @returns An array of child directory names.
|
|
176
|
+
*/
|
|
177
|
+
async function listRemoteChildDirsViaAPI(language, resource) {
|
|
178
|
+
const url = buildContentsURL(language, resource);
|
|
179
|
+
const res = await fetch(url, { headers: GITHUB_HEADERS });
|
|
180
|
+
if (!res.ok)
|
|
181
|
+
throw new Error(`Failed to fetch from GitHub API: ${res.status} ${res.statusText}`);
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
if (!Array.isArray(data))
|
|
184
|
+
throw new Error("Invalid response from GitHub API: expected array of contents");
|
|
185
|
+
const names = data
|
|
186
|
+
.filter((entry) => entry?.type === "dir" && typeof entry.name === "string")
|
|
187
|
+
.map((entry) => entry.name)
|
|
188
|
+
.filter((n) => n.toLowerCase() !== SHARED_DIR_NAME);
|
|
189
|
+
return names;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the on-disk directory that contains templates for a given language and resource.
|
|
193
|
+
* @param language - The programming language for the templates.
|
|
194
|
+
* @param resource - Optional resource within the language.
|
|
195
|
+
* @returns An object with the path and source of the templates directory.
|
|
196
|
+
*/
|
|
197
|
+
async function resolveTemplatesDir(language, resource) {
|
|
198
|
+
if (constants_1.IS_LOCAL_MODE) {
|
|
199
|
+
const localDir = await getLocalTemplatesSubdir(language, resource);
|
|
200
|
+
if (localDir)
|
|
201
|
+
return localDir;
|
|
202
|
+
const root = (await getLocalTemplatesRoot()) || "<no local templates root>";
|
|
203
|
+
const resourcePart = resource ? ` and resource "${resource}"` : "";
|
|
204
|
+
throw new Error(`Local templates not found at ${root} for language "${language}"${resourcePart}.`);
|
|
205
|
+
}
|
|
206
|
+
// Remote mode
|
|
207
|
+
const remoteDir = await downloadRemoteTemplatesSubdir(language, resource);
|
|
208
|
+
if (await (0, fs_1.isDirAsync)(remoteDir))
|
|
209
|
+
return remoteDir;
|
|
210
|
+
const resourcePart = resource ? ` and resource "${resource}"` : "";
|
|
211
|
+
throw new Error(`No remote templates found for language "${language}"${resourcePart} in ${DEFAULT_GITHUB_OWNER}/${DEFAULT_GITHUB_REPO}.`);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* List available template names (subdirectories) for a given language and resource.
|
|
215
|
+
* @param language - The programming language for the templates.
|
|
216
|
+
* @param resource - The resource type
|
|
217
|
+
* @returns An array of available template names.
|
|
218
|
+
*/
|
|
219
|
+
async function listAvailableTemplates(language, resource) {
|
|
220
|
+
if (constants_1.IS_LOCAL_MODE) {
|
|
221
|
+
const resourceDir = await getLocalTemplatesSubdir(language, resource);
|
|
222
|
+
if (!resourceDir)
|
|
223
|
+
return [];
|
|
224
|
+
return listChildDirs(resourceDir);
|
|
225
|
+
}
|
|
226
|
+
// Prefer API listing
|
|
227
|
+
const apiNames = await listRemoteChildDirsViaAPI(language, resource);
|
|
228
|
+
return apiNames;
|
|
229
|
+
}
|