@workdynamite/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/README.md +39 -0
- package/bin/workdynamite.js +3 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +62 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +99 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +24 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/pick.d.ts +3 -0
- package/dist/commands/pick.d.ts.map +1 -0
- package/dist/commands/pick.js +81 -0
- package/dist/commands/pick.js.map +1 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +119 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +80 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/tasks.d.ts +3 -0
- package/dist/commands/tasks.d.ts.map +1 -0
- package/dist/commands/tasks.js +102 -0
- package/dist/commands/tasks.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +70 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +71 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +55 -0
- package/dist/lib/config.js.map +1 -0
- package/package.json +47 -0
- package/src/commands/config.ts +69 -0
- package/src/commands/login.ts +116 -0
- package/src/commands/logout.ts +21 -0
- package/src/commands/pick.ts +80 -0
- package/src/commands/projects.ts +139 -0
- package/src/commands/status.ts +87 -0
- package/src/commands/tasks.ts +112 -0
- package/src/index.ts +50 -0
- package/src/lib/api.ts +128 -0
- package/src/lib/config.ts +61 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { clearConfig, isAuthenticated, getConfig } from '../lib/config.js';
|
|
4
|
+
|
|
5
|
+
export const logoutCommand = new Command('logout')
|
|
6
|
+
.description('Sign out of WorkDynamite')
|
|
7
|
+
.action(async () => {
|
|
8
|
+
if (!isAuthenticated()) {
|
|
9
|
+
console.log(chalk.yellow('Not currently logged in.'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
const email = config.user?.email || 'unknown';
|
|
15
|
+
|
|
16
|
+
clearConfig();
|
|
17
|
+
|
|
18
|
+
console.log(chalk.green(`✓ Logged out from ${email}`));
|
|
19
|
+
console.log(chalk.dim('Your API token has been removed from local storage.'));
|
|
20
|
+
console.log(chalk.dim('To revoke the token entirely, visit Workspace Settings in the web app.'));
|
|
21
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import clipboard from 'clipboardy';
|
|
5
|
+
import { getTasks } from '../lib/api.js';
|
|
6
|
+
import { isAuthenticated, getConfig } from '../lib/config.js';
|
|
7
|
+
|
|
8
|
+
export const pickCommand = new Command('pick')
|
|
9
|
+
.description('Copy a task reference to clipboard for commit messages')
|
|
10
|
+
.argument('<number>', 'Task number to pick')
|
|
11
|
+
.option('-w, --wip', 'Use WIP prefix')
|
|
12
|
+
.option('-f, --fixes', 'Use Fixes prefix (default)')
|
|
13
|
+
.option('-c, --closes', 'Use Closes prefix')
|
|
14
|
+
.option('-r, --refs', 'Use Refs prefix')
|
|
15
|
+
.action(async (taskNumber, options) => {
|
|
16
|
+
if (!isAuthenticated()) {
|
|
17
|
+
console.log(chalk.red('Not logged in. Run "workdynamite login" first.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spinner = ora('Finding task...').start();
|
|
22
|
+
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
|
|
25
|
+
// Fetch tasks to find the one matching the number
|
|
26
|
+
const { data, error } = await getTasks({
|
|
27
|
+
project: config.default_project || undefined,
|
|
28
|
+
limit: 100,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
spinner.fail(chalk.red(`Failed to fetch tasks: ${error}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const task = data!.tasks.find(t => t.number === parseInt(taskNumber, 10));
|
|
37
|
+
|
|
38
|
+
if (!task) {
|
|
39
|
+
spinner.fail(chalk.red(`Task #${taskNumber} not found in your assigned tasks.`));
|
|
40
|
+
console.log(chalk.dim('Make sure you\'re looking at the right project.'));
|
|
41
|
+
console.log(chalk.dim('Run "workdynamite tasks" to see available tasks.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Determine which prefix to use
|
|
46
|
+
let refType: 'wip' | 'fixes' | 'closes' | 'refs' = 'fixes';
|
|
47
|
+
if (options.wip) refType = 'wip';
|
|
48
|
+
else if (options.closes) refType = 'closes';
|
|
49
|
+
else if (options.refs) refType = 'refs';
|
|
50
|
+
// else default to fixes
|
|
51
|
+
|
|
52
|
+
const reference = task.refs[refType];
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await clipboard.write(reference);
|
|
56
|
+
spinner.succeed(chalk.green('Copied to clipboard!'));
|
|
57
|
+
|
|
58
|
+
console.log('\n' + chalk.dim('─'.repeat(50)));
|
|
59
|
+
console.log('\n ' + chalk.bold(reference));
|
|
60
|
+
console.log('\n' + chalk.dim('─'.repeat(50)));
|
|
61
|
+
|
|
62
|
+
console.log(chalk.dim('\nPaste this in your commit message or PR title.'));
|
|
63
|
+
console.log(chalk.dim('The webhook will automatically update the task.\n'));
|
|
64
|
+
|
|
65
|
+
// Show other options
|
|
66
|
+
console.log(chalk.dim('Other formats:'));
|
|
67
|
+
if (refType !== 'wip') console.log(chalk.dim(` -w, --wip → ${task.refs.wip}`));
|
|
68
|
+
if (refType !== 'fixes') console.log(chalk.dim(` -f, --fixes → ${task.refs.fixes}`));
|
|
69
|
+
if (refType !== 'closes') console.log(chalk.dim(` -c, --closes → ${task.refs.closes}`));
|
|
70
|
+
if (refType !== 'refs') console.log(chalk.dim(` -r, --refs → ${task.refs.refs}`));
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
} catch (clipboardError) {
|
|
74
|
+
spinner.warn(chalk.yellow('Could not copy to clipboard.'));
|
|
75
|
+
console.log('\n' + chalk.dim('─'.repeat(50)));
|
|
76
|
+
console.log('\n ' + chalk.bold(reference));
|
|
77
|
+
console.log('\n' + chalk.dim('─'.repeat(50)));
|
|
78
|
+
console.log(chalk.dim('\nCopy the text above manually.\n'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { getProjects } from '../lib/api.js';
|
|
6
|
+
import { isAuthenticated, getConfig, setDefaultProject } from '../lib/config.js';
|
|
7
|
+
|
|
8
|
+
export const projectsCommand = new Command('projects')
|
|
9
|
+
.description('List and manage projects')
|
|
10
|
+
.option('--set', 'Interactively set the default project')
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
if (!isAuthenticated()) {
|
|
13
|
+
console.log(chalk.red('Not logged in. Run "workdynamite login" first.'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const spinner = ora('Fetching projects...').start();
|
|
18
|
+
|
|
19
|
+
const { data, error } = await getProjects();
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
spinner.fail(chalk.red(`Failed to fetch projects: ${error}`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
spinner.stop();
|
|
27
|
+
|
|
28
|
+
const projects = data!.projects;
|
|
29
|
+
const config = getConfig();
|
|
30
|
+
|
|
31
|
+
if (projects.length === 0) {
|
|
32
|
+
console.log(chalk.yellow('\nNo projects found.'));
|
|
33
|
+
console.log(chalk.dim('Create a project in the web app to see it here.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options.set) {
|
|
38
|
+
// Interactive project selection
|
|
39
|
+
const { selectedProject } = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: 'list',
|
|
42
|
+
name: 'selectedProject',
|
|
43
|
+
message: 'Select default project:',
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: '(None - show all projects)', value: null },
|
|
46
|
+
...projects.map(p => ({
|
|
47
|
+
name: `${p.name} (${p.status})`,
|
|
48
|
+
value: p.id,
|
|
49
|
+
})),
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
setDefaultProject(selectedProject);
|
|
55
|
+
|
|
56
|
+
if (selectedProject) {
|
|
57
|
+
const project = projects.find(p => p.id === selectedProject);
|
|
58
|
+
console.log(chalk.green(`\n✓ Default project set to: ${project?.name}`));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(chalk.green('\n✓ Default project cleared. Tasks from all projects will be shown.'));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// List projects
|
|
66
|
+
console.log(chalk.bold('\n📁 Your Projects\n'));
|
|
67
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
68
|
+
|
|
69
|
+
for (const project of projects) {
|
|
70
|
+
const isDefault = project.id === config.default_project;
|
|
71
|
+
const statusColor = getStatusColor(project.status);
|
|
72
|
+
|
|
73
|
+
console.log(
|
|
74
|
+
` ${isDefault ? chalk.green('→') : ' '} ` +
|
|
75
|
+
`${chalk.bold(project.name.padEnd(30))} ` +
|
|
76
|
+
`${statusColor(project.status.padEnd(12))} ` +
|
|
77
|
+
`${chalk.dim(`${project.progress}%`)} ` +
|
|
78
|
+
`${chalk.dim(`[${project.role}]`)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
83
|
+
console.log(chalk.dim(`\n${projects.length} project(s)`));
|
|
84
|
+
|
|
85
|
+
if (config.default_project) {
|
|
86
|
+
const defaultProject = projects.find(p => p.id === config.default_project);
|
|
87
|
+
console.log(chalk.dim(`Default: ${defaultProject?.name || 'Unknown'}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(chalk.dim('\nRun "workdynamite projects --set" to change the default project.\n'));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
projectsCommand
|
|
94
|
+
.command('set <name>')
|
|
95
|
+
.description('Set default project by name')
|
|
96
|
+
.action(async (name) => {
|
|
97
|
+
if (!isAuthenticated()) {
|
|
98
|
+
console.log(chalk.red('Not logged in. Run "workdynamite login" first.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const spinner = ora('Fetching projects...').start();
|
|
103
|
+
|
|
104
|
+
const { data, error } = await getProjects();
|
|
105
|
+
|
|
106
|
+
if (error) {
|
|
107
|
+
spinner.fail(chalk.red(`Failed to fetch projects: ${error}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const project = data!.projects.find(
|
|
112
|
+
p => p.name.toLowerCase() === name.toLowerCase() || p.id === name
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!project) {
|
|
116
|
+
spinner.fail(chalk.red(`Project "${name}" not found.`));
|
|
117
|
+
console.log(chalk.dim('Run "workdynamite projects" to see available projects.'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setDefaultProject(project.id);
|
|
122
|
+
spinner.succeed(chalk.green(`Default project set to: ${project.name}`));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function getStatusColor(status: string): (text: string) => string {
|
|
126
|
+
switch (status.toLowerCase()) {
|
|
127
|
+
case 'active':
|
|
128
|
+
case 'in_progress':
|
|
129
|
+
return chalk.green;
|
|
130
|
+
case 'planning':
|
|
131
|
+
return chalk.blue;
|
|
132
|
+
case 'on_hold':
|
|
133
|
+
return chalk.yellow;
|
|
134
|
+
case 'completed':
|
|
135
|
+
return chalk.gray;
|
|
136
|
+
default:
|
|
137
|
+
return chalk.white;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getMe, getProjects, getTasks } from '../lib/api.js';
|
|
5
|
+
import { isAuthenticated, getConfig } from '../lib/config.js';
|
|
6
|
+
|
|
7
|
+
export const statusCommand = new Command('status')
|
|
8
|
+
.description('Show current login status and context')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const config = getConfig();
|
|
11
|
+
|
|
12
|
+
console.log(chalk.bold('\n🔧 WorkDynamite CLI Status\n'));
|
|
13
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
14
|
+
|
|
15
|
+
if (!isAuthenticated()) {
|
|
16
|
+
console.log('\n ' + chalk.red('✗') + ' Not logged in');
|
|
17
|
+
console.log(chalk.dim('\n Run "workdynamite login" to authenticate.\n'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spinner = ora('Fetching status...').start();
|
|
22
|
+
|
|
23
|
+
// Fetch user info
|
|
24
|
+
const { data: meData, error: meError } = await getMe();
|
|
25
|
+
|
|
26
|
+
if (meError) {
|
|
27
|
+
spinner.fail(chalk.red(`Failed to fetch status: ${meError}`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fetch projects and tasks count
|
|
32
|
+
const { data: projectsData } = await getProjects();
|
|
33
|
+
const { data: tasksData } = await getTasks({ limit: 100 });
|
|
34
|
+
|
|
35
|
+
spinner.stop();
|
|
36
|
+
|
|
37
|
+
const user = meData!.user;
|
|
38
|
+
const projects = projectsData?.projects || [];
|
|
39
|
+
const tasks = tasksData?.tasks || [];
|
|
40
|
+
|
|
41
|
+
// Count tasks by state
|
|
42
|
+
const tasksByState = tasks.reduce((acc, task) => {
|
|
43
|
+
const state = task.state || 'unknown';
|
|
44
|
+
acc[state] = (acc[state] || 0) + 1;
|
|
45
|
+
return acc;
|
|
46
|
+
}, {} as Record<string, number>);
|
|
47
|
+
|
|
48
|
+
console.log('\n ' + chalk.green('✓') + ' Logged in');
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(' ' + chalk.dim('User: ') + chalk.bold(user.name || user.email));
|
|
51
|
+
console.log(' ' + chalk.dim('Email: ') + user.email);
|
|
52
|
+
console.log(' ' + chalk.dim('Projects: ') + projects.length);
|
|
53
|
+
console.log('');
|
|
54
|
+
|
|
55
|
+
// Default project
|
|
56
|
+
if (config.default_project) {
|
|
57
|
+
const defaultProject = projects.find(p => p.id === config.default_project);
|
|
58
|
+
console.log(' ' + chalk.dim('Default Project: ') + chalk.cyan(defaultProject?.name || 'Unknown'));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(' ' + chalk.dim('Default Project: ') + chalk.yellow('(none set)'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
65
|
+
console.log(chalk.bold('\n📋 Task Summary\n'));
|
|
66
|
+
|
|
67
|
+
if (tasks.length === 0) {
|
|
68
|
+
console.log(' ' + chalk.dim('No tasks assigned'));
|
|
69
|
+
} else {
|
|
70
|
+
const inProgress = tasksByState['in_progress'] || 0;
|
|
71
|
+
const open = tasksByState['open'] || tasksByState['todo'] || 0;
|
|
72
|
+
const done = tasksByState['done'] || tasksByState['completed'] || 0;
|
|
73
|
+
|
|
74
|
+
console.log(' ' + chalk.yellow(`In Progress: ${inProgress}`));
|
|
75
|
+
console.log(' ' + chalk.blue(`Open: ${open}`));
|
|
76
|
+
console.log(' ' + chalk.green(`Done: ${done}`));
|
|
77
|
+
console.log(' ' + chalk.dim(`Total: ${tasks.length}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
82
|
+
console.log(chalk.dim('\nQuick commands:'));
|
|
83
|
+
console.log(chalk.dim(' workdynamite tasks List your tasks'));
|
|
84
|
+
console.log(chalk.dim(' workdynamite pick <#> Copy task ref to clipboard'));
|
|
85
|
+
console.log(chalk.dim(' workdynamite projects List your projects'));
|
|
86
|
+
console.log('');
|
|
87
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getTasks, Task } from '../lib/api.js';
|
|
5
|
+
import { isAuthenticated, getConfig } from '../lib/config.js';
|
|
6
|
+
|
|
7
|
+
export const tasksCommand = new Command('tasks')
|
|
8
|
+
.description('List your assigned tasks')
|
|
9
|
+
.option('-p, --project <id>', 'Filter by project ID')
|
|
10
|
+
.option('-s, --state <state>', 'Filter by state (open, in_progress, done)')
|
|
11
|
+
.option('-l, --limit <number>', 'Maximum number of tasks', '20')
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
if (!isAuthenticated()) {
|
|
14
|
+
console.log(chalk.red('Not logged in. Run "workdynamite login" first.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const spinner = ora('Fetching tasks...').start();
|
|
19
|
+
|
|
20
|
+
const config = getConfig();
|
|
21
|
+
const projectId = options.project || config.default_project;
|
|
22
|
+
|
|
23
|
+
const { data, error } = await getTasks({
|
|
24
|
+
project: projectId,
|
|
25
|
+
state: options.state,
|
|
26
|
+
limit: parseInt(options.limit, 10),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (error) {
|
|
30
|
+
spinner.fail(chalk.red(`Failed to fetch tasks: ${error}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
spinner.stop();
|
|
35
|
+
|
|
36
|
+
const tasks = data!.tasks;
|
|
37
|
+
|
|
38
|
+
if (tasks.length === 0) {
|
|
39
|
+
console.log(chalk.yellow('\nNo tasks assigned to you.'));
|
|
40
|
+
console.log(chalk.dim('Create tasks in the web app to see them here.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Group tasks by project
|
|
45
|
+
const tasksByProject = tasks.reduce((acc, task) => {
|
|
46
|
+
const project = task.project || 'No Project';
|
|
47
|
+
if (!acc[project]) acc[project] = [];
|
|
48
|
+
acc[project].push(task);
|
|
49
|
+
return acc;
|
|
50
|
+
}, {} as Record<string, Task[]>);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.bold('\n📋 Your Assigned Tasks\n'));
|
|
53
|
+
|
|
54
|
+
for (const [project, projectTasks] of Object.entries(tasksByProject)) {
|
|
55
|
+
console.log(chalk.bold.blue(`${project}`));
|
|
56
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
57
|
+
|
|
58
|
+
for (const task of projectTasks) {
|
|
59
|
+
const stateColor = getStateColor(task.state);
|
|
60
|
+
const priorityBadge = getPriorityBadge(task.priority);
|
|
61
|
+
|
|
62
|
+
console.log(
|
|
63
|
+
` ${chalk.dim('#')}${chalk.bold(String(task.number).padEnd(5))} ` +
|
|
64
|
+
`${priorityBadge} ` +
|
|
65
|
+
`${task.title.substring(0, 40)}${task.title.length > 40 ? '...' : ''} ` +
|
|
66
|
+
`${stateColor(task.state)}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
73
|
+
console.log(chalk.dim(`Showing ${tasks.length} task(s)`));
|
|
74
|
+
console.log(chalk.dim('\nCommands:'));
|
|
75
|
+
console.log(chalk.dim(' workdynamite pick <number> Copy task ref to clipboard'));
|
|
76
|
+
console.log(chalk.dim(' workdynamite pick <number> -w Copy as WIP'));
|
|
77
|
+
console.log('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function getStateColor(state: string): (text: string) => string {
|
|
81
|
+
switch (state.toLowerCase()) {
|
|
82
|
+
case 'open':
|
|
83
|
+
case 'todo':
|
|
84
|
+
return chalk.gray;
|
|
85
|
+
case 'in_progress':
|
|
86
|
+
case 'in progress':
|
|
87
|
+
return chalk.yellow;
|
|
88
|
+
case 'done':
|
|
89
|
+
case 'completed':
|
|
90
|
+
return chalk.green;
|
|
91
|
+
case 'blocked':
|
|
92
|
+
return chalk.red;
|
|
93
|
+
default:
|
|
94
|
+
return chalk.white;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getPriorityBadge(priority: string): string {
|
|
99
|
+
switch (priority?.toLowerCase()) {
|
|
100
|
+
case 'critical':
|
|
101
|
+
case 'highest':
|
|
102
|
+
return chalk.bgRed.white(' !! ');
|
|
103
|
+
case 'high':
|
|
104
|
+
return chalk.red('[H]');
|
|
105
|
+
case 'medium':
|
|
106
|
+
return chalk.yellow('[M]');
|
|
107
|
+
case 'low':
|
|
108
|
+
return chalk.green('[L]');
|
|
109
|
+
default:
|
|
110
|
+
return chalk.dim('[–]');
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { loginCommand } from './commands/login.js';
|
|
3
|
+
import { logoutCommand } from './commands/logout.js';
|
|
4
|
+
import { tasksCommand } from './commands/tasks.js';
|
|
5
|
+
import { pickCommand } from './commands/pick.js';
|
|
6
|
+
import { projectsCommand } from './commands/projects.js';
|
|
7
|
+
import { statusCommand } from './commands/status.js';
|
|
8
|
+
import { configCommand } from './commands/config.js';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('workdynamite')
|
|
14
|
+
.description('WorkDynamite CLI - Manage tasks from your terminal')
|
|
15
|
+
.version('1.0.0');
|
|
16
|
+
|
|
17
|
+
// Register commands
|
|
18
|
+
program.addCommand(loginCommand);
|
|
19
|
+
program.addCommand(logoutCommand);
|
|
20
|
+
program.addCommand(tasksCommand);
|
|
21
|
+
program.addCommand(pickCommand);
|
|
22
|
+
program.addCommand(projectsCommand);
|
|
23
|
+
program.addCommand(statusCommand);
|
|
24
|
+
program.addCommand(configCommand);
|
|
25
|
+
|
|
26
|
+
// Aliases
|
|
27
|
+
program
|
|
28
|
+
.command('ls')
|
|
29
|
+
.description('Alias for "tasks" - list your assigned tasks')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
await tasksCommand.parseAsync(['tasks'], { from: 'user' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('p <number>')
|
|
36
|
+
.description('Alias for "pick" - copy task reference to clipboard')
|
|
37
|
+
.option('-w, --wip', 'Use WIP prefix')
|
|
38
|
+
.option('-f, --fixes', 'Use Fixes prefix (default)')
|
|
39
|
+
.option('-c, --closes', 'Use Closes prefix')
|
|
40
|
+
.option('-r, --refs', 'Use Refs prefix')
|
|
41
|
+
.action(async (number, options) => {
|
|
42
|
+
const args = ['pick', number];
|
|
43
|
+
if (options.wip) args.push('--wip');
|
|
44
|
+
if (options.fixes) args.push('--fixes');
|
|
45
|
+
if (options.closes) args.push('--closes');
|
|
46
|
+
if (options.refs) args.push('--refs');
|
|
47
|
+
await pickCommand.parseAsync(args, { from: 'user' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
program.parse();
|
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getToken, getApiUrl, getConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
interface ApiResponse<T = any> {
|
|
4
|
+
data?: T;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function request<T>(
|
|
9
|
+
endpoint: string,
|
|
10
|
+
options: RequestInit = {}
|
|
11
|
+
): Promise<ApiResponse<T>> {
|
|
12
|
+
const token = getToken();
|
|
13
|
+
const apiUrl = getApiUrl();
|
|
14
|
+
|
|
15
|
+
if (!token && !endpoint.startsWith('login/')) {
|
|
16
|
+
return { error: 'Not authenticated. Run "workdynamite login" first.' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const url = `${apiUrl}/${endpoint}`;
|
|
20
|
+
|
|
21
|
+
const headers: Record<string, string> = {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
...((options.headers as Record<string, string>) || {}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (token) {
|
|
27
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
headers,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const data = await response.json() as T & { error?: string };
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
return { error: data.error || `Request failed with status ${response.status}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { data };
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
return { error: error.message || 'Network request failed' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function initLogin(): Promise<ApiResponse<{
|
|
49
|
+
device_code: string;
|
|
50
|
+
user_code: string;
|
|
51
|
+
verification_uri: string;
|
|
52
|
+
verification_uri_complete: string;
|
|
53
|
+
expires_in: number;
|
|
54
|
+
interval: number;
|
|
55
|
+
}>> {
|
|
56
|
+
return request('login/init', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: JSON.stringify({}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function pollLogin(deviceCode: string): Promise<ApiResponse<{
|
|
63
|
+
access_token: string;
|
|
64
|
+
token_type: string;
|
|
65
|
+
user: { id: string; email: string; name: string };
|
|
66
|
+
message?: string;
|
|
67
|
+
}>> {
|
|
68
|
+
return request('login/poll', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getMe(): Promise<ApiResponse<{
|
|
75
|
+
user: { id: string; email: string; name: string; avatar_url?: string };
|
|
76
|
+
projects_count: number;
|
|
77
|
+
}>> {
|
|
78
|
+
return request('me');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getProjects(): Promise<ApiResponse<{
|
|
82
|
+
projects: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
status: string;
|
|
87
|
+
progress: number;
|
|
88
|
+
role: string;
|
|
89
|
+
}>;
|
|
90
|
+
}>> {
|
|
91
|
+
return request('projects');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface Task {
|
|
95
|
+
id: string;
|
|
96
|
+
number: number;
|
|
97
|
+
title: string;
|
|
98
|
+
state: string;
|
|
99
|
+
priority: string;
|
|
100
|
+
story_points?: number;
|
|
101
|
+
due_date?: string;
|
|
102
|
+
project: string;
|
|
103
|
+
project_id: string;
|
|
104
|
+
refs: {
|
|
105
|
+
wip: string;
|
|
106
|
+
fixes: string;
|
|
107
|
+
closes: string;
|
|
108
|
+
refs: string;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getTasks(options: {
|
|
113
|
+
project?: string;
|
|
114
|
+
state?: string;
|
|
115
|
+
limit?: number;
|
|
116
|
+
} = {}): Promise<ApiResponse<{ tasks: Task[] }>> {
|
|
117
|
+
const params = new URLSearchParams();
|
|
118
|
+
if (options.project) params.set('project', options.project);
|
|
119
|
+
if (options.state) params.set('state', options.state);
|
|
120
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
121
|
+
|
|
122
|
+
const query = params.toString();
|
|
123
|
+
return request(`tasks${query ? `?${query}` : ''}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function getTask(taskId: string): Promise<ApiResponse<{ task: Task }>> {
|
|
127
|
+
return request(`tasks/${taskId}`);
|
|
128
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
interface ConfigSchema {
|
|
4
|
+
api_url: string;
|
|
5
|
+
token: string | null;
|
|
6
|
+
default_project: string | null;
|
|
7
|
+
user: {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const config = new Conf<ConfigSchema>({
|
|
15
|
+
projectName: 'workdynamite',
|
|
16
|
+
defaults: {
|
|
17
|
+
api_url: 'https://tovgfzknrookxgmvibsk.supabase.co/functions/v1/cli-api',
|
|
18
|
+
token: null,
|
|
19
|
+
default_project: null,
|
|
20
|
+
user: null,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function getConfig(): ConfigSchema {
|
|
25
|
+
return {
|
|
26
|
+
api_url: config.get('api_url'),
|
|
27
|
+
token: config.get('token'),
|
|
28
|
+
default_project: config.get('default_project'),
|
|
29
|
+
user: config.get('user'),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setToken(token: string): void {
|
|
34
|
+
config.set('token', token);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function setUser(user: ConfigSchema['user']): void {
|
|
38
|
+
config.set('user', user);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setDefaultProject(projectId: string | null): void {
|
|
42
|
+
config.set('default_project', projectId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clearConfig(): void {
|
|
46
|
+
config.set('token', null);
|
|
47
|
+
config.set('user', null);
|
|
48
|
+
config.set('default_project', null);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isAuthenticated(): boolean {
|
|
52
|
+
return !!config.get('token');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getToken(): string | null {
|
|
56
|
+
return config.get('token');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getApiUrl(): string {
|
|
60
|
+
return config.get('api_url');
|
|
61
|
+
}
|