@navneet_25/tempjs 1.0.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/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # Website Templates
2
+
3
+ A single repository containing multiple website project templates, plus a small CLI that instantiates any template directly into your current directory.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ # Install the CLI globally (after publishing or linking locally)
9
+ npm install -g tempjs
10
+
11
+ # Create a new project
12
+ mkdir hotel-client
13
+ cd hotel-client
14
+ tempjs hotel
15
+ git init
16
+ git add .
17
+ git commit -m "Initial project"
18
+ ```
19
+
20
+ ## Available templates
21
+
22
+ | ID | Name | Description |
23
+ |---------------|----------------------|--------------------------------------------------|
24
+ | `hotel` | Hotel Website | Hotel and resort site with admin, gallery, booking |
25
+ | `real-estate` | Real Estate Website | Property listings with admin panel |
26
+
27
+ List templates from the CLI:
28
+
29
+ ```bash
30
+ tempjs list
31
+ ```
32
+
33
+ ## CLI usage
34
+
35
+ ```bash
36
+ tempjs list # show available templates
37
+ tempjs hotel # create project from hotel template
38
+ tempjs real-estate --force # overwrite existing files
39
+ tempjs --help # show help
40
+ ```
41
+
42
+ ### Options
43
+
44
+ | Option | Description |
45
+ |---------------|-------------|
46
+ | `--force` | Overwrite files in the current directory without prompting |
47
+ | `--remote` | Fetch from GitHub even when a local template copy exists |
48
+ | `--init-git` | Run `git init` after copying (optional) |
49
+ | `--help` | Show help |
50
+
51
+ The CLI copies template **contents** into the current directory — not a nested folder:
52
+
53
+ ```
54
+ hotel-client/
55
+ ├── .gitignore
56
+ ├── package.json
57
+ ├── app/
58
+ ├── public/
59
+ └── ...
60
+ ```
61
+
62
+ Generated projects are fully independent. No submodule, worktree, or connection to this repository.
63
+
64
+ ## Local development (without publishing)
65
+
66
+ From this repository root:
67
+
68
+ ```bash
69
+ npm link
70
+
71
+ mkdir /tmp/hotel-client
72
+ cd /tmp/hotel-client
73
+ tempjs hotel
74
+ ```
75
+
76
+ When templates exist locally under `templates/`, the CLI uses them directly (fast, no network). Use `--remote` to test GitHub fetching.
77
+
78
+ ## Installation
79
+
80
+ ### Global install (npm)
81
+
82
+ ```bash
83
+ npm install -g tempjs
84
+ ```
85
+
86
+ ### From this repository
87
+
88
+ ```bash
89
+ git clone https://github.com/<username>/templates.git
90
+ cd templates
91
+ npm link
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ All template mappings live in **`templates.json`** at the repository root:
97
+
98
+ ```json
99
+ {
100
+ "repository": {
101
+ "owner": "your-username",
102
+ "repo": "templates",
103
+ "branch": "main",
104
+ "templatesPath": "templates"
105
+ },
106
+ "templates": {
107
+ "hotel": {
108
+ "directory": "hotel-website-template",
109
+ "name": "Hotel Website",
110
+ "description": "Modern hotel and resort website template"
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### GitHub repository URL
117
+
118
+ Configure in one place — `templates.json` or environment variables:
119
+
120
+ | Variable | Description |
121
+ |------------------------|-------------|
122
+ | `TEMPLATES_REPO_URL` | Full URL or `owner/repo` |
123
+ | `TEMPLATES_REPO_OWNER` | GitHub username or org |
124
+ | `TEMPLATES_REPO_REPO` | Repository name |
125
+ | `TEMPLATES_REPO_BRANCH`| Branch (default: `main`) |
126
+ | `GITHUB_TOKEN` | Optional token for higher API rate limits |
127
+
128
+ Example:
129
+
130
+ ```bash
131
+ export TEMPLATES_REPO_URL="https://github.com/myuser/templates"
132
+ tempjs hotel --remote
133
+ ```
134
+
135
+ ## How fetching works
136
+
137
+ `tempjs hotel` does **not** clone the entire repository.
138
+
139
+ 1. The CLI reads the GitHub API tree for the configured branch.
140
+ 2. It filters files under `templates/hotel-website-template/`.
141
+ 3. Only those files are downloaded (via blob API or raw content).
142
+ 4. Files are validated in a temporary directory, then copied to your project.
143
+
144
+ Other templates are not transferred.
145
+
146
+ ## Repository structure
147
+
148
+ ```
149
+ templates/ # this repository root
150
+ ├── README.md
151
+ ├── package.json # CLI package (bin: tempjs)
152
+ ├── templates.json # manifest + repo config
153
+ ├── .gitignore # for THIS repo only (not copied to projects)
154
+ ├── cli/
155
+ │ ├── index.js
156
+ │ ├── config.js
157
+ │ ├── copy.js
158
+ │ └── fetch.js
159
+ └── templates/
160
+ ├── hotel-website-template/
161
+ │ ├── .gitignore # copied to generated projects
162
+ │ ├── package.json
163
+ │ ├── app/
164
+ │ └── ...
165
+ └── real-estate-website-template/
166
+ └── ...
167
+ ```
168
+
169
+ ## Adding a new template
170
+
171
+ 1. Add a directory under `templates/`, e.g. `templates/restaurant-website-template/`.
172
+ 2. Include a template-specific `.gitignore` for that stack (Next.js, Vite, etc.).
173
+ 3. Add one entry to `templates.json`:
174
+
175
+ ```json
176
+ "restaurant": {
177
+ "directory": "restaurant-website-template",
178
+ "name": "Restaurant Website",
179
+ "description": "Restaurant website with menu and reservations"
180
+ }
181
+ ```
182
+
183
+ 4. Commit and push. No CLI source changes required.
184
+
185
+ ## Gitignore rules
186
+
187
+ ### Template repository (this repo)
188
+
189
+ The root `.gitignore` keeps the templates repository clean:
190
+
191
+ - `node_modules/`, `.next/`, `dist/`, build caches, logs
192
+ - `.env` files (but not `.env.example`)
193
+ - OS and IDE junk
194
+ - Nested `.git/` directories inside templates
195
+
196
+ ### Generated projects
197
+
198
+ Each template has its own `.gitignore` (e.g. Next.js ignores). That file is copied into the generated project. The **root** repository `.gitignore` is never copied.
199
+
200
+ ## Safety
201
+
202
+ - Non-empty directories: warns and prompts before overwriting (use `--force` to skip).
203
+ - Never copies `.git` from templates.
204
+ - Warns if the target directory is already a Git repository.
205
+ - Does not run `npm install`, builds, or other scripts automatically.
206
+ - Downloads to a temp directory first; cleans up on failure.
207
+
208
+ ## Tech stack (current templates)
209
+
210
+ Both current templates use:
211
+
212
+ - **Next.js 16** (App Router)
213
+ - **React 19**
214
+ - **TypeScript**
215
+ - **Tailwind CSS 4**
216
+ - **Prisma** + MariaDB
217
+ - **NextAuth**
218
+
219
+ After generating a project:
220
+
221
+ ```bash
222
+ pnpm install
223
+ cp .env.example .env # configure database credentials
224
+ pnpm dev
225
+ ```
226
+
227
+ ## Contributing
228
+
229
+ 1. Fork and clone this repository.
230
+ 2. Make changes inside `templates/<template-name>/`.
231
+ 3. Update `templates.json` if adding a new template.
232
+ 4. Test locally with `npm link` and `tempjs <id>`.
233
+ 5. Open a pull request.
234
+
235
+ ## License
236
+
237
+ MIT
package/cli/config.js ADDED
@@ -0,0 +1,78 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
6
+
7
+ /** @typedef {{ owner: string, repo: string, branch: string, templatesPath: string }} RepositoryConfig */
8
+ /** @typedef {{ directory: string, name: string, description: string }} TemplateEntry */
9
+ /** @typedef {{ repository: RepositoryConfig, templates: Record<string, TemplateEntry> }} Manifest */
10
+
11
+ /**
12
+ * @returns {Manifest}
13
+ */
14
+ export function loadManifest() {
15
+ const manifestPath = join(packageRoot, "templates.json");
16
+ const raw = readFileSync(manifestPath, "utf8");
17
+ return JSON.parse(raw);
18
+ }
19
+
20
+ export function getPackageRoot() {
21
+ return packageRoot;
22
+ }
23
+
24
+ /**
25
+ * Repository settings can be overridden via environment variables.
26
+ * @param {RepositoryConfig} defaults
27
+ * @returns {RepositoryConfig}
28
+ */
29
+ export function resolveRepositoryConfig(defaults) {
30
+ const url = process.env.TEMPLATES_REPO_URL?.trim();
31
+ if (url) {
32
+ const parsed = parseGitHubUrl(url);
33
+ if (parsed) {
34
+ return {
35
+ owner: parsed.owner,
36
+ repo: parsed.repo,
37
+ branch:
38
+ process.env.TEMPLATES_REPO_BRANCH?.trim() || parsed.branch || defaults.branch,
39
+ templatesPath: defaults.templatesPath,
40
+ };
41
+ }
42
+ }
43
+
44
+ const owner = process.env.TEMPLATES_REPO_OWNER?.trim();
45
+ const repo = process.env.TEMPLATES_REPO_REPO?.trim();
46
+ const branch = process.env.TEMPLATES_REPO_BRANCH?.trim();
47
+
48
+ return {
49
+ owner: owner || defaults.owner,
50
+ repo: repo || defaults.repo,
51
+ branch: branch || defaults.branch,
52
+ templatesPath: defaults.templatesPath,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * @param {string} url
58
+ * @returns {{ owner: string, repo: string, branch?: string } | null}
59
+ */
60
+ function parseGitHubUrl(url) {
61
+ const patterns = [
62
+ /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+))?$/,
63
+ /^git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/,
64
+ /^([^/]+)\/([^/]+)$/,
65
+ ];
66
+
67
+ for (const pattern of patterns) {
68
+ const match = url.match(pattern);
69
+ if (match) {
70
+ return {
71
+ owner: match[1],
72
+ repo: match[2].replace(/\.git$/, ""),
73
+ branch: match[3] || undefined,
74
+ };
75
+ }
76
+ }
77
+ return null;
78
+ }
package/cli/copy.js ADDED
@@ -0,0 +1,164 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readlinkSync,
8
+ rmSync,
9
+ statSync,
10
+ } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ const NEVER_COPY = new Set([".git", ".gitignore.bak"]);
14
+ const NEVER_OVERWRITE = new Set([".git"]);
15
+
16
+ /**
17
+ * Files that must never be transferred into a generated project.
18
+ * @param {string} name
19
+ * @returns {boolean}
20
+ */
21
+ function shouldSkipFile(name) {
22
+ if (NEVER_COPY.has(name)) return true;
23
+ if (name === ".env" || name.startsWith(".env.")) {
24
+ return name !== ".env.example";
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /**
30
+ * @param {string} dir
31
+ * @returns {string[]}
32
+ */
33
+ export function listDirectoryEntries(dir) {
34
+ if (!existsSync(dir)) return [];
35
+ return readdirSync(dir).filter((name) => name !== "." && name !== "..");
36
+ }
37
+
38
+ /**
39
+ * @param {string} dir
40
+ * @returns {boolean}
41
+ */
42
+ export function isDirectoryEmpty(dir) {
43
+ return listDirectoryEntries(dir).length === 0;
44
+ }
45
+
46
+ /**
47
+ * @param {string} targetDir
48
+ * @returns {boolean}
49
+ */
50
+ export function targetHasGitRepo(targetDir) {
51
+ return existsSync(join(targetDir, ".git"));
52
+ }
53
+
54
+ /**
55
+ * Files in targetDir that would be overwritten by source copy.
56
+ * @param {string} sourceDir
57
+ * @param {string} targetDir
58
+ * @returns {string[]}
59
+ */
60
+ export function findConflictingPaths(sourceDir, targetDir) {
61
+ const conflicts = [];
62
+ collectConflicts(sourceDir, targetDir, "", conflicts);
63
+ return conflicts.sort();
64
+ }
65
+
66
+ /**
67
+ * @param {string} sourceDir
68
+ * @param {string} targetDir
69
+ * @param {string} relative
70
+ * @param {string[]} conflicts
71
+ */
72
+ function collectConflicts(sourceDir, targetDir, relative, conflicts) {
73
+ const currentSource = join(sourceDir, relative);
74
+ if (!existsSync(currentSource)) return;
75
+
76
+ const sourceNames = readdirSync(currentSource);
77
+ for (const name of sourceNames) {
78
+ if (shouldSkipFile(name)) continue;
79
+
80
+ const relPath = relative ? join(relative, name) : name;
81
+ const sourcePath = join(sourceDir, relPath);
82
+ const targetPath = join(targetDir, relPath);
83
+
84
+ if (existsSync(targetPath)) {
85
+ conflicts.push(relPath);
86
+ }
87
+
88
+ if (statSync(sourcePath).isDirectory()) {
89
+ collectConflicts(sourceDir, targetDir, relPath, conflicts);
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Copy template source into target directory.
96
+ * @param {string} sourceDir
97
+ * @param {string} targetDir
98
+ * @param {{ force?: boolean }} options
99
+ */
100
+ export function copyTemplate(sourceDir, targetDir, options = {}) {
101
+ const force = options.force ?? false;
102
+
103
+ if (!existsSync(sourceDir)) {
104
+ throw new Error(`Template source not found: ${sourceDir}`);
105
+ }
106
+
107
+ if (!force && !isDirectoryEmpty(targetDir)) {
108
+ const conflicts = findConflictingPaths(sourceDir, targetDir);
109
+ if (conflicts.length > 0) {
110
+ const err = new Error("TARGET_NOT_EMPTY");
111
+ err.conflicts = conflicts;
112
+ throw err;
113
+ }
114
+ }
115
+
116
+ copyRecursive(sourceDir, targetDir, "");
117
+ }
118
+
119
+ /**
120
+ * @param {string} sourceRoot
121
+ * @param {string} targetRoot
122
+ * @param {string} relative
123
+ */
124
+ function copyRecursive(sourceRoot, targetRoot, relative) {
125
+ const sourcePath = join(sourceRoot, relative);
126
+ const names = readdirSync(sourcePath);
127
+
128
+ for (const name of names) {
129
+ if (shouldSkipFile(name)) continue;
130
+
131
+ const relPath = relative ? join(relative, name) : name;
132
+ const from = join(sourceRoot, relPath);
133
+ const to = join(targetRoot, relPath);
134
+
135
+ if (NEVER_OVERWRITE.has(name) && existsSync(to)) {
136
+ continue;
137
+ }
138
+
139
+ const stat = lstatSync(from);
140
+ if (stat.isSymbolicLink()) {
141
+ const linkTarget = readlinkSync(from);
142
+ mkdirSync(join(to, ".."), { recursive: true });
143
+ cpSync(from, to, { recursive: true, force: true });
144
+ continue;
145
+ }
146
+
147
+ if (stat.isDirectory()) {
148
+ mkdirSync(to, { recursive: true });
149
+ copyRecursive(sourceRoot, targetRoot, relPath);
150
+ } else {
151
+ mkdirSync(join(to, ".."), { recursive: true });
152
+ cpSync(from, to, { force: true });
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {string} dir
159
+ */
160
+ export function removeDirectory(dir) {
161
+ if (existsSync(dir)) {
162
+ rmSync(dir, { recursive: true, force: true });
163
+ }
164
+ }
package/cli/fetch.js ADDED
@@ -0,0 +1,243 @@
1
+ import { createWriteStream } from "node:fs";
2
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { Readable } from "node:stream";
7
+
8
+ const GITHUB_API = "https://api.github.com";
9
+ const RAW_BASE = "https://raw.githubusercontent.com";
10
+ const LARGE_FILE_BYTES = 1024 * 1024;
11
+
12
+ /**
13
+ * Download a single template directory from GitHub without cloning the full repo.
14
+ * @param {import('./config.js').RepositoryConfig} repo
15
+ * @param {string} templateDirectory
16
+ * @returns {Promise<string>} Path to temp directory containing template files
17
+ */
18
+ export async function fetchTemplateFromGitHub(repo, templateDirectory) {
19
+ const prefix = `${repo.templatesPath}/${templateDirectory}/`;
20
+ const tree = await fetchRepoTree(repo);
21
+
22
+ const blobs = tree
23
+ .filter((entry) => entry.type === "blob" && entry.path.startsWith(prefix))
24
+ .map((entry) => ({
25
+ githubPath: entry.path,
26
+ relPath: entry.path.slice(prefix.length),
27
+ sha: entry.sha,
28
+ }));
29
+
30
+ if (blobs.length === 0) {
31
+ throw new Error(
32
+ `Template directory not found on GitHub: ${repo.templatesPath}/${templateDirectory}`
33
+ );
34
+ }
35
+
36
+ const tempRoot = await mkdtemp(join(tmpdir(), "template-cli-"));
37
+ const templateRoot = join(tempRoot, "template");
38
+ await mkdir(templateRoot, { recursive: true });
39
+
40
+ const concurrency = 8;
41
+ let index = 0;
42
+ const errors = [];
43
+
44
+ async function worker() {
45
+ while (index < blobs.length) {
46
+ const current = index++;
47
+ const blob = blobs[current];
48
+ try {
49
+ await downloadBlob(repo, blob, templateRoot);
50
+ } catch (error) {
51
+ errors.push({ relPath: blob.relPath, error });
52
+ }
53
+ }
54
+ }
55
+
56
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
57
+
58
+ if (errors.length > 0) {
59
+ const detail = errors
60
+ .slice(0, 5)
61
+ .map((e) => `${e.relPath}: ${e.error.message}`)
62
+ .join("\n");
63
+ throw new Error(
64
+ `Failed to download ${errors.length} file(s).\n${detail}`
65
+ );
66
+ }
67
+
68
+ return templateRoot;
69
+ }
70
+
71
+ /**
72
+ * @param {import('./config.js').RepositoryConfig} repo
73
+ * @returns {Promise<Array<{ path: string, type: string, sha: string }>>}
74
+ */
75
+ async function fetchRepoTree(repo) {
76
+ const refUrl = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/git/ref/heads/${repo.branch}`;
77
+ const refResponse = await githubRequest(refUrl);
78
+
79
+ if (!refResponse.ok) {
80
+ throw await formatGitHubError(
81
+ refResponse,
82
+ `Could not resolve branch "${repo.branch}" for ${repo.owner}/${repo.repo}`
83
+ );
84
+ }
85
+
86
+ const refData = await refResponse.json();
87
+ const commitSha = refData.object?.sha;
88
+ if (!commitSha) {
89
+ throw new Error(`Invalid ref response for branch ${repo.branch}`);
90
+ }
91
+
92
+ const commitUrl = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/git/commits/${commitSha}`;
93
+ const commitResponse = await githubRequest(commitUrl);
94
+ if (!commitResponse.ok) {
95
+ throw await formatGitHubError(commitResponse, "Could not fetch commit metadata");
96
+ }
97
+
98
+ const commitData = await commitResponse.json();
99
+ const treeSha = commitData.tree?.sha;
100
+ if (!treeSha) {
101
+ throw new Error("Invalid commit response: missing tree SHA");
102
+ }
103
+
104
+ const treeUrl = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/git/trees/${treeSha}?recursive=1`;
105
+ const treeResponse = await githubRequest(treeUrl);
106
+ if (!treeResponse.ok) {
107
+ throw await formatGitHubError(treeResponse, "Could not fetch repository tree");
108
+ }
109
+
110
+ const treeData = await treeResponse.json();
111
+ if (treeData.truncated) {
112
+ throw new Error(
113
+ "Repository tree is too large for a single API request. Contact the repository maintainer."
114
+ );
115
+ }
116
+
117
+ return treeData.tree ?? [];
118
+ }
119
+
120
+ /**
121
+ * @param {import('./config.js').RepositoryConfig} repo
122
+ * @param {{ githubPath: string, relPath: string, sha: string }} blob
123
+ * @param {string} templateRoot
124
+ */
125
+ async function downloadBlob(repo, blob, templateRoot) {
126
+ const { githubPath, relPath, sha } = blob;
127
+
128
+ if (relPath.includes(".git/") || relPath === ".git" || githubPath.includes("/.git/")) {
129
+ return;
130
+ }
131
+
132
+ const baseName = relPath.includes("/") ? relPath.slice(relPath.lastIndexOf("/") + 1) : relPath;
133
+ if (baseName === ".env" || (baseName.startsWith(".env.") && baseName !== ".env.example")) {
134
+ return;
135
+ }
136
+
137
+ const targetPath = join(templateRoot, relPath);
138
+ await mkdir(dirname(targetPath), { recursive: true });
139
+
140
+ const blobUrl = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/git/blobs/${sha}`;
141
+ const blobResponse = await githubRequest(blobUrl);
142
+ if (!blobResponse.ok) {
143
+ throw await formatGitHubError(blobResponse, `Failed to fetch blob for ${relPath}`);
144
+ }
145
+
146
+ const blobData = await blobResponse.json();
147
+
148
+ if (blobData.encoding === "base64" && blobData.content && blobData.size <= LARGE_FILE_BYTES) {
149
+ const content = Buffer.from(blobData.content.replace(/\n/g, ""), "base64");
150
+ await writeFile(targetPath, content);
151
+ return;
152
+ }
153
+
154
+ await downloadRawFile(repo, githubPath, targetPath);
155
+ }
156
+
157
+ /**
158
+ * @param {import('./config.js').RepositoryConfig} repo
159
+ * @param {string} githubPath
160
+ * @param {string} targetPath
161
+ */
162
+ async function downloadRawFile(repo, githubPath, targetPath) {
163
+ const url = `${RAW_BASE}/${repo.owner}/${repo.repo}/${repo.branch}/${githubPath}`;
164
+ const response = await fetch(url, {
165
+ headers: buildHeaders(),
166
+ redirect: "follow",
167
+ });
168
+
169
+ if (!response.ok) {
170
+ throw new Error(`HTTP ${response.status} for ${url}`);
171
+ }
172
+
173
+ if (!response.body) {
174
+ throw new Error(`Empty response body for ${url}`);
175
+ }
176
+
177
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(targetPath));
178
+ }
179
+
180
+ /**
181
+ * @param {string} url
182
+ */
183
+ async function githubRequest(url) {
184
+ return fetch(url, {
185
+ headers: {
186
+ ...buildHeaders(),
187
+ Accept: "application/vnd.github+json",
188
+ "X-GitHub-Api-Version": "2022-11-28",
189
+ },
190
+ });
191
+ }
192
+
193
+ function buildHeaders() {
194
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
195
+ if (token) {
196
+ return { Authorization: `Bearer ${token}` };
197
+ }
198
+ return {};
199
+ }
200
+
201
+ /**
202
+ * @param {Response} response
203
+ * @param {string} message
204
+ */
205
+ async function formatGitHubError(response, message) {
206
+ let detail = message;
207
+ try {
208
+ const body = await response.json();
209
+ if (body?.message) detail = `${message}: ${body.message}`;
210
+ } catch {
211
+ // ignore parse errors
212
+ }
213
+
214
+ if (response.status === 404) {
215
+ return new Error(
216
+ `${detail}\nVerify TEMPLATES_REPO_URL / templates.json repository settings.`
217
+ );
218
+ }
219
+
220
+ if (response.status === 403) {
221
+ return new Error(
222
+ `${detail}\nGitHub API rate limit may apply. Set GITHUB_TOKEN for higher limits.`
223
+ );
224
+ }
225
+
226
+ return new Error(`${detail} (HTTP ${response.status})`);
227
+ }
228
+
229
+ /**
230
+ * @param {string} packageRoot
231
+ * @param {string} templatesPath
232
+ * @param {string} templateDirectory
233
+ * @returns {Promise<string | null>}
234
+ */
235
+ export async function resolveLocalTemplate(packageRoot, templatesPath, templateDirectory) {
236
+ const localPath = join(packageRoot, templatesPath, templateDirectory);
237
+ try {
238
+ await readFile(join(localPath, "package.json"));
239
+ return localPath;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
package/cli/index.js ADDED
@@ -0,0 +1,249 @@
1
+ import { execSync } from "node:child_process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { loadManifest, getPackageRoot, resolveRepositoryConfig } from "./config.js";
5
+ import {
6
+ copyTemplate,
7
+ findConflictingPaths,
8
+ isDirectoryEmpty,
9
+ listDirectoryEntries,
10
+ removeDirectory,
11
+ targetHasGitRepo,
12
+ } from "./copy.js";
13
+ import { fetchTemplateFromGitHub, resolveLocalTemplate } from "./fetch.js";
14
+
15
+ const HELP_TEXT = `
16
+ tempjs — instantiate project templates from GitHub
17
+
18
+ USAGE
19
+ tempjs list
20
+ tempjs <template-id> [options]
21
+ tempjs --help
22
+
23
+ OPTIONS
24
+ --force Overwrite existing files in the current directory
25
+ --remote Fetch from GitHub even if a local template copy exists
26
+ --init-git Run git init after copying the template
27
+ --help Show this help message
28
+
29
+ EXAMPLES
30
+ mkdir hotel-client && cd hotel-client
31
+ tempjs hotel
32
+ git init
33
+
34
+ ENVIRONMENT
35
+ TEMPLATES_REPO_URL GitHub repo URL or owner/repo (overrides templates.json)
36
+ TEMPLATES_REPO_OWNER GitHub owner/username
37
+ TEMPLATES_REPO_REPO GitHub repository name
38
+ TEMPLATES_REPO_BRANCH Branch name (default: main)
39
+ TEMPLATE_USE_REMOTE=1 Always fetch from GitHub
40
+ GITHUB_TOKEN / GH_TOKEN GitHub token for API rate limits
41
+
42
+ CONFIGURATION
43
+ Repository and template mappings live in templates.json at the package root.
44
+ `;
45
+
46
+ /**
47
+ * @param {Record<string, { name: string, description: string }>} templates
48
+ */
49
+ function printTemplateList(templates) {
50
+ console.log("Available templates:\n");
51
+ const ids = Object.keys(templates).sort();
52
+ const idWidth = Math.max(...ids.map((id) => id.length), 10);
53
+
54
+ for (const id of ids) {
55
+ const entry = templates[id];
56
+ console.log(`${id.padEnd(idWidth + 2)}${entry.name}`);
57
+ if (entry.description) {
58
+ console.log(`${"".padEnd(idWidth + 2)}${entry.description}`);
59
+ }
60
+ }
61
+ console.log("");
62
+ }
63
+
64
+ /**
65
+ * @param {string[]} argv
66
+ */
67
+ function parseArgs(argv) {
68
+ const flags = {
69
+ force: false,
70
+ remote: false,
71
+ initGit: false,
72
+ help: false,
73
+ };
74
+ const positionals = [];
75
+
76
+ for (const arg of argv) {
77
+ if (arg === "--force") flags.force = true;
78
+ else if (arg === "--remote") flags.remote = true;
79
+ else if (arg === "--init-git") flags.initGit = true;
80
+ else if (arg === "--help" || arg === "-h") flags.help = true;
81
+ else if (arg.startsWith("-")) {
82
+ throw new Error(`Unknown option: ${arg}`);
83
+ } else {
84
+ positionals.push(arg);
85
+ }
86
+ }
87
+
88
+ return { flags, positionals };
89
+ }
90
+
91
+ /**
92
+ * @param {string[]} conflicts
93
+ */
94
+ function printConflictWarning(conflicts) {
95
+ console.log("Current directory is not empty.");
96
+ console.log("The following files may be overwritten:");
97
+ const preview = conflicts.slice(0, 20);
98
+ for (const path of preview) {
99
+ console.log(` ${path}`);
100
+ }
101
+ if (conflicts.length > preview.length) {
102
+ console.log(` ... and ${conflicts.length - preview.length} more`);
103
+ }
104
+ console.log("");
105
+ }
106
+
107
+ /**
108
+ * @param {boolean} force
109
+ */
110
+ async function confirmOverwrite(force) {
111
+ if (force) return true;
112
+ const rl = createInterface({ input, output });
113
+ try {
114
+ const answer = await rl.question("Continue? [y/N] ");
115
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
116
+ } finally {
117
+ rl.close();
118
+ }
119
+ }
120
+
121
+ /**
122
+ * @param {string} targetDir
123
+ * @param {string} templateId
124
+ * @param {{ force: boolean, remote: boolean, initGit: boolean }} flags
125
+ */
126
+ async function runTemplate(targetDir, templateId, flags) {
127
+ const manifest = loadManifest();
128
+ const entry = manifest.templates[templateId];
129
+
130
+ if (!entry) {
131
+ console.error(`Unknown template: ${templateId}`);
132
+ console.error("Run `tempjs list` to see available templates.");
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+
137
+ const repo = resolveRepositoryConfig(manifest.repository);
138
+ const packageRoot = getPackageRoot();
139
+ const useRemote =
140
+ flags.remote ||
141
+ process.env.TEMPLATE_USE_REMOTE === "1" ||
142
+ process.env.TEMPLATE_USE_REMOTE === "true";
143
+
144
+ let sourceDir = null;
145
+ let cleanupDir = null;
146
+
147
+ try {
148
+ if (!useRemote) {
149
+ sourceDir = await resolveLocalTemplate(
150
+ packageRoot,
151
+ repo.templatesPath,
152
+ entry.directory
153
+ );
154
+ }
155
+
156
+ if (!sourceDir) {
157
+ console.log(`Fetching template "${templateId}" from ${repo.owner}/${repo.repo}...`);
158
+ sourceDir = await fetchTemplateFromGitHub(repo, entry.directory);
159
+ cleanupDir = sourceDir.replace(/[/\\]template$/, "");
160
+ } else {
161
+ console.log(`Using local template "${templateId}"...`);
162
+ }
163
+
164
+ if (targetHasGitRepo(targetDir)) {
165
+ console.warn(
166
+ "Warning: This directory already contains a .git folder (existing Git repository)."
167
+ );
168
+ }
169
+
170
+ const entries = listDirectoryEntries(targetDir);
171
+ const hasContent = entries.length > 0;
172
+
173
+ if (hasContent && !flags.force) {
174
+ const conflicts = findConflictingPaths(sourceDir, targetDir);
175
+ if (conflicts.length > 0) {
176
+ printConflictWarning(conflicts);
177
+ const confirmed = await confirmOverwrite(false);
178
+ if (!confirmed) {
179
+ console.log("Aborted.");
180
+ return;
181
+ }
182
+ } else if (!isDirectoryEmpty(targetDir)) {
183
+ console.log("Current directory is not empty, but no files would be overwritten.");
184
+ const confirmed = await confirmOverwrite(false);
185
+ if (!confirmed) {
186
+ console.log("Aborted.");
187
+ return;
188
+ }
189
+ }
190
+ }
191
+
192
+ const allowOverwrite = flags.force || hasContent;
193
+ copyTemplate(sourceDir, targetDir, { force: allowOverwrite });
194
+
195
+ if (flags.initGit && !targetHasGitRepo(targetDir)) {
196
+ execSync("git init", { cwd: targetDir, stdio: "inherit" });
197
+ }
198
+
199
+ console.log(`\nTemplate "${entry.name}" created successfully in ${targetDir}`);
200
+ console.log("\nNext steps:");
201
+ console.log(" pnpm install # or npm install");
202
+ console.log(" pnpm dev # start development server");
203
+ if (!flags.initGit && !targetHasGitRepo(targetDir)) {
204
+ console.log(" git init # optional: initialize a new repository");
205
+ }
206
+ } catch (error) {
207
+ if (error instanceof Error && error.message === "TARGET_NOT_EMPTY") {
208
+ printConflictWarning(error.conflicts ?? []);
209
+ console.error("Use --force to overwrite without prompting.");
210
+ process.exitCode = 1;
211
+ return;
212
+ }
213
+
214
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
215
+ process.exitCode = 1;
216
+ } finally {
217
+ if (cleanupDir) {
218
+ removeDirectory(cleanupDir);
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * @param {string[]} argv
225
+ */
226
+ async function main(argv) {
227
+ const { flags, positionals } = parseArgs(argv);
228
+ const command = positionals[0];
229
+
230
+ if (flags.help || command === "help" || (!command && argv.length === 0)) {
231
+ console.log(HELP_TEXT.trim());
232
+ return;
233
+ }
234
+
235
+ const manifest = loadManifest();
236
+
237
+ if (command === "list") {
238
+ printTemplateList(manifest.templates);
239
+ return;
240
+ }
241
+
242
+ const targetDir = process.cwd();
243
+ await runTemplate(targetDir, command, flags);
244
+ }
245
+
246
+ main(process.argv.slice(2)).catch((error) => {
247
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
248
+ process.exit(1);
249
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@navneet_25/tempjs",
3
+ "version": "1.0.0",
4
+ "description": "CLI to instantiate website project templates from a single GitHub repository",
5
+ "type": "module",
6
+ "bin": {
7
+ "tempjs": "./cli/index.js"
8
+ },
9
+ "files": [
10
+ "cli",
11
+ "templates.json"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "author": "navneet_25 <navneetnishchal1420@gmail.com>",
17
+ "keywords": [
18
+ "template",
19
+ "cli",
20
+ "website",
21
+ "nextjs"
22
+ ],
23
+ "license": "MIT"
24
+ }
package/templates.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "repository": {
3
+ "owner": "your-username",
4
+ "repo": "templates",
5
+ "branch": "main",
6
+ "templatesPath": "templates"
7
+ },
8
+ "templates": {
9
+ "hotel": {
10
+ "directory": "hotel-website-template",
11
+ "name": "Hotel Website",
12
+ "description": "Modern hotel and resort website with admin panel, gallery, and booking features"
13
+ },
14
+ "real-estate": {
15
+ "directory": "real-estate-website-template",
16
+ "name": "Real Estate Website",
17
+ "description": "Real estate and property listing website with admin panel and property management"
18
+ }
19
+ }
20
+ }