@slope-dev/cli 0.1.0 → 0.2.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/dist/commands/briefing.js +29 -2
- package/dist/commands/claim.js +99 -0
- package/dist/commands/init.js +6 -0
- package/dist/commands/release.js +64 -0
- package/dist/commands/status.js +64 -0
- package/dist/config.js +2 -0
- package/dist/index.js +40 -3
- package/dist/registries/api-registry.js +40 -0
- package/dist/registries/file-registry.js +53 -0
- package/dist/registries/index.js +18 -0
- package/package.json +10 -7
- package/LICENSE +0 -21
|
@@ -3,7 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { formatBriefing } from '@slope-dev/core';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { loadScorecards } from '../loader.js';
|
|
6
|
-
|
|
6
|
+
import { createRegistry } from '../registries/index.js';
|
|
7
|
+
export async function briefingCommand(args) {
|
|
7
8
|
const config = loadConfig();
|
|
8
9
|
const cwd = process.cwd();
|
|
9
10
|
const scorecards = loadScorecards(config, cwd);
|
|
@@ -29,6 +30,7 @@ export function briefingCommand(args) {
|
|
|
29
30
|
const categories = [];
|
|
30
31
|
const keywords = [];
|
|
31
32
|
let includeTraining = true;
|
|
33
|
+
let sprintFlag;
|
|
32
34
|
for (const arg of args) {
|
|
33
35
|
if (arg.startsWith('--categories=')) {
|
|
34
36
|
categories.push(...arg.slice('--categories='.length).split(',').map(s => s.trim()).filter(Boolean));
|
|
@@ -36,14 +38,39 @@ export function briefingCommand(args) {
|
|
|
36
38
|
else if (arg.startsWith('--keywords=')) {
|
|
37
39
|
keywords.push(...arg.slice('--keywords='.length).split(',').map(s => s.trim()).filter(Boolean));
|
|
38
40
|
}
|
|
41
|
+
else if (arg.startsWith('--sprint=')) {
|
|
42
|
+
sprintFlag = parseInt(arg.slice('--sprint='.length), 10);
|
|
43
|
+
}
|
|
39
44
|
else if (arg === '--no-training') {
|
|
40
45
|
includeTraining = false;
|
|
41
46
|
}
|
|
42
47
|
}
|
|
48
|
+
// Resolve sprint number
|
|
49
|
+
let sprintNumber;
|
|
50
|
+
if (sprintFlag) {
|
|
51
|
+
sprintNumber = sprintFlag;
|
|
52
|
+
}
|
|
53
|
+
else if (config.currentSprint) {
|
|
54
|
+
sprintNumber = config.currentSprint;
|
|
55
|
+
}
|
|
56
|
+
else if (scorecards.length > 0) {
|
|
57
|
+
const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
|
|
58
|
+
sprintNumber = maxSprint + 1;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
sprintNumber = 1;
|
|
62
|
+
}
|
|
63
|
+
// Load claims
|
|
64
|
+
let claims = [];
|
|
65
|
+
try {
|
|
66
|
+
const registry = createRegistry(config, cwd);
|
|
67
|
+
claims = await registry.list(sprintNumber);
|
|
68
|
+
}
|
|
69
|
+
catch { /* skip — claims are optional */ }
|
|
43
70
|
const filter = (categories.length > 0 || keywords.length > 0)
|
|
44
71
|
? { categories: categories.length > 0 ? categories : undefined, keywords: keywords.length > 0 ? keywords : undefined }
|
|
45
72
|
: undefined;
|
|
46
|
-
const output = formatBriefing({ scorecards, commonIssues, lastSession, filter, includeTraining });
|
|
73
|
+
const output = formatBriefing({ scorecards, commonIssues, lastSession, filter, includeTraining, claims });
|
|
47
74
|
console.log('');
|
|
48
75
|
console.log(output);
|
|
49
76
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { checkConflicts } from '@slope-dev/core';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { loadScorecards } from '../loader.js';
|
|
4
|
+
import { createRegistry } from '../registries/index.js';
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const arg of args) {
|
|
8
|
+
const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
|
|
9
|
+
if (match)
|
|
10
|
+
result[match[1]] = match[2];
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
function resolveSprint(flags, cwd) {
|
|
15
|
+
if (flags.sprint)
|
|
16
|
+
return parseInt(flags.sprint, 10);
|
|
17
|
+
const config = loadConfig(cwd);
|
|
18
|
+
if (config.currentSprint)
|
|
19
|
+
return config.currentSprint;
|
|
20
|
+
const scorecards = loadScorecards(config, cwd);
|
|
21
|
+
if (scorecards.length === 0)
|
|
22
|
+
return 1;
|
|
23
|
+
const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
|
|
24
|
+
return maxSprint + 1;
|
|
25
|
+
}
|
|
26
|
+
export async function claimCommand(args) {
|
|
27
|
+
const flags = parseArgs(args);
|
|
28
|
+
const force = args.includes('--force');
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const config = loadConfig(cwd);
|
|
31
|
+
const registry = createRegistry(config, cwd);
|
|
32
|
+
const target = flags.target;
|
|
33
|
+
if (!target) {
|
|
34
|
+
console.error('Error: --target is required');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const scope = flags.scope || 'ticket';
|
|
38
|
+
const player = flags.player || process.env.USER || 'unknown';
|
|
39
|
+
const sprintNumber = resolveSprint(flags, cwd);
|
|
40
|
+
// Preflight conflict check: build a temporary claim and test against existing claims
|
|
41
|
+
const existingClaims = await registry.list(sprintNumber);
|
|
42
|
+
const tempClaim = {
|
|
43
|
+
id: '__pending__',
|
|
44
|
+
sprint_number: sprintNumber,
|
|
45
|
+
player,
|
|
46
|
+
target,
|
|
47
|
+
scope,
|
|
48
|
+
claimed_at: new Date().toISOString(),
|
|
49
|
+
...(flags.notes ? { notes: flags.notes } : {}),
|
|
50
|
+
};
|
|
51
|
+
const conflicts = checkConflicts([...existingClaims, tempClaim]);
|
|
52
|
+
const overlaps = conflicts.filter(c => c.severity === 'overlap');
|
|
53
|
+
const adjacents = conflicts.filter(c => c.severity === 'adjacent');
|
|
54
|
+
// Block on overlaps unless --force
|
|
55
|
+
if (overlaps.length > 0 && !force) {
|
|
56
|
+
console.error(`\nClaim blocked — overlap conflict(s) detected:`);
|
|
57
|
+
for (const c of overlaps) {
|
|
58
|
+
console.error(` [!!] ${c.reason}`);
|
|
59
|
+
}
|
|
60
|
+
console.error(`\nUse --force to override.`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// Register the claim
|
|
64
|
+
const claim = await registry.claim({
|
|
65
|
+
sprint_number: sprintNumber,
|
|
66
|
+
player,
|
|
67
|
+
target,
|
|
68
|
+
scope,
|
|
69
|
+
...(flags.notes ? { notes: flags.notes } : {}),
|
|
70
|
+
});
|
|
71
|
+
// Forced overlap warning
|
|
72
|
+
if (overlaps.length > 0 && force) {
|
|
73
|
+
console.log(`\nClaim registered (forced override):`);
|
|
74
|
+
console.log(` Warning: ${overlaps.length} overlap conflict(s) overridden:`);
|
|
75
|
+
for (const c of overlaps) {
|
|
76
|
+
console.log(` [!!] ${c.reason}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(`\nClaim registered:`);
|
|
81
|
+
}
|
|
82
|
+
console.log(` ID: ${claim.id}`);
|
|
83
|
+
console.log(` Sprint: ${claim.sprint_number}`);
|
|
84
|
+
console.log(` Player: ${claim.player}`);
|
|
85
|
+
console.log(` Target: ${claim.target} (${claim.scope})`);
|
|
86
|
+
if (claim.notes)
|
|
87
|
+
console.log(` Notes: ${claim.notes}`);
|
|
88
|
+
// Adjacent conflicts are informational only
|
|
89
|
+
if (adjacents.length > 0) {
|
|
90
|
+
console.log(`\n Note: ${adjacents.length} adjacent conflict(s):`);
|
|
91
|
+
for (const c of adjacents) {
|
|
92
|
+
console.log(` [~] ${c.reason}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (overlaps.length === 0 && adjacents.length === 0) {
|
|
96
|
+
console.log(`\n No conflicts detected.`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -94,6 +94,12 @@ export function initCommand(args) {
|
|
|
94
94
|
writeFileSync(sessionsPath, JSON.stringify({ sessions: [] }, null, 2) + '\n');
|
|
95
95
|
console.log(` Created ${sessionsPath}`);
|
|
96
96
|
}
|
|
97
|
+
// Write empty claims.json
|
|
98
|
+
const claimsPath = join(cwd, '.slope', 'claims.json');
|
|
99
|
+
if (!existsSync(claimsPath)) {
|
|
100
|
+
writeFileSync(claimsPath, JSON.stringify({ claims: [] }, null, 2) + '\n');
|
|
101
|
+
console.log(` Created ${claimsPath}`);
|
|
102
|
+
}
|
|
97
103
|
// Claude Code templates
|
|
98
104
|
if (claudeCode) {
|
|
99
105
|
const templatesRoot = join(__dirname, '..', '..', '..', '..', 'templates', 'claude-code');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { loadScorecards } from '../loader.js';
|
|
3
|
+
import { createRegistry } from '../registries/index.js';
|
|
4
|
+
function parseArgs(args) {
|
|
5
|
+
const result = {};
|
|
6
|
+
for (const arg of args) {
|
|
7
|
+
const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
|
|
8
|
+
if (match)
|
|
9
|
+
result[match[1]] = match[2];
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
function resolveSprintRange(flags, cwd) {
|
|
14
|
+
const config = loadConfig(cwd);
|
|
15
|
+
if (flags.sprint)
|
|
16
|
+
return [parseInt(flags.sprint, 10)];
|
|
17
|
+
if (config.currentSprint)
|
|
18
|
+
return [config.currentSprint];
|
|
19
|
+
const scorecards = loadScorecards(config, cwd);
|
|
20
|
+
if (scorecards.length === 0)
|
|
21
|
+
return [1];
|
|
22
|
+
const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
|
|
23
|
+
// Check the current and next sprint (most likely locations)
|
|
24
|
+
return Array.from({ length: maxSprint + 1 }, (_, i) => i + 1);
|
|
25
|
+
}
|
|
26
|
+
export async function releaseCommand(args) {
|
|
27
|
+
const flags = parseArgs(args);
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const config = loadConfig(cwd);
|
|
30
|
+
const registry = createRegistry(config, cwd);
|
|
31
|
+
// Release by ID
|
|
32
|
+
if (flags.id) {
|
|
33
|
+
const released = await registry.release(flags.id);
|
|
34
|
+
if (released) {
|
|
35
|
+
console.log(`\nClaim ${flags.id} released.\n`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.error(`\nClaim ${flags.id} not found.\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Release by target + player lookup
|
|
44
|
+
if (flags.target) {
|
|
45
|
+
const player = flags.player || process.env.USER || 'unknown';
|
|
46
|
+
const sprints = resolveSprintRange(flags, cwd);
|
|
47
|
+
for (const sprint of sprints) {
|
|
48
|
+
const claims = await registry.list(sprint);
|
|
49
|
+
const match = claims.find(c => c.target === flags.target && c.player === player);
|
|
50
|
+
if (match) {
|
|
51
|
+
const released = await registry.release(match.id);
|
|
52
|
+
if (released) {
|
|
53
|
+
console.log(`\nClaim ${match.id} (${match.target} by ${match.player}, sprint ${match.sprint_number}) released.\n`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.error(`\nNo claim found for target "${flags.target}" by player "${player}".\n`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.error('Error: --id or --target is required');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { checkConflicts } from '@slope-dev/core';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { loadScorecards } from '../loader.js';
|
|
4
|
+
import { createRegistry } from '../registries/index.js';
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const arg of args) {
|
|
8
|
+
const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
|
|
9
|
+
if (match)
|
|
10
|
+
result[match[1]] = match[2];
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
function resolveSprint(flags, cwd) {
|
|
15
|
+
if (flags.sprint)
|
|
16
|
+
return parseInt(flags.sprint, 10);
|
|
17
|
+
const config = loadConfig(cwd);
|
|
18
|
+
if (config.currentSprint)
|
|
19
|
+
return config.currentSprint;
|
|
20
|
+
const scorecards = loadScorecards(config, cwd);
|
|
21
|
+
if (scorecards.length === 0)
|
|
22
|
+
return 1;
|
|
23
|
+
const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
|
|
24
|
+
return maxSprint + 1;
|
|
25
|
+
}
|
|
26
|
+
export async function statusCommand(args) {
|
|
27
|
+
const flags = parseArgs(args);
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const config = loadConfig(cwd);
|
|
30
|
+
const registry = createRegistry(config, cwd);
|
|
31
|
+
const sprintNumber = resolveSprint(flags, cwd);
|
|
32
|
+
const claims = await registry.list(sprintNumber);
|
|
33
|
+
console.log(`\nSprint ${sprintNumber} — Course Status`);
|
|
34
|
+
console.log('═'.repeat(40));
|
|
35
|
+
if (claims.length === 0) {
|
|
36
|
+
console.log('\n No claims registered.\n');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Group by player
|
|
40
|
+
const byPlayer = new Map();
|
|
41
|
+
for (const claim of claims) {
|
|
42
|
+
const list = byPlayer.get(claim.player) || [];
|
|
43
|
+
list.push(claim);
|
|
44
|
+
byPlayer.set(claim.player, list);
|
|
45
|
+
}
|
|
46
|
+
for (const [player, playerClaims] of byPlayer) {
|
|
47
|
+
console.log(`\n ${player}:`);
|
|
48
|
+
for (const c of playerClaims) {
|
|
49
|
+
const scopeTag = c.scope === 'area' ? '[area]' : '[ticket]';
|
|
50
|
+
const notes = c.notes ? ` — ${c.notes}` : '';
|
|
51
|
+
console.log(` ${scopeTag} ${c.target}${notes} (${c.id})`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Check conflicts
|
|
55
|
+
const conflicts = checkConflicts(claims);
|
|
56
|
+
if (conflicts.length > 0) {
|
|
57
|
+
console.log(`\n Conflicts (${conflicts.length}):`);
|
|
58
|
+
for (const c of conflicts) {
|
|
59
|
+
const icon = c.severity === 'overlap' ? '!!' : '~';
|
|
60
|
+
console.log(` [${icon}] ${c.reason} (${c.severity})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* slope card Display handicap card
|
|
8
8
|
* slope validate [path] Validate scorecard(s)
|
|
9
9
|
* slope review [path] [--plain] Format sprint review
|
|
10
|
-
* slope briefing [options]
|
|
10
|
+
* slope briefing [--sprint=N] [options] Pre-round briefing
|
|
11
11
|
* slope plan --complexity=<level> Pre-shot advisor
|
|
12
12
|
* slope classify --scope=... ... Classify a shot
|
|
13
|
+
* slope claim --target=<t> [--force] Claim a ticket or area
|
|
14
|
+
* slope release --id=<id> Release a claim
|
|
15
|
+
* slope status [--sprint=N] Show sprint course status
|
|
13
16
|
*/
|
|
14
17
|
import { initCommand } from './commands/init.js';
|
|
15
18
|
import { cardCommand } from './commands/card.js';
|
|
@@ -18,6 +21,9 @@ import { reviewCommand } from './commands/review.js';
|
|
|
18
21
|
import { briefingCommand } from './commands/briefing.js';
|
|
19
22
|
import { planCommand } from './commands/plan.js';
|
|
20
23
|
import { classifyCommand } from './commands/classify.js';
|
|
24
|
+
import { claimCommand } from './commands/claim.js';
|
|
25
|
+
import { releaseCommand } from './commands/release.js';
|
|
26
|
+
import { statusCommand } from './commands/status.js';
|
|
21
27
|
const subcommand = process.argv[2];
|
|
22
28
|
switch (subcommand) {
|
|
23
29
|
case 'init':
|
|
@@ -37,7 +43,10 @@ switch (subcommand) {
|
|
|
37
43
|
break;
|
|
38
44
|
}
|
|
39
45
|
case 'briefing':
|
|
40
|
-
briefingCommand(process.argv.slice(3))
|
|
46
|
+
briefingCommand(process.argv.slice(3)).catch(err => {
|
|
47
|
+
console.error('Error:', err.message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
41
50
|
break;
|
|
42
51
|
case 'plan':
|
|
43
52
|
planCommand(process.argv.slice(3));
|
|
@@ -45,6 +54,24 @@ switch (subcommand) {
|
|
|
45
54
|
case 'classify':
|
|
46
55
|
classifyCommand(process.argv.slice(3));
|
|
47
56
|
break;
|
|
57
|
+
case 'claim':
|
|
58
|
+
claimCommand(process.argv.slice(3)).catch(err => {
|
|
59
|
+
console.error('Error:', err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
case 'release':
|
|
64
|
+
releaseCommand(process.argv.slice(3)).catch(err => {
|
|
65
|
+
console.error('Error:', err.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
break;
|
|
69
|
+
case 'status':
|
|
70
|
+
statusCommand(process.argv.slice(3)).catch(err => {
|
|
71
|
+
console.error('Error:', err.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
break;
|
|
48
75
|
default:
|
|
49
76
|
console.log(`
|
|
50
77
|
SLOPE CLI — Sprint Lifecycle & Operational Performance Engine
|
|
@@ -54,9 +81,13 @@ Usage:
|
|
|
54
81
|
slope card Show handicap card
|
|
55
82
|
slope validate [path] Validate scorecard(s)
|
|
56
83
|
slope review [path] [--plain] Format sprint review markdown
|
|
57
|
-
slope briefing [options]
|
|
84
|
+
slope briefing [--sprint=N] [options] Pre-round briefing
|
|
58
85
|
slope plan --complexity=<level> Pre-shot advisor (club + training + hazards)
|
|
59
86
|
slope classify --scope=... ... Classify a shot from execution trace
|
|
87
|
+
slope claim --target=<t> [--force] Claim a ticket or area for the sprint
|
|
88
|
+
slope release --id=<id> Release a claim by ID
|
|
89
|
+
slope release --target=<t> [--player=<p>] Release a claim by target
|
|
90
|
+
slope status [--sprint=N] Show sprint course status + conflicts
|
|
60
91
|
|
|
61
92
|
Examples:
|
|
62
93
|
slope init Create .slope/ with config + example scorecard
|
|
@@ -72,6 +103,12 @@ Examples:
|
|
|
72
103
|
slope plan --complexity=medium Club recommendation for medium ticket
|
|
73
104
|
slope plan --complexity=large --areas=db Include hazard warnings for db area
|
|
74
105
|
slope classify --scope="a.ts" --modified="a.ts" --tests=pass --reverts=0
|
|
106
|
+
slope briefing --sprint=2 Briefing for sprint 2
|
|
107
|
+
slope claim --target=S2-1 --sprint=2 Claim ticket S2-1 for sprint 2
|
|
108
|
+
slope claim --target=packages/cli --scope=area Claim an area
|
|
109
|
+
slope claim --target=S2-1 --force Claim even if overlap conflict exists
|
|
110
|
+
slope status --sprint=2 Show all claims for sprint 2
|
|
111
|
+
slope release --target=S2-1 Release your claim on S2-1
|
|
75
112
|
`);
|
|
76
113
|
process.exit(subcommand ? 1 : 0);
|
|
77
114
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class ApiRegistry {
|
|
2
|
+
baseUrl;
|
|
3
|
+
constructor(baseUrl) {
|
|
4
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
5
|
+
}
|
|
6
|
+
async claim(input) {
|
|
7
|
+
const res = await fetch(`${this.baseUrl}/claims`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
body: JSON.stringify(input),
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
14
|
+
return res.json();
|
|
15
|
+
}
|
|
16
|
+
async release(id) {
|
|
17
|
+
const res = await fetch(`${this.baseUrl}/claims/${encodeURIComponent(id)}`, {
|
|
18
|
+
method: 'DELETE',
|
|
19
|
+
});
|
|
20
|
+
if (res.status === 404)
|
|
21
|
+
return false;
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
async list(sprintNumber) {
|
|
27
|
+
const res = await fetch(`${this.baseUrl}/claims?sprint=${sprintNumber}`);
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
30
|
+
return res.json();
|
|
31
|
+
}
|
|
32
|
+
async get(id) {
|
|
33
|
+
const res = await fetch(`${this.baseUrl}/claims/${encodeURIComponent(id)}`);
|
|
34
|
+
if (res.status === 404)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
export class FileRegistry {
|
|
4
|
+
filePath;
|
|
5
|
+
constructor(filePath) {
|
|
6
|
+
this.filePath = filePath;
|
|
7
|
+
}
|
|
8
|
+
async claim(input) {
|
|
9
|
+
const claims = this.readClaims();
|
|
10
|
+
const claim = {
|
|
11
|
+
...input,
|
|
12
|
+
id: `claim-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
13
|
+
claimed_at: new Date().toISOString(),
|
|
14
|
+
};
|
|
15
|
+
claims.push(claim);
|
|
16
|
+
this.writeClaims(claims);
|
|
17
|
+
return claim;
|
|
18
|
+
}
|
|
19
|
+
async release(id) {
|
|
20
|
+
const claims = this.readClaims();
|
|
21
|
+
const idx = claims.findIndex(c => c.id === id);
|
|
22
|
+
if (idx === -1)
|
|
23
|
+
return false;
|
|
24
|
+
claims.splice(idx, 1);
|
|
25
|
+
this.writeClaims(claims);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
async list(sprintNumber) {
|
|
29
|
+
return this.readClaims().filter(c => c.sprint_number === sprintNumber);
|
|
30
|
+
}
|
|
31
|
+
async get(id) {
|
|
32
|
+
return this.readClaims().find(c => c.id === id);
|
|
33
|
+
}
|
|
34
|
+
readClaims() {
|
|
35
|
+
if (!existsSync(this.filePath))
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(readFileSync(this.filePath, 'utf8'));
|
|
39
|
+
return Array.isArray(data.claims) ? data.claims : [];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
writeClaims(claims) {
|
|
46
|
+
const dir = dirname(this.filePath);
|
|
47
|
+
if (!existsSync(dir)) {
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
const data = { claims };
|
|
51
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2) + '\n');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { FileRegistry } from './file-registry.js';
|
|
3
|
+
import { ApiRegistry } from './api-registry.js';
|
|
4
|
+
export function createRegistry(config, cwd = process.cwd()) {
|
|
5
|
+
switch (config.registry) {
|
|
6
|
+
case 'api': {
|
|
7
|
+
if (!config.registryApiUrl) {
|
|
8
|
+
throw new Error('registryApiUrl is required when registry is set to "api"');
|
|
9
|
+
}
|
|
10
|
+
return new ApiRegistry(config.registryApiUrl);
|
|
11
|
+
}
|
|
12
|
+
case 'file':
|
|
13
|
+
default:
|
|
14
|
+
return new FileRegistry(join(cwd, config.claimsPath));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export { FileRegistry } from './file-registry.js';
|
|
18
|
+
export { ApiRegistry } from './api-registry.js';
|
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slope-dev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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
|
+
},
|
|
9
14
|
"dependencies": {
|
|
10
|
-
"@slope-dev/core": "
|
|
15
|
+
"@slope-dev/core": "workspace:*"
|
|
11
16
|
},
|
|
12
17
|
"devDependencies": {
|
|
13
18
|
"@types/node": "^25.3.0",
|
|
14
|
-
"typescript": "^5.7.0"
|
|
19
|
+
"typescript": "^5.7.0",
|
|
20
|
+
"vitest": "^3.0.0"
|
|
15
21
|
},
|
|
16
22
|
"files": [
|
|
17
23
|
"dist"
|
|
@@ -34,8 +40,5 @@
|
|
|
34
40
|
],
|
|
35
41
|
"engines": {
|
|
36
42
|
"node": ">=18"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsc"
|
|
40
43
|
}
|
|
41
|
-
}
|
|
44
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|