Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions copilot/prompts/architecture.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: architecture
description: Defines the System Architecture - stack, DBs, infra
tools:
- codebase
- editFiles
---

{{INLINE:commands/architecture.md}}
10 changes: 10 additions & 0 deletions copilot/prompts/implement.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: implement
description: Runs tasks - delegates coding to sub-agents, tracks progress
tools:
- codebase
- editFiles
- runInTerminal
---

{{INLINE:commands/implement.md}}
9 changes: 9 additions & 0 deletions copilot/prompts/product.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: product
description: Defines the Product - what, why, and for who
tools:
- codebase
- editFiles
---

{{INLINE:commands/product.md}}
9 changes: 9 additions & 0 deletions copilot/prompts/roadmap.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: roadmap
description: Builds the Product Roadmap - features and their order
tools:
- codebase
- editFiles
---

{{INLINE:commands/roadmap.md}}
9 changes: 9 additions & 0 deletions copilot/prompts/spec.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: spec
description: Creates the Functional Spec - what the feature does for the user
tools:
- codebase
- editFiles
---

{{INLINE:commands/spec.md}}
9 changes: 9 additions & 0 deletions copilot/prompts/tasks.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: tasks
description: Breaks the Tech Spec into a task list for engineers
tools:
- codebase
- editFiles
---

{{INLINE:commands/tasks.md}}
9 changes: 9 additions & 0 deletions copilot/prompts/tech.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: tech
description: Creates the Technical Spec - how the feature will be built
tools:
- codebase
- editFiles
---

{{INLINE:commands/tech.md}}
14 changes: 14 additions & 0 deletions copilot/vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"chat.promptFilesRecommendations": {
"product": true,
"roadmap": true,
"architecture": true,
"spec": true,
"tech": true,
"tasks": true,
"implement": true
},
"chat.tools.terminal.autoApprove": {
".awos/scripts/": true
}
}
60 changes: 58 additions & 2 deletions src/config/setup-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,118 @@

/**
* Directories to create during setup
* tools: array of tool names this directory applies to ('claude', 'copilot')
* When tool='all' is selected, all directories are included automatically
*/
const directories = [
{
path: '.awos',
description: 'AWOS configuration directory',
tools: ['claude', 'copilot'],
},
{
path: '.claude',
description: 'Claude configuration directory',
tools: ['claude'],
},
{
path: '.awos',
description: 'awos configuration directory',
path: '.github/prompts',
description: 'Copilot prompts directory',
tools: ['copilot'],
},
{
path: '.vscode',
description: 'VS Code configuration directory',
tools: ['copilot'],
},
{
path: 'context',
description: 'A home for project documentation',
tools: ['claude', 'copilot'],
},
{
path: 'context/product',
description: 'Global product definitions',
tools: ['claude', 'copilot'],
},
{
path: 'context/spec',
description: 'A home for specifications',
tools: ['claude', 'copilot'],
},
];

/**
* File copy operations to perform during setup
* Each operation defines what to copy from source to destination
* tools: array of tool names this operation applies to ('claude', 'copilot')
* When tool='all' is selected, all operations are included automatically
*/
const copyOperations = [
// Shared AWOS core (always installed for references)
{
source: 'commands',
destination: '.awos/commands',
patterns: ['*'],
description: 'AWOS command prompts',
tools: ['claude', 'copilot'],
},
{
source: 'templates',
destination: '.awos/templates',
patterns: ['*'],
description: 'AWOS templates',
tools: ['claude', 'copilot'],
},
{
source: 'scripts',
destination: '.awos/scripts',
patterns: ['*'],
description: 'AWOS scripts',
tools: ['claude', 'copilot'],
},
{
source: 'claude/commands',
destination: '.claude/commands/awos',
patterns: ['*'],
description: 'Claude Code commands',
tools: ['claude'],
},
// Note: Copilot prompts are generated by prompt-generator.js (not copied directly)
// because Copilot doesn't resolve file references - content must be inlined
];

