@we-scrum/cli 1.0.4 → 1.0.6
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/dist/cli.js +173 -47
- package/package.json +13 -13
- package/src/cli.ts +116 -51
- package/src/hbs.d.ts +4 -0
- package/src/helpers/auth-helper.ts +1 -1
- package/src/helpers/index.ts +2 -0
- package/src/helpers/project-helper.ts +96 -0
- package/src/helpers/settings-helper.ts +0 -1
- package/src/helpers/template.helper.ts +25 -0
- package/src/helpers/we-scrum.helper.ts +119 -102
- package/templates/develop-story-skill.hbs +46 -0
- package/templates/get-next-task.hbs +21 -0
- package/templates/review-analysis-skill.hbs +61 -0
- package/tsconfig.json +2 -1
- package/src/mcp-server/index.ts +0 -1
- package/src/mcp-server/mcp-server.ts +0 -82
- package/src/mcp-server/tools/complete-task.ts +0 -6
- package/src/mcp-server/tools/get-next-task.ts +0 -150
- package/src/mcp-server/tools/get-unit-test-guidelines.ts +0 -15
- package/src/mcp-server/tools/index.ts +0 -4
- package/src/mcp-server/tools/start-task.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@we-scrum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Cli tool for we-scrum application",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,22 +20,23 @@
|
|
|
20
20
|
"noExternal": [
|
|
21
21
|
"@we-scrum/*",
|
|
22
22
|
"@my-devkit/*"
|
|
23
|
-
]
|
|
23
|
+
],
|
|
24
|
+
"loader": {
|
|
25
|
+
".hbs": "text"
|
|
26
|
+
}
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
|
-
"@inquirer/prompts": "8.
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"google-auth-library": "^10.6.1",
|
|
29
|
+
"@inquirer/prompts": "^8.4.2",
|
|
30
|
+
"commander": "^14.0.3",
|
|
31
|
+
"firebase": "^12.12.1",
|
|
32
|
+
"google-auth-library": "^10.6.2",
|
|
33
|
+
"handlebars": "^4.7.9",
|
|
32
34
|
"open": "^11.0.0"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
|
-
"@types/node": "22.
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"typescript": "5.9.3",
|
|
37
|
+
"@types/node": "^22.19.17",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
39
40
|
"@my-devkit/cli": "2.1.0",
|
|
40
41
|
"@my-devkit/core": "1.0.0",
|
|
41
42
|
"@we-scrum/commands": "1.0.0",
|
|
@@ -45,7 +46,6 @@
|
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"start": "node dist/cli.js",
|
|
48
|
-
"inspect": "mcp-inspector node dist/cli.js mcp",
|
|
49
49
|
"build": "tsup",
|
|
50
50
|
"watch": "tsup --watch",
|
|
51
51
|
"deploy": "pnpm build && pnpm publish --access public"
|
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { select } from '@inquirer/prompts';
|
|
3
|
-
import { Logger } from '@my-devkit/core';
|
|
3
|
+
import { assert, Logger } from '@my-devkit/core';
|
|
4
4
|
import { UserProjectModel } from '@we-scrum/models';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { version } from '../package.json';
|
|
7
7
|
|
|
8
|
-
import { AuthHelper, FirebaseHelper,
|
|
9
|
-
import { startMcpServer } from './mcp-server';
|
|
8
|
+
import { AuthHelper, FirebaseHelper, ProjectHelper, WeScrumHelper } from './helpers';
|
|
10
9
|
|
|
11
10
|
Logger.registerLogger(new Logger.ConsoleImplementation());
|
|
12
11
|
|
|
@@ -14,59 +13,77 @@ const program = new Command();
|
|
|
14
13
|
|
|
15
14
|
program.name('we-scrum').description('CLI for we-scrum application').version(version);
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
async function requireAuth(): Promise<void> {
|
|
17
|
+
const initialized = await AuthHelper.initializeFromStoredCredentials();
|
|
18
|
+
assert(initialized, 'Not authenticated. Please run "we-scrum login" first.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function requireProject(): Promise<void> {
|
|
22
|
+
const root = ProjectHelper.requireRoot();
|
|
23
|
+
await ProjectHelper.init(root);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runCommand<T = void>(fn: (options: T) => Promise<string | void>) {
|
|
27
|
+
return async (options: T) => {
|
|
21
28
|
try {
|
|
22
|
-
await
|
|
29
|
+
const output = await fn(options);
|
|
30
|
+
if (output) process.stdout.write(output + '\n');
|
|
23
31
|
process.exit(0);
|
|
24
32
|
} catch (error) {
|
|
25
|
-
|
|
33
|
+
if (error?.name === 'ExitPromptError') process.exit(0);
|
|
34
|
+
Logger.error(error?.message ?? 'Command failed');
|
|
26
35
|
process.exit(1);
|
|
27
36
|
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runProjectCommand<T = void>(fn: (options: T) => Promise<string | void>) {
|
|
41
|
+
return runCommand<T>(async (options) => {
|
|
42
|
+
await requireProject();
|
|
43
|
+
await requireAuth();
|
|
44
|
+
return fn(options);
|
|
28
45
|
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('login')
|
|
50
|
+
.description('Authenticate with your Google account')
|
|
51
|
+
.action(
|
|
52
|
+
runCommand(async () => {
|
|
53
|
+
await AuthHelper.loginWithGoogle();
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
29
56
|
|
|
30
57
|
program
|
|
31
58
|
.command('logout')
|
|
32
59
|
.description('Log out and remove stored credentials')
|
|
33
|
-
.action(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
.action(
|
|
61
|
+
runCommand(async () => {
|
|
62
|
+
AuthHelper.logout();
|
|
63
|
+
Logger.info('Logged out successfully.');
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
38
66
|
|
|
39
67
|
program
|
|
40
68
|
.command('use-project')
|
|
41
|
-
.description('Select the active project')
|
|
69
|
+
.description('Select the active project and initialize we-scrum.json')
|
|
42
70
|
.option('--projectId <id>', 'Set the active project directly without the interactive prompt')
|
|
43
|
-
.action(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!initialized) {
|
|
47
|
-
Logger.error('Not authenticated. Please run "we-scrum login" first.');
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
71
|
+
.action(
|
|
72
|
+
runCommand<{ projectId?: string }>(async (options) => {
|
|
73
|
+
await requireAuth();
|
|
50
74
|
|
|
51
75
|
const projects = await FirebaseHelper.getCollection<UserProjectModel>(`users/${AuthHelper.userId}/projects`);
|
|
52
|
-
|
|
53
|
-
Logger.error('No projects found for your account.');
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
76
|
+
assert(projects.length > 0, 'No projects found for your account. Please create a project in the we-scrum web app first.');
|
|
56
77
|
|
|
57
78
|
let selectedProjectId: string;
|
|
58
79
|
|
|
59
80
|
if (options.projectId) {
|
|
60
81
|
const match = projects.find((p) => p.projectId === options.projectId);
|
|
61
|
-
|
|
62
|
-
Logger.error(`Project "${options.projectId}" not found in your account.`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
82
|
+
assert(!!match, `Project "${options.projectId}" not found in your account.`);
|
|
65
83
|
selectedProjectId = match.projectId;
|
|
66
84
|
} else {
|
|
67
85
|
selectedProjectId = await select({
|
|
68
86
|
message: 'Select a project',
|
|
69
|
-
default: SettingsHelper.get().selectedProjectId,
|
|
70
87
|
choices: projects.map((p) => ({
|
|
71
88
|
name: p.name,
|
|
72
89
|
value: p.projectId,
|
|
@@ -75,27 +92,75 @@ program
|
|
|
75
92
|
});
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
Logger.error(error?.message ?? 'Failed to select project');
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
95
|
+
const projectRoot = await ProjectHelper.confirmAndCreate(selectedProjectId);
|
|
96
|
+
await ProjectHelper.init(projectRoot);
|
|
97
|
+
|
|
98
|
+
Logger.info(`Project configured: ${projects.find((p) => p.projectId === selectedProjectId)?.name}`);
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
89
101
|
|
|
90
102
|
program
|
|
91
|
-
.command('
|
|
92
|
-
.description('
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
.command('start-task')
|
|
104
|
+
.description('Mark a specific task as in progress')
|
|
105
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
106
|
+
.requiredOption('--taskId <id>', 'Unique identifier of the change to act on')
|
|
107
|
+
.action(
|
|
108
|
+
runProjectCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
|
|
109
|
+
await WeScrumHelper.startTask(options.identificationNumber, options.taskId);
|
|
110
|
+
Logger.info('Task started successfully. You can now make changes to the codebase.');
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('complete-task')
|
|
116
|
+
.description('Mark the current in-progress task as done')
|
|
117
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
118
|
+
.requiredOption('--taskId <id>', 'Unique identifier of the change to act on')
|
|
119
|
+
.action(
|
|
120
|
+
runProjectCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
|
|
121
|
+
await WeScrumHelper.completeTask(options.identificationNumber, options.taskId);
|
|
122
|
+
Logger.info('Task completed successfully.');
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
program
|
|
127
|
+
.command('get-story-description')
|
|
128
|
+
.description('Get the description of a story')
|
|
129
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
130
|
+
.action(
|
|
131
|
+
runProjectCommand<{ identificationNumber: string }>(async (options) => {
|
|
132
|
+
return WeScrumHelper.getStoryDescription(options.identificationNumber);
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
program
|
|
137
|
+
.command('get-story-analysis')
|
|
138
|
+
.description('Get the analysis DSL for a story')
|
|
139
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
140
|
+
.action(
|
|
141
|
+
runProjectCommand<{ identificationNumber: string }>(async (options) => {
|
|
142
|
+
return WeScrumHelper.getStoryAnalysis(options.identificationNumber);
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
program
|
|
147
|
+
.command('get-next-task')
|
|
148
|
+
.description('Get the next pending task for a story')
|
|
149
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
150
|
+
.action(
|
|
151
|
+
runProjectCommand<{ identificationNumber: string }>(async (options) => {
|
|
152
|
+
return WeScrumHelper.getNextTask(options.identificationNumber);
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command('synchronize-development-policies')
|
|
158
|
+
.description('Sync development guidelines from we-scrum into the project')
|
|
159
|
+
.action(
|
|
160
|
+
runProjectCommand(async () => {
|
|
161
|
+
const count = await WeScrumHelper.synchronizeDevelopmentPolicies();
|
|
162
|
+
Logger.info(`Synchronized ${count} development ${count === 1 ? 'policy' : 'policies'}.`);
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
100
165
|
|
|
101
166
|
program.parse(process.argv);
|
package/src/hbs.d.ts
ADDED
package/src/helpers/index.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { assert, Logger } from '@my-devkit/core';
|
|
2
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { version as cliVersion } from '../../package.json';
|
|
6
|
+
import { renderTemplate } from './template.helper';
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = 'we-scrum.json';
|
|
9
|
+
|
|
10
|
+
export interface WeScrumProjectConfig {
|
|
11
|
+
projectId: string;
|
|
12
|
+
version: string;
|
|
13
|
+
developmentPoliciesPath: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ProjectHelper {
|
|
17
|
+
public static findRoot(from = process.cwd()): string | null {
|
|
18
|
+
let dir = resolve(from);
|
|
19
|
+
while (true) {
|
|
20
|
+
if (existsSync(join(dir, CONFIG_FILE))) return dir;
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir) return null;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static requireRoot(): string {
|
|
28
|
+
const root = this.findRoot();
|
|
29
|
+
assert(!!root, `we-scrum.json not found. Please run "we-scrum use-project" first.`);
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static read(root: string): WeScrumProjectConfig {
|
|
34
|
+
const content = readFileSync(join(root, CONFIG_FILE), 'utf8');
|
|
35
|
+
return JSON.parse(content) as WeScrumProjectConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public static write(root: string, partial: Partial<WeScrumProjectConfig>): void {
|
|
39
|
+
const existing = existsSync(join(root, CONFIG_FILE)) ? this.read(root) : ({} as WeScrumProjectConfig);
|
|
40
|
+
writeFileSync(join(root, CONFIG_FILE), JSON.stringify({ ...existing, ...partial }, null, 2) + '\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static async init(root: string): Promise<void> {
|
|
44
|
+
const config = this.read(root);
|
|
45
|
+
assert(!!config.projectId, 'No project selected. Please run "we-scrum use-project" first.');
|
|
46
|
+
if (config.version === cliVersion) return;
|
|
47
|
+
|
|
48
|
+
this.installSkills(root);
|
|
49
|
+
this.write(root, { version: cliVersion });
|
|
50
|
+
Logger.info(`we-scrum initialized (v${cliVersion}).`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static async confirmAndCreate(projectId: string): Promise<string> {
|
|
54
|
+
const defaultRoot = process.cwd();
|
|
55
|
+
|
|
56
|
+
const useDefault = await confirm({
|
|
57
|
+
message: `Use "${defaultRoot}" as the project root?`,
|
|
58
|
+
default: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let root: string;
|
|
62
|
+
if (useDefault) {
|
|
63
|
+
root = defaultRoot;
|
|
64
|
+
} else {
|
|
65
|
+
root = await input({ message: 'Enter the project root directory:' });
|
|
66
|
+
root = resolve(root);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
assert(existsSync(root), `Directory "${root}" does not exist.`);
|
|
70
|
+
|
|
71
|
+
const defaultPoliciesPath = '.claude/development-policies';
|
|
72
|
+
const confirmPolicies = await confirm({
|
|
73
|
+
message: `Store development policies in "${defaultPoliciesPath}"?`,
|
|
74
|
+
default: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const developmentPoliciesPath = confirmPolicies
|
|
78
|
+
? defaultPoliciesPath
|
|
79
|
+
: await input({ message: 'Enter the development policies path (relative to project root):' });
|
|
80
|
+
|
|
81
|
+
this.write(root, { projectId, version: '0.0.0', developmentPoliciesPath });
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static installSkills(root: string): void {
|
|
86
|
+
if (!existsSync(join(root, '.claude'))) return;
|
|
87
|
+
for (const [name, template] of [
|
|
88
|
+
['develop-story', 'develop-story-skill'],
|
|
89
|
+
['review-analysis', 'review-analysis-skill'],
|
|
90
|
+
] as const) {
|
|
91
|
+
const skillDir = join(root, '.claude', 'skills', name);
|
|
92
|
+
mkdirSync(skillDir, { recursive: true });
|
|
93
|
+
writeFileSync(join(skillDir, 'SKILL.md'), renderTemplate(template));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Handlebars from 'handlebars';
|
|
2
|
+
import developStorySkill from '../../templates/develop-story-skill.hbs';
|
|
3
|
+
import getNextTask from '../../templates/get-next-task.hbs';
|
|
4
|
+
import reviewAnalysisSkill from '../../templates/review-analysis-skill.hbs';
|
|
5
|
+
|
|
6
|
+
const sources = {
|
|
7
|
+
'develop-story-skill': developStorySkill,
|
|
8
|
+
'get-next-task': getNextTask,
|
|
9
|
+
'review-analysis-skill': reviewAnalysisSkill,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type TemplateName = keyof typeof sources;
|
|
13
|
+
|
|
14
|
+
const compiled = new Map<TemplateName, Handlebars.TemplateDelegate>();
|
|
15
|
+
|
|
16
|
+
function getTemplate(name: TemplateName): Handlebars.TemplateDelegate {
|
|
17
|
+
if (!compiled.has(name)) {
|
|
18
|
+
compiled.set(name, Handlebars.compile(sources[name]));
|
|
19
|
+
}
|
|
20
|
+
return compiled.get(name)!;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderTemplate(name: TemplateName, context?: Record<string, unknown>): string {
|
|
24
|
+
return getTemplate(name)(context ?? {}).trim();
|
|
25
|
+
}
|