@slope-dev/cli 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,128 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { writeFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { buildScorecard, computeSlope } from '@slope-dev/core';
5
+ import { loadConfig } from '../config.js';
6
+ function parseArgs(args) {
7
+ const result = {};
8
+ for (const arg of args) {
9
+ const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
10
+ if (match)
11
+ result[match[1]] = match[2];
12
+ }
13
+ return result;
14
+ }
15
+ function git(cmd) {
16
+ try {
17
+ return execSync(`git ${cmd}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
18
+ }
19
+ catch {
20
+ return '';
21
+ }
22
+ }
23
+ function getCommits(since, branch) {
24
+ const ref = branch ?? 'HEAD';
25
+ const sinceArg = since ? `--since="${since}"` : '';
26
+ const log = git(`log ${ref} ${sinceArg} --format="%H|||%s" --no-merges`);
27
+ if (!log)
28
+ return [];
29
+ return log.split('\n').filter(Boolean).map((line) => {
30
+ const [hash, subject] = line.split('|||');
31
+ const stat = git(`diff-tree --no-commit-id --name-only -r ${hash}`);
32
+ return {
33
+ hash,
34
+ subject: subject ?? '',
35
+ filesChanged: stat ? stat.split('\n').filter(Boolean).length : 0,
36
+ };
37
+ });
38
+ }
39
+ function inferTicketKey(subject, index, sprintNumber) {
40
+ const ticketMatch = subject.match(/\b[A-Z]+-\d+\b/) ?? subject.match(/\bS\d+-\d+\b/i);
41
+ if (ticketMatch)
42
+ return ticketMatch[0];
43
+ return `S${sprintNumber}-${index + 1}`;
44
+ }
45
+ function inferClub(filesChanged) {
46
+ if (filesChanged >= 10)
47
+ return 'driver';
48
+ if (filesChanged >= 5)
49
+ return 'long_iron';
50
+ if (filesChanged >= 2)
51
+ return 'short_iron';
52
+ return 'wedge';
53
+ }
54
+ function inferSlopeFactors(commits) {
55
+ const factors = [];
56
+ const allSubjects = commits.map((c) => c.subject.toLowerCase()).join(' ');
57
+ if (allSubjects.includes('migration') || allSubjects.includes('schema'))
58
+ factors.push('schema_migration');
59
+ if (allSubjects.includes('deploy') || allSubjects.includes('ci') || allSubjects.includes('docker'))
60
+ factors.push('deployment');
61
+ if (commits.some((c) => c.filesChanged > 15))
62
+ factors.push('large_scope');
63
+ if (allSubjects.includes('refactor'))
64
+ factors.push('refactor');
65
+ if (new Set(commits.flatMap((c) => c.subject.match(/packages\/\w+/g) ?? [])).size > 1)
66
+ factors.push('cross_package');
67
+ return factors;
68
+ }
69
+ export function autoCardCommand(args) {
70
+ const opts = parseArgs(args);
71
+ const config = loadConfig();
72
+ const sprintNumber = parseInt(opts.sprint ?? '', 10);
73
+ if (!sprintNumber) {
74
+ console.error('\nUsage: slope auto-card --sprint=<N> [--since=<date>] [--branch=<ref>] [--theme=<text>] [--dry-run]\n');
75
+ console.error(' --sprint Sprint number (required)');
76
+ console.error(' --since Git log start date, e.g. "2026-02-20" (optional)');
77
+ console.error(' --branch Git ref to scan (default: HEAD)');
78
+ console.error(' --theme Sprint theme (default: auto-generated)');
79
+ console.error(' --dry-run Print scorecard JSON without writing to disk\n');
80
+ process.exit(1);
81
+ }
82
+ const commits = getCommits(opts.since, opts.branch);
83
+ if (commits.length === 0) {
84
+ console.error('\nNo commits found. Try specifying --since or --branch.\n');
85
+ process.exit(1);
86
+ }
87
+ const shots = commits.map((commit, i) => ({
88
+ ticket_key: inferTicketKey(commit.subject, i, sprintNumber),
89
+ title: commit.subject,
90
+ club: inferClub(commit.filesChanged),
91
+ result: 'in_the_hole',
92
+ hazards: [],
93
+ notes: `${commit.filesChanged} files changed`,
94
+ }));
95
+ const slopeFactors = inferSlopeFactors(commits);
96
+ const theme = opts.theme ?? `Sprint ${sprintNumber}`;
97
+ const card = buildScorecard({
98
+ sprint_number: sprintNumber,
99
+ theme,
100
+ par: shots.length <= 2 ? 3 : shots.length <= 4 ? 4 : 5,
101
+ slope: computeSlope(slopeFactors),
102
+ date: new Date().toISOString().split('T')[0],
103
+ shots,
104
+ });
105
+ const output = {
106
+ ...card,
107
+ slope_factors: slopeFactors,
108
+ _auto_generated: true,
109
+ _commits: commits.length,
110
+ _note: 'Auto-generated by `slope auto-card`. Review shot results and adjust before filing.',
111
+ };
112
+ const json = JSON.stringify(output, null, 2);
113
+ if (args.includes('--dry-run')) {
114
+ console.log('\n' + json + '\n');
115
+ return;
116
+ }
117
+ const cwd = process.cwd();
118
+ const outPath = join(cwd, config.scorecardDir, `sprint-${sprintNumber}.json`);
119
+ if (existsSync(outPath)) {
120
+ console.error(`\n ${outPath} already exists. Use --dry-run to preview, then write manually.\n`);
121
+ process.exit(1);
122
+ }
123
+ writeFileSync(outPath, json + '\n');
124
+ console.log(`\n Written to ${outPath}`);
125
+ console.log(` ${commits.length} commits → ${shots.length} shots`);
126
+ console.log(` Par: ${card.par} | Slope: ${card.slope} (${slopeFactors.join(', ') || 'none'})`);
127
+ console.log(`\n ⚠ Review shot results before filing — all default to in_the_hole.\n`);
128
+ }
@@ -1,4 +1,4 @@
1
- import { writeFileSync, mkdirSync, existsSync, cpSync } from 'node:fs';
1
+ import { writeFileSync, mkdirSync, existsSync, cpSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { createConfig } from '../config.js';
@@ -64,70 +64,140 @@ const EXAMPLE_COMMON_ISSUES = {
64
64
  },
65
65
  ],
66
66
  };
67
+ function detectProvider(args) {
68
+ if (args.includes('--claude-code'))
69
+ return 'claude-code';
70
+ if (args.includes('--cursor'))
71
+ return 'cursor';
72
+ if (args.includes('--generic'))
73
+ return 'generic';
74
+ return null;
75
+ }
76
+ function getTemplatesRoot() {
77
+ return join(__dirname, '..', '..', '..', '..', 'templates');
78
+ }
79
+ function installClaudeCodeTemplates(cwd) {
80
+ const templatesRoot = join(getTemplatesRoot(), 'claude-code');
81
+ const rulesDir = join(cwd, '.claude', 'rules');
82
+ const hooksDir = join(cwd, '.claude', 'hooks');
83
+ mkdirSync(rulesDir, { recursive: true });
84
+ mkdirSync(hooksDir, { recursive: true });
85
+ const ruleFiles = ['sprint-checklist.md', 'commit-discipline.md', 'review-loop.md'];
86
+ for (const file of ruleFiles) {
87
+ const src = join(templatesRoot, 'rules', file);
88
+ const dest = join(rulesDir, file);
89
+ if (existsSync(src) && !existsSync(dest)) {
90
+ cpSync(src, dest);
91
+ console.log(` Created ${dest}`);
92
+ }
93
+ }
94
+ const hookFiles = ['pre-merge-check.sh'];
95
+ for (const file of hookFiles) {
96
+ const src = join(templatesRoot, 'hooks', file);
97
+ const dest = join(hooksDir, file);
98
+ if (existsSync(src) && !existsSync(dest)) {
99
+ cpSync(src, dest);
100
+ console.log(` Created ${dest}`);
101
+ }
102
+ }
103
+ console.log('\n Claude Code templates installed to .claude/rules/ and .claude/hooks/');
104
+ }
105
+ function installCursorTemplates(cwd) {
106
+ const templatesRoot = join(getTemplatesRoot(), 'cursor', 'rules');
107
+ const rulesDir = join(cwd, '.cursor', 'rules');
108
+ mkdirSync(rulesDir, { recursive: true });
109
+ try {
110
+ const files = readdirSync(templatesRoot).filter((f) => f.endsWith('.mdc'));
111
+ for (const file of files) {
112
+ const src = join(templatesRoot, file);
113
+ const dest = join(rulesDir, file);
114
+ if (!existsSync(dest)) {
115
+ cpSync(src, dest);
116
+ console.log(` Created ${dest}`);
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ console.error(' Warning: Could not find Cursor rule templates');
122
+ }
123
+ console.log('\n Cursor rules installed to .cursor/rules/');
124
+ }
125
+ function installGenericTemplates(cwd) {
126
+ const templatesRoot = join(getTemplatesRoot(), 'generic');
127
+ const dest = join(cwd, 'SLOPE-CHECKLIST.md');
128
+ const src = join(templatesRoot, 'SLOPE-CHECKLIST.md');
129
+ if (existsSync(src) && !existsSync(dest)) {
130
+ cpSync(src, dest);
131
+ console.log(` Created ${dest}`);
132
+ }
133
+ console.log('\n Generic SLOPE checklist installed.');
134
+ }
135
+ const SLOPE_MCP_ENTRY = {
136
+ command: 'npx',
137
+ args: ['@slope-dev/mcp-tools'],
138
+ };
139
+ function installCursorMcpConfig(cwd) {
140
+ const mcpPath = join(cwd, '.cursor', 'mcp.json');
141
+ let config = {};
142
+ if (existsSync(mcpPath)) {
143
+ try {
144
+ const raw = readFileSync(mcpPath, 'utf8');
145
+ config = JSON.parse(raw);
146
+ }
147
+ catch {
148
+ config = {};
149
+ }
150
+ }
151
+ if (!config.mcpServers)
152
+ config.mcpServers = {};
153
+ config.mcpServers.slope = SLOPE_MCP_ENTRY;
154
+ mkdirSync(join(cwd, '.cursor'), { recursive: true });
155
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n');
156
+ console.log(` Created/updated ${mcpPath} (slope MCP server)`);
157
+ }
67
158
  export function initCommand(args) {
68
159
  const cwd = process.cwd();
69
- const claudeCode = args.includes('--claude-code');
70
- // Create .slope directory and config
160
+ const provider = detectProvider(args);
71
161
  const configPath = createConfig(cwd);
72
162
  console.log(` Created ${configPath}`);
73
- // Create scorecard directory
74
163
  const scorecardDir = join(cwd, 'docs', 'retros');
75
164
  if (!existsSync(scorecardDir)) {
76
165
  mkdirSync(scorecardDir, { recursive: true });
77
166
  console.log(` Created ${scorecardDir}/`);
78
167
  }
79
- // Write example scorecard
80
168
  const examplePath = join(scorecardDir, 'sprint-1.json');
81
169
  if (!existsSync(examplePath)) {
82
170
  writeFileSync(examplePath, JSON.stringify(EXAMPLE_SCORECARD, null, 2) + '\n');
83
171
  console.log(` Created ${examplePath}`);
84
172
  }
85
- // Write example common-issues.json
86
173
  const commonIssuesPath = join(cwd, '.slope', 'common-issues.json');
87
174
  if (!existsSync(commonIssuesPath)) {
88
175
  writeFileSync(commonIssuesPath, JSON.stringify(EXAMPLE_COMMON_ISSUES, null, 2) + '\n');
89
176
  console.log(` Created ${commonIssuesPath}`);
90
177
  }
91
- // Write example sessions.json
92
178
  const sessionsPath = join(cwd, '.slope', 'sessions.json');
93
179
  if (!existsSync(sessionsPath)) {
94
180
  writeFileSync(sessionsPath, JSON.stringify({ sessions: [] }, null, 2) + '\n');
95
181
  console.log(` Created ${sessionsPath}`);
96
182
  }
97
- // Write empty claims.json
98
183
  const claimsPath = join(cwd, '.slope', 'claims.json');
99
184
  if (!existsSync(claimsPath)) {
100
185
  writeFileSync(claimsPath, JSON.stringify({ claims: [] }, null, 2) + '\n');
101
186
  console.log(` Created ${claimsPath}`);
102
187
  }
103
- // Claude Code templates
104
- if (claudeCode) {
105
- const templatesRoot = join(__dirname, '..', '..', '..', '..', 'templates', 'claude-code');
106
- const rulesDir = join(cwd, '.claude', 'rules');
107
- const hooksDir = join(cwd, '.claude', 'hooks');
108
- mkdirSync(rulesDir, { recursive: true });
109
- mkdirSync(hooksDir, { recursive: true });
110
- // Copy rule templates
111
- const ruleFiles = ['sprint-checklist.md', 'commit-discipline.md', 'review-loop.md'];
112
- for (const file of ruleFiles) {
113
- const src = join(templatesRoot, 'rules', file);
114
- const dest = join(rulesDir, file);
115
- if (existsSync(src) && !existsSync(dest)) {
116
- cpSync(src, dest);
117
- console.log(` Created ${dest}`);
118
- }
119
- }
120
- // Copy hook templates
121
- const hookFiles = ['pre-merge-check.sh'];
122
- for (const file of hookFiles) {
123
- const src = join(templatesRoot, 'hooks', file);
124
- const dest = join(hooksDir, file);
125
- if (existsSync(src) && !existsSync(dest)) {
126
- cpSync(src, dest);
127
- console.log(` Created ${dest}`);
128
- }
129
- }
130
- console.log('\n Claude Code templates installed to .claude/rules/ and .claude/hooks/');
188
+ switch (provider) {
189
+ case 'claude-code':
190
+ installClaudeCodeTemplates(cwd);
191
+ break;
192
+ case 'cursor':
193
+ installCursorTemplates(cwd);
194
+ installCursorMcpConfig(cwd);
195
+ break;
196
+ case 'generic':
197
+ installGenericTemplates(cwd);
198
+ break;
199
+ default:
200
+ break;
131
201
  }
132
202
  console.log('\nSLOPE initialized. Try:');
133
203
  console.log(' slope card');
@@ -0,0 +1,27 @@
1
+ import { loadConfig } from '../config.js';
2
+ import { resolveCurrentSprint, detectLatestSprint } from '../loader.js';
3
+ export function nextCommand() {
4
+ const config = loadConfig();
5
+ const cwd = process.cwd();
6
+ const latest = detectLatestSprint(config, cwd);
7
+ const next = resolveCurrentSprint(config, cwd);
8
+ console.log('');
9
+ if (latest === 0) {
10
+ console.log(' No scorecards found. Next sprint: S1');
11
+ }
12
+ else {
13
+ console.log(` Latest scorecard: S${latest}`);
14
+ console.log(` Next sprint: S${next}`);
15
+ }
16
+ if (config.currentSprint) {
17
+ console.log(` (set explicitly in .slope/config.json)`);
18
+ }
19
+ else {
20
+ console.log(` (auto-detected from scorecards)`);
21
+ }
22
+ console.log('');
23
+ console.log(' Quick start:');
24
+ console.log(` slope briefing --sprint=${next}`);
25
+ console.log(` slope auto-card --sprint=${next} --since="$(date -d yesterday +%Y-%m-%d)"`);
26
+ console.log('');
27
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,9 @@ import { classifyCommand } from './commands/classify.js';
24
24
  import { claimCommand } from './commands/claim.js';
25
25
  import { releaseCommand } from './commands/release.js';
26
26
  import { statusCommand } from './commands/status.js';
27
+ import { tournamentCommand } from './commands/tournament.js';
28
+ import { autoCardCommand } from './commands/auto-card.js';
29
+ import { nextCommand } from './commands/next.js';
27
30
  const subcommand = process.argv[2];
28
31
  switch (subcommand) {
29
32
  case 'init':
@@ -72,12 +75,21 @@ switch (subcommand) {
72
75
  process.exit(1);
73
76
  });
74
77
  break;
78
+ case 'tournament':
79
+ tournamentCommand(process.argv.slice(3));
80
+ break;
81
+ case 'auto-card':
82
+ autoCardCommand(process.argv.slice(3));
83
+ break;
84
+ case 'next':
85
+ nextCommand();
86
+ break;
75
87
  default:
76
88
  console.log(`
77
89
  SLOPE CLI — Sprint Lifecycle & Operational Performance Engine
78
90
 
79
91
  Usage:
80
- slope init [--claude-code] Initialize .slope/ directory
92
+ slope init [--claude-code|--cursor|--generic] Initialize .slope/ directory
81
93
  slope card Show handicap card
82
94
  slope validate [path] Validate scorecard(s)
83
95
  slope review [path] [--plain] Format sprint review markdown
@@ -88,9 +100,14 @@ Usage:
88
100
  slope release --id=<id> Release a claim by ID
89
101
  slope release --target=<t> [--player=<p>] Release a claim by target
90
102
  slope status [--sprint=N] Show sprint course status + conflicts
103
+ slope tournament --id=<id> --sprints=N..M Build tournament review from sprints
104
+ slope auto-card --sprint=<N> [options] Generate scorecard from git commits
105
+ slope next Show next sprint number (auto-detect)
91
106
 
92
107
  Examples:
93
108
  slope init Create .slope/ with config + example scorecard
109
+ slope init --cursor Also install Cursor IDE rules
110
+ slope init --cursor Also add SLOPE MCP server to .cursor/mcp.json
94
111
  slope init --claude-code Also install Claude Code rules + hooks
95
112
  slope card Show handicap across all scorecards
96
113
  slope validate docs/retros/sprint-1.json Validate a specific scorecard
package/dist/loader.js CHANGED
@@ -39,6 +39,25 @@ export function loadScorecards(config, cwd = process.cwd()) {
39
39
  }
40
40
  return scorecards;
41
41
  }
42
+ /**
43
+ * Detect the latest sprint number from existing scorecards.
44
+ * Returns 0 if no scorecards are found.
45
+ */
46
+ export function detectLatestSprint(config, cwd = process.cwd()) {
47
+ const cards = loadScorecards(config, cwd);
48
+ if (cards.length === 0)
49
+ return 0;
50
+ return Math.max(...cards.map((c) => c.sprint_number));
51
+ }
52
+ /**
53
+ * Resolve the current sprint number: explicit config > auto-detect + 1.
54
+ */
55
+ export function resolveCurrentSprint(config, cwd = process.cwd()) {
56
+ if (config.currentSprint)
57
+ return config.currentSprint;
58
+ const latest = detectLatestSprint(config, cwd);
59
+ return latest + 1;
60
+ }
42
61
  function escapeRegex(s) {
43
62
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slope-dev/cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "SLOPE CLI — Sprint Lifecycle & Operational Performance Engine",
5
5
  "type": "module",
6
6
  "bin": {