@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 +237 -0
- package/cli/config.js +78 -0
- package/cli/copy.js +164 -0
- package/cli/fetch.js +243 -0
- package/cli/index.js +249 -0
- package/package.json +24 -0
- package/templates.json +20 -0
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
|
+
}
|