@navneet_25/tempjs 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,10 @@ tempjs list
34
34
 
35
35
  ```bash
36
36
  tempjs list # show available templates
37
- tempjs hotel # create project from hotel template
37
+ tempjs hotel # create project from hotel template using default theme
38
+ tempjs hotel config # create project and run interactive theme/font setup
39
+ tempjs theme # change/reset the theme of an initialized project
40
+ tempjs font # change/reset the font pairing of an initialized project
38
41
  tempjs real-estate --force # overwrite existing files
39
42
  tempjs --help # show help
40
43
  ```
@@ -43,6 +46,7 @@ tempjs --help # show help
43
46
 
44
47
  | Option | Description |
45
48
  |---------------|-------------|
49
+ | `--config` | Prompt for theme and font pairings during template initialization |
46
50
  | `--force` | Overwrite files in the current directory without prompting |
47
51
  | `--remote` | Fetch from GitHub even when a local template copy exists |
48
52
  | `--init-git` | Run `git init` after copying (optional) |
@@ -141,14 +145,13 @@ tempjs hotel --remote
141
145
 
142
146
  ## How fetching works
143
147
 
144
- `tempjs hotel` does **not** clone the entire repository.
148
+ `tempjs hotel` does **not** run `git clone` and does **not** call the GitHub REST API per file.
145
149
 
146
- 1. The CLI reads the GitHub API tree for the configured branch.
147
- 2. It filters files under `templates/hotel-website-template/`.
148
- 3. Only those files are downloaded (via blob API or raw content).
149
- 4. Files are validated in a temporary directory, then copied to your project.
150
+ 1. The CLI downloads **one** repository archive from `codeload.github.com` (a single HTTP request — no API rate-limit issues for public repos).
151
+ 2. It extracts only `templates/hotel-website-template/` from that archive into a temp directory.
152
+ 3. Files are validated, then copied into your project directory.
150
153
 
151
- Other templates are not transferred.
154
+ The archive contains the whole templates repo on the wire, but only the requested template folder is extracted locally. No `GITHUB_TOKEN` is required for public repositories.
152
155
 
153
156
  ## Repository structure
154
157
 
