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.
Files changed (68) hide show
  1. package/README.md +387 -0
  2. package/dist/commands/create.d.ts +2 -0
  3. package/dist/commands/create.js +260 -0
  4. package/dist/commands/doctor.d.ts +1 -0
  5. package/dist/commands/doctor.js +138 -0
  6. package/dist/commands/init.d.ts +3 -0
  7. package/dist/commands/init.js +252 -0
  8. package/dist/commands/install-plugin.d.ts +1 -0
  9. package/dist/commands/install-plugin.js +35 -0
  10. package/dist/commands/list.d.ts +3 -0
  11. package/dist/commands/list.js +123 -0
  12. package/dist/commands/remove.d.ts +6 -0
  13. package/dist/commands/remove.js +121 -0
  14. package/dist/commands/update.d.ts +4 -0
  15. package/dist/commands/update.js +141 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +156 -0
  18. package/dist/lib/__tests__/frontmatter.test.d.ts +1 -0
  19. package/dist/lib/__tests__/frontmatter.test.js +180 -0
  20. package/dist/lib/__tests__/paths.test.d.ts +1 -0
  21. package/dist/lib/__tests__/paths.test.js +29 -0
  22. package/dist/lib/__tests__/symlinks.test.d.ts +1 -0
  23. package/dist/lib/__tests__/symlinks.test.js +142 -0
  24. package/dist/lib/format.d.ts +13 -0
  25. package/dist/lib/format.js +47 -0
  26. package/dist/lib/frontmatter.d.ts +9 -0
  27. package/dist/lib/frontmatter.js +45 -0
  28. package/dist/lib/paths.d.ts +33 -0
  29. package/dist/lib/paths.js +111 -0
  30. package/dist/lib/plugins.d.ts +3 -0
  31. package/dist/lib/plugins.js +24 -0
  32. package/dist/lib/symlinks.d.ts +8 -0
  33. package/dist/lib/symlinks.js +56 -0
  34. package/dist/lib/templates.d.ts +26 -0
  35. package/dist/lib/templates.js +75 -0
  36. package/dist/types.d.ts +25 -0
  37. package/dist/types.js +1 -0
  38. package/package.json +47 -0
  39. package/templates/CLAUDE-CODE-BEST-PRACTICES.md +508 -0
  40. package/templates/CLOUD-CLI-GUIDE.md +405 -0
  41. package/templates/agents/architect.md +128 -0
  42. package/templates/agents/aws-specialist.md +104 -0
  43. package/templates/agents/azure-specialist.md +117 -0
  44. package/templates/agents/database-specialist.md +104 -0
  45. package/templates/agents/dod-specialist.md +101 -0
  46. package/templates/agents/gcp-specialist.md +124 -0
  47. package/templates/agents/idea-refiner.md +146 -0
  48. package/templates/agents/implementation-planner.md +149 -0
  49. package/templates/agents/nodejs-specialist.md +105 -0
  50. package/templates/agents/pr-reviewer.md +132 -0
  51. package/templates/agents/product-owner.md +88 -0
  52. package/templates/agents/project-manager.md +95 -0
  53. package/templates/agents/prompt-engineer.md +115 -0
  54. package/templates/agents/python-specialist.md +103 -0
  55. package/templates/agents/react-specialist.md +94 -0
  56. package/templates/agents/security-specialist.md +145 -0
  57. package/templates/agents/test-specialist.md +157 -0
  58. package/templates/agents/uxui-specialist.md +102 -0
  59. package/templates/global-CLAUDE.md +100 -0
  60. package/templates/skills/architecture-decision/SKILL.md +102 -0
  61. package/templates/skills/meet-dod/SKILL.md +124 -0
  62. package/templates/skills/pm-templates/SKILL.md +125 -0
  63. package/templates/skills/pr-template/SKILL.md +87 -0
  64. package/templates/skills/product-templates/SKILL.md +97 -0
  65. package/templates/skills/python-patterns/SKILL.md +123 -0
  66. package/templates/skills/security-checklist/SKILL.md +80 -0
  67. package/templates/skills/sql-templates/SKILL.md +93 -0
  68. package/templates/skills/ui-review-checklist/SKILL.md +73 -0
@@ -0,0 +1,4 @@
1
+ export declare function update(options?: {
2
+ force?: boolean;
3
+ dryRun?: boolean;
4
+ }): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;