lightspec 0.1.1 → 0.2.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.
- package/README.md +11 -10
- package/dist/cli/index.js +12 -24
- package/dist/commands/feedback.js +2 -2
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +1 -0
- package/dist/core/configurators/slash/amazon-q.js +6 -2
- package/dist/core/configurators/slash/antigravity.js +4 -2
- package/dist/core/configurators/slash/auggie.js +8 -1
- package/dist/core/configurators/slash/base.d.ts +14 -3
- package/dist/core/configurators/slash/base.js +160 -16
- package/dist/core/configurators/slash/claude.js +8 -1
- package/dist/core/configurators/slash/cline.js +4 -2
- package/dist/core/configurators/slash/codebuddy.js +8 -1
- package/dist/core/configurators/slash/codex.d.ts +0 -8
- package/dist/core/configurators/slash/codex.js +0 -103
- package/dist/core/configurators/slash/continue.js +8 -1
- package/dist/core/configurators/slash/costrict.js +4 -0
- package/dist/core/configurators/slash/crush.js +8 -1
- package/dist/core/configurators/slash/cursor.js +8 -1
- package/dist/core/configurators/slash/factory.js +8 -1
- package/dist/core/configurators/slash/gemini.js +4 -2
- package/dist/core/configurators/slash/github-copilot.js +6 -2
- package/dist/core/configurators/slash/iflow.js +8 -1
- package/dist/core/configurators/slash/kilocode.js +2 -1
- package/dist/core/configurators/slash/mistral-vibe.d.ts +6 -0
- package/dist/core/configurators/slash/mistral-vibe.js +6 -0
- package/dist/core/configurators/slash/opencode.js +4 -0
- package/dist/core/configurators/slash/qoder.js +8 -1
- package/dist/core/configurators/slash/qwen.js +4 -2
- package/dist/core/configurators/slash/registry.d.ts +2 -1
- package/dist/core/configurators/slash/registry.js +8 -0
- package/dist/core/configurators/slash/roocode.js +4 -2
- package/dist/core/configurators/slash/toml-base.d.ts +0 -6
- package/dist/core/configurators/slash/toml-base.js +0 -49
- package/dist/core/configurators/slash/windsurf.js +4 -2
- package/dist/core/init.d.ts +6 -0
- package/dist/core/init.js +49 -22
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +4 -7
- package/dist/core/templates/apply-template.d.ts +3 -0
- package/dist/core/templates/apply-template.js +21 -0
- package/dist/core/templates/archive-template.d.ts +3 -0
- package/dist/core/templates/archive-template.js +29 -0
- package/dist/core/templates/context-check-template.d.ts +3 -0
- package/dist/core/templates/context-check-template.js +128 -0
- package/dist/core/templates/index.d.ts +1 -0
- package/dist/core/templates/index.js +4 -6
- package/dist/core/templates/proposal-template.d.ts +3 -0
- package/dist/core/templates/proposal-template.js +30 -0
- package/dist/core/templates/skill-common-template.d.ts +2 -0
- package/dist/core/templates/skill-common-template.js +5 -0
- package/dist/core/templates/slash-command-templates.d.ts +3 -1
- package/dist/core/templates/slash-command-templates.js +17 -43
- package/dist/core/update.js +5 -5
- package/dist/telemetry/index.d.ts +2 -3
- package/dist/telemetry/index.js +2 -3
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +5 -0
- package/package.json +19 -22
package/README.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://github.com/augmenter-dev/lightspec">
|
|
3
3
|
<picture>
|
|
4
|
-
<
|
|
5
|
-
<source srcset="assets/lightspec_pixel_light.svg" media="(prefers-color-scheme: light)">
|
|
6
|
-
<img src="assets/lightspec_pixel_light.svg" alt="LightSpec logo" height="64">
|
|
4
|
+
<img src="assets/augmenter-lightspec.svg" alt="LightSpec logo" height="64">
|
|
7
5
|
</picture>
|
|
8
6
|
</a>
|
|
9
7
|
|
|
@@ -19,7 +17,7 @@
|
|
|
19
17
|
</p>
|
|
20
18
|
|
|
21
19
|
<p align="center">
|
|
22
|
-
<img src="assets/
|
|
20
|
+
<img src="assets/openspec_dashboard.png" alt="LightSpec dashboard preview" width="90%">
|
|
23
21
|
</p>
|
|
24
22
|
|
|
25
23
|
<p align="center">
|
|
@@ -28,7 +26,7 @@
|
|
|
28
26
|
|
|
29
27
|
# LightSpec
|
|
30
28
|
|
|
31
|
-
A fork of [LightSpec](https://github.com/
|
|
29
|
+
A fork of [LightSpec](https://github.com/augmenter-dev/LightSpec), focused on simplicity and skill-based agents.
|
|
32
30
|
|
|
33
31
|
LightSpec aligns humans and AI coding assistants with spec-driven development so you agree on what to build before any code is written. **No API keys required.**
|
|
34
32
|
|
|
@@ -207,14 +205,14 @@ lightspec init
|
|
|
207
205
|
|
|
208
206
|
### Optional: Populate Project Context
|
|
209
207
|
|
|
210
|
-
After `lightspec init` completes, you'll receive a suggested
|
|
208
|
+
After `lightspec init` completes, you'll receive a suggested command to validate and populate your project context:
|
|
211
209
|
|
|
212
210
|
```text
|
|
213
|
-
|
|
214
|
-
"
|
|
211
|
+
Validate and populate your project context:
|
|
212
|
+
"/lightspec:context-check"
|
|
215
213
|
```
|
|
216
214
|
|
|
217
|
-
Use
|
|
215
|
+
Use the `/lightspec:context-check` skill to validate that your agent instruction file (CLAUDE.md or AGENTS.md) contains adequate project context. The skill will check for required properties like Purpose, Tech Stack, Architecture Patterns, and more. If anything is missing, it can help you explore the codebase and populate the missing information.
|
|
218
216
|
|
|
219
217
|
### Create Your First Change
|
|
220
218
|
|
|
@@ -379,6 +377,9 @@ Deltas are "patches" that show how specs change:
|
|
|
379
377
|
|
|
380
378
|
## How LightSpec Compares
|
|
381
379
|
|
|
380
|
+
### vs. OpenSpec
|
|
381
|
+
OpenSpec has evolved into a more mature yet complex tool with a rich feature set. LightSpec focuses on simplicity and ease of adoption, especially for teams new to spec-driven development. LightSpec's minimalist approach has the additional benefit of reducing the number of skills and commands needed, and reducing the risk of involuntary skill activation from AI assistants.
|
|
382
|
+
|
|
382
383
|
### vs. spec-kit
|
|
383
384
|
LightSpec’s two-folder model (`lightspec/specs/` for the current truth, `lightspec/changes/` for proposed updates) keeps state and diffs separate. This scales when you modify existing features or touch multiple specs. spec-kit is strong for greenfield/0→1 but provides less structure for cross-spec updates and evolving features.
|
|
384
385
|
|
|
@@ -424,7 +425,7 @@ See [MAINTAINERS.md](MAINTAINERS.md) for the list of core maintainers and adviso
|
|
|
424
425
|
## Agent Skills
|
|
425
426
|
|
|
426
427
|
LightSpec includes 3 Claude Code skills for the core development workflow:
|
|
427
|
-
- `lightspec-
|
|
428
|
+
- `lightspec-proposal` - Create a new change
|
|
428
429
|
- `lightspec-apply` - Get apply instructions for implementation
|
|
429
430
|
- `lightspec-archive` - Archive a completed change
|
|
430
431
|
|
package/dist/cli/index.js
CHANGED
|
@@ -19,23 +19,6 @@ import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/i
|
|
|
19
19
|
const program = new Command();
|
|
20
20
|
const require = createRequire(import.meta.url);
|
|
21
21
|
const { version } = require('../../package.json');
|
|
22
|
-
/**
|
|
23
|
-
* Get the full command path for nested commands.
|
|
24
|
-
* For example: 'change show' -> 'change:show'
|
|
25
|
-
*/
|
|
26
|
-
function getCommandPath(command) {
|
|
27
|
-
const names = [];
|
|
28
|
-
let current = command;
|
|
29
|
-
while (current) {
|
|
30
|
-
const name = current.name();
|
|
31
|
-
// Skip the root 'lightspec' command
|
|
32
|
-
if (name && name !== 'lightspec') {
|
|
33
|
-
names.unshift(name);
|
|
34
|
-
}
|
|
35
|
-
current = current.parent;
|
|
36
|
-
}
|
|
37
|
-
return names.join(':') || 'lightspec';
|
|
38
|
-
}
|
|
39
22
|
program
|
|
40
23
|
.name('lightspec')
|
|
41
24
|
.description('AI-native system for spec-driven development')
|
|
@@ -43,19 +26,15 @@ program
|
|
|
43
26
|
// Global options
|
|
44
27
|
program.option('--no-color', 'Disable color output');
|
|
45
28
|
// Apply global flags and telemetry before any command runs
|
|
46
|
-
|
|
47
|
-
// - thisCommand: the command where hook was added (root program)
|
|
48
|
-
// - actionCommand: the command actually being executed (subcommand)
|
|
49
|
-
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
29
|
+
program.hook('preAction', async (thisCommand) => {
|
|
50
30
|
const opts = thisCommand.opts();
|
|
51
31
|
if (opts.color === false) {
|
|
52
32
|
process.env.NO_COLOR = '1';
|
|
53
33
|
}
|
|
54
34
|
// Show first-run telemetry notice (if not seen)
|
|
55
35
|
await maybeShowTelemetryNotice();
|
|
56
|
-
// Track command execution
|
|
57
|
-
|
|
58
|
-
await trackCommand(commandPath, version);
|
|
36
|
+
// Track command execution
|
|
37
|
+
await trackCommand();
|
|
59
38
|
});
|
|
60
39
|
// Shutdown telemetry after command completes
|
|
61
40
|
program.hook('postAction', async () => {
|
|
@@ -63,10 +42,12 @@ program.hook('postAction', async () => {
|
|
|
63
42
|
});
|
|
64
43
|
const availableToolIds = AI_TOOLS.filter((tool) => tool.available).map((tool) => tool.value);
|
|
65
44
|
const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
|
|
45
|
+
const skillLocationOptionDescription = 'Install generated skills in "project" or "home" location (defaults to interactive selection).';
|
|
66
46
|
program
|
|
67
47
|
.command('init [path]')
|
|
68
48
|
.description('Initialize LightSpec in your project')
|
|
69
49
|
.option('--tools <tools>', toolsOptionDescription)
|
|
50
|
+
.option('--skills-location <location>', skillLocationOptionDescription)
|
|
70
51
|
.action(async (targetPath = '.', options) => {
|
|
71
52
|
try {
|
|
72
53
|
// Validate that the path is a valid directory
|
|
@@ -90,8 +71,15 @@ program
|
|
|
90
71
|
}
|
|
91
72
|
}
|
|
92
73
|
const { InitCommand } = await import('../core/init.js');
|
|
74
|
+
const skillLocation = options?.skillsLocation === 'project' || options?.skillsLocation === 'home'
|
|
75
|
+
? options.skillsLocation
|
|
76
|
+
: undefined;
|
|
77
|
+
if (options?.skillsLocation && !skillLocation) {
|
|
78
|
+
throw new Error('Invalid --skills-location value. Use "project" or "home".');
|
|
79
|
+
}
|
|
93
80
|
const initCommand = new InitCommand({
|
|
94
81
|
tools: options?.tools,
|
|
82
|
+
skillLocation,
|
|
95
83
|
});
|
|
96
84
|
await initCommand.execute(targetPath);
|
|
97
85
|
}
|
|
@@ -87,7 +87,7 @@ function formatBody(bodyText) {
|
|
|
87
87
|
* Generate a pre-filled GitHub issue URL for manual submission
|
|
88
88
|
*/
|
|
89
89
|
function generateManualSubmissionUrl(title, body) {
|
|
90
|
-
const repo = '
|
|
90
|
+
const repo = 'augmenter-dev/LightSpec';
|
|
91
91
|
const encodedTitle = encodeURIComponent(title);
|
|
92
92
|
const encodedBody = encodeURIComponent(body);
|
|
93
93
|
const encodedLabels = encodeURIComponent('feedback');
|
|
@@ -114,7 +114,7 @@ function submitViaGhCli(title, body) {
|
|
|
114
114
|
'issue',
|
|
115
115
|
'create',
|
|
116
116
|
'--repo',
|
|
117
|
-
'
|
|
117
|
+
'augmenter-dev/LightSpec',
|
|
118
118
|
'--title',
|
|
119
119
|
title,
|
|
120
120
|
'--body',
|
package/dist/core/config.d.ts
CHANGED
package/dist/core/config.js
CHANGED
|
@@ -20,6 +20,7 @@ export const AI_TOOLS = [
|
|
|
20
20
|
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
|
|
21
21
|
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' },
|
|
22
22
|
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
|
23
|
+
{ name: 'Mistral Vibe', value: 'mistral-vibe', available: true, successLabel: 'Mistral Vibe' },
|
|
23
24
|
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
|
24
25
|
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
|
|
25
26
|
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' },
|
|
@@ -2,7 +2,8 @@ import { SlashCommandConfigurator } from './base.js';
|
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.amazonq/prompts/lightspec-proposal.md',
|
|
4
4
|
apply: '.amazonq/prompts/lightspec-apply.md',
|
|
5
|
-
archive: '.amazonq/prompts/lightspec-archive.md'
|
|
5
|
+
archive: '.amazonq/prompts/lightspec-archive.md',
|
|
6
|
+
'context-check': '.aws/amazonq/commands/lightspec-context-check.md'
|
|
6
7
|
};
|
|
7
8
|
const FRONTMATTER = {
|
|
8
9
|
proposal: `---
|
|
@@ -31,7 +32,10 @@ The user wants to archive the following deployed change. Use the lightspec instr
|
|
|
31
32
|
|
|
32
33
|
<ChangeId>
|
|
33
34
|
$ARGUMENTS
|
|
34
|
-
</ChangeId
|
|
35
|
+
</ChangeId>`,
|
|
36
|
+
'context-check': `---
|
|
37
|
+
description: Validate project context in agent instruction files and help populate missing information.
|
|
38
|
+
---`
|
|
35
39
|
};
|
|
36
40
|
export class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
37
41
|
toolId = 'amazon-q';
|
|
@@ -2,12 +2,14 @@ import { SlashCommandConfigurator } from './base.js';
|
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.agent/workflows/lightspec-proposal.md',
|
|
4
4
|
apply: '.agent/workflows/lightspec-apply.md',
|
|
5
|
-
archive: '.agent/workflows/lightspec-archive.md'
|
|
5
|
+
archive: '.agent/workflows/lightspec-archive.md',
|
|
6
|
+
'context-check': '.antigravity/commands/lightspec-context-check.md'
|
|
6
7
|
};
|
|
7
8
|
const DESCRIPTIONS = {
|
|
8
9
|
proposal: 'Scaffold a new LightSpec change and validate strictly.',
|
|
9
10
|
apply: 'Implement an approved LightSpec change and keep tasks in sync.',
|
|
10
|
-
archive: 'Archive a deployed LightSpec change and update specs.'
|
|
11
|
+
archive: 'Archive a deployed LightSpec change and update specs.',
|
|
12
|
+
'context-check': 'Validate project context in agent instruction files and help populate missing information.'
|
|
11
13
|
};
|
|
12
14
|
export class AntigravitySlashCommandConfigurator extends SlashCommandConfigurator {
|
|
13
15
|
toolId = 'antigravity';
|
|
@@ -2,7 +2,8 @@ import { SlashCommandConfigurator } from './base.js';
|
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.augment/commands/lightspec-proposal.md',
|
|
4
4
|
apply: '.augment/commands/lightspec-apply.md',
|
|
5
|
-
archive: '.augment/commands/lightspec-archive.md'
|
|
5
|
+
archive: '.augment/commands/lightspec-archive.md',
|
|
6
|
+
'context-check': '.auggie/commands/lightspec-context-check.md'
|
|
6
7
|
};
|
|
7
8
|
const FRONTMATTER = {
|
|
8
9
|
proposal: `---
|
|
@@ -16,6 +17,12 @@ argument-hint: change-id
|
|
|
16
17
|
archive: `---
|
|
17
18
|
description: Archive a deployed LightSpec change and update specs.
|
|
18
19
|
argument-hint: change-id
|
|
20
|
+
---`,
|
|
21
|
+
'context-check': `---
|
|
22
|
+
name: LightSpec: Context Check
|
|
23
|
+
description: Validate project context in agent instruction files and help populate missing information.
|
|
24
|
+
category: LightSpec
|
|
25
|
+
tags: [lightspec, context, validation]
|
|
19
26
|
---`
|
|
20
27
|
};
|
|
21
28
|
export class AuggieSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
@@ -2,18 +2,29 @@ import { SlashCommandId } from '../../templates/index.js';
|
|
|
2
2
|
export interface SlashCommandTarget {
|
|
3
3
|
id: SlashCommandId;
|
|
4
4
|
path: string;
|
|
5
|
-
kind: '
|
|
5
|
+
kind: 'skill';
|
|
6
6
|
}
|
|
7
|
+
export type SkillInstallLocation = 'project' | 'home';
|
|
7
8
|
export declare abstract class SlashCommandConfigurator {
|
|
8
9
|
abstract readonly toolId: string;
|
|
9
10
|
abstract readonly isAvailable: boolean;
|
|
11
|
+
private installLocation;
|
|
12
|
+
setInstallLocation(location: SkillInstallLocation): void;
|
|
10
13
|
getTargets(): SlashCommandTarget[];
|
|
11
14
|
generateAll(projectPath: string, _lightspecDir: string): Promise<string[]>;
|
|
12
15
|
updateExisting(projectPath: string, _lightspecDir: string): Promise<string[]>;
|
|
13
|
-
protected abstract getRelativePath(id: SlashCommandId): string;
|
|
14
|
-
protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
|
|
15
16
|
protected getBody(id: SlashCommandId): string;
|
|
16
17
|
resolveAbsolutePath(projectPath: string, id: SlashCommandId): string;
|
|
18
|
+
private getRelativeSkillPath;
|
|
19
|
+
private getToolRoot;
|
|
20
|
+
private getHomeRootPath;
|
|
21
|
+
private getSkillName;
|
|
22
|
+
private buildSkillFile;
|
|
17
23
|
protected updateBody(filePath: string, body: string): Promise<void>;
|
|
24
|
+
private cleanupLegacyArtifacts;
|
|
25
|
+
private relativeToToolRoot;
|
|
26
|
+
private removeLegacyLightSpecFiles;
|
|
27
|
+
private walkAndRemove;
|
|
28
|
+
private isLegacyLightSpecFile;
|
|
18
29
|
}
|
|
19
30
|
//# sourceMappingURL=base.d.ts.map
|
|
@@ -1,57 +1,151 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
1
4
|
import { FileSystemUtils } from '../../../utils/file-system.js';
|
|
2
5
|
import { TemplateManager } from '../../templates/index.js';
|
|
3
6
|
import { LIGHTSPEC_MARKERS } from '../../config.js';
|
|
4
|
-
const ALL_COMMANDS = ['proposal', 'apply', 'archive'];
|
|
7
|
+
const ALL_COMMANDS = ['proposal', 'apply', 'archive', 'context-check'];
|
|
8
|
+
const TOOL_SKILL_ROOTS = {
|
|
9
|
+
'amazon-q': '.amazonq',
|
|
10
|
+
antigravity: '.antigravity',
|
|
11
|
+
auggie: '.auggie',
|
|
12
|
+
claude: '.claude',
|
|
13
|
+
cline: '.cline',
|
|
14
|
+
codex: '.codex',
|
|
15
|
+
codebuddy: '.codebuddy',
|
|
16
|
+
continue: '.continue',
|
|
17
|
+
costrict: '.cospec/lightspec',
|
|
18
|
+
crush: '.crush',
|
|
19
|
+
cursor: '.cursor',
|
|
20
|
+
factory: '.factory',
|
|
21
|
+
gemini: '.gemini',
|
|
22
|
+
'github-copilot': '.github/copilot',
|
|
23
|
+
iflow: '.iflow',
|
|
24
|
+
kilocode: '.kilocode',
|
|
25
|
+
'mistral-vibe': '.vibe',
|
|
26
|
+
opencode: '.opencode',
|
|
27
|
+
qoder: '.qoder',
|
|
28
|
+
qwen: '.qwen',
|
|
29
|
+
roocode: '.roocode',
|
|
30
|
+
windsurf: '.windsurf',
|
|
31
|
+
};
|
|
32
|
+
const TOOL_LEGACY_DIRS = {
|
|
33
|
+
'amazon-q': ['.amazonq/prompts', '.aws/amazonq/commands'],
|
|
34
|
+
antigravity: ['.antigravity/commands', '.agent/workflows'],
|
|
35
|
+
auggie: ['.augment/commands', '.auggie/commands'],
|
|
36
|
+
claude: ['.claude/commands'],
|
|
37
|
+
cline: ['.cline/commands', '.clinerules/workflows'],
|
|
38
|
+
codex: ['.codex/prompts'],
|
|
39
|
+
codebuddy: ['.codebuddy/commands'],
|
|
40
|
+
continue: ['.continue/prompts', '.continue/commands'],
|
|
41
|
+
costrict: ['.cospec/lightspec/commands'],
|
|
42
|
+
crush: ['.crush/commands'],
|
|
43
|
+
cursor: ['.cursor/commands'],
|
|
44
|
+
factory: ['.factory/commands'],
|
|
45
|
+
gemini: ['.gemini/commands'],
|
|
46
|
+
'github-copilot': ['.github/prompts', '.github/copilot/prompts'],
|
|
47
|
+
iflow: ['.iflow/commands'],
|
|
48
|
+
kilocode: ['.kilocode/commands', '.kilocode/workflows'],
|
|
49
|
+
'mistral-vibe': ['.vibe/commands', '.vibe/workflows', '.vibe/prompts'],
|
|
50
|
+
opencode: ['.opencode/command', '.opencode/commands'],
|
|
51
|
+
qoder: ['.qoder/commands', '.qoder/prompts'],
|
|
52
|
+
qwen: ['.qwen/commands', '.qwen/prompts'],
|
|
53
|
+
roocode: ['.roo/commands', '.roocode/commands'],
|
|
54
|
+
windsurf: ['.windsurf/workflows', '.windsurf/commands'],
|
|
55
|
+
};
|
|
5
56
|
export class SlashCommandConfigurator {
|
|
57
|
+
installLocation = 'project';
|
|
58
|
+
setInstallLocation(location) {
|
|
59
|
+
this.installLocation = location;
|
|
60
|
+
}
|
|
6
61
|
getTargets() {
|
|
7
62
|
return ALL_COMMANDS.map((id) => ({
|
|
8
63
|
id,
|
|
9
|
-
path: this.
|
|
10
|
-
kind: '
|
|
64
|
+
path: this.getRelativeSkillPath(id),
|
|
65
|
+
kind: 'skill',
|
|
11
66
|
}));
|
|
12
67
|
}
|
|
13
68
|
async generateAll(projectPath, _lightspecDir) {
|
|
14
69
|
const createdOrUpdated = [];
|
|
15
70
|
for (const target of this.getTargets()) {
|
|
16
71
|
const body = this.getBody(target.id);
|
|
17
|
-
const filePath =
|
|
72
|
+
const filePath = this.resolveAbsolutePath(projectPath, target.id);
|
|
18
73
|
if (await FileSystemUtils.fileExists(filePath)) {
|
|
19
74
|
await this.updateBody(filePath, body);
|
|
20
75
|
}
|
|
21
76
|
else {
|
|
22
|
-
const frontmatter =
|
|
23
|
-
const
|
|
24
|
-
if (frontmatter) {
|
|
25
|
-
sections.push(frontmatter.trim());
|
|
26
|
-
}
|
|
27
|
-
sections.push(`${LIGHTSPEC_MARKERS.start}\n${body}\n${LIGHTSPEC_MARKERS.end}`);
|
|
28
|
-
const content = sections.join('\n') + '\n';
|
|
77
|
+
const frontmatter = TemplateManager.getSlashCommandFrontmatter(target.id).trim();
|
|
78
|
+
const content = this.buildSkillFile(frontmatter, body);
|
|
29
79
|
await FileSystemUtils.writeFile(filePath, content);
|
|
30
80
|
}
|
|
31
81
|
createdOrUpdated.push(target.path);
|
|
32
82
|
}
|
|
83
|
+
await this.cleanupLegacyArtifacts(projectPath);
|
|
33
84
|
return createdOrUpdated;
|
|
34
85
|
}
|
|
35
86
|
async updateExisting(projectPath, _lightspecDir) {
|
|
36
87
|
const updated = [];
|
|
37
88
|
for (const target of this.getTargets()) {
|
|
38
|
-
const filePath =
|
|
89
|
+
const filePath = this.resolveAbsolutePath(projectPath, target.id);
|
|
39
90
|
if (await FileSystemUtils.fileExists(filePath)) {
|
|
40
91
|
const body = this.getBody(target.id);
|
|
41
92
|
await this.updateBody(filePath, body);
|
|
42
93
|
updated.push(target.path);
|
|
43
94
|
}
|
|
44
95
|
}
|
|
96
|
+
await this.cleanupLegacyArtifacts(projectPath);
|
|
45
97
|
return updated;
|
|
46
98
|
}
|
|
47
99
|
getBody(id) {
|
|
48
100
|
return TemplateManager.getSlashCommandBody(id).trim();
|
|
49
101
|
}
|
|
50
|
-
// Resolve absolute path for a given slash command target. Subclasses may override
|
|
51
|
-
// to redirect to tool-specific locations (e.g., global directories).
|
|
52
102
|
resolveAbsolutePath(projectPath, id) {
|
|
53
|
-
const
|
|
54
|
-
|
|
103
|
+
const relativePath = this.getRelativeSkillPath(id);
|
|
104
|
+
if (this.installLocation === 'project') {
|
|
105
|
+
return FileSystemUtils.joinPath(projectPath, relativePath);
|
|
106
|
+
}
|
|
107
|
+
const homeRoot = this.getHomeRootPath();
|
|
108
|
+
const rootPrefix = this.getToolRoot();
|
|
109
|
+
const normalizedRelativePath = FileSystemUtils.toPosixPath(relativePath);
|
|
110
|
+
if (!normalizedRelativePath.startsWith(`${rootPrefix}/`)) {
|
|
111
|
+
throw new Error(`Skill path '${relativePath}' does not match expected root '${rootPrefix}' for ${this.toolId}`);
|
|
112
|
+
}
|
|
113
|
+
const relativeUnderRoot = normalizedRelativePath.slice(rootPrefix.length + 1);
|
|
114
|
+
return FileSystemUtils.joinPath(homeRoot, relativeUnderRoot);
|
|
115
|
+
}
|
|
116
|
+
getRelativeSkillPath(id) {
|
|
117
|
+
const root = this.getToolRoot();
|
|
118
|
+
const skillName = this.getSkillName(id);
|
|
119
|
+
return `${root}/skills/${skillName}/SKILL.md`;
|
|
120
|
+
}
|
|
121
|
+
getToolRoot() {
|
|
122
|
+
const root = TOOL_SKILL_ROOTS[this.toolId];
|
|
123
|
+
if (!root) {
|
|
124
|
+
throw new Error(`No skill root directory configured for tool '${this.toolId}'`);
|
|
125
|
+
}
|
|
126
|
+
return root;
|
|
127
|
+
}
|
|
128
|
+
getHomeRootPath() {
|
|
129
|
+
if (this.toolId === 'codex') {
|
|
130
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
131
|
+
return codexHome && codexHome.length > 0
|
|
132
|
+
? codexHome
|
|
133
|
+
: FileSystemUtils.joinPath(os.homedir(), '.codex');
|
|
134
|
+
}
|
|
135
|
+
const toolRoot = this.getToolRoot();
|
|
136
|
+
const trimmed = toolRoot.startsWith('./') ? toolRoot.slice(2) : toolRoot;
|
|
137
|
+
return path.join(os.homedir(), trimmed);
|
|
138
|
+
}
|
|
139
|
+
getSkillName(id) {
|
|
140
|
+
return `lightspec-${id}`;
|
|
141
|
+
}
|
|
142
|
+
buildSkillFile(frontmatter, body) {
|
|
143
|
+
const sections = [];
|
|
144
|
+
if (frontmatter) {
|
|
145
|
+
sections.push(frontmatter);
|
|
146
|
+
}
|
|
147
|
+
sections.push(`${LIGHTSPEC_MARKERS.start}\n${body}\n${LIGHTSPEC_MARKERS.end}`);
|
|
148
|
+
return `${sections.join('\n\n')}\n`;
|
|
55
149
|
}
|
|
56
150
|
async updateBody(filePath, body) {
|
|
57
151
|
const content = await FileSystemUtils.readFile(filePath);
|
|
@@ -65,5 +159,55 @@ export class SlashCommandConfigurator {
|
|
|
65
159
|
const updatedContent = `${before}\n${body}\n${after}`;
|
|
66
160
|
await FileSystemUtils.writeFile(filePath, updatedContent);
|
|
67
161
|
}
|
|
162
|
+
async cleanupLegacyArtifacts(projectPath) {
|
|
163
|
+
const legacyDirs = TOOL_LEGACY_DIRS[this.toolId] ?? [];
|
|
164
|
+
for (const legacyDir of legacyDirs) {
|
|
165
|
+
const absoluteDir = this.installLocation === 'project'
|
|
166
|
+
? FileSystemUtils.joinPath(projectPath, legacyDir)
|
|
167
|
+
: FileSystemUtils.joinPath(this.getHomeRootPath(), this.relativeToToolRoot(legacyDir));
|
|
168
|
+
await this.removeLegacyLightSpecFiles(absoluteDir);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
relativeToToolRoot(relativePath) {
|
|
172
|
+
const rootPrefix = this.getToolRoot();
|
|
173
|
+
const normalized = FileSystemUtils.toPosixPath(relativePath);
|
|
174
|
+
if (normalized.startsWith(`${rootPrefix}/`)) {
|
|
175
|
+
return normalized.slice(rootPrefix.length + 1);
|
|
176
|
+
}
|
|
177
|
+
if (normalized === rootPrefix) {
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
return normalized.startsWith('./') ? normalized.slice(2) : normalized;
|
|
181
|
+
}
|
|
182
|
+
async removeLegacyLightSpecFiles(dirPath) {
|
|
183
|
+
if (!(await FileSystemUtils.directoryExists(dirPath))) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await this.walkAndRemove(dirPath);
|
|
187
|
+
}
|
|
188
|
+
async walkAndRemove(currentPath) {
|
|
189
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
await this.walkAndRemove(entryPath);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (this.isLegacyLightSpecFile(entryPath)) {
|
|
197
|
+
await fs.unlink(entryPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
isLegacyLightSpecFile(filePath) {
|
|
202
|
+
const normalized = FileSystemUtils.toPosixPath(filePath).toLowerCase();
|
|
203
|
+
return (normalized.includes('/lightspec-proposal') ||
|
|
204
|
+
normalized.includes('/lightspec-apply') ||
|
|
205
|
+
normalized.includes('/lightspec-archive') ||
|
|
206
|
+
normalized.includes('/lightspec-context-check') ||
|
|
207
|
+
normalized.includes('/lightspec/proposal') ||
|
|
208
|
+
normalized.includes('/lightspec/apply') ||
|
|
209
|
+
normalized.includes('/lightspec/archive') ||
|
|
210
|
+
normalized.includes('/lightspec/context-check'));
|
|
211
|
+
}
|
|
68
212
|
}
|
|
69
213
|
//# sourceMappingURL=base.js.map
|
|
@@ -2,7 +2,8 @@ import { SlashCommandConfigurator } from './base.js';
|
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.claude/commands/lightspec/proposal.md',
|
|
4
4
|
apply: '.claude/commands/lightspec/apply.md',
|
|
5
|
-
archive: '.claude/commands/lightspec/archive.md'
|
|
5
|
+
archive: '.claude/commands/lightspec/archive.md',
|
|
6
|
+
'context-check': '.claude/commands/lightspec/context-check.md'
|
|
6
7
|
};
|
|
7
8
|
const FRONTMATTER = {
|
|
8
9
|
proposal: `---
|
|
@@ -22,6 +23,12 @@ name: LightSpec: Archive
|
|
|
22
23
|
description: Archive a deployed LightSpec change and update specs.
|
|
23
24
|
category: LightSpec
|
|
24
25
|
tags: [lightspec, archive]
|
|
26
|
+
---`,
|
|
27
|
+
'context-check': `---
|
|
28
|
+
name: LightSpec: Context Check
|
|
29
|
+
description: Validate project context in agent instruction files and help populate missing information.
|
|
30
|
+
category: LightSpec
|
|
31
|
+
tags: [lightspec, context, validation]
|
|
25
32
|
---`
|
|
26
33
|
};
|
|
27
34
|
export class ClaudeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
@@ -2,7 +2,8 @@ import { SlashCommandConfigurator } from './base.js';
|
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.clinerules/workflows/lightspec-proposal.md',
|
|
4
4
|
apply: '.clinerules/workflows/lightspec-apply.md',
|
|
5
|
-
archive: '.clinerules/workflows/lightspec-archive.md'
|
|
5
|
+
archive: '.clinerules/workflows/lightspec-archive.md',
|
|
6
|
+
'context-check': '.cline/commands/lightspec-context-check.md'
|
|
6
7
|
};
|
|
7
8
|
export class ClineSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
8
9
|
toolId = 'cline';
|
|
@@ -14,7 +15,8 @@ export class ClineSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
|
14
15
|
const descriptions = {
|
|
15
16
|
proposal: 'Scaffold a new LightSpec change and validate strictly.',
|
|
16
17
|
apply: 'Implement an approved LightSpec change and keep tasks in sync.',
|
|
17
|
-
archive: 'Archive a deployed LightSpec change and update specs.'
|
|
18
|
+
archive: 'Archive a deployed LightSpec change and update specs.',
|
|
19
|
+
'context-check': 'Validate project context in agent instruction files and help populate missing information.'
|
|
18
20
|
};
|
|
19
21
|
const description = descriptions[id];
|
|
20
22
|
return `# LightSpec: ${id.charAt(0).toUpperCase() + id.slice(1)}\n\n${description}`;
|
|
@@ -2,7 +2,8 @@ import { SlashCommandConfigurator } from './base.js';
|
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.codebuddy/commands/lightspec/proposal.md',
|
|
4
4
|
apply: '.codebuddy/commands/lightspec/apply.md',
|
|
5
|
-
archive: '.codebuddy/commands/lightspec/archive.md'
|
|
5
|
+
archive: '.codebuddy/commands/lightspec/archive.md',
|
|
6
|
+
'context-check': '.codebuddy/commands/lightspec-context-check.md'
|
|
6
7
|
};
|
|
7
8
|
const FRONTMATTER = {
|
|
8
9
|
proposal: `---
|
|
@@ -19,6 +20,12 @@ argument-hint: "[change-id]"
|
|
|
19
20
|
name: LightSpec: Archive
|
|
20
21
|
description: "Archive a deployed LightSpec change and update specs."
|
|
21
22
|
argument-hint: "[change-id]"
|
|
23
|
+
---`,
|
|
24
|
+
'context-check': `---
|
|
25
|
+
name: LightSpec: Context Check
|
|
26
|
+
description: Validate project context in agent instruction files and help populate missing information.
|
|
27
|
+
category: LightSpec
|
|
28
|
+
tags: [lightspec, context, validation]
|
|
22
29
|
---`
|
|
23
30
|
};
|
|
24
31
|
export class CodeBuddySlashCommandConfigurator extends SlashCommandConfigurator {
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { SlashCommandConfigurator } from "./base.js";
|
|
2
|
-
import { SlashCommandId } from "../../templates/index.js";
|
|
3
2
|
export declare class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
4
3
|
readonly toolId = "codex";
|
|
5
4
|
readonly isAvailable = true;
|
|
6
|
-
protected getRelativePath(id: SlashCommandId): string;
|
|
7
|
-
protected getFrontmatter(id: SlashCommandId): string | undefined;
|
|
8
|
-
private getGlobalPromptsDir;
|
|
9
|
-
generateAll(projectPath: string, _lightspecDir: string): Promise<string[]>;
|
|
10
|
-
updateExisting(projectPath: string, _lightspecDir: string): Promise<string[]>;
|
|
11
|
-
private updateFullFile;
|
|
12
|
-
resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string;
|
|
13
5
|
}
|
|
14
6
|
//# sourceMappingURL=codex.d.ts.map
|