package/cli/fetch.js CHANGED
@@ -1,231 +1,189 @@
1
- import { createWriteStream } from "node:fs";
2
- import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
1
+ import { execFileSync } from "node:child_process";
2
+ import { createWriteStream, existsSync } from "node:fs";
3
+ import { mkdir, mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
+ import { join } from "node:path";
5
6
  import { pipeline } from "node:stream/promises";
6
7
  import { Readable } from "node:stream";
7
8
 
8
- const GITHUB_API = "https://api.github.com";
9
- const RAW_BASE = "https://raw.githubusercontent.com";
10
- const LARGE_FILE_BYTES = 1024 * 1024;
9
+ const CODELOAD_BASE = "https://codeload.github.com";
11
10
 
12
11
  /**
13
- * Download a single template directory from GitHub without cloning the full repo.
12
+ * Download a single template directory from GitHub without cloning or per-file API calls.
13
+ *
14
+ * Strategy: one tarball download from codeload.github.com (not the REST API), then
15
+ * extract only templates/<templateDirectory>/ from the archive. This uses a single
16
+ * HTTP request instead of 60+ blob API calls, avoiding unauthenticated rate limits.
17
+ *
14
18
  * @param {import('./config.js').RepositoryConfig} repo
15
19
  * @param {string} templateDirectory
16
20
  * @returns {Promise<string>} Path to temp directory containing template files
17
21
  */
18
22
  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
23
  const tempRoot = await mkdtemp(join(tmpdir(), "template-cli-"));
24
+ const tarballPath = join(tempRoot, "archive.tar.gz");
37
25
  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
26
 
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}`
27
+ try {
28
+ await downloadTarball(repo, tarballPath);
29
+ await extractTemplateFromTarball(
30
+ tarballPath,
31
+ templateRoot,
32
+ repo,
33
+ templateDirectory
65
34
  );
35
+ await readFile(join(templateRoot, "package.json"));
36
+ return templateRoot;
37
+ } catch (error) {
38
+ await rm(tempRoot, { recursive: true, force: true });
39
+ throw error;
66
40
  }
67
-
68
- return templateRoot;
69
41
  }
70
42
 
71
43
  /**
72
44
  * @param {import('./config.js').RepositoryConfig} repo
73
- * @returns {Promise<Array<{ path: string, type: string, sha: string }>>}
45
+ * @param {string} destPath
74
46
  */
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
- }
47
+ async function downloadTarball(repo, destPath) {
48
+ const branch = encodeURIComponent(repo.branch);
49
+ const url = `${CODELOAD_BASE}/${repo.owner}/${repo.repo}/tar.gz/${branch}`;
97
50
 
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
- }
51
+ const response = await fetch(url, {
52
+ headers: buildHeaders(),
53
+ redirect: "follow",
54
+ });
103
55
 
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");
56
+ if (!response.ok) {
57
+ let detail = `Could not download ${repo.owner}/${repo.repo}@${repo.branch}`;
58
+ if (response.status === 404) {
59
+ detail += "\nVerify the repository, branch, and templates.json settings.";
60
+ }
61
+ if (response.status === 401 || response.status === 403) {
62
+ detail += "\nFor private repositories, set GITHUB_TOKEN or GH_TOKEN.";
63
+ }
64
+ throw new Error(`${detail} (HTTP ${response.status})`);
108
65
  }
109
66
 
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
- );
67
+ if (!response.body) {
68
+ throw new Error("Empty response while downloading repository archive");
115
69
  }
116
70
 
117
- return treeData.tree ?? [];
71
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath));
118
72
  }
119
73
 
120
74
  /**
75
+ * @param {string} tarballPath
76
+ * @param {string} destDir
121
77
  * @param {import('./config.js').RepositoryConfig} repo
122
- * @param {{ githubPath: string, relPath: string, sha: string }} blob
123
- * @param {string} templateRoot
78
+ * @param {string} templateDirectory
124
79
  */
125
- async function downloadBlob(repo, blob, templateRoot) {
126
- const { githubPath, relPath, sha } = blob;
80
+ async function extractTemplateFromTarball(
81
+ tarballPath,
82
+ destDir,
83
+ repo,
84
+ templateDirectory
85
+ ) {
86
+ const listing = execFileSync("tar", ["-tzf", tarballPath], {
87
+ encoding: "utf8",
88
+ maxBuffer: 64 * 1024 * 1024,
89
+ });
127
90
 
128
- if (relPath.includes(".git/") || relPath === ".git" || githubPath.includes("/.git/")) {
129
- return;
91
+ const lines = listing.split("\n").filter(Boolean);
92
+ if (lines.length === 0) {
93
+ throw new Error("Repository archive is empty");
130
94
  }
131
95
 
132
- const baseName = relPath.includes("/") ? relPath.slice(relPath.lastIndexOf("/") + 1) : relPath;
133
- if (baseName === ".env" || (baseName.startsWith(".env.") && baseName !== ".env.example")) {
134
- return;
135
- }
96
+ const archiveRoot = lines[0].replace(/\/$/, "").split("/")[0];
97
+ const templateArchivePath = `${archiveRoot}/${repo.templatesPath}/${templateDirectory}`;
98
+ const templatePrefix = `${templateArchivePath}/`;
136
99
 
137
- const targetPath = join(templateRoot, relPath);
138
- await mkdir(dirname(targetPath), { recursive: true });
100
+ const hasTemplate = lines.some(
101
+ (line) => line === templatePrefix || line.startsWith(templatePrefix)
102
+ );
139
103
 
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}`);
104
+ if (!hasTemplate) {
105
+ throw new Error(
106
+ `Template directory not found in repository: ${repo.templatesPath}/${templateDirectory}`
107
+ );
144
108
  }
