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,69 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
|
|
4
|
+
import { detectAgents, getAgentName } from '../agents/detect.js';
|
|
5
|
+
import { setupClawdbot } from '../agents/clawdbot.js';
|
|
6
|
+
import { setupClaudeCode } from '../agents/claude-code.js';
|
|
7
|
+
import { setupCursor } from '../agents/cursor.js';
|
|
8
|
+
import { resolveWorkspace } from '../setup/workspace.js';
|
|
9
|
+
|
|
10
|
+
export default async function updateSkill(options) {
|
|
11
|
+
console.log(chalk.bold.cyan('\nšļø Updating Agent Skill\n'));
|
|
12
|
+
|
|
13
|
+
const workspacePath = resolveWorkspace();
|
|
14
|
+
|
|
15
|
+
if (!workspacePath) {
|
|
16
|
+
console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn init'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Determine which agents to update
|
|
21
|
+
let agentsToUpdate = [];
|
|
22
|
+
|
|
23
|
+
if (options.agent) {
|
|
24
|
+
agentsToUpdate = [options.agent];
|
|
25
|
+
} else {
|
|
26
|
+
const { detected } = detectAgents();
|
|
27
|
+
if (detected.length === 0) {
|
|
28
|
+
console.log(chalk.yellow('ā '), 'No agents detected');
|
|
29
|
+
console.log(chalk.dim('Specify agent:'), chalk.cyan('cairn update-skill --agent <type>'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
agentsToUpdate = detected;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Update each agent
|
|
36
|
+
for (const agent of agentsToUpdate) {
|
|
37
|
+
const spinner = ora(`Updating ${getAgentName(agent)}`).start();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
switch (agent) {
|
|
41
|
+
case 'clawdbot':
|
|
42
|
+
await setupClawdbot(workspacePath);
|
|
43
|
+
spinner.succeed(`${getAgentName(agent)} skill updated`);
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'claude-code':
|
|
47
|
+
await setupClaudeCode(workspacePath);
|
|
48
|
+
spinner.succeed(`${getAgentName(agent)} skill updated`);
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'cursor':
|
|
52
|
+
await setupCursor(workspacePath);
|
|
53
|
+
spinner.succeed(`${getAgentName(agent)} skill updated`);
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
spinner.info(`${getAgentName(agent)} - no auto-update available`);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
spinner.fail(`Failed to update ${getAgentName(agent)}`);
|
|
61
|
+
console.error(chalk.red('Error:'), error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(chalk.green('ā'), 'Agent skills updated');
|
|
67
|
+
console.log(chalk.dim('Your agents now have the latest Cairn workflow documentation.'));
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check npm registry for latest version and prompt to update
|
|
15
|
+
*/
|
|
16
|
+
export default async function update() {
|
|
17
|
+
console.log(chalk.bold.cyan('\nšļø Checking for updates...\n'));
|
|
18
|
+
|
|
19
|
+
// Get current version
|
|
20
|
+
const packageJson = JSON.parse(
|
|
21
|
+
readFileSync(join(__dirname, '../../package.json'), 'utf8')
|
|
22
|
+
);
|
|
23
|
+
const currentVersion = packageJson.version;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Check npm registry for latest version
|
|
27
|
+
const { stdout } = await execAsync('npm view cairn version');
|
|
28
|
+
const latestVersion = stdout.trim();
|
|
29
|
+
|
|
30
|
+
console.log(chalk.dim('Current version:'), chalk.cyan(currentVersion));
|
|
31
|
+
console.log(chalk.dim('Latest version:'), chalk.cyan(latestVersion));
|
|
32
|
+
console.log();
|
|
33
|
+
|
|
34
|
+
if (currentVersion === latestVersion) {
|
|
35
|
+
console.log(chalk.green('ā'), 'You are running the latest version!');
|
|
36
|
+
console.log();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Prompt to update
|
|
41
|
+
const { shouldUpdate } = await inquirer.prompt([{
|
|
42
|
+
type: 'confirm',
|
|
43
|
+
name: 'shouldUpdate',
|
|
44
|
+
message: `Update to v${latestVersion}?`,
|
|
45
|
+
default: true
|
|
46
|
+
}]);
|
|
47
|
+
|
|
48
|
+
if (!shouldUpdate) {
|
|
49
|
+
console.log(chalk.yellow('\nUpdate cancelled.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Perform update
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(chalk.dim('Running:'), chalk.cyan('npm install -g cairn@latest'));
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
const updateProcess = exec('npm install -g cairn@latest');
|
|
59
|
+
updateProcess.stdout.pipe(process.stdout);
|
|
60
|
+
updateProcess.stderr.pipe(process.stderr);
|
|
61
|
+
|
|
62
|
+
updateProcess.on('exit', (code) => {
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
console.log();
|
|
65
|
+
console.log(chalk.green('ā'), 'Update complete!');
|
|
66
|
+
console.log(chalk.dim('Run'), chalk.cyan('cairn --version'), chalk.dim('to verify.'));
|
|
67
|
+
console.log();
|
|
68
|
+
} else {
|
|
69
|
+
console.log();
|
|
70
|
+
console.error(chalk.red('ā'), 'Update failed');
|
|
71
|
+
console.log(chalk.dim('Try running manually:'), chalk.cyan('npm install -g cairn@latest'));
|
|
72
|
+
console.log();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(chalk.red('Error checking for updates:'), error.message);
|
|
79
|
+
console.log(chalk.dim('\nYou can update manually:'));
|
|
80
|
+
console.log(chalk.cyan(' npm install -g cairn@latest'));
|
|
81
|
+
console.log();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create Cairn workspace structure
|
|
9
|
+
*/
|
|
10
|
+
export function createWorkspace(path) {
|
|
11
|
+
const spinner = ora('Creating workspace structure').start();
|
|
12
|
+
|
|
13
|
+
const folders = [
|
|
14
|
+
'',
|
|
15
|
+
'projects',
|
|
16
|
+
'inbox',
|
|
17
|
+
'_drafts',
|
|
18
|
+
'_conflicts',
|
|
19
|
+
'_abandoned',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const folder of folders) {
|
|
23
|
+
const fullPath = join(path, folder);
|
|
24
|
+
if (!existsSync(fullPath)) {
|
|
25
|
+
mkdirSync(fullPath, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.succeed('Workspace structure created');
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create welcome README in workspace
|
|
35
|
+
*/
|
|
36
|
+
export function createWelcomeFile(path) {
|
|
37
|
+
const readmePath = join(path, 'README.md');
|
|
38
|
+
|
|
39
|
+
if (existsSync(readmePath)) {
|
|
40
|
+
return; // Don't overwrite existing README
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = `# Welcome to Cairn šļø
|
|
44
|
+
|
|
45
|
+
You've successfully set up Cairn!
|
|
46
|
+
|
|
47
|
+
## Your Workspace
|
|
48
|
+
|
|
49
|
+
This folder (\`${path}\`) contains all your project files.
|
|
50
|
+
|
|
51
|
+
### Structure
|
|
52
|
+
|
|
53
|
+
- \`projects/\` - Your projects (e.g., "launch-my-app")
|
|
54
|
+
- \`inbox/\` - Ideas and incoming tasks to triage
|
|
55
|
+
- \`_drafts/\` - Work in progress
|
|
56
|
+
- \`_conflicts/\` - Sync conflicts (if using multi-device sync)
|
|
57
|
+
|
|
58
|
+
## Getting Started
|
|
59
|
+
|
|
60
|
+
### Create Your First Project
|
|
61
|
+
|
|
62
|
+
\`\`\`bash
|
|
63
|
+
cairn create project "My First Project"
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
Or create manually:
|
|
67
|
+
- Create folder: \`projects/my-first-project/\`
|
|
68
|
+
- Create file: \`charter.md\`
|
|
69
|
+
- Add frontmatter (see examples below)
|
|
70
|
+
|
|
71
|
+
### Work with Your AI Agent
|
|
72
|
+
|
|
73
|
+
Your AI agent is configured to work with Cairn! Try:
|
|
74
|
+
|
|
75
|
+
\`\`\`
|
|
76
|
+
"Help me create a project called Launch My App"
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## File Format
|
|
80
|
+
|
|
81
|
+
Cairn uses markdown files with YAML frontmatter:
|
|
82
|
+
|
|
83
|
+
\`\`\`yaml
|
|
84
|
+
---
|
|
85
|
+
title: My Project
|
|
86
|
+
description: What we're building
|
|
87
|
+
status: active
|
|
88
|
+
priority: 1
|
|
89
|
+
owner: me
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Why This Matters
|
|
93
|
+
|
|
94
|
+
[Your goals]
|
|
95
|
+
|
|
96
|
+
## Success Criteria
|
|
97
|
+
|
|
98
|
+
[What does done look like?]
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
## Learn More
|
|
102
|
+
|
|
103
|
+
- **Commands:** \`cairn --help\`
|
|
104
|
+
- **Check health:** \`cairn doctor\`
|
|
105
|
+
- **Update skill:** \`cairn update-skill\`
|
|
106
|
+
|
|
107
|
+
Happy building! šļø
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
writeFileSync(readmePath, content);
|
|
111
|
+
console.log(chalk.green('ā'), 'Welcome file created');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if workspace exists
|
|
116
|
+
*/
|
|
117
|
+
export function workspaceExists(path) {
|
|
118
|
+
return existsSync(join(path, 'projects'));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find an existing workspace by checking known locations.
|
|
123
|
+
* Returns the first match, or null if none found.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveWorkspace() {
|
|
126
|
+
const candidates = [
|
|
127
|
+
process.cwd(),
|
|
128
|
+
join(homedir(), 'cairn'),
|
|
129
|
+
];
|
|
130
|
+
return candidates.find(p => workspaceExists(p)) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate workspace structure
|
|
135
|
+
*/
|
|
136
|
+
export function validateWorkspace(path) {
|
|
137
|
+
const requiredFolders = ['projects', 'inbox'];
|
|
138
|
+
const missing = [];
|
|
139
|
+
|
|
140
|
+
for (const folder of requiredFolders) {
|
|
141
|
+
if (!existsSync(join(path, folder))) {
|
|
142
|
+
missing.push(folder);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { valid: missing.length === 0, missing };
|
|
147
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
createWorkspace,
|
|
4
|
+
workspaceExists,
|
|
5
|
+
validateWorkspace,
|
|
6
|
+
createWelcomeFile
|
|
7
|
+
} from './workspace.js';
|
|
8
|
+
import { existsSync, rmSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
|
|
12
|
+
describe('Workspace Management', () => {
|
|
13
|
+
let testPath;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Create unique test directory
|
|
17
|
+
testPath = join(tmpdir(), `cairn-test-workspace-${Date.now()}`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Clean up test directory
|
|
22
|
+
if (existsSync(testPath)) {
|
|
23
|
+
rmSync(testPath, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('createWorkspace creates all required folders', () => {
|
|
28
|
+
createWorkspace(testPath);
|
|
29
|
+
|
|
30
|
+
const expectedFolders = [
|
|
31
|
+
'',
|
|
32
|
+
'projects',
|
|
33
|
+
'inbox',
|
|
34
|
+
'_drafts',
|
|
35
|
+
'_conflicts',
|
|
36
|
+
'_abandoned',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const folder of expectedFolders) {
|
|
40
|
+
const fullPath = join(testPath, folder);
|
|
41
|
+
expect(existsSync(fullPath)).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('createWorkspace returns workspace path', () => {
|
|
46
|
+
const result = createWorkspace(testPath);
|
|
47
|
+
expect(result).toBe(testPath);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('createWorkspace is idempotent', () => {
|
|
51
|
+
// Create workspace twice
|
|
52
|
+
createWorkspace(testPath);
|
|
53
|
+
createWorkspace(testPath);
|
|
54
|
+
|
|
55
|
+
// Should still exist and be valid
|
|
56
|
+
expect(existsSync(join(testPath, 'projects'))).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('workspaceExists returns false for non-existent workspace', () => {
|
|
60
|
+
expect(workspaceExists(testPath)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('workspaceExists returns true after creation', () => {
|
|
64
|
+
createWorkspace(testPath);
|
|
65
|
+
expect(workspaceExists(testPath)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('validateWorkspace detects valid workspace', () => {
|
|
69
|
+
createWorkspace(testPath);
|
|
70
|
+
|
|
71
|
+
const result = validateWorkspace(testPath);
|
|
72
|
+
|
|
73
|
+
expect(result.valid).toBe(true);
|
|
74
|
+
expect(result.missing).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('validateWorkspace detects missing folders', () => {
|
|
78
|
+
// Create workspace but remove a required folder
|
|
79
|
+
createWorkspace(testPath);
|
|
80
|
+
rmSync(join(testPath, 'inbox'), { recursive: true, force: true });
|
|
81
|
+
|
|
82
|
+
const result = validateWorkspace(testPath);
|
|
83
|
+
|
|
84
|
+
expect(result.valid).toBe(false);
|
|
85
|
+
expect(result.missing).toContain('inbox');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('validateWorkspace detects completely invalid workspace', () => {
|
|
89
|
+
const result = validateWorkspace(testPath);
|
|
90
|
+
|
|
91
|
+
expect(result.valid).toBe(false);
|
|
92
|
+
expect(result.missing.length).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('createWelcomeFile creates README', () => {
|
|
96
|
+
createWorkspace(testPath);
|
|
97
|
+
createWelcomeFile(testPath);
|
|
98
|
+
|
|
99
|
+
const readmePath = join(testPath, 'README.md');
|
|
100
|
+
expect(existsSync(readmePath)).toBe(true);
|
|
101
|
+
|
|
102
|
+
const content = readFileSync(readmePath, 'utf-8');
|
|
103
|
+
expect(content).toContain('Welcome to Cairn');
|
|
104
|
+
expect(content).toContain('projects/');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('createWelcomeFile does not overwrite existing README', () => {
|
|
108
|
+
createWorkspace(testPath);
|
|
109
|
+
|
|
110
|
+
const readmePath = join(testPath, 'README.md');
|
|
111
|
+
const originalContent = '# My Custom README';
|
|
112
|
+
|
|
113
|
+
// Create custom README first
|
|
114
|
+
require('fs').writeFileSync(readmePath, originalContent);
|
|
115
|
+
|
|
116
|
+
// Try to create welcome file
|
|
117
|
+
createWelcomeFile(testPath);
|
|
118
|
+
|
|
119
|
+
// Should still have original content
|
|
120
|
+
const content = readFileSync(readmePath, 'utf-8');
|
|
121
|
+
expect(content).toBe(originalContent);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cairn-work",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AI-native project management - work with AI agents using markdown files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cairn": "bin/cairn.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "bun test",
|
|
11
|
+
"dev": "bun link",
|
|
12
|
+
"build": "echo 'No build step needed'",
|
|
13
|
+
"prepublishOnly": "bun test"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"lib/",
|
|
18
|
+
"skills/",
|
|
19
|
+
"templates/"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"project-management",
|
|
23
|
+
"ai",
|
|
24
|
+
"agents",
|
|
25
|
+
"markdown",
|
|
26
|
+
"task-management",
|
|
27
|
+
"productivity",
|
|
28
|
+
"clawdbot",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"cursor"
|
|
31
|
+
],
|
|
32
|
+
"author": "Gregory Hill",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/gregoryehill/cairn.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://cairn.app",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/gregoryehill/cairn/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"commander": "^11.0.0",
|
|
47
|
+
"chalk": "^5.3.0",
|
|
48
|
+
"ora": "^8.0.0",
|
|
49
|
+
"inquirer": "^9.2.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/bun": "^1.1.0",
|
|
53
|
+
"@types/inquirer": "^9.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|