lightspec 0.4.1 → 0.5.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/README.md CHANGED
@@ -82,28 +82,39 @@ See the full comparison in [How LightSpec Compares](#how-lightspec-compares).
82
82
 
83
83
  ### Supported AI Tools
84
84
 
85
+ - AdaL
85
86
  - Amazon Q Developer
86
- - Antigravity
87
- - Auggie (Augment CLI)
87
+ - Augment
88
88
  - Claude Code
89
89
  - Cline
90
- - Codex
91
- - CodeBuddy Code (CLI)
92
- - Continue (VS Code / JetBrains / CLI)
90
+ - CodeBuddy
91
+ - Command Code
92
+ - Continue
93
93
  - CoStrict
94
+ - Cortex Code
94
95
  - Crush
95
- - Cursor
96
- - Factory Droid
97
- - Gemini CLI
98
- - GitHub Copilot
99
- - iFlow
96
+ - Droid
97
+ - iFlow CLI
98
+ - Junie
100
99
  - Kilo Code
100
+ - Kiro CLI
101
+ - Kode
102
+ - MCPJam
101
103
  - Mistral Vibe
102
- - OpenCode
103
- - Qoder (CLI)
104
+ - Mux
105
+ - Neovate
106
+ - OpenClaw
107
+ - OpenHands
108
+ - Pochi
109
+ - Pi
110
+ - Qoder
104
111
  - Qwen Code
105
- - RooCode
112
+ - Roo Code
113
+ - Trae
114
+ - Trae CN
115
+ - Universal agent skills (`.agents`, for Codex, Amp, VS Code, Zed, Warp, Goose, Cursor, Gemini CLI, GitHub Copilot, OpenCode, Replit, and similar assistants)
106
116
  - Windsurf
117
+ - Zencoder
107
118
  - Any AGENTS.md-compatible assistant (via Universal `AGENTS.md`)
108
119
 
109
120
  ### Install & Initialize
@@ -170,7 +181,7 @@ lightspec init
170
181
  ```
171
182
 
172
183
  **What happens during initialization:**
173
- - You'll be prompted to pick any natively supported AI tools (Claude Code, CodeBuddy, Cursor, OpenCode, Qoder,etc.); other assistants always rely on the shared `AGENTS.md` stub
184
+ - You'll be prompted to pick any natively supported AI tools using the current LightSpec provider IDs and install paths (for example `claude-code`, `cline`, `costrict`, `qoder`, `qwen-code`, `roo`, `universal`)
174
185
  - LightSpec automatically configures skills for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
175
186
  - A new `lightspec/` directory structure is created in your project
176
187
 
@@ -13,5 +13,7 @@ export interface AIToolOption {
13
13
  available: boolean;
14
14
  successLabel?: string;
15
15
  }
16
+ export declare const LEGACY_TOOL_ALIASES: Record<string, string>;
17
+ export declare function normalizeToolId(toolId: string): string;
16
18
  export declare const AI_TOOLS: AIToolOption[];
17
19
  //# sourceMappingURL=config.d.ts.map
@@ -3,29 +3,80 @@ export const LIGHTSPEC_MARKERS = {
3
3
  start: '<!-- LIGHTSPEC:START -->',
4
4
  end: '<!-- LIGHTSPEC:END -->'
5
5
  };
6
+ const UNIVERSAL_AGENTS_SUPPORTED_PROVIDERS = [
7
+ 'Codex',
8
+ 'Amp',
9
+ 'VS Code',
10
+ 'Zed',
11
+ 'Warp',
12
+ 'Goose',
13
+ ];
14
+ const UNIVERSAL_AGENTS_PROVIDER_PREVIEW_COUNT = 5;
15
+ const universalAgentsProviderPreview = UNIVERSAL_AGENTS_SUPPORTED_PROVIDERS.slice(0, UNIVERSAL_AGENTS_PROVIDER_PREVIEW_COUNT).join(', ');
16
+ const universalAgentsProviderSuffix = UNIVERSAL_AGENTS_SUPPORTED_PROVIDERS.length > UNIVERSAL_AGENTS_PROVIDER_PREVIEW_COUNT
17
+ ? ', ...'
18
+ : '';
19
+ const UNIVERSAL_AGENTS_OPTION_LABEL = `Universal agent skills (${universalAgentsProviderPreview}${universalAgentsProviderSuffix})`;
20
+ export const LEGACY_TOOL_ALIASES = {
21
+ agents: 'universal',
22
+ amp: 'universal',
23
+ antigravity: 'universal',
24
+ auggie: 'augment',
25
+ claude: 'claude-code',
26
+ codex: 'universal',
27
+ cursor: 'universal',
28
+ deepagents: 'universal',
29
+ factory: 'droid',
30
+ gemini: 'universal',
31
+ 'gemini-cli': 'universal',
32
+ goose: 'universal',
33
+ 'github-copilot': 'universal',
34
+ iflow: 'iflow-cli',
35
+ 'kimi-cli': 'universal',
36
+ kilocode: 'kilo',
37
+ opencode: 'universal',
38
+ qwen: 'qwen-code',
39
+ replit: 'universal',
40
+ roocode: 'roo',
41
+ warp: 'universal',
42
+ };
43
+ export function normalizeToolId(toolId) {
44
+ const normalized = toolId.trim().toLowerCase();
45
+ return LEGACY_TOOL_ALIASES[normalized] ?? normalized;
46
+ }
6
47
  export const AI_TOOLS = [
48
+ { name: UNIVERSAL_AGENTS_OPTION_LABEL, value: 'universal', available: true, successLabel: 'Universal agent skills' },
7
49
  { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
8
- { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity' },
9
- { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' },
10
- { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
50
+ { name: 'Augment', value: 'augment', available: true, successLabel: 'Augment' },
51
+ { name: 'Claude Code', value: 'claude-code', available: true, successLabel: 'Claude Code' },
11
52
  { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
12
- { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
13
- { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
14
- { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)' },
53
+ { name: 'CodeBuddy', value: 'codebuddy', available: true, successLabel: 'CodeBuddy' },
54
+ { name: 'Command Code', value: 'command-code', available: true, successLabel: 'Command Code' },
55
+ { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue' },
15
56
  { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
57
+ { name: 'Cortex Code', value: 'cortex', available: true, successLabel: 'Cortex Code' },
16
58
  { name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
17
- { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
18
- { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
19
- { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
20
- { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
21
- { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' },
22
- { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
59
+ { name: 'Droid', value: 'droid', available: true, successLabel: 'Droid' },
60
+ { name: 'iFlow CLI', value: 'iflow-cli', available: true, successLabel: 'iFlow CLI' },
61
+ { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie' },
62
+ { name: 'Kilo Code', value: 'kilo', available: true, successLabel: 'Kilo Code' },
63
+ { name: 'Kiro CLI', value: 'kiro-cli', available: true, successLabel: 'Kiro CLI' },
64
+ { name: 'Kode', value: 'kode', available: true, successLabel: 'Kode' },
65
+ { name: 'MCPJam', value: 'mcpjam', available: true, successLabel: 'MCPJam' },
23
66
  { name: 'Mistral Vibe', value: 'mistral-vibe', available: true, successLabel: 'Mistral Vibe' },
24
- { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
25
- { name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
26
- { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' },
27
- { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
67
+ { name: 'Mux', value: 'mux', available: true, successLabel: 'Mux' },
68
+ { name: 'Neovate', value: 'neovate', available: true, successLabel: 'Neovate' },
69
+ { name: 'OpenClaw', value: 'openclaw', available: true, successLabel: 'OpenClaw' },
70
+ { name: 'OpenHands', value: 'openhands', available: true, successLabel: 'OpenHands' },
71
+ { name: 'Pochi', value: 'pochi', available: true, successLabel: 'Pochi' },
72
+ { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi' },
73
+ { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder' },
74
+ { name: 'Qwen Code', value: 'qwen-code', available: true, successLabel: 'Qwen Code' },
75
+ { name: 'Roo Code', value: 'roo', available: true, successLabel: 'Roo Code' },
76
+ { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae' },
77
+ { name: 'Trae CN', value: 'trae-cn', available: true, successLabel: 'Trae CN' },
28
78
  { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
29
- { name: 'AGENTS.md + .agents (works with Amp, VS Code, …)', value: 'agents', available: true, successLabel: 'your AGENTS.md-compatible assistant' }
79
+ { name: 'Zencoder', value: 'zencoder', available: true, successLabel: 'Zencoder' },
80
+ { name: 'AdaL', value: 'adal', available: true, successLabel: 'AdaL' },
30
81
  ];
31
82
  //# sourceMappingURL=config.js.map
@@ -6,6 +6,7 @@ import { QoderConfigurator } from './qoder.js';
6
6
  import { IflowConfigurator } from './iflow.js';
7
7
  import { AgentsStandardConfigurator } from './agents.js';
8
8
  import { QwenConfigurator } from './qwen.js';
9
+ import { normalizeToolId } from '../config.js';
9
10
  export class ToolRegistry {
10
11
  static tools = new Map();
11
12
  static {
@@ -18,20 +19,20 @@ export class ToolRegistry {
18
19
  const agentsConfigurator = new AgentsStandardConfigurator();
19
20
  const qwenConfigurator = new QwenConfigurator();
20
21
  // Register with the ID that matches the checkbox value
21
- this.tools.set('claude', claudeConfigurator);
22
+ this.tools.set('claude-code', claudeConfigurator);
22
23
  this.tools.set('cline', clineConfigurator);
23
24
  this.tools.set('codebuddy', codeBuddyConfigurator);
24
25
  this.tools.set('costrict', costrictConfigurator);
25
26
  this.tools.set('qoder', qoderConfigurator);
26
- this.tools.set('iflow', iflowConfigurator);
27
- this.tools.set('agents', agentsConfigurator);
28
- this.tools.set('qwen', qwenConfigurator);
27
+ this.tools.set('iflow-cli', iflowConfigurator);
28
+ this.tools.set('universal', agentsConfigurator);
29
+ this.tools.set('qwen-code', qwenConfigurator);
29
30
  }
30
31
  static register(tool) {
31
32
  this.tools.set(tool.name.toLowerCase().replace(/\s+/g, '-'), tool);
32
33
  }
33
34
  static get(toolId) {
34
- return this.tools.get(toolId);
35
+ return this.tools.get(normalizeToolId(toolId));
35
36
  }
36
37
  static getAll() {
37
38
  return Array.from(this.tools.values());
@@ -5,6 +5,7 @@ export interface AgentSkillTarget {
5
5
  kind: 'skill';
6
6
  }
7
7
  export type SkillInstallLocation = 'project' | 'home';
8
+ export declare function normalizeAgentSkillToolId(toolId: string): string;
8
9
  export declare const AGENT_SKILL_TOOL_IDS: readonly string[];
9
10
  export declare class AgentSkillConfigurator {
10
11
  readonly toolId: string;
@@ -17,8 +18,14 @@ export declare class AgentSkillConfigurator {
17
18
  updateExisting(projectPath: string, _lightspecDir: string): Promise<string[]>;
18
19
  protected getBody(id: AgentSkillId): string;
19
20
  resolveAbsolutePath(projectPath: string, id: AgentSkillId): string;
21
+ resolveExistingAbsolutePaths(projectPath: string, id: AgentSkillId): Array<{
22
+ absolutePath: string;
23
+ relativePath: string;
24
+ }>;
20
25
  private getRelativeSkillPath;
21
- private getToolRoot;
26
+ private getAllRelativeSkillPaths;
27
+ private resolvePathFromRelative;
28
+ private getDescriptor;
22
29
  private getHomeRootPath;
23
30
  private getSkillName;
24
31
  private buildSkillFile;
@@ -1,47 +1,210 @@
1
1
  import os from 'os';
2
- import path from 'path';
3
2
  import { FileSystemUtils } from '../../../utils/file-system.js';
4
3
  import { TemplateManager } from '../../templates/index.js';
5
- import { LIGHTSPEC_MARKERS } from '../../config.js';
4
+ import { LIGHTSPEC_MARKERS, normalizeToolId } from '../../config.js';
6
5
  const ALL_SKILL_IDS = ['proposal', 'apply', 'archive', 'agentsmd-check'];
7
- const TOOL_SKILL_ROOTS = {
8
- 'amazon-q': '.amazonq',
9
- antigravity: '.antigravity',
10
- agents: '.agents',
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',
6
+ const TOOL_SKILL_DESCRIPTORS = {
7
+ 'amazon-q': {
8
+ projectSkillDir: '.amazonq/skills',
9
+ homeSkillDir: '.amazonq/skills',
10
+ },
11
+ adal: {
12
+ projectSkillDir: '.adal/skills',
13
+ homeSkillDir: '.adal/skills',
14
+ },
15
+ augment: {
16
+ projectSkillDir: '.augment/skills',
17
+ homeSkillDir: '.augment/skills',
18
+ aliases: ['auggie'],
19
+ legacyProjectSkillDirs: ['.auggie/skills'],
20
+ legacyHomeSkillDirs: ['.auggie/skills'],
21
+ },
22
+ 'claude-code': {
23
+ projectSkillDir: '.claude/skills',
24
+ homeSkillDir: '.claude/skills',
25
+ aliases: ['claude'],
26
+ },
27
+ cline: {
28
+ projectSkillDir: '.agents/skills',
29
+ homeSkillDir: '.agents/skills',
30
+ legacyProjectSkillDirs: ['.cline/skills'],
31
+ legacyHomeSkillDirs: ['.cline/skills'],
32
+ },
33
+ codebuddy: {
34
+ projectSkillDir: '.codebuddy/skills',
35
+ homeSkillDir: '.codebuddy/skills',
36
+ },
37
+ 'command-code': {
38
+ projectSkillDir: '.commandcode/skills',
39
+ homeSkillDir: '.commandcode/skills',
40
+ },
41
+ continue: {
42
+ projectSkillDir: '.continue/skills',
43
+ homeSkillDir: '.continue/skills',
44
+ },
45
+ costrict: {
46
+ projectSkillDir: '.cospec/lightspec/skills',
47
+ homeSkillDir: '.cospec/lightspec/skills',
48
+ },
49
+ cortex: {
50
+ projectSkillDir: '.cortex/skills',
51
+ homeSkillDir: '.snowflake/cortex/skills',
52
+ },
53
+ crush: {
54
+ projectSkillDir: '.crush/skills',
55
+ homeSkillDir: '.config/crush/skills',
56
+ legacyHomeSkillDirs: ['.crush/skills'],
57
+ },
58
+ droid: {
59
+ projectSkillDir: '.factory/skills',
60
+ homeSkillDir: '.factory/skills',
61
+ aliases: ['factory'],
62
+ },
63
+ 'iflow-cli': {
64
+ projectSkillDir: '.iflow/skills',
65
+ homeSkillDir: '.iflow/skills',
66
+ aliases: ['iflow'],
67
+ },
68
+ junie: {
69
+ projectSkillDir: '.junie/skills',
70
+ homeSkillDir: '.junie/skills',
71
+ },
72
+ kilo: {
73
+ projectSkillDir: '.kilocode/skills',
74
+ homeSkillDir: '.kilocode/skills',
75
+ aliases: ['kilocode'],
76
+ },
77
+ 'kiro-cli': {
78
+ projectSkillDir: '.kiro/skills',
79
+ homeSkillDir: '.kiro/skills',
80
+ },
81
+ kode: {
82
+ projectSkillDir: '.kode/skills',
83
+ homeSkillDir: '.kode/skills',
84
+ },
85
+ mcpjam: {
86
+ projectSkillDir: '.mcpjam/skills',
87
+ homeSkillDir: '.mcpjam/skills',
88
+ },
89
+ 'mistral-vibe': {
90
+ projectSkillDir: '.vibe/skills',
91
+ homeSkillDir: '.vibe/skills',
92
+ },
93
+ mux: {
94
+ projectSkillDir: '.mux/skills',
95
+ homeSkillDir: '.mux/skills',
96
+ },
97
+ neovate: {
98
+ projectSkillDir: '.neovate/skills',
99
+ homeSkillDir: '.neovate/skills',
100
+ },
101
+ openclaw: {
102
+ projectSkillDir: 'skills',
103
+ homeSkillDir: '.openclaw/skills',
104
+ },
105
+ openhands: {
106
+ projectSkillDir: '.openhands/skills',
107
+ homeSkillDir: '.openhands/skills',
108
+ },
109
+ pochi: {
110
+ projectSkillDir: '.pochi/skills',
111
+ homeSkillDir: '.pochi/skills',
112
+ },
113
+ pi: {
114
+ projectSkillDir: '.pi/skills',
115
+ homeSkillDir: '.pi/agent/skills',
116
+ },
117
+ qoder: {
118
+ projectSkillDir: '.qoder/skills',
119
+ homeSkillDir: '.qoder/skills',
120
+ },
121
+ 'qwen-code': {
122
+ projectSkillDir: '.qwen/skills',
123
+ homeSkillDir: '.qwen/skills',
124
+ aliases: ['qwen'],
125
+ },
126
+ roo: {
127
+ projectSkillDir: '.roo/skills',
128
+ homeSkillDir: '.roo/skills',
129
+ aliases: ['roocode'],
130
+ legacyProjectSkillDirs: ['.roocode/skills'],
131
+ legacyHomeSkillDirs: ['.roocode/skills'],
132
+ },
133
+ 'trae-cn': {
134
+ projectSkillDir: '.trae/skills',
135
+ homeSkillDir: '.trae-cn/skills',
136
+ },
137
+ trae: {
138
+ projectSkillDir: '.trae/skills',
139
+ homeSkillDir: '.trae/skills',
140
+ },
141
+ universal: {
142
+ projectSkillDir: '.agents/skills',
143
+ homeSkillDir: '.config/agents/skills',
144
+ aliases: [
145
+ 'agents',
146
+ 'amp',
147
+ 'antigravity',
148
+ 'codex',
149
+ 'cursor',
150
+ 'deepagents',
151
+ 'gemini',
152
+ 'gemini-cli',
153
+ 'github-copilot',
154
+ 'goose',
155
+ 'kimi-cli',
156
+ 'opencode',
157
+ 'replit',
158
+ 'warp',
159
+ ],
160
+ legacyProjectSkillDirs: [
161
+ '.antigravity/skills',
162
+ '.cursor/skills',
163
+ '.codex/skills',
164
+ '.gemini/skills',
165
+ '.github/copilot/skills',
166
+ '.opencode/skills',
167
+ ],
168
+ legacyHomeSkillDirs: [
169
+ '.agents/skills',
170
+ '.antigravity/skills',
171
+ '.cursor/skills',
172
+ '.codex/skills',
173
+ '.deepagents/agent/skills',
174
+ '.gemini/skills',
175
+ '.github/copilot/skills',
176
+ '.copilot/skills',
177
+ '.opencode/skills',
178
+ ],
179
+ },
180
+ windsurf: {
181
+ projectSkillDir: '.windsurf/skills',
182
+ homeSkillDir: '.codeium/windsurf/skills',
183
+ legacyHomeSkillDirs: ['.windsurf/skills'],
184
+ },
185
+ zencoder: {
186
+ projectSkillDir: '.zencoder/skills',
187
+ homeSkillDir: '.zencoder/skills',
188
+ },
31
189
  };
32
- export const AGENT_SKILL_TOOL_IDS = Object.freeze(Object.keys(TOOL_SKILL_ROOTS));
190
+ const TOOL_ID_ALIASES = Object.freeze(Object.fromEntries(Object.entries(TOOL_SKILL_DESCRIPTORS).flatMap(([toolId, descriptor]) => (descriptor.aliases ?? []).map((alias) => [alias, toolId]))));
191
+ export function normalizeAgentSkillToolId(toolId) {
192
+ const normalized = normalizeToolId(toolId);
193
+ return TOOL_ID_ALIASES[normalized] ?? normalized;
194
+ }
195
+ export const AGENT_SKILL_TOOL_IDS = Object.freeze(Object.keys(TOOL_SKILL_DESCRIPTORS));
33
196
  const TOOL_BODY_SUFFIX = {
34
- factory: '\n\n$ARGUMENTS',
197
+ droid: '\n\n$ARGUMENTS',
35
198
  };
36
199
  export class AgentSkillConfigurator {
37
200
  toolId;
38
201
  isAvailable;
39
202
  installLocation = 'project';
40
203
  constructor(toolId, isAvailable = true) {
41
- this.toolId = toolId;
204
+ this.toolId = normalizeAgentSkillToolId(toolId);
42
205
  this.isAvailable = isAvailable;
43
- if (!TOOL_SKILL_ROOTS[this.toolId]) {
44
- throw new Error(`No skill root directory configured for tool '${this.toolId}'`);
206
+ if (!TOOL_SKILL_DESCRIPTORS[this.toolId]) {
207
+ throw new Error(`No skill root directory configured for tool '${toolId}'`);
45
208
  }
46
209
  }
47
210
  setInstallLocation(location) {
@@ -74,11 +237,14 @@ export class AgentSkillConfigurator {
74
237
  async updateExisting(projectPath, _lightspecDir) {
75
238
  const updated = [];
76
239
  for (const target of this.getTargets()) {
77
- const filePath = this.resolveAbsolutePath(projectPath, target.id);
78
- if (await FileSystemUtils.fileExists(filePath)) {
240
+ const candidatePaths = this.resolveExistingAbsolutePaths(projectPath, target.id);
241
+ for (const candidate of candidatePaths) {
242
+ if (!await FileSystemUtils.fileExists(candidate.absolutePath)) {
243
+ continue;
244
+ }
79
245
  const body = this.getBody(target.id);
80
- await this.updateBody(filePath, body);
81
- updated.push(target.path);
246
+ await this.updateBody(candidate.absolutePath, body);
247
+ updated.push(candidate.relativePath);
82
248
  }
83
249
  }
84
250
  return updated;
@@ -90,40 +256,54 @@ export class AgentSkillConfigurator {
90
256
  }
91
257
  resolveAbsolutePath(projectPath, id) {
92
258
  const relativePath = this.getRelativeSkillPath(id);
259
+ return this.resolvePathFromRelative(projectPath, relativePath);
260
+ }
261
+ resolveExistingAbsolutePaths(projectPath, id) {
262
+ const relativePaths = this.getAllRelativeSkillPaths(id);
263
+ return relativePaths.map((relativePath) => ({
264
+ absolutePath: this.resolvePathFromRelative(projectPath, relativePath),
265
+ relativePath,
266
+ }));
267
+ }
268
+ getRelativeSkillPath(id) {
269
+ const descriptor = this.getDescriptor();
270
+ const skillName = this.getSkillName(id);
271
+ const skillDir = this.installLocation === 'project'
272
+ ? descriptor.projectSkillDir
273
+ : descriptor.homeSkillDir;
274
+ return `${skillDir}/${skillName}/SKILL.md`;
275
+ }
276
+ getAllRelativeSkillPaths(id) {
277
+ const descriptor = this.getDescriptor();
278
+ const skillName = this.getSkillName(id);
279
+ const skillDirs = this.installLocation === 'project'
280
+ ? [descriptor.projectSkillDir, ...(descriptor.legacyProjectSkillDirs ?? [])]
281
+ : [descriptor.homeSkillDir, ...(descriptor.legacyHomeSkillDirs ?? [])];
282
+ return Array.from(new Set(skillDirs.map((dir) => `${dir}/${skillName}/SKILL.md`)));
283
+ }
284
+ resolvePathFromRelative(projectPath, relativePath) {
93
285
  if (this.installLocation === 'project') {
94
286
  return FileSystemUtils.joinPath(projectPath, relativePath);
95
287
  }
96
288
  const homeRoot = this.getHomeRootPath();
97
- const rootPrefix = this.getToolRoot();
98
- const normalizedRelativePath = FileSystemUtils.toPosixPath(relativePath);
99
- if (!normalizedRelativePath.startsWith(`${rootPrefix}/`)) {
100
- throw new Error(`Skill path '${relativePath}' does not match expected root '${rootPrefix}' for ${this.toolId}`);
101
- }
102
- const relativeUnderRoot = normalizedRelativePath.slice(rootPrefix.length + 1);
103
- return FileSystemUtils.joinPath(homeRoot, relativeUnderRoot);
104
- }
105
- getRelativeSkillPath(id) {
106
- const root = this.getToolRoot();
107
- const skillName = this.getSkillName(id);
108
- return `${root}/skills/${skillName}/SKILL.md`;
289
+ return FileSystemUtils.joinPath(homeRoot, relativePath);
109
290
  }
110
- getToolRoot() {
111
- const root = TOOL_SKILL_ROOTS[this.toolId];
112
- if (!root) {
291
+ getDescriptor() {
292
+ const descriptor = TOOL_SKILL_DESCRIPTORS[this.toolId];
293
+ if (!descriptor) {
113
294
  throw new Error(`No skill root directory configured for tool '${this.toolId}'`);
114
295
  }
115
- return root;
296
+ return descriptor;
116
297
  }
117
298
  getHomeRootPath() {
118
- if (this.toolId === 'codex') {
299
+ const descriptor = this.getDescriptor();
300
+ if (descriptor.homeBase === 'codex-home') {
119
301
  const codexHome = process.env.CODEX_HOME?.trim();
120
302
  return codexHome && codexHome.length > 0
121
303
  ? codexHome
122
304
  : FileSystemUtils.joinPath(os.homedir(), '.codex');
123
305
  }
124
- const toolRoot = this.getToolRoot();
125
- const trimmed = toolRoot.startsWith('./') ? toolRoot.slice(2) : toolRoot;
126
- return path.join(os.homedir(), trimmed);
306
+ return os.homedir();
127
307
  }
128
308
  getSkillName(id) {
129
309
  return `lightspec-${id}`;
@@ -1,4 +1,4 @@
1
- import { AGENT_SKILL_TOOL_IDS, AgentSkillConfigurator, } from './base.js';
1
+ import { AGENT_SKILL_TOOL_IDS, AgentSkillConfigurator, normalizeAgentSkillToolId, } from './base.js';
2
2
  export class AgentSkillRegistry {
3
3
  static configurators = new Map();
4
4
  static {
@@ -10,7 +10,7 @@ export class AgentSkillRegistry {
10
10
  this.configurators.set(configurator.toolId, configurator);
11
11
  }
12
12
  static get(toolId) {
13
- return this.configurators.get(toolId);
13
+ return this.configurators.get(normalizeAgentSkillToolId(toolId));
14
14
  }
15
15
  static getAll() {
16
16
  return Array.from(this.configurators.values());
@@ -40,6 +40,7 @@ export declare class InitCommand {
40
40
  private getSkillInstallLocation;
41
41
  private getSelectedTools;
42
42
  private resolveToolsArg;
43
+ private normalizeSelectedTools;
43
44
  private promptForAITools;
44
45
  private getExistingToolStates;
45
46
  private isToolConfigured;
package/dist/core/init.js CHANGED
@@ -7,7 +7,7 @@ import { FileSystemUtils } from '../utils/file-system.js';
7
7
  import { TemplateManager } from './templates/index.js';
8
8
  import { ToolRegistry } from './configurators/registry.js';
9
9
  import { AgentSkillRegistry } from './configurators/skills/registry.js';
10
- import { AI_TOOLS, LIGHTSPEC_DIR_NAME, LIGHTSPEC_MARKERS, } from './config.js';
10
+ import { AI_TOOLS, LIGHTSPEC_DIR_NAME, LIGHTSPEC_MARKERS, normalizeToolId, } from './config.js';
11
11
  import { PALETTE } from './styles/palette.js';
12
12
  const PROGRESS_SPINNER = {
13
13
  interval: 80,
@@ -41,8 +41,6 @@ const parseToolLabel = (raw) => {
41
41
  };
42
42
  const isSelectableChoice = (choice) => choice.selectable;
43
43
  const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
44
- const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
45
- const LIST_SPACER_VALUE = '__list-spacer__';
46
44
  const toolSelectionWizard = createPrompt((config, done) => {
47
45
  const totalSteps = 3;
48
46
  const [step, setStep] = useState('intro');
@@ -340,10 +338,10 @@ export class InitCommand {
340
338
  async getSelectedTools(existingTools, extendMode) {
341
339
  const nonInteractiveSelection = this.resolveToolsArg();
342
340
  if (nonInteractiveSelection !== null) {
343
- return nonInteractiveSelection;
341
+ return this.normalizeSelectedTools(nonInteractiveSelection);
344
342
  }
345
343
  // Fall back to interactive mode
346
- return this.promptForAITools(existingTools, extendMode);
344
+ return this.normalizeSelectedTools(await this.promptForAITools(existingTools, extendMode));
347
345
  }
348
346
  resolveToolsArg() {
349
347
  if (typeof this.toolsArg === 'undefined') {
@@ -371,7 +369,7 @@ export class InitCommand {
371
369
  if (tokens.length === 0) {
372
370
  throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
373
371
  }
374
- const normalizedTokens = tokens.map((token) => token.toLowerCase());
372
+ const normalizedTokens = tokens.map((token) => normalizeToolId(token));
375
373
  if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
376
374
  throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
377
375
  }
@@ -387,6 +385,18 @@ export class InitCommand {
387
385
  }
388
386
  return deduped;
389
387
  }
388
+ normalizeSelectedTools(selected) {
389
+ const availableSet = new Set(AI_TOOLS.filter((tool) => tool.available).map((tool) => tool.value));
390
+ const normalized = [];
391
+ for (const toolId of selected) {
392
+ const canonical = normalizeToolId(toolId);
393
+ if (!availableSet.has(canonical) || normalized.includes(canonical)) {
394
+ continue;
395
+ }
396
+ normalized.push(canonical);
397
+ }
398
+ return normalized;
399
+ }
390
400
  async promptForAITools(existingTools, extendMode) {
391
401
  const availableTools = AI_TOOLS.filter((tool) => tool.available);
392
402
  const baseMessage = extendMode
@@ -396,7 +406,7 @@ export class InitCommand {
396
406
  ? availableTools
397
407
  .filter((tool) => existingTools[tool.value])
398
408
  .map((tool) => tool.value)
399
- : [];
409
+ : ['universal'];
400
410
  const initialSelected = Array.from(new Set(initialNativeSelection));
401
411
  const choices = [
402
412
  {
@@ -414,34 +424,6 @@ export class InitCommand {
414
424
  configured: Boolean(existingTools[tool.value]),
415
425
  selectable: true,
416
426
  })),
417
- ...(availableTools.length
418
- ? [
419
- {
420
- kind: 'info',
421
- value: LIST_SPACER_VALUE,
422
- label: { primary: '' },
423
- selectable: false,
424
- },
425
- ]
426
- : []),
427
- {
428
- kind: 'heading',
429
- value: OTHER_TOOLS_HEADING_VALUE,
430
- label: {
431
- primary: 'Other tools (use Universal AGENTS.md for Amp, VS Code, GitHub Copilot, …)',
432
- },
433
- selectable: false,
434
- },
435
- {
436
- kind: 'option',
437
- value: ROOT_STUB_CHOICE_VALUE,
438
- label: {
439
- primary: 'Universal AGENTS.md',
440
- annotation: 'always available',
441
- },
442
- configured: extendMode,
443
- selectable: true,
444
- },
445
427
  ];
446
428
  return this.prompt({
447
429
  extendMode,
@@ -485,10 +467,15 @@ export class InitCommand {
485
467
  const skillConfigurator = AgentSkillRegistry.get(toolId);
486
468
  if (skillConfigurator) {
487
469
  for (const target of skillConfigurator.getTargets()) {
488
- const absolute = skillConfigurator.resolveAbsolutePath(projectPath, target.id);
489
- if ((await FileSystemUtils.fileExists(absolute)) && (await fileHasMarkers(absolute))) {
490
- hasSkills = true;
491
- break; // At least one file with markers is sufficient
470
+ const candidates = skillConfigurator.resolveExistingAbsolutePaths(projectPath, target.id);
471
+ for (const candidate of candidates) {
472
+ if ((await FileSystemUtils.fileExists(candidate.absolutePath)) && (await fileHasMarkers(candidate.absolutePath))) {
473
+ hasSkills = true;
474
+ break; // At least one file with markers is sufficient
475
+ }
476
+ }
477
+ if (hasSkills) {
478
+ break;
492
479
  }
493
480
  }
494
481
  }
@@ -560,7 +547,7 @@ export class InitCommand {
560
547
  return rootStubStatus;
561
548
  }
562
549
  async configureRootAgentsStub(projectPath, lightspecDir) {
563
- const configurator = ToolRegistry.get('agents');
550
+ const configurator = ToolRegistry.get('universal');
564
551
  if (!configurator || !configurator.isAvailable) {
565
552
  return 'skipped';
566
553
  }
@@ -1,2 +1,2 @@
1
- export declare const agentsRootStubTemplate = "# LightSpec Instructions\n\nThese instructions are for AI assistants working in this project.\n\n**If (and only if) the feature is complex** (new capability, breaking change, architecture/performance/security work), follow these steps:\n\n- [ ] Suggest switching to `/plan` mode before writing the proposal if it's not already the case\n- [ ] Run `lightspec-proposal`.\n- [ ] Do not implement the feature until the proposal is approved by the user. Implementation MUST be operated using `lightspec-apply` to ensure the implementation is properly tracked and documented.\n- [ ] If unsure which skill to run, list installed skills.\n\nKeep this managed block so 'lightspec update' can refresh the instructions.\n";
1
+ export declare const agentsRootStubTemplate = "# LightSpec Instructions\n\nIF (and only if) the user message:\n - Mentions planning or proposals (words like proposal, spec, change, plan)\n - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work\n - Sounds ambiguous and you need the authoritative spec before coding\n\nTHEN: \nDo the following:\n- [ ] Suggest switching to `/plan` mode before writing the proposal if it's not already the case\n- [ ] When the user approves the plan and terminates planning mode, run `lightspec-apply` to implement the approved proposal.\n- [ ] Run `lightspec-proposal`.\n- [ ] Do not implement the feature until the proposal is approved by the user. Implementation MUST be operated using `lightspec-apply` to ensure the implementation is properly tracked and documented.\n- [ ] If unsure which skill to run, list installed skills.\n\nKeep this managed block so 'lightspec update' can refresh the instructions.\n";
2
2
  //# sourceMappingURL=agents-root-stub.d.ts.map
@@ -1,10 +1,14 @@
1
1
  export const agentsRootStubTemplate = `# LightSpec Instructions
2
2
 
3
- These instructions are for AI assistants working in this project.
4
-
5
- **If (and only if) the feature is complex** (new capability, breaking change, architecture/performance/security work), follow these steps:
3
+ IF (and only if) the user message:
4
+ - Mentions planning or proposals (words like proposal, spec, change, plan)
5
+ - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
6
+ - Sounds ambiguous and you need the authoritative spec before coding
6
7
 
8
+ THEN:
9
+ Do the following:
7
10
  - [ ] Suggest switching to \`/plan\` mode before writing the proposal if it's not already the case
11
+ - [ ] When the user approves the plan and terminates planning mode, run \`lightspec-apply\` to implement the approved proposal.
8
12
  - [ ] Run \`lightspec-proposal\`.
9
13
  - [ ] Do not implement the feature until the proposal is approved by the user. Implementation MUST be operated using \`lightspec-apply\` to ensure the implementation is properly tracked and documented.
10
14
  - [ ] If unsure which skill to run, list installed skills.
@@ -71,7 +71,7 @@ export class UpdateCommand {
71
71
  }
72
72
  if (updatedSkillFiles.length > 0) {
73
73
  // Normalize to forward slashes for cross-platform log consistency
74
- const normalized = updatedSkillFiles.map((p) => FileSystemUtils.toPosixPath(p));
74
+ const normalized = Array.from(new Set(updatedSkillFiles.map((p) => FileSystemUtils.toPosixPath(p))));
75
75
  summaryParts.push(`Updated skills: ${normalized.join(', ')}`);
76
76
  }
77
77
  const failedItems = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightspec",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "lightspec",