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 +21 -0
- package/README.md +142 -0
- package/bin/bananahub.js +121 -0
- package/lib/color.js +11 -0
- package/lib/commands/add.js +376 -0
- package/lib/commands/info.js +74 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/list.js +38 -0
- package/lib/commands/registry-cmd.js +15 -0
- package/lib/commands/remove.js +25 -0
- package/lib/commands/search.js +277 -0
- package/lib/commands/update.js +48 -0
- package/lib/commands/validate-cmd.js +42 -0
- package/lib/constants.js +19 -0
- package/lib/frontmatter.js +130 -0
- package/lib/github.js +79 -0
- package/lib/hub.js +103 -0
- package/lib/registry.js +68 -0
- package/lib/validate.js +236 -0
- package/package.json +38 -0
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
|
package/bin/bananahub.js
ADDED
|
@@ -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
|
+
}
|