/**
* Filter copy operations by selected tool
* @param {Array} operations - Copy operations array
* @param {string} tool - Selected tool ('claude', 'copilot', 'all')
* @returns {Array} Filtered operations (all operations when tool='all')
*/
function filterOperationsByTool(operations, tool) {
if (tool === 'all') return operations;
return operations.filter((op) => {
if (!op.tools) return true;
return op.tools.includes(tool);
});
Comment on lines 95 to 100
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering functions check if 'all' is included in the tools array for each operation/directory. However, the logic op.tools.includes(tool) means that when tool='all', it will only match operations that explicitly have 'all' in their tools array. This is correct for the current implementation where all operations include 'all' in their tools arrays. But this could be fragile - if a new operation is added with only ['claude', 'copilot'] and not 'all', it won't be included when tool='all'. Consider documenting this pattern or implementing tool='all' to match any operation that has either 'claude' or 'copilot'.

Copilot uses AI. Check for mistakes.
}

/**
* Filter directories by selected tool
* @param {Array} dirs - Directories array
* @param {string} tool - Selected tool ('claude', 'copilot', 'all')
* @returns {Array} Filtered directories (all directories when tool='all')
*/
function filterDirectoriesByTool(dirs, tool) {
if (tool === 'all') return dirs;
return dirs.filter((dir) => {
if (!dir.tools) return true;
return dir.tools.includes(tool);
});
}

module.exports = {
directories,
copyOperations,
filterOperationsByTool,
filterDirectoriesByTool,
};
7 changes: 7 additions & 0 deletions src/copilot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Copilot Services
* Exports all Copilot-specific installer functionality
*/
const { generatePrompts } = require('./prompt-generator');

module.exports = { generatePrompts };
128 changes: 128 additions & 0 deletions src/copilot/prompt-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copilot File Generator Service
* Generates Copilot prompt and agent files by inlining referenced content
*
* Copilot doesn't resolve file references in prompts, so we inline
* the full content at install time.
*/
const fs = require('fs').promises;
const path = require('path');
const { log } = require('../utils/logger');

const INLINE_PATTERN = /\{\{INLINE:([^}]+)\}\}/g;

/**
* Remove YAML frontmatter from markdown content
* @param {string} content - Markdown content with potential frontmatter
* @returns {string} Content without frontmatter
*/
function removeFrontmatter(content) {
// Handle both Unix (\n) and Windows (\r\n) line endings
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
if (frontmatterMatch) {
return content.slice(frontmatterMatch[0].length);
}
return content;
}

