tempora-cli 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # tempora-cli
2
+
3
+ A fast, language-agnostic CLI that bootstraps your project from a curated vault of community templates — no setup, no guesswork.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g tempora-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ **Guided mode:**
14
+ ```bash
15
+ tempora init
16
+ ```
17
+
18
+ **Direct mode:**
19
+ ```bash
20
+ tempora init next-tailwind my-app
21
+ ```
22
+
23
+ **Scaffold into current folder:**
24
+ ```bash
25
+ tempora init next-tailwind .
26
+ ```
27
+
28
+ **Browse template info:**
29
+ ```bash
30
+ tempora info next-tailwind
31
+ ```
32
+
33
+ ## Requirements
34
+
35
+ - Node.js 18+
36
+ - git
37
+ - npm
38
+
39
+ ## Links
40
+
41
+ - [Documentation](https://tempora.vercel.app)
42
+ - [GitHub](https://github.com/DidIrb/tempora)
43
+ - [npm](https://www.npmjs.com/package/tempora-cli)
44
+
45
+ ## License
46
+
47
+ MIT © Tempora
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import { createRequire as createRequire2 } from "module";
6
+
7
+ // src/commands/init.ts
8
+ import ora from "ora";
9
+
10
+ // src/utils/logger.ts
11
+ import pc from "picocolors";
12
+ var logger = {
13
+ info: (msg) => console.log(pc.cyan(" info ") + msg),
14
+ success: (msg) => console.log(pc.green(" \u2714 ") + msg),
15
+ warn: (msg) => console.log(pc.yellow(" warn ") + msg),
16
+ error: (msg) => console.error(pc.red(" \u2716 ") + msg),
17
+ log: (msg) => console.log(" " + msg)
18
+ };
19
+
20
+ // src/utils/versionCheck.ts
21
+ import { createRequire } from "module";
22
+ import pc2 from "picocolors";
23
+ var require2 = createRequire(import.meta.url);
24
+ var { version: current } = require2("../package.json");
25
+ async function checkVersion() {
26
+ try {
27
+ const res = await fetch("https://registry.npmjs.org/tempora-cli/latest", {
28
+ signal: AbortSignal.timeout(3e3)
29
+ });
30
+ if (!res.ok) return;
31
+ const { version: latest } = await res.json();
32
+ if (!current || !latest || current === latest) return;
33
+ const [curMajor, curMinor] = current.split(".").map(Number);
34
+ const [latMajor, latMinor] = latest.split(".").map(Number);
35
+ const isMinorOrMajor = latMajor > curMajor || latMajor === curMajor && latMinor > curMinor;
36
+ if (!isMinorOrMajor) return;
37
+ console.log("");
38
+ console.log(" " + pc2.bgYellow(pc2.black(" UPDATE ")) + " " + pc2.dim(current) + " \u2192 " + pc2.green(pc2.bold(latest)));
39
+ console.log(" " + pc2.dim("Run: ") + pc2.cyan("npm install -g tempora-cli"));
40
+ console.log(" " + pc2.dim("https://www.npmjs.com/package/tempora-cli"));
41
+ console.log("");
42
+ } catch {
43
+ }
44
+ }
45
+
46
+ // src/utils/antiOverwrite.ts
47
+ import fs from "fs";
48
+ import path from "path";
49
+ import inquirer from "inquirer";
50
+ async function resolveTargetDir(directory) {
51
+ const target = path.resolve(process.cwd(), directory ?? ".");
52
+ if (!fs.existsSync(target)) {
53
+ return { targetDir: target, overwrite: false };
54
+ }
55
+ const entries = fs.readdirSync(target).filter((f) => f !== ".git" && f !== ".DS_Store");
56
+ if (entries.length === 0) {
57
+ return { targetDir: target, overwrite: false };
58
+ }
59
+ logger.warn(`Directory "${path.basename(target)}" is not empty.`);
60
+ const { proceed } = await inquirer.prompt([
61
+ {
62
+ type: "confirm",
63
+ name: "proceed",
64
+ message: "Proceeding may overwrite existing files. Continue?",
65
+ default: false
66
+ }
67
+ ]);
68
+ if (!proceed) {
69
+ logger.error("Aborted. No files were changed.");
70
+ return null;
71
+ }
72
+ return { targetDir: target, overwrite: true };
73
+ }
74
+
75
+ // src/utils/downloader.ts
76
+ import fs2 from "fs";
77
+ import path2 from "path";
78
+ import { execSync } from "child_process";
79
+ import { fileURLToPath } from "url";
80
+
81
+ // src/config.ts
82
+ var org = "DidIrb";
83
+ var repo = "tempora";
84
+ var branch = "main";
85
+ var config = {
86
+ github: {
87
+ org,
88
+ repo,
89
+ branch,
90
+ apiBase: `https://api.github.com/repos/${org}/${repo}/contents`,
91
+ rawBase: `https://raw.githubusercontent.com/${org}/${repo}/${branch}`,
92
+ registryUrl: `https://raw.githubusercontent.com/${org}/${repo}/${branch}/registry.json`
93
+ },
94
+ docs: {
95
+ url: "https://tempora.dev/templates"
96
+ }
97
+ };
98
+
99
+ // src/utils/downloader.ts
100
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
101
+ function findLocalTemplatesDir() {
102
+ let current2 = __dirname;
103
+ for (let i = 0; i < 6; i++) {
104
+ const candidate = path2.join(current2, "templates");
105
+ if (fs2.existsSync(candidate)) return candidate;
106
+ current2 = path2.dirname(current2);
107
+ }
108
+ return null;
109
+ }
110
+ function copyDirLocal(srcDir, destDir, overwrite) {
111
+ fs2.mkdirSync(destDir, { recursive: true });
112
+ for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
113
+ const srcPath = path2.join(srcDir, entry.name);
114
+ const destPath = path2.join(destDir, entry.name);
115
+ if (entry.isDirectory()) {
116
+ copyDirLocal(srcPath, destPath, overwrite);
117
+ } else {
118
+ if (!overwrite && fs2.existsSync(destPath)) continue;
119
+ fs2.copyFileSync(srcPath, destPath);
120
+ }
121
+ }
122
+ }
123
+ function cloneTemplateSparse(templatePath, targetDir, overwrite) {
124
+ const repoUrl = `https://github.com/${config.github.org}/${config.github.repo}.git`;
125
+ const tmpDir = path2.join(targetDir, ".tempora-tmp");
126
+ try {
127
+ execSync("git --version", { stdio: "pipe" });
128
+ } catch {
129
+ throw new Error(
130
+ "git is not installed or not available on your PATH.\nTempora requires git to download templates. Install it from https://git-scm.com and try again."
131
+ );
132
+ }
133
+ try {
134
+ fs2.mkdirSync(tmpDir, { recursive: true });
135
+ try {
136
+ execSync(`git clone --filter=blob:none --sparse --depth=1 ${repoUrl} .`, {
137
+ cwd: tmpDir,
138
+ stdio: "pipe"
139
+ });
140
+ } catch {
141
+ throw new Error(
142
+ "Could not connect to GitHub to download the template.\nCheck your internet connection and try again."
143
+ );
144
+ }
145
+ try {
146
+ execSync(`git sparse-checkout set ${templatePath}`, {
147
+ cwd: tmpDir,
148
+ stdio: "pipe"
149
+ });
150
+ } catch {
151
+ throw new Error(
152
+ `Failed to checkout template path "${templatePath}" from the repository.
153
+ The template may have been moved or removed. Run "tempora init" to browse available templates.`
154
+ );
155
+ }
156
+ const clonedTemplatePath = path2.join(tmpDir, templatePath);
157
+ if (!fs2.existsSync(clonedTemplatePath)) {
158
+ throw new Error(`Template path "${templatePath}" not found in repository.`);
159
+ }
160
+ copyDirLocal(clonedTemplatePath, targetDir, overwrite);
161
+ } finally {
162
+ fs2.rmSync(tmpDir, { recursive: true, force: true });
163
+ }
164
+ }
165
+ async function downloadTemplate(template, targetDir, overwrite, spinner) {
166
+ const localTemplatesDir = findLocalTemplatesDir();
167
+ if (localTemplatesDir) {
168
+ if (spinner) spinner.text = "Copying template from local...";
169
+ await new Promise((r) => setTimeout(r, 50));
170
+ const relativeParts = template.path.replace(/^templates\//, "").split("/");
171
+ const localSrc = path2.join(localTemplatesDir, ...relativeParts);
172
+ if (fs2.existsSync(localSrc)) {
173
+ copyDirLocal(localSrc, targetDir, overwrite);
174
+ return;
175
+ }
176
+ }
177
+ if (spinner) spinner.text = `Downloading ${template.name} from GitHub...`;
178
+ await new Promise((r) => setTimeout(r, 50));
179
+ cloneTemplateSparse(template.path, targetDir, overwrite);
180
+ }
181
+
182
+ // src/utils/postInstall.ts
183
+ import path3 from "path";
184
+ var DEFAULT_NEXT_STEPS = ["pnpm install", "pnpm dev"];
185
+ function printPostInstall(template, targetDir) {
186
+ const isCwd = path3.resolve(targetDir) === path3.resolve(process.cwd());
187
+ const dirName = isCwd ? null : path3.basename(targetDir);
188
+ const steps = template.nextSteps ?? DEFAULT_NEXT_STEPS;
189
+ logger.log(`
190
+ ${template.description}
191
+ `);
192
+ logger.log(" Next steps:\n");
193
+ if (dirName) {
194
+ logger.log(` cd ${dirName}`);
195
+ }
196
+ for (const step of steps) {
197
+ logger.log(` ${step}`);
198
+ }
199
+ logger.log("");
200
+ }
201
+
202
+ // src/utils/registry.ts
203
+ import fs3 from "fs";
204
+ import path4 from "path";
205
+ import { fileURLToPath as fileURLToPath2 } from "url";
206
+ var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
207
+ function loadRegistry() {
208
+ const registryPath = path4.resolve(__dirname2, "./registry.json");
209
+ if (!fs3.existsSync(registryPath)) {
210
+ throw new Error("Registry not found. Please rebuild the CLI with npm run build.");
211
+ }
212
+ const raw = fs3.readFileSync(registryPath, "utf-8");
213
+ return JSON.parse(raw);
214
+ }
215
+
216
+ // src/utils/guided.ts
217
+ import inquirer2 from "inquirer";
218
+ var MAX_RESULTS = 4;
219
+ async function runGuidedSelection(registry) {
220
+ const languages = Object.keys(registry.byLanguage);
221
+ if (languages.length === 0) {
222
+ logger.error("No templates found in registry.");
223
+ logger.log(`Browse templates at ${config.docs.url}`);
224
+ return null;
225
+ }
226
+ const { language } = await inquirer2.prompt([{
227
+ type: "list",
228
+ name: "language",
229
+ message: "What language?",
230
+ choices: languages
231
+ }]);
232
+ const languageIds = registry.byLanguage[language] ?? [];
233
+ const availableCategories = Object.entries(registry.byCategory).filter(([, ids]) => ids.some((id) => languageIds.includes(id))).map(([cat]) => cat);
234
+ if (availableCategories.length === 0) {
235
+ logger.error(`No templates found for language: ${language}`);
236
+ logger.log(`Browse templates at ${config.docs.url}`);
237
+ return null;
238
+ }
239
+ const { category } = await inquirer2.prompt([{
240
+ type: "list",
241
+ name: "category",
242
+ message: "What category?",
243
+ choices: availableCategories
244
+ }]);
245
+ const categoryIds = registry.byCategory[category] ?? [];
246
+ const afterCategory = languageIds.filter((id) => categoryIds.includes(id));
247
+ const availableLibraries = Object.entries(registry.byLibrary).filter(([, ids]) => ids.some((id) => afterCategory.includes(id))).map(([lib]) => lib);
248
+ if (availableLibraries.length === 0) {
249
+ logger.error(`No templates found for ${language} / ${category}`);
250
+ logger.log(`Browse templates at ${config.docs.url}`);
251
+ return null;
252
+ }
253
+ const { library } = await inquirer2.prompt([{
254
+ type: "list",
255
+ name: "library",
256
+ message: "What library or framework?",
257
+ choices: availableLibraries
258
+ }]);
259
+ const libraryIds = registry.byLibrary[library] ?? [];
260
+ const matchedIds = afterCategory.filter((id) => libraryIds.includes(id));
261
+ if (matchedIds.length === 0) {
262
+ logger.error(`No templates found for ${language} / ${category} / ${library}`);
263
+ logger.log(`Browse templates at ${config.docs.url}`);
264
+ return null;
265
+ }
266
+ const hasMore = matchedIds.length > MAX_RESULTS;
267
+ const visibleIds = matchedIds.slice(0, MAX_RESULTS);
268
+ const choices = visibleIds.map((id) => ({
269
+ name: `${registry.templates[id].name} \u2014 ${registry.templates[id].description}`,
270
+ value: id
271
+ }));
272
+ if (hasMore) {
273
+ logger.log(`
274
+ Showing ${MAX_RESULTS} of ${matchedIds.length} templates.`);
275
+ logger.log(` See all at ${config.docs.url}
276
+ `);
277
+ }
278
+ const { templateId } = await inquirer2.prompt([{
279
+ type: "list",
280
+ name: "templateId",
281
+ message: "Pick a template:",
282
+ choices
283
+ }]);
284
+ return registry.templates[templateId] ?? null;
285
+ }
286
+
287
+ // src/commands/init.ts
288
+ function registerInitCommand(program2) {
289
+ program2.command("init [template] [directory]").description("Scaffold a new project from a Tempora template").action(async (template, directory) => {
290
+ const spinner = ora();
291
+ try {
292
+ const registry = loadRegistry();
293
+ let resolvedTemplate = template;
294
+ let resolvedDirectory = directory;
295
+ if (!resolvedTemplate) {
296
+ const entry2 = await runGuidedSelection(registry);
297
+ if (!entry2) return;
298
+ resolvedTemplate = entry2.id;
299
+ }
300
+ const entry = registry.templates[resolvedTemplate];
301
+ if (!entry) {
302
+ logger.error(`Template "${resolvedTemplate}" not found.`);
303
+ logger.log(`Browse all templates at ${config.docs.url}`);
304
+ process.exit(1);
305
+ }
306
+ const result = await resolveTargetDir(resolvedDirectory);
307
+ if (!result) return;
308
+ const { targetDir, overwrite } = result;
309
+ await new Promise((r) => setTimeout(r, 100));
310
+ spinner.start();
311
+ await downloadTemplate(entry, targetDir, overwrite, spinner);
312
+ spinner.succeed(`${entry.name} scaffolded successfully!`);
313
+ printPostInstall(entry, targetDir);
314
+ await checkVersion();
315
+ } catch (err) {
316
+ spinner.stop();
317
+ logger.error(err instanceof Error ? err.message : "Something went wrong.");
318
+ process.exit(1);
319
+ }
320
+ });
321
+ }
322
+
323
+ // src/commands/info.ts
324
+ function registerInfoCommand(program2) {
325
+ program2.command("info <template>").description("Show details about a specific template").action(async (template) => {
326
+ try {
327
+ const registry = loadRegistry();
328
+ const entry = registry.templates[template];
329
+ if (!entry) {
330
+ logger.error(`Template "${template}" not found.`);
331
+ logger.log("Run tempora init to browse available templates.");
332
+ process.exit(1);
333
+ }
334
+ logger.log("");
335
+ logger.log(` ${entry.name}`);
336
+ logger.log(` ${entry.description}`);
337
+ logger.log("");
338
+ logger.log(` Language ${entry.language}`);
339
+ logger.log(` Category ${entry.category}`);
340
+ logger.log(` Library ${entry.library}`);
341
+ logger.log(` Version ${entry.version}`);
342
+ logger.log(` Tags ${entry.tags.join(", ")}`);
343
+ logger.log("");
344
+ if (entry.nextSteps && entry.nextSteps.length > 0) {
345
+ logger.log(" Next steps after scaffolding:");
346
+ for (const step of entry.nextSteps) {
347
+ logger.log(` ${step}`);
348
+ }
349
+ logger.log("");
350
+ }
351
+ await checkVersion();
352
+ } catch (err) {
353
+ logger.error(err instanceof Error ? err.message : "Something went wrong.");
354
+ process.exit(1);
355
+ }
356
+ });
357
+ }
358
+
359
+ // src/index.ts
360
+ var require3 = createRequire2(import.meta.url);
361
+ var { version } = require3("../package.json");
362
+ var program = new Command();
363
+ program.name("tempora").description("Scaffold projects from curated templates").version(version, "-v, --version", "Show the current version").addHelpText("after", `
364
+ Examples:
365
+ $ tempora init next-tailwind my-app
366
+ $ tempora init next-tailwind .
367
+ $ tempora init
368
+ $ tempora info next-tailwind
369
+ `);
370
+ registerInitCommand(program);
371
+ registerInfoCommand(program);
372
+ program.parse(process.argv);
@@ -0,0 +1,93 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "updatedAt": "2026-06-28T01:10:10.949Z",
4
+ "templates": {
5
+ "angular-starter": {
6
+ "id": "angular-starter",
7
+ "name": "Angular Starter",
8
+ "language": "typescript",
9
+ "category": "frontend",
10
+ "library": "angular",
11
+ "description": "Angular 17 standalone components with TypeScript and Angular CLI preconfigured.",
12
+ "tags": [
13
+ "angular",
14
+ "typescript",
15
+ "frontend",
16
+ "spa"
17
+ ],
18
+ "version": "1.0.0",
19
+ "nextSteps": [
20
+ "npm install",
21
+ "ng serve"
22
+ ],
23
+ "path": "templates/typescript/frontend/angular/angular-starter"
24
+ },
25
+ "next-tailwind": {
26
+ "id": "next-tailwind",
27
+ "name": "Next.js + Tailwind",
28
+ "language": "typescript",
29
+ "category": "frontend",
30
+ "library": "nextjs",
31
+ "description": "Next.js 14 app router with Tailwind CSS and Prettier preconfigured.",
32
+ "tags": [
33
+ "nextjs",
34
+ "tailwind",
35
+ "typescript",
36
+ "react"
37
+ ],
38
+ "version": "1.0.0",
39
+ "nextSteps": [
40
+ "pnpm install",
41
+ "pnpm dev"
42
+ ],
43
+ "path": "templates/typescript/frontend/nextjs/next-tailwind"
44
+ },
45
+ "nextjs-tailwind": {
46
+ "id": "nextjs-tailwind",
47
+ "name": "Next.js + Tailwind",
48
+ "language": "typescript",
49
+ "category": "fullstack",
50
+ "library": "tailwind",
51
+ "description": "Next.js 14 app router with Tailwind CSS and Prettier preconfigured.",
52
+ "tags": [
53
+ "nextjs",
54
+ "tailwind",
55
+ "typescript",
56
+ "react"
57
+ ],
58
+ "version": "1.0.0",
59
+ "nextSteps": [
60
+ "pnpm install",
61
+ "pnpm dev"
62
+ ],
63
+ "path": "templates/typescript/nextjs-tailwind"
64
+ }
65
+ },
66
+ "byLanguage": {
67
+ "typescript": [
68
+ "angular-starter",
69
+ "next-tailwind",
70
+ "nextjs-tailwind"
71
+ ]
72
+ },
73
+ "byCategory": {
74
+ "frontend": [
75
+ "angular-starter",
76
+ "next-tailwind"
77
+ ],
78
+ "fullstack": [
79
+ "nextjs-tailwind"
80
+ ]
81
+ },
82
+ "byLibrary": {
83
+ "angular": [
84
+ "angular-starter"
85
+ ],
86
+ "nextjs": [
87
+ "next-tailwind"
88
+ ],
89
+ "tailwind": [
90
+ "nextjs-tailwind"
91
+ ]
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tempora-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Tempora CLI — scaffold projects from curated templates",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,7 +14,8 @@
14
14
  "files": [
15
15
  "bin",
16
16
  "dist",
17
- "registry.json"
17
+ "registry.json",
18
+ "README.md"
18
19
  ],
19
20
  "dependencies": {
20
21
  "chalk": "^5.3.0",
package/registry.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "updatedAt": "2026-06-28T01:10:10.949Z",
4
+ "templates": {
5
+ "angular-starter": {
6
+ "id": "angular-starter",
7
+ "name": "Angular Starter",
8
+ "language": "typescript",
9
+ "category": "frontend",
10
+ "library": "angular",
11
+ "description": "Angular 17 standalone components with TypeScript and Angular CLI preconfigured.",
12
+ "tags": [
13
+ "angular",
14
+ "typescript",
15
+ "frontend",
16
+ "spa"
17
+ ],
18
+ "version": "1.0.0",
19
+ "nextSteps": [
20
+ "npm install",
21
+ "ng serve"
22
+ ],
23
+ "path": "templates/typescript/frontend/angular/angular-starter"
24
+ },
25
+ "next-tailwind": {
26
+ "id": "next-tailwind",
27
+ "name": "Next.js + Tailwind",
28
+ "language": "typescript",
29
+ "category": "frontend",
30
+ "library": "nextjs",
31
+ "description": "Next.js 14 app router with Tailwind CSS and Prettier preconfigured.",
32
+ "tags": [
33
+ "nextjs",
34
+ "tailwind",
35
+ "typescript",
36
+ "react"
37
+ ],
38
+ "version": "1.0.0",
39
+ "nextSteps": [
40
+ "pnpm install",
41
+ "pnpm dev"
42
+ ],
43
+ "path": "templates/typescript/frontend/nextjs/next-tailwind"
44
+ },
45
+ "nextjs-tailwind": {
46
+ "id": "nextjs-tailwind",
47
+ "name": "Next.js + Tailwind",
48
+ "language": "typescript",
49
+ "category": "fullstack",
50
+ "library": "tailwind",
51
+ "description": "Next.js 14 app router with Tailwind CSS and Prettier preconfigured.",
52
+ "tags": [
53
+ "nextjs",
54
+ "tailwind",
55
+ "typescript",
56
+ "react"
57
+ ],
58
+ "version": "1.0.0",
59
+ "nextSteps": [
60
+ "pnpm install",
61
+ "pnpm dev"
62
+ ],
63
+ "path": "templates/typescript/nextjs-tailwind"
64
+ }
65
+ },
66
+ "byLanguage": {
67
+ "typescript": [
68
+ "angular-starter",
69
+ "next-tailwind",
70
+ "nextjs-tailwind"
71
+ ]
72
+ },
73
+ "byCategory": {
74
+ "frontend": [
75
+ "angular-starter",
76
+ "next-tailwind"
77
+ ],
78
+ "fullstack": [
79
+ "nextjs-tailwind"
80
+ ]
81
+ },
82
+ "byLibrary": {
83
+ "angular": [
84
+ "angular-starter"
85
+ ],
86
+ "nextjs": [
87
+ "next-tailwind"
88
+ ],
89
+ "tailwind": [
90
+ "nextjs-tailwind"
91
+ ]
92
+ }
93
+ }