@fission-ai/openspec 0.1.0 ā 0.3.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/LICENSE +22 -0
- package/README.md +285 -77
- package/dist/cli/index.js +14 -14
- package/dist/core/config.d.ts +7 -5
- package/dist/core/config.js +3 -4
- package/dist/core/configurators/agents.d.ts +8 -0
- package/dist/core/configurators/agents.js +15 -0
- package/dist/core/configurators/registry.js +3 -0
- package/dist/core/configurators/slash/base.d.ts +17 -0
- package/dist/core/configurators/slash/base.js +61 -0
- package/dist/core/configurators/slash/claude.d.ts +9 -0
- package/dist/core/configurators/slash/claude.js +37 -0
- package/dist/core/configurators/slash/cursor.d.ts +9 -0
- package/dist/core/configurators/slash/cursor.js +37 -0
- package/dist/core/configurators/slash/registry.d.ts +8 -0
- package/dist/core/configurators/slash/registry.js +21 -0
- package/dist/core/init.d.ts +28 -0
- package/dist/core/init.js +387 -47
- package/dist/core/templates/agents-template.d.ts +2 -0
- package/dist/core/templates/agents-template.js +455 -0
- package/dist/core/templates/claude-template.d.ts +1 -1
- package/dist/core/templates/claude-template.js +1 -95
- package/dist/core/templates/index.d.ts +4 -0
- package/dist/core/templates/index.js +10 -3
- package/dist/core/templates/slash-command-templates.d.ts +4 -0
- package/dist/core/templates/slash-command-templates.js +40 -0
- package/dist/core/update.js +39 -6
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +148 -0
- package/dist/utils/file-system.d.ts +1 -0
- package/dist/utils/file-system.js +16 -0
- package/package.json +2 -2
- package/dist/core/diff.d.ts +0 -11
- package/dist/core/diff.js +0 -193
- package/dist/core/templates/readme-template.d.ts +0 -2
- package/dist/core/templates/readme-template.js +0 -519
package/dist/core/update.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
3
|
-
import { OPENSPEC_DIR_NAME } from './config.js';
|
|
4
|
-
import {
|
|
3
|
+
import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
|
|
4
|
+
import { agentsTemplate } from './templates/agents-template.js';
|
|
5
|
+
import { TemplateManager } from './templates/index.js';
|
|
5
6
|
import { ToolRegistry } from './configurators/registry.js';
|
|
7
|
+
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
|
6
8
|
export class UpdateCommand {
|
|
7
9
|
async execute(projectPath) {
|
|
8
10
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
@@ -12,18 +14,28 @@ export class UpdateCommand {
|
|
|
12
14
|
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
|
13
15
|
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
|
14
16
|
}
|
|
15
|
-
// 2. Update
|
|
16
|
-
const
|
|
17
|
-
|
|
17
|
+
// 2. Update AGENTS.md (full replacement)
|
|
18
|
+
const agentsPath = path.join(openspecPath, 'AGENTS.md');
|
|
19
|
+
const rootAgentsPath = path.join(resolvedProjectPath, 'AGENTS.md');
|
|
20
|
+
const rootAgentsExisted = await FileSystemUtils.fileExists(rootAgentsPath);
|
|
21
|
+
await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
|
|
22
|
+
const agentsStandardContent = TemplateManager.getAgentsStandardTemplate();
|
|
23
|
+
await FileSystemUtils.updateFileWithMarkers(rootAgentsPath, agentsStandardContent, OPENSPEC_MARKERS.start, OPENSPEC_MARKERS.end);
|
|
18
24
|
// 3. Update existing AI tool configuration files only
|
|
19
25
|
const configurators = ToolRegistry.getAll();
|
|
26
|
+
const slashConfigurators = SlashCommandRegistry.getAll();
|
|
20
27
|
let updatedFiles = [];
|
|
21
28
|
let failedFiles = [];
|
|
29
|
+
let updatedSlashFiles = [];
|
|
30
|
+
let failedSlashTools = [];
|
|
22
31
|
for (const configurator of configurators) {
|
|
23
32
|
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
|
|
24
33
|
// Only update if the file already exists
|
|
25
34
|
if (await FileSystemUtils.fileExists(configFilePath)) {
|
|
26
35
|
try {
|
|
36
|
+
if (!await FileSystemUtils.canWriteFile(configFilePath)) {
|
|
37
|
+
throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
|
|
38
|
+
}
|
|
27
39
|
await configurator.configure(resolvedProjectPath, openspecPath);
|
|
28
40
|
updatedFiles.push(configurator.configFileName);
|
|
29
41
|
}
|
|
@@ -33,14 +45,35 @@ export class UpdateCommand {
|
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
}
|
|
48
|
+
for (const slashConfigurator of slashConfigurators) {
|
|
49
|
+
if (!slashConfigurator.isAvailable) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
|
|
54
|
+
updatedSlashFiles = updatedSlashFiles.concat(updated);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
failedSlashTools.push(slashConfigurator.toolId);
|
|
58
|
+
console.error(`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
36
61
|
// 4. Success message (ASCII-safe)
|
|
37
|
-
const
|
|
62
|
+
const instructionUpdates = ['openspec/AGENTS.md'];
|
|
63
|
+
instructionUpdates.push(`AGENTS.md${rootAgentsExisted ? '' : ' (created)'}`);
|
|
64
|
+
const messages = [`Updated OpenSpec instructions (${instructionUpdates.join(', ')})`];
|
|
38
65
|
if (updatedFiles.length > 0) {
|
|
39
66
|
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
|
|
40
67
|
}
|
|
68
|
+
if (updatedSlashFiles.length > 0) {
|
|
69
|
+
messages.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
|
|
70
|
+
}
|
|
41
71
|
if (failedFiles.length > 0) {
|
|
42
72
|
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
|
|
43
73
|
}
|
|
74
|
+
if (failedSlashTools.length > 0) {
|
|
75
|
+
messages.push(`Failed slash command updates: ${failedSlashTools.join(', ')}`);
|
|
76
|
+
}
|
|
44
77
|
console.log(messages.join('\n'));
|
|
45
78
|
}
|
|
46
79
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { getTaskProgressForChange } from '../utils/task-progress.js';
|
|
5
|
+
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
6
|
+
export class ViewCommand {
|
|
7
|
+
async execute(targetPath = '.') {
|
|
8
|
+
const openspecDir = path.join(targetPath, 'openspec');
|
|
9
|
+
if (!fs.existsSync(openspecDir)) {
|
|
10
|
+
console.error(chalk.red('No openspec directory found'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
console.log(chalk.bold('\nOpenSpec Dashboard\n'));
|
|
14
|
+
console.log('ā'.repeat(60));
|
|
15
|
+
// Get changes and specs data
|
|
16
|
+
const changesData = await this.getChangesData(openspecDir);
|
|
17
|
+
const specsData = await this.getSpecsData(openspecDir);
|
|
18
|
+
// Display summary metrics
|
|
19
|
+
this.displaySummary(changesData, specsData);
|
|
20
|
+
// Display active changes
|
|
21
|
+
if (changesData.active.length > 0) {
|
|
22
|
+
console.log(chalk.bold.cyan('\nActive Changes'));
|
|
23
|
+
console.log('ā'.repeat(60));
|
|
24
|
+
changesData.active.forEach(change => {
|
|
25
|
+
const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
|
|
26
|
+
const percentage = change.progress.total > 0
|
|
27
|
+
? Math.round((change.progress.completed / change.progress.total) * 100)
|
|
28
|
+
: 0;
|
|
29
|
+
console.log(` ${chalk.yellow('ā')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Display completed changes
|
|
33
|
+
if (changesData.completed.length > 0) {
|
|
34
|
+
console.log(chalk.bold.green('\nCompleted Changes'));
|
|
35
|
+
console.log('ā'.repeat(60));
|
|
36
|
+
changesData.completed.forEach(change => {
|
|
37
|
+
console.log(` ${chalk.green('ā')} ${change.name}`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Display specifications
|
|
41
|
+
if (specsData.length > 0) {
|
|
42
|
+
console.log(chalk.bold.blue('\nSpecifications'));
|
|
43
|
+
console.log('ā'.repeat(60));
|
|
44
|
+
// Sort specs by requirement count (descending)
|
|
45
|
+
specsData.sort((a, b) => b.requirementCount - a.requirementCount);
|
|
46
|
+
specsData.forEach(spec => {
|
|
47
|
+
const reqLabel = spec.requirementCount === 1 ? 'requirement' : 'requirements';
|
|
48
|
+
console.log(` ${chalk.blue('āŖ')} ${chalk.bold(spec.name.padEnd(30))} ${chalk.dim(`${spec.requirementCount} ${reqLabel}`)}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
console.log('\n' + 'ā'.repeat(60));
|
|
52
|
+
console.log(chalk.dim(`\nUse ${chalk.white('openspec list --changes')} or ${chalk.white('openspec list --specs')} for detailed views`));
|
|
53
|
+
}
|
|
54
|
+
async getChangesData(openspecDir) {
|
|
55
|
+
const changesDir = path.join(openspecDir, 'changes');
|
|
56
|
+
if (!fs.existsSync(changesDir)) {
|
|
57
|
+
return { active: [], completed: [] };
|
|
58
|
+
}
|
|
59
|
+
const active = [];
|
|
60
|
+
const completed = [];
|
|
61
|
+
const entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (entry.isDirectory() && entry.name !== 'archive') {
|
|
64
|
+
const progress = await getTaskProgressForChange(changesDir, entry.name);
|
|
65
|
+
if (progress.total === 0 || progress.completed === progress.total) {
|
|
66
|
+
completed.push({ name: entry.name });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
active.push({ name: entry.name, progress });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Sort active changes by completion percentage (ascending) and then by name for deterministic ordering
|
|
74
|
+
active.sort((a, b) => {
|
|
75
|
+
const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
|
|
76
|
+
const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
|
|
77
|
+
if (percentageA < percentageB)
|
|
78
|
+
return -1;
|
|
79
|
+
if (percentageA > percentageB)
|
|
80
|
+
return 1;
|
|
81
|
+
return a.name.localeCompare(b.name);
|
|
82
|
+
});
|
|
83
|
+
completed.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
return { active, completed };
|
|
85
|
+
}
|
|
86
|
+
async getSpecsData(openspecDir) {
|
|
87
|
+
const specsDir = path.join(openspecDir, 'specs');
|
|
88
|
+
if (!fs.existsSync(specsDir)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const specs = [];
|
|
92
|
+
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
const specFile = path.join(specsDir, entry.name, 'spec.md');
|
|
96
|
+
if (fs.existsSync(specFile)) {
|
|
97
|
+
try {
|
|
98
|
+
const content = fs.readFileSync(specFile, 'utf-8');
|
|
99
|
+
const parser = new MarkdownParser(content);
|
|
100
|
+
const spec = parser.parseSpec(entry.name);
|
|
101
|
+
const requirementCount = spec.requirements.length;
|
|
102
|
+
specs.push({ name: entry.name, requirementCount });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// If spec cannot be parsed, include with 0 count
|
|
106
|
+
specs.push({ name: entry.name, requirementCount: 0 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return specs;
|
|
112
|
+
}
|
|
113
|
+
displaySummary(changesData, specsData) {
|
|
114
|
+
const totalChanges = changesData.active.length + changesData.completed.length;
|
|
115
|
+
const totalSpecs = specsData.length;
|
|
116
|
+
const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
|
|
117
|
+
// Calculate total task progress
|
|
118
|
+
let totalTasks = 0;
|
|
119
|
+
let completedTasks = 0;
|
|
120
|
+
changesData.active.forEach(change => {
|
|
121
|
+
totalTasks += change.progress.total;
|
|
122
|
+
completedTasks += change.progress.completed;
|
|
123
|
+
});
|
|
124
|
+
changesData.completed.forEach(() => {
|
|
125
|
+
// Completed changes count as 100% done (we don't know exact task count)
|
|
126
|
+
// This is a simplification
|
|
127
|
+
});
|
|
128
|
+
console.log(chalk.bold('Summary:'));
|
|
129
|
+
console.log(` ${chalk.cyan('ā')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
|
|
130
|
+
console.log(` ${chalk.yellow('ā')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
|
|
131
|
+
console.log(` ${chalk.green('ā')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
|
|
132
|
+
if (totalTasks > 0) {
|
|
133
|
+
const overallProgress = Math.round((completedTasks / totalTasks) * 100);
|
|
134
|
+
console.log(` ${chalk.magenta('ā')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
createProgressBar(completed, total, width = 20) {
|
|
138
|
+
if (total === 0)
|
|
139
|
+
return chalk.dim('ā'.repeat(width));
|
|
140
|
+
const percentage = completed / total;
|
|
141
|
+
const filled = Math.round(percentage * width);
|
|
142
|
+
const empty = width - filled;
|
|
143
|
+
const filledBar = chalk.green('ā'.repeat(filled));
|
|
144
|
+
const emptyBar = chalk.dim('ā'.repeat(empty));
|
|
145
|
+
return `[${filledBar}${emptyBar}]`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=view.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare class FileSystemUtils {
|
|
2
2
|
static createDirectory(dirPath: string): Promise<void>;
|
|
3
3
|
static fileExists(filePath: string): Promise<boolean>;
|
|
4
|
+
static canWriteFile(filePath: string): Promise<boolean>;
|
|
4
5
|
static directoryExists(dirPath: string): Promise<boolean>;
|
|
5
6
|
static writeFile(filePath: string, content: string): Promise<void>;
|
|
6
7
|
static readFile(filePath: string): Promise<string>;
|
|
@@ -16,6 +16,22 @@ export class FileSystemUtils {
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
static async canWriteFile(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
const stats = await fs.stat(filePath);
|
|
22
|
+
if (!stats.isFile()) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return (stats.mode & 0o222) !== 0;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === 'ENOENT') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
19
35
|
static async directoryExists(dirPath) {
|
|
20
36
|
try {
|
|
21
37
|
const stats = await fs.stat(dirPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"vitest": "^3.2.4"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@inquirer/core": "^10.2.2",
|
|
51
52
|
"@inquirer/prompts": "^7.8.0",
|
|
52
53
|
"chalk": "^5.5.0",
|
|
53
54
|
"commander": "^14.0.0",
|
|
54
|
-
"jest-diff": "^30.0.5",
|
|
55
55
|
"ora": "^8.2.0",
|
|
56
56
|
"zod": "^4.0.17"
|
|
57
57
|
},
|
package/dist/core/diff.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export declare class DiffCommand {
|
|
2
|
-
private filesChanged;
|
|
3
|
-
private linesAdded;
|
|
4
|
-
private linesRemoved;
|
|
5
|
-
execute(changeName?: string): Promise<void>;
|
|
6
|
-
private selectChange;
|
|
7
|
-
private showDiffs;
|
|
8
|
-
private walkAndDiff;
|
|
9
|
-
private diffFile;
|
|
10
|
-
}
|
|
11
|
-
//# sourceMappingURL=diff.d.ts.map
|
package/dist/core/diff.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { diffStringsUnified } from 'jest-diff';
|
|
5
|
-
import { select } from '@inquirer/prompts';
|
|
6
|
-
import { Validator } from './validation/validator.js';
|
|
7
|
-
// Constants
|
|
8
|
-
const ARCHIVE_DIR = 'archive';
|
|
9
|
-
const MARKDOWN_EXT = '.md';
|
|
10
|
-
const OPENSPEC_DIR = 'openspec';
|
|
11
|
-
const CHANGES_DIR = 'changes';
|
|
12
|
-
const SPECS_DIR = 'specs';
|
|
13
|
-
export class DiffCommand {
|
|
14
|
-
filesChanged = 0;
|
|
15
|
-
linesAdded = 0;
|
|
16
|
-
linesRemoved = 0;
|
|
17
|
-
async execute(changeName) {
|
|
18
|
-
const changesDir = path.join(process.cwd(), OPENSPEC_DIR, CHANGES_DIR);
|
|
19
|
-
try {
|
|
20
|
-
await fs.access(changesDir);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
throw new Error('No OpenSpec changes directory found');
|
|
24
|
-
}
|
|
25
|
-
if (!changeName) {
|
|
26
|
-
changeName = await this.selectChange(changesDir);
|
|
27
|
-
if (!changeName)
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const changeDir = path.join(changesDir, changeName);
|
|
31
|
-
try {
|
|
32
|
-
await fs.access(changeDir);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
throw new Error(`Change '${changeName}' not found`);
|
|
36
|
-
}
|
|
37
|
-
const changeSpecsDir = path.join(changeDir, SPECS_DIR);
|
|
38
|
-
try {
|
|
39
|
-
await fs.access(changeSpecsDir);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
console.log(`No spec changes found for '${changeName}'`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
// Validate specs and show warnings (non-blocking)
|
|
46
|
-
const validator = new Validator();
|
|
47
|
-
let hasWarnings = false;
|
|
48
|
-
try {
|
|
49
|
-
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
|
50
|
-
for (const entry of entries) {
|
|
51
|
-
if (entry.isDirectory()) {
|
|
52
|
-
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
|
53
|
-
try {
|
|
54
|
-
await fs.access(specFile);
|
|
55
|
-
const report = await validator.validateSpec(specFile);
|
|
56
|
-
if (report.issues.length > 0) {
|
|
57
|
-
const warnings = report.issues.filter(i => i.level === 'WARNING');
|
|
58
|
-
const errors = report.issues.filter(i => i.level === 'ERROR');
|
|
59
|
-
if (errors.length > 0 || warnings.length > 0) {
|
|
60
|
-
if (!hasWarnings) {
|
|
61
|
-
console.log(chalk.yellow('\nā ļø Validation warnings found:'));
|
|
62
|
-
hasWarnings = true;
|
|
63
|
-
}
|
|
64
|
-
console.log(chalk.yellow(`\n ${entry.name}/spec.md:`));
|
|
65
|
-
for (const issue of errors) {
|
|
66
|
-
console.log(chalk.red(` ā ${issue.message}`));
|
|
67
|
-
}
|
|
68
|
-
for (const issue of warnings) {
|
|
69
|
-
console.log(chalk.yellow(` ā ${issue.message}`));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// Spec file doesn't exist, skip validation
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (hasWarnings) {
|
|
80
|
-
console.log(chalk.yellow('\nConsider fixing these issues before archiving.\n'));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// No specs directory, skip validation
|
|
85
|
-
}
|
|
86
|
-
// Reset counters
|
|
87
|
-
this.filesChanged = 0;
|
|
88
|
-
this.linesAdded = 0;
|
|
89
|
-
this.linesRemoved = 0;
|
|
90
|
-
await this.showDiffs(changeSpecsDir);
|
|
91
|
-
// Show summary
|
|
92
|
-
if (this.filesChanged > 0) {
|
|
93
|
-
console.log(chalk.bold(`\nš Summary: ${this.filesChanged} file(s) changed, ${chalk.green(`+${this.linesAdded}`)} ${chalk.red(`-${this.linesRemoved}`)}`));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async selectChange(changesDir) {
|
|
97
|
-
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
98
|
-
const changes = entries
|
|
99
|
-
.filter(entry => entry.isDirectory() && entry.name !== ARCHIVE_DIR)
|
|
100
|
-
.map(entry => entry.name);
|
|
101
|
-
if (changes.length === 0) {
|
|
102
|
-
console.log('No changes found');
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
console.log('Available changes:');
|
|
106
|
-
const choices = changes.map((name) => ({
|
|
107
|
-
name: name,
|
|
108
|
-
value: name
|
|
109
|
-
}));
|
|
110
|
-
const answer = await select({
|
|
111
|
-
message: 'Select a change',
|
|
112
|
-
choices
|
|
113
|
-
});
|
|
114
|
-
return answer;
|
|
115
|
-
}
|
|
116
|
-
async showDiffs(changeSpecsDir) {
|
|
117
|
-
const currentSpecsDir = path.join(process.cwd(), OPENSPEC_DIR, SPECS_DIR);
|
|
118
|
-
await this.walkAndDiff(changeSpecsDir, currentSpecsDir, '');
|
|
119
|
-
}
|
|
120
|
-
async walkAndDiff(changeDir, currentDir, relativePath) {
|
|
121
|
-
const entries = await fs.readdir(path.join(changeDir, relativePath), { withFileTypes: true });
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
const entryPath = path.join(relativePath, entry.name);
|
|
124
|
-
if (entry.isDirectory()) {
|
|
125
|
-
await this.walkAndDiff(changeDir, currentDir, entryPath);
|
|
126
|
-
}
|
|
127
|
-
else if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXT)) {
|
|
128
|
-
await this.diffFile(path.join(changeDir, entryPath), path.join(currentDir, entryPath), entryPath);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async diffFile(changePath, currentPath, displayPath) {
|
|
133
|
-
let changeContent = '';
|
|
134
|
-
let currentContent = '';
|
|
135
|
-
let isNewFile = false;
|
|
136
|
-
let isDeleted = false;
|
|
137
|
-
try {
|
|
138
|
-
changeContent = await fs.readFile(changePath, 'utf-8');
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
changeContent = '';
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
currentContent = await fs.readFile(currentPath, 'utf-8');
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
currentContent = '';
|
|
148
|
-
isNewFile = true;
|
|
149
|
-
}
|
|
150
|
-
if (changeContent === currentContent) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (changeContent === '' && currentContent !== '') {
|
|
154
|
-
isDeleted = true;
|
|
155
|
-
}
|
|
156
|
-
// Enhanced header with file status
|
|
157
|
-
console.log(chalk.bold.cyan(`\n${'ā'.repeat(60)}`));
|
|
158
|
-
console.log(chalk.bold.cyan(`š ${displayPath}`));
|
|
159
|
-
if (isNewFile) {
|
|
160
|
-
console.log(chalk.green(` Status: NEW FILE`));
|
|
161
|
-
}
|
|
162
|
-
else if (isDeleted) {
|
|
163
|
-
console.log(chalk.red(` Status: DELETED`));
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
console.log(chalk.yellow(` Status: MODIFIED`));
|
|
167
|
-
}
|
|
168
|
-
// Use jest-diff for the actual diff with custom options
|
|
169
|
-
const diffOptions = {
|
|
170
|
-
aAnnotation: 'Current',
|
|
171
|
-
bAnnotation: 'Proposed',
|
|
172
|
-
aColor: chalk.red,
|
|
173
|
-
bColor: chalk.green,
|
|
174
|
-
commonColor: chalk.gray,
|
|
175
|
-
contextLines: 3,
|
|
176
|
-
expand: false,
|
|
177
|
-
includeChangeCounts: true,
|
|
178
|
-
};
|
|
179
|
-
const diff = diffStringsUnified(currentContent, changeContent, diffOptions);
|
|
180
|
-
// Count lines for statistics (approximate)
|
|
181
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
182
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
183
|
-
console.log(chalk.gray(` Lines: ${chalk.green(`+${addedLines}`)} ${chalk.red(`-${removedLines}`)}`));
|
|
184
|
-
console.log(chalk.bold.cyan(`${'ā'.repeat(60)}\n`));
|
|
185
|
-
// Display the diff
|
|
186
|
-
console.log(diff);
|
|
187
|
-
// Update counters
|
|
188
|
-
this.filesChanged++;
|
|
189
|
-
this.linesAdded += addedLines;
|
|
190
|
-
this.linesRemoved += removedLines;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
//# sourceMappingURL=diff.js.map
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export declare const readmeTemplate = "# OpenSpec Instructions\n\nThis document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.\n\n## Core Principle\n\nOpenSpec is an AI-native system for change-driven development where:\n- **Specs** (`specs/`) reflect what IS currently built and deployed\n- **Changes** (`changes/`) contain proposals for what SHOULD be changed\n- **AI drives the process** - You generate proposals, humans review and approve\n- **Specs are living documentation** - Always kept in sync with deployed code\n\n## Start Simple\n\n**Default to minimal implementations:**\n- New features should be <100 lines of code initially\n- Use the simplest solution that works\n- Avoid premature optimization (no caching, parallelization, or complex patterns without proven need)\n- Choose boring technology over cutting-edge solutions\n\n**Complexity triggers** - Only add complexity when you have:\n- **Performance data** showing current solution is too slow\n- **Scale requirements** with specific numbers (>1000 users, >100MB data)\n- **Multiple use cases** requiring the same abstraction\n- **Regulatory compliance** mandating specific patterns\n- **Security threats** that simple solutions cannot address\n\nWhen triggered, document the specific justification in your change proposal.\n\n## Directory Structure\n\n```\nopenspec/\n\u251C\u2500\u2500 project.md # Project-specific context (tech stack, conventions)\n\u251C\u2500\u2500 README.md # This file - OpenSpec instructions\n\u251C\u2500\u2500 specs/ # Current truth - what IS built\n\u2502 \u251C\u2500\u2500 [capability]/ # Single, focused capability\n\u2502 \u2502 \u251C\u2500\u2500 spec.md # WHAT the capability does and WHY\n\u2502 \u2502 \u2514\u2500\u2500 design.md # HOW it's built (established patterns)\n\u2502 \u2514\u2500\u2500 ...\n\u251C\u2500\u2500 changes/ # Proposed changes - what we're CHANGING\n\u2502 \u251C\u2500\u2500 [change-name]/\n\u2502 \u2502 \u251C\u2500\u2500 proposal.md # Why, what, impact (consolidated)\n\u2502 \u2502 \u251C\u2500\u2500 tasks.md # Implementation checklist\n\u2502 \u2502 \u251C\u2500\u2500 design.md # Technical decisions (optional, for complex changes)\n\u2502 \u2502 \u2514\u2500\u2500 specs/ # Delta changes to specs\n\u2502 \u2502 \u2514\u2500\u2500 [capability]/\n\u2502 \u2502 \u2514\u2500\u2500 spec.md # Delta format (ADDED/MODIFIED/REMOVED/RENAMED)\n\u2502 \u2514\u2500\u2500 archive/ # Completed changes (dated)\n```\n\n### Capability Organization\n\n**Use capabilities, not features** - Each directory under `specs/` represents a single, focused responsibility:\n- **Verb-noun naming**: `user-auth`, `payment-capture`, `order-checkout`\n- **10-minute rule**: Each capability should be understandable in <10 minutes\n- **Single purpose**: If it needs \"AND\" to describe it, split it\n\nExamples:\n```\n\u2705 GOOD: user-auth, user-sessions, payment-capture, payment-refunds\n\u274C BAD: users, payments, core, misc\n```\n\n## Key Behavioral Rules\n\n### 1. Always Start by Reading\n\nBefore any task:\n1. **Read relevant specs** in `specs/[capability]/spec.md` to understand current state\n2. **Check pending changes** in `changes/` directory for potential conflicts\n3. **Read project.md** for project-specific conventions\n\n### 2. When to Create Change Proposals\n\n**ALWAYS create a change proposal for:**\n- New features or functionality\n- Breaking changes (API changes, schema updates)\n- Architecture changes or new patterns\n- Performance optimizations that change behavior\n- Security updates affecting auth/access patterns\n- Any change requiring multiple steps or affecting multiple systems\n\n**SKIP proposals for:**\n- Bug fixes that restore intended behavior\n- Typos, formatting, or comment updates\n- Dependency updates (unless breaking)\n- Configuration or environment variable changes\n- Adding tests for existing behavior\n- Documentation fixes\n\n**Complexity assessment:**\n- If your solution requires >100 lines of new code, justify the complexity\n- If adding dependencies, frameworks, or architectural patterns, document why simpler alternatives won't work\n- Default to single-file implementations until proven insufficient\n\n### 3. Delta-Based Change Format\n\nChanges use a delta format with clear sections:\n\n```markdown\n## ADDED Requirements\n### Requirement: New Feature\n[Complete requirement content in structured format]\n\n## MODIFIED Requirements \n### Requirement: Existing Feature\n[Complete modified requirement (header must match current spec)]\n\n## REMOVED Requirements\n### Requirement: Old Feature\n**Reason for removal**: [Why removing]\n**Migration path**: [How to handle existing usage]\n\n## RENAMED Requirements\n- FROM: `### Requirement: Old Name`\n- TO: `### Requirement: New Name`\n```\n\nKey rules:\n- Headers are matched using `normalize(header) = trim(header)`\n- Include complete requirements (not diffs)\n- Use standard symbols in CLI output: + (added), ~ (modified), - (removed), \u2192 (renamed)\n\n### 4. Creating a Change Proposal\n\nWhen a user requests a significant change:\n\n```bash\n# 1. Create the change directory\nopenspec/changes/[descriptive-name]/\n\n# 2. Generate proposal.md with all context\n## Why\n[1-2 sentences on the problem/opportunity]\n\n## What Changes \n[Bullet list of changes, including breaking changes]\n\n## Impact\n- Affected specs: [list capabilities that will change]\n- Affected code: [list key files/systems]\n\n# 3. Create delta specs for ALL affected capabilities\n# - Store only the changes (not complete future state)\n# - Use sections: ## ADDED, ## MODIFIED, ## REMOVED, ## RENAMED\n# - Include complete requirements in their final form\n# Example spec.md content:\n# ## ADDED Requirements\n# ### Requirement: Password Reset\n# Users SHALL be able to reset passwords via email...\n# \n# ## MODIFIED Requirements\n# ### Requirement: User Authentication\n# [Complete modified requirement with new password reset hook]\nspecs/\n\u2514\u2500\u2500 [capability]/\n \u2514\u2500\u2500 spec.md # Contains delta sections\n\n# 4. Create tasks.md with implementation steps\n## 1. [Task Group]\n- [ ] 1.1 [Specific task]\n- [ ] 1.2 [Specific task]\n\n# 5. For complex changes, add design.md\n[Technical decisions and trade-offs]\n```\n\n### 5. The Change Lifecycle\n\n1. **Propose** \u2192 Create change directory with delta-based documentation\n2. **Review** \u2192 User reviews and approves the proposal\n3. **Implement** \u2192 Follow the approved tasks.md (can be multiple PRs)\n4. **Deploy** \u2192 User confirms deployment\n5. **Update Specs** \u2192 Apply deltas to sync specs/ with new reality (IF the change affects system capabilities)\n6. **Archive** \u2192 Move to `changes/archive/YYYY-MM-DD-[name]/`\n\n### 6. Implementing Changes\n\nWhen implementing an approved change:\n1. Follow the tasks.md checklist exactly\n2. **Mark completed tasks** in tasks.md as you finish them (e.g., `- [x] 1.1 Task completed`)\n3. Ensure code matches the proposed behavior\n4. Update any affected tests\n5. **Keep change in `changes/` directory** - do NOT archive in implementation PR\n\n**Multiple Implementation PRs:**\n- Changes can be implemented across multiple PRs\n- Each PR should update tasks.md to mark what was completed\n- Different developers can work on different task groups\n- Example: PR #1 completes tasks 1.1-1.3, PR #2 completes tasks 2.1-2.4\n\n### 7. Updating Specs and Archiving After Deployment\n\n**Create a separate PR after deployment** that:\n1. Moves change to `changes/archive/YYYY-MM-DD-[name]/`\n2. Updates relevant files in `specs/` to reflect new reality (if needed)\n3. If design.md exists, incorporates proven patterns into `specs/[capability]/design.md`\n\nThis ensures changes are only archived when truly complete and deployed.\n\n### 8. Types of Changes That Don't Require Specs\n\nSome changes only affect development infrastructure and don't need specs:\n- Initial project setup (package.json, tsconfig.json, etc.)\n- Development tooling changes (linters, formatters, build tools)\n- CI/CD configuration\n- Development dependencies\n\nFor these changes:\n1. Implement \u2192 Deploy \u2192 Mark tasks complete \u2192 Archive\n2. Skip the \"Update Specs\" step entirely\n\n### What Deserves a Spec?\n\nAsk yourself:\n- Is this a system capability that users or other systems interact with?\n- Does it have ongoing behavior that needs documentation?\n- Would a new developer need to understand this to work with the system?\n\nIf NO to all \u2192 No spec needed (likely just tooling/infrastructure)\n\n## Understanding Specs vs Code\n\n### Specs Document WHAT and WHY\n```markdown\n# Authentication Spec\n\nUsers SHALL authenticate with email and password.\n\nWHEN credentials are valid THEN issue JWT token.\nWHEN credentials are invalid THEN return generic error.\n\nWHY: Prevent user enumeration attacks.\n```\n\n### Code Documents HOW\n```javascript\n// Implementation details\nconst user = await db.users.findOne({ email });\nconst valid = await bcrypt.compare(password, user.hashedPassword);\n```\n\n**Key Distinction**: Specs capture intent, constraints, and decisions that aren't obvious from code.\n\n## Common Scenarios\n\n### New Feature Request\n```\nUser: \"Add password reset functionality\"\n\nYou should:\n1. Read specs/user-auth/spec.md\n2. Check changes/ for pending auth changes\n3. Create changes/add-password-reset/ with:\n - proposal.md describing the change\n - specs/user-auth/spec.md with:\n ## ADDED Requirements\n ### Requirement: Password Reset\n [Complete requirement for password reset]\n \n ## MODIFIED Requirements\n ### Requirement: User Authentication\n [Updated to integrate with password reset]\n4. Wait for approval before implementing\n```\n\n### Bug Fix\n```\nUser: \"Getting null pointer error when bio is empty\"\n\nYou should:\n1. Check if spec says bios are optional\n2. If yes \u2192 Fix directly (it's a bug)\n3. If no \u2192 Create change proposal (it's a behavior change)\n```\n\n### Infrastructure Setup\n```\nUser: \"Initialize TypeScript project\"\n\nYou should:\n1. Create change proposal for TypeScript setup\n2. Implement configuration files (PR #1)\n3. Mark tasks complete in tasks.md\n4. After deployment, create separate PR to archive\n (no specs update needed - this is tooling, not a capability)\n```\n\n## Summary Workflow\n\n1. **Receive request** \u2192 Determine if it needs a change proposal\n2. **Read current state** \u2192 Check specs and pending changes\n3. **Create proposal** \u2192 Generate complete change documentation\n4. **Get approval** \u2192 User reviews the proposal\n5. **Implement** \u2192 Follow approved tasks, mark completed items in tasks.md\n6. **Deploy** \u2192 User deploys the implementation\n7. **Archive PR** \u2192 Create separate PR to:\n - Move change to archive\n - Update specs if needed\n - Mark change as complete\n\n## PR Workflow Examples\n\n### Single Developer, Simple Change\n```\nPR #1: Implementation\n- Implement all tasks\n- Update tasks.md marking items complete\n- Get merged and deployed\n\nPR #2: Archive (after deployment)\n- Move changes/feature-x/ \u2192 changes/archive/2025-01-15-feature-x/\n- Update specs if needed\n```\n\n### Multiple Developers, Complex Change\n```\nPR #1: Alice implements auth components\n- Complete tasks 1.1, 1.2, 1.3\n- Update tasks.md marking these complete\n\nPR #2: Bob implements UI components \n- Complete tasks 2.1, 2.2\n- Update tasks.md marking these complete\n\nPR #3: Alice fixes integration issues\n- Complete remaining task 1.4\n- Update tasks.md\n\n[Deploy all changes]\n\nPR #4: Archive\n- Move to archive with deployment date\n- Update specs to reflect new auth flow\n```\n\n### Key Rules\n- **Never archive in implementation PRs** - changes aren't done until deployed\n- **Always update tasks.md** - shows accurate progress\n- **One archive PR per change** - clear completion boundary\n- **Archive PR includes spec updates** - keeps specs current\n\n## Capability Organization Best Practices\n\n### Naming Capabilities\n- Use **verb-noun** patterns: `user-auth`, `payment-capture`, `order-checkout`\n- Be specific: `payment-capture` not just `payments`\n- Keep flat: Avoid nesting capabilities within capabilities\n- Singular focus: If you need \"AND\" to describe it, split it\n\n### When to Split Capabilities\nSplit when you have:\n- Multiple unrelated API endpoints\n- Different user personas or actors\n- Separate deployment considerations\n- Independent evolution paths\n\n#### Capability Boundary Guidelines\n- Would you import these separately? \u2192 Separate capabilities\n- Different deployment cadence? \u2192 Separate capabilities\n- Different teams own them? \u2192 Separate capabilities\n- Shared data models are OK, shared business logic means combine\n\nExamples:\n- user-auth (login/logout) vs user-sessions (token management) \u2192 SEPARATE\n- payment-capture vs payment-refunds \u2192 SEPARATE (different workflows)\n- user-profile vs user-settings \u2192 COMBINE (same data model, same owner)\n\n### Cross-Cutting Concerns\nFor system-wide policies (rate limiting, error handling, security), document them in:\n- `project.md` for project-wide conventions\n- Within relevant capability specs where they apply\n- Or create a dedicated capability if complex enough (e.g., `api-rate-limiting/`)\n\n### Examples of Well-Organized Capabilities\n```\nspecs/\n\u251C\u2500\u2500 user-auth/ # Login, logout, password reset\n\u251C\u2500\u2500 user-sessions/ # Token management, refresh\n\u251C\u2500\u2500 user-profile/ # Profile CRUD operations\n\u251C\u2500\u2500 payment-capture/ # Processing payments\n\u251C\u2500\u2500 payment-refunds/ # Handling refunds\n\u2514\u2500\u2500 order-checkout/ # Checkout workflow\n```\n\nFor detailed guidance, see the [Capability Organization Guide](../docs/capability-organization.md).\n\n## Common Scenarios and Clarifications\n\n### Decision Ambiguity: Bug vs Behavior Change\n\nWhen specs are missing or ambiguous:\n- If NO spec exists \u2192 Treat current code behavior as implicit spec, require proposal\n- If spec is VAGUE \u2192 Require proposal to clarify spec alongside fix\n- If code and spec DISAGREE \u2192 Spec is truth, code is buggy (fix without proposal)\n- If unsure \u2192 Default to creating a proposal (safer option)\n\nExample:\n```\nUser: \"The API returns 404 for missing users but should return 400\"\nAI: Is this a bug (spec says 400) or behavior change (spec says 404)?\n```\n\n### When You Don't Know the Scope\nIt's OK to explore first! Tell the user you need to investigate, then create an informed proposal.\n\n### Exploration Phase (When Needed)\n\nBEFORE creating proposal, you may need exploration when:\n- User request is vague or high-level\n- Multiple implementation approaches exist\n- Scope is unclear without seeing code\n\nExploration checklist:\n1. Tell user you need to explore first\n2. Use Grep/Read to understand current state\n3. Create initial proposal based on findings\n4. Refine with user feedback\n\nExample:\n```\nUser: \"Add caching to improve performance\"\nAI: \"Let me explore the codebase to understand the current architecture and identify caching opportunities.\"\n[After exploration]\nAI: \"Based on my analysis, I've identified three areas where caching would help. Here's my proposal...\"\n```\n\n### When No Specs Exist\nTreat current code as implicit spec. Your proposal should document current state AND proposed changes.\n\n### When in Doubt\nDefault to creating a proposal. It's easier to skip an unnecessary proposal than fix an undocumented change.\n\n### AI Workflow Adaptations\n\nTask tracking with OpenSpec:\n- Track exploration tasks separately from implementation\n- Document proposal creation steps as you go\n- Keep implementation tasks separate until proposal approved\n\nParallel operations encouraged:\n- Read multiple specs simultaneously\n- Check multiple pending changes at once\n- Batch related searches for efficiency\n\nProgress communication:\n- \"Exploring codebase to understand scope...\"\n- \"Creating proposal based on findings...\"\n- \"Implementing approved changes...\"\n\n### For AI Assistants\n- **Bias toward simplicity** - Propose the minimal solution that works\n- Use your exploration tools liberally before proposing\n- Batch operations for efficiency\n- Communicate your progress\n- It's OK to revise proposals based on discoveries\n- **Question complexity** - If your solution feels complex, simplify first\n\n## Edge Case Handling\n\n### Multi-Capability Changes\nCreate ONE proposal that:\n- Lists all affected capabilities\n- Shows changes per capability\n- Has unified task list\n- Gets approved as a whole\n\n### Outdated Specs\nIf specs clearly outdated:\n1. Create proposal to update specs to match reality\n2. Implement new feature in separate proposal\n3. OR combine both in one proposal with clear sections\n\n### Emergency Hotfixes\nFor critical production issues:\n1. Announce: \"This is an emergency fix\"\n2. Implement fix immediately\n3. Create retroactive proposal\n4. Update specs after deployment\n5. Tag with [EMERGENCY] in archive\n\n### Pure Refactoring\nNo proposal needed for:\n- Code formatting/style\n- Internal refactoring (same API)\n- Performance optimization (same behavior)\n- Adding types to untyped code\n\nProposal REQUIRED for:\n- API changes (even if compatible)\n- Database schema changes\n- Architecture changes\n- New dependencies\n\n### Observability Additions\nNo proposal needed for:\n- Adding log statements\n- New metrics/traces\n- Debugging additions\n- Error tracking\n\nProposal REQUIRED if:\n- Changes log format/structure\n- Adds new monitoring service\n- Changes what's logged (privacy)\n\n## Remember\n\n- You are the process driver - automate documentation burden\n- Specs must always reflect deployed reality\n- Changes are proposed, not imposed\n- Impact analysis prevents surprises\n- Simplicity is the power - just markdown files, minimal solutions\n- Start simple, add complexity only when justified\n\nBy following these conventions, you enable true spec-driven development where documentation stays current, changes are traceable, and evolution is intentional.\n";
|
|
2
|
-
//# sourceMappingURL=readme-template.d.ts.map
|