145
109
 
146
- const blobData = await blobResponse.json();
110
+ const stripComponents = templateArchivePath.split("/").length;
147
111
 
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
- }
112
+ await mkdir(destDir, { recursive: true });
113
+
114
+ execFileSync(
115
+ "tar",
116
+ [
117
+ "-xzf",
118
+ tarballPath,
119
+ "-C",
120
+ destDir,
121
+ `--strip-components=${stripComponents}`,
122
+ templateArchivePath,
123
+ ],
124
+ { stdio: "pipe", maxBuffer: 64 * 1024 * 1024 }
125
+ );
153
126
 
154
- await downloadRawFile(repo, githubPath, targetPath);
127
+ await removeSkippedFiles(destDir);
155
128
  }
156
129
 
157
130
  /**
158
- * @param {import('./config.js').RepositoryConfig} repo
159
- * @param {string} githubPath
160
- * @param {string} targetPath
131
+ * Remove files that must never appear in generated projects.
132
+ * @param {string} dir
161
133
  */
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
- }
134
+ async function removeSkippedFiles(dir) {
135
+ if (!existsSync(dir)) return;
136
+
137
+ const entries = await readDirSafe(dir);
138
+ for (const entry of entries) {
139
+ const fullPath = join(dir, entry.name);
140
+ if (entry.isDirectory) {
141
+ if (entry.name === ".git") {
142
+ await rm(fullPath, { recursive: true, force: true });
143
+ continue;
144
+ }
145
+ await removeSkippedFiles(fullPath);
146
+ continue;
147
+ }
172
148
 
173
- if (!response.body) {
174
- throw new Error(`Empty response body for ${url}`);
149
+ if (shouldSkipFileName(entry.name)) {
150
+ await rm(fullPath, { force: true });
151
+ }
175
152
  }
153
+ }
176
154
 
177
- await pipeline(Readable.fromWeb(response.body), createWriteStream(targetPath));
155
+ /**
156
+ * @param {string} dir
157
+ */
158
+ async function readDirSafe(dir) {
159
+ const names = await readdir(dir);
160
+ const result = [];
161
+ for (const name of names) {
162
+ const fullPath = join(dir, name);
163
+ const info = await stat(fullPath);
164
+ result.push({ name, isDirectory: info.isDirectory() });
165
+ }
166
+ return result;
178
167
  }
179
168
 
180
169
  /**
181
- * @param {string} url
170
+ * @param {string} name
182
171
  */
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
- });
172
+ function shouldSkipFileName(name) {
173
+ if (name === ".env" || (name.startsWith(".env.") && name !== ".env.example")) {
174
+ return true;
175
+ }
176
+ return false;
191
177
  }
192
178
 
193
179
  function buildHeaders() {
194
180
  const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
195
181
  if (token) {
196
- return { Authorization: `Bearer ${token}` };
182
+ return { Authorization: `token ${token}` };
197
183
  }
198
184
  return {};
199
185
  }
