@slope-dev/cli 0.1.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,49 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { formatBriefing } from '@slope-dev/core';
4
+ import { loadConfig } from '../config.js';
5
+ import { loadScorecards } from '../loader.js';
6
+ export function briefingCommand(args) {
7
+ const config = loadConfig();
8
+ const cwd = process.cwd();
9
+ const scorecards = loadScorecards(config, cwd);
10
+ // Load common-issues
11
+ let commonIssues;
12
+ try {
13
+ commonIssues = JSON.parse(readFileSync(join(cwd, config.commonIssuesPath), 'utf8'));
14
+ }
15
+ catch {
16
+ commonIssues = { recurring_patterns: [] };
17
+ }
18
+ // Load last session
19
+ let lastSession;
20
+ try {
21
+ const sessionsData = JSON.parse(readFileSync(join(cwd, config.sessionsPath), 'utf8'));
22
+ const sessions = sessionsData.sessions;
23
+ if (sessions && sessions.length > 0) {
24
+ lastSession = sessions[sessions.length - 1];
25
+ }
26
+ }
27
+ catch { /* skip */ }
28
+ // Parse args
29
+ const categories = [];
30
+ const keywords = [];
31
+ let includeTraining = true;
32
+ for (const arg of args) {
33
+ if (arg.startsWith('--categories=')) {
34
+ categories.push(...arg.slice('--categories='.length).split(',').map(s => s.trim()).filter(Boolean));
35
+ }
36
+ else if (arg.startsWith('--keywords=')) {
37
+ keywords.push(...arg.slice('--keywords='.length).split(',').map(s => s.trim()).filter(Boolean));
38
+ }
39
+ else if (arg === '--no-training') {
40
+ includeTraining = false;
41
+ }
42
+ }
43
+ const filter = (categories.length > 0 || keywords.length > 0)
44
+ ? { categories: categories.length > 0 ? categories : undefined, keywords: keywords.length > 0 ? keywords : undefined }
45
+ : undefined;
46
+ const output = formatBriefing({ scorecards, commonIssues, lastSession, filter, includeTraining });
47
+ console.log('');
48
+ console.log(output);
49
+ }
@@ -0,0 +1,50 @@
1
+ import { computeHandicapCard } from '@slope-dev/core';
2
+ import { loadConfig } from '../config.js';
3
+ import { loadScorecards } from '../loader.js';
4
+ export function cardCommand() {
5
+ const config = loadConfig();
6
+ const scorecards = loadScorecards(config);
7
+ if (scorecards.length === 0) {
8
+ console.log('\nNo scorecards found. Run `slope init` to create an example.\n');
9
+ process.exit(0);
10
+ }
11
+ const card = computeHandicapCard(scorecards);
12
+ const pad = (s, w) => String(s).padStart(w);
13
+ const pct = (n) => n.toFixed(1) + '%';
14
+ const minSprint = config.minSprint;
15
+ console.log(`\nSLOPE Handicap Card (${scorecards.length} scorecard${scorecards.length === 1 ? '' : 's'}, Sprint ${minSprint}+)`);
16
+ console.log('\u2501'.repeat(47));
17
+ console.log('');
18
+ console.log(`${'Stat'.padEnd(20)}${'Last 5'.padStart(9)}${'Last 10'.padStart(10)}${'All-time'.padStart(10)}`);
19
+ console.log('\u2500'.repeat(49));
20
+ const rows = [
21
+ ['Handicap', w => `+${w.handicap.toFixed(1)}`],
22
+ ['Fairways', w => pct(w.fairway_pct)],
23
+ ['GIR', w => pct(w.gir_pct)],
24
+ ['Avg putts/hole', w => w.avg_putts.toFixed(1)],
25
+ ['Penalties/round', w => w.penalties_per_round.toFixed(1)],
26
+ ['Mulligans', w => String(w.mulligans)],
27
+ ['Gimmes', w => String(w.gimmes)],
28
+ ];
29
+ for (const [label, fn] of rows) {
30
+ console.log(`${label.padEnd(20)}${pad(fn(card.last_5), 9)}${pad(fn(card.last_10), 10)}${pad(fn(card.all_time), 10)}`);
31
+ }
32
+ // Miss pattern summary
33
+ const allMiss = card.all_time.miss_pattern;
34
+ const totalMisses = allMiss.long + allMiss.short + allMiss.left + allMiss.right;
35
+ console.log('');
36
+ if (totalMisses === 0) {
37
+ console.log('Miss Pattern: No misses recorded.');
38
+ }
39
+ else {
40
+ const dirs = ['long', 'short', 'left', 'right']
41
+ .filter(d => allMiss[d] > 0)
42
+ .map(d => `${d}: ${allMiss[d]}`)
43
+ .join(', ');
44
+ console.log(`Miss Pattern: ${dirs} (${totalMisses} total)`);
45
+ }
46
+ if (scorecards.length < 5) {
47
+ console.log(`\nNote: Only ${scorecards.length} scorecard${scorecards.length === 1 ? '' : 's'} \u2014 windows fill at Sprint ${minSprint + 5 - scorecards.length}.`);
48
+ }
49
+ console.log('');
50
+ }
@@ -0,0 +1,69 @@
1
+ import { classifyShot } from '@slope-dev/core';
2
+ export function classifyCommand(args) {
3
+ let scope;
4
+ let modified;
5
+ let tests;
6
+ let reverts;
7
+ let hazards;
8
+ for (const arg of args) {
9
+ if (arg.startsWith('--scope='))
10
+ scope = arg.slice('--scope='.length);
11
+ else if (arg.startsWith('--modified='))
12
+ modified = arg.slice('--modified='.length);
13
+ else if (arg.startsWith('--tests='))
14
+ tests = arg.slice('--tests='.length);
15
+ else if (arg.startsWith('--reverts='))
16
+ reverts = arg.slice('--reverts='.length);
17
+ else if (arg.startsWith('--hazards='))
18
+ hazards = arg.slice('--hazards='.length);
19
+ }
20
+ if (!scope || !modified || !tests || reverts == null) {
21
+ console.error('\nUsage: slope classify --scope="a.ts,b.ts" --modified="a.ts,b.ts" --tests=pass|fail|partial --reverts=N [--hazards=N]\n');
22
+ process.exit(1);
23
+ return; // unreachable, helps TS narrow
24
+ }
25
+ const validTests = ['pass', 'fail', 'partial'];
26
+ if (!validTests.includes(tests)) {
27
+ console.error(`\nInvalid --tests value "${tests}". Must be one of: ${validTests.join(', ')}\n`);
28
+ process.exit(1);
29
+ return;
30
+ }
31
+ const scopePaths = scope.split(',').map((s) => s.trim()).filter(Boolean);
32
+ const modifiedFiles = modified.split(',').map((s) => s.trim()).filter(Boolean);
33
+ const revertCount = parseInt(reverts, 10) || 0;
34
+ const hazardCount = parseInt(hazards ?? '0', 10) || 0;
35
+ // Build test_results
36
+ const testResults = [];
37
+ if (tests === 'pass') {
38
+ testResults.push({ suite: 'all', passed: true, first_run: true });
39
+ }
40
+ else if (tests === 'fail') {
41
+ testResults.push({ suite: 'all', passed: false, first_run: true });
42
+ }
43
+ else {
44
+ testResults.push({ suite: 'unit', passed: true, first_run: true });
45
+ testResults.push({ suite: 'integration', passed: false, first_run: true });
46
+ }
47
+ // Build hazards_encountered
48
+ const hazardsEncountered = Array.from({ length: hazardCount }, (_, i) => ({
49
+ type: 'rough',
50
+ description: `Hazard ${i + 1}`,
51
+ }));
52
+ const trace = {
53
+ planned_scope_paths: scopePaths,
54
+ modified_files: modifiedFiles,
55
+ test_results: testResults,
56
+ reverts: revertCount,
57
+ elapsed_minutes: 0,
58
+ hazards_encountered: hazardsEncountered,
59
+ };
60
+ const result = classifyShot(trace);
61
+ console.log('');
62
+ console.log('SHOT CLASSIFICATION');
63
+ console.log('\u2550'.repeat(40));
64
+ console.log(` Result: ${result.result}`);
65
+ console.log(` Miss direction: ${result.miss_direction ?? 'none'}`);
66
+ console.log(` Confidence: ${Math.round(result.confidence * 100)}%`);
67
+ console.log(` Reasoning: ${result.reasoning}`);
68
+ console.log('');
69
+ }
@@ -0,0 +1,130 @@
1
+ import { writeFileSync, mkdirSync, existsSync, cpSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createConfig } from '../config.js';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const EXAMPLE_SCORECARD = {
7
+ sprint_number: 1,
8
+ theme: 'Example Sprint',
9
+ par: 3,
10
+ slope: 0,
11
+ score: 3,
12
+ score_label: 'par',
13
+ date: new Date().toISOString().split('T')[0],
14
+ shots: [
15
+ {
16
+ ticket_key: 'S1-1',
17
+ title: 'Set up project',
18
+ club: 'short_iron',
19
+ result: 'green',
20
+ hazards: [],
21
+ notes: 'Clean setup',
22
+ },
23
+ {
24
+ ticket_key: 'S1-2',
25
+ title: 'Add core feature',
26
+ club: 'short_iron',
27
+ result: 'in_the_hole',
28
+ hazards: [],
29
+ },
30
+ {
31
+ ticket_key: 'S1-3',
32
+ title: 'Write tests',
33
+ club: 'wedge',
34
+ result: 'green',
35
+ hazards: [{ type: 'rough', description: 'Flaky test environment' }],
36
+ },
37
+ ],
38
+ conditions: [],
39
+ special_plays: [],
40
+ stats: {
41
+ fairways_hit: 3,
42
+ fairways_total: 3,
43
+ greens_in_regulation: 3,
44
+ greens_total: 3,
45
+ putts: 0,
46
+ penalties: 0,
47
+ hazards_hit: 1,
48
+ miss_directions: { long: 0, short: 0, left: 0, right: 0 },
49
+ },
50
+ yardage_book_updates: [],
51
+ bunker_locations: [],
52
+ course_management_notes: ['This is an example scorecard — replace with your own sprint data.'],
53
+ };
54
+ const EXAMPLE_COMMON_ISSUES = {
55
+ recurring_patterns: [
56
+ {
57
+ id: 1,
58
+ title: 'Example pattern',
59
+ category: 'general',
60
+ sprints_hit: [1],
61
+ gotcha_refs: [],
62
+ description: 'This is an example recurring pattern. Replace with your own.',
63
+ prevention: 'Add your prevention strategy here.',
64
+ },
65
+ ],
66
+ };
67
+ export function initCommand(args) {
68
+ const cwd = process.cwd();
69
+ const claudeCode = args.includes('--claude-code');
70
+ // Create .slope directory and config
71
+ const configPath = createConfig(cwd);
72
+ console.log(` Created ${configPath}`);
73
+ // Create scorecard directory
74
+ const scorecardDir = join(cwd, 'docs', 'retros');
75
+ if (!existsSync(scorecardDir)) {
76
+ mkdirSync(scorecardDir, { recursive: true });
77
+ console.log(` Created ${scorecardDir}/`);
78
+ }
79
+ // Write example scorecard
80
+ const examplePath = join(scorecardDir, 'sprint-1.json');
81
+ if (!existsSync(examplePath)) {
82
+ writeFileSync(examplePath, JSON.stringify(EXAMPLE_SCORECARD, null, 2) + '\n');
83
+ console.log(` Created ${examplePath}`);
84
+ }
85
+ // Write example common-issues.json
86
+ const commonIssuesPath = join(cwd, '.slope', 'common-issues.json');
87
+ if (!existsSync(commonIssuesPath)) {
88
+ writeFileSync(commonIssuesPath, JSON.stringify(EXAMPLE_COMMON_ISSUES, null, 2) + '\n');
89
+ console.log(` Created ${commonIssuesPath}`);
90
+ }
91
+ // Write example sessions.json
92
+ const sessionsPath = join(cwd, '.slope', 'sessions.json');
93
+ if (!existsSync(sessionsPath)) {
94
+ writeFileSync(sessionsPath, JSON.stringify({ sessions: [] }, null, 2) + '\n');
95
+ console.log(` Created ${sessionsPath}`);
96
+ }
97
+ // Claude Code templates
98
+ if (claudeCode) {
99
+ const templatesRoot = join(__dirname, '..', '..', '..', '..', 'templates', 'claude-code');
100
+ const rulesDir = join(cwd, '.claude', 'rules');
101
+ const hooksDir = join(cwd, '.claude', 'hooks');
102
+ mkdirSync(rulesDir, { recursive: true });
103
+ mkdirSync(hooksDir, { recursive: true });
104
+ // Copy rule templates
105
+ const ruleFiles = ['sprint-checklist.md', 'commit-discipline.md', 'review-loop.md'];
106
+ for (const file of ruleFiles) {
107
+ const src = join(templatesRoot, 'rules', file);
108
+ const dest = join(rulesDir, file);
109
+ if (existsSync(src) && !existsSync(dest)) {
110
+ cpSync(src, dest);
111
+ console.log(` Created ${dest}`);
112
+ }
113
+ }
114
+ // Copy hook templates
115
+ const hookFiles = ['pre-merge-check.sh'];
116
+ for (const file of hookFiles) {
117
+ const src = join(templatesRoot, 'hooks', file);
118
+ const dest = join(hooksDir, file);
119
+ if (existsSync(src) && !existsSync(dest)) {
120
+ cpSync(src, dest);
121
+ console.log(` Created ${dest}`);
122
+ }
123
+ }
124
+ console.log('\n Claude Code templates installed to .claude/rules/ and .claude/hooks/');
125
+ }
126
+ console.log('\nSLOPE initialized. Try:');
127
+ console.log(' slope card');
128
+ console.log(' slope validate');
129
+ console.log('');
130
+ }
@@ -0,0 +1,53 @@
1
+ import { recommendClub, computeHandicapCard, computeDispersion, generateTrainingPlan, hazardBriefing, formatAdvisorReport, } from '@slope-dev/core';
2
+ import { loadConfig } from '../config.js';
3
+ import { loadScorecards } from '../loader.js';
4
+ export function planCommand(args) {
5
+ let complexity;
6
+ const slopeFactors = [];
7
+ const areas = [];
8
+ for (const arg of args) {
9
+ if (arg.startsWith('--complexity=')) {
10
+ complexity = arg.slice('--complexity='.length);
11
+ }
12
+ else if (arg.startsWith('--slope-factors=')) {
13
+ slopeFactors.push(...arg.slice('--slope-factors='.length).split(',').map(s => s.trim()).filter(Boolean));
14
+ }
15
+ else if (arg.startsWith('--areas=')) {
16
+ areas.push(...arg.slice('--areas='.length).split(',').map(s => s.trim()).filter(Boolean));
17
+ }
18
+ }
19
+ if (!complexity) {
20
+ console.error('\nUsage: slope plan --complexity=<trivial|small|medium|large> [--slope-factors=a,b] [--areas=x,y]\n');
21
+ process.exit(1);
22
+ return;
23
+ }
24
+ const validComplexities = ['trivial', 'small', 'medium', 'large'];
25
+ if (!validComplexities.includes(complexity)) {
26
+ console.error(`\nInvalid complexity "${complexity}". Must be one of: ${validComplexities.join(', ')}\n`);
27
+ process.exit(1);
28
+ return;
29
+ }
30
+ const config = loadConfig();
31
+ const scorecards = loadScorecards(config);
32
+ // Club recommendation
33
+ const clubRec = recommendClub({
34
+ ticketComplexity: complexity,
35
+ scorecards,
36
+ slopeFactors,
37
+ });
38
+ // Training plan
39
+ let trainingPlan = [];
40
+ if (scorecards.length > 0) {
41
+ const handicap = computeHandicapCard(scorecards);
42
+ const dispersion = computeDispersion(scorecards);
43
+ trainingPlan = generateTrainingPlan({ handicap, dispersion, recentScorecards: scorecards });
44
+ }
45
+ // Hazard warnings
46
+ let hazardWarnings = [];
47
+ if (areas.length > 0 && scorecards.length > 0) {
48
+ hazardWarnings = hazardBriefing({ areas, scorecards });
49
+ }
50
+ const report = formatAdvisorReport({ clubRecommendation: clubRec, trainingPlan, hazardWarnings });
51
+ console.log('');
52
+ console.log(report);
53
+ }
@@ -0,0 +1,46 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { formatSprintReview } from '@slope-dev/core';
4
+ import { loadConfig } from '../config.js';
5
+ export function reviewCommand(path, mode) {
6
+ const config = loadConfig();
7
+ const cwd = process.cwd();
8
+ if (!path) {
9
+ // Default to latest scorecard
10
+ const dir = join(cwd, config.scorecardDir);
11
+ const patternParts = config.scorecardPattern.split('*');
12
+ const prefix = patternParts[0] ?? '';
13
+ const suffix = patternParts[1] ?? '';
14
+ const regex = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)${suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
15
+ try {
16
+ const files = readdirSync(dir)
17
+ .filter((f) => {
18
+ const m = f.match(regex);
19
+ return m && parseInt(m[1], 10) >= config.minSprint;
20
+ })
21
+ .sort();
22
+ if (files.length === 0) {
23
+ console.log('\nNo scorecards found.\n');
24
+ process.exit(1);
25
+ }
26
+ path = join(dir, files[files.length - 1]);
27
+ }
28
+ catch {
29
+ console.log('\nScorecard directory not found.\n');
30
+ process.exit(1);
31
+ }
32
+ }
33
+ let raw;
34
+ try {
35
+ raw = JSON.parse(readFileSync(path, 'utf8'));
36
+ }
37
+ catch {
38
+ console.error(`\nFailed to parse ${path}\n`);
39
+ process.exit(1);
40
+ }
41
+ const card = { ...raw, sprint_number: raw.sprint_number ?? raw.sprint };
42
+ const reviewMode = mode === 'plain' ? 'plain' : 'technical';
43
+ const review = formatSprintReview(card, undefined, undefined, reviewMode);
44
+ console.log('');
45
+ console.log(review);
46
+ }
@@ -0,0 +1,73 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { validateScorecard } from '@slope-dev/core';
4
+ import { loadConfig } from '../config.js';
5
+ export function validateCommand(path) {
6
+ const config = loadConfig();
7
+ const files = [];
8
+ if (path) {
9
+ files.push(path);
10
+ }
11
+ else {
12
+ // Validate all scorecards matching config
13
+ const dir = join(process.cwd(), config.scorecardDir);
14
+ const patternParts = config.scorecardPattern.split('*');
15
+ const prefix = patternParts[0] ?? '';
16
+ const suffix = patternParts[1] ?? '';
17
+ const regex = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)${suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
18
+ try {
19
+ const dirFiles = readdirSync(dir)
20
+ .filter((f) => {
21
+ const m = f.match(regex);
22
+ return m && parseInt(m[1], 10) >= config.minSprint;
23
+ })
24
+ .sort();
25
+ for (const f of dirFiles) {
26
+ files.push(join(dir, f));
27
+ }
28
+ }
29
+ catch {
30
+ // Directory doesn't exist
31
+ }
32
+ }
33
+ if (files.length === 0) {
34
+ console.log('\nNo scorecards found to validate.\n');
35
+ process.exit(0);
36
+ }
37
+ let allValid = true;
38
+ for (const file of files) {
39
+ let raw;
40
+ try {
41
+ raw = JSON.parse(readFileSync(file, 'utf8'));
42
+ }
43
+ catch {
44
+ console.log(`\n\u2717 ${file}: Failed to parse JSON`);
45
+ allValid = false;
46
+ continue;
47
+ }
48
+ const card = { ...raw, sprint_number: raw.sprint_number ?? raw.sprint };
49
+ const result = validateScorecard(card);
50
+ const sprintLabel = card.sprint_number ? `Sprint ${card.sprint_number}` : file;
51
+ if (result.valid && result.warnings.length === 0) {
52
+ console.log(`\u2713 ${sprintLabel}: Valid (no errors, no warnings)`);
53
+ }
54
+ else if (result.valid) {
55
+ console.log(`\u2713 ${sprintLabel}: Valid (${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'})`);
56
+ for (const w of result.warnings) {
57
+ console.log(` \u26A0 [${w.code}] ${w.message}`);
58
+ }
59
+ }
60
+ else {
61
+ console.log(`\u2717 ${sprintLabel}: INVALID (${result.errors.length} error${result.errors.length === 1 ? '' : 's'}, ${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'})`);
62
+ for (const e of result.errors) {
63
+ console.log(` \u2717 [${e.code}] ${e.message}${e.field ? ` (${e.field})` : ''}`);
64
+ }
65
+ for (const w of result.warnings) {
66
+ console.log(` \u26A0 [${w.code}] ${w.message}`);
67
+ }
68
+ allValid = false;
69
+ }
70
+ }
71
+ console.log('');
72
+ process.exit(allValid ? 0 : 1);
73
+ }
package/dist/config.js ADDED
@@ -0,0 +1,36 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const DEFAULT_CONFIG = {
4
+ scorecardDir: 'docs/retros',
5
+ scorecardPattern: 'sprint-*.json',
6
+ minSprint: 1,
7
+ commonIssuesPath: '.slope/common-issues.json',
8
+ sessionsPath: '.slope/sessions.json',
9
+ };
10
+ const CONFIG_DIR = '.slope';
11
+ const CONFIG_FILE = 'config.json';
12
+ export function loadConfig(cwd = process.cwd()) {
13
+ const configPath = join(cwd, CONFIG_DIR, CONFIG_FILE);
14
+ if (!existsSync(configPath)) {
15
+ return { ...DEFAULT_CONFIG };
16
+ }
17
+ try {
18
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
19
+ return { ...DEFAULT_CONFIG, ...raw };
20
+ }
21
+ catch {
22
+ return { ...DEFAULT_CONFIG };
23
+ }
24
+ }
25
+ export function createConfig(cwd = process.cwd()) {
26
+ const dir = join(cwd, CONFIG_DIR);
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true });
29
+ }
30
+ const configPath = join(dir, CONFIG_FILE);
31
+ writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n');
32
+ return configPath;
33
+ }
34
+ export function resolveConfigPath(config, relativePath, cwd = process.cwd()) {
35
+ return join(cwd, relativePath);
36
+ }
package/dist/index.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SLOPE CLI — Sprint Lifecycle & Operational Performance Engine
4
+ *
5
+ * Usage:
6
+ * slope init Initialize .slope/ directory
7
+ * slope card Display handicap card
8
+ * slope validate [path] Validate scorecard(s)
9
+ * slope review [path] [--plain] Format sprint review
10
+ * slope briefing [options] Pre-round briefing
11
+ * slope plan --complexity=<level> Pre-shot advisor
12
+ * slope classify --scope=... ... Classify a shot
13
+ */
14
+ import { initCommand } from './commands/init.js';
15
+ import { cardCommand } from './commands/card.js';
16
+ import { validateCommand } from './commands/validate.js';
17
+ import { reviewCommand } from './commands/review.js';
18
+ import { briefingCommand } from './commands/briefing.js';
19
+ import { planCommand } from './commands/plan.js';
20
+ import { classifyCommand } from './commands/classify.js';
21
+ const subcommand = process.argv[2];
22
+ switch (subcommand) {
23
+ case 'init':
24
+ initCommand(process.argv.slice(3));
25
+ break;
26
+ case 'card':
27
+ cardCommand();
28
+ break;
29
+ case 'validate':
30
+ validateCommand(process.argv[3]);
31
+ break;
32
+ case 'review': {
33
+ const reviewArgs = process.argv.slice(3);
34
+ const plainFlag = reviewArgs.includes('--plain');
35
+ const path = reviewArgs.find((a) => !a.startsWith('--'));
36
+ reviewCommand(path, plainFlag ? 'plain' : undefined);
37
+ break;
38
+ }
39
+ case 'briefing':
40
+ briefingCommand(process.argv.slice(3));
41
+ break;
42
+ case 'plan':
43
+ planCommand(process.argv.slice(3));
44
+ break;
45
+ case 'classify':
46
+ classifyCommand(process.argv.slice(3));
47
+ break;
48
+ default:
49
+ console.log(`
50
+ SLOPE CLI — Sprint Lifecycle & Operational Performance Engine
51
+
52
+ Usage:
53
+ slope init [--claude-code] Initialize .slope/ directory
54
+ slope card Show handicap card
55
+ slope validate [path] Validate scorecard(s)
56
+ slope review [path] [--plain] Format sprint review markdown
57
+ slope briefing [options] Pre-round briefing
58
+ slope plan --complexity=<level> Pre-shot advisor (club + training + hazards)
59
+ slope classify --scope=... ... Classify a shot from execution trace
60
+
61
+ Examples:
62
+ slope init Create .slope/ with config + example scorecard
63
+ slope init --claude-code Also install Claude Code rules + hooks
64
+ slope card Show handicap across all scorecards
65
+ slope validate docs/retros/sprint-1.json Validate a specific scorecard
66
+ slope validate Validate all scorecards
67
+ slope review Review the latest scorecard
68
+ slope review --plain Non-technical sprint review
69
+ slope briefing Full briefing (top 10 recent gotchas)
70
+ slope briefing --categories=testing Filter by category
71
+ slope briefing --keywords=migration Filter by keyword
72
+ slope plan --complexity=medium Club recommendation for medium ticket
73
+ slope plan --complexity=large --areas=db Include hazard warnings for db area
74
+ slope classify --scope="a.ts" --modified="a.ts" --tests=pass --reverts=0
75
+ `);
76
+ process.exit(subcommand ? 1 : 0);
77
+ }
package/dist/loader.js ADDED
@@ -0,0 +1,44 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Load SLOPE scorecards from the configured directory.
5
+ * Filters by minSprint and normalizes sprint_number.
6
+ */
7
+ export function loadScorecards(config, cwd = process.cwd()) {
8
+ const dir = join(cwd, config.scorecardDir);
9
+ if (!existsSync(dir)) {
10
+ return [];
11
+ }
12
+ // Build regex from pattern (e.g. "sprint-*.json" → /^sprint-(\d+)\.json$/)
13
+ const patternParts = config.scorecardPattern.split('*');
14
+ const prefix = patternParts[0] ?? '';
15
+ const suffix = patternParts[1] ?? '';
16
+ const regex = new RegExp(`^${escapeRegex(prefix)}(\\d+)${escapeRegex(suffix)}$`);
17
+ const files = readdirSync(dir)
18
+ .filter((f) => regex.test(f))
19
+ .sort();
20
+ const scorecards = [];
21
+ for (const file of files) {
22
+ const match = file.match(regex);
23
+ if (!match)
24
+ continue;
25
+ const num = parseInt(match[1], 10);
26
+ if (num < config.minSprint)
27
+ continue;
28
+ try {
29
+ const raw = JSON.parse(readFileSync(join(dir, file), 'utf8'));
30
+ const card = {
31
+ ...raw,
32
+ sprint_number: raw.sprint_number ?? raw.sprint,
33
+ };
34
+ scorecards.push(card);
35
+ }
36
+ catch {
37
+ console.error(` Warning: Could not parse ${file}, skipping`);
38
+ }
39
+ }
40
+ return scorecards;
41
+ }
42
+ function escapeRegex(s) {
43
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@slope-dev/cli",
3
+ "version": "0.1.0",
4
+ "description": "SLOPE CLI — Sprint Lifecycle & Operational Performance Engine",
5
+ "type": "module",
6
+ "bin": {
7
+ "slope": "./dist/index.js"
8
+ },
9
+ "dependencies": {
10
+ "@slope-dev/core": "0.1.0"
11
+ },
12
+ "devDependencies": {
13
+ "@types/node": "^25.3.0",
14
+ "typescript": "^5.7.0"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "license": "MIT",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/srbryers/slope.git",
26
+ "directory": "packages/cli"
27
+ },
28
+ "keywords": [
29
+ "slope",
30
+ "sprint",
31
+ "scoring",
32
+ "cli",
33
+ "retro"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc"
40
+ }
41
+ }