Skip to content

Commit 56d57da

Browse files
briananderson1222Brian Anderson
andauthored
Amazon Q Developer integration (#160)
* feat: add Amazon Q Developer CLI integration - Add AmazonQSlashCommandConfigurator for .amazonq/prompts/ support - Register Amazon Q in SlashCommandRegistry and AI_TOOLS config - Generate slash commands compatible with Amazon Q CLI (@-syntax) - Update README.md with Amazon Q Developer in tools table * test: add Amazon Q Developer integration tests - Add init tests for Amazon Q prompt file creation and configuration detection - Add update tests for Amazon Q prompt refresh and missing file handling - Follow same test patterns as GitHub Copilot integration - Verify .amazonq/prompts/ directory structure and file content --------- Co-authored-by: Brian Anderson <[email protected]>
1 parent f56189a commit 56d57da

File tree

6 files changed

+170
-0
lines changed

6 files changed

+170
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
9797
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
9898
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
9999
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
100+
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
100101

101102
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
102103

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ export const AI_TOOLS: AIToolOption[] = [
2424
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
2525
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
2626
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
27+
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
2728
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
2829
];
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const FILE_PATHS: Record<SlashCommandId, string> = {
5+
proposal: '.amazonq/prompts/openspec-proposal.md',
6+
apply: '.amazonq/prompts/openspec-apply.md',
7+
archive: '.amazonq/prompts/openspec-archive.md'
8+
};
9+
10+
const FRONTMATTER: Record<SlashCommandId, string> = {
11+
proposal: `---
12+
description: Scaffold a new OpenSpec change and validate strictly.
13+
---
14+
15+
The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
16+
17+
<UserRequest>
18+
$ARGUMENTS
19+
</UserRequest>`,
20+
apply: `---
21+
description: Implement an approved OpenSpec change and keep tasks in sync.
22+
---
23+
24+
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
25+
26+
<ChangeId>
27+
$ARGUMENTS
28+
</ChangeId>`,
29+
archive: `---
30+
description: Archive a deployed OpenSpec change and update specs.
31+
---
32+
33+
The user wants to archive the following deployed change. Use the openspec instructions to archive the change and update specs.
34+
35+
<ChangeId>
36+
$ARGUMENTS
37+
</ChangeId>`
38+
};
39+
40+
export class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
41+
readonly toolId = 'amazon-q';
42+
readonly isAvailable = true;
43+
44+
protected getRelativePath(id: SlashCommandId): string {
45+
return FILE_PATHS[id];
46+
}
47+
48+
protected getFrontmatter(id: SlashCommandId): string {
49+
return FRONTMATTER[id];
50+
}
51+
}

src/core/configurators/slash/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
66
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
77
import { CodexSlashCommandConfigurator } from './codex.js';
88
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
9+
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
910

