@yohei1996/nanobanana 1.0.0 → 1.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/README.md +40 -27
- package/bin/nanobanana.js +32 -6
- package/package.json +11 -4
- package/skills/nanobanana/SKILL.md +42 -0
- package/skills/nanobanana/scripts/generate-image.js +197 -0
- package/src/index.js +57 -1
package/README.md
CHANGED
|
@@ -1,57 +1,70 @@
|
|
|
1
1
|
# @yohei1996/nanobanana
|
|
2
2
|
|
|
3
|
-
🍌 Nano Banana - Gemini Pro
|
|
3
|
+
🍌 Nano Banana - Gemini Pro画像生成 CLI & Claude Codeスキル
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Gemini Proを使用した高品質画像生成(4K対応)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## インストール
|
|
8
|
+
|
|
9
|
+
### スキルとしてインストール(推奨)
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
|
|
12
|
+
# プロジェクトディレクトリで実行
|
|
13
|
+
npx @yohei1996/nanobanana install
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
これで `.claude/skills/nanobanana/` にスキルがインストールされます。
|
|
17
|
+
|
|
18
|
+
### グローバルインストール
|
|
14
19
|
|
|
15
20
|
```bash
|
|
16
|
-
|
|
21
|
+
npm install -g @yohei1996/nanobanana
|
|
17
22
|
```
|
|
18
23
|
|
|
19
|
-
##
|
|
24
|
+
## セットアップ
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
Gemini APIキーを設定:
|
|
22
27
|
|
|
23
28
|
```bash
|
|
24
29
|
export GEMINI_API_KEY=your-api-key
|
|
25
30
|
```
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
または VSCode/Cursor設定で `bizAgent-task-kanban.geminiApiKey` を設定。
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
## 使い方
|
|
35
|
+
|
|
36
|
+
### Claude Codeスキルとして使用
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
nanobanana
|
|
38
|
+
```
|
|
39
|
+
/nanobanana 猫がピアノを弾いている水彩画
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### CLIから直接実行
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 基本
|
|
46
|
+
npx @yohei1996/nanobanana "A cat playing piano in watercolor style"
|
|
35
47
|
|
|
36
|
-
#
|
|
37
|
-
nanobanana "
|
|
48
|
+
# 4K解像度
|
|
49
|
+
npx @yohei1996/nanobanana "Landscape photo" --resolution 4k
|
|
38
50
|
|
|
39
|
-
#
|
|
40
|
-
nanobanana "
|
|
51
|
+
# アスペクト比指定
|
|
52
|
+
npx @yohei1996/nanobanana "Logo design" --aspect 1:1 --output ./logo.png
|
|
41
53
|
```
|
|
42
54
|
|
|
43
|
-
##
|
|
55
|
+
## オプション
|
|
44
56
|
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
| `--
|
|
49
|
-
| `--
|
|
50
|
-
| `--
|
|
57
|
+
| オプション | 説明 | 例 |
|
|
58
|
+
|-----------|------|-----|
|
|
59
|
+
| `install [dir]` | スキルをインストール | `install .` |
|
|
60
|
+
| `--aspect <ratio>` | アスペクト比 | `1:1`, `16:9`, `9:16`, `4:3` |
|
|
61
|
+
| `--resolution <r>` | 解像度 | `4k`, `2k`, `1k` |
|
|
62
|
+
| `--output <path>` | 出力先パス | `./image.png` |
|
|
63
|
+
| `--api-key <key>` | Gemini APIキー | |
|
|
51
64
|
|
|
52
|
-
##
|
|
65
|
+
## 出力先
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
デフォルト: `~/nanobanana-images/`
|
|
55
68
|
|
|
56
69
|
## License
|
|
57
70
|
|
package/bin/nanobanana.js
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const { generateImage } = require('../src/index.js');
|
|
2
|
+
const { generateImage, installSkills } = require('../src/index.js');
|
|
3
3
|
|
|
4
4
|
const args = process.argv.slice(2);
|
|
5
5
|
|
|
6
|
+
// Handle install command
|
|
7
|
+
if (args[0] === 'install') {
|
|
8
|
+
const targetDir = args[1] || process.cwd();
|
|
9
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
10
|
+
|
|
11
|
+
installSkills(targetDir, { force })
|
|
12
|
+
.then((installPath) => {
|
|
13
|
+
console.log('\n使用方法: Claude Codeでスキルを発火してください');
|
|
14
|
+
console.log('例: /nanobanana 猫がピアノを弾いている');
|
|
15
|
+
})
|
|
16
|
+
.catch((err) => {
|
|
17
|
+
console.error('Error:', err.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
7
24
|
console.log(`
|
|
8
25
|
🍌 Nano Banana - Gemini Pro Image Generator
|
|
9
26
|
|
|
10
|
-
Usage:
|
|
27
|
+
Usage:
|
|
28
|
+
nanobanana install [dir] Install skills to .claude/skills/
|
|
29
|
+
nanobanana <prompt> Generate image directly
|
|
11
30
|
|
|
12
|
-
Options:
|
|
31
|
+
Install Options:
|
|
32
|
+
install [dir] Install to specified directory (default: current dir)
|
|
33
|
+
--force, -f Overwrite existing files
|
|
34
|
+
|
|
35
|
+
Generate Options:
|
|
13
36
|
--aspect <ratio> Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, etc.)
|
|
14
37
|
--resolution <r> Resolution (4k, 2k, 1k)
|
|
15
38
|
--output <path> Output file path
|
|
@@ -17,9 +40,12 @@ Options:
|
|
|
17
40
|
--help, -h Show this help message
|
|
18
41
|
|
|
19
42
|
Examples:
|
|
20
|
-
|
|
21
|
-
nanobanana
|
|
22
|
-
|
|
43
|
+
# Install skills
|
|
44
|
+
npx @yohei1996/nanobanana install
|
|
45
|
+
|
|
46
|
+
# Generate image directly
|
|
47
|
+
npx @yohei1996/nanobanana "A cat playing piano in watercolor style"
|
|
48
|
+
npx @yohei1996/nanobanana "Landscape photo" --resolution 4k
|
|
23
49
|
|
|
24
50
|
Environment:
|
|
25
51
|
GEMINI_API_KEY Your Gemini API key
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yohei1996/nanobanana",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Gemini Pro image generation CLI - High quality image generation with 4K support",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Gemini Pro image generation CLI & Claude Code skill - High quality image generation with 4K support",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nanobanana": "./bin/nanobanana.js"
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"image-generation",
|
|
16
16
|
"ai",
|
|
17
17
|
"cli",
|
|
18
|
-
"4k"
|
|
18
|
+
"4k",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"skill"
|
|
19
21
|
],
|
|
20
22
|
"author": "yohei1996",
|
|
21
23
|
"license": "MIT",
|
|
@@ -26,5 +28,10 @@
|
|
|
26
28
|
},
|
|
27
29
|
"engines": {
|
|
28
30
|
"node": ">=18.0.0"
|
|
29
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"src/",
|
|
35
|
+
"skills/"
|
|
36
|
+
]
|
|
30
37
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nanobanana
|
|
3
|
+
description: Gemini Pro画像生成(高品質・4K対応)。「画像生成」「画像作成」「イラスト作成」「〜の絵」と言われたら使用。
|
|
4
|
+
argument-hint: [prompt]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Nano Banana 画像生成
|
|
8
|
+
|
|
9
|
+
Gemini Proを使用して高品質な画像を生成する。
|
|
10
|
+
|
|
11
|
+
## 実行方法
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node .claude/skills/nanobanana/scripts/generate-image.js "$ARGUMENTS"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
APIキーはVSCode設定(`bizAgent-task-kanban.geminiApiKey`)または環境変数から自動取得。
|
|
18
|
+
|
|
19
|
+
## オプション
|
|
20
|
+
|
|
21
|
+
| オプション | 説明 | 例 |
|
|
22
|
+
|-----------|------|-----|
|
|
23
|
+
| --aspect | アスペクト比 | --aspect 16:9 |
|
|
24
|
+
| --resolution | 解像度(4k,2k,1k) | --resolution 4k |
|
|
25
|
+
| --output | 出力先パス | --output ./image.png |
|
|
26
|
+
|
|
27
|
+
## 使用例
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 基本(Proモデル)
|
|
31
|
+
node .claude/skills/nanobanana/scripts/generate-image.js "猫がピアノを弾いている水彩画"
|
|
32
|
+
|
|
33
|
+
# 4K高解像度
|
|
34
|
+
node .claude/skills/nanobanana/scripts/generate-image.js "風景写真" --resolution 4k
|
|
35
|
+
|
|
36
|
+
# アスペクト比指定
|
|
37
|
+
node .claude/skills/nanobanana/scripts/generate-image.js "ロゴデザイン" --aspect 1:1 --output ./logo.png
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 出力先
|
|
41
|
+
|
|
42
|
+
デフォルト: `~/nanobanana-images/`
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gemini画像生成スクリプト
|
|
4
|
+
* - Flash: gemini-2.0-flash-exp-image-generation(高速)
|
|
5
|
+
* - Pro: gemini-2.0-flash-preview-image-generation(高品質・4K)
|
|
6
|
+
*
|
|
7
|
+
* 使用例:
|
|
8
|
+
* node generate-image.js "猫" --output ./cat.png
|
|
9
|
+
* node generate-image.js "風景" --pro --resolution 4k
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
const MODELS = {
|
|
16
|
+
flash: "gemini-2.0-flash-exp-image-generation",
|
|
17
|
+
pro: "gemini-3-pro-image-preview",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function getGeminiApiKey() {
|
|
21
|
+
// 1. VSCode/Cursor設定から取得
|
|
22
|
+
const os = require("os");
|
|
23
|
+
const settingsPaths = [
|
|
24
|
+
path.join(os.homedir(), "Library/Application Support/Code/User/settings.json"),
|
|
25
|
+
path.join(os.homedir(), "Library/Application Support/Cursor/User/settings.json"),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const settingsPath of settingsPaths) {
|
|
29
|
+
if (fs.existsSync(settingsPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
32
|
+
const apiKey = settings["bizAgent-task-kanban.geminiApiKey"];
|
|
33
|
+
if (apiKey) return apiKey;
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. 環境変数から取得
|
|
39
|
+
const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
40
|
+
if (envKey) return envKey;
|
|
41
|
+
|
|
42
|
+
throw new Error("Gemini APIキーが見つかりません。VSCode設定またはGEMINI_API_KEY環境変数を設定してください。");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function generateImage(options) {
|
|
46
|
+
const { prompt, aspectRatio, outputPath, model = "flash", resolution } = options;
|
|
47
|
+
|
|
48
|
+
const apiKey = getGeminiApiKey();
|
|
49
|
+
|
|
50
|
+
const modelId = MODELS[model] || MODELS.flash;
|
|
51
|
+
const modelLabel = model === "pro" ? "🚀 Pro" : "⚡ Flash";
|
|
52
|
+
|
|
53
|
+
console.log(`${modelLabel} 画像生成中: "${prompt.substring(0, 50)}..."`);
|
|
54
|
+
if (aspectRatio) console.log(`📐 アスペクト比: ${aspectRatio}`);
|
|
55
|
+
if (resolution) console.log(`📏 解像度: ${resolution}`);
|
|
56
|
+
|
|
57
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${apiKey}`;
|
|
58
|
+
|
|
59
|
+
// プロンプトの強化(Proモデル用)
|
|
60
|
+
let enhancedPrompt = prompt;
|
|
61
|
+
if (model === "pro") {
|
|
62
|
+
if (prompt.length < 50) {
|
|
63
|
+
enhancedPrompt = `Create a high-quality, detailed image: ${prompt}. Pay attention to composition, lighting, and fine details.`;
|
|
64
|
+
}
|
|
65
|
+
if (resolution === "4k") {
|
|
66
|
+
enhancedPrompt += " Render at maximum 4K quality with exceptional detail.";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const requestBody = {
|
|
71
|
+
contents: [{ parts: [{ text: enhancedPrompt }] }],
|
|
72
|
+
generationConfig: {
|
|
73
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// アスペクト比とimageConfigの設定
|
|
78
|
+
if (aspectRatio || resolution) {
|
|
79
|
+
requestBody.generationConfig.imageConfig = {};
|
|
80
|
+
if (aspectRatio) {
|
|
81
|
+
requestBody.generationConfig.imageConfig.aspectRatio = aspectRatio;
|
|
82
|
+
}
|
|
83
|
+
if (resolution) {
|
|
84
|
+
const sizeMap = { "4k": "4K", "2k": "2K", "1k": "1K", high: "1K" };
|
|
85
|
+
requestBody.generationConfig.imageConfig.imageSize = sizeMap[resolution.toLowerCase()] || "1K";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify(requestBody),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const errorText = await response.text();
|
|
97
|
+
throw new Error(`API Error: ${response.status} - ${errorText}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = await response.json();
|
|
101
|
+
|
|
102
|
+
if (!result.candidates || !result.candidates[0]?.content?.parts) {
|
|
103
|
+
console.error("APIレスポンス:", JSON.stringify(result, null, 2));
|
|
104
|
+
throw new Error("画像が生成されませんでした");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const images = [];
|
|
108
|
+
for (const part of result.candidates[0].content.parts) {
|
|
109
|
+
if (part.inlineData && part.inlineData.mimeType?.startsWith("image/")) {
|
|
110
|
+
images.push({
|
|
111
|
+
data: Buffer.from(part.inlineData.data, "base64"),
|
|
112
|
+
mimeType: part.inlineData.mimeType,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (images.length === 0) {
|
|
118
|
+
console.error("パーツ:", JSON.stringify(result.candidates[0].content.parts, null, 2));
|
|
119
|
+
throw new Error("画像が生成されませんでした(画像パーツなし)");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 出力先ディレクトリの決定
|
|
123
|
+
const outputDir =
|
|
124
|
+
outputPath && fs.statSync(outputPath, { throwIfNoEntry: false })?.isDirectory()
|
|
125
|
+
? outputPath
|
|
126
|
+
: outputPath
|
|
127
|
+
? path.dirname(outputPath)
|
|
128
|
+
: path.join(process.env.HOME, "nanobanana-images");
|
|
129
|
+
|
|
130
|
+
if (!fs.existsSync(outputDir)) {
|
|
131
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const savedPaths = [];
|
|
135
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
136
|
+
const prefix = model === "pro" ? "pro_" : "";
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < images.length; i++) {
|
|
139
|
+
const ext = images[i].mimeType === "image/png" ? "png" : "jpg";
|
|
140
|
+
const filename =
|
|
141
|
+
outputPath && !fs.statSync(outputPath, { throwIfNoEntry: false })?.isDirectory()
|
|
142
|
+
? path.basename(outputPath)
|
|
143
|
+
: `${prefix}image_${timestamp}_${i + 1}.${ext}`;
|
|
144
|
+
const fullPath = path.join(outputDir, filename);
|
|
145
|
+
|
|
146
|
+
fs.writeFileSync(fullPath, images[i].data);
|
|
147
|
+
savedPaths.push(fullPath);
|
|
148
|
+
console.log(`✅ 保存: ${fullPath}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return savedPaths;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// CLI実行
|
|
155
|
+
if (require.main === module) {
|
|
156
|
+
const args = process.argv.slice(2);
|
|
157
|
+
if (args.length === 0) {
|
|
158
|
+
console.log(`使用法: node generate-image.js <prompt> [options]
|
|
159
|
+
|
|
160
|
+
オプション:
|
|
161
|
+
--pro Proモデル使用(高品質・4K対応)
|
|
162
|
+
--aspect <ratio> アスペクト比(1:1, 16:9, 9:16, 4:3, 3:4など)
|
|
163
|
+
--resolution <r> 解像度(4k, 2k, 1k, high)※Proモデル推奨
|
|
164
|
+
--output <path> 出力先パス
|
|
165
|
+
|
|
166
|
+
例:
|
|
167
|
+
node generate-image.js "猫がピアノを弾いている"
|
|
168
|
+
node generate-image.js "風景写真" --pro --resolution 4k
|
|
169
|
+
node generate-image.js "ロゴ" --aspect 1:1 --output ./logo.png
|
|
170
|
+
`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const options = { prompt: "", model: "pro" }; // デフォルトでProモデルを使用
|
|
175
|
+
for (let i = 0; i < args.length; i++) {
|
|
176
|
+
if (args[i] === "--pro") {
|
|
177
|
+
options.model = "pro";
|
|
178
|
+
} else if (args[i] === "--aspect" && args[i + 1]) {
|
|
179
|
+
options.aspectRatio = args[++i];
|
|
180
|
+
} else if (args[i] === "--resolution" && args[i + 1]) {
|
|
181
|
+
options.resolution = args[++i];
|
|
182
|
+
} else if (args[i] === "--output" && args[i + 1]) {
|
|
183
|
+
options.outputPath = args[++i];
|
|
184
|
+
} else if (!args[i].startsWith("--")) {
|
|
185
|
+
options.prompt = args[i];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
generateImage(options)
|
|
190
|
+
.then((paths) => console.log("\n生成完了:", paths.join(", ")))
|
|
191
|
+
.catch((err) => {
|
|
192
|
+
console.error("エラー:", err.message);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { generateImage };
|
package/src/index.js
CHANGED
|
@@ -142,4 +142,60 @@ async function generateImage(options) {
|
|
|
142
142
|
return savedPaths;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Install nanobanana skills to target directory
|
|
147
|
+
* @param {string} targetDir - Target directory (default: current working directory)
|
|
148
|
+
* @param {object} options - Installation options
|
|
149
|
+
* @param {boolean} options.force - Overwrite existing files
|
|
150
|
+
* @param {boolean} options.silent - Suppress output
|
|
151
|
+
*/
|
|
152
|
+
async function installSkills(targetDir = process.cwd(), options = {}) {
|
|
153
|
+
const { force = false, silent = false } = options;
|
|
154
|
+
|
|
155
|
+
const skillsSourceDir = path.join(__dirname, '..', 'skills', 'nanobanana');
|
|
156
|
+
const skillsTargetDir = path.join(targetDir, '.claude', 'skills', 'nanobanana');
|
|
157
|
+
|
|
158
|
+
// Check if source exists
|
|
159
|
+
if (!fs.existsSync(skillsSourceDir)) {
|
|
160
|
+
throw new Error(`Skills source not found: ${skillsSourceDir}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create target directory
|
|
164
|
+
if (!fs.existsSync(skillsTargetDir)) {
|
|
165
|
+
fs.mkdirSync(skillsTargetDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Copy files recursively
|
|
169
|
+
const copyDir = (src, dest) => {
|
|
170
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
171
|
+
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const srcPath = path.join(src, entry.name);
|
|
174
|
+
const destPath = path.join(dest, entry.name);
|
|
175
|
+
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
if (!fs.existsSync(destPath)) {
|
|
178
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
copyDir(srcPath, destPath);
|
|
181
|
+
} else {
|
|
182
|
+
if (!force && fs.existsSync(destPath)) {
|
|
183
|
+
if (!silent) console.log(`⏭️ Skipping (exists): ${destPath}`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
fs.copyFileSync(srcPath, destPath);
|
|
187
|
+
if (!silent) console.log(`✅ Installed: ${destPath}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
copyDir(skillsSourceDir, skillsTargetDir);
|
|
193
|
+
|
|
194
|
+
if (!silent) {
|
|
195
|
+
console.log(`\n🍌 nanobanana skills installed to: ${skillsTargetDir}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return skillsTargetDir;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { generateImage, getGeminiApiKey, MODELS, installSkills };
|