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.
@@ -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
+ }