@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 +10 -7
- package/cli/fetch.js +125 -167
- package/cli/index.js +44 -9
- package/cli/theme-manager.js +352 -0
- package/package.json +1 -1
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
|
|
148
|
+
`tempjs hotel` does **not** run `git clone` and does **not** call the GitHub REST API per file.
|
|
145
149
|
|
|
146
|
-
1. The CLI
|
|
147
|
-
2. It
|
|
148
|
-
3.
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
* @
|
|
45
|
+
* @param {string} destPath
|
|
74
46
|
*/
|
|
75
|
-
async function
|
|
76
|
-
const
|
|
77
|
-
const
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
headers: buildHeaders(),
|
|
53
|
+
redirect: "follow",
|
|
54
|
+
});
|
|
103
55
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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 {
|
|
123
|
-
* @param {string} templateRoot
|
|
78
|
+
* @param {string} templateDirectory
|
|
124
79
|
*/
|
|
125
|
-
async function
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
96
|
+
const archiveRoot = lines[0].replace(/\/$/, "").split("/")[0];
|
|
97
|
+
const templateArchivePath = `${archiveRoot}/${repo.templatesPath}/${templateDirectory}`;
|
|
98
|
+
const templatePrefix = `${templateArchivePath}/`;
|
|
136
99
|
|
|
137
|
-
const
|
|
138
|
-
|
|
100
|
+
const hasTemplate = lines.some(
|
|
101
|
+
(line) => line === templatePrefix || line.startsWith(templatePrefix)
|
|
102
|
+
);
|
|
139
103
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
104
|
+
if (!hasTemplate) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Template directory not found in repository: ${repo.templatesPath}/${templateDirectory}`
|
|
107
|
+
);
|
|
144
108
|
}
|
|
145
109
|
|
|
146
|
-
const
|
|
110
|
+
const stripComponents = templateArchivePath.split("/").length;
|
|
147
111
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
127
|
+
await removeSkippedFiles(destDir);
|
|
155
128
|
}
|
|
156
129
|
|
|
157
130
|
/**
|
|
158
|
-
*
|
|
159
|
-
* @param {string}
|
|
160
|
-
* @param {string} targetPath
|
|
131
|
+
* Remove files that must never appear in generated projects.
|
|
132
|
+
* @param {string} dir
|
|
161
133
|
*/
|
|
162
|
-
async function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
149
|
+
if (shouldSkipFileName(entry.name)) {
|
|
150
|
+
await rm(fullPath, { force: true });
|
|
151
|
+
}
|
|
175
152
|
}
|
|
153
|
+
}
|
|
176
154
|
|
|
177
|
-
|
|
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}
|
|
170
|
+
* @param {string} name
|
|
182
171
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
257
|
+
if (command === "theme" || command === "font") {
|
|
258
|
+
const targetDir = process.cwd();
|
|
259
|
+
const currentConfig = getSavedConfig(targetDir);
|
|
238
260
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|