@zoobbe/cli 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/bin/zoobbe +5 -0
- package/package.json +53 -0
- package/src/commands/ai.js +80 -0
- package/src/commands/auth.js +120 -0
- package/src/commands/board.js +156 -0
- package/src/commands/card.js +228 -0
- package/src/commands/config.js +57 -0
- package/src/commands/page.js +102 -0
- package/src/commands/search.js +48 -0
- package/src/commands/status.js +67 -0
- package/src/commands/workspace.js +147 -0
- package/src/index.js +55 -0
- package/src/lib/client.js +74 -0
- package/src/lib/config.js +20 -0
- package/src/lib/output.js +74 -0
- package/src/utils/spinner.js +24 -0
package/bin/zoobbe
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoobbe/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zoobbe CLI - Manage boards, cards, and projects from the terminal",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zoobbe": "./bin/zoobbe"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/zoobbe",
|
|
11
|
+
"dev": "node bin/zoobbe",
|
|
12
|
+
"test": "echo \"Tests coming soon\" && exit 0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"zoobbe",
|
|
16
|
+
"cli",
|
|
17
|
+
"project-management",
|
|
18
|
+
"kanban",
|
|
19
|
+
"task-management",
|
|
20
|
+
"boards",
|
|
21
|
+
"cards",
|
|
22
|
+
"ai-agent"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "Zoobbe",
|
|
27
|
+
"url": "https://zoobbe.com"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://zoobbe.com/cli",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/zoobbe/zoobbe-cli"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/zoobbe/zoobbe-cli/issues"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"bin/",
|
|
39
|
+
"src/"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"cli-table3": "^0.6.5",
|
|
44
|
+
"commander": "^12.1.0",
|
|
45
|
+
"conf": "^10.2.0",
|
|
46
|
+
"inquirer": "^8.2.6",
|
|
47
|
+
"open": "^8.4.2",
|
|
48
|
+
"ora": "^5.4.1"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { success, error, info } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const ai = new Command('ask')
|
|
9
|
+
.description('AI-powered natural language commands')
|
|
10
|
+
.argument('<prompt...>', 'Natural language command')
|
|
11
|
+
.option('-p, --provider <provider>', 'AI provider (claude|openai|ollama)', 'claude')
|
|
12
|
+
.option('--no-confirm', 'Execute without confirmation')
|
|
13
|
+
.option('-s, --stream', 'Stream the response')
|
|
14
|
+
.action(async (promptParts, options) => {
|
|
15
|
+
try {
|
|
16
|
+
const prompt = promptParts.join(' ');
|
|
17
|
+
const workspaceId = config.get('activeWorkspace');
|
|
18
|
+
|
|
19
|
+
if (!workspaceId) {
|
|
20
|
+
return error('No active workspace. Run "zoobbe workspace switch <name>".');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
info(`Processing: "${prompt}"`);
|
|
24
|
+
console.log();
|
|
25
|
+
|
|
26
|
+
const data = await withSpinner('Thinking...', () =>
|
|
27
|
+
client.post('/agent/query', {
|
|
28
|
+
message: prompt,
|
|
29
|
+
workspace: workspaceId,
|
|
30
|
+
provider: options.provider,
|
|
31
|
+
stream: false,
|
|
32
|
+
confirm: options.confirm !== false,
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const result = data.data || data;
|
|
37
|
+
|
|
38
|
+
// Display the AI response
|
|
39
|
+
if (result.plan) {
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(chalk.bold(' Plan:'));
|
|
42
|
+
if (Array.isArray(result.plan)) {
|
|
43
|
+
result.plan.forEach((step, i) => {
|
|
44
|
+
console.log(` ${i + 1}. ${step.description || step}`);
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
console.log(` ${result.plan}`);
|
|
48
|
+
}
|
|
49
|
+
console.log();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result.response) {
|
|
53
|
+
console.log(chalk.bold(' Response:'));
|
|
54
|
+
console.log(` ${result.response}`);
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (result.actions && result.actions.length > 0) {
|
|
59
|
+
console.log(chalk.bold(' Actions taken:'));
|
|
60
|
+
result.actions.forEach(action => {
|
|
61
|
+
console.log(` ${chalk.green('✓')} ${action.description || action.type}`);
|
|
62
|
+
});
|
|
63
|
+
console.log();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (result.results) {
|
|
67
|
+
console.log(chalk.bold(' Results:'));
|
|
68
|
+
console.log(JSON.stringify(result.results, null, 2));
|
|
69
|
+
console.log();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!result.plan && !result.response && !result.actions) {
|
|
73
|
+
console.log(JSON.stringify(result, null, 2));
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
error(`AI command failed: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
module.exports = ai;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const config = require('../lib/config');
|
|
5
|
+
const client = require('../lib/client');
|
|
6
|
+
const { success, error, info } = require('../lib/output');
|
|
7
|
+
|
|
8
|
+
const auth = new Command('auth')
|
|
9
|
+
.description('Authentication commands');
|
|
10
|
+
|
|
11
|
+
auth
|
|
12
|
+
.command('login')
|
|
13
|
+
.description('Authenticate with Zoobbe')
|
|
14
|
+
.option('-t, --token <token>', 'API key for direct login')
|
|
15
|
+
.option('--url <url>', 'Custom API URL (default: https://api.zoobbe.com)')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
if (options.url) {
|
|
19
|
+
config.set('apiUrl', options.url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let apiKey = options.token;
|
|
23
|
+
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
// Interactive login — open browser for auth
|
|
26
|
+
try {
|
|
27
|
+
const open = require('open');
|
|
28
|
+
const apiUrl = config.get('apiUrl');
|
|
29
|
+
const authUrl = `${apiUrl.replace('api.', '')}/cli/auth`;
|
|
30
|
+
info(`Opening browser for authentication...`);
|
|
31
|
+
info(`If it doesn't open, visit: ${chalk.underline(authUrl)}`);
|
|
32
|
+
await open(authUrl);
|
|
33
|
+
} catch {
|
|
34
|
+
// open may fail in headless environments
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const answers = await inquirer.prompt([{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'apiKey',
|
|
40
|
+
message: 'Paste your API key:',
|
|
41
|
+
validate: v => v.startsWith('zb_live_') ? true : 'Invalid API key format (should start with zb_live_)',
|
|
42
|
+
}]);
|
|
43
|
+
apiKey = answers.apiKey;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!apiKey.startsWith('zb_live_')) {
|
|
47
|
+
return error('Invalid API key format. Keys start with "zb_live_".');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
config.set('apiKey', apiKey);
|
|
51
|
+
|
|
52
|
+
// Verify the key works
|
|
53
|
+
const userData = await client.get('/users/me');
|
|
54
|
+
const user = userData.user || userData.data || userData;
|
|
55
|
+
|
|
56
|
+
config.set('userId', user._id || user.id || '');
|
|
57
|
+
config.set('userName', user.name || user.userName || '');
|
|
58
|
+
config.set('email', user.email || '');
|
|
59
|
+
|
|
60
|
+
success(`Logged in as ${chalk.bold(user.name || user.email)}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
config.set('apiKey', '');
|
|
63
|
+
error(`Login failed: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
auth
|
|
68
|
+
.command('logout')
|
|
69
|
+
.description('Clear stored credentials')
|
|
70
|
+
.action(() => {
|
|
71
|
+
config.set('apiKey', '');
|
|
72
|
+
config.set('userId', '');
|
|
73
|
+
config.set('userName', '');
|
|
74
|
+
config.set('email', '');
|
|
75
|
+
config.set('activeWorkspace', '');
|
|
76
|
+
config.set('activeWorkspaceName', '');
|
|
77
|
+
success('Logged out successfully');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
auth
|
|
81
|
+
.command('whoami')
|
|
82
|
+
.description('Show current authenticated user')
|
|
83
|
+
.action(async () => {
|
|
84
|
+
try {
|
|
85
|
+
const apiKey = config.get('apiKey');
|
|
86
|
+
if (!apiKey) {
|
|
87
|
+
return error('Not logged in. Run "zoobbe auth login" first.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const userData = await client.get('/users/me');
|
|
91
|
+
const user = userData.user || userData.data || userData;
|
|
92
|
+
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(chalk.bold(' User: '), user.name || user.userName || 'N/A');
|
|
95
|
+
console.log(chalk.bold(' Email: '), user.email);
|
|
96
|
+
console.log(chalk.bold(' API URL: '), config.get('apiUrl'));
|
|
97
|
+
console.log(chalk.bold(' Workspace:'), config.get('activeWorkspaceName') || 'Not set');
|
|
98
|
+
console.log();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
error(`Failed to fetch user info: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
auth
|
|
105
|
+
.command('status')
|
|
106
|
+
.description('Check authentication status')
|
|
107
|
+
.action(() => {
|
|
108
|
+
const apiKey = config.get('apiKey');
|
|
109
|
+
if (apiKey) {
|
|
110
|
+
success(`Authenticated (key: ${apiKey.substring(0, 16)}...)`);
|
|
111
|
+
info(`API URL: ${config.get('apiUrl')}`);
|
|
112
|
+
if (config.get('activeWorkspaceName')) {
|
|
113
|
+
info(`Workspace: ${config.get('activeWorkspaceName')}`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
error('Not authenticated. Run "zoobbe auth login".');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
module.exports = auth;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { output, success, error } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const board = new Command('board')
|
|
9
|
+
.alias('b')
|
|
10
|
+
.description('Board management commands');
|
|
11
|
+
|
|
12
|
+
board
|
|
13
|
+
.command('list')
|
|
14
|
+
.alias('ls')
|
|
15
|
+
.description('List boards in the active workspace')
|
|
16
|
+
.option('-f, --format <format>', 'Output format')
|
|
17
|
+
.option('-a, --all', 'Include archived boards')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const workspaceId = config.get('activeWorkspace');
|
|
21
|
+
if (!workspaceId) {
|
|
22
|
+
return error('No active workspace. Run "zoobbe workspace switch <name>".');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const data = await withSpinner('Fetching boards...', () =>
|
|
26
|
+
client.get(`/boards?workspaceId=${workspaceId}`)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
let boards = data.data || data;
|
|
30
|
+
if (!options.all) {
|
|
31
|
+
boards = boards.filter(b => !b.isArchived);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rows = boards.map(b => ({
|
|
35
|
+
name: b.name || b.title,
|
|
36
|
+
id: b.shortId,
|
|
37
|
+
visibility: b.visibility || 'Private',
|
|
38
|
+
lists: String(b.actionLists?.length || 0),
|
|
39
|
+
members: String(b.members?.length || 0),
|
|
40
|
+
status: b.isArchived ? chalk.yellow('archived') : chalk.green('active'),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
output(rows, {
|
|
44
|
+
headers: ['Name', 'ID', 'Visibility', 'Lists', 'Members', 'Status'],
|
|
45
|
+
format: options.format,
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
error(`Failed to list boards: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
board
|
|
53
|
+
.command('create <name>')
|
|
54
|
+
.description('Create a new board')
|
|
55
|
+
.option('-v, --visibility <type>', 'Visibility (Private|Public|Workspace)', 'Private')
|
|
56
|
+
.action(async (name, options) => {
|
|
57
|
+
try {
|
|
58
|
+
const workspaceId = config.get('activeWorkspace');
|
|
59
|
+
if (!workspaceId) {
|
|
60
|
+
return error('No active workspace. Run "zoobbe workspace switch <name>".');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await withSpinner('Creating board...', () =>
|
|
64
|
+
client.post('/boards', {
|
|
65
|
+
name,
|
|
66
|
+
workspaceId,
|
|
67
|
+
visibility: options.visibility,
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const b = data.data || data;
|
|
72
|
+
success(`Created board: ${chalk.bold(b.name || name)} (${b.shortId})`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
error(`Failed to create board: ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
board
|
|
79
|
+
.command('open <nameOrId>')
|
|
80
|
+
.description('Open a board in the browser')
|
|
81
|
+
.action(async (nameOrId) => {
|
|
82
|
+
try {
|
|
83
|
+
const open = require('open');
|
|
84
|
+
const apiUrl = config.get('apiUrl');
|
|
85
|
+
const baseUrl = apiUrl.replace('api.', '');
|
|
86
|
+
const workspaceName = config.get('activeWorkspaceName');
|
|
87
|
+
|
|
88
|
+
// Try to find the board to get its shortId
|
|
89
|
+
const workspaceId = config.get('activeWorkspace');
|
|
90
|
+
const data = await client.get(`/boards?workspaceId=${workspaceId}`);
|
|
91
|
+
const boards = data.data || data;
|
|
92
|
+
const match = boards.find(b =>
|
|
93
|
+
b.shortId === nameOrId ||
|
|
94
|
+
(b.name || '').toLowerCase() === nameOrId.toLowerCase()
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const boardId = match ? match.shortId : nameOrId;
|
|
98
|
+
const url = `${baseUrl}/board/${boardId}`;
|
|
99
|
+
await open(url);
|
|
100
|
+
success(`Opened board in browser`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
error(`Failed to open board: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
board
|
|
107
|
+
.command('archive <nameOrId>')
|
|
108
|
+
.description('Archive a board')
|
|
109
|
+
.action(async (nameOrId) => {
|
|
110
|
+
try {
|
|
111
|
+
const workspaceId = config.get('activeWorkspace');
|
|
112
|
+
const data = await client.get(`/boards?workspaceId=${workspaceId}`);
|
|
113
|
+
const boards = data.data || data;
|
|
114
|
+
const match = boards.find(b =>
|
|
115
|
+
b.shortId === nameOrId ||
|
|
116
|
+
(b.name || '').toLowerCase() === nameOrId.toLowerCase()
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!match) {
|
|
120
|
+
return error(`Board "${nameOrId}" not found.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await withSpinner('Archiving board...', () =>
|
|
124
|
+
client.post(`/boards/archive/${match.shortId}`)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
success(`Archived board: ${chalk.bold(match.name)}`);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
error(`Failed to archive board: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
board
|
|
134
|
+
.command('info <nameOrId>')
|
|
135
|
+
.description('Show board details')
|
|
136
|
+
.action(async (nameOrId) => {
|
|
137
|
+
try {
|
|
138
|
+
const data = await withSpinner('Fetching board...', () =>
|
|
139
|
+
client.get(`/boards/${nameOrId}`)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const b = data.data || data;
|
|
143
|
+
console.log();
|
|
144
|
+
console.log(chalk.bold(' Name: '), b.name);
|
|
145
|
+
console.log(chalk.bold(' ID: '), b.shortId);
|
|
146
|
+
console.log(chalk.bold(' Visibility: '), b.visibility || 'Private');
|
|
147
|
+
console.log(chalk.bold(' Members: '), b.members?.length || 0);
|
|
148
|
+
console.log(chalk.bold(' Lists: '), b.actionLists?.length || 0);
|
|
149
|
+
console.log(chalk.bold(' Created: '), new Date(b.createdAt).toLocaleDateString());
|
|
150
|
+
console.log();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
error(`Failed to get board info: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
module.exports = board;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { output, success, error } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const card = new Command('card')
|
|
9
|
+
.alias('c')
|
|
10
|
+
.description('Card management commands');
|
|
11
|
+
|
|
12
|
+
card
|
|
13
|
+
.command('list')
|
|
14
|
+
.alias('ls')
|
|
15
|
+
.description('List cards')
|
|
16
|
+
.option('-b, --board <boardId>', 'Board ID to filter by')
|
|
17
|
+
.option('-l, --list <listId>', 'List ID to filter by')
|
|
18
|
+
.option('-a, --assignee <user>', 'Filter by assignee (use "me" for yourself)')
|
|
19
|
+
.option('--due <when>', 'Filter by due date (today|week|overdue)')
|
|
20
|
+
.option('-f, --format <format>', 'Output format')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
try {
|
|
23
|
+
let path;
|
|
24
|
+
|
|
25
|
+
if (options.assignee === 'me') {
|
|
26
|
+
const userName = config.get('userName');
|
|
27
|
+
path = `/${userName}/cards`;
|
|
28
|
+
} else if (options.board) {
|
|
29
|
+
path = `/boards/${options.board}/cards`;
|
|
30
|
+
} else if (options.list) {
|
|
31
|
+
path = `/actionLists/${options.list}/cards/`;
|
|
32
|
+
} else {
|
|
33
|
+
// Get cards for current user
|
|
34
|
+
const userName = config.get('userName');
|
|
35
|
+
if (!userName) {
|
|
36
|
+
return error('No user context. Run "zoobbe auth login" first.');
|
|
37
|
+
}
|
|
38
|
+
path = `/${userName}/cards`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = await withSpinner('Fetching cards...', () =>
|
|
42
|
+
client.get(path)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
let cards = data.data || data;
|
|
46
|
+
|
|
47
|
+
// Apply due date filter
|
|
48
|
+
if (options.due && Array.isArray(cards)) {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
cards = cards.filter(c => {
|
|
51
|
+
if (!c.dueDate) return false;
|
|
52
|
+
const due = new Date(c.dueDate);
|
|
53
|
+
switch (options.due) {
|
|
54
|
+
case 'today': return due.toDateString() === now.toDateString();
|
|
55
|
+
case 'week': return due <= new Date(now.getTime() + 7 * 86400000);
|
|
56
|
+
case 'overdue': return due < now;
|
|
57
|
+
default: return true;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rows = (Array.isArray(cards) ? cards : []).map(c => ({
|
|
63
|
+
title: (c.title || c.name || '').substring(0, 50),
|
|
64
|
+
id: c.shortId || c._id,
|
|
65
|
+
list: c.actionList?.name || c.actionListName || '',
|
|
66
|
+
due: c.dueDate ? new Date(c.dueDate).toLocaleDateString() : '-',
|
|
67
|
+
members: String(c.members?.length || 0),
|
|
68
|
+
priority: c.priority || '-',
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
output(rows, {
|
|
72
|
+
headers: ['Title', 'ID', 'List', 'Due', 'Members', 'Priority'],
|
|
73
|
+
format: options.format,
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
error(`Failed to list cards: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
card
|
|
81
|
+
.command('create <title>')
|
|
82
|
+
.description('Create a new card')
|
|
83
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
84
|
+
.option('-l, --list <listId>', 'Action list ID (uses first list if not specified)')
|
|
85
|
+
.option('-d, --description <desc>', 'Card description')
|
|
86
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD)')
|
|
87
|
+
.option('--priority <level>', 'Priority (low|medium|high|critical)')
|
|
88
|
+
.action(async (title, options) => {
|
|
89
|
+
try {
|
|
90
|
+
if (!options.board) {
|
|
91
|
+
return error('Board ID is required. Use -b <boardId>.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let listId = options.list;
|
|
95
|
+
|
|
96
|
+
// If no list specified, get the first list from the board
|
|
97
|
+
if (!listId) {
|
|
98
|
+
const listsData = await client.get(`/boards/${options.board}/lists`);
|
|
99
|
+
const lists = listsData.data || listsData;
|
|
100
|
+
if (!lists || lists.length === 0) {
|
|
101
|
+
return error('Board has no lists. Create one first.');
|
|
102
|
+
}
|
|
103
|
+
listId = lists[0].shortId || lists[0]._id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = {
|
|
107
|
+
title,
|
|
108
|
+
boardId: options.board,
|
|
109
|
+
actionListId: listId,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (options.description) body.description = options.description;
|
|
113
|
+
if (options.due) body.dueDate = options.due;
|
|
114
|
+
if (options.priority) body.priority = options.priority;
|
|
115
|
+
|
|
116
|
+
const data = await withSpinner('Creating card...', () =>
|
|
117
|
+
client.post('/cards', body)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const c = data.data || data;
|
|
121
|
+
success(`Created card: ${chalk.bold(title)} (${c.shortId || c._id})`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
error(`Failed to create card: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
card
|
|
128
|
+
.command('move <cardId>')
|
|
129
|
+
.description('Move a card to a different list')
|
|
130
|
+
.option('-l, --list <listId>', 'Target list ID (required)')
|
|
131
|
+
.action(async (cardId, options) => {
|
|
132
|
+
try {
|
|
133
|
+
if (!options.list) {
|
|
134
|
+
return error('Target list ID is required. Use -l <listId>.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await withSpinner('Moving card...', () =>
|
|
138
|
+
client.post(`/cards/update/${cardId}`, {
|
|
139
|
+
actionListId: options.list,
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
success(`Moved card ${cardId} to list ${options.list}`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
error(`Failed to move card: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
card
|
|
150
|
+
.command('assign <cardId>')
|
|
151
|
+
.description('Assign a user to a card')
|
|
152
|
+
.option('-u, --user <userId>', 'User ID to assign (required)')
|
|
153
|
+
.action(async (cardId, options) => {
|
|
154
|
+
try {
|
|
155
|
+
if (!options.user) {
|
|
156
|
+
return error('User ID is required. Use -u <userId>.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await withSpinner('Assigning card...', () =>
|
|
160
|
+
client.post(`/cards/${cardId}/addMember`, {
|
|
161
|
+
memberId: options.user,
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
success(`Assigned user to card ${cardId}`);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
error(`Failed to assign card: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
card
|
|
172
|
+
.command('comment <cardId> <message>')
|
|
173
|
+
.description('Add a comment to a card')
|
|
174
|
+
.action(async (cardId, message) => {
|
|
175
|
+
try {
|
|
176
|
+
await withSpinner('Adding comment...', () =>
|
|
177
|
+
client.post(`/cards/${cardId}/comments`, { text: message })
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
success(`Comment added to card ${cardId}`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
error(`Failed to add comment: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
card
|
|
187
|
+
.command('done <cardId>')
|
|
188
|
+
.description('Mark a card as complete')
|
|
189
|
+
.action(async (cardId) => {
|
|
190
|
+
try {
|
|
191
|
+
await withSpinner('Updating card...', () =>
|
|
192
|
+
client.post(`/cards/update/${cardId}`, { isComplete: true })
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
success(`Card ${cardId} marked as complete`);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
error(`Failed to update card: ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
card
|
|
202
|
+
.command('info <cardId>')
|
|
203
|
+
.description('Show card details')
|
|
204
|
+
.action(async (cardId) => {
|
|
205
|
+
try {
|
|
206
|
+
const data = await withSpinner('Fetching card...', () =>
|
|
207
|
+
client.get(`/cards/${cardId}`)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const c = data.data || data;
|
|
211
|
+
console.log();
|
|
212
|
+
console.log(chalk.bold(' Title: '), c.title || c.name);
|
|
213
|
+
console.log(chalk.bold(' ID: '), c.shortId || c._id);
|
|
214
|
+
console.log(chalk.bold(' List: '), c.actionList?.name || 'N/A');
|
|
215
|
+
console.log(chalk.bold(' Members: '), c.members?.length || 0);
|
|
216
|
+
console.log(chalk.bold(' Due Date: '), c.dueDate ? new Date(c.dueDate).toLocaleDateString() : 'None');
|
|
217
|
+
console.log(chalk.bold(' Priority: '), c.priority || 'None');
|
|
218
|
+
console.log(chalk.bold(' Complete: '), c.isComplete ? 'Yes' : 'No');
|
|
219
|
+
if (c.description) {
|
|
220
|
+
console.log(chalk.bold(' Description:'), c.description.substring(0, 100));
|
|
221
|
+
}
|
|
222
|
+
console.log();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
error(`Failed to get card info: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
module.exports = card;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const { success, error } = require('../lib/output');
|
|
5
|
+
|
|
6
|
+
const configCmd = new Command('config')
|
|
7
|
+
.description('CLI configuration');
|
|
8
|
+
|
|
9
|
+
configCmd
|
|
10
|
+
.command('set <key> <value>')
|
|
11
|
+
.description('Set a config value (apiUrl, format)')
|
|
12
|
+
.action((key, value) => {
|
|
13
|
+
const allowedKeys = ['apiUrl', 'format'];
|
|
14
|
+
if (!allowedKeys.includes(key)) {
|
|
15
|
+
return error(`Invalid config key. Allowed: ${allowedKeys.join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
config.set(key, value);
|
|
18
|
+
success(`Set ${key} = ${value}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
configCmd
|
|
22
|
+
.command('get <key>')
|
|
23
|
+
.description('Get a config value')
|
|
24
|
+
.action((key) => {
|
|
25
|
+
const value = config.get(key);
|
|
26
|
+
if (value !== undefined && value !== '') {
|
|
27
|
+
console.log(value);
|
|
28
|
+
} else {
|
|
29
|
+
error(`Config key "${key}" not set`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
configCmd
|
|
34
|
+
.command('list')
|
|
35
|
+
.alias('ls')
|
|
36
|
+
.description('Show all config values')
|
|
37
|
+
.action(() => {
|
|
38
|
+
const store = config.store;
|
|
39
|
+
console.log();
|
|
40
|
+
Object.entries(store).forEach(([key, value]) => {
|
|
41
|
+
// Mask API key
|
|
42
|
+
const displayValue = key === 'apiKey' && value
|
|
43
|
+
? `${value.substring(0, 16)}...`
|
|
44
|
+
: value;
|
|
45
|
+
console.log(` ${chalk.bold(key)}: ${displayValue || chalk.dim('(not set)')}`);
|
|
46
|
+
});
|
|
47
|
+
console.log();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
configCmd
|
|
51
|
+
.command('path')
|
|
52
|
+
.description('Show config file location')
|
|
53
|
+
.action(() => {
|
|
54
|
+
console.log(config.path);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
module.exports = configCmd;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { output, success, error } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const page = new Command('page')
|
|
9
|
+
.alias('p')
|
|
10
|
+
.description('Page management commands');
|
|
11
|
+
|
|
12
|
+
page
|
|
13
|
+
.command('list')
|
|
14
|
+
.alias('ls')
|
|
15
|
+
.description('List pages in the active workspace')
|
|
16
|
+
.option('-f, --format <format>', 'Output format')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
const workspaceId = config.get('activeWorkspace');
|
|
20
|
+
|
|
21
|
+
const data = await withSpinner('Fetching pages...', () =>
|
|
22
|
+
client.get(`/pages/all${workspaceId ? `?workspaceId=${workspaceId}` : ''}`)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const pages = data.data || data;
|
|
26
|
+
const rows = (Array.isArray(pages) ? pages : []).map(p => ({
|
|
27
|
+
title: (p.title || 'Untitled').substring(0, 50),
|
|
28
|
+
id: p.shortId || p._id,
|
|
29
|
+
visibility: p.visibility || 'Private',
|
|
30
|
+
updated: new Date(p.updatedAt || p.createdAt).toLocaleDateString(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
output(rows, {
|
|
34
|
+
headers: ['Title', 'ID', 'Visibility', 'Updated'],
|
|
35
|
+
format: options.format,
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
error(`Failed to list pages: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
page
|
|
43
|
+
.command('create <title>')
|
|
44
|
+
.description('Create a new page')
|
|
45
|
+
.option('--workspace <id>', 'Workspace ID (uses active workspace)')
|
|
46
|
+
.action(async (title, options) => {
|
|
47
|
+
try {
|
|
48
|
+
const workspaceId = options.workspace || config.get('activeWorkspace');
|
|
49
|
+
if (!workspaceId) {
|
|
50
|
+
return error('No workspace specified. Use --workspace or set an active workspace.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = await withSpinner('Creating page...', () =>
|
|
54
|
+
client.post('/pages', { title, workspaceId })
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const p = data.data || data;
|
|
58
|
+
success(`Created page: ${chalk.bold(title)} (${p.shortId || p._id})`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
error(`Failed to create page: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
page
|
|
65
|
+
.command('open <pageId>')
|
|
66
|
+
.description('Open a page in the browser')
|
|
67
|
+
.action(async (pageId) => {
|
|
68
|
+
try {
|
|
69
|
+
const open = require('open');
|
|
70
|
+
const apiUrl = config.get('apiUrl');
|
|
71
|
+
const baseUrl = apiUrl.replace('api.', '');
|
|
72
|
+
await open(`${baseUrl}/page/${pageId}`);
|
|
73
|
+
success('Opened page in browser');
|
|
74
|
+
} catch (err) {
|
|
75
|
+
error(`Failed to open page: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
page
|
|
80
|
+
.command('info <pageId>')
|
|
81
|
+
.description('Show page details')
|
|
82
|
+
.action(async (pageId) => {
|
|
83
|
+
try {
|
|
84
|
+
const data = await withSpinner('Fetching page...', () =>
|
|
85
|
+
client.get(`/pages/${pageId}`)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const p = data.data || data;
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(chalk.bold(' Title: '), p.title || 'Untitled');
|
|
91
|
+
console.log(chalk.bold(' ID: '), p.shortId || p._id);
|
|
92
|
+
console.log(chalk.bold(' Visibility: '), p.visibility || 'Private');
|
|
93
|
+
console.log(chalk.bold(' Members: '), p.members?.length || 0);
|
|
94
|
+
console.log(chalk.bold(' Created: '), new Date(p.createdAt).toLocaleDateString());
|
|
95
|
+
console.log(chalk.bold(' Updated: '), new Date(p.updatedAt).toLocaleDateString());
|
|
96
|
+
console.log();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
error(`Failed to get page info: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
module.exports = page;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { output, error } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const search = new Command('search')
|
|
9
|
+
.alias('s')
|
|
10
|
+
.description('Search across boards, cards, and pages')
|
|
11
|
+
.argument('<query>', 'Search query')
|
|
12
|
+
.option('-f, --format <format>', 'Output format')
|
|
13
|
+
.action(async (query, options) => {
|
|
14
|
+
try {
|
|
15
|
+
const workspaceId = config.get('activeWorkspace');
|
|
16
|
+
|
|
17
|
+
const data = await withSpinner(`Searching for "${query}"...`, () =>
|
|
18
|
+
client.post('/global/search', {
|
|
19
|
+
query,
|
|
20
|
+
workspaceId,
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const results = data.data || data;
|
|
25
|
+
|
|
26
|
+
if (!results || (Array.isArray(results) && results.length === 0)) {
|
|
27
|
+
console.log(chalk.yellow(' No results found.'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Format results based on type
|
|
32
|
+
const rows = (Array.isArray(results) ? results : [results]).map(r => ({
|
|
33
|
+
type: r.type || 'item',
|
|
34
|
+
title: (r.title || r.name || '').substring(0, 50),
|
|
35
|
+
id: r.shortId || r._id || '',
|
|
36
|
+
context: r.boardName || r.workspaceName || '',
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
output(rows, {
|
|
40
|
+
headers: ['Type', 'Title', 'ID', 'Context'],
|
|
41
|
+
format: options.format,
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
error(`Search failed: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = search;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { output, error, info } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const status = new Command('status')
|
|
9
|
+
.description('Show your cards across all boards')
|
|
10
|
+
.option('-f, --format <format>', 'Output format')
|
|
11
|
+
.option('--due', 'Only show cards with due dates')
|
|
12
|
+
.option('--overdue', 'Only show overdue cards')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
const userName = config.get('userName');
|
|
16
|
+
if (!userName) {
|
|
17
|
+
return error('Not logged in. Run "zoobbe auth login" first.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = await withSpinner('Fetching your cards...', () =>
|
|
21
|
+
client.get(`/${userName}/cards`)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
let cards = data.data || data;
|
|
25
|
+
const now = new Date();
|
|
26
|
+
|
|
27
|
+
if (options.overdue) {
|
|
28
|
+
cards = cards.filter(c => c.dueDate && new Date(c.dueDate) < now);
|
|
29
|
+
} else if (options.due) {
|
|
30
|
+
cards = cards.filter(c => c.dueDate);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!cards || cards.length === 0) {
|
|
34
|
+
info('No cards assigned to you.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.bold(` Your cards (${cards.length}):`));
|
|
40
|
+
console.log();
|
|
41
|
+
|
|
42
|
+
const rows = cards.map(c => {
|
|
43
|
+
let dueStr = '-';
|
|
44
|
+
if (c.dueDate) {
|
|
45
|
+
const due = new Date(c.dueDate);
|
|
46
|
+
dueStr = due < now ? chalk.red(due.toLocaleDateString()) : chalk.green(due.toLocaleDateString());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
title: (c.title || c.name || '').substring(0, 40),
|
|
51
|
+
id: c.shortId || '',
|
|
52
|
+
board: c.boardName || c.board?.name || '',
|
|
53
|
+
list: c.actionList?.name || '',
|
|
54
|
+
due: dueStr,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
output(rows, {
|
|
59
|
+
headers: ['Title', 'ID', 'Board', 'List', 'Due'],
|
|
60
|
+
format: options.format,
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
error(`Failed to fetch status: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
module.exports = status;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const client = require('../lib/client');
|
|
5
|
+
const { output, success, error, info } = require('../lib/output');
|
|
6
|
+
const { withSpinner } = require('../utils/spinner');
|
|
7
|
+
|
|
8
|
+
const workspace = new Command('workspace')
|
|
9
|
+
.alias('ws')
|
|
10
|
+
.description('Workspace management commands');
|
|
11
|
+
|
|
12
|
+
workspace
|
|
13
|
+
.command('list')
|
|
14
|
+
.alias('ls')
|
|
15
|
+
.description('List your workspaces')
|
|
16
|
+
.option('-f, --format <format>', 'Output format (table|json|plain)')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
const data = await withSpinner('Fetching workspaces...', () =>
|
|
20
|
+
client.get('/workspaces/me')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const workspaces = data.data || data;
|
|
24
|
+
const active = config.get('activeWorkspace');
|
|
25
|
+
|
|
26
|
+
const rows = workspaces.map(ws => ({
|
|
27
|
+
name: ws.name + (ws.shortId === active ? chalk.green(' (active)') : ''),
|
|
28
|
+
id: ws.shortId,
|
|
29
|
+
members: String(ws.members?.length || 0),
|
|
30
|
+
boards: String(ws.boards?.length || 0),
|
|
31
|
+
role: ws.members?.find(m => m.user === config.get('userId'))?.role || 'member',
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
output(rows, {
|
|
35
|
+
headers: ['Name', 'ID', 'Members', 'Boards', 'Role'],
|
|
36
|
+
format: options.format,
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
error(`Failed to list workspaces: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
workspace
|
|
44
|
+
.command('switch <nameOrId>')
|
|
45
|
+
.description('Set the active workspace')
|
|
46
|
+
.action(async (nameOrId) => {
|
|
47
|
+
try {
|
|
48
|
+
const data = await withSpinner('Fetching workspaces...', () =>
|
|
49
|
+
client.get('/workspaces/me')
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const workspaces = data.data || data;
|
|
53
|
+
const match = workspaces.find(ws =>
|
|
54
|
+
ws.shortId === nameOrId ||
|
|
55
|
+
ws.name.toLowerCase() === nameOrId.toLowerCase() ||
|
|
56
|
+
ws.shortName?.toLowerCase() === nameOrId.toLowerCase()
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!match) {
|
|
60
|
+
return error(`Workspace "${nameOrId}" not found. Run "zoobbe workspace list" to see available workspaces.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
config.set('activeWorkspace', match.shortId);
|
|
64
|
+
config.set('activeWorkspaceName', match.name);
|
|
65
|
+
success(`Switched to workspace: ${chalk.bold(match.name)}`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
error(`Failed to switch workspace: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
workspace
|
|
72
|
+
.command('create <name>')
|
|
73
|
+
.description('Create a new workspace')
|
|
74
|
+
.action(async (name) => {
|
|
75
|
+
try {
|
|
76
|
+
const data = await withSpinner('Creating workspace...', () =>
|
|
77
|
+
client.post('/workspaces/me', { name })
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const ws = data.data || data;
|
|
81
|
+
success(`Created workspace: ${chalk.bold(ws.name)} (${ws.shortId})`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
error(`Failed to create workspace: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
workspace
|
|
88
|
+
.command('members')
|
|
89
|
+
.description('List members of the active workspace')
|
|
90
|
+
.option('-f, --format <format>', 'Output format')
|
|
91
|
+
.action(async (options) => {
|
|
92
|
+
try {
|
|
93
|
+
const workspaceId = config.get('activeWorkspace');
|
|
94
|
+
if (!workspaceId) {
|
|
95
|
+
return error('No active workspace. Run "zoobbe workspace switch <name>".');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const data = await withSpinner('Fetching members...', () =>
|
|
99
|
+
client.get(`/members/workspace/${workspaceId}`)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const members = data.data || data;
|
|
103
|
+
const rows = members.map(m => {
|
|
104
|
+
const user = m.user || m;
|
|
105
|
+
return {
|
|
106
|
+
name: user.userName || user.username || 'N/A',
|
|
107
|
+
email: user.email || 'N/A',
|
|
108
|
+
role: m.role || 'member',
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
output(rows, {
|
|
113
|
+
headers: ['Name', 'Email', 'Role'],
|
|
114
|
+
format: options.format,
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
error(`Failed to list members: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
workspace
|
|
122
|
+
.command('info')
|
|
123
|
+
.description('Show active workspace details')
|
|
124
|
+
.action(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const workspaceId = config.get('activeWorkspace');
|
|
127
|
+
if (!workspaceId) {
|
|
128
|
+
return error('No active workspace. Run "zoobbe workspace switch <name>".');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = await withSpinner('Fetching workspace info...', () =>
|
|
132
|
+
client.get(`/workspaces/${workspaceId}`)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const ws = data.data || data;
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(chalk.bold(' Name: '), ws.name);
|
|
138
|
+
console.log(chalk.bold(' ID: '), ws.shortId);
|
|
139
|
+
console.log(chalk.bold(' Members: '), ws.members?.length || 0);
|
|
140
|
+
console.log(chalk.bold(' Boards: '), ws.boards?.length || 0);
|
|
141
|
+
console.log();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
error(`Failed to get workspace info: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
module.exports = workspace;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const pkg = require('../package.json');
|
|
4
|
+
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('zoobbe')
|
|
9
|
+
.description('Zoobbe CLI — Manage boards, cards, and projects from the terminal')
|
|
10
|
+
.version(pkg.version)
|
|
11
|
+
.option('-f, --format <format>', 'Output format (table|json|plain)')
|
|
12
|
+
.option('--workspace <id>', 'Override active workspace');
|
|
13
|
+
|
|
14
|
+
// Register commands
|
|
15
|
+
program.addCommand(require('./commands/auth'));
|
|
16
|
+
program.addCommand(require('./commands/workspace'));
|
|
17
|
+
program.addCommand(require('./commands/board'));
|
|
18
|
+
program.addCommand(require('./commands/card'));
|
|
19
|
+
program.addCommand(require('./commands/page'));
|
|
20
|
+
program.addCommand(require('./commands/search'));
|
|
21
|
+
program.addCommand(require('./commands/ai'));
|
|
22
|
+
program.addCommand(require('./commands/status'));
|
|
23
|
+
program.addCommand(require('./commands/config'));
|
|
24
|
+
|
|
25
|
+
// Global option handling
|
|
26
|
+
program.hook('preAction', (thisCommand) => {
|
|
27
|
+
const config = require('./lib/config');
|
|
28
|
+
const opts = program.opts();
|
|
29
|
+
|
|
30
|
+
if (opts.format) {
|
|
31
|
+
config.set('format', opts.format);
|
|
32
|
+
}
|
|
33
|
+
if (opts.workspace) {
|
|
34
|
+
config.set('activeWorkspace', opts.workspace);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Default action (no command)
|
|
39
|
+
program.action(() => {
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(chalk.bold(' Zoobbe CLI') + ` v${pkg.version}`);
|
|
42
|
+
console.log();
|
|
43
|
+
console.log(' Quick start:');
|
|
44
|
+
console.log(` ${chalk.cyan('zoobbe auth login')} Authenticate with Zoobbe`);
|
|
45
|
+
console.log(` ${chalk.cyan('zoobbe workspace list')} List your workspaces`);
|
|
46
|
+
console.log(` ${chalk.cyan('zoobbe workspace switch')} Set active workspace`);
|
|
47
|
+
console.log(` ${chalk.cyan('zoobbe board list')} List boards`);
|
|
48
|
+
console.log(` ${chalk.cyan('zoobbe card list')} List your cards`);
|
|
49
|
+
console.log(` ${chalk.cyan('zoobbe ask "..."')} AI-powered commands`);
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(` Run ${chalk.cyan('zoobbe --help')} for all commands.`);
|
|
52
|
+
console.log();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const config = require('./config');
|
|
2
|
+
|
|
3
|
+
class ZoobbeClient {
|
|
4
|
+
get baseUrl() {
|
|
5
|
+
return config.get('apiUrl');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get apiKey() {
|
|
9
|
+
return config.get('apiKey');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get headers() {
|
|
13
|
+
return {
|
|
14
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'User-Agent': 'Zoobbe-CLI/1.0',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ensureAuth() {
|
|
21
|
+
if (!this.apiKey) {
|
|
22
|
+
throw new Error('Not authenticated. Run "zoobbe login" first.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async request(method, path, body = null) {
|
|
27
|
+
this.ensureAuth();
|
|
28
|
+
|
|
29
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
30
|
+
const options = {
|
|
31
|
+
method,
|
|
32
|
+
headers: this.headers,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (body && method !== 'GET') {
|
|
36
|
+
options.body = JSON.stringify(body);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const response = await fetch(url, options);
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const msg = data.error || data.message || `Request failed with status ${response.status}`;
|
|
44
|
+
const error = new Error(msg);
|
|
45
|
+
error.status = response.status;
|
|
46
|
+
error.data = data;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async get(path) {
|
|
54
|
+
return this.request('GET', path);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async post(path, body) {
|
|
58
|
+
return this.request('POST', path, body);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async put(path, body) {
|
|
62
|
+
return this.request('PUT', path, body);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async patch(path, body) {
|
|
66
|
+
return this.request('PATCH', path, body);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async delete(path) {
|
|
70
|
+
return this.request('DELETE', path);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = new ZoobbeClient();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const Conf = require('conf');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const config = new Conf({
|
|
6
|
+
projectName: 'zoobbe',
|
|
7
|
+
cwd: path.join(os.homedir(), '.zoobbe'),
|
|
8
|
+
schema: {
|
|
9
|
+
apiKey: { type: 'string', default: '' },
|
|
10
|
+
apiUrl: { type: 'string', default: 'https://api.zoobbe.com' },
|
|
11
|
+
activeWorkspace: { type: 'string', default: '' },
|
|
12
|
+
activeWorkspaceName: { type: 'string', default: '' },
|
|
13
|
+
format: { type: 'string', enum: ['table', 'json', 'plain'], default: 'table' },
|
|
14
|
+
userId: { type: 'string', default: '' },
|
|
15
|
+
userName: { type: 'string', default: '' },
|
|
16
|
+
email: { type: 'string', default: '' },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
module.exports = config;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const Table = require('cli-table3');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Output data in the configured format (table, json, or plain).
|
|
7
|
+
*/
|
|
8
|
+
function output(data, options = {}) {
|
|
9
|
+
const format = options.format || config.get('format') || 'table';
|
|
10
|
+
|
|
11
|
+
switch (format) {
|
|
12
|
+
case 'json':
|
|
13
|
+
console.log(JSON.stringify(data, null, 2));
|
|
14
|
+
break;
|
|
15
|
+
case 'plain':
|
|
16
|
+
if (Array.isArray(data)) {
|
|
17
|
+
data.forEach(item => {
|
|
18
|
+
const values = Object.values(item);
|
|
19
|
+
console.log(values.join('\t'));
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
console.log(Object.values(data).join('\t'));
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
case 'table':
|
|
26
|
+
default:
|
|
27
|
+
if (options.headers && Array.isArray(data)) {
|
|
28
|
+
printTable(options.headers, data);
|
|
29
|
+
} else if (Array.isArray(data)) {
|
|
30
|
+
data.forEach(item => console.log(item));
|
|
31
|
+
} else {
|
|
32
|
+
console.log(data);
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printTable(headers, rows) {
|
|
39
|
+
const table = new Table({
|
|
40
|
+
head: headers.map(h => chalk.cyan.bold(h)),
|
|
41
|
+
style: { head: [], border: [] },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
rows.forEach(row => {
|
|
45
|
+
if (Array.isArray(row)) {
|
|
46
|
+
table.push(row);
|
|
47
|
+
} else {
|
|
48
|
+
table.push(headers.map(h => {
|
|
49
|
+
const key = h.toLowerCase().replace(/\s+/g, '');
|
|
50
|
+
return row[key] || row[h] || '';
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log(table.toString());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function success(message) {
|
|
59
|
+
console.log(chalk.green('✓'), message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function error(message) {
|
|
63
|
+
console.error(chalk.red('✗'), message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function warn(message) {
|
|
67
|
+
console.log(chalk.yellow('!'), message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function info(message) {
|
|
71
|
+
console.log(chalk.blue('ℹ'), message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { output, printTable, success, error, warn, info };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const ora = require('ora');
|
|
2
|
+
|
|
3
|
+
function createSpinner(text) {
|
|
4
|
+
return ora({ text, spinner: 'dots' });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Execute an async function with a spinner.
|
|
9
|
+
*/
|
|
10
|
+
async function withSpinner(text, fn) {
|
|
11
|
+
const spinner = createSpinner(text);
|
|
12
|
+
spinner.start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await fn(spinner);
|
|
16
|
+
spinner.succeed();
|
|
17
|
+
return result;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
spinner.fail(error.message);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { createSpinner, withSpinner };
|