@klingai/plugin-cn 0.0.0-stage → 0.0.2-beta
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/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/plugin.json +40 -0
- package/.mcp.json +13 -0
- package/.workbuddy-plugin/plugin.json +20 -0
- package/README.cn.md +48 -0
- package/README.global.md +48 -0
- package/README.md +47 -2
- package/archives/claude-skill.zip +0 -0
- package/assets/kling-logo.png +0 -0
- package/bin/plugin.mjs +179 -0
- package/bin/postinstall.mjs +16 -0
- package/dist/cli.js +22255 -0
- package/lib/install.mjs +910 -0
- package/lib/region-data.mjs +48 -0
- package/lib/region.mjs +16 -0
- package/lib/uninstall.mjs +236 -0
- package/mcp.json +24 -0
- package/package.json +67 -4
- package/plugin.json +44 -0
- package/skills/kling-ai-plugin/SKILL.md +154 -0
- package/skills/kling-ai-plugin/agents/openai.yaml +6 -0
- package/skills/kling-ai-plugin/api-examples.md +328 -0
- package/skills/kling-ai-plugin/dist/cli.mjs +22255 -0
- package/skills/kling-ai-plugin/package.json +9 -0
- package/skills/kling-ai-plugin/reference.md +90 -0
- package/skills/kling-ai-plugin/region.md +9 -0
package/lib/install.mjs
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
accessSync,
|
|
5
|
+
chmodSync,
|
|
6
|
+
constants,
|
|
7
|
+
cpSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readlinkSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
symlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { basename, delimiter, dirname, join, resolve, sep } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { PLUGIN_REGIONS, pluginRegionForPackage } from './region.mjs';
|
|
23
|
+
|
|
24
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const DISTRIBUTION_PACKAGE = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
26
|
+
const DISTRIBUTION = pluginRegionForPackage(DISTRIBUTION_PACKAGE.name);
|
|
27
|
+
|
|
28
|
+
export const PACKAGE_NAME = DISTRIBUTION.packageName;
|
|
29
|
+
export const PLUGIN_NAME = 'kling-universal';
|
|
30
|
+
export const LEGACY_PLUGIN_NAME = 'kling-agent-plugin';
|
|
31
|
+
export const MARKETPLACE_NAME = DISTRIBUTION.marketplaceName;
|
|
32
|
+
export const MCP_SERVER_NAME = 'kling-ai';
|
|
33
|
+
export const PLUGIN_DISPLAY_NAME = DISTRIBUTION.displayName;
|
|
34
|
+
export const PLUGIN_BASE_URL = DISTRIBUTION.baseUrl;
|
|
35
|
+
export const SKILL_HOST_PATHS = Object.freeze({
|
|
36
|
+
agents: ['.agents', 'skills'],
|
|
37
|
+
cursor: ['.cursor', 'skills'],
|
|
38
|
+
gemini: ['.gemini', 'skills'],
|
|
39
|
+
copilot: ['.copilot', 'skills'],
|
|
40
|
+
vscode: ['.copilot', 'skills'],
|
|
41
|
+
opencode: ['.config', 'opencode', 'skills'],
|
|
42
|
+
qwen: ['.qwen', 'skills'],
|
|
43
|
+
qwenwork: ['.qwenworkcn', 'skills'],
|
|
44
|
+
cline: ['.cline', 'skills'],
|
|
45
|
+
roo: ['.roo', 'skills'],
|
|
46
|
+
windsurf: ['.codeium', 'windsurf', 'skills'],
|
|
47
|
+
devin: ['.config', 'devin', 'skills'],
|
|
48
|
+
amp: ['.config', 'agents', 'skills'],
|
|
49
|
+
goose: ['.agents', 'skills'],
|
|
50
|
+
openhands: ['.agents', 'skills'],
|
|
51
|
+
factory: ['.factory', 'skills'],
|
|
52
|
+
droid: ['.factory', 'skills'],
|
|
53
|
+
kiro: ['.kiro', 'skills'],
|
|
54
|
+
junie: ['.junie', 'skills'],
|
|
55
|
+
letta: ['.letta', 'skills'],
|
|
56
|
+
qoder: ['.qoder', 'skills'],
|
|
57
|
+
qoderwork: ['.qoderwork', 'skills'],
|
|
58
|
+
codebuddy: ['.codebuddy', 'skills'],
|
|
59
|
+
workbuddy: ['.workbuddy', 'skills'],
|
|
60
|
+
'workbuddy-ai': ['.workbuddy-ai', 'skills'],
|
|
61
|
+
kimi: ['.kimi-code', 'skills'],
|
|
62
|
+
antigravity: ['.gemini', 'config', 'skills'],
|
|
63
|
+
commandcode: ['.commandcode', 'skills'],
|
|
64
|
+
trae: ['.trae', 'skills'],
|
|
65
|
+
'trae-cn': ['.trae-cn', 'skills'],
|
|
66
|
+
'trae-cli': ['.traecli', 'skills'],
|
|
67
|
+
vibe: ['.vibe', 'skills'],
|
|
68
|
+
hermes: ['.hermes', 'skills'],
|
|
69
|
+
openclaw: ['.openclaw', 'skills'],
|
|
70
|
+
});
|
|
71
|
+
// Codex and Pi discover the shared directory; install one copy for both.
|
|
72
|
+
export const ALL_SKILL_HOST_PATHS = Object.freeze({
|
|
73
|
+
codex: ['.agents', 'skills'],
|
|
74
|
+
claude: ['.claude', 'skills'],
|
|
75
|
+
pi: ['.agents', 'skills'],
|
|
76
|
+
...SKILL_HOST_PATHS,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function hostSkillPaths(host, options = {}) {
|
|
80
|
+
return (options.skillsOnly ? ALL_SKILL_HOST_PATHS : SKILL_HOST_PATHS)[host];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const SUPPORTED_HOSTS = [
|
|
84
|
+
'codex',
|
|
85
|
+
'claude',
|
|
86
|
+
'pi',
|
|
87
|
+
...Object.keys(SKILL_HOST_PATHS),
|
|
88
|
+
];
|
|
89
|
+
// Expand this list only after checking discovery with the actual Host runtime.
|
|
90
|
+
// Native registration/discovery and Agent invocation are separate acceptance steps.
|
|
91
|
+
export const DEFAULT_SKILL_HOSTS = ['codex', 'claude', 'pi', 'workbuddy', 'workbuddy-ai', 'qwenwork', 'openclaw'];
|
|
92
|
+
export const SKILL_NAMES = [
|
|
93
|
+
'kling-ai-plugin',
|
|
94
|
+
];
|
|
95
|
+
export const LEGACY_SKILL_NAMES = [
|
|
96
|
+
'Kling AI Plugin',
|
|
97
|
+
'kling-universal',
|
|
98
|
+
'kling-universal-generate-image',
|
|
99
|
+
'kling-universal-generate-video',
|
|
100
|
+
'kling-ai',
|
|
101
|
+
'kling-ai-generate-image',
|
|
102
|
+
'kling-ai-generate-video',
|
|
103
|
+
];
|
|
104
|
+
const CODEX_DISCOVERED_SKILL_PATHS = [
|
|
105
|
+
['.codex', 'skills'],
|
|
106
|
+
['.agents', 'skills'],
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const WRAPPER_NAME = 'kling-plugin';
|
|
110
|
+
// Earlier releases exposed the private wrapper under these names.
|
|
111
|
+
const LEGACY_WRAPPER_NAMES = ['kling-ai-plugin', 'kling-ai-plugin.cmd', 'kling', 'kling.cmd'];
|
|
112
|
+
const MANAGED_MARKER = 'managed-by: @klingai/plugin-family';
|
|
113
|
+
const LEGACY_MANAGED_MARKER = 'managed-by: @klingai/plugin';
|
|
114
|
+
const SKILL_MARKER = '.kling-universal';
|
|
115
|
+
const LEGACY_SKILL_MARKER = '.kling-agent-plugin';
|
|
116
|
+
const MANAGED_PACKAGE_NAMES = new Set(['@klingai/plugin', ...Object.values(PLUGIN_REGIONS).map((region) => region.packageName)]);
|
|
117
|
+
const PLUGIN_ENTRIES = [
|
|
118
|
+
'.mcp.json',
|
|
119
|
+
'.claude-plugin',
|
|
120
|
+
'.codex-plugin',
|
|
121
|
+
'.workbuddy-plugin',
|
|
122
|
+
'assets',
|
|
123
|
+
'bin',
|
|
124
|
+
'dist',
|
|
125
|
+
'lib',
|
|
126
|
+
'mcp.json',
|
|
127
|
+
'plugin.json',
|
|
128
|
+
'package.json',
|
|
129
|
+
'README.md',
|
|
130
|
+
'skills',
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
function readJson(path) {
|
|
134
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeJson(path, value) {
|
|
138
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
139
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function assertPackageComplete(packageRoot) {
|
|
143
|
+
for (const entry of PLUGIN_ENTRIES) {
|
|
144
|
+
if (!existsSync(join(packageRoot, entry))) {
|
|
145
|
+
throw new Error(`Package is incomplete: missing ${entry}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertManagedWrapperTargets(klingHome) {
|
|
151
|
+
for (const name of [WRAPPER_NAME, `${WRAPPER_NAME}.cmd`]) {
|
|
152
|
+
const wrapper = join(klingHome, 'bin', name);
|
|
153
|
+
const content = existsSync(wrapper) ? readFileSync(wrapper, 'utf8') : '';
|
|
154
|
+
if (content && !isManagedContent(content)) {
|
|
155
|
+
throw new Error(`Refusing to overwrite an unmanaged executable: ${wrapper}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isManagedContent(content) {
|
|
161
|
+
return content.includes(MANAGED_MARKER) || content.includes(LEGACY_MANAGED_MARKER);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pathExists(path) {
|
|
165
|
+
return Boolean(lstatSync(path, { throwIfNoEntry: false }));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isManagedSkill(destination, pluginRoot) {
|
|
169
|
+
const stat = lstatSync(destination, { throwIfNoEntry: false });
|
|
170
|
+
if (stat?.isSymbolicLink()) {
|
|
171
|
+
return Boolean(pluginRoot)
|
|
172
|
+
&& resolve(dirname(destination), readlinkSync(destination))
|
|
173
|
+
=== join(pluginRoot, 'skills', basename(destination));
|
|
174
|
+
}
|
|
175
|
+
if (!stat?.isDirectory()) return false;
|
|
176
|
+
const marker = [SKILL_MARKER, LEGACY_SKILL_MARKER]
|
|
177
|
+
.map((name) => join(destination, name))
|
|
178
|
+
.find(existsSync);
|
|
179
|
+
if (!marker) return false;
|
|
180
|
+
const content = readFileSync(marker, 'utf8');
|
|
181
|
+
try {
|
|
182
|
+
return MANAGED_PACKAGE_NAMES.has(JSON.parse(content).managedBy);
|
|
183
|
+
} catch {
|
|
184
|
+
return isManagedContent(content);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function replaceSkillLink(source, destination) {
|
|
189
|
+
const parent = dirname(destination);
|
|
190
|
+
const suffix = randomUUID();
|
|
191
|
+
const next = join(parent, `.kling-next-${suffix}`);
|
|
192
|
+
const previous = join(parent, `.kling-previous-${suffix}`);
|
|
193
|
+
mkdirSync(parent, { recursive: true });
|
|
194
|
+
symlinkSync(source, next, process.platform === 'win32' ? 'junction' : 'dir');
|
|
195
|
+
try {
|
|
196
|
+
if (pathExists(destination)) renameSync(destination, previous);
|
|
197
|
+
renameSync(next, destination);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
rmSync(next, { force: true });
|
|
200
|
+
if (!pathExists(destination) && pathExists(previous)) renameSync(previous, destination);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (pathExists(previous)) rmSync(previous, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function replaceDirectory(source, destination) {
|
|
207
|
+
const parent = dirname(destination);
|
|
208
|
+
const next = join(parent, `.next-${process.pid}`);
|
|
209
|
+
const previous = join(parent, `.previous-${process.pid}`);
|
|
210
|
+
mkdirSync(parent, { recursive: true });
|
|
211
|
+
rmSync(next, { recursive: true, force: true });
|
|
212
|
+
rmSync(previous, { recursive: true, force: true });
|
|
213
|
+
cpSync(source, next, { recursive: true });
|
|
214
|
+
if (pathExists(destination)) renameSync(destination, previous);
|
|
215
|
+
renameSync(next, destination);
|
|
216
|
+
rmSync(previous, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function copyPluginPackage(sourceRoot, destination, options) {
|
|
220
|
+
const staging = join(dirname(destination), `.plugin-${process.pid}`);
|
|
221
|
+
rmSync(staging, { recursive: true, force: true });
|
|
222
|
+
mkdirSync(staging, { recursive: true });
|
|
223
|
+
try {
|
|
224
|
+
for (const entry of PLUGIN_ENTRIES) {
|
|
225
|
+
const source = join(sourceRoot, entry);
|
|
226
|
+
if (!existsSync(source)) throw new Error(`Package is incomplete: missing ${entry}`);
|
|
227
|
+
cpSync(source, join(staging, entry), { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
replaceDirectory(staging, destination);
|
|
230
|
+
} finally {
|
|
231
|
+
rmSync(staging, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function createMarketplace(pluginRoot, marketplaceRoot) {
|
|
236
|
+
const staging = join(dirname(marketplaceRoot), `.marketplace-${process.pid}`);
|
|
237
|
+
rmSync(staging, { recursive: true, force: true });
|
|
238
|
+
mkdirSync(join(staging, 'plugins'), { recursive: true });
|
|
239
|
+
cpSync(pluginRoot, join(staging, 'plugins', PLUGIN_NAME), { recursive: true });
|
|
240
|
+
// Local Codex/Claude/WorkBuddy marketplaces consume their compatibility
|
|
241
|
+
// manifests. Keep the portable root manifest in the durable/npm package,
|
|
242
|
+
// but omit it from this host-specific marketplace copy so older hosts do
|
|
243
|
+
// not stop before loading .codex-plugin/.claude-plugin metadata.
|
|
244
|
+
rmSync(join(staging, 'plugins', PLUGIN_NAME, 'plugin.json'));
|
|
245
|
+
|
|
246
|
+
writeJson(join(staging, '.agents', 'plugins', 'marketplace.json'), {
|
|
247
|
+
name: MARKETPLACE_NAME,
|
|
248
|
+
interface: { displayName: PLUGIN_DISPLAY_NAME },
|
|
249
|
+
plugins: [
|
|
250
|
+
{
|
|
251
|
+
name: PLUGIN_NAME,
|
|
252
|
+
source: { source: 'local', path: `./plugins/${PLUGIN_NAME}` },
|
|
253
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
254
|
+
category: 'Creativity',
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
writeJson(join(staging, '.claude-plugin', 'marketplace.json'), {
|
|
260
|
+
$schema: 'https://anthropic.com/claude-code/marketplace.schema.json',
|
|
261
|
+
name: MARKETPLACE_NAME,
|
|
262
|
+
description: `${PLUGIN_DISPLAY_NAME} official plugin marketplace`,
|
|
263
|
+
owner: { name: 'Kling AI' },
|
|
264
|
+
plugins: [
|
|
265
|
+
{
|
|
266
|
+
name: PLUGIN_NAME,
|
|
267
|
+
description: 'Universal Kling Plugin and Skills for image and video generation.',
|
|
268
|
+
source: `./plugins/${PLUGIN_NAME}`,
|
|
269
|
+
category: 'productivity',
|
|
270
|
+
homepage: PLUGIN_BASE_URL,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
writeJson(join(staging, 'marketplace.json'), {
|
|
276
|
+
name: MARKETPLACE_NAME,
|
|
277
|
+
interface: { displayName: PLUGIN_DISPLAY_NAME },
|
|
278
|
+
plugins: [
|
|
279
|
+
{
|
|
280
|
+
name: PLUGIN_NAME,
|
|
281
|
+
source: { source: 'local', path: `./plugins/${PLUGIN_NAME}` },
|
|
282
|
+
category: 'Creativity',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
replaceDirectory(staging, marketplaceRoot);
|
|
288
|
+
rmSync(staging, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function shellQuote(value) {
|
|
292
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function writeCliWrappers(klingHome, cliPath) {
|
|
296
|
+
const binDir = join(klingHome, 'bin');
|
|
297
|
+
mkdirSync(binDir, { recursive: true });
|
|
298
|
+
for (const { name, target } of [{ name: WRAPPER_NAME, target: cliPath }]) {
|
|
299
|
+
const wrapper = join(binDir, name);
|
|
300
|
+
const content = [
|
|
301
|
+
'#!/bin/sh',
|
|
302
|
+
`# ${MANAGED_MARKER}`,
|
|
303
|
+
`exec node ${shellQuote(target)} "$@"`,
|
|
304
|
+
'',
|
|
305
|
+
].join('\n');
|
|
306
|
+
writeFileSync(wrapper, content);
|
|
307
|
+
chmodSync(wrapper, 0o755);
|
|
308
|
+
writeFileSync(
|
|
309
|
+
`${wrapper}.cmd`,
|
|
310
|
+
`@REM ${MANAGED_MARKER}\r\n@node "${target.replaceAll('"', '""')}" %*\r\n`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
for (const name of LEGACY_WRAPPER_NAMES) {
|
|
314
|
+
const old = join(binDir, name);
|
|
315
|
+
if (existsSync(old) && isManagedContent(readFileSync(old, 'utf8'))) rmSync(old);
|
|
316
|
+
}
|
|
317
|
+
return join(binDir, WRAPPER_NAME);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Make `kling-plugin` runnable from a terminal without editing shell profiles:
|
|
321
|
+
// link it into a writable directory that is already on PATH. Never overwrite a
|
|
322
|
+
// command this package does not own; callers print a PATH hint when no link fits.
|
|
323
|
+
export function exposeCommand(wrapper, options = {}) {
|
|
324
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
325
|
+
const pathDirs = (options.pathEnv ?? process.env.PATH ?? '').split(delimiter).filter(Boolean).map((dir) => resolve(dir));
|
|
326
|
+
const wrapperDir = resolve(dirname(wrapper));
|
|
327
|
+
if (pathDirs.includes(wrapperDir)) return { status: 'on-path', path: wrapper };
|
|
328
|
+
const preferred = (options.preferredDirs ?? [join(userHome, '.local', 'bin'), '/opt/homebrew/bin', '/usr/local/bin']).map((dir) => resolve(dir));
|
|
329
|
+
const candidates = [...preferred.filter((dir) => pathDirs.includes(dir)), ...pathDirs.filter((dir) => dir.startsWith(`${userHome}${sep}`) && !preferred.includes(dir))];
|
|
330
|
+
const conflicts = [];
|
|
331
|
+
for (const dir of candidates) {
|
|
332
|
+
const link = join(dir, WRAPPER_NAME);
|
|
333
|
+
try {
|
|
334
|
+
accessSync(dir, constants.W_OK);
|
|
335
|
+
const stat = lstatSync(link, { throwIfNoEntry: false });
|
|
336
|
+
if (stat) {
|
|
337
|
+
const ours = stat.isSymbolicLink() && resolve(dir, readlinkSync(link)) === resolve(wrapper);
|
|
338
|
+
const managed = stat.isSymbolicLink() && /[\\/]\.kling[\\/]bin[\\/]kling-plugin$/.test(resolve(dir, readlinkSync(link)));
|
|
339
|
+
if (ours) return { status: 'linked', path: link };
|
|
340
|
+
if (!managed) { conflicts.push(link); continue; }
|
|
341
|
+
rmSync(link);
|
|
342
|
+
}
|
|
343
|
+
symlinkSync(wrapper, link);
|
|
344
|
+
return { status: 'linked', path: link };
|
|
345
|
+
} catch { /* try the next PATH directory */ }
|
|
346
|
+
}
|
|
347
|
+
return conflicts.length > 0
|
|
348
|
+
? { status: 'conflict', path: conflicts[0], reason: `${conflicts[0]} is not managed by this package` }
|
|
349
|
+
: { status: 'not-on-path', path: wrapper };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function removeExposedCommand(klingHome, options = {}) {
|
|
353
|
+
const wrapper = resolve(join(klingHome, 'bin', WRAPPER_NAME));
|
|
354
|
+
const removed = [];
|
|
355
|
+
for (const dir of (options.pathEnv ?? process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
|
|
356
|
+
const link = join(resolve(dir), WRAPPER_NAME);
|
|
357
|
+
try {
|
|
358
|
+
const stat = lstatSync(link, { throwIfNoEntry: false });
|
|
359
|
+
if (stat?.isSymbolicLink() && resolve(dirname(link), readlinkSync(link)) === wrapper) { rmSync(link); removed.push(link); }
|
|
360
|
+
} catch { /* not ours or not removable */ }
|
|
361
|
+
}
|
|
362
|
+
return removed;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function skillRootForHost(userHome, host, options = {}) {
|
|
366
|
+
const segments = hostSkillPaths(host, options);
|
|
367
|
+
return segments ? join(userHome, ...segments) : undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function unmanagedSkillMessage(destination) {
|
|
371
|
+
return `Refusing to overwrite an unmanaged Skill: ${destination}. `
|
|
372
|
+
+ `If you do not need it, move it aside (for example: mv "${destination}" "${destination}.bak") and run the installer again`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function installSkillsAtRoot(skillsRoot, pluginRoot, options = {}) {
|
|
376
|
+
const version = readJson(join(pluginRoot, 'package.json')).version;
|
|
377
|
+
for (const name of SKILL_NAMES) {
|
|
378
|
+
const destination = join(skillsRoot, name);
|
|
379
|
+
if (pathExists(destination) && !isManagedSkill(destination, pluginRoot)) {
|
|
380
|
+
throw new Error(unmanagedSkillMessage(destination));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
for (const name of SKILL_NAMES) {
|
|
384
|
+
const destination = join(skillsRoot, name);
|
|
385
|
+
const source = join(pluginRoot, 'skills', name);
|
|
386
|
+
if (options.linkSkills) {
|
|
387
|
+
replaceSkillLink(source, destination);
|
|
388
|
+
} else {
|
|
389
|
+
replaceDirectory(source, destination);
|
|
390
|
+
writeJson(join(destination, SKILL_MARKER), {
|
|
391
|
+
managedBy: PACKAGE_NAME,
|
|
392
|
+
plugin: PLUGIN_NAME,
|
|
393
|
+
skill: name,
|
|
394
|
+
version,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const name of LEGACY_SKILL_NAMES) {
|
|
399
|
+
const destination = join(skillsRoot, name);
|
|
400
|
+
if (isManagedSkill(destination, pluginRoot)) {
|
|
401
|
+
rmSync(destination, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return skillsRoot;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function removeManagedSkills(skillsRoot, pluginRoot) {
|
|
408
|
+
for (const name of [...SKILL_NAMES, ...LEGACY_SKILL_NAMES]) {
|
|
409
|
+
const target = join(skillsRoot, name);
|
|
410
|
+
if (isManagedSkill(target, pluginRoot)) {
|
|
411
|
+
rmSync(target, { recursive: true, force: true });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function removeManagedCodexDuplicates(userHome, pluginRoot) {
|
|
417
|
+
for (const segments of CODEX_DISCOVERED_SKILL_PATHS) {
|
|
418
|
+
removeManagedSkills(join(userHome, ...segments), pluginRoot);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Earlier all-host installs wrote unverified directories that can shadow shared Skills.
|
|
423
|
+
// Default upgrades retire only our copies there; explicit/custom installs stay scoped.
|
|
424
|
+
export function removeManagedObsoleteSkills(staged) {
|
|
425
|
+
const keep = new Set(DEFAULT_SKILL_HOSTS.map((host) => join(staged.userHome, ...ALL_SKILL_HOST_PATHS[host])));
|
|
426
|
+
const roots = new Set([...Object.values(ALL_SKILL_HOST_PATHS), ...CODEX_DISCOVERED_SKILL_PATHS]
|
|
427
|
+
.map((parts) => join(staged.userHome, ...parts)));
|
|
428
|
+
for (const root of roots) {
|
|
429
|
+
if (!keep.has(root)) removeManagedSkills(root, staged.pluginRoot);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function otherLinkedSkillRoots(userHome, pluginRoot, excludedRoot) {
|
|
434
|
+
const roots = new Set(Object.values(ALL_SKILL_HOST_PATHS).map((segments) => join(userHome, ...segments)));
|
|
435
|
+
return [...roots].filter((skillsRoot) => skillsRoot !== excludedRoot && existsSync(skillsRoot) && SKILL_NAMES.some((name) => {
|
|
436
|
+
const destination = join(skillsRoot, name);
|
|
437
|
+
return lstatSync(destination, { throwIfNoEntry: false })?.isSymbolicLink()
|
|
438
|
+
&& isManagedSkill(destination, pluginRoot);
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function skillFilesReady(skillsRoot, name) {
|
|
443
|
+
try {
|
|
444
|
+
const root = join(skillsRoot, name);
|
|
445
|
+
const content = readFileSync(join(root, 'SKILL.md'), 'utf8');
|
|
446
|
+
if (!content.startsWith(`---\nname: ${name}\n`)) return false;
|
|
447
|
+
return [...content.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)].every(([, link]) => (
|
|
448
|
+
/^[a-z]+:|^#/.test(link) || existsSync(resolve(root, link.split('#')[0]))
|
|
449
|
+
));
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function skillInstallChecks(skillsRoot, pluginRoot, version) {
|
|
456
|
+
return !LEGACY_SKILL_NAMES.some((name) => isManagedSkill(join(skillsRoot, name), pluginRoot)) && SKILL_NAMES.every((name) => {
|
|
457
|
+
const destination = join(skillsRoot, name);
|
|
458
|
+
if (!isManagedSkill(destination, pluginRoot) || !skillFilesReady(skillsRoot, name)) return false;
|
|
459
|
+
if (lstatSync(destination).isSymbolicLink()) return true;
|
|
460
|
+
try {
|
|
461
|
+
const marker = readJson(join(destination, SKILL_MARKER));
|
|
462
|
+
return marker.managedBy === PACKAGE_NAME && marker.version === version;
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function readInstallState(klingHome) {
|
|
470
|
+
const path = join(klingHome, 'plugins', PLUGIN_NAME, 'installations.json');
|
|
471
|
+
if (!existsSync(path)) return { skills: [], npm: [] };
|
|
472
|
+
const state = readJson(path);
|
|
473
|
+
if (state.managedBy !== 'kling-plugin-installer' || !Array.isArray(state.skills) || !Array.isArray(state.npm)) {
|
|
474
|
+
throw new Error(`Invalid install record: ${path}`);
|
|
475
|
+
}
|
|
476
|
+
return state;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function writeInstallState(klingHome, state) {
|
|
480
|
+
writeJson(join(klingHome, 'plugins', PLUGIN_NAME, 'installations.json'), {
|
|
481
|
+
managedBy: 'kling-plugin-installer', ...state,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function recordNpmInstallation(packageRoot, options = {}) {
|
|
486
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
487
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
488
|
+
const root = resolve(packageRoot);
|
|
489
|
+
if (basename(dirname(dirname(root))) !== 'node_modules') return;
|
|
490
|
+
const manifest = readJson(join(root, 'package.json'));
|
|
491
|
+
if (!MANAGED_PACKAGE_NAMES.has(manifest.name)) return;
|
|
492
|
+
const global = options.global ?? process.env.npm_config_global === 'true';
|
|
493
|
+
const parent = dirname(dirname(dirname(root)));
|
|
494
|
+
const prefix = global && basename(parent) === 'lib' ? dirname(parent) : parent;
|
|
495
|
+
const state = readInstallState(klingHome);
|
|
496
|
+
state.npm = state.npm.filter((item) => item.path !== root);
|
|
497
|
+
state.npm.push({ name: manifest.name, path: root, prefix, global });
|
|
498
|
+
writeInstallState(klingHome, state);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function installSkillDirectory(skillsDir, staged, options = {}) {
|
|
502
|
+
const skillsRoot = resolve(skillsDir);
|
|
503
|
+
installSkillsAtRoot(skillsRoot, staged.pluginRoot, options);
|
|
504
|
+
const state = readInstallState(staged.klingHome);
|
|
505
|
+
state.skills = [...new Set([...state.skills, skillsRoot])];
|
|
506
|
+
writeInstallState(staged.klingHome, state);
|
|
507
|
+
return { status: 'installed', path: skillsRoot };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function doctorSkillDirectory(skillsDir, options = {}) {
|
|
511
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
512
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
513
|
+
const pluginRoot = join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
514
|
+
const checks = {
|
|
515
|
+
package: existsSync(join(pluginRoot, 'package.json')),
|
|
516
|
+
cli: existsSync(join(pluginRoot, 'dist', 'cli.js')),
|
|
517
|
+
mcp: existsSync(join(pluginRoot, '.mcp.json'))
|
|
518
|
+
&& existsSync(join(pluginRoot, 'mcp.json')),
|
|
519
|
+
skill: SKILL_NAMES.every((name) => skillFilesReady(join(pluginRoot, 'skills'), name)),
|
|
520
|
+
wrapper: existsSync(join(klingHome, 'bin', WRAPPER_NAME)),
|
|
521
|
+
skillCli: SKILL_NAMES.every((name) => existsSync(join(resolve(skillsDir), name, 'dist', 'cli.mjs'))),
|
|
522
|
+
hostInstall: existsSync(join(pluginRoot, 'package.json'))
|
|
523
|
+
&& skillInstallChecks(resolve(skillsDir), pluginRoot, readJson(join(pluginRoot, 'package.json')).version),
|
|
524
|
+
};
|
|
525
|
+
return { ok: Object.values(checks).every(Boolean), pluginRoot, checks };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function uninstallSkillDirectory(skillsDir, options = {}) {
|
|
529
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
530
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
531
|
+
const pluginRoot = join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
532
|
+
if (options.purge && otherLinkedSkillRoots(userHome, pluginRoot, resolve(skillsDir)).length > 0) {
|
|
533
|
+
throw new Error('Cannot purge while another Host still links to the shared Kling Skills');
|
|
534
|
+
}
|
|
535
|
+
removeManagedSkills(resolve(skillsDir), pluginRoot);
|
|
536
|
+
if (options.purge) {
|
|
537
|
+
for (const name of [WRAPPER_NAME, `${WRAPPER_NAME}.cmd`, ...LEGACY_WRAPPER_NAMES]) {
|
|
538
|
+
const wrapper = join(klingHome, 'bin', name);
|
|
539
|
+
if (existsSync(wrapper) && isManagedContent(readFileSync(wrapper, 'utf8'))) rmSync(wrapper);
|
|
540
|
+
}
|
|
541
|
+
removeExposedCommand(klingHome, options);
|
|
542
|
+
rmSync(join(klingHome, 'plugins', PLUGIN_NAME), { recursive: true, force: true });
|
|
543
|
+
}
|
|
544
|
+
return { status: 'uninstalled', credentialsPreserved: true };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function stagePackage(options = {}) {
|
|
548
|
+
const packageRoot = resolve(options.packageRoot ?? PACKAGE_ROOT);
|
|
549
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
550
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
551
|
+
const base = join(klingHome, 'plugins', PLUGIN_NAME);
|
|
552
|
+
const pluginRoot = join(base, 'current');
|
|
553
|
+
const marketplaceRoot = join(base, 'marketplace');
|
|
554
|
+
|
|
555
|
+
assertPackageComplete(packageRoot);
|
|
556
|
+
assertManagedWrapperTargets(klingHome);
|
|
557
|
+
copyPluginPackage(packageRoot, pluginRoot, options);
|
|
558
|
+
createMarketplace(pluginRoot, marketplaceRoot);
|
|
559
|
+
const wrapper = writeCliWrappers(klingHome, join(pluginRoot, 'dist', 'cli.js'));
|
|
560
|
+
return { packageRoot, userHome, klingHome, pluginRoot, marketplaceRoot, wrapper };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function commandExists(command) {
|
|
564
|
+
const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
|
|
565
|
+
return result.status === 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function captureCommand(command, args) {
|
|
569
|
+
const result = spawnSync(command, args, { encoding: 'utf8' });
|
|
570
|
+
return result.status === 0 ? result.stdout : '';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function nativeRegistrationMatches(host, pluginRoot, version, capture = captureCommand) {
|
|
574
|
+
try {
|
|
575
|
+
if (host === 'codex') {
|
|
576
|
+
const data = JSON.parse(capture('codex', ['plugin', 'list', '--json']));
|
|
577
|
+
return data.installed?.some((item) => item.pluginId === `${PLUGIN_NAME}@${MARKETPLACE_NAME}`
|
|
578
|
+
&& item.installed && item.enabled && item.version === version) ?? false;
|
|
579
|
+
}
|
|
580
|
+
if (host === 'claude') {
|
|
581
|
+
const data = JSON.parse(capture('claude', ['plugin', 'list', '--json']));
|
|
582
|
+
return data.some((item) => item.id === `${PLUGIN_NAME}@${MARKETPLACE_NAME}`
|
|
583
|
+
&& item.enabled && item.version === version);
|
|
584
|
+
}
|
|
585
|
+
if (host === 'pi') {
|
|
586
|
+
return capture('pi', ['list']).includes(pluginRoot);
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function nativeHostCommand(host) {
|
|
595
|
+
return host === 'claude' ? 'claude' : host;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function hostInstallTarget(host, options = {}) {
|
|
599
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
600
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
601
|
+
if (hostSkillPaths(host, options)) return skillRootForHost(userHome, host, options);
|
|
602
|
+
return join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function hasHostConfiguration(configRoot, skillsRoot, pluginRoot) {
|
|
606
|
+
if (!existsSync(configRoot)) return false;
|
|
607
|
+
return readdirSync(configRoot).some((entry) => {
|
|
608
|
+
if (join(configRoot, entry) !== skillsRoot) return true;
|
|
609
|
+
return readdirSync(skillsRoot).some((name) => !isManagedSkill(join(skillsRoot, name), pluginRoot));
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function detectHosts(options = {}) {
|
|
614
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
615
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
616
|
+
const pluginRoot = join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
617
|
+
const hasCommand = options.commandExists ?? commandExists;
|
|
618
|
+
const detected = [];
|
|
619
|
+
|
|
620
|
+
for (const host of ['codex', 'claude', 'pi']) {
|
|
621
|
+
if (hasCommand(nativeHostCommand(host))) detected.push(host);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const seenSkillRoots = new Set();
|
|
625
|
+
for (const [host, segments] of Object.entries(SKILL_HOST_PATHS)) {
|
|
626
|
+
const configRoot = join(userHome, ...segments.slice(0, -1));
|
|
627
|
+
const skillsRoot = skillRootForHost(userHome, host);
|
|
628
|
+
// Codex also discovers ~/.agents/skills. Prefer its native plugin during
|
|
629
|
+
// automatic installation so the same Skills are not loaded twice.
|
|
630
|
+
if (detected.includes('codex') && skillsRoot === join(userHome, '.agents', 'skills')) continue;
|
|
631
|
+
// --host all may have created a directory containing only our Skills.
|
|
632
|
+
// That is not evidence that the corresponding Host is installed.
|
|
633
|
+
if (!hasHostConfiguration(configRoot, skillsRoot, pluginRoot) || seenSkillRoots.has(skillsRoot)) continue;
|
|
634
|
+
seenSkillRoots.add(skillsRoot);
|
|
635
|
+
detected.push(host);
|
|
636
|
+
}
|
|
637
|
+
return detected;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function uniqueInstallHosts(hosts, options = {}) {
|
|
641
|
+
const seen = new Set();
|
|
642
|
+
const unique = [];
|
|
643
|
+
for (const host of hosts) {
|
|
644
|
+
const key = hostSkillPaths(host, options)
|
|
645
|
+
? `skills:${hostInstallTarget(host, options)}`
|
|
646
|
+
: `native:${host}`;
|
|
647
|
+
if (seen.has(key)) continue;
|
|
648
|
+
seen.add(key);
|
|
649
|
+
unique.push(host);
|
|
650
|
+
}
|
|
651
|
+
return unique;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function resolveInstallHosts(host, options = {}) {
|
|
655
|
+
if (host && host !== 'all') {
|
|
656
|
+
if (!SUPPORTED_HOSTS.includes(host)) throw new Error(`Unsupported host: ${host}`);
|
|
657
|
+
return [host];
|
|
658
|
+
}
|
|
659
|
+
return uniqueInstallHosts(options.includeUnverified ? SUPPORTED_HOSTS : DEFAULT_SKILL_HOSTS, { ...options, skillsOnly: true });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function createInstallPlan(hosts, options = {}) {
|
|
663
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
664
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
665
|
+
const pluginRoot = join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
666
|
+
return {
|
|
667
|
+
dryRun: true,
|
|
668
|
+
pluginRoot,
|
|
669
|
+
wrappers: [join(klingHome, 'bin', WRAPPER_NAME)],
|
|
670
|
+
hosts: hosts.map((host) => ({
|
|
671
|
+
host,
|
|
672
|
+
mode: hostSkillPaths(host, options) ? 'skills' : 'native',
|
|
673
|
+
target: hostInstallTarget(host, { ...options, userHome, klingHome }),
|
|
674
|
+
})),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function preflightInstallHosts(hosts, options = {}) {
|
|
679
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
680
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
681
|
+
const pluginRoot = join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
682
|
+
const hasCommand = options.commandExists ?? commandExists;
|
|
683
|
+
return hosts.map((host) => {
|
|
684
|
+
if (hostSkillPaths(host, options)) {
|
|
685
|
+
const skillsRoot = skillRootForHost(userHome, host, options);
|
|
686
|
+
const conflict = SKILL_NAMES
|
|
687
|
+
.map((name) => join(skillsRoot, name))
|
|
688
|
+
.find((destination) => pathExists(destination) && !isManagedSkill(destination, pluginRoot));
|
|
689
|
+
if (conflict) {
|
|
690
|
+
return {
|
|
691
|
+
host,
|
|
692
|
+
status: 'failed',
|
|
693
|
+
reason: unmanagedSkillMessage(conflict),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return { host, status: 'ready' };
|
|
697
|
+
}
|
|
698
|
+
if (!hasCommand(nativeHostCommand(host))) {
|
|
699
|
+
return { host, status: 'unavailable', reason: `${host} CLI is not installed` };
|
|
700
|
+
}
|
|
701
|
+
if (host === 'claude') {
|
|
702
|
+
const conflicts = detectClaudeConflicts({ ...options, strict: true });
|
|
703
|
+
if (conflicts.length > 0) {
|
|
704
|
+
return {
|
|
705
|
+
host,
|
|
706
|
+
status: 'conflict',
|
|
707
|
+
reason: `Another enabled plugin provides ${MCP_SERVER_NAME}: ${[...new Set(conflicts.map((item) => item.id))].join(', ')}. Keep one Kling connector in Claude Code`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return { host, status: 'ready' };
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function detectClaudeConflicts(options = {}) {
|
|
716
|
+
const capture = options.captureCommand ?? captureCommand;
|
|
717
|
+
const managedPluginIds = new Set([`${PLUGIN_NAME}@${MARKETPLACE_NAME}`]);
|
|
718
|
+
try {
|
|
719
|
+
const plugins = JSON.parse(capture('claude', ['plugin', 'list', '--json']));
|
|
720
|
+
return plugins
|
|
721
|
+
.filter((item) => !(managedPluginIds.has(item.id) && (!item.scope || item.scope === 'user'))
|
|
722
|
+
&& item.enabled && item.mcpServers?.[MCP_SERVER_NAME])
|
|
723
|
+
.map((item) => ({
|
|
724
|
+
type: 'mcp-server',
|
|
725
|
+
id: item.id,
|
|
726
|
+
scope: item.scope,
|
|
727
|
+
blocking: true,
|
|
728
|
+
remediation: 'Review the other Kling plugin in Claude Code and keep only one active kling-ai MCP registration',
|
|
729
|
+
}));
|
|
730
|
+
} catch (error) {
|
|
731
|
+
if (options.strict) throw new Error(`Cannot inspect Claude plugins before install: ${error.message}`);
|
|
732
|
+
return [];
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function run(command, args, options = {}) {
|
|
737
|
+
const result = spawnSync(command, args, { stdio: options.quiet ? 'ignore' : 'inherit' });
|
|
738
|
+
if (result.error?.code === 'ENOENT') {
|
|
739
|
+
if (options.allowFailure) return false;
|
|
740
|
+
throw new Error(`${command} is not installed`);
|
|
741
|
+
}
|
|
742
|
+
if (result.status !== 0 && !options.allowFailure) {
|
|
743
|
+
throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}`);
|
|
744
|
+
}
|
|
745
|
+
return result.status === 0;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function installHost(host, staged, options = {}) {
|
|
749
|
+
const runner = options.run ?? run;
|
|
750
|
+
const hasCommand = options.commandExists ?? commandExists;
|
|
751
|
+
if (!SUPPORTED_HOSTS.includes(host)) throw new Error(`Unsupported host: ${host}`);
|
|
752
|
+
|
|
753
|
+
if (hostSkillPaths(host, options)) {
|
|
754
|
+
return installSkillDirectory(skillRootForHost(staged.userHome, host, options), staged, options);
|
|
755
|
+
}
|
|
756
|
+
if (!hasCommand(nativeHostCommand(host))) {
|
|
757
|
+
throw new Error(`${host} CLI is not installed; the staged plugin remains at ${staged.pluginRoot}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (host === 'codex') {
|
|
761
|
+
runner('codex', ['plugin', 'remove', `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, '--json'], { allowFailure: true, quiet: true });
|
|
762
|
+
runner('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME], { allowFailure: true, quiet: true });
|
|
763
|
+
runner('codex', ['plugin', 'marketplace', 'add', staged.marketplaceRoot, '--json']);
|
|
764
|
+
runner('codex', ['plugin', 'add', `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, '--json']);
|
|
765
|
+
removeManagedCodexDuplicates(staged.userHome, staged.pluginRoot);
|
|
766
|
+
} else if (host === 'claude') {
|
|
767
|
+
runner('claude', ['plugin', 'uninstall', `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, '--scope', 'user'], { allowFailure: true, quiet: true });
|
|
768
|
+
runner('claude', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME], { allowFailure: true, quiet: true });
|
|
769
|
+
runner('claude', ['plugin', 'marketplace', 'add', staged.marketplaceRoot]);
|
|
770
|
+
runner('claude', ['plugin', 'install', `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, '--scope', 'user']);
|
|
771
|
+
} else if (host === 'pi') {
|
|
772
|
+
runner('pi', ['remove', staged.pluginRoot], { allowFailure: true, quiet: true });
|
|
773
|
+
runner('pi', ['install', staged.pluginRoot]);
|
|
774
|
+
}
|
|
775
|
+
return { status: 'installed', path: staged.pluginRoot };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function doctor(host, options = {}) {
|
|
779
|
+
if (!SUPPORTED_HOSTS.includes(host)) throw new Error(`Unsupported host: ${host}`);
|
|
780
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
781
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
782
|
+
if (options.skillsOnly) {
|
|
783
|
+
return doctorSkillDirectory(skillRootForHost(userHome, host, options), { ...options, userHome, klingHome });
|
|
784
|
+
}
|
|
785
|
+
const hasCommand = options.commandExists ?? commandExists;
|
|
786
|
+
const base = join(klingHome, 'plugins', PLUGIN_NAME);
|
|
787
|
+
const pluginRoot = join(base, 'current');
|
|
788
|
+
const checks = {
|
|
789
|
+
package: existsSync(join(pluginRoot, 'package.json')),
|
|
790
|
+
cli: existsSync(join(pluginRoot, 'dist', 'cli.js')),
|
|
791
|
+
mcp: existsSync(join(pluginRoot, '.mcp.json'))
|
|
792
|
+
&& existsSync(join(pluginRoot, 'mcp.json')),
|
|
793
|
+
skill: SKILL_NAMES.every((name) => skillFilesReady(join(pluginRoot, 'skills'), name)),
|
|
794
|
+
wrapper: existsSync(join(klingHome, 'bin', WRAPPER_NAME)),
|
|
795
|
+
hostCli: Boolean(SKILL_HOST_PATHS[host]) || hasCommand(nativeHostCommand(host)),
|
|
796
|
+
};
|
|
797
|
+
if (SKILL_HOST_PATHS[host]) {
|
|
798
|
+
const skillsRoot = skillRootForHost(userHome, host);
|
|
799
|
+
checks.hostInstall = checks.package
|
|
800
|
+
&& skillInstallChecks(skillsRoot, pluginRoot, readJson(join(pluginRoot, 'package.json')).version);
|
|
801
|
+
} else {
|
|
802
|
+
checks.hostInstall = checks.package && checks.hostCli
|
|
803
|
+
&& nativeRegistrationMatches(
|
|
804
|
+
host,
|
|
805
|
+
pluginRoot,
|
|
806
|
+
readJson(join(pluginRoot, 'package.json')).version,
|
|
807
|
+
options.captureCommand,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const conflicts = host === 'claude' ? detectClaudeConflicts(options) : [];
|
|
811
|
+
return {
|
|
812
|
+
ok: Object.values(checks).every(Boolean) && conflicts.every((item) => !item.blocking),
|
|
813
|
+
pluginRoot,
|
|
814
|
+
checks,
|
|
815
|
+
conflicts,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function uninstallHost(host, options = {}) {
|
|
820
|
+
if (!SUPPORTED_HOSTS.includes(host)) throw new Error(`Unsupported host: ${host}`);
|
|
821
|
+
const runner = options.run ?? run;
|
|
822
|
+
const hasCommand = options.commandExists ?? commandExists;
|
|
823
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
824
|
+
const klingHome = resolve(options.klingHome ?? process.env.KLING_HOME ?? join(userHome, '.kling'));
|
|
825
|
+
const pluginRoot = join(klingHome, 'plugins', PLUGIN_NAME, 'current');
|
|
826
|
+
if (options.purge && otherLinkedSkillRoots(userHome, pluginRoot, skillRootForHost(userHome, host, options)).length > 0) {
|
|
827
|
+
throw new Error('Cannot purge while another Host still links to the shared Kling Skills');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (options.skillsOnly) {
|
|
831
|
+
removeManagedSkills(skillRootForHost(userHome, host, options), pluginRoot);
|
|
832
|
+
} else if (host === 'codex' && hasCommand('codex')) {
|
|
833
|
+
runner('codex', ['plugin', 'remove', `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, '--json'], { allowFailure: true });
|
|
834
|
+
runner('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME], { allowFailure: true });
|
|
835
|
+
} else if (host === 'claude' && hasCommand('claude')) {
|
|
836
|
+
runner('claude', ['plugin', 'uninstall', `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, '--scope', 'user'], { allowFailure: true });
|
|
837
|
+
runner('claude', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME], { allowFailure: true });
|
|
838
|
+
} else if (host === 'pi' && hasCommand('pi')) {
|
|
839
|
+
runner('pi', ['remove', join(klingHome, 'plugins', PLUGIN_NAME, 'current')], { allowFailure: true });
|
|
840
|
+
} else if (SKILL_HOST_PATHS[host]) {
|
|
841
|
+
removeManagedSkills(skillRootForHost(userHome, host), pluginRoot);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (options.purge) {
|
|
845
|
+
for (const name of [WRAPPER_NAME, `${WRAPPER_NAME}.cmd`, ...LEGACY_WRAPPER_NAMES]) {
|
|
846
|
+
const wrapper = join(klingHome, 'bin', name);
|
|
847
|
+
if (existsSync(wrapper) && isManagedContent(readFileSync(wrapper, 'utf8'))) rmSync(wrapper);
|
|
848
|
+
}
|
|
849
|
+
removeExposedCommand(klingHome, options);
|
|
850
|
+
rmSync(join(klingHome, 'plugins', PLUGIN_NAME), { recursive: true, force: true });
|
|
851
|
+
}
|
|
852
|
+
return { status: 'uninstalled', credentialsPreserved: true };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export function parseArgs(argv) {
|
|
856
|
+
const [command = 'help', ...rest] = argv;
|
|
857
|
+
let host;
|
|
858
|
+
let skillsDir;
|
|
859
|
+
let purge = false;
|
|
860
|
+
let dryRun = false;
|
|
861
|
+
let linkSkills = false;
|
|
862
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
863
|
+
const arg = rest[index];
|
|
864
|
+
if (arg === '--host') {
|
|
865
|
+
const value = rest[index + 1];
|
|
866
|
+
if (!value || value.startsWith('--')) throw new Error('--host requires a value');
|
|
867
|
+
host = value;
|
|
868
|
+
index += 1;
|
|
869
|
+
}
|
|
870
|
+
else if (arg.startsWith('--host=')) host = arg.slice('--host='.length);
|
|
871
|
+
else if (arg === '--skills-dir') {
|
|
872
|
+
const value = rest[index + 1];
|
|
873
|
+
if (!value || value.startsWith('--')) throw new Error('--skills-dir requires a path');
|
|
874
|
+
skillsDir = value;
|
|
875
|
+
index += 1;
|
|
876
|
+
}
|
|
877
|
+
else if (arg.startsWith('--skills-dir=')) skillsDir = arg.slice('--skills-dir='.length);
|
|
878
|
+
else if (arg === '--purge') purge = true;
|
|
879
|
+
else if (arg === '--dry-run') dryRun = true;
|
|
880
|
+
else if (arg === '--link-skills') linkSkills = true;
|
|
881
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
882
|
+
}
|
|
883
|
+
if (host !== undefined) host = host.toLowerCase();
|
|
884
|
+
if (host !== undefined && !host.trim()) throw new Error('--host requires a value');
|
|
885
|
+
if (skillsDir !== undefined && !skillsDir.trim()) throw new Error('--skills-dir requires a path');
|
|
886
|
+
if (host && skillsDir) throw new Error('Choose either --host or --skills-dir, not both');
|
|
887
|
+
if (purge && command !== 'uninstall') throw new Error('--purge is only supported by uninstall');
|
|
888
|
+
if (dryRun && command !== 'install' && command !== 'uninstall') {
|
|
889
|
+
throw new Error('--dry-run is only supported by install or complete uninstall');
|
|
890
|
+
}
|
|
891
|
+
if (dryRun && command === 'uninstall' && (host || skillsDir)) {
|
|
892
|
+
throw new Error('uninstall --dry-run previews complete uninstall; omit --host and --skills-dir');
|
|
893
|
+
}
|
|
894
|
+
if (linkSkills && command !== 'install') throw new Error('--link-skills is only supported by install');
|
|
895
|
+
if (linkSkills && (!host || host === 'all' || !SKILL_HOST_PATHS[host])) {
|
|
896
|
+
throw new Error('--link-skills requires one directory-based --host');
|
|
897
|
+
}
|
|
898
|
+
if (host && host !== 'all' && !SUPPORTED_HOSTS.includes(host)) {
|
|
899
|
+
throw new Error(`Unsupported host: ${host}. Choose: ${SUPPORTED_HOSTS.join(', ')}, all`);
|
|
900
|
+
}
|
|
901
|
+
return { command, host, skillsDir, purge, dryRun, linkSkills };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
export function packageVersion(packageRoot = PACKAGE_ROOT) {
|
|
905
|
+
return readJson(join(packageRoot, 'package.json')).version;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function displayPath(path) {
|
|
909
|
+
return basename(path) === 'marketplace.json' ? path : resolve(path);
|
|
910
|
+
}
|