bananahub 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bananahub contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # bananahub
2
+
3
+ Template manager for [Nanobanana](https://github.com/nano-banana-hub/nanobanana) — the agent-native Gemini image workflow.
4
+
5
+ Install, manage, and share prompt or workflow modules for the Nanobanana Claude Code workflow. BananaHub keeps the base skill lean and lets reusable prompt structures and guided SOPs travel as installable units.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g bananahub
11
+ ```
12
+
13
+ Or run directly with npx:
14
+
15
+ ```bash
16
+ npx bananahub <command>
17
+ ```
18
+
19
+ ## Requirements
20
+
21
+ - Node.js >= 18.0.0
22
+
23
+ ## Commands
24
+
25
+ ### `add <user/repo[/path/to/template]>`
26
+
27
+ Install template(s) from a GitHub repository, a specific template directory, or a known template collection.
28
+
29
+ ```bash
30
+ bananahub add user/nanobanana-cyberpunk
31
+ bananahub add nano-banana-hub/nanobanana/cute-sticker
32
+ bananahub add user/multi-template-repo --template portrait
33
+ ```
34
+
35
+ Options:
36
+ - `--template <name>` — Install a specific template from a multi-template directory or known collection
37
+ - `--all` — Install all templates from a multi-template directory or known collection
38
+
39
+ ### `remove <template-id>`
40
+
41
+ Uninstall an installed template.
42
+
43
+ ```bash
44
+ bananahub remove cyberpunk
45
+ ```
46
+
47
+ ### `list`
48
+
49
+ List all installed templates.
50
+
51
+ ```bash
52
+ bananahub list
53
+ ```
54
+
55
+ ### `update [template-id]`
56
+
57
+ Update one or all installed templates.
58
+
59
+ ```bash
60
+ bananahub update cyberpunk # update a specific template
61
+ bananahub update # update all templates
62
+ ```
63
+
64
+ ### `info <template-id>`
65
+
66
+ Show details about an installed template (metadata, version, source).
67
+
68
+ ```bash
69
+ bananahub info cyberpunk
70
+ ```
71
+
72
+ ### `search <keyword>`
73
+
74
+ Search the hub catalog for prompt or workflow templates.
75
+
76
+ ```bash
77
+ bananahub search portrait
78
+ bananahub search logo --curated
79
+ ```
80
+
81
+ Options:
82
+ - `--limit <n>` — Limit the number of results (default: 8, max: 20)
83
+ - `--curated` — Search only curated templates
84
+ - `--discovered` — Search only discovered templates
85
+
86
+ ### `trending`
87
+
88
+ Show recent install trends from the BananaHub API.
89
+
90
+ ```bash
91
+ bananahub trending
92
+ bananahub trending --period 24h
93
+ ```
94
+
95
+ Options:
96
+ - `--period <24h|7d>` — Trending window (default: `7d`)
97
+ - `--limit <n>` — Limit the number of results (default: 10, max: 20)
98
+
99
+ ### `init`
100
+
101
+ Scaffold a new prompt or workflow template project in the current directory.
102
+
103
+ ```bash
104
+ bananahub init
105
+ bananahub init --type workflow
106
+ ```
107
+
108
+ ### `validate [path]`
109
+
110
+ Validate a template directory against the Nanobanana template spec.
111
+
112
+ ```bash
113
+ bananahub validate ./my-template
114
+ bananahub validate # validates current directory
115
+ ```
116
+
117
+ ### `registry rebuild`
118
+
119
+ Rebuild the local registry index from installed templates.
120
+
121
+ ```bash
122
+ bananahub registry rebuild
123
+ ```
124
+
125
+ ## Global Options
126
+
127
+ | Flag | Description |
128
+ |------|-------------|
129
+ | `--help`, `-h` | Show help message |
130
+ | `--version`, `-v` | Show version |
131
+
132
+ ## Template Format
133
+
134
+ A valid Nanobanana template directory must contain a `template.md` file with YAML frontmatter at its root. Templates may be `type: prompt` or `type: workflow`, and may live as:
135
+
136
+ - a single-template repository with `template.md` at repo root
137
+ - a multi-template repository with `bananahub.json` plus per-template subdirectories
138
+ - a known collection layout such as `references/templates/<template-id>/template.md`
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { bold, dim, cyan, yellow } from '../lib/color.js';
4
+
5
+ const VERSION = '0.1.0';
6
+
7
+ const HELP = `
8
+ ${bold('bananahub')} ${dim(`v${VERSION}`)} — Template manager for Nanobanana
9
+
10
+ ${bold('USAGE')}
11
+ bananahub <command> [options]
12
+
13
+ ${bold('COMMANDS')}
14
+ ${cyan('add')} <user/repo[/path/to/template]> Install template(s) from a GitHub repo
15
+ --template <name> Pick one template from a multi-template directory
16
+ --all Install all templates from a collection
17
+ ${cyan('remove')} <template-id> Uninstall a template
18
+ ${cyan('list')} List installed templates
19
+ ${cyan('update')} [template-id] Update one or all installed templates
20
+ ${cyan('info')} <template-id> Show template details
21
+ ${cyan('search')} <keyword> [--limit N] [--curated|--discovered]
22
+ Search hub for templates
23
+ ${cyan('trending')} [--period 24h|7d] [--limit N]
24
+ Show trending templates
25
+ ${cyan('init')} Scaffold a new prompt or workflow template project
26
+ ${cyan('validate')} [path] Validate a template directory
27
+ ${cyan('registry')} rebuild Rebuild local registry index
28
+
29
+ ${bold('OPTIONS')}
30
+ --help, -h Show this help message
31
+ --version, -v Show version
32
+
33
+ ${bold('EXAMPLES')}
34
+ bananahub add user/nanobanana-cyberpunk
35
+ bananahub add nano-banana-hub/nanobanana/cute-sticker
36
+ bananahub add user/multi-template-repo --template portrait
37
+ bananahub search logo --curated
38
+ bananahub trending --period 7d
39
+ bananahub list
40
+ bananahub validate ./my-template
41
+ bananahub init
42
+ bananahub init --type workflow
43
+ `;
44
+
45
+ async function main() {
46
+ const args = process.argv.slice(2);
47
+ const command = args[0];
48
+ const cmdArgs = args.slice(1);
49
+
50
+ if (!command || command === '--help' || command === '-h') {
51
+ console.log(HELP);
52
+ return;
53
+ }
54
+
55
+ if (command === '--version' || command === '-v') {
56
+ console.log(VERSION);
57
+ return;
58
+ }
59
+
60
+ switch (command) {
61
+ case 'add': {
62
+ const { addCommand } = await import('../lib/commands/add.js');
63
+ await addCommand(cmdArgs);
64
+ break;
65
+ }
66
+ case 'remove': {
67
+ const { removeCommand } = await import('../lib/commands/remove.js');
68
+ await removeCommand(cmdArgs);
69
+ break;
70
+ }
71
+ case 'list': {
72
+ const { listCommand } = await import('../lib/commands/list.js');
73
+ await listCommand();
74
+ break;
75
+ }
76
+ case 'update': {
77
+ const { updateCommand } = await import('../lib/commands/update.js');
78
+ await updateCommand(cmdArgs);
79
+ break;
80
+ }
81
+ case 'info': {
82
+ const { infoCommand } = await import('../lib/commands/info.js');
83
+ await infoCommand(cmdArgs);
84
+ break;
85
+ }
86
+ case 'search': {
87
+ const { searchCommand } = await import('../lib/commands/search.js');
88
+ await searchCommand(cmdArgs);
89
+ break;
90
+ }
91
+ case 'trending': {
92
+ const { trendingCommand } = await import('../lib/commands/search.js');
93
+ await trendingCommand(cmdArgs);
94
+ break;
95
+ }
96
+ case 'init': {
97
+ const { initCommand } = await import('../lib/commands/init.js');
98
+ await initCommand(cmdArgs);
99
+ break;
100
+ }
101
+ case 'validate': {
102
+ const { validateCommand } = await import('../lib/commands/validate-cmd.js');
103
+ await validateCommand(cmdArgs);
104
+ break;
105
+ }
106
+ case 'registry': {
107
+ const { registryCommand } = await import('../lib/commands/registry-cmd.js');
108
+ await registryCommand(cmdArgs);
109
+ break;
110
+ }
111
+ default:
112
+ console.error(yellow(` Unknown command: "${command}"`));
113
+ console.log(HELP);
114
+ process.exitCode = 1;
115
+ }
116
+ }
117
+
118
+ main().catch((err) => {
119
+ console.error(err.message || err);
120
+ process.exit(1);
121
+ });
package/lib/color.js ADDED
@@ -0,0 +1,11 @@
1
+ // Minimal ANSI color helpers — no dependencies
2
+ const esc = (code) => `\x1b[${code}m`;
3
+ const wrap = (code, reset) => (s) => `${esc(code)}${s}${esc(reset)}`;
4
+
5
+ export const bold = wrap(1, 22);
6
+ export const dim = wrap(2, 22);
7
+ export const red = wrap(31, 39);
8
+ export const green = wrap(32, 39);
9
+ export const yellow = wrap(33, 39);
10
+ export const cyan = wrap(36, 39);
11
+ export const gray = wrap(90, 39);
@@ -0,0 +1,376 @@
1
+ import { access, cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { createGunzip } from 'node:zlib';
5
+ import { Readable } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { extract } from 'tar';
8
+ import { downloadTarball, getDefaultBranchInfo, getLatestSha } from '../github.js';
9
+ import { validateTemplate } from '../validate.js';
10
+ import { rebuildRegistry } from '../registry.js';
11
+ import { TEMPLATES_DIR, CLI_VERSION, HUB_API } from '../constants.js';
12
+ import { bold, green, red, yellow, cyan, dim } from '../color.js';
13
+
14
+ const KNOWN_TEMPLATE_ROOTS = ['references/templates', 'templates'];
15
+
16
+ export async function addCommand(args) {
17
+ const target = parseInstallTarget(args[0]);
18
+ if (!target) {
19
+ console.error(red('Usage: bananahub add <user/repo[/path/to/template]> [--template <name>] [--all]'));
20
+ process.exit(1);
21
+ }
22
+
23
+ const templateFlag = args.indexOf('--template');
24
+ const specificTemplate = templateFlag !== -1 ? args[templateFlag + 1] : null;
25
+ const installAll = args.includes('--all');
26
+ const { repo, requestedPath } = target;
27
+
28
+ console.log(dim(`Resolving ${repo}${requestedPath ? `/${requestedPath}` : ''}...`));
29
+
30
+ let branchInfo;
31
+ try {
32
+ branchInfo = await getDefaultBranchInfo(repo);
33
+ } catch (error) {
34
+ console.error(red(`Error: ${error.message}`));
35
+ process.exit(1);
36
+ }
37
+
38
+ const sha = await getLatestSha(repo, branchInfo.branch);
39
+
40
+ console.log(dim('Downloading...'));
41
+ let tarBuffer;
42
+ try {
43
+ tarBuffer = await downloadTarball(repo, branchInfo.branch);
44
+ } catch (error) {
45
+ console.error(red(`Error: ${error.message}`));
46
+ process.exit(1);
47
+ }
48
+
49
+ const tmpDir = await mkdtemp(join(tmpdir(), 'bananahub-'));
50
+
51
+ try {
52
+ await pipeline(
53
+ Readable.from(tarBuffer),
54
+ createGunzip(),
55
+ extract({ cwd: tmpDir, strip: 1 })
56
+ );
57
+
58
+ const templateDirs = await resolveTemplateDirs({
59
+ tmpDir,
60
+ repo,
61
+ requestedPath,
62
+ specificTemplate,
63
+ installAll
64
+ });
65
+
66
+ if (templateDirs.length === 0) {
67
+ return;
68
+ }
69
+
70
+ let installed = 0;
71
+
72
+ for (const template of templateDirs) {
73
+ const result = await validateTemplate(template.path);
74
+ if (!result.valid) {
75
+ console.error(red(`\nValidation failed for ${template.relativePath || repo}:`));
76
+ for (const error of result.errors) {
77
+ console.error(red(` - ${error}`));
78
+ }
79
+ continue;
80
+ }
81
+
82
+ for (const warning of result.warnings) {
83
+ console.log(yellow(` Warning: ${warning}`));
84
+ }
85
+
86
+ const id = result.meta.id || template.name || repo.split('/')[1];
87
+ const destDir = join(TEMPLATES_DIR, id);
88
+ await rm(destDir, { recursive: true, force: true });
89
+ await mkdir(destDir, { recursive: true });
90
+ await cp(template.path, destDir, { recursive: true });
91
+
92
+ const installTarget = buildInstallTarget(branchInfo.fullName, template.relativePath);
93
+ const source = {
94
+ repo: branchInfo.fullName,
95
+ ref: branchInfo.branch,
96
+ sha: sha || '',
97
+ template_path: template.relativePath || '',
98
+ install_target: installTarget,
99
+ installed_at: new Date().toISOString(),
100
+ version: result.meta.version || '0.0.0',
101
+ cli_version: CLI_VERSION
102
+ };
103
+ await writeFile(join(destDir, '.source.json'), JSON.stringify(source, null, 2));
104
+
105
+ console.log(green(`\n Installed: ${bold(id)} v${result.meta.version || '0.0.0'}`));
106
+ console.log(dim(` Source: ${installTarget}`));
107
+ if (result.meta.tags?.length) {
108
+ console.log(dim(` Tags: ${result.meta.tags.join(', ')}`));
109
+ }
110
+ console.log(cyan(`\n Use: /nanobanana use ${id}\n`));
111
+
112
+ trackInstall(branchInfo.fullName, id, template.relativePath, installTarget).catch(() => {});
113
+ installed += 1;
114
+ }
115
+
116
+ if (installed > 0) {
117
+ await rebuildRegistry();
118
+ }
119
+ } finally {
120
+ await rm(tmpDir, { recursive: true, force: true });
121
+ }
122
+ }
123
+
124
+ function parseInstallTarget(target) {
125
+ if (!target) {
126
+ return null;
127
+ }
128
+
129
+ const segments = target.split('/').filter(Boolean);
130
+ if (segments.length < 2) {
131
+ return null;
132
+ }
133
+
134
+ return {
135
+ repo: `${segments[0]}/${segments[1]}`,
136
+ requestedPath: segments.slice(2).join('/') || ''
137
+ };
138
+ }
139
+
140
+ async function resolveTemplateDirs({ tmpDir, repo, requestedPath, specificTemplate, installAll }) {
141
+ if (requestedPath) {
142
+ const requestedMatches = await findTargetsFromRequestedPath(tmpDir, requestedPath, specificTemplate, installAll);
143
+ if (requestedMatches.length > 0) {
144
+ return requestedMatches;
145
+ }
146
+
147
+ console.error(red(`Error: Could not resolve template path "${requestedPath}" inside ${repo}.`));
148
+ process.exit(1);
149
+ }
150
+
151
+ const rootTemplate = await getTemplateDir(tmpDir, '');
152
+ if (rootTemplate) {
153
+ return [rootTemplate];
154
+ }
155
+
156
+ const rootManifestTargets = await getManifestTargets(tmpDir, '', specificTemplate, installAll);
157
+ if (rootManifestTargets) {
158
+ return rootManifestTargets;
159
+ }
160
+
161
+ const collectionTargets = await findTargetsFromKnownCollections(tmpDir, specificTemplate, installAll);
162
+ if (collectionTargets.length > 0) {
163
+ return collectionTargets;
164
+ }
165
+
166
+ const collections = await listKnownCollections(tmpDir);
167
+ if (collections.length > 0) {
168
+ printCollectionHelp(repo, collections);
169
+ return [];
170
+ }
171
+
172
+ console.error(red('Error: Repository has no template.md, bananahub.json, or known template collection.'));
173
+ process.exit(1);
174
+ }
175
+
176
+ async function findTargetsFromRequestedPath(tmpDir, requestedPath, specificTemplate, installAll) {
177
+ const directCandidates = buildRequestedPathCandidates(requestedPath);
178
+
179
+ for (const candidate of directCandidates) {
180
+ const directTemplate = await getTemplateDir(tmpDir, candidate);
181
+ if (directTemplate) {
182
+ return [directTemplate];
183
+ }
184
+
185
+ const manifestTargets = await getManifestTargets(tmpDir, candidate, specificTemplate, installAll);
186
+ if (manifestTargets) {
187
+ return manifestTargets;
188
+ }
189
+ }
190
+
191
+ return [];
192
+ }
193
+
194
+ async function findTargetsFromKnownCollections(tmpDir, specificTemplate, installAll) {
195
+ const collections = await listKnownCollections(tmpDir);
196
+ if (collections.length === 0) {
197
+ return [];
198
+ }
199
+
200
+ if (specificTemplate) {
201
+ for (const collection of collections) {
202
+ const match = collection.templates.find((template) => template.name === specificTemplate);
203
+ if (match) {
204
+ return [match];
205
+ }
206
+ }
207
+
208
+ const choices = collections.flatMap((collection) => collection.templates.map((template) => template.name));
209
+ console.error(red(`Template "${specificTemplate}" not found. Available: ${choices.join(', ')}`));
210
+ process.exit(1);
211
+ }
212
+
213
+ if (installAll) {
214
+ return collections.flatMap((collection) => collection.templates);
215
+ }
216
+
217
+ return [];
218
+ }
219
+
220
+ function buildRequestedPathCandidates(requestedPath) {
221
+ const candidates = [trimSlashes(requestedPath)];
222
+
223
+ if (!requestedPath.includes('/')) {
224
+ for (const root of KNOWN_TEMPLATE_ROOTS) {
225
+ candidates.push(`${root}/${trimSlashes(requestedPath)}`);
226
+ }
227
+ }
228
+
229
+ return [...new Set(candidates.filter(Boolean))];
230
+ }
231
+
232
+ async function getManifestTargets(tmpDir, baseRelativePath, specificTemplate, installAll) {
233
+ const baseDir = resolveWithinTemp(tmpDir, baseRelativePath);
234
+ let manifest;
235
+
236
+ try {
237
+ const raw = await readFile(join(baseDir, 'bananahub.json'), 'utf8');
238
+ manifest = JSON.parse(raw);
239
+ } catch {
240
+ return null;
241
+ }
242
+
243
+ if (!manifest || !Array.isArray(manifest.templates) || manifest.templates.length === 0) {
244
+ console.error(red(`Error: Invalid bananahub.json at ${baseRelativePath || '.'}`));
245
+ process.exit(1);
246
+ }
247
+
248
+ const templates = manifest.templates.map((name) => ({
249
+ path: resolveWithinTemp(tmpDir, joinRelative(baseRelativePath, name)),
250
+ name,
251
+ relativePath: joinRelative(baseRelativePath, name)
252
+ }));
253
+
254
+ if (specificTemplate) {
255
+ const match = templates.find((template) => template.name === specificTemplate);
256
+ if (!match) {
257
+ console.error(red(`Template "${specificTemplate}" not found in ${baseRelativePath || 'repo root'}. Available: ${manifest.templates.join(', ')}`));
258
+ process.exit(1);
259
+ }
260
+ return [match];
261
+ }
262
+
263
+ if (installAll) {
264
+ return templates;
265
+ }
266
+
267
+ console.log(`\nMulti-template directory at ${cyan(baseRelativePath || '/')} with ${manifest.templates.length} templates:`);
268
+ for (const templateName of manifest.templates) {
269
+ console.log(` - ${templateName}`);
270
+ }
271
+ console.log(`\nUse ${cyan('--all')} to install all, ${cyan('--template <name>')} to pick one, or install directly via ${cyan(`bananahub add <user/repo>/${manifest.templates[0]}`)}.`);
272
+ return [];
273
+ }
274
+
275
+ async function listKnownCollections(tmpDir) {
276
+ const collections = [];
277
+
278
+ for (const root of KNOWN_TEMPLATE_ROOTS) {
279
+ const templates = await listTemplatesInCollection(tmpDir, root);
280
+ if (templates.length > 0) {
281
+ collections.push({ root, templates });
282
+ }
283
+ }
284
+
285
+ return collections;
286
+ }
287
+
288
+ async function listTemplatesInCollection(tmpDir, baseRelativePath) {
289
+ const baseDir = resolveWithinTemp(tmpDir, baseRelativePath);
290
+
291
+ try {
292
+ const entries = await readdir(baseDir, { withFileTypes: true });
293
+ const templates = [];
294
+
295
+ for (const entry of entries) {
296
+ if (!entry.isDirectory()) {
297
+ continue;
298
+ }
299
+
300
+ const relativePath = joinRelative(baseRelativePath, entry.name);
301
+ const templateDir = await getTemplateDir(tmpDir, relativePath);
302
+ if (templateDir) {
303
+ templates.push(templateDir);
304
+ }
305
+ }
306
+
307
+ return templates;
308
+ } catch {
309
+ return [];
310
+ }
311
+ }
312
+
313
+ async function getTemplateDir(tmpDir, relativePath) {
314
+ const dir = resolveWithinTemp(tmpDir, relativePath);
315
+ try {
316
+ await access(join(dir, 'template.md'));
317
+ return {
318
+ path: dir,
319
+ name: basename(relativePath) || null,
320
+ relativePath: trimSlashes(relativePath)
321
+ };
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
326
+
327
+ function resolveWithinTemp(tmpDir, relativePath) {
328
+ return relativePath ? join(tmpDir, trimSlashes(relativePath)) : tmpDir;
329
+ }
330
+
331
+ function trimSlashes(value) {
332
+ return String(value || '').replace(/^\/+|\/+$/g, '');
333
+ }
334
+
335
+ function joinRelative(baseRelativePath, child) {
336
+ const parts = [trimSlashes(baseRelativePath), trimSlashes(child)].filter(Boolean);
337
+ return parts.join('/');
338
+ }
339
+
340
+ function buildInstallTarget(repo, relativePath) {
341
+ return relativePath ? `${repo}/${relativePath}` : repo;
342
+ }
343
+
344
+ function printCollectionHelp(repo, collections) {
345
+ const allTemplates = collections.flatMap((collection) => collection.templates);
346
+ console.log(`\nTemplate collections discovered in ${bold(repo)}:`);
347
+ for (const collection of collections) {
348
+ console.log(dim(` ${collection.root}`));
349
+ for (const template of collection.templates) {
350
+ console.log(` - ${template.name}`);
351
+ }
352
+ }
353
+ console.log();
354
+ console.log(dim(` Install one: bananahub add ${repo}/${allTemplates[0].name}`));
355
+ console.log(dim(` Or pick explicitly: bananahub add ${repo} --template <name>`));
356
+ console.log(dim(' Or install everything: bananahub add ' + repo + ' --all'));
357
+ console.log();
358
+ }
359
+
360
+ async function trackInstall(repo, templateId, templatePath = '', installTarget = '') {
361
+ try {
362
+ await fetch(`${HUB_API}/installs`, {
363
+ method: 'POST',
364
+ headers: { 'Content-Type': 'application/json' },
365
+ body: JSON.stringify({
366
+ repo,
367
+ template_id: templateId,
368
+ template_path: templatePath || '',
369
+ install_target: installTarget || '',
370
+ cli_version: CLI_VERSION,
371
+ timestamp: new Date().toISOString()
372
+ }),
373
+ signal: AbortSignal.timeout(3000)
374
+ });
375
+ } catch {}
376
+ }