cairn-work 0.2.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 +241 -0
- package/bin/cairn.js +79 -0
- package/bin/cairn.test.js +26 -0
- package/lib/agents/claude-code.js +91 -0
- package/lib/agents/clawdbot.js +85 -0
- package/lib/agents/cursor.js +107 -0
- package/lib/agents/detect.js +60 -0
- package/lib/agents/detect.test.js +108 -0
- package/lib/agents/generic.js +60 -0
- package/lib/commands/create.js +158 -0
- package/lib/commands/doctor.js +137 -0
- package/lib/commands/init.js +28 -0
- package/lib/commands/onboard.js +141 -0
- package/lib/commands/update-skill.js +69 -0
- package/lib/commands/update.js +84 -0
- package/lib/setup/workspace.js +147 -0
- package/lib/setup/workspace.test.js +123 -0
- package/package.json +55 -0
- package/skills/agent-skill.template.md +348 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { detectAgents, getAgentName, isSupportedAgent } from './detect.js';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
|
|
7
|
+
describe('Agent Detection', () => {
|
|
8
|
+
let originalEnv;
|
|
9
|
+
let testHome;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Save original environment
|
|
13
|
+
originalEnv = { ...process.env };
|
|
14
|
+
|
|
15
|
+
// Create temporary home directory for testing
|
|
16
|
+
testHome = join(tmpdir(), `cairn-test-${Date.now()}`);
|
|
17
|
+
mkdirSync(testHome, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Restore environment
|
|
22
|
+
process.env = originalEnv;
|
|
23
|
+
|
|
24
|
+
// Clean up test directory
|
|
25
|
+
if (existsSync(testHome)) {
|
|
26
|
+
rmSync(testHome, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('detectAgents returns generic when no agents found', () => {
|
|
31
|
+
// Clear environment variables that might detect agents
|
|
32
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
33
|
+
delete process.env.CLAUDE_CODE_WORKSPACE;
|
|
34
|
+
delete process.env.CURSOR_WORKSPACE;
|
|
35
|
+
delete process.env.WINDSURF_WORKSPACE;
|
|
36
|
+
|
|
37
|
+
const result = detectAgents();
|
|
38
|
+
|
|
39
|
+
expect(result).toHaveProperty('primary');
|
|
40
|
+
expect(result).toHaveProperty('detected');
|
|
41
|
+
expect(Array.isArray(result.detected)).toBe(true);
|
|
42
|
+
|
|
43
|
+
// If running in environment with actual agents installed, primary might not be generic
|
|
44
|
+
// Just verify it returns a valid agent type
|
|
45
|
+
expect(isSupportedAgent(result.primary)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('detectAgents finds Clawdbot when .clawdbot exists', () => {
|
|
49
|
+
// This test only runs if .clawdbot actually exists
|
|
50
|
+
const clawdbotPath = join(process.env.HOME, '.clawdbot');
|
|
51
|
+
if (existsSync(clawdbotPath)) {
|
|
52
|
+
const result = detectAgents();
|
|
53
|
+
expect(result.detected).toContain('clawdbot');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('detectAgents finds Claude Code when env var set', () => {
|
|
58
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
59
|
+
|
|
60
|
+
const result = detectAgents();
|
|
61
|
+
|
|
62
|
+
expect(result.detected).toContain('claude-code');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('detectAgents finds Cursor when .cursorrules exists', () => {
|
|
66
|
+
// Only test if in actual Cursor environment
|
|
67
|
+
if (existsSync('.cursorrules')) {
|
|
68
|
+
const result = detectAgents();
|
|
69
|
+
expect(result.detected).toContain('cursor');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('detectAgents handles multiple agents', () => {
|
|
74
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
75
|
+
process.env.CURSOR_WORKSPACE = 'test-workspace';
|
|
76
|
+
|
|
77
|
+
const result = detectAgents();
|
|
78
|
+
|
|
79
|
+
expect(result.detected.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(result.primary).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('getAgentName returns correct display names', () => {
|
|
84
|
+
expect(getAgentName('clawdbot')).toBe('Clawdbot');
|
|
85
|
+
expect(getAgentName('claude-code')).toBe('Claude Code');
|
|
86
|
+
expect(getAgentName('cursor')).toBe('Cursor');
|
|
87
|
+
expect(getAgentName('windsurf')).toBe('Windsurf');
|
|
88
|
+
expect(getAgentName('generic')).toBe('Generic AI Agent');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('getAgentName returns input for unknown agent', () => {
|
|
92
|
+
expect(getAgentName('unknown-agent')).toBe('unknown-agent');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('isSupportedAgent validates known agents', () => {
|
|
96
|
+
expect(isSupportedAgent('clawdbot')).toBe(true);
|
|
97
|
+
expect(isSupportedAgent('claude-code')).toBe(true);
|
|
98
|
+
expect(isSupportedAgent('cursor')).toBe(true);
|
|
99
|
+
expect(isSupportedAgent('windsurf')).toBe(true);
|
|
100
|
+
expect(isSupportedAgent('generic')).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('isSupportedAgent rejects unknown agents', () => {
|
|
104
|
+
expect(isSupportedAgent('unknown')).toBe(false);
|
|
105
|
+
expect(isSupportedAgent('invalid')).toBe(false);
|
|
106
|
+
expect(isSupportedAgent('')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { join, dirname } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generic agent setup - just provide instructions
|
|
10
|
+
*/
|
|
11
|
+
export async function setupGeneric(workspacePath) {
|
|
12
|
+
// For generic agents, we can't auto-configure
|
|
13
|
+
// Just provide clear instructions
|
|
14
|
+
console.log(chalk.yellow('ā¹'), 'Generic agent detected - manual setup required');
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get setup instructions for generic agents
|
|
20
|
+
*/
|
|
21
|
+
export function getGenericInstructions(workspacePath) {
|
|
22
|
+
const skillPath = join(dirname(__dirname), '../skills/agent-skill.template.md');
|
|
23
|
+
|
|
24
|
+
let output = '\n';
|
|
25
|
+
output += chalk.bold('Manual Setup Required') + '\n\n';
|
|
26
|
+
output += 'Your Cairn workspace: ' + chalk.cyan(workspacePath) + '\n\n';
|
|
27
|
+
output += chalk.bold('To configure your AI agent:') + '\n\n';
|
|
28
|
+
output += '1. Share this with your agent:\n';
|
|
29
|
+
output += ' ' + chalk.dim('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā') + '\n';
|
|
30
|
+
output += ' ' + chalk.yellow('I\'m using Cairn for project management.') + '\n';
|
|
31
|
+
output += ' ' + chalk.yellow('My workspace is at: ' + workspacePath) + '\n';
|
|
32
|
+
output += ' ' + chalk.yellow('Please read the agent skill documentation at:') + '\n';
|
|
33
|
+
output += ' ' + chalk.yellow(skillPath) + '\n';
|
|
34
|
+
output += ' ' + chalk.dim('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā') + '\n\n';
|
|
35
|
+
output += '2. ' + chalk.bold('Or copy the skill to your agent\'s context:') + '\n';
|
|
36
|
+
output += ' - For AI coding assistants: Add ' + chalk.cyan(skillPath) + ' to your workspace\n';
|
|
37
|
+
output += ' - For chat agents: Share the skill content in your first message\n\n';
|
|
38
|
+
output += '3. ' + chalk.bold('Test it:') + '\n';
|
|
39
|
+
output += ' Ask your agent:\n';
|
|
40
|
+
output += ' ' + chalk.yellow('"Help me create a project called Launch My App"') + '\n\n';
|
|
41
|
+
output += chalk.bold('Supported agents with auto-setup:') + '\n';
|
|
42
|
+
output += '- Clawdbot (detected via ~/.clawdbot/)\n';
|
|
43
|
+
output += '- Claude Code (workspace-based)\n';
|
|
44
|
+
output += '- Cursor (uses .cursorrules)\n';
|
|
45
|
+
output += '- Windsurf (workspace-based)\n\n';
|
|
46
|
+
output += chalk.dim('If you install one of these agents later, run:') + '\n';
|
|
47
|
+
output += chalk.dim(' cairn onboard --force') + '\n';
|
|
48
|
+
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Verify generic setup (always returns guidance)
|
|
54
|
+
*/
|
|
55
|
+
export function verifyGeneric() {
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
message: 'Manual setup - verify agent can read workspace'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { resolveWorkspace } from '../setup/workspace.js';
|
|
6
|
+
|
|
7
|
+
function slugify(text) {
|
|
8
|
+
return text
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getToday() {
|
|
15
|
+
return new Date().toISOString().split('T')[0];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getDueDate(daysFromNow = 7) {
|
|
19
|
+
const date = new Date();
|
|
20
|
+
date.setDate(date.getDate() + daysFromNow);
|
|
21
|
+
return date.toISOString().split('T')[0];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default async function create(type, name, options) {
|
|
25
|
+
const workspacePath = resolveWorkspace();
|
|
26
|
+
|
|
27
|
+
if (!workspacePath) {
|
|
28
|
+
console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn init'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const slug = slugify(name);
|
|
33
|
+
|
|
34
|
+
switch (type) {
|
|
35
|
+
case 'project':
|
|
36
|
+
await createProject(workspacePath, name, slug, options);
|
|
37
|
+
break;
|
|
38
|
+
case 'task':
|
|
39
|
+
await createTask(workspacePath, name, slug, options);
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
console.error(chalk.red('Error:'), `Unknown type: ${type}`);
|
|
43
|
+
console.log(chalk.dim('Valid types: project, task'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function createProject(workspacePath, name, slug, options) {
|
|
49
|
+
const projectPath = join(workspacePath, 'projects', slug);
|
|
50
|
+
|
|
51
|
+
if (existsSync(projectPath)) {
|
|
52
|
+
console.error(chalk.red('Error:'), `Project already exists: ${slug}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create folders
|
|
57
|
+
mkdirSync(projectPath, { recursive: true });
|
|
58
|
+
mkdirSync(join(projectPath, 'tasks'), { recursive: true });
|
|
59
|
+
|
|
60
|
+
// Create charter.md
|
|
61
|
+
const charterPath = join(projectPath, 'charter.md');
|
|
62
|
+
const charter = `---
|
|
63
|
+
title: ${name}
|
|
64
|
+
description: ${options.description || name}
|
|
65
|
+
status: active
|
|
66
|
+
priority: 2
|
|
67
|
+
created: ${getToday()}
|
|
68
|
+
due: ${options.due || getDueDate(30)}
|
|
69
|
+
owner: ${options.assignee || 'me'}
|
|
70
|
+
default_autonomy: draft
|
|
71
|
+
budget: 100
|
|
72
|
+
spent: 0
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Why This Matters
|
|
76
|
+
|
|
77
|
+
${options.objective || '[Describe why this project is important]'}
|
|
78
|
+
|
|
79
|
+
## Success Criteria
|
|
80
|
+
|
|
81
|
+
- [ ] [Define what success looks like]
|
|
82
|
+
- [ ] [Add measurable outcomes]
|
|
83
|
+
- [ ] [Specify completion criteria]
|
|
84
|
+
|
|
85
|
+
## Context
|
|
86
|
+
|
|
87
|
+
[Add relevant background, constraints, or dependencies]
|
|
88
|
+
|
|
89
|
+
## Work Log
|
|
90
|
+
|
|
91
|
+
### ${getToday()} - Created
|
|
92
|
+
Project created via Cairn CLI.
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
writeFileSync(charterPath, charter);
|
|
96
|
+
|
|
97
|
+
console.log(chalk.green('ā'), `Project created: ${chalk.cyan(slug)}`);
|
|
98
|
+
console.log(chalk.dim(` Location: ${projectPath}`));
|
|
99
|
+
console.log(chalk.dim(` Charter: ${charterPath}`));
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(chalk.dim('Next:'), `cairn create task "Task Name" --project ${slug}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function createTask(workspacePath, name, slug, options) {
|
|
105
|
+
const projectSlug = options.project;
|
|
106
|
+
|
|
107
|
+
if (!projectSlug) {
|
|
108
|
+
console.error(chalk.red('Error:'), 'Missing --project flag');
|
|
109
|
+
console.log(chalk.dim('Example:'), chalk.cyan(`cairn create task "${name}" --project my-project`));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const projectPath = join(workspacePath, 'projects', projectSlug);
|
|
114
|
+
if (!existsSync(projectPath)) {
|
|
115
|
+
console.error(chalk.red('Error:'), `Project not found: ${projectSlug}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tasksDir = join(projectPath, 'tasks');
|
|
120
|
+
if (!existsSync(tasksDir)) {
|
|
121
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const taskPath = join(tasksDir, `${slug}.md`);
|
|
125
|
+
|
|
126
|
+
if (existsSync(taskPath)) {
|
|
127
|
+
console.error(chalk.red('Error:'), `Task already exists: ${slug}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create task file
|
|
132
|
+
const task = `---
|
|
133
|
+
title: ${name}
|
|
134
|
+
description: ${options.description || name}
|
|
135
|
+
assignee: ${options.assignee || 'you'}
|
|
136
|
+
status: ${options.status || 'pending'}
|
|
137
|
+
created: ${getToday()}
|
|
138
|
+
due: ${options.due || getDueDate(7)}
|
|
139
|
+
autonomy: draft
|
|
140
|
+
spend: 0
|
|
141
|
+
artifacts: []
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Objective
|
|
145
|
+
|
|
146
|
+
${options.objective || '[Describe what needs to be accomplished]'}
|
|
147
|
+
|
|
148
|
+
## Work Log
|
|
149
|
+
|
|
150
|
+
### ${getToday()} - Created
|
|
151
|
+
Task created via Cairn CLI.
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
writeFileSync(taskPath, task);
|
|
155
|
+
|
|
156
|
+
console.log(chalk.green('ā'), `Task created: ${chalk.cyan(slug)}`);
|
|
157
|
+
console.log(chalk.dim(` Location: ${taskPath}`));
|
|
158
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
import { detectAgents, getAgentName } from '../agents/detect.js';
|
|
9
|
+
import { verifyClawdbot } from '../agents/clawdbot.js';
|
|
10
|
+
import { verifyClaudeCode } from '../agents/claude-code.js';
|
|
11
|
+
import { verifyCursor } from '../agents/cursor.js';
|
|
12
|
+
import { validateWorkspace, createWorkspace, createWelcomeFile, resolveWorkspace } from '../setup/workspace.js';
|
|
13
|
+
|
|
14
|
+
export default async function doctor() {
|
|
15
|
+
console.log(chalk.bold.cyan('\nšļø Cairn Doctor\n'));
|
|
16
|
+
console.log(chalk.dim('Checking workspace health...\n'));
|
|
17
|
+
|
|
18
|
+
const workspacePath = resolveWorkspace() || join(homedir(), 'cairn');
|
|
19
|
+
let issues = [];
|
|
20
|
+
let warnings = [];
|
|
21
|
+
|
|
22
|
+
// Check 1: Workspace exists
|
|
23
|
+
const spinner = ora('Checking workspace').start();
|
|
24
|
+
if (!existsSync(workspacePath)) {
|
|
25
|
+
spinner.fail('Workspace not found');
|
|
26
|
+
issues.push({
|
|
27
|
+
problem: `Workspace not found at ${workspacePath}`,
|
|
28
|
+
fix: 'Run: cairn init'
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
const { valid, missing } = validateWorkspace(workspacePath);
|
|
32
|
+
if (!valid) {
|
|
33
|
+
spinner.warn('Workspace incomplete');
|
|
34
|
+
warnings.push({
|
|
35
|
+
problem: `Missing folders: ${missing.join(', ')}`,
|
|
36
|
+
fix: 'Run: cairn init --path ' + workspacePath
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
spinner.succeed('Workspace structure valid');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check 2: Agent configuration
|
|
44
|
+
const agentSpinner = ora('Checking agent configuration').start();
|
|
45
|
+
const { primary, detected } = detectAgents();
|
|
46
|
+
|
|
47
|
+
if (detected.length === 0) {
|
|
48
|
+
agentSpinner.info('No specific agent detected');
|
|
49
|
+
warnings.push({
|
|
50
|
+
problem: 'No AI agent auto-detected',
|
|
51
|
+
fix: 'Run: cairn onboard --agent <type>'
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
agentSpinner.succeed(`Detected: ${detected.map(getAgentName).join(', ')}`);
|
|
55
|
+
|
|
56
|
+
// Verify each detected agent
|
|
57
|
+
for (const agent of detected) {
|
|
58
|
+
const verifySpinner = ora(`Verifying ${getAgentName(agent)}`).start();
|
|
59
|
+
let result;
|
|
60
|
+
|
|
61
|
+
switch (agent) {
|
|
62
|
+
case 'clawdbot':
|
|
63
|
+
result = verifyClawdbot();
|
|
64
|
+
break;
|
|
65
|
+
case 'claude-code':
|
|
66
|
+
result = verifyClaudeCode();
|
|
67
|
+
break;
|
|
68
|
+
case 'cursor':
|
|
69
|
+
result = verifyCursor();
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
result = { success: true, message: 'No verification available' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (result.success) {
|
|
76
|
+
verifySpinner.succeed(result.message);
|
|
77
|
+
} else {
|
|
78
|
+
verifySpinner.fail(result.message);
|
|
79
|
+
issues.push({
|
|
80
|
+
problem: `${getAgentName(agent)}: ${result.message}`,
|
|
81
|
+
fix: `Run: cairn onboard --agent ${agent} --force`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check 3: README exists
|
|
88
|
+
const readmeSpinner = ora('Checking README').start();
|
|
89
|
+
const readmePath = join(workspacePath, 'README.md');
|
|
90
|
+
if (existsSync(readmePath)) {
|
|
91
|
+
readmeSpinner.succeed('README exists');
|
|
92
|
+
} else {
|
|
93
|
+
readmeSpinner.warn('README missing');
|
|
94
|
+
warnings.push({
|
|
95
|
+
problem: 'Welcome README not found',
|
|
96
|
+
fix: 'Will be created automatically'
|
|
97
|
+
});
|
|
98
|
+
if (existsSync(workspacePath)) {
|
|
99
|
+
createWelcomeFile(workspacePath);
|
|
100
|
+
console.log(chalk.green('ā'), 'README created');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Summary
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(chalk.bold('Summary:\n'));
|
|
107
|
+
|
|
108
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
109
|
+
console.log(chalk.green('ā'), chalk.bold('Everything looks good!'));
|
|
110
|
+
console.log();
|
|
111
|
+
console.log(chalk.dim('Your Cairn workspace is healthy.'));
|
|
112
|
+
console.log(chalk.dim('Location:'), chalk.cyan(workspacePath));
|
|
113
|
+
console.log();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (issues.length > 0) {
|
|
118
|
+
console.log(chalk.red.bold(`${issues.length} issue(s) found:\n`));
|
|
119
|
+
for (const issue of issues) {
|
|
120
|
+
console.log(chalk.red('ā'), issue.problem);
|
|
121
|
+
console.log(chalk.dim(' Fix:'), chalk.cyan(issue.fix));
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (warnings.length > 0) {
|
|
127
|
+
console.log(chalk.yellow.bold(`${warnings.length} warning(s):\n`));
|
|
128
|
+
for (const warning of warnings) {
|
|
129
|
+
console.log(chalk.yellow('ā '), warning.problem);
|
|
130
|
+
console.log(chalk.dim(' Fix:'), chalk.cyan(warning.fix));
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(chalk.dim('Run'), chalk.cyan('cairn --help'), chalk.dim('for more commands.'));
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { createWorkspace, createWelcomeFile, workspaceExists } from '../setup/workspace.js';
|
|
4
|
+
|
|
5
|
+
export default async function init(options) {
|
|
6
|
+
console.log(chalk.bold.cyan('\nšļø Initializing Cairn\n'));
|
|
7
|
+
|
|
8
|
+
const workspacePath = options.path || process.cwd();
|
|
9
|
+
|
|
10
|
+
if (workspaceExists(workspacePath)) {
|
|
11
|
+
console.log(chalk.yellow('ā '), `Workspace already exists at ${workspacePath}`);
|
|
12
|
+
console.log(chalk.dim('Use --path to specify a different location'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Create workspace
|
|
17
|
+
createWorkspace(workspacePath);
|
|
18
|
+
createWelcomeFile(workspacePath);
|
|
19
|
+
|
|
20
|
+
console.log();
|
|
21
|
+
console.log(chalk.green('ā'), `Workspace created at ${chalk.cyan(workspacePath)}`);
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.dim('Next steps:'));
|
|
24
|
+
console.log(chalk.dim(' ⢠Create a project: cairn create project "My Project"'));
|
|
25
|
+
console.log(chalk.dim(' ⢠Configure agent: cairn onboard'));
|
|
26
|
+
console.log(chalk.dim(' ⢠Check health: cairn doctor'));
|
|
27
|
+
console.log();
|
|
28
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
import { detectAgents, getAgentName } from '../agents/detect.js';
|
|
8
|
+
import { setupClawdbot, getClawdbotInstructions } from '../agents/clawdbot.js';
|
|
9
|
+
import { setupClaudeCode, getClaudeCodeInstructions } from '../agents/claude-code.js';
|
|
10
|
+
import { setupCursor, getCursorInstructions } from '../agents/cursor.js';
|
|
11
|
+
import { setupGeneric, getGenericInstructions } from '../agents/generic.js';
|
|
12
|
+
import { createWorkspace, createWelcomeFile, workspaceExists } from '../setup/workspace.js';
|
|
13
|
+
|
|
14
|
+
export default async function onboard(options) {
|
|
15
|
+
console.log(chalk.bold.cyan('\nšļø Cairn Onboarding\n'));
|
|
16
|
+
|
|
17
|
+
// Prompt for workspace path (if not provided)
|
|
18
|
+
let workspacePath = options.path;
|
|
19
|
+
if (!workspacePath) {
|
|
20
|
+
const currentDir = process.cwd();
|
|
21
|
+
const cairnDir = join(homedir(), 'cairn');
|
|
22
|
+
|
|
23
|
+
const { location } = await inquirer.prompt([{
|
|
24
|
+
type: 'list',
|
|
25
|
+
name: 'location',
|
|
26
|
+
message: 'Where should Cairn store your project files?',
|
|
27
|
+
choices: [
|
|
28
|
+
{ name: `Here (${currentDir})`, value: currentDir },
|
|
29
|
+
{ name: `~/cairn`, value: cairnDir },
|
|
30
|
+
{ name: 'Somewhere else...', value: '__custom__' }
|
|
31
|
+
]
|
|
32
|
+
}]);
|
|
33
|
+
|
|
34
|
+
if (location === '__custom__') {
|
|
35
|
+
const { customPath } = await inquirer.prompt([{
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'customPath',
|
|
38
|
+
message: 'Enter the full path:'
|
|
39
|
+
}]);
|
|
40
|
+
workspacePath = customPath;
|
|
41
|
+
} else {
|
|
42
|
+
workspacePath = location;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if already set up
|
|
47
|
+
if (workspaceExists(workspacePath) && !options.force) {
|
|
48
|
+
const { proceed } = await inquirer.prompt([{
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
name: 'proceed',
|
|
51
|
+
message: `Workspace already exists at ${workspacePath}. Re-run onboarding?`,
|
|
52
|
+
default: false
|
|
53
|
+
}]);
|
|
54
|
+
|
|
55
|
+
if (!proceed) {
|
|
56
|
+
console.log(chalk.yellow('\nOnboarding cancelled.'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 1: Detect agents
|
|
62
|
+
let agentType = options.agent;
|
|
63
|
+
|
|
64
|
+
if (!agentType) {
|
|
65
|
+
const spinner = ora('Detecting AI agents').start();
|
|
66
|
+
const { primary, detected } = detectAgents();
|
|
67
|
+
spinner.stop();
|
|
68
|
+
|
|
69
|
+
if (detected.length > 1) {
|
|
70
|
+
console.log(chalk.green('ā'), `Detected agents: ${detected.map(getAgentName).join(', ')}`);
|
|
71
|
+
|
|
72
|
+
const { selected } = await inquirer.prompt([{
|
|
73
|
+
type: 'list',
|
|
74
|
+
name: 'selected',
|
|
75
|
+
message: 'Multiple agents detected. Which one do you want to configure?',
|
|
76
|
+
choices: detected.map(type => ({
|
|
77
|
+
name: getAgentName(type),
|
|
78
|
+
value: type
|
|
79
|
+
}))
|
|
80
|
+
}]);
|
|
81
|
+
|
|
82
|
+
agentType = selected;
|
|
83
|
+
} else if (detected.length === 1) {
|
|
84
|
+
agentType = primary;
|
|
85
|
+
console.log(chalk.green('ā'), `Detected: ${getAgentName(agentType)}`);
|
|
86
|
+
} else {
|
|
87
|
+
agentType = 'generic';
|
|
88
|
+
console.log(chalk.yellow('ā¹'), 'No specific agent detected - using generic setup');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step 2: Create workspace
|
|
93
|
+
console.log();
|
|
94
|
+
createWorkspace(workspacePath);
|
|
95
|
+
createWelcomeFile(workspacePath);
|
|
96
|
+
|
|
97
|
+
// Step 3: Set up agent
|
|
98
|
+
console.log();
|
|
99
|
+
const setupSpinner = ora(`Configuring ${getAgentName(agentType)}`).start();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
switch (agentType) {
|
|
103
|
+
case 'clawdbot':
|
|
104
|
+
await setupClawdbot(workspacePath);
|
|
105
|
+
setupSpinner.succeed(`${getAgentName(agentType)} configured`);
|
|
106
|
+
console.log(getClawdbotInstructions(workspacePath));
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'claude-code':
|
|
110
|
+
await setupClaudeCode(workspacePath);
|
|
111
|
+
setupSpinner.succeed(`${getAgentName(agentType)} configured`);
|
|
112
|
+
console.log(getClaudeCodeInstructions(workspacePath));
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'cursor':
|
|
116
|
+
await setupCursor(workspacePath);
|
|
117
|
+
setupSpinner.succeed(`${getAgentName(agentType)} configured`);
|
|
118
|
+
console.log(getCursorInstructions(workspacePath));
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'generic':
|
|
122
|
+
default:
|
|
123
|
+
await setupGeneric(workspacePath);
|
|
124
|
+
setupSpinner.stop();
|
|
125
|
+
console.log(getGenericInstructions(workspacePath));
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
setupSpinner.fail('Setup failed');
|
|
130
|
+
console.error(chalk.red('Error:'), error.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Success!
|
|
135
|
+
console.log(chalk.bold.green('\nš Onboarding complete!\n'));
|
|
136
|
+
console.log(chalk.dim('Next steps:'));
|
|
137
|
+
console.log(chalk.dim(' 1. Test your agent with: "Help me create a project"'));
|
|
138
|
+
console.log(chalk.dim(' 2. Read the docs: cairn --help'));
|
|
139
|
+
console.log(chalk.dim(' 3. Create your first project: cairn create project "My Project"'));
|
|
140
|
+
console.log();
|
|
141
|
+
}
|