@slope-dev/cli 0.3.2 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sam Bryers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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 } 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,116 @@ 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
+ }
67
135
  export function initCommand(args) {
68
136
  const cwd = process.cwd();
69
- const claudeCode = args.includes('--claude-code');
70
- // Create .slope directory and config
137
+ const provider = detectProvider(args);
71
138
  const configPath = createConfig(cwd);
72
139
  console.log(` Created ${configPath}`);
73
- // Create scorecard directory
74
140
  const scorecardDir = join(cwd, 'docs', 'retros');
75
141
  if (!existsSync(scorecardDir)) {
76
142
  mkdirSync(scorecardDir, { recursive: true });
77
143
  console.log(` Created ${scorecardDir}/`);
78
144
  }
79
- // Write example scorecard
80
145
  const examplePath = join(scorecardDir, 'sprint-1.json');
81
146
  if (!existsSync(examplePath)) {
82
147
  writeFileSync(examplePath, JSON.stringify(EXAMPLE_SCORECARD, null, 2) + '\n');
83
148
  console.log(` Created ${examplePath}`);
84
149
  }
85
- // Write example common-issues.json
86
150
  const commonIssuesPath = join(cwd, '.slope', 'common-issues.json');
87
151
  if (!existsSync(commonIssuesPath)) {
88
152
  writeFileSync(commonIssuesPath, JSON.stringify(EXAMPLE_COMMON_ISSUES, null, 2) + '\n');
89
153
  console.log(` Created ${commonIssuesPath}`);
90
154
  }
91
- // Write example sessions.json
92
155
  const sessionsPath = join(cwd, '.slope', 'sessions.json');
93
156
  if (!existsSync(sessionsPath)) {
94
157
  writeFileSync(sessionsPath, JSON.stringify({ sessions: [] }, null, 2) + '\n');
95
158
  console.log(` Created ${sessionsPath}`);
96
159
  }
97
- // Write empty claims.json
98
160
  const claimsPath = join(cwd, '.slope', 'claims.json');
99
161
  if (!existsSync(claimsPath)) {
100
162
  writeFileSync(claimsPath, JSON.stringify({ claims: [] }, null, 2) + '\n');
101
163
  console.log(` Created ${claimsPath}`);
102
164
  }
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/');
165
+ switch (provider) {
166
+ case 'claude-code':
167
+ installClaudeCodeTemplates(cwd);
168
+ break;
169
+ case 'cursor':
170
+ installCursorTemplates(cwd);
171
+ break;
172
+ case 'generic':
173
+ installGenericTemplates(cwd);
174
+ break;
175
+ default:
176
+ break;
131
177
  }
132
178
  console.log('\nSLOPE initialized. Try:');
133
179
  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,13 @@ 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
94
110
  slope init --claude-code Also install Claude Code rules + hooks
95
111
  slope card Show handicap across all scorecards
96
112
  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,18 +1,13 @@
1
1
  {
2
2
  "name": "@slope-dev/cli",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "SLOPE CLI — Sprint Lifecycle & Operational Performance Engine",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "slope": "./dist/index.js"
8
8
  },
9
- "scripts": {
10
- "build": "tsc",
11
- "test": "vitest run",
12
- "typecheck": "tsc --noEmit"
13
- },
14
9
  "dependencies": {
15
- "@slope-dev/core": "^0.3.2"
10
+ "@slope-dev/core": "0.3.3"
16
11
  },
17
12
  "devDependencies": {
18
13
  "@types/node": "^25.3.0",
@@ -40,5 +35,10 @@
40
35
  ],
41
36
  "engines": {
42
37
  "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "test": "vitest run",
42
+ "typecheck": "tsc --noEmit"
43
43
  }
44
- }
44
+ }