claudiao 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +387 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +260 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +138 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +252 -0
- package/dist/commands/install-plugin.d.ts +1 -0
- package/dist/commands/install-plugin.js +35 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +123 -0
- package/dist/commands/remove.d.ts +6 -0
- package/dist/commands/remove.js +121 -0
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +141 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +156 -0
- package/dist/lib/__tests__/frontmatter.test.d.ts +1 -0
- package/dist/lib/__tests__/frontmatter.test.js +180 -0
- package/dist/lib/__tests__/paths.test.d.ts +1 -0
- package/dist/lib/__tests__/paths.test.js +29 -0
- package/dist/lib/__tests__/symlinks.test.d.ts +1 -0
- package/dist/lib/__tests__/symlinks.test.js +142 -0
- package/dist/lib/format.d.ts +13 -0
- package/dist/lib/format.js +47 -0
- package/dist/lib/frontmatter.d.ts +9 -0
- package/dist/lib/frontmatter.js +45 -0
- package/dist/lib/paths.d.ts +33 -0
- package/dist/lib/paths.js +111 -0
- package/dist/lib/plugins.d.ts +3 -0
- package/dist/lib/plugins.js +24 -0
- package/dist/lib/symlinks.d.ts +8 -0
- package/dist/lib/symlinks.js +56 -0
- package/dist/lib/templates.d.ts +26 -0
- package/dist/lib/templates.js +75 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
- package/templates/CLAUDE-CODE-BEST-PRACTICES.md +508 -0
- package/templates/CLOUD-CLI-GUIDE.md +405 -0
- package/templates/agents/architect.md +128 -0
- package/templates/agents/aws-specialist.md +104 -0
- package/templates/agents/azure-specialist.md +117 -0
- package/templates/agents/database-specialist.md +104 -0
- package/templates/agents/dod-specialist.md +101 -0
- package/templates/agents/gcp-specialist.md +124 -0
- package/templates/agents/idea-refiner.md +146 -0
- package/templates/agents/implementation-planner.md +149 -0
- package/templates/agents/nodejs-specialist.md +105 -0
- package/templates/agents/pr-reviewer.md +132 -0
- package/templates/agents/product-owner.md +88 -0
- package/templates/agents/project-manager.md +95 -0
- package/templates/agents/prompt-engineer.md +115 -0
- package/templates/agents/python-specialist.md +103 -0
- package/templates/agents/react-specialist.md +94 -0
- package/templates/agents/security-specialist.md +145 -0
- package/templates/agents/test-specialist.md +157 -0
- package/templates/agents/uxui-specialist.md +102 -0
- package/templates/global-CLAUDE.md +100 -0
- package/templates/skills/architecture-decision/SKILL.md +102 -0
- package/templates/skills/meet-dod/SKILL.md +124 -0
- package/templates/skills/pm-templates/SKILL.md +125 -0
- package/templates/skills/pr-template/SKILL.md +87 -0
- package/templates/skills/product-templates/SKILL.md +97 -0
- package/templates/skills/python-patterns/SKILL.md +123 -0
- package/templates/skills/security-checklist/SKILL.md +80 -0
- package/templates/skills/sql-templates/SKILL.md +93 -0
- package/templates/skills/ui-review-checklist/SKILL.md +73 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getExternalRepoPath, getAgentsSource, getSkillsSource, CLAUDE_AGENTS_DIR, CLAUDE_SKILLS_DIR } from '../lib/paths.js';
|
|
6
|
+
import { createSymlink, ensureDir, isSymlink, getSymlinkTarget } from '../lib/symlinks.js';
|
|
7
|
+
import { banner, success, warn, heading, info } from '../lib/format.js';
|
|
8
|
+
export function update(options) {
|
|
9
|
+
const force = options?.force ?? false;
|
|
10
|
+
const dryRun = options?.dryRun ?? false;
|
|
11
|
+
banner();
|
|
12
|
+
if (dryRun) {
|
|
13
|
+
info('[dry-run] Nenhuma alteracao sera feita');
|
|
14
|
+
console.log('');
|
|
15
|
+
}
|
|
16
|
+
const repoPath = getExternalRepoPath();
|
|
17
|
+
// Git pull if external repo
|
|
18
|
+
if (repoPath) {
|
|
19
|
+
heading('Atualizando repositorio...');
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
info(`[dry-run] Executaria git pull em ${repoPath}`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
try {
|
|
25
|
+
const result = execSync('git pull', { cwd: repoPath, encoding: 'utf-8' });
|
|
26
|
+
if (result.includes('Already up to date')) {
|
|
27
|
+
info('Repositorio ja esta atualizado');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
success('Git pull concluido');
|
|
31
|
+
console.log(chalk.dim(' ' + result.trim()));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
warn('Falha no git pull (pode ser um diretorio local sem remote)');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Re-link new agents
|
|
40
|
+
const agentsSource = getAgentsSource();
|
|
41
|
+
if (agentsSource && existsSync(agentsSource)) {
|
|
42
|
+
heading(force ? 'Re-linkando todos os agentes...' : 'Verificando novos agentes...');
|
|
43
|
+
ensureDir(CLAUDE_AGENTS_DIR);
|
|
44
|
+
const agentFiles = readdirSync(agentsSource).filter(f => f.endsWith('.md'));
|
|
45
|
+
let newAgents = 0;
|
|
46
|
+
let relinkedAgents = 0;
|
|
47
|
+
for (const file of agentFiles) {
|
|
48
|
+
const source = join(agentsSource, file);
|
|
49
|
+
const target = join(CLAUDE_AGENTS_DIR, file);
|
|
50
|
+
if (!existsSync(target)) {
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
info(`[dry-run] Linkaria novo agente: ${file.replace('.md', '')}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
createSymlink(source, target);
|
|
56
|
+
success(`Novo agente: ${file.replace('.md', '')}`);
|
|
57
|
+
}
|
|
58
|
+
newAgents++;
|
|
59
|
+
}
|
|
60
|
+
else if (force) {
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
info(`[dry-run] Re-linkaria: ${file.replace('.md', '')}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
createSymlink(source, target);
|
|
66
|
+
info(`Re-linkado: ${file.replace('.md', '')}`);
|
|
67
|
+
}
|
|
68
|
+
relinkedAgents++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (newAgents === 0 && relinkedAgents === 0) {
|
|
72
|
+
info('Nenhum agente novo');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Re-link new skills
|
|
76
|
+
const skillsSource = getSkillsSource();
|
|
77
|
+
if (skillsSource && existsSync(skillsSource)) {
|
|
78
|
+
heading(force ? 'Re-linkando todas as skills...' : 'Verificando novas skills...');
|
|
79
|
+
ensureDir(CLAUDE_SKILLS_DIR);
|
|
80
|
+
const skillDirs = readdirSync(skillsSource, { withFileTypes: true })
|
|
81
|
+
.filter(d => d.isDirectory())
|
|
82
|
+
.map(d => d.name);
|
|
83
|
+
let newSkills = 0;
|
|
84
|
+
let relinkedSkills = 0;
|
|
85
|
+
for (const dir of skillDirs) {
|
|
86
|
+
const source = join(skillsSource, dir);
|
|
87
|
+
const target = join(CLAUDE_SKILLS_DIR, dir);
|
|
88
|
+
if (!existsSync(target)) {
|
|
89
|
+
if (dryRun) {
|
|
90
|
+
info(`[dry-run] Linkaria nova skill: /${dir}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
createSymlink(source, target);
|
|
94
|
+
success(`Nova skill: /${dir}`);
|
|
95
|
+
}
|
|
96
|
+
newSkills++;
|
|
97
|
+
}
|
|
98
|
+
else if (force) {
|
|
99
|
+
if (dryRun) {
|
|
100
|
+
info(`[dry-run] Re-linkaria: /${dir}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
createSymlink(source, target);
|
|
104
|
+
info(`Re-linkada: /${dir}`);
|
|
105
|
+
}
|
|
106
|
+
relinkedSkills++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (newSkills === 0 && relinkedSkills === 0) {
|
|
110
|
+
info('Nenhuma skill nova');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Clean orphan symlinks
|
|
114
|
+
heading('Verificando symlinks orfaos...');
|
|
115
|
+
let orphans = 0;
|
|
116
|
+
if (existsSync(CLAUDE_AGENTS_DIR)) {
|
|
117
|
+
for (const file of readdirSync(CLAUDE_AGENTS_DIR)) {
|
|
118
|
+
const filePath = join(CLAUDE_AGENTS_DIR, file);
|
|
119
|
+
if (isSymlink(filePath)) {
|
|
120
|
+
const target = getSymlinkTarget(filePath);
|
|
121
|
+
const resolvedTarget = target ? resolve(dirname(filePath), target) : null;
|
|
122
|
+
if (resolvedTarget && !existsSync(resolvedTarget)) {
|
|
123
|
+
if (dryRun) {
|
|
124
|
+
warn(`[dry-run] Removeria symlink orfao: ${file}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
rmSync(filePath);
|
|
128
|
+
warn(`Symlink orfao removido: ${file}`);
|
|
129
|
+
}
|
|
130
|
+
orphans++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (orphans === 0) {
|
|
136
|
+
info('Nenhum symlink orfao');
|
|
137
|
+
}
|
|
138
|
+
console.log('');
|
|
139
|
+
success('Atualizacao concluida!');
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { init } from './commands/init.js';
|
|
7
|
+
import { createAgent, createSkill } from './commands/create.js';
|
|
8
|
+
import { listAgents, listSkills, listPlugins } from './commands/list.js';
|
|
9
|
+
import { doctor } from './commands/doctor.js';
|
|
10
|
+
import { removeAgent, removeSkill } from './commands/remove.js';
|
|
11
|
+
import { update } from './commands/update.js';
|
|
12
|
+
import { installPlugin } from './commands/install-plugin.js';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name('claudiao')
|
|
19
|
+
.description('Seu Claude Code no próximo nível. Agentes, skills e plugins em um comando.')
|
|
20
|
+
.version(pkg.version);
|
|
21
|
+
// ============================================================
|
|
22
|
+
// init
|
|
23
|
+
// ============================================================
|
|
24
|
+
program
|
|
25
|
+
.command('init')
|
|
26
|
+
.description('Configura tudo: instala agentes, skills, CLAUDE.md global e plugins opcionais')
|
|
27
|
+
.option('--dry-run', 'Mostra o que seria feito sem executar')
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
await init(options);
|
|
30
|
+
});
|
|
31
|
+
// ============================================================
|
|
32
|
+
// create
|
|
33
|
+
// ============================================================
|
|
34
|
+
const create = program
|
|
35
|
+
.command('create')
|
|
36
|
+
.description('Cria um novo agente ou skill');
|
|
37
|
+
create
|
|
38
|
+
.command('agent [description]')
|
|
39
|
+
.description('Cria um novo agente especializado a partir de uma descricao')
|
|
40
|
+
.addHelpText('after', `
|
|
41
|
+
Exemplos:
|
|
42
|
+
claudiao create agent
|
|
43
|
+
claudiao create agent "Especialista em Go com foco em microservices e gRPC"
|
|
44
|
+
claudiao create agent "Agente de DevOps focado em Kubernetes e Helm charts"
|
|
45
|
+
`)
|
|
46
|
+
.action(async (description) => {
|
|
47
|
+
await createAgent(description);
|
|
48
|
+
});
|
|
49
|
+
create
|
|
50
|
+
.command('skill [description]')
|
|
51
|
+
.description('Cria uma nova skill (slash command) com template/checklist')
|
|
52
|
+
.addHelpText('after', `
|
|
53
|
+
Exemplos:
|
|
54
|
+
claudiao create skill
|
|
55
|
+
claudiao create skill "Checklist de deploy para producao"
|
|
56
|
+
claudiao create skill "Template de postmortem de incidente"
|
|
57
|
+
`)
|
|
58
|
+
.action(async (description) => {
|
|
59
|
+
await createSkill(description);
|
|
60
|
+
});
|
|
61
|
+
// ============================================================
|
|
62
|
+
// list
|
|
63
|
+
// ============================================================
|
|
64
|
+
const list = program
|
|
65
|
+
.command('list')
|
|
66
|
+
.description('Lista agentes, skills ou plugins');
|
|
67
|
+
list
|
|
68
|
+
.command('agents')
|
|
69
|
+
.description('Lista todos os agentes instalados com descricao e categoria')
|
|
70
|
+
.action(() => {
|
|
71
|
+
listAgents();
|
|
72
|
+
});
|
|
73
|
+
list
|
|
74
|
+
.command('skills')
|
|
75
|
+
.description('Lista todas as skills instaladas (slash commands)')
|
|
76
|
+
.action(() => {
|
|
77
|
+
listSkills();
|
|
78
|
+
});
|
|
79
|
+
list
|
|
80
|
+
.command('plugins')
|
|
81
|
+
.description('Lista plugins da comunidade disponiveis')
|
|
82
|
+
.action(() => {
|
|
83
|
+
listPlugins();
|
|
84
|
+
});
|
|
85
|
+
// ============================================================
|
|
86
|
+
// install plugin
|
|
87
|
+
// ============================================================
|
|
88
|
+
program
|
|
89
|
+
.command('install')
|
|
90
|
+
.description('Instala um plugin da comunidade')
|
|
91
|
+
.argument('<type>', 'Tipo: plugin')
|
|
92
|
+
.argument('<name>', 'Nome do plugin (superpowers, get-shit-done, claude-mem)')
|
|
93
|
+
.addHelpText('after', `
|
|
94
|
+
Exemplos:
|
|
95
|
+
claudiao install plugin superpowers
|
|
96
|
+
claudiao install plugin get-shit-done
|
|
97
|
+
claudiao install plugin claude-mem
|
|
98
|
+
`)
|
|
99
|
+
.action((type, name) => {
|
|
100
|
+
if (type !== 'plugin') {
|
|
101
|
+
console.log(`Tipo "${type}" nao reconhecido. Use: claudiao install plugin <nome>`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
installPlugin(name);
|
|
105
|
+
});
|
|
106
|
+
// ============================================================
|
|
107
|
+
// remove
|
|
108
|
+
// ============================================================
|
|
109
|
+
const remove = program
|
|
110
|
+
.command('remove')
|
|
111
|
+
.description('Remove um agente ou skill');
|
|
112
|
+
remove
|
|
113
|
+
.command('agent <name>')
|
|
114
|
+
.description('Remove um agente instalado')
|
|
115
|
+
.option('--dry-run', 'Mostra o que seria feito sem executar')
|
|
116
|
+
.action(async (name, options) => {
|
|
117
|
+
await removeAgent(name, options);
|
|
118
|
+
});
|
|
119
|
+
remove
|
|
120
|
+
.command('skill <name>')
|
|
121
|
+
.description('Remove uma skill instalada')
|
|
122
|
+
.option('--dry-run', 'Mostra o que seria feito sem executar')
|
|
123
|
+
.action(async (name, options) => {
|
|
124
|
+
await removeSkill(name, options);
|
|
125
|
+
});
|
|
126
|
+
// ============================================================
|
|
127
|
+
// update
|
|
128
|
+
// ============================================================
|
|
129
|
+
program
|
|
130
|
+
.command('update')
|
|
131
|
+
.description('Atualiza agentes e skills do repositorio (git pull + relink)')
|
|
132
|
+
.option('--force', 'Re-linka todos os agentes e skills, nao apenas novos')
|
|
133
|
+
.option('--dry-run', 'Mostra o que seria feito sem executar')
|
|
134
|
+
.action((options) => {
|
|
135
|
+
update(options);
|
|
136
|
+
});
|
|
137
|
+
// ============================================================
|
|
138
|
+
// doctor
|
|
139
|
+
// ============================================================
|
|
140
|
+
program
|
|
141
|
+
.command('doctor')
|
|
142
|
+
.description('Verifica se tudo esta instalado e funcionando corretamente')
|
|
143
|
+
.action(() => {
|
|
144
|
+
doctor();
|
|
145
|
+
});
|
|
146
|
+
// ============================================================
|
|
147
|
+
// Parse
|
|
148
|
+
// ============================================================
|
|
149
|
+
try {
|
|
150
|
+
await program.parseAsync();
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
+
console.error(`\n \x1b[31m✗\x1b[0m Erro inesperado: ${message}\n`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { parseAgentFile, parseSkillFile, serializeAgent, serializeSkill } from '../frontmatter.js';
|
|
7
|
+
let tmpDir;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
10
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
function makeTmp() {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'claudiao-fm-'));
|
|
15
|
+
return tmpDir;
|
|
16
|
+
}
|
|
17
|
+
describe('parseAgentFile', () => {
|
|
18
|
+
it('should parse all frontmatter fields from an agent file', () => {
|
|
19
|
+
const dir = makeTmp();
|
|
20
|
+
const file = join(dir, 'test-agent.md');
|
|
21
|
+
writeFileSync(file, `---
|
|
22
|
+
name: architect
|
|
23
|
+
description: Decisoes de arquitetura
|
|
24
|
+
tools: Read, Grep, Bash
|
|
25
|
+
model: opus
|
|
26
|
+
category: development
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
You are an architecture specialist.
|
|
30
|
+
`);
|
|
31
|
+
const result = parseAgentFile(file);
|
|
32
|
+
expect(result.name).toBe('architect');
|
|
33
|
+
expect(result.description).toBe('Decisoes de arquitetura');
|
|
34
|
+
expect(result.tools).toEqual(['Read', 'Grep', 'Bash']);
|
|
35
|
+
expect(result.model).toBe('opus');
|
|
36
|
+
expect(result.category).toBe('development');
|
|
37
|
+
expect(result.content).toContain('architecture specialist');
|
|
38
|
+
});
|
|
39
|
+
it('should use defaults when frontmatter fields are missing', () => {
|
|
40
|
+
const dir = makeTmp();
|
|
41
|
+
const file = join(dir, 'minimal.md');
|
|
42
|
+
writeFileSync(file, `---
|
|
43
|
+
name: minimal
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
Body content.
|
|
47
|
+
`);
|
|
48
|
+
const result = parseAgentFile(file);
|
|
49
|
+
expect(result.name).toBe('minimal');
|
|
50
|
+
expect(result.description).toBe('');
|
|
51
|
+
expect(result.tools).toEqual(['']);
|
|
52
|
+
expect(result.model).toBe('opus');
|
|
53
|
+
expect(result.category).toBe('other');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('parseSkillFile', () => {
|
|
57
|
+
it('should parse all frontmatter fields from a skill file', () => {
|
|
58
|
+
const dir = makeTmp();
|
|
59
|
+
const file = join(dir, 'SKILL.md');
|
|
60
|
+
writeFileSync(file, `---
|
|
61
|
+
name: security-checklist
|
|
62
|
+
description: Checklist pre-deploy
|
|
63
|
+
allowed-tools: Read, Grep
|
|
64
|
+
model: sonnet
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# Security Checklist
|
|
68
|
+
`);
|
|
69
|
+
const result = parseSkillFile(file);
|
|
70
|
+
expect(result.name).toBe('security-checklist');
|
|
71
|
+
expect(result.description).toBe('Checklist pre-deploy');
|
|
72
|
+
expect(result.allowedTools).toEqual(['Read', 'Grep']);
|
|
73
|
+
expect(result.model).toBe('sonnet');
|
|
74
|
+
expect(result.content).toContain('Security Checklist');
|
|
75
|
+
});
|
|
76
|
+
it('should use defaults when frontmatter fields are missing', () => {
|
|
77
|
+
const dir = makeTmp();
|
|
78
|
+
const file = join(dir, 'SKILL.md');
|
|
79
|
+
writeFileSync(file, `---
|
|
80
|
+
name: basic
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
Content.
|
|
84
|
+
`);
|
|
85
|
+
const result = parseSkillFile(file);
|
|
86
|
+
expect(result.name).toBe('basic');
|
|
87
|
+
expect(result.description).toBe('');
|
|
88
|
+
expect(result.allowedTools).toEqual(['']);
|
|
89
|
+
expect(result.model).toBe('sonnet');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('serializeAgent', () => {
|
|
93
|
+
it('should produce valid markdown with frontmatter', () => {
|
|
94
|
+
const meta = {
|
|
95
|
+
name: 'test-agent',
|
|
96
|
+
description: 'A test agent',
|
|
97
|
+
tools: ['Read', 'Write', 'Bash'],
|
|
98
|
+
model: 'opus',
|
|
99
|
+
category: 'testing',
|
|
100
|
+
};
|
|
101
|
+
const output = serializeAgent(meta, '\nYou are a test agent.\n');
|
|
102
|
+
expect(output).toContain('name: test-agent');
|
|
103
|
+
expect(output).toContain('description: A test agent');
|
|
104
|
+
// gray-matter quotes values containing commas
|
|
105
|
+
expect(output).toContain("tools: 'Read, Write, Bash'");
|
|
106
|
+
expect(output).toContain('model: opus');
|
|
107
|
+
expect(output).toContain('category: testing');
|
|
108
|
+
expect(output).toContain('You are a test agent.');
|
|
109
|
+
});
|
|
110
|
+
it('should omit category when not provided', () => {
|
|
111
|
+
const meta = {
|
|
112
|
+
name: 'no-cat',
|
|
113
|
+
description: 'No category',
|
|
114
|
+
tools: ['Read'],
|
|
115
|
+
model: 'opus',
|
|
116
|
+
};
|
|
117
|
+
const output = serializeAgent(meta, '\nBody.\n');
|
|
118
|
+
// "category" appears inside "description: No category" but should not appear as its own key
|
|
119
|
+
expect(output).not.toMatch(/^category:/m);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('serializeSkill', () => {
|
|
123
|
+
it('should produce valid markdown with frontmatter', () => {
|
|
124
|
+
const meta = {
|
|
125
|
+
name: 'my-skill',
|
|
126
|
+
description: 'A skill',
|
|
127
|
+
allowedTools: ['Grep', 'Read'],
|
|
128
|
+
model: 'sonnet',
|
|
129
|
+
};
|
|
130
|
+
const output = serializeSkill(meta, '\nSkill body.\n');
|
|
131
|
+
expect(output).toContain('name: my-skill');
|
|
132
|
+
expect(output).toContain('description: A skill');
|
|
133
|
+
// gray-matter quotes values containing commas
|
|
134
|
+
expect(output).toContain("allowed-tools: 'Grep, Read'");
|
|
135
|
+
expect(output).toContain('model: sonnet');
|
|
136
|
+
expect(output).toContain('Skill body.');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('round-trip', () => {
|
|
140
|
+
it('should serialize then parse an agent and get the same data back', () => {
|
|
141
|
+
const dir = makeTmp();
|
|
142
|
+
const meta = {
|
|
143
|
+
name: 'round-trip-agent',
|
|
144
|
+
description: 'Round trip test',
|
|
145
|
+
tools: ['Read', 'Bash'],
|
|
146
|
+
model: 'opus',
|
|
147
|
+
category: 'testing',
|
|
148
|
+
};
|
|
149
|
+
const body = '\nAgent instructions here.\n';
|
|
150
|
+
const serialized = serializeAgent(meta, body);
|
|
151
|
+
const file = join(dir, 'agent.md');
|
|
152
|
+
writeFileSync(file, serialized);
|
|
153
|
+
const parsed = parseAgentFile(file);
|
|
154
|
+
expect(parsed.name).toBe(meta.name);
|
|
155
|
+
expect(parsed.description).toBe(meta.description);
|
|
156
|
+
expect(parsed.tools).toEqual(meta.tools);
|
|
157
|
+
expect(parsed.model).toBe(meta.model);
|
|
158
|
+
expect(parsed.category).toBe(meta.category);
|
|
159
|
+
expect(parsed.content).toContain('Agent instructions here.');
|
|
160
|
+
});
|
|
161
|
+
it('should serialize then parse a skill and get the same data back', () => {
|
|
162
|
+
const dir = makeTmp();
|
|
163
|
+
const meta = {
|
|
164
|
+
name: 'round-trip-skill',
|
|
165
|
+
description: 'Round trip skill test',
|
|
166
|
+
allowedTools: ['Grep', 'Write'],
|
|
167
|
+
model: 'haiku',
|
|
168
|
+
};
|
|
169
|
+
const body = '\nSkill instructions here.\n';
|
|
170
|
+
const serialized = serializeSkill(meta, body);
|
|
171
|
+
const file = join(dir, 'SKILL.md');
|
|
172
|
+
writeFileSync(file, serialized);
|
|
173
|
+
const parsed = parseSkillFile(file);
|
|
174
|
+
expect(parsed.name).toBe(meta.name);
|
|
175
|
+
expect(parsed.description).toBe(meta.description);
|
|
176
|
+
expect(parsed.allowedTools).toEqual(meta.allowedTools);
|
|
177
|
+
expect(parsed.model).toBe(meta.model);
|
|
178
|
+
expect(parsed.content).toContain('Skill instructions here.');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { CLAUDE_DIR, CLAUDE_AGENTS_DIR, CLAUDE_SKILLS_DIR, getTemplatesPath } from '../paths.js';
|
|
6
|
+
describe('path constants', () => {
|
|
7
|
+
it('CLAUDE_DIR should point to ~/.claude', () => {
|
|
8
|
+
expect(CLAUDE_DIR).toBe(join(homedir(), '.claude'));
|
|
9
|
+
});
|
|
10
|
+
it('CLAUDE_AGENTS_DIR should be inside CLAUDE_DIR', () => {
|
|
11
|
+
expect(CLAUDE_AGENTS_DIR).toBe(join(CLAUDE_DIR, 'agents'));
|
|
12
|
+
});
|
|
13
|
+
it('CLAUDE_SKILLS_DIR should be inside CLAUDE_DIR', () => {
|
|
14
|
+
expect(CLAUDE_SKILLS_DIR).toBe(join(CLAUDE_DIR, 'skills'));
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('getTemplatesPath', () => {
|
|
18
|
+
it('should return a path that exists', () => {
|
|
19
|
+
const templatesPath = getTemplatesPath();
|
|
20
|
+
expect(existsSync(templatesPath)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('should point to a directory containing agents and/or skills', () => {
|
|
23
|
+
const templatesPath = getTemplatesPath();
|
|
24
|
+
// At minimum the templates directory itself should exist
|
|
25
|
+
// The bundled templates dir ships with the package
|
|
26
|
+
expect(typeof templatesPath).toBe('string');
|
|
27
|
+
expect(templatesPath.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, existsSync, readFileSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { ensureDir, isSymlink, createSymlink, removeSymlink } from '../symlinks.js';
|
|
7
|
+
let tmpDir;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
10
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
function makeTmp() {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'claudiao-test-'));
|
|
15
|
+
return tmpDir;
|
|
16
|
+
}
|
|
17
|
+
describe('ensureDir', () => {
|
|
18
|
+
it('should create nested directories that do not exist', () => {
|
|
19
|
+
const dir = join(makeTmp(), 'a', 'b', 'c');
|
|
20
|
+
expect(existsSync(dir)).toBe(false);
|
|
21
|
+
ensureDir(dir);
|
|
22
|
+
expect(existsSync(dir)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('should not throw when directory already exists', () => {
|
|
25
|
+
const dir = makeTmp();
|
|
26
|
+
expect(() => ensureDir(dir)).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('isSymlink', () => {
|
|
30
|
+
it('should return true for a symlink', () => {
|
|
31
|
+
const dir = makeTmp();
|
|
32
|
+
const source = join(dir, 'source.txt');
|
|
33
|
+
const link = join(dir, 'link.txt');
|
|
34
|
+
writeFileSync(source, 'hello');
|
|
35
|
+
symlinkSync(source, link);
|
|
36
|
+
expect(isSymlink(link)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('should return false for a regular file', () => {
|
|
39
|
+
const dir = makeTmp();
|
|
40
|
+
const file = join(dir, 'file.txt');
|
|
41
|
+
writeFileSync(file, 'hello');
|
|
42
|
+
expect(isSymlink(file)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should return false for a non-existent path', () => {
|
|
45
|
+
expect(isSymlink('/tmp/does-not-exist-xyz-123')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('createSymlink', () => {
|
|
49
|
+
it('should create a new symlink (status: created)', () => {
|
|
50
|
+
const dir = makeTmp();
|
|
51
|
+
const source = join(dir, 'source.md');
|
|
52
|
+
const target = join(dir, 'installed', 'agent.md');
|
|
53
|
+
writeFileSync(source, '# Agent');
|
|
54
|
+
const result = createSymlink(source, target);
|
|
55
|
+
expect(result.status).toBe('created');
|
|
56
|
+
expect(isSymlink(target)).toBe(true);
|
|
57
|
+
expect(readFileSync(target, 'utf-8')).toBe('# Agent');
|
|
58
|
+
});
|
|
59
|
+
it('should skip if symlink already points to the same target (status: skipped)', () => {
|
|
60
|
+
const dir = makeTmp();
|
|
61
|
+
const source = join(dir, 'source.md');
|
|
62
|
+
const target = join(dir, 'link.md');
|
|
63
|
+
writeFileSync(source, 'content');
|
|
64
|
+
symlinkSync(source, target);
|
|
65
|
+
const result = createSymlink(source, target);
|
|
66
|
+
expect(result.status).toBe('skipped');
|
|
67
|
+
expect(isSymlink(target)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('should update symlink pointing to a different target (status: updated)', () => {
|
|
70
|
+
const dir = makeTmp();
|
|
71
|
+
const oldSource = join(dir, 'old.md');
|
|
72
|
+
const newSource = join(dir, 'new.md');
|
|
73
|
+
const target = join(dir, 'link.md');
|
|
74
|
+
writeFileSync(oldSource, 'old');
|
|
75
|
+
writeFileSync(newSource, 'new');
|
|
76
|
+
symlinkSync(oldSource, target);
|
|
77
|
+
const result = createSymlink(newSource, target);
|
|
78
|
+
expect(result.status).toBe('updated');
|
|
79
|
+
expect(isSymlink(target)).toBe(true);
|
|
80
|
+
expect(readFileSync(target, 'utf-8')).toBe('new');
|
|
81
|
+
});
|
|
82
|
+
it('should backup a non-symlink file before creating symlink (status: backup)', () => {
|
|
83
|
+
const dir = makeTmp();
|
|
84
|
+
const source = join(dir, 'source.md');
|
|
85
|
+
const target = join(dir, 'existing.md');
|
|
86
|
+
writeFileSync(source, 'from source');
|
|
87
|
+
writeFileSync(target, 'original content');
|
|
88
|
+
const result = createSymlink(source, target);
|
|
89
|
+
expect(result.status).toBe('backup');
|
|
90
|
+
expect(isSymlink(target)).toBe(true);
|
|
91
|
+
expect(readFileSync(target, 'utf-8')).toBe('from source');
|
|
92
|
+
expect(existsSync(target + '.bak')).toBe(true);
|
|
93
|
+
expect(readFileSync(target + '.bak', 'utf-8')).toBe('original content');
|
|
94
|
+
});
|
|
95
|
+
it('should create parent directories for the target', () => {
|
|
96
|
+
const dir = makeTmp();
|
|
97
|
+
const source = join(dir, 'source.md');
|
|
98
|
+
const target = join(dir, 'deep', 'nested', 'link.md');
|
|
99
|
+
writeFileSync(source, 'data');
|
|
100
|
+
createSymlink(source, target);
|
|
101
|
+
expect(existsSync(join(dir, 'deep', 'nested'))).toBe(true);
|
|
102
|
+
expect(isSymlink(target)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('removeSymlink', () => {
|
|
106
|
+
it('should remove a symlink and return true', () => {
|
|
107
|
+
const dir = makeTmp();
|
|
108
|
+
const source = join(dir, 'source.md');
|
|
109
|
+
const link = join(dir, 'link.md');
|
|
110
|
+
writeFileSync(source, 'content');
|
|
111
|
+
symlinkSync(source, link);
|
|
112
|
+
const removed = removeSymlink(link);
|
|
113
|
+
expect(removed).toBe(true);
|
|
114
|
+
expect(existsSync(link)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
it('should restore backup after removing symlink', () => {
|
|
117
|
+
const dir = makeTmp();
|
|
118
|
+
const source = join(dir, 'source.md');
|
|
119
|
+
const target = join(dir, 'file.md');
|
|
120
|
+
writeFileSync(source, 'new');
|
|
121
|
+
writeFileSync(target, 'original');
|
|
122
|
+
// createSymlink backs up the original file
|
|
123
|
+
createSymlink(source, target);
|
|
124
|
+
expect(existsSync(target + '.bak')).toBe(true);
|
|
125
|
+
// removeSymlink restores the backup
|
|
126
|
+
const removed = removeSymlink(target);
|
|
127
|
+
expect(removed).toBe(true);
|
|
128
|
+
expect(existsSync(target)).toBe(true);
|
|
129
|
+
expect(isSymlink(target)).toBe(false);
|
|
130
|
+
expect(readFileSync(target, 'utf-8')).toBe('original');
|
|
131
|
+
expect(existsSync(target + '.bak')).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
it('should return false for a non-symlink path', () => {
|
|
134
|
+
const dir = makeTmp();
|
|
135
|
+
const file = join(dir, 'regular.md');
|
|
136
|
+
writeFileSync(file, 'content');
|
|
137
|
+
expect(removeSymlink(file)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
it('should return false for a non-existent path', () => {
|
|
140
|
+
expect(removeSymlink('/tmp/does-not-exist-xyz-456')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function banner(): void;
|
|
2
|
+
export declare function success(msg: string): void;
|
|
3
|
+
export declare function warn(msg: string): void;
|
|
4
|
+
export declare function error(msg: string): void;
|
|
5
|
+
export declare function info(msg: string): void;
|
|
6
|
+
export declare function dim(msg: string): void;
|
|
7
|
+
export declare function heading(msg: string): void;
|
|
8
|
+
export declare function separator(): void;
|
|
9
|
+
export declare function table(rows: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
}>): void;
|