200
186
 
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
187
  /**
230
188
  * @param {string} packageRoot
231
189
  * @param {string} templatesPath
package/cli/index.js CHANGED
@@ -4,6 +4,12 @@ import { execSync } from "node:child_process";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { loadManifest, getPackageRoot, resolveRepositoryConfig } from "./config.js";
7
+ import {
8
+ promptTheme,
9
+ promptFont,
10
+ applyThemeAndFont,
11
+ getSavedConfig,
12
+ } from "./theme-manager.js";
7
13
  import {
8
14
  copyTemplate,
9
15
  findConflictingPaths,
@@ -20,9 +26,13 @@ tempjs — instantiate project templates from GitHub
20
26
  USAGE
21
27
  tempjs list
22
28
  tempjs <template-id> [options]
29
+ tempjs <template-id> config Initialize with interactive theme & typography setup
30
+ tempjs theme Change the project's theme in an initialized directory
31
+ tempjs font Change the project's font styling in an initialized directory
23
32
  tempjs --help
24
33
 
25
34
  OPTIONS
35
+ --config Prompt for theme and font pairings during initialization
26
36
  --force Overwrite existing files in the current directory
27
37
  --remote Fetch from GitHub even if a local template copy exists
28
38
  --init-git Run git init after copying the template
@@ -30,8 +40,8 @@ OPTIONS
30
40
 
31
41
  EXAMPLES
32
42
  mkdir hotel-client && cd hotel-client
33
- tempjs hotel
34
- git init
43
+ tempjs hotel config
44
+ tempjs theme
35
45
 
36
46
  ENVIRONMENT
37
47
  TEMPLATES_REPO_URL GitHub repo URL or owner/repo (overrides templates.json)
@@ -39,7 +49,7 @@ ENVIRONMENT
39
49
  TEMPLATES_REPO_REPO GitHub repository name
40
50
  TEMPLATES_REPO_BRANCH Branch name (default: main)
41
51
  TEMPLATE_USE_REMOTE=1 Always fetch from GitHub
42
- GITHUB_TOKEN / GH_TOKEN GitHub token for API rate limits
52
+ GITHUB_TOKEN / GH_TOKEN Optional required only for private repositories
43
53
 
44
54
  CONFIGURATION
45
55
  Repository and template mappings live in templates.json at the package root.
@@ -72,6 +82,7 @@ function parseArgs(argv) {
72
82
  remote: false,
73
83
  initGit: false,
74
84
  help: false,
85
+ config: false,
75
86
  };
76
87
  const positionals = [];
77
88
 
@@ -80,6 +91,7 @@ function parseArgs(argv) {
80
91
  else if (arg === "--remote") flags.remote = true;
81
92
  else if (arg === "--init-git") flags.initGit = true;
82
93
  else if (arg === "--help" || arg === "-h") flags.help = true;
94
+ else if (arg === "--config") flags.config = true;
83
95
  else if (arg.startsWith("-")) {
84
96
  throw new Error(`Unknown option: ${arg}`);
85
97
  } else {
@@ -124,8 +136,9 @@ async function confirmOverwrite(force) {
124
136
  * @param {string} targetDir
125
137
  * @param {string} templateId
126
138
  * @param {{ force: boolean, remote: boolean, initGit: boolean }} flags
139
+ * @param {boolean} runWithConfig
127
140
  */
