@biggora/claude-plugins 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/.claude/settings.local.json +13 -0
- package/CLAUDE.md +55 -0
- package/LICENSE +1 -1
- package/README.md +208 -39
- package/bin/cli.js +39 -0
- package/package.json +30 -17
- package/registry/registry.json +166 -1
- package/registry/schema.json +10 -0
- package/src/commands/skills/add.js +194 -0
- package/src/commands/skills/list.js +52 -0
- package/src/commands/skills/remove.js +27 -0
- package/src/commands/skills/update.js +74 -0
- package/src/config.js +5 -0
- package/src/skills/codex-cli/SKILL.md +265 -0
- package/src/skills/commafeed-api/SKILL.md +1012 -0
- package/src/skills/gemini-cli/SKILL.md +379 -0
- package/src/skills/gemini-cli/references/commands.md +145 -0
- package/src/skills/gemini-cli/references/configuration.md +182 -0
- package/src/skills/gemini-cli/references/headless-and-scripting.md +181 -0
- package/src/skills/gemini-cli/references/mcp-and-extensions.md +254 -0
- package/src/skills/n8n-api/SKILL.md +623 -0
- package/src/skills/notebook-lm/SKILL.md +217 -0
- package/src/skills/notebook-lm/references/artifact-options.md +168 -0
- package/src/skills/notebook-lm/references/auth.md +58 -0
- package/src/skills/notebook-lm/references/workflows.md +144 -0
- package/src/skills/screen-recording/SKILL.md +309 -0
- package/src/skills/screen-recording/references/approach1-programmatic.md +311 -0
- package/src/skills/screen-recording/references/approach2-xvfb.md +232 -0
- package/src/skills/screen-recording/references/design-patterns.md +168 -0
- package/src/skills/test-mobile-app/SKILL.md +212 -0
- package/src/skills/test-mobile-app/references/report-template.md +95 -0
- package/src/skills/test-mobile-app/references/setup-appium.md +154 -0
- package/src/skills/test-mobile-app/scripts/analyze_apk.py +164 -0
- package/src/skills/test-mobile-app/scripts/check_environment.py +116 -0
- package/src/skills/test-mobile-app/scripts/generate_report.py +250 -0
- package/src/skills/test-mobile-app/scripts/run_tests.py +326 -0
- package/src/skills/test-web-ui/SKILL.md +232 -0
- package/src/skills/test-web-ui/references/test_case_schema.md +102 -0
- package/src/skills/test-web-ui/scripts/discover.py +176 -0
- package/src/skills/test-web-ui/scripts/generate_report.py +237 -0
- package/src/skills/test-web-ui/scripts/run_tests.py +296 -0
- package/src/skills/text-to-speech/SKILL.md +236 -0
- package/src/skills/text-to-speech/references/espeak-cli.md +277 -0
- package/src/skills/text-to-speech/references/kokoro-onnx.md +124 -0
- package/src/skills/text-to-speech/references/online-engines.md +128 -0
- package/src/skills/text-to-speech/references/pyttsx3-espeak.md +143 -0
- package/src/skills/tm-search/SKILL.md +240 -0
- package/src/skills/tm-search/references/field-guide.md +79 -0
- package/src/skills/tm-search/references/scraping-fallback.md +140 -0
- package/src/skills/tm-search/scripts/tm_search.py +375 -0
- package/src/skills/wp-rest-api/SKILL.md +114 -0
- package/src/skills/wp-rest-api/references/authentication.md +18 -0
- package/src/skills/wp-rest-api/references/custom-content-types.md +20 -0
- package/src/skills/wp-rest-api/references/discovery-and-params.md +20 -0
- package/src/skills/wp-rest-api/references/responses-and-fields.md +30 -0
- package/src/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
- package/src/skills/wp-rest-api/references/schema.md +22 -0
- package/src/skills/youtube-search/SKILL.md +412 -0
- package/src/skills/youtube-search/references/parsing-examples.md +159 -0
- package/src/skills/youtube-search/references/youtube-api-quota.md +85 -0
- package/src/skills/youtube-thumbnail/SKILL.md +1060 -0
- package/tests/commands/info.test.js +49 -0
- package/tests/commands/install.test.js +36 -0
- package/tests/commands/list.test.js +66 -0
- package/tests/commands/publish.test.js +182 -0
- package/tests/commands/search.test.js +45 -0
- package/tests/commands/uninstall.test.js +29 -0
- package/tests/commands/update.test.js +59 -0
- package/tests/functional/skills-lifecycle.test.js +293 -0
- package/tests/helpers/fixtures.js +63 -0
- package/tests/integration/cli.test.js +83 -0
- package/tests/skills/add.test.js +138 -0
- package/tests/skills/list.test.js +63 -0
- package/tests/skills/remove.test.js +38 -0
- package/tests/skills/update.test.js +60 -0
- package/tests/unit/config.test.js +31 -0
- package/tests/unit/registry.test.js +79 -0
- package/tests/unit/utils.test.js +150 -0
- package/tests/validation/registry-schema.test.js +112 -0
- package/tests/validation/skills-validation.test.js +96 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { findPlugin } from '../../src/registry.js';
|
|
4
|
+
import { MOCK_REGISTRY } from '../helpers/fixtures.js';
|
|
5
|
+
|
|
6
|
+
describe('info command logic', () => {
|
|
7
|
+
it('returns full plugin metadata when found', () => {
|
|
8
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'test-plugin');
|
|
9
|
+
assert.ok(plugin);
|
|
10
|
+
assert.equal(plugin.name, 'test-plugin');
|
|
11
|
+
assert.equal(plugin.version, '1.0.0');
|
|
12
|
+
assert.equal(plugin.description, 'A test plugin for unit tests');
|
|
13
|
+
assert.equal(plugin.author.name, 'tester');
|
|
14
|
+
assert.equal(plugin.license, 'MIT');
|
|
15
|
+
assert.equal(plugin.category, 'testing');
|
|
16
|
+
assert.equal(plugin.repository, 'https://github.com/test/test-plugin');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns undefined for non-existent plugin', () => {
|
|
20
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'does-not-exist');
|
|
21
|
+
assert.equal(plugin, undefined);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('handles plugin with missing optional fields', () => {
|
|
25
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'no-extras');
|
|
26
|
+
assert.ok(plugin);
|
|
27
|
+
assert.equal(plugin.author, undefined);
|
|
28
|
+
assert.equal(plugin.license, undefined);
|
|
29
|
+
assert.equal(plugin.category, undefined);
|
|
30
|
+
assert.equal(plugin.keywords, undefined);
|
|
31
|
+
assert.equal(plugin.commands, undefined);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('plugin fields default correctly for display', () => {
|
|
35
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'no-extras');
|
|
36
|
+
// Mirror how info.js handles defaults
|
|
37
|
+
const author = plugin.author?.name || '-';
|
|
38
|
+
const license = plugin.license || '-';
|
|
39
|
+
const category = plugin.category || '-';
|
|
40
|
+
const keywords = (plugin.keywords || []).join(', ') || '-';
|
|
41
|
+
const commands = (plugin.commands || []).join(', ') || '-';
|
|
42
|
+
|
|
43
|
+
assert.equal(author, '-');
|
|
44
|
+
assert.equal(license, '-');
|
|
45
|
+
assert.equal(category, '-');
|
|
46
|
+
assert.equal(keywords, '-');
|
|
47
|
+
assert.equal(commands, '-');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { findPlugin } from '../../src/registry.js';
|
|
4
|
+
import { MOCK_REGISTRY } from '../helpers/fixtures.js';
|
|
5
|
+
|
|
6
|
+
describe('install command logic', () => {
|
|
7
|
+
it('findPlugin returns plugin when found', () => {
|
|
8
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'test-plugin');
|
|
9
|
+
assert.ok(plugin);
|
|
10
|
+
assert.equal(plugin.name, 'test-plugin');
|
|
11
|
+
assert.equal(plugin.repository, 'https://github.com/test/test-plugin');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('findPlugin returns undefined when not found', () => {
|
|
15
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'nonexistent');
|
|
16
|
+
assert.equal(plugin, undefined);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('plugin repository URL is used for git clone', () => {
|
|
20
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'test-plugin');
|
|
21
|
+
const gitUrl = `${plugin.repository}.git`;
|
|
22
|
+
assert.equal(gitUrl, 'https://github.com/test/test-plugin.git');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('plugin has commands array for post-install display', () => {
|
|
26
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'test-plugin');
|
|
27
|
+
assert.ok(Array.isArray(plugin.commands));
|
|
28
|
+
assert.equal(plugin.commands.length, 1);
|
|
29
|
+
assert.equal(plugin.commands[0], '/test');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('plugin without commands has empty or undefined commands', () => {
|
|
33
|
+
const plugin = findPlugin(MOCK_REGISTRY, 'no-extras');
|
|
34
|
+
assert.ok(!plugin.commands || plugin.commands.length === 0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync, mkdtempSync, readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
describe('list command logic', () => {
|
|
8
|
+
let tmp;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (tmp) {
|
|
12
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
13
|
+
tmp = null;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('readdirSync returns empty for empty directory', () => {
|
|
18
|
+
tmp = mkdtempSync(join(tmpdir(), 'list-test-'));
|
|
19
|
+
const entries = readdirSync(tmp, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
20
|
+
assert.equal(entries.length, 0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('detects plugin directories', () => {
|
|
24
|
+
tmp = mkdtempSync(join(tmpdir(), 'list-test-'));
|
|
25
|
+
mkdirSync(join(tmp, 'plugin-a'));
|
|
26
|
+
mkdirSync(join(tmp, 'plugin-b'));
|
|
27
|
+
writeFileSync(join(tmp, 'not-a-dir.txt'), 'file');
|
|
28
|
+
|
|
29
|
+
const entries = readdirSync(tmp, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
30
|
+
assert.equal(entries.length, 2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('reads plugin manifest for version and description', () => {
|
|
34
|
+
tmp = mkdtempSync(join(tmpdir(), 'list-test-'));
|
|
35
|
+
const pluginDir = join(tmp, 'my-plugin');
|
|
36
|
+
mkdirSync(join(pluginDir, '.claude-plugin'), { recursive: true });
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(pluginDir, '.claude-plugin', 'plugin.json'),
|
|
39
|
+
JSON.stringify({ name: 'my-plugin', version: '1.2.3', description: 'Test desc' })
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const manifestPath = join(pluginDir, '.claude-plugin', 'plugin.json');
|
|
43
|
+
assert.ok(existsSync(manifestPath));
|
|
44
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
45
|
+
assert.equal(manifest.version, '1.2.3');
|
|
46
|
+
assert.equal(manifest.description, 'Test desc');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles missing manifest gracefully', () => {
|
|
50
|
+
tmp = mkdtempSync(join(tmpdir(), 'list-test-'));
|
|
51
|
+
mkdirSync(join(tmp, 'plugin-no-manifest'));
|
|
52
|
+
const manifestPath = join(tmp, 'plugin-no-manifest', '.claude-plugin', 'plugin.json');
|
|
53
|
+
assert.equal(existsSync(manifestPath), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles invalid JSON in manifest', () => {
|
|
57
|
+
tmp = mkdtempSync(join(tmpdir(), 'list-test-'));
|
|
58
|
+
const pluginDir = join(tmp, 'bad-json');
|
|
59
|
+
mkdirSync(join(pluginDir, '.claude-plugin'), { recursive: true });
|
|
60
|
+
writeFileSync(join(pluginDir, '.claude-plugin', 'plugin.json'), '{invalid json}');
|
|
61
|
+
|
|
62
|
+
assert.throws(() => {
|
|
63
|
+
JSON.parse(readFileSync(join(pluginDir, '.claude-plugin', 'plugin.json'), 'utf-8'));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, mkdtempSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { VALID_PLUGIN_MANIFEST } from '../helpers/fixtures.js';
|
|
7
|
+
|
|
8
|
+
describe('publish command validation logic', () => {
|
|
9
|
+
let tmp;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (tmp) {
|
|
13
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
14
|
+
tmp = null;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function createPluginDir(manifest = VALID_PLUGIN_MANIFEST, includeReadme = true) {
|
|
19
|
+
tmp = mkdtempSync(join(tmpdir(), 'publish-test-'));
|
|
20
|
+
mkdirSync(join(tmp, '.claude-plugin'), { recursive: true });
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(tmp, '.claude-plugin', 'plugin.json'),
|
|
23
|
+
JSON.stringify(manifest, null, 2)
|
|
24
|
+
);
|
|
25
|
+
if (includeReadme) {
|
|
26
|
+
writeFileSync(join(tmp, 'README.md'), '# Test Plugin');
|
|
27
|
+
}
|
|
28
|
+
return tmp;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('required files', () => {
|
|
32
|
+
it('detects missing .claude-plugin/plugin.json', () => {
|
|
33
|
+
tmp = mkdtempSync(join(tmpdir(), 'publish-test-'));
|
|
34
|
+
writeFileSync(join(tmp, 'README.md'), '# Test');
|
|
35
|
+
assert.equal(existsSync(join(tmp, '.claude-plugin', 'plugin.json')), false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('detects missing README.md', () => {
|
|
39
|
+
tmp = mkdtempSync(join(tmpdir(), 'publish-test-'));
|
|
40
|
+
mkdirSync(join(tmp, '.claude-plugin'));
|
|
41
|
+
writeFileSync(join(tmp, '.claude-plugin', 'plugin.json'), '{}');
|
|
42
|
+
assert.equal(existsSync(join(tmp, 'README.md')), false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('validates both required files exist', () => {
|
|
46
|
+
const dir = createPluginDir();
|
|
47
|
+
assert.ok(existsSync(join(dir, '.claude-plugin', 'plugin.json')));
|
|
48
|
+
assert.ok(existsSync(join(dir, 'README.md')));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('manifest parsing', () => {
|
|
53
|
+
it('parses valid JSON', () => {
|
|
54
|
+
const dir = createPluginDir();
|
|
55
|
+
const manifest = JSON.parse(readFileSync(join(dir, '.claude-plugin', 'plugin.json'), 'utf-8'));
|
|
56
|
+
assert.equal(manifest.name, 'test-plugin');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('detects invalid JSON', () => {
|
|
60
|
+
tmp = mkdtempSync(join(tmpdir(), 'publish-test-'));
|
|
61
|
+
mkdirSync(join(tmp, '.claude-plugin'));
|
|
62
|
+
writeFileSync(join(tmp, '.claude-plugin', 'plugin.json'), '{bad json}');
|
|
63
|
+
writeFileSync(join(tmp, 'README.md'), '# Test');
|
|
64
|
+
|
|
65
|
+
assert.throws(() => {
|
|
66
|
+
JSON.parse(readFileSync(join(tmp, '.claude-plugin', 'plugin.json'), 'utf-8'));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('required fields', () => {
|
|
72
|
+
it('validates name is present', () => {
|
|
73
|
+
const { name, ...noName } = VALID_PLUGIN_MANIFEST;
|
|
74
|
+
assert.equal(noName.name, undefined);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('validates version is present', () => {
|
|
78
|
+
const { version, ...noVersion } = VALID_PLUGIN_MANIFEST;
|
|
79
|
+
assert.equal(noVersion.version, undefined);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('validates description is present', () => {
|
|
83
|
+
const { description, ...noDesc } = VALID_PLUGIN_MANIFEST;
|
|
84
|
+
assert.equal(noDesc.description, undefined);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('name validation pattern', () => {
|
|
89
|
+
const pattern = /^[a-z0-9-]+$/;
|
|
90
|
+
|
|
91
|
+
it('accepts valid names', () => {
|
|
92
|
+
assert.ok(pattern.test('my-plugin'));
|
|
93
|
+
assert.ok(pattern.test('plugin123'));
|
|
94
|
+
assert.ok(pattern.test('a-b-c'));
|
|
95
|
+
assert.ok(pattern.test('test'));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('rejects names with uppercase', () => {
|
|
99
|
+
assert.equal(pattern.test('MyPlugin'), false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects names with spaces', () => {
|
|
103
|
+
assert.equal(pattern.test('my plugin'), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects names with underscores', () => {
|
|
107
|
+
assert.equal(pattern.test('my_plugin'), false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('rejects names with special characters', () => {
|
|
111
|
+
assert.equal(pattern.test('my@plugin'), false);
|
|
112
|
+
assert.equal(pattern.test('my.plugin'), false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('rejects empty name', () => {
|
|
116
|
+
assert.equal(pattern.test(''), false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('recommended fields (warnings)', () => {
|
|
121
|
+
it('detects missing repository', () => {
|
|
122
|
+
const { repository, ...noRepo } = VALID_PLUGIN_MANIFEST;
|
|
123
|
+
assert.equal(noRepo.repository, undefined);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('detects missing keywords', () => {
|
|
127
|
+
const { keywords, ...noKw } = VALID_PLUGIN_MANIFEST;
|
|
128
|
+
assert.equal(noKw.keywords, undefined);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('detects missing license', () => {
|
|
132
|
+
const { license, ...noLic } = VALID_PLUGIN_MANIFEST;
|
|
133
|
+
assert.equal(noLic.license, undefined);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('registry entry generation', () => {
|
|
138
|
+
it('generates correct entry structure', () => {
|
|
139
|
+
const manifest = VALID_PLUGIN_MANIFEST;
|
|
140
|
+
const repoUrl = manifest.repository || '';
|
|
141
|
+
|
|
142
|
+
const entry = {
|
|
143
|
+
name: manifest.name,
|
|
144
|
+
version: manifest.version,
|
|
145
|
+
description: manifest.description,
|
|
146
|
+
author: manifest.author || { name: 'unknown' },
|
|
147
|
+
repository: repoUrl.replace(/\.git$/, ''),
|
|
148
|
+
keywords: manifest.keywords || [],
|
|
149
|
+
license: manifest.license || 'MIT',
|
|
150
|
+
commands: manifest.commands || [],
|
|
151
|
+
category: manifest.category || 'other',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
assert.equal(entry.name, 'test-plugin');
|
|
155
|
+
assert.equal(entry.version, '1.0.0');
|
|
156
|
+
assert.equal(entry.author.name, 'tester');
|
|
157
|
+
assert.equal(entry.repository, 'https://github.com/test/test-plugin');
|
|
158
|
+
assert.deepStrictEqual(entry.keywords, ['test']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('strips .git suffix from repository URL', () => {
|
|
162
|
+
const url = 'https://github.com/test/repo.git';
|
|
163
|
+
assert.equal(url.replace(/\.git$/, ''), 'https://github.com/test/repo');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('defaults author to unknown when missing', () => {
|
|
167
|
+
const author = undefined;
|
|
168
|
+
const result = author || { name: 'unknown' };
|
|
169
|
+
assert.equal(result.name, 'unknown');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('defaults category to other when missing', () => {
|
|
173
|
+
const category = undefined;
|
|
174
|
+
assert.equal(category || 'other', 'other');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('defaults license to MIT when missing', () => {
|
|
178
|
+
const license = undefined;
|
|
179
|
+
assert.equal(license || 'MIT', 'MIT');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { MOCK_REGISTRY } from '../helpers/fixtures.js';
|
|
4
|
+
|
|
5
|
+
// We need to mock modules before importing the search command
|
|
6
|
+
// Since mock.module() requires careful ordering, we test via CLI integration
|
|
7
|
+
// For pure unit tests, we test the underlying functions directly
|
|
8
|
+
|
|
9
|
+
import { searchPlugins } from '../../src/registry.js';
|
|
10
|
+
|
|
11
|
+
describe('search command logic', () => {
|
|
12
|
+
let logSpy, errorSpy;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
logSpy = mock.method(console, 'log', () => {});
|
|
16
|
+
errorSpy = mock.method(console, 'error', () => {});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
logSpy.mock.restore();
|
|
21
|
+
errorSpy.mock.restore();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('lists all plugins when no query given', () => {
|
|
25
|
+
const results = searchPlugins(MOCK_REGISTRY, '');
|
|
26
|
+
assert.equal(results.length, MOCK_REGISTRY.plugins.length);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('filters results with query', () => {
|
|
30
|
+
const results = searchPlugins(MOCK_REGISTRY, 'test');
|
|
31
|
+
assert.ok(results.length > 0);
|
|
32
|
+
assert.ok(results.length < MOCK_REGISTRY.plugins.length);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns empty for no match', () => {
|
|
36
|
+
const results = searchPlugins(MOCK_REGISTRY, 'zzz-nonexistent');
|
|
37
|
+
assert.equal(results.length, 0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('search is case-insensitive', () => {
|
|
41
|
+
const r1 = searchPlugins(MOCK_REGISTRY, 'TEST');
|
|
42
|
+
const r2 = searchPlugins(MOCK_REGISTRY, 'test');
|
|
43
|
+
assert.equal(r1.length, r2.length);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, mkdtempSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
describe('uninstall command logic', () => {
|
|
8
|
+
it('rmSync with recursive and force removes directory', () => {
|
|
9
|
+
const tmp = mkdtempSync(join(tmpdir(), 'uninstall-test-'));
|
|
10
|
+
const pluginDir = join(tmp, 'test-plugin');
|
|
11
|
+
mkdirSync(pluginDir);
|
|
12
|
+
assert.ok(existsSync(pluginDir));
|
|
13
|
+
|
|
14
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
15
|
+
assert.ok(!existsSync(pluginDir));
|
|
16
|
+
|
|
17
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rmSync with force does not throw for non-existent path', () => {
|
|
21
|
+
assert.doesNotThrow(() => {
|
|
22
|
+
rmSync(join(tmpdir(), 'nonexistent-' + Date.now()), { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('existsSync returns false for non-existent plugin', () => {
|
|
27
|
+
assert.equal(existsSync(join(tmpdir(), 'no-such-plugin-' + Date.now())), false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, mkdtempSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
describe('update command logic', () => {
|
|
8
|
+
let tmp;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (tmp) {
|
|
12
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
13
|
+
tmp = null;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('plugin must exist to update', () => {
|
|
18
|
+
tmp = mkdtempSync(join(tmpdir(), 'update-test-'));
|
|
19
|
+
assert.equal(existsSync(join(tmp, 'nonexistent')), false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('detects if plugin has .git directory', () => {
|
|
23
|
+
tmp = mkdtempSync(join(tmpdir(), 'update-test-'));
|
|
24
|
+
const pluginDir = join(tmp, 'my-plugin');
|
|
25
|
+
mkdirSync(pluginDir);
|
|
26
|
+
assert.equal(existsSync(join(pluginDir, '.git')), false);
|
|
27
|
+
|
|
28
|
+
mkdirSync(join(pluginDir, '.git'));
|
|
29
|
+
assert.equal(existsSync(join(pluginDir, '.git')), true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('lists all plugin directories for update-all', () => {
|
|
33
|
+
tmp = mkdtempSync(join(tmpdir(), 'update-test-'));
|
|
34
|
+
mkdirSync(join(tmp, 'plugin-a'));
|
|
35
|
+
mkdirSync(join(tmp, 'plugin-b'));
|
|
36
|
+
mkdirSync(join(tmp, 'plugin-c'));
|
|
37
|
+
|
|
38
|
+
const entries = readdirSync(tmp, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
39
|
+
assert.equal(entries.length, 3);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('empty plugins directory means nothing to update', () => {
|
|
43
|
+
tmp = mkdtempSync(join(tmpdir(), 'update-test-'));
|
|
44
|
+
const entries = readdirSync(tmp, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
45
|
+
assert.equal(entries.length, 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('git pull output containing "Already up to date" means no changes', () => {
|
|
49
|
+
const output = 'Already up to date.\n';
|
|
50
|
+
assert.ok(output.includes('Already up to date'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('counts updated plugins correctly', () => {
|
|
54
|
+
const results = [true, false, true, true, false];
|
|
55
|
+
const updated = results.filter(Boolean).length;
|
|
56
|
+
assert.equal(updated, 3);
|
|
57
|
+
assert.equal(`${updated}/${results.length}`, '3/5');
|
|
58
|
+
});
|
|
59
|
+
});
|