/**
* Generate files by inlining {{INLINE:path}} markers with file content
* @param {Object} config - Generation configuration
* @param {string} config.packageRoot - Root directory of the AWOS package
* @param {string} config.sourceDir - Source directory relative to packageRoot
* @param {string} config.outputDir - Output directory (absolute path)
* @param {string} config.filePattern - File extension pattern to match (e.g., '.prompt.md')
* @param {boolean} config.dryRun - Whether to run in dry-run mode
* @returns {Promise<Object>} Statistics: { generated: files successfully processed, skipped: files with failed inlines }
*/
async function generateFiles({
packageRoot,
sourceDir,
outputDir,
filePattern,
dryRun = false,
}) {
const fullSourceDir = path.join(packageRoot, sourceDir);
const stats = { generated: 0, skipped: 0 };

// Ensure output directory exists
if (!dryRun) {
await fs.mkdir(outputDir, { recursive: true });
}
Comment on lines +48 to +51
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In dry-run mode, the output directory is not created (line 48-50), but this could lead to unexpected behavior if other code expects the directory structure to exist during dry-run validation. Consider documenting this behavior or creating the directory structure in memory for validation purposes.

Copilot uses AI. Check for mistakes.

// Read all template files
let files;
try {
files = await fs.readdir(fullSourceDir);
} catch (err) {
log(`Source directory not found: ${fullSourceDir}`, 'error');
return stats;
}

const templateFiles = files.filter((f) => f.endsWith(filePattern));

for (const file of templateFiles) {
const templatePath = path.join(fullSourceDir, file);
let content = await fs.readFile(templatePath, 'utf8');

// Replace all {{INLINE:path}} markers with file content
const matches = [...content.matchAll(INLINE_PATTERN)];
let inlineFailed = false;

for (const match of matches) {
const relativePath = match[1];
const inlinePath = path.join(packageRoot, relativePath);

try {
const inlineContent = await fs.readFile(inlinePath, 'utf8');
// Remove frontmatter from inlined content (keep only body)
const bodyContent = removeFrontmatter(inlineContent);
// Use replaceAll to handle multiple occurrences of the same marker
content = content.replaceAll(match[0], bodyContent);
} catch (err) {
log(`Failed to inline ${relativePath}: ${err.message}`, 'error');
inlineFailed = true;
break;
}
}

// Skip file if any inline failed
if (inlineFailed) {
stats.skipped++;
continue;
}

// Write generated file
const outputPath = path.join(outputDir, file);
if (dryRun) {
log(`[DRY-RUN] Would generate: ${outputPath}`, 'info');
} else {
await fs.writeFile(outputPath, content, 'utf8');
log(`Generated ${file}`, 'success');
Comment on lines 95 to 101
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In dry-run mode, the function doesn't log what files would be generated. Users running with --dry-run won't see which prompt/agent files would be created. Consider adding dry-run logging similar to other operations in the codebase (e.g., line 112: "[DRY-RUN] Would merge settings").

Suggested change
// Write generated file
if (!dryRun) {
const outputPath = path.join(outputDir, file);
await fs.writeFile(outputPath, content, 'utf8');
log(`Generated ${file}`, 'success');
// Write generated file (or log in dry-run mode)
const outputPath = path.join(outputDir, file);
if (!dryRun) {
await fs.writeFile(outputPath, content, 'utf8');
log(`Generated ${file}`, 'success');
} else {
log(`[DRY-RUN] Would generate ${file} at ${outputPath}`, 'info');

Copilot uses AI. Check for mistakes.
}

stats.generated++;
Comment on lines 82 to 104
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an inline file fails to load, the function logs an error and increments skipped but then continues to increment generated count. This means a file that failed to inline properly will still be counted as successfully generated. The continue statement should skip to the next file in the outer loop, but it actually just skips to the next inline match. If any inline fails, the partially-processed file should not be written or counted as generated.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +104
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the file-copier service which explicitly handles existing files (checking and unlinking before copy), this function always overwrites existing prompt files without checking. For consistency with the rest of the installer and to avoid surprising users, consider adding a check for existing files and either skip generation (preserving user modifications) or log that the file is being updated.

Suggested change
if (dryRun) {
log(`[DRY-RUN] Would generate: ${outputPath}`, 'info');
} else {
await fs.writeFile(outputPath, content, 'utf8');
log(`Generated ${file}`, 'success');
}
stats.generated++;
// Check if output file already exists to avoid overwriting user changes
let exists = false;
try {
await fs.access(outputPath);
exists = true;
} catch (err) {
// File does not exist; safe to generate
exists = false;
}
if (dryRun) {
if (exists) {
log(`[DRY-RUN] Would skip (already exists): ${outputPath}`, 'info');
stats.skipped++;
} else {
log(`[DRY-RUN] Would generate: ${outputPath}`, 'info');
stats.generated++;
}
} else {
if (exists) {
log(`Skipping ${file} (already exists)`, 'info');
stats.skipped++;
} else {
await fs.writeFile(outputPath, content, 'utf8');
log(`Generated ${file}`, 'success');
stats.generated++;
}
}

Copilot uses AI. Check for mistakes.
}

return stats;
}

/**
* Generate Copilot prompt files by inlining command content
* @param {Object} config - Generation configuration
* @param {string} config.packageRoot - Root directory of the AWOS package
* @param {string} config.targetDir - Target directory for generated files
* @param {boolean} config.dryRun - Whether to run in dry-run mode
* @returns {Promise<Object>} Statistics: { generated, skipped }
*/
async function generatePrompts({ packageRoot, targetDir, dryRun = false }) {
return generateFiles({
packageRoot,
sourceDir: 'copilot/prompts',
outputDir: path.join(targetDir, '.github/prompts'),
filePattern: '.prompt.md',
dryRun,
});
}

module.exports = { generatePrompts };
Loading
Loading