128
- async function runTemplate(targetDir, templateId, flags) {
141
+ async function runTemplate(targetDir, templateId, flags, runWithConfig = false) {
129
142
  const manifest = loadManifest();
130
143
  const entry = manifest.templates[templateId];
131
144
 
@@ -198,6 +211,13 @@ async function runTemplate(targetDir, templateId, flags) {
198
211
  execSync("git init", { cwd: targetDir, stdio: "inherit" });
199
212
  }
200
213
 
214
+ if (runWithConfig) {
215
+ console.log("\nConfiguring project theme and typography...");
216
+ const selectedTheme = await promptTheme("theme1");
217
+ const selectedFont = await promptFont("default");
218
+ await applyThemeAndFont(targetDir, selectedTheme, selectedFont);
219
+ }
220
+
201
221
  console.log(`\nTemplate "${entry.name}" created successfully in ${targetDir}`);
202
222
  console.log("\nNext steps:");
203
223
  console.log(" pnpm install # or npm install");
@@ -227,22 +247,37 @@ async function runTemplate(targetDir, templateId, flags) {
227
247
  */
228
248
  async function main(argv) {
229
249
  const { flags, positionals } = parseArgs(argv);
230
- const command = positionals[0];
250
+ let command = positionals[0];
231
251
 
232
252
  if (flags.help || command === "help" || (!command && argv.length === 0)) {
233
253
  console.log(HELP_TEXT.trim());
234
254
  return;
235
255
  }
236
256
 
237
- const manifest = loadManifest();
257
+ if (command === "theme" || command === "font") {
258
+ const targetDir = process.cwd();
259
+ const currentConfig = getSavedConfig(targetDir);
238
260
 
239
- if (command === "list") {
240
- printTemplateList(manifest.templates);
261
+ if (command === "theme") {
262
+ const selectedTheme = await promptTheme(currentConfig.theme || "theme1");
263
+ await applyThemeAndFont(targetDir, selectedTheme, currentConfig.font || "default");
264
+ } else {
265
+ const selectedFont = await promptFont(currentConfig.font || "default");
266
+ await applyThemeAndFont(targetDir, currentConfig.theme || "theme1", selectedFont);
267
+ }
241
268
  return;
242
269
  }
243
270
 
271
+ const manifest = loadManifest();
272
+
273
+ let runWithConfig = flags.config;
274
+ if (positionals.length >= 2 && positionals[1] === "config") {
275
+ runWithConfig = true;
276
+ positionals.splice(1, 1);
277
+ }
278
+
244
279
  const targetDir = process.cwd();
245
- await runTemplate(targetDir, command, flags);
280
+ await runTemplate(targetDir, command, flags, runWithConfig);
246
281
  }
247
282
 
248
283
  main(process.argv.slice(2)).catch((error) => {
@@ -0,0 +1,352 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { readdir, readFile, writeFile } from "node:fs/promises";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ export const THEMES = [
8
+ {
9
+ id: "theme1",
10
+ name: "Theme 1 (Slate / Blue)",
11
+ colors: {
12
+ primary: "#2563EB",
13
+ primaryHover: "#1D4ED8",
14
+ accent: "#38BDF8",
15
+ accentDark: "#0284C7",
16
+ accentLight: "#EFF6FF",
17
+ textMain: "#0F172A",
18
+ textMuted: "#64748B",
19
+ bgMain: "#F8FAFC",
20
+ bgLight: "#EFF6FF",
21
+ bgCard: "#FFFFFF",
22
+ footerBg: "#E2E8F0",
23
+ ctaPrimary: "#2563EB",
24
+ ctaPrimaryHover: "#1D4ED8",
25
+ }
26
+ },
27
+ {
28
+ id: "theme2",
29
+ name: "Theme 2 (Forest / Green)",
30
+ colors: {
31
+ primary: "#58812F",
32
+ primaryHover: "#466725",
33
+ accent: "#8BC34A",
34
+ accentDark: "#689F38",
35
+ accentLight: "#F1F5EA",
36
+ textMain: "#1D3108",
37
+ textMuted: "#4A5441",
38
+ bgMain: "#F9FAF7",
39
+ bgLight: "#F1F5EA",
40
+ bgCard: "#FFFFFF",
41
+ footerBg: "#E6EBDC",
42
+ ctaPrimary: "#58812F",
43
+ ctaPrimaryHover: "#466725",
44
+ }
45
+ },
46
+ {
47
+ id: "theme3",
48
+ name: "Theme 3 (Purple / Violet)",
49
+ colors: {
50
+ primary: "#7C3AED",
51
+ primaryHover: "#6D28D9",
52
+ accent: "#A78BFA",
53
+ accentDark: "#8B5CF6",
54
+ accentLight: "#F5F3FF",
55
+ textMain: "#2E1065",
56
+ textMuted: "#6B6382",
57
+ bgMain: "#FAF9FF",
58
+ bgLight: "#F5F3FF",
59
+ bgCard: "#FFFFFF",
60
+ footerBg: "#E9E3FF",
61
+ ctaPrimary: "#7C3AED",
62
+ ctaPrimaryHover: "#6D28D9",
63
+ }
64
+ },
65
+ {
66
+ id: "theme4",
67
+ name: "Theme 4 (Red / Crimson)",
68
+ colors: {
69
+ primary: "#DC2626",
70
+ primaryHover: "#B91C1C",
71
+ accent: "#F87171",
72
+ accentDark: "#EF4444",
73
+ accentLight: "#FEF2F2",
74
+ textMain: "#450A0A",
75
+ textMuted: "#7F1D1D",
76
+ bgMain: "#FFFBFB",
77
+ bgLight: "#FEF2F2",
78
+ bgCard: "#FFFFFF",
79
+ footerBg: "#FEE2E2",
80
+ ctaPrimary: "#DC2626",
81
+ ctaPrimaryHover: "#B91C1C",
82
+ }
83
+ },
84
+ {
85
+ id: "theme5",
86
+ name: "Theme 5 (Amber / Gold)",
87
+ colors: {
88
+ primary: "#D97706",
89
+ primaryHover: "#B45309",
90
+ accent: "#FBBF24",
91
+ accentDark: "#F59E0B",
92
+ accentLight: "#FFFBEB",
93
+ textMain: "#1C1917",
94
+ textMuted: "#78350F",
95
+ bgMain: "#FFFCF5",
96
+ bgLight: "#FFFBEB",
97
+ bgCard: "#FFFFFF",
98
+ footerBg: "#FEF3C7",
99
+ ctaPrimary: "#D97706",
100
+ ctaPrimaryHover: "#B45309",
101
+ }
102
+ }
103
+ ];
104
+
105
+ export const FONTS = [
106
+ {
107
+ id: "default",
108
+ name: "Playfair Display (Serif) + Outfit (Sans-serif) [Default]",
109
+ serif: "'Playfair Display', Georgia, serif",
110
+ sans: "'Outfit', system-ui, sans-serif",
111
+ importUrl: "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"
112
+ },
113
+ {
114
+ id: "inter",
115
+ name: "Inter (Sans-serif) + Inter (Sans-serif)",
116
+ serif: "'Inter', system-ui, sans-serif",
117
+ sans: "'Inter', system-ui, sans-serif",
118
+ importUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
119
+ },
120
+ {
121
+ id: "lora-montserrat",
122
+ name: "Lora (Serif) + Montserrat (Sans-serif)",
123
+ serif: "'Lora', Georgia, serif",
124
+ sans: "'Montserrat', system-ui, sans-serif",
125
+ importUrl: "https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,700;1,400&family=Montserrat:wght@300;400;500;600;700&display=swap"
126
+ },
127
+ {
128
+ id: "merriweather-open-sans",
129
+ name: "Merriweather (Serif) + Open Sans (Sans-serif)",
130
+ serif: "'Merriweather', Georgia, serif",
131
+ sans: "'Open Sans', system-ui, sans-serif",
132
+ importUrl: "https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;1,300&family=Open+Sans:wght@300;400;500;600;700&display=swap"
133
+ },
134
+ {
135
+ id: "cinzel-montserrat",
136
+ name: "Cinzel (Serif) + Montserrat (Sans-serif)",
137
+ serif: "'Cinzel', serif",
138
+ sans: "'Montserrat', system-ui, sans-serif",
139
+ importUrl: "https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Montserrat:wght@300;400;500;600;700&display=swap"
140
+ }
141
+ ];
142
+
143
+ export function getSavedConfig(targetDir) {
144
+ const configPath = join(targetDir, ".tempjsrc");
145
+ if (existsSync(configPath)) {
146
+ try {
147
+ return JSON.parse(readFileSync(configPath, "utf8"));
148
+ } catch {
149
+ return {};
150
+ }
151
+ }
152
+ return {};
153
+ }
154
+
155
+ async function promptSelection(options, promptText, defaultValue) {
156
+ const rl = createInterface({ input, output });
157
+ try {
158
+ console.log(`\n${promptText}`);
159
+ for (let i = 0; i < options.length; i++) {
160
+ const isDefault = options[i].id === defaultValue ? " (current default)" : "";
161
+ console.log(` [${i + 1}] ${options[i].name}${isDefault}`);
162
+ }
163
+ const defaultIndex = options.findIndex(o => o.id === defaultValue) + 1;
164
+ const placeholder = defaultIndex > 0 ? defaultIndex : 1;
165
+ const actualDefault = defaultIndex > 0 ? defaultValue : options[0].id;
166
+
167
+ while (true) {
168
+ const answer = await rl.question(`Choose an option (1-${options.length}) [${placeholder}]: `);
169
+ const trimmed = answer.trim();
170
+ if (trimmed === "") return actualDefault;
171
+ const num = parseInt(trimmed, 10);
172
+ if (num >= 1 && num <= options.length) {
173
+ return options[num - 1].id;
174
+ }
175
+ console.log("Invalid option. Please try again.");
176
+ }
177
+ } finally {
178
+ rl.close();
179
+ }
180
+ }
181
+
182
+ export async function promptTheme(currentThemeId = "theme1") {
183
+ return promptSelection(THEMES, "Available Themes:", currentThemeId);
184
+ }
185
+
186
+ export async function promptFont(currentFontId = "default") {
187
+ return promptSelection(FONTS, "Available Font combinations:", currentFontId);
188
+ }
189
+
190
+ export async function findGlobalsCss(dir) {
191
+ const commonPaths = [
192
+ join(dir, "app/globals.css"),
193
+ join(dir, "src/app/globals.css"),
194
+ join(dir, "src/globals.css"),
195
+ join(dir, "globals.css"),
196
+ ];
197
+ for (const p of commonPaths) {
198
+ if (existsSync(p)) return p;
199
+ }
200
+
201
+ async function search(currentDir) {
202
+ let entries;
203
+ try {
204
+ entries = await readdir(currentDir, { withFileTypes: true });
205
+ } catch {
206
+ return null;
207
+ }
208
+ for (const entry of entries) {
209
+ if (entry.isDirectory()) {
210
+ if (
211
+ entry.name === "node_modules" ||
212
+ entry.name === ".next" ||
213
+ entry.name === ".git" ||
214
+ entry.name === "dist" ||
215
+ entry.name === "build"
216
+ ) {
217
+ continue;
218
+ }
219
+ const found = await search(join(currentDir, entry.name));
220
+ if (found) return found;
221
+ } else if (entry.name === "globals.css") {
222
+ return join(currentDir, entry.name);
223
+ }
224
+ }
225
+ return null;
226
+ }
227
+ return search(dir);
228
+ }
229
+
230
+ export async function findSiteTs(dir) {
231
+ const commonPaths = [
232
+ join(dir, "constants/site.ts"),
233
+ join(dir, "src/constants/site.ts"),
234
+ join(dir, "constants/site.js"),
235
+ join(dir, "src/constants/site.js"),
236
+ ];
237
+ for (const p of commonPaths) {
238
+ if (existsSync(p)) return p;
239
+ }
240
+
241
+ async function search(currentDir) {
242
+ let entries;
243
+ try {
244
+ entries = await readdir(currentDir, { withFileTypes: true });
245
+ } catch {
246
+ return null;
247
+ }
248
+ for (const entry of entries) {
249
+ if (entry.isDirectory()) {
250
+ if (
251
+ entry.name === "node_modules" ||
252
+ entry.name === ".next" ||
253
+ entry.name === ".git" ||
254
+ entry.name === "dist" ||
255
+ entry.name === "build"
256
+ ) {
257
+ continue;
258
+ }
259
+ const found = await search(join(currentDir, entry.name));
260
+ if (found) return found;
261
+ } else if (entry.name === "site.ts" || entry.name === "site.js") {
262
+ return join(currentDir, entry.name);
263
+ }
264
+ }
265
+ return null;
266
+ }
267
+ return search(dir);
268
+ }
269
+
270
+ export async function applyThemeAndFont(targetDir, themeId, fontId) {
271
+ const theme = THEMES.find(t => t.id === themeId) || THEMES[0];
272
+ const font = FONTS.find(f => f.id === fontId) || FONTS[0];
273
+
274
+ // 1. Write metadata config file .tempjsrc
275
+ const configPath = join(targetDir, ".tempjsrc");
276
+ const config = {
277
+ theme: theme.id,
278
+ font: font.id,
279
+ updatedAt: new Date().toISOString()
280
+ };
281
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
282
+
283
+ // 2. Find and update globals.css
284
+ const globalsPath = await findGlobalsCss(targetDir);
285
+ if (globalsPath) {
286
+ const cssDir = join(globalsPath, "..");
287
+ const themeCssPath = join(cssDir, "tempjs-theme.css");
288
+
289
+ // Construct the theme css content
290
+ const cssContent = `/* tempjs generated theme settings */
291
+ @import url('${font.importUrl}');
292
+
293
+ :root {
294
+ --primary: ${theme.colors.primary} !important;
295
+ --primary-hover: ${theme.colors.primaryHover} !important;
296
+ --accent-gold: ${theme.colors.accent} !important;
297
+ --accent-gold-dark: ${theme.colors.accentDark} !important;
298
+ --accent-gold-light: ${theme.colors.accentLight} !important;
299
+ --text-main: ${theme.colors.textMain} !important;
300
+ --text-muted: ${theme.colors.textMuted} !important;
301
+ --bg-tan: ${theme.colors.bgMain} !important;
302
+ --bg-light: ${theme.colors.bgLight} !important;
303
+ --bg-card: ${theme.colors.bgCard} !important;
304
+ --footer-bg: ${theme.colors.footerBg} !important;
305
+ --cta-primary: ${theme.colors.ctaPrimary} !important;
306
+ --cta-primary-hover: ${theme.colors.ctaPrimaryHover} !important;
307
+
308
+ --font-serif: ${font.serif} !important;
309
+ --font-sans: ${font.sans} !important;
310
+ }
311
+ `;
312
+ await writeFile(themeCssPath, cssContent, "utf8");
313
+
314
+ // Ensure it's imported in globals.css
315
+ let globalsContent = await readFile(globalsPath, "utf8");
316
+ if (!globalsContent.includes("tempjs-theme.css")) {
317
+ // Prepend import at the top of the file
318
+ globalsContent = `@import "./tempjs-theme.css";\n` + globalsContent;
319
+ await writeFile(globalsPath, globalsContent, "utf8");
320
+ }
321
+ } else {
322
+ console.warn("Could not locate globals.css file in the project. CSS variables and font imports were not applied.");
323
+ }
324
+
325
+ // 3. Find and update site.ts
326
+ const siteTsPath = await findSiteTs(targetDir);
327
+ if (siteTsPath) {
328
+ let siteContent = await readFile(siteTsPath, "utf8");
329
+ const colorsRegex = /colors:\s*\{[\s\S]*?\}/;
330
+ const replacement = `colors: {
331
+ primary: "${theme.colors.primary}",
332
+ primaryHover: "${theme.colors.primaryHover}",
333
+ accent: "${theme.colors.accent}",
334
+ accentDark: "${theme.colors.accentDark}",
335
+ accentLight: "${theme.colors.accentLight}",
336
+ textMain: "${theme.colors.textMain}",
337
+ textMuted: "${theme.colors.textMuted}",
338
+ bgMain: "${theme.colors.bgMain}",
339
+ bgLight: "${theme.colors.bgLight}",
340
+ bgCard: "${theme.colors.bgCard}",
341
+ footerBg: "${theme.colors.footerBg}",
342
+ ctaPrimary: "${theme.colors.ctaPrimary}",
343
+ ctaPrimaryHover: "${theme.colors.ctaPrimaryHover}",
344
+ }`;
345
+ if (colorsRegex.test(siteContent)) {
346
+ siteContent = siteContent.replace(colorsRegex, replacement);
347
+ await writeFile(siteTsPath, siteContent, "utf8");
348
+ }
349
+ }
350
+
351
+ console.log(`\nSuccessfully applied theme "${theme.name}" and font pairing "${font.name}".`);
352
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navneet_25/tempjs",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "CLI to instantiate website project templates from a single GitHub repository",
5
5
  "type": "module",
6
6
  "bin": {