1011
export class SlashCommandRegistry {
1112
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -18,6 +19,7 @@ export class SlashCommandRegistry {
1819
const opencode = new OpenCodeSlashCommandConfigurator();
1920
const codex = new CodexSlashCommandConfigurator();
2021
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
22+
const amazonQ = new AmazonQSlashCommandConfigurator();
2123

2224
this.configurators.set(claude.toolId, claude);
2325
this.configurators.set(cursor.toolId, cursor);
@@ -26,6 +28,7 @@ export class SlashCommandRegistry {
2628
this.configurators.set(opencode.toolId, opencode);
2729
this.configurators.set(codex.toolId, codex);
2830
this.configurators.set(githubCopilot.toolId, githubCopilot);
31+
this.configurators.set(amazonQ.toolId, amazonQ);
2932
}
3033

3134
static register(configurator: SlashCommandConfigurator): void {

test/core/init.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,54 @@ describe('InitCommand', () => {
570570
);
571571
expect(githubCopilotChoice.configured).toBe(true);
572572
});
573+
574+
it('should create Amazon Q Developer prompt files with templates', async () => {
575+
queueSelections('amazon-q', DONE);
576+
577+
await initCommand.execute(testDir);
578+
579+
const proposalPath = path.join(
580+
testDir,
581+
'.amazonq/prompts/openspec-proposal.md'
582+
);
583+
const applyPath = path.join(
584+
testDir,
585+
'.amazonq/prompts/openspec-apply.md'
586+
);
587+
const archivePath = path.join(
588+
testDir,
589+
'.amazonq/prompts/openspec-archive.md'
590+
);
591+
592+
expect(await fileExists(proposalPath)).toBe(true);
593+
expect(await fileExists(applyPath)).toBe(true);
594+
expect(await fileExists(archivePath)).toBe(true);
595+
596+
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
597+
expect(proposalContent).toContain('---');
598+
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
599+
expect(proposalContent).toContain('$ARGUMENTS');
600+
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
601+
expect(proposalContent).toContain('**Guardrails**');
602+
603+
const applyContent = await fs.readFile(applyPath, 'utf-8');
604+
expect(applyContent).toContain('---');
605+
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
606+
expect(applyContent).toContain('$ARGUMENTS');
607+
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
608+
});
609+
610+
it('should mark Amazon Q Developer as already configured during extend mode', async () => {
611+
queueSelections('amazon-q', DONE, 'amazon-q', DONE);
612+
await initCommand.execute(testDir);
613+
await initCommand.execute(testDir);
614+
615+
const secondRunArgs = mockPrompt.mock.calls[1][0];
616+
const amazonQChoice = secondRunArgs.choices.find(
617+
(choice: any) => choice.value === 'amazon-q'
618+
);
619+
expect(amazonQChoice.configured).toBe(true);
620+
});
573621
});
574622

575623
describe('error handling', () => {

test/core/update.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,72 @@ Old body
377377
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
378378
});
379379

380+
it('should refresh existing Amazon Q Developer prompts', async () => {
381+
const aqPath = path.join(
382+
testDir,
383+
'.amazonq/prompts/openspec-apply.md'
384+
);
385+
await fs.mkdir(path.dirname(aqPath), { recursive: true });
386+
const initialContent = `---
387+
description: Implement an approved OpenSpec change and keep tasks in sync.
388+
---
389+
390+
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
391+
392+
<ChangeId>
393+
$ARGUMENTS
394+
</ChangeId>
395+
<!-- OPENSPEC:START -->
396+
Old body
397+
<!-- OPENSPEC:END -->`;
398+
await fs.writeFile(aqPath, initialContent);
399+
400+
const consoleSpy = vi.spyOn(console, 'log');
401+
402+
await updateCommand.execute(testDir);
403+
404+
const updatedContent = await fs.readFile(aqPath, 'utf-8');
405+
expect(updatedContent).toContain('**Guardrails**');
406+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
407+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
408+
expect(updatedContent).not.toContain('Old body');
409+
410+
expect(consoleSpy).toHaveBeenCalledWith(
411+
expect.stringContaining('.amazonq/prompts/openspec-apply.md')
412+
);
413+
414+
consoleSpy.mockRestore();
415+
});
416+
417+
it('should not create missing Amazon Q Developer prompts on update', async () => {
418+
const aqApply = path.join(
419+
testDir,
420+
'.amazonq/prompts/openspec-apply.md'
421+
);
422+
423+
// Only create apply; leave proposal and archive missing
424+
await fs.mkdir(path.dirname(aqApply), { recursive: true });
425+
await fs.writeFile(
426+
aqApply,
427+
'---\ndescription: Old\n---\n\nThe user wants to apply the following change.\n\n<ChangeId>\n $ARGUMENTS\n</ChangeId>\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
428+
);
429+
430+
await updateCommand.execute(testDir);
431+
432+
const aqProposal = path.join(
433+
testDir,
434+
'.amazonq/prompts/openspec-proposal.md'
435+
);
436+
const aqArchive = path.join(
437+
testDir,
438+
'.amazonq/prompts/openspec-archive.md'
439+
);
440+
441+
// Confirm they weren't created by update
442+
await expect(FileSystemUtils.fileExists(aqProposal)).resolves.toBe(false);
443+
await expect(FileSystemUtils.fileExists(aqArchive)).resolves.toBe(false);
444+
});
445+
380446
it('should preserve Windsurf content outside markers during update', async () => {
381447
const wsPath = path.join(
382448
testDir,

0 commit comments

Comments
 (0)