@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@we-scrum/cli",
3
- "version": "1.0.4",
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.3.0",
27
- "zod": "4.3.6",
28
- "@modelcontextprotocol/sdk": "1.27.1",
29
- "commander": "14.0.3",
30
- "firebase": "12.10.0",
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.18.6",
36
- "@modelcontextprotocol/inspector": "0.21.1",
37
- "tsup": "8.5.1",
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, SettingsHelper } from './helpers';
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
- program
18
- .command('login')
19
- .description('Authenticate with your Google account')
20
- .action(async () => {
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 AuthHelper.loginWithGoogle();
29
+ const output = await fn(options);
30
+ if (output) process.stdout.write(output + '\n');
23
31
  process.exit(0);
24
32
  } catch (error) {
25
- Logger.error(error?.message ?? 'Login failed');
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
- AuthHelper.logout();
35
- Logger.info('Logged out successfully.');
36
- process.exit(0);
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(async (options: { projectId?: string }) => {
44
- try {
45
- const initialized = await AuthHelper.initializeFromStoredCredentials();
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
- if (projects.length === 0) {
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
- if (!match) {
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
- SettingsHelper.set({ selectedProjectId });
79
- Logger.info(`Active project set to: ${projects.find((p) => p.projectId === selectedProjectId)?.name}`);
80
- process.exit(0);
81
- } catch (error) {
82
- if (error?.name === 'ExitPromptError') {
83
- process.exit(0);
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('mcp')
92
- .description('Start the we-scrum MCP server (stdio transport)')
93
- .option('--projectId <id>', 'Override the active project for this session')
94
- .action(async (options: { projectId?: string }) => {
95
- if (options.projectId) {
96
- SettingsHelper.set({ selectedProjectId: options.projectId });
97
- }
98
- await startMcpServer();
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
@@ -0,0 +1,4 @@
1
+ declare module '*.hbs' {
2
+ const content: string;
3
+ export default content;
4
+ }
@@ -70,7 +70,7 @@ export class AuthHelper {
70
70
 
71
71
  // Exchange the Authorization Code for tokens using the PKCE Verifier
72
72
  const { tokens } = await oAuth2Client.getToken({
73
- code: code,
73
+ code,
74
74
  codeVerifier: verifier,
75
75
  });
76
76
 
@@ -1,4 +1,6 @@
1
1
  export * from './auth-helper';
2
2
  export * from './firebase-helper';
3
+ export * from './project-helper';
3
4
  export * from './settings-helper';
5
+ export * from './template.helper';
4
6
  export * from './we-scrum.helper';
@@ -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
+ }
@@ -8,7 +8,6 @@ const SETTINGS_FILE = pathJoin(homedir(), '.we-scrum.json');
8
8
 
9
9
  export interface WeScrumSettings {
10
10
  credentials?: Credentials;
11
- selectedProjectId?: string;
12
11
  }
13
12
 
14
13
  export class SettingsHelper {
@@ -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
+ }