bananahub 0.1.0 → 0.1.1

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # bananahub
2
2
 
3
- Template manager for [Nanobanana](https://github.com/nano-banana-hub/nanobanana) — the agent-native Gemini image workflow.
3
+ Template manager for [BananaHub Skill](https://github.com/bananahub-ai/banana-hub-skill) — the agent-native Gemini image workflow.
4
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.
5
+ Install, manage, and share prompt or workflow modules for the BananaHub Skill workflow. BananaHub keeps the runtime lean and lets reusable prompt structures and guided SOPs travel as installable units.
6
6
 
7
7
  ## Installation
8
8
 
@@ -27,8 +27,8 @@ npx bananahub <command>
27
27
  Install template(s) from a GitHub repository, a specific template directory, or a known template collection.
28
28
 
29
29
  ```bash
30
- bananahub add user/nanobanana-cyberpunk
31
- bananahub add nano-banana-hub/nanobanana/cute-sticker
30
+ bananahub add user/bananahub-cyberpunk
31
+ bananahub add bananahub-ai/banana-hub-skill/cute-sticker
32
32
  bananahub add user/multi-template-repo --template portrait
33
33
  ```
34
34
 
@@ -107,7 +107,7 @@ bananahub init --type workflow
107
107
 
108
108
  ### `validate [path]`
109
109
 
110
- Validate a template directory against the Nanobanana template spec.
110
+ Validate a template directory against the BananaHub template spec.
111
111
 
112
112
  ```bash
113
113
  bananahub validate ./my-template
@@ -131,7 +131,7 @@ bananahub registry rebuild
131
131
 
132
132
  ## Template Format
133
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:
134
+ A valid BananaHub 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
135
 
136
136
  - a single-template repository with `template.md` at repo root
137
137
  - a multi-template repository with `bananahub.json` plus per-template subdirectories
package/bin/bananahub.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { bold, dim, cyan, yellow } from '../lib/color.js';
4
4
 
5
- const VERSION = '0.1.0';
5
+ const VERSION = '0.1.1';
6
6
 
7
7
  const HELP = `
8
- ${bold('bananahub')} ${dim(`v${VERSION}`)} — Template manager for Nanobanana
8
+ ${bold('bananahub')} ${dim(`v${VERSION}`)} — Template manager for BananaHub Skill
9
9
 
10
10
  ${bold('USAGE')}
11
11
  bananahub <command> [options]
@@ -31,8 +31,8 @@ ${bold('OPTIONS')}
31
31
  --version, -v Show version
32
32
 
33
33
  ${bold('EXAMPLES')}
34
- bananahub add user/nanobanana-cyberpunk
35
- bananahub add nano-banana-hub/nanobanana/cute-sticker
34
+ bananahub add user/bananahub-cyberpunk
35
+ bananahub add bananahub-ai/banana-hub-skill/cute-sticker
36
36
  bananahub add user/multi-template-repo --template portrait
37
37
  bananahub search logo --curated
38
38
  bananahub trending --period 7d
@@ -8,7 +8,7 @@ import { extract } from 'tar';
8
8
  import { downloadTarball, getDefaultBranchInfo, getLatestSha } from '../github.js';
9
9
  import { validateTemplate } from '../validate.js';
10
10
  import { rebuildRegistry } from '../registry.js';
11
- import { TEMPLATES_DIR, CLI_VERSION, HUB_API } from '../constants.js';
11
+ import { TEMPLATES_DIR, CLI_VERSION, HUB_API, SKILL_COMMAND } from '../constants.js';
12
12
  import { bold, green, red, yellow, cyan, dim } from '../color.js';
13
13
 
14
14
  const KNOWN_TEMPLATE_ROOTS = ['references/templates', 'templates'];
@@ -107,7 +107,7 @@ export async function addCommand(args) {
107
107
  if (result.meta.tags?.length) {
108
108
  console.log(dim(` Tags: ${result.meta.tags.join(', ')}`));
109
109
  }
110
- console.log(cyan(`\n Use: /nanobanana use ${id}\n`));
110
+ console.log(cyan(`\n Use: ${SKILL_COMMAND} use ${id}\n`));
111
111
 
112
112
  trackInstall(branchInfo.fullName, id, template.relativePath, installTarget).catch(() => {});
113
113
  installed += 1;
@@ -1,9 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { TEMPLATES_DIR, SOURCE_FILE } from '../constants.js';
3
+ import { SOURCE_FILE, SKILL_COMMAND } from '../constants.js';
4
4
  import { parseFrontmatter } from '../frontmatter.js';
5
5
  import { bold, dim, cyan, green } from '../color.js';
6
6
  import { red } from '../color.js';
7
+ import { resolveInstalledTemplateDir } from '../paths.js';
7
8
 
8
9
  export async function infoCommand(args) {
9
10
  const id = args[0];
@@ -13,8 +14,9 @@ export async function infoCommand(args) {
13
14
  }
14
15
 
15
16
  let content;
17
+ const templateDir = await resolveInstalledTemplateDir(id);
16
18
  try {
17
- content = await readFile(join(TEMPLATES_DIR, id, 'template.md'), 'utf8');
19
+ content = await readFile(join(templateDir, 'template.md'), 'utf8');
18
20
  } catch {
19
21
  console.error(red(`Template "${id}" is not installed.`));
20
22
  process.exit(1);
@@ -28,7 +30,7 @@ export async function infoCommand(args) {
28
30
 
29
31
  let source = null;
30
32
  try {
31
- const raw = await readFile(join(TEMPLATES_DIR, id, SOURCE_FILE), 'utf8');
33
+ const raw = await readFile(join(templateDir, SOURCE_FILE), 'utf8');
32
34
  source = JSON.parse(raw);
33
35
  } catch { /* ok */ }
34
36
 
@@ -70,5 +72,5 @@ export async function infoCommand(args) {
70
72
  if (source.sha) console.log(dim(` SHA: ${source.sha.slice(0, 8)}`));
71
73
  }
72
74
 
73
- console.log(green(`\n Use: /nanobanana use ${id}\n`));
75
+ console.log(green(`\n Use: ${SKILL_COMMAND} use ${id}\n`));
74
76
  }
@@ -124,7 +124,7 @@ Your prompt here with {{variable|default value}} slots
124
124
  function buildReadme(titleEn, id, profile, type) {
125
125
  return `# ${titleEn}
126
126
 
127
- A Nanobanana ${type} template for ${profile} workflows.
127
+ A BananaHub ${type} template for ${profile} workflows.
128
128
 
129
129
  ## Install
130
130
 
@@ -1,8 +1,7 @@
1
1
  import { rm, access } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { TEMPLATES_DIR } from '../constants.js';
4
2
  import { rebuildRegistry } from '../registry.js';
5
3
  import { bold, green, red } from '../color.js';
4
+ import { resolveInstalledTemplateDirs } from '../paths.js';
6
5
 
7
6
  export async function removeCommand(args) {
8
7
  const id = args[0];
@@ -11,15 +10,20 @@ export async function removeCommand(args) {
11
10
  process.exit(1);
12
11
  }
13
12
 
14
- const dir = join(TEMPLATES_DIR, id);
15
- try {
16
- await access(dir);
17
- } catch {
13
+ const dirs = await resolveInstalledTemplateDirs(id);
14
+ if (dirs.length === 0) {
18
15
  console.error(red(`Template "${id}" is not installed.`));
19
16
  process.exit(1);
20
17
  }
21
18
 
22
- await rm(dir, { recursive: true, force: true });
19
+ for (const dir of dirs) {
20
+ try {
21
+ await access(dir);
22
+ await rm(dir, { recursive: true, force: true });
23
+ } catch {
24
+ // ignore races and partial legacy cleanup
25
+ }
26
+ }
23
27
  await rebuildRegistry();
24
28
  console.log(green(` Removed: ${bold(id)}`));
25
29
  }
@@ -1,9 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { TEMPLATES_DIR, SOURCE_FILE } from '../constants.js';
3
+ import { SOURCE_FILE } from '../constants.js';
4
4
  import { loadRegistry } from '../registry.js';
5
5
  import { addCommand } from './add.js';
6
6
  import { bold, dim, yellow, green, red } from '../color.js';
7
+ import { resolveInstalledTemplateDir } from '../paths.js';
7
8
 
8
9
  export async function updateCommand(args) {
9
10
  const targetId = args[0];
@@ -26,8 +27,9 @@ export async function updateCommand(args) {
26
27
 
27
28
  for (const template of toUpdate) {
28
29
  let source;
30
+ const templateDir = await resolveInstalledTemplateDir(template.id);
29
31
  try {
30
- const raw = await readFile(join(TEMPLATES_DIR, template.id, SOURCE_FILE), 'utf8');
32
+ const raw = await readFile(join(templateDir, SOURCE_FILE), 'utf8');
31
33
  source = JSON.parse(raw);
32
34
  } catch {
33
35
  console.log(yellow(` Skipping ${bold(template.id)}: no source info (locally created?)`));
package/lib/constants.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const CLI_VERSION = '0.1.0';
5
- export const TEMPLATES_DIR = join(homedir(), '.config', 'nanobanana', 'templates');
4
+ export const CLI_VERSION = '0.1.1';
5
+ export const CONFIG_HOME = join(homedir(), '.config', 'bananahub');
6
+ export const LEGACY_CONFIG_HOME = join(homedir(), '.config', 'nanobanana');
7
+ export const TEMPLATES_DIR = join(CONFIG_HOME, 'templates');
8
+ export const LEGACY_TEMPLATES_DIR = join(LEGACY_CONFIG_HOME, 'templates');
6
9
  export const REGISTRY_FILE = '.registry.json';
7
10
  export const SOURCE_FILE = '.source.json';
8
11
  export const GITHUB_API = 'https://api.github.com';
9
12
  export const HUB_API = 'https://bananahub-api.zhan9kun.workers.dev/api';
10
- export const HUB_SITE = 'https://nano-banana-hub.github.io';
13
+ export const HUB_SITE = 'https://bananahub-ai.github.io';
11
14
  export const HUB_CATALOG_URL = `${HUB_SITE}/catalog.json`;
15
+ export const SKILL_COMMAND = '/bananahub';
16
+ export const LEGACY_SKILL_COMMAND = '/nanobanana';
12
17
 
13
18
  export const VALID_PROFILES = [
14
19
  'photo', 'illustration', 'diagram', 'text-heavy',
package/lib/github.js CHANGED
@@ -70,7 +70,7 @@ export async function getLatestSha(repo, ref = 'HEAD') {
70
70
  function ghHeaders() {
71
71
  const headers = {
72
72
  'Accept': 'application/vnd.github.v3+json',
73
- 'User-Agent': 'bananahub-cli/0.1.0'
73
+ 'User-Agent': 'bananahub-cli/0.1.1'
74
74
  };
75
75
  if (process.env.GITHUB_TOKEN) {
76
76
  headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
package/lib/paths.js ADDED
@@ -0,0 +1,62 @@
1
+ import { access, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { TEMPLATES_DIR, LEGACY_TEMPLATES_DIR } from './constants.js';
4
+
5
+ async function dirExists(path) {
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export async function ensurePrimaryTemplatesDir() {
15
+ await mkdir(TEMPLATES_DIR, { recursive: true });
16
+ return TEMPLATES_DIR;
17
+ }
18
+
19
+ export async function getTemplateRoots() {
20
+ const roots = [];
21
+ const primaryExists = await dirExists(TEMPLATES_DIR);
22
+ const legacyExists = await dirExists(LEGACY_TEMPLATES_DIR);
23
+
24
+ if (primaryExists) {
25
+ roots.push(TEMPLATES_DIR);
26
+ }
27
+
28
+ if (legacyExists) {
29
+ roots.push(LEGACY_TEMPLATES_DIR);
30
+ }
31
+
32
+ if (roots.length === 0) {
33
+ await ensurePrimaryTemplatesDir();
34
+ roots.push(TEMPLATES_DIR);
35
+ }
36
+
37
+ return roots;
38
+ }
39
+
40
+ export async function resolveInstalledTemplateDir(id) {
41
+ for (const root of await getTemplateRoots()) {
42
+ const candidate = join(root, id);
43
+ if (await dirExists(candidate)) {
44
+ return candidate;
45
+ }
46
+ }
47
+
48
+ return join(TEMPLATES_DIR, id);
49
+ }
50
+
51
+ export async function resolveInstalledTemplateDirs(id) {
52
+ const dirs = [];
53
+
54
+ for (const root of await getTemplateRoots()) {
55
+ const candidate = join(root, id);
56
+ if (await dirExists(candidate)) {
57
+ dirs.push(candidate);
58
+ }
59
+ }
60
+
61
+ return dirs;
62
+ }
package/lib/registry.js CHANGED
@@ -1,47 +1,55 @@
1
- import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
1
+ import { readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { TEMPLATES_DIR, REGISTRY_FILE } from './constants.js';
4
4
  import { parseFrontmatter } from './frontmatter.js';
5
+ import { ensurePrimaryTemplatesDir, getTemplateRoots } from './paths.js';
5
6
 
6
7
  /**
7
8
  * Rebuild .registry.json by scanning all installed template directories.
8
9
  */
9
10
  export async function rebuildRegistry() {
10
- await mkdir(TEMPLATES_DIR, { recursive: true });
11
- const entries = await readdir(TEMPLATES_DIR, { withFileTypes: true });
11
+ await ensurePrimaryTemplatesDir();
12
+ const roots = await getTemplateRoots();
12
13
  const templates = [];
14
+ const seenIds = new Set();
13
15
 
14
- for (const entry of entries) {
15
- if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
16
- const tmplPath = join(TEMPLATES_DIR, entry.name, 'template.md');
17
- try {
18
- const content = await readFile(tmplPath, 'utf8');
19
- const fm = parseFrontmatter(content);
20
- if (!fm) continue;
16
+ for (const root of roots) {
17
+ const entries = await readdir(root, { withFileTypes: true });
21
18
 
22
- let source = null;
19
+ for (const entry of entries) {
20
+ if (!entry.isDirectory() || entry.name.startsWith('.') || seenIds.has(entry.name)) continue;
21
+
22
+ const tmplPath = join(root, entry.name, 'template.md');
23
23
  try {
24
- const srcJson = await readFile(join(TEMPLATES_DIR, entry.name, '.source.json'), 'utf8');
25
- source = JSON.parse(srcJson);
26
- } catch { /* no source info */ }
24
+ const content = await readFile(tmplPath, 'utf8');
25
+ const fm = parseFrontmatter(content);
26
+ if (!fm) continue;
27
+
28
+ let source = null;
29
+ try {
30
+ const srcJson = await readFile(join(root, entry.name, '.source.json'), 'utf8');
31
+ source = JSON.parse(srcJson);
32
+ } catch { /* no source info */ }
27
33
 
28
- templates.push({
29
- id: fm.id || entry.name,
30
- type: fm.type || 'prompt',
31
- title: fm.title || '',
32
- title_en: fm.title_en || '',
33
- author: fm.author || '',
34
- profile: fm.profile || 'general',
35
- tags: fm.tags || [],
36
- difficulty: fm.difficulty || 'beginner',
37
- aspect: fm.aspect || '',
38
- models: Array.isArray(fm.models) ? fm.models.map(m => m.name || m) : [],
39
- source: source?.repo || '',
40
- version: fm.version || '0.0.0',
41
- installed_at: source?.installed_at || ''
42
- });
43
- } catch {
44
- // skip unreadable templates
34
+ templates.push({
35
+ id: fm.id || entry.name,
36
+ type: fm.type || 'prompt',
37
+ title: fm.title || '',
38
+ title_en: fm.title_en || '',
39
+ author: fm.author || '',
40
+ profile: fm.profile || 'general',
41
+ tags: fm.tags || [],
42
+ difficulty: fm.difficulty || 'beginner',
43
+ aspect: fm.aspect || '',
44
+ models: Array.isArray(fm.models) ? fm.models.map((m) => m.name || m) : [],
45
+ source: source?.repo || '',
46
+ version: fm.version || '0.0.0',
47
+ installed_at: source?.installed_at || ''
48
+ });
49
+ seenIds.add(entry.name);
50
+ } catch {
51
+ // skip unreadable templates
52
+ }
45
53
  }
46
54
  }
47
55
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "bananahub",
3
- "version": "0.1.0",
4
- "description": "Template manager for Nanobanana — Gemini image generation skill",
3
+ "version": "0.1.1",
4
+ "description": "Template manager for BananaHub Skill installable Gemini workflow modules",
5
5
  "type": "module",
6
6
  "bin": {
7
- "bananahub": "./bin/bananahub.js"
7
+ "bananahub": "bin/bananahub.js"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=18.0.0"
@@ -26,11 +26,11 @@
26
26
  "license": "MIT",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/nano-banana-hub/bananahub.git"
29
+ "url": "git+https://github.com/bananahub-ai/bananahub.git"
30
30
  },
31
- "homepage": "https://github.com/nano-banana-hub/bananahub#readme",
31
+ "homepage": "https://github.com/bananahub-ai/bananahub#readme",
32
32
  "bugs": {
33
- "url": "https://github.com/nano-banana-hub/bananahub/issues"
33
+ "url": "https://github.com/bananahub-ai/bananahub/issues"
34
34
  },
35
35
  "dependencies": {
36
36
  "tar": "^7.0.0"