agy-plugins-cli 1.1.14
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 +94 -0
- package/dist/config.js +90 -0
- package/dist/fetcher.js +265 -0
- package/dist/hooks-merger.js +89 -0
- package/dist/index.js +388 -0
- package/dist/manager.js +151 -0
- package/dist/mcp-merger.js +88 -0
- package/dist/state.js +89 -0
- package/i18n/ja/README.md +94 -0
- package/i18n/ko/README.md +94 -0
- package/i18n/zh-CN/README.md +94 -0
- package/i18n/zh-TW/README.md +94 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZaunEkko
|
|
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,94 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🚀 Antigravity CLI Package Manager
|
|
4
|
+
**The official, ultra-fast plugin manager for Google Antigravity (agy).**
|
|
5
|
+
|
|
6
|
+
[English](README.md) · [简体中文](i18n/zh-CN/README.md) · [繁體中文](i18n/zh-TW/README.md) · [日本語](i18n/ja/README.md) · [한국어](i18n/ko/README.md)
|
|
7
|
+
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://www.npmjs.com/package/agy-plugins-cli)
|
|
11
|
+
[](https://github.com/ZaunEkko/agy-plugins-cli)
|
|
12
|
+
[](https://github.com/ZaunEkko/agy-plugins-cli/blob/main/LICENSE)
|
|
13
|
+
|
|
14
|
+
> Seamlessly install, update, and manage Antigravity plugins across multiple repositories, with interactive terminal UI, granular namespace support, and secure context merging.
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<br />
|
|
19
|
+
|
|
20
|
+
## ✨ Features
|
|
21
|
+
|
|
22
|
+
- **🌐 Multi-Marketplace Support**: Add and switch between multiple remote GitHub repositories as your plugin sources (e.g. `ZaunEkko/agy-plugins`).
|
|
23
|
+
- **⚡ Hot Updates with Precision**: Fetch the newest `commit` fingerprint from remote repos and perform updates instantly at global, namespace, or individual plugin levels.
|
|
24
|
+
- **🖥️ Interactive TUI**: Browse, check update status, and batch-install plugins via a stunning terminal user interface built with `@clack/prompts`.
|
|
25
|
+
- **🔐 Zero-Config Private Repos**: Natively detects and utilizes your local `gh auth token` to bypass GitHub API rate limits and access enterprise private plugin repositories.
|
|
26
|
+
- **🛡️ Hook & MCP Merging**: Merges remote `hooks.json` and `mcp.json` into your Antigravity config. Hook arrays are deduplicated, and same-name MCP servers are replaced with a warning so updates stay explicit.
|
|
27
|
+
|
|
28
|
+
<br />
|
|
29
|
+
|
|
30
|
+
## 📦 Installation
|
|
31
|
+
|
|
32
|
+
To install `agy-plugins-cli` globally on your machine, simply run:
|
|
33
|
+
|
|
34
|
+
Requires Node.js `^22.13.0 || >=23.5.0`.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g agy-plugins-cli@latest
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
*Now you can use the `agy-plugin` command anywhere in your terminal!*
|
|
41
|
+
|
|
42
|
+
<br />
|
|
43
|
+
|
|
44
|
+
## 🚀 Quick Start
|
|
45
|
+
|
|
46
|
+
### 1. Add and Explore Marketplaces
|
|
47
|
+
Link a GitHub repository to your local registry and explore its plugins interactively.
|
|
48
|
+
```bash
|
|
49
|
+
# Add a repository as a marketplace
|
|
50
|
+
agy-plugin marketplace add ZaunEkko/agy-plugins
|
|
51
|
+
|
|
52
|
+
# Open the interactive marketplace dashboard (Shows Outdated/Installed status!)
|
|
53
|
+
agy-plugin marketplace list
|
|
54
|
+
```
|
|
55
|
+
*(The interactive TUI lets you browse available plugins, see update dates, and batch install them with a single keystroke!)*
|
|
56
|
+
|
|
57
|
+
### 2. Install a Plugin
|
|
58
|
+
Install plugins from the connected marketplace by specifying the plugin name and the repository namespace.
|
|
59
|
+
```bash
|
|
60
|
+
agy-plugin add commit-commands@zaunekko
|
|
61
|
+
agy-plugin add explanatory-output-style@zaunekko
|
|
62
|
+
```
|
|
63
|
+
*(The CLI will securely download the `skills`, and `hooks` to your local `.gemini/config/` directory!)*
|
|
64
|
+
|
|
65
|
+
### 3. Keep Plugins Updated
|
|
66
|
+
Easily fetch and sync the latest plugin updates from the remote authors. Our CLI tracks the local and remote SHA fingerprints to dynamically detect out-of-date plugins.
|
|
67
|
+
```bash
|
|
68
|
+
# Update all installed plugins across all namespaces globally
|
|
69
|
+
agy-plugin update
|
|
70
|
+
|
|
71
|
+
# Update all plugins in a specific namespace
|
|
72
|
+
agy-plugin update @zaunekko
|
|
73
|
+
|
|
74
|
+
# Update a specific plugin directly
|
|
75
|
+
agy-plugin update commit-commands@zaunekko
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
<br />
|
|
79
|
+
|
|
80
|
+
## 🧠 Architecture & Security
|
|
81
|
+
|
|
82
|
+
- **Fingerprint Tracking**: `agy-plugin` caches the latest Git `sha` for downloaded directories in `~/.agy-plugin/installed.json`, while marketplace configuration lives in `~/.agy-plugin/config.json`. This enables intelligent delta-updates and distinct visual states (Installed vs Update Available) in the TUI.
|
|
83
|
+
- **Dynamic Context Injection**: Antigravity natively loads everything dropped into the `.gemini/config/` directory. Our CLI acts as the secure bridge between remote community repos and your local AI context.
|
|
84
|
+
- **`gh` CLI Integration**: Under the hood, if `GITHUB_TOKEN` is missing, the tool attempts to extract tokens via `gh auth token` to ensure your local git workflow is entirely uninterrupted.
|
|
85
|
+
|
|
86
|
+
## 📄 License
|
|
87
|
+
|
|
88
|
+
This project is open-sourced under the [MIT License](https://github.com/ZaunEkko/agy-plugins-cli/blob/main/LICENSE).
|
|
89
|
+
|
|
90
|
+
<br />
|
|
91
|
+
|
|
92
|
+
<div align="center">
|
|
93
|
+
<i>Built for Antigravity, by Antigravity.</i>
|
|
94
|
+
</div>
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadConfig = loadConfig;
|
|
37
|
+
exports.saveConfig = saveConfig;
|
|
38
|
+
exports.addMarketplace = addMarketplace;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
// Path to the configuration file (e.g. ~/.agy-plugin/config.json)
|
|
43
|
+
const CONFIG_DIR = path.join(os.homedir(), '.agy-plugin');
|
|
44
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
45
|
+
const DEFAULT_CONFIG = {
|
|
46
|
+
marketplaces: []
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Ensure the config file exists and load it.
|
|
50
|
+
*/
|
|
51
|
+
function loadConfig() {
|
|
52
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
53
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
56
|
+
saveConfig(DEFAULT_CONFIG);
|
|
57
|
+
return DEFAULT_CONFIG;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
61
|
+
return JSON.parse(data);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('Error reading config file. Using default config.');
|
|
65
|
+
return DEFAULT_CONFIG;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Save the config to disk.
|
|
70
|
+
*/
|
|
71
|
+
function saveConfig(config) {
|
|
72
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
73
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Add a marketplace repository if it doesn't already exist.
|
|
79
|
+
*/
|
|
80
|
+
function addMarketplace(repo) {
|
|
81
|
+
const config = loadConfig();
|
|
82
|
+
if (!config.marketplaces.includes(repo)) {
|
|
83
|
+
config.marketplaces.push(repo);
|
|
84
|
+
saveConfig(config);
|
|
85
|
+
console.log(`Successfully added marketplace: ${repo}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`Marketplace ${repo} already exists in your config.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/fetcher.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.getLatestCommitInfo = getLatestCommitInfo;
|
|
40
|
+
exports.getLatestCommitSha = getLatestCommitSha;
|
|
41
|
+
exports.downloadPlugin = downloadPlugin;
|
|
42
|
+
exports.listPluginsInRepo = listPluginsInRepo;
|
|
43
|
+
const axios_1 = __importDefault(require("axios"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
const readline = __importStar(require("readline"));
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const mcp_merger_1 = require("./mcp-merger");
|
|
50
|
+
const hooks_merger_1 = require("./hooks-merger");
|
|
51
|
+
let cachedToken = null;
|
|
52
|
+
function getGithubToken() {
|
|
53
|
+
if (cachedToken)
|
|
54
|
+
return cachedToken;
|
|
55
|
+
if (process.env.GITHUB_TOKEN) {
|
|
56
|
+
cachedToken = process.env.GITHUB_TOKEN;
|
|
57
|
+
return cachedToken;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const token = (0, child_process_1.execSync)('gh auth token', { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf-8' }).trim();
|
|
61
|
+
if (token) {
|
|
62
|
+
cachedToken = token;
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
// Ignore error
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const GITHUB_API_BASE = 'https://api.github.com/repos';
|
|
72
|
+
function askQuestion(query) {
|
|
73
|
+
const rl = readline.createInterface({
|
|
74
|
+
input: process.stdin,
|
|
75
|
+
output: process.stdout,
|
|
76
|
+
});
|
|
77
|
+
return new Promise(resolve => rl.question(query, ans => {
|
|
78
|
+
rl.close();
|
|
79
|
+
resolve(ans.toLowerCase().startsWith('y'));
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the latest commit info (SHA, date, message) for a specific path
|
|
84
|
+
*/
|
|
85
|
+
async function getLatestCommitInfo(repo, pluginPath) {
|
|
86
|
+
const url = `${GITHUB_API_BASE}/${repo}/commits?path=${pluginPath}&per_page=1`;
|
|
87
|
+
try {
|
|
88
|
+
const headers = {
|
|
89
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
90
|
+
'User-Agent': 'agy-plugins-cli'
|
|
91
|
+
};
|
|
92
|
+
const token = getGithubToken();
|
|
93
|
+
if (token) {
|
|
94
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
95
|
+
}
|
|
96
|
+
const response = await axios_1.default.get(url, { headers });
|
|
97
|
+
if (response.data && response.data.length > 0) {
|
|
98
|
+
return {
|
|
99
|
+
sha: response.data[0].sha,
|
|
100
|
+
date: response.data[0].commit.committer.date,
|
|
101
|
+
message: response.data[0].commit.message.split('\n')[0]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
// Ignore, just return null if we can't fetch it
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the latest commit SHA for a specific path in the repository
|
|
112
|
+
*/
|
|
113
|
+
async function getLatestCommitSha(repo, pluginPath) {
|
|
114
|
+
const info = await getLatestCommitInfo(repo, pluginPath);
|
|
115
|
+
return info ? info.sha : null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if the plugin contains dangerous executable hooks before downloading
|
|
119
|
+
*/
|
|
120
|
+
async function checkSecurity(repo, pluginPath) {
|
|
121
|
+
const url = `${GITHUB_API_BASE}/${repo}/contents/${pluginPath}`;
|
|
122
|
+
try {
|
|
123
|
+
const headers = {
|
|
124
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
125
|
+
'User-Agent': 'agy-plugins-cli'
|
|
126
|
+
};
|
|
127
|
+
const token = getGithubToken();
|
|
128
|
+
if (token) {
|
|
129
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
130
|
+
}
|
|
131
|
+
const response = await axios_1.default.get(url, { headers });
|
|
132
|
+
const hasHooks = response.data.some(item => item.name === 'hooks' && item.type === 'dir');
|
|
133
|
+
if (hasHooks) {
|
|
134
|
+
console.log(chalk_1.default.red.bold(`\n⚠️ SECURITY WARNING ⚠️`));
|
|
135
|
+
console.log(chalk_1.default.yellow(`The plugin '${pluginPath}' contains executable hooks scripts.`));
|
|
136
|
+
console.log(chalk_1.default.yellow(`Executing unverified scripts can be dangerous and compromise your system.`));
|
|
137
|
+
const isTrusted = await askQuestion(chalk_1.default.white.bold(`Do you trust this plugin and want to proceed? (y/N): `));
|
|
138
|
+
if (!isTrusted) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
// Ignore errors here, they will be caught during actual download
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fetch and download a plugin folder from GitHub
|
|
150
|
+
*/
|
|
151
|
+
async function downloadPlugin(repo, pluginPath, targetDir, skipSecurityCheck = false, rootTargetDir) {
|
|
152
|
+
if (!skipSecurityCheck) {
|
|
153
|
+
const safeToProceed = await checkSecurity(repo, pluginPath);
|
|
154
|
+
if (!safeToProceed) {
|
|
155
|
+
throw new Error("Installation cancelled by user due to security warning.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// If this is the initial call, set rootTargetDir to targetDir
|
|
159
|
+
const activeRootTargetDir = rootTargetDir || targetDir;
|
|
160
|
+
let installedFiles = [];
|
|
161
|
+
let installedHooks = [];
|
|
162
|
+
const url = `${GITHUB_API_BASE}/${repo}/contents/${pluginPath}`;
|
|
163
|
+
try {
|
|
164
|
+
const headers = {
|
|
165
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
166
|
+
'User-Agent': 'agy-plugins-cli'
|
|
167
|
+
};
|
|
168
|
+
const token = getGithubToken();
|
|
169
|
+
if (token) {
|
|
170
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
171
|
+
}
|
|
172
|
+
const response = await axios_1.default.get(url, { headers });
|
|
173
|
+
const contents = Array.isArray(response.data) ? response.data : [response.data];
|
|
174
|
+
for (const item of contents) {
|
|
175
|
+
const localPath = path.join(targetDir, item.name);
|
|
176
|
+
if (item.type === 'dir') {
|
|
177
|
+
// Recursive fetch for directories
|
|
178
|
+
if (!fs.existsSync(localPath)) {
|
|
179
|
+
fs.mkdirSync(localPath, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
const subResult = await downloadPlugin(repo, item.path, localPath, true, activeRootTargetDir);
|
|
182
|
+
installedFiles = [...installedFiles, ...subResult.files];
|
|
183
|
+
installedHooks = [...installedHooks, ...subResult.hooks];
|
|
184
|
+
}
|
|
185
|
+
else if (item.type === 'file' && item.download_url) {
|
|
186
|
+
console.log(chalk_1.default.gray(`Downloading ${item.path}...`));
|
|
187
|
+
const fileResponse = await axios_1.default.get(item.download_url, { responseType: 'arraybuffer' });
|
|
188
|
+
// Handle mcp.json merge specially
|
|
189
|
+
if (item.name === 'mcp.json' && targetDir === activeRootTargetDir) {
|
|
190
|
+
console.log(chalk_1.default.blue(`Found mcp.json. Merging...`));
|
|
191
|
+
const remoteContent = Buffer.from(fileResponse.data).toString('utf-8');
|
|
192
|
+
(0, mcp_merger_1.mergeMcpConfig)(localPath, remoteContent);
|
|
193
|
+
installedFiles.push(localPath);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Handle hooks.json merge specially
|
|
197
|
+
if (item.name === 'hooks.json' && path.basename(path.dirname(localPath)) === 'hooks') {
|
|
198
|
+
console.log(chalk_1.default.blue(`Found hooks.json. Merging into root hooks.json...`));
|
|
199
|
+
const remoteContent = Buffer.from(fileResponse.data).toString('utf-8');
|
|
200
|
+
const rootHooksPath = path.join(activeRootTargetDir, 'hooks.json');
|
|
201
|
+
const mergedKeys = (0, hooks_merger_1.mergeHooksConfig)(rootHooksPath, remoteContent);
|
|
202
|
+
installedHooks = [...installedHooks, ...mergedKeys];
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Ensure parent dir exists
|
|
206
|
+
const parentDir = path.dirname(localPath);
|
|
207
|
+
if (!fs.existsSync(parentDir)) {
|
|
208
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
fs.writeFileSync(localPath, fileResponse.data);
|
|
211
|
+
installedFiles.push(localPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { files: installedFiles, hooks: installedHooks };
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error.response && error.response.status === 404) {
|
|
218
|
+
console.error(chalk_1.default.red(`Error: Plugin '${pluginPath}' not found in repository '${repo}'.`));
|
|
219
|
+
}
|
|
220
|
+
else if (error.message !== "Installation cancelled by user due to security warning.") {
|
|
221
|
+
console.error(chalk_1.default.red(`Failed to fetch from GitHub API: ${error.message}`));
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* List all available plugins (directories) in a remote repository along with update date
|
|
228
|
+
*/
|
|
229
|
+
async function listPluginsInRepo(repo) {
|
|
230
|
+
const url = `${GITHUB_API_BASE}/${repo}/contents/`;
|
|
231
|
+
try {
|
|
232
|
+
const headers = {
|
|
233
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
234
|
+
'User-Agent': 'agy-plugins-cli'
|
|
235
|
+
};
|
|
236
|
+
const token = getGithubToken();
|
|
237
|
+
if (token) {
|
|
238
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
239
|
+
}
|
|
240
|
+
const response = await axios_1.default.get(url, { headers });
|
|
241
|
+
const dirs = response.data.filter(item => item.type === 'dir' && !item.name.startsWith('.'));
|
|
242
|
+
// Fetch commit info concurrently for all plugin directories
|
|
243
|
+
const plugins = await Promise.all(dirs.map(async (dir) => {
|
|
244
|
+
const info = await getLatestCommitInfo(repo, dir.name);
|
|
245
|
+
return { name: dir.name, date: info ? info.date : null, sha: info ? info.sha : null };
|
|
246
|
+
}));
|
|
247
|
+
// Sort by most recently updated
|
|
248
|
+
return plugins.sort((a, b) => {
|
|
249
|
+
if (!a.date)
|
|
250
|
+
return 1;
|
|
251
|
+
if (!b.date)
|
|
252
|
+
return -1;
|
|
253
|
+
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error.response && error.response.status === 404) {
|
|
258
|
+
console.error(chalk_1.default.red(`Error: Repository '${repo}' not found.`));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.error(chalk_1.default.red(`Failed to fetch repository contents: ${error.message}`));
|
|
262
|
+
}
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.mergeHooksConfig = mergeHooksConfig;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
/**
|
|
43
|
+
* Deep merge two hook configurations.
|
|
44
|
+
*/
|
|
45
|
+
function mergeHooksConfigData(target, source) {
|
|
46
|
+
for (const key of Object.keys(source)) {
|
|
47
|
+
if (!target[key]) {
|
|
48
|
+
target[key] = source[key];
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for (const hookType of Object.keys(source[key])) {
|
|
52
|
+
if (!target[key][hookType]) {
|
|
53
|
+
target[key][hookType] = source[key][hookType];
|
|
54
|
+
}
|
|
55
|
+
else if (Array.isArray(target[key][hookType]) && Array.isArray(source[key][hookType])) {
|
|
56
|
+
const merged = [...target[key][hookType], ...source[key][hookType]];
|
|
57
|
+
const unique = Array.from(new Set(merged.map(item => JSON.stringify(item)))).map(item => JSON.parse(item));
|
|
58
|
+
target[key][hookType] = unique;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
target[key][hookType] = source[key][hookType];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function mergeHooksConfig(targetPath, remoteContent) {
|
|
68
|
+
let targetData = {};
|
|
69
|
+
if (fs.existsSync(targetPath)) {
|
|
70
|
+
try {
|
|
71
|
+
targetData = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.warn(chalk_1.default.yellow(`Warning: Could not parse existing hooks.json at ${targetPath}. Overwriting.`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let remoteData = {};
|
|
78
|
+
try {
|
|
79
|
+
remoteData = JSON.parse(remoteContent);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(chalk_1.default.red(`Error parsing remote hooks.json content.`));
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
mergeHooksConfigData(targetData, remoteData);
|
|
86
|
+
fs.writeFileSync(targetPath, JSON.stringify(targetData, null, 2), 'utf-8');
|
|
87
|
+
console.log(chalk_1.default.green(`✓ Successfully merged hooks.json into ${targetPath}`));
|
|
88
|
+
return Object.keys(remoteData);
|
|
89
|
+
}
|