@fission-ai/openspec 0.20.0 → 0.22.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 +35 -0
- package/dist/cli/index.js +19 -0
- package/dist/commands/artifact-workflow.js +155 -22
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +11 -2
- package/dist/core/artifact-graph/instruction-loader.js +59 -7
- package/dist/core/artifact-graph/resolver.d.ts +32 -12
- package/dist/core/artifact-graph/resolver.js +88 -18
- package/dist/core/completions/command-registry.js +88 -0
- package/dist/core/completions/types.d.ts +2 -1
- package/dist/core/config-prompts.d.ts +36 -0
- package/dist/core/config-prompts.js +151 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/templates/skill-templates.d.ts +5 -0
- package/dist/core/templates/skill-templates.js +165 -87
- package/dist/utils/change-metadata.d.ts +8 -4
- package/dist/utils/change-metadata.js +27 -10
- package/dist/utils/change-utils.d.ts +14 -3
- package/dist/utils/change-utils.js +27 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,6 +140,8 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
|
|
|
140
140
|
|
|
141
141
|
#### Step 1: Install the CLI globally
|
|
142
142
|
|
|
143
|
+
**Option A: Using npm**
|
|
144
|
+
|
|
143
145
|
```bash
|
|
144
146
|
npm install -g @fission-ai/openspec@latest
|
|
145
147
|
```
|
|
@@ -149,6 +151,39 @@ Verify installation:
|
|
|
149
151
|
openspec --version
|
|
150
152
|
```
|
|
151
153
|
|
|
154
|
+
**Option B: Using Nix (NixOS and Nix package manager)**
|
|
155
|
+
|
|
156
|
+
Run OpenSpec directly without installation:
|
|
157
|
+
```bash
|
|
158
|
+
nix run github:Fission-AI/OpenSpec -- init
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Or install to your profile:
|
|
162
|
+
```bash
|
|
163
|
+
nix profile install github:Fission-AI/OpenSpec
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Or add to your development environment in `flake.nix`:
|
|
167
|
+
```nix
|
|
168
|
+
{
|
|
169
|
+
inputs = {
|
|
170
|
+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
171
|
+
openspec.url = "github:Fission-AI/OpenSpec";
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
outputs = { nixpkgs, openspec, ... }: {
|
|
175
|
+
devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell {
|
|
176
|
+
buildInputs = [ openspec.packages.x86_64-linux.default ];
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Verify installation:
|
|
183
|
+
```bash
|
|
184
|
+
openspec --version
|
|
185
|
+
```
|
|
186
|
+
|
|
152
187
|
#### Step 2: Initialize OpenSpec in your project
|
|
153
188
|
|
|
154
189
|
Navigate to your project directory:
|
package/dist/cli/index.js
CHANGED
|
@@ -13,8 +13,10 @@ import { ChangeCommand } from '../commands/change.js';
|
|
|
13
13
|
import { ValidateCommand } from '../commands/validate.js';
|
|
14
14
|
import { ShowCommand } from '../commands/show.js';
|
|
15
15
|
import { CompletionCommand } from '../commands/completion.js';
|
|
16
|
+
import { FeedbackCommand } from '../commands/feedback.js';
|
|
16
17
|
import { registerConfigCommand } from '../commands/config.js';
|
|
17
18
|
import { registerArtifactWorkflowCommands } from '../commands/artifact-workflow.js';
|
|
19
|
+
import { registerSchemaCommand } from '../commands/schema.js';
|
|
18
20
|
import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
|
|
19
21
|
const program = new Command();
|
|
20
22
|
const require = createRequire(import.meta.url);
|
|
@@ -229,6 +231,7 @@ program
|
|
|
229
231
|
});
|
|
230
232
|
registerSpecCommand(program);
|
|
231
233
|
registerConfigCommand(program);
|
|
234
|
+
registerSchemaCommand(program);
|
|
232
235
|
// Top-level validate command
|
|
233
236
|
program
|
|
234
237
|
.command('validate [item-name]')
|
|
@@ -279,6 +282,22 @@ program
|
|
|
279
282
|
process.exit(1);
|
|
280
283
|
}
|
|
281
284
|
});
|
|
285
|
+
// Feedback command
|
|
286
|
+
program
|
|
287
|
+
.command('feedback <message>')
|
|
288
|
+
.description('Submit feedback about OpenSpec')
|
|
289
|
+
.option('--body <text>', 'Detailed description for the feedback')
|
|
290
|
+
.action(async (message, options) => {
|
|
291
|
+
try {
|
|
292
|
+
const feedbackCommand = new FeedbackCommand();
|
|
293
|
+
await feedbackCommand.execute(message, options);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
console.log();
|
|
297
|
+
ora().fail(`Error: ${error.message}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
282
301
|
// Completion command with subcommands
|
|
283
302
|
const completionCmd = program
|
|
284
303
|
.command('completion')
|
|
@@ -16,6 +16,7 @@ import { loadChangeContext, formatChangeStatus, generateInstructions, listSchema
|
|
|
16
16
|
import { createChange, validateChangeName } from '../utils/change-utils.js';
|
|
17
17
|
import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
|
|
18
18
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
19
|
+
import { promptForConfig, serializeConfig, isExitPromptError } from '../core/config-prompts.js';
|
|
19
20
|
const DEFAULT_SCHEMA = 'spec-driven';
|
|
20
21
|
/**
|
|
21
22
|
* Checks if color output is disabled via NO_COLOR env or --no-color flag.
|
|
@@ -97,11 +98,14 @@ async function validateChangeExists(changeName, projectRoot) {
|
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
99
100
|
* Validates that a schema exists and returns available schemas if not.
|
|
101
|
+
*
|
|
102
|
+
* @param schemaName - The schema name to validate
|
|
103
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
100
104
|
*/
|
|
101
|
-
function validateSchemaExists(schemaName) {
|
|
102
|
-
const schemaDir = getSchemaDir(schemaName);
|
|
105
|
+
function validateSchemaExists(schemaName, projectRoot) {
|
|
106
|
+
const schemaDir = getSchemaDir(schemaName, projectRoot);
|
|
103
107
|
if (!schemaDir) {
|
|
104
|
-
const availableSchemas = listSchemas();
|
|
108
|
+
const availableSchemas = listSchemas(projectRoot);
|
|
105
109
|
throw new Error(`Schema '${schemaName}' not found. Available schemas:\n ${availableSchemas.join('\n ')}`);
|
|
106
110
|
}
|
|
107
111
|
return schemaName;
|
|
@@ -113,7 +117,7 @@ async function statusCommand(options) {
|
|
|
113
117
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
114
118
|
// Validate schema if explicitly provided
|
|
115
119
|
if (options.schema) {
|
|
116
|
-
validateSchemaExists(options.schema);
|
|
120
|
+
validateSchemaExists(options.schema, projectRoot);
|
|
117
121
|
}
|
|
118
122
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
119
123
|
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
|
@@ -158,7 +162,7 @@ async function instructionsCommand(artifactId, options) {
|
|
|
158
162
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
159
163
|
// Validate schema if explicitly provided
|
|
160
164
|
if (options.schema) {
|
|
161
|
-
validateSchemaExists(options.schema);
|
|
165
|
+
validateSchemaExists(options.schema, projectRoot);
|
|
162
166
|
}
|
|
163
167
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
164
168
|
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
|
@@ -173,7 +177,7 @@ async function instructionsCommand(artifactId, options) {
|
|
|
173
177
|
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
|
174
178
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
|
|
175
179
|
}
|
|
176
|
-
const instructions = generateInstructions(context, artifactId);
|
|
180
|
+
const instructions = generateInstructions(context, artifactId, projectRoot);
|
|
177
181
|
const isBlocked = instructions.dependencies.some((d) => !d.done);
|
|
178
182
|
spinner.stop();
|
|
179
183
|
if (options.json) {
|
|
@@ -433,7 +437,7 @@ async function applyInstructionsCommand(options) {
|
|
|
433
437
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
434
438
|
// Validate schema if explicitly provided
|
|
435
439
|
if (options.schema) {
|
|
436
|
-
validateSchemaExists(options.schema);
|
|
440
|
+
validateSchemaExists(options.schema, projectRoot);
|
|
437
441
|
}
|
|
438
442
|
// generateApplyInstructions uses loadChangeContext which auto-detects schema
|
|
439
443
|
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema);
|
|
@@ -498,18 +502,29 @@ function printApplyInstructionsText(instructions) {
|
|
|
498
502
|
async function templatesCommand(options) {
|
|
499
503
|
const spinner = ora('Loading templates...').start();
|
|
500
504
|
try {
|
|
501
|
-
const
|
|
502
|
-
const
|
|
505
|
+
const projectRoot = process.cwd();
|
|
506
|
+
const schemaName = validateSchemaExists(options.schema ?? DEFAULT_SCHEMA, projectRoot);
|
|
507
|
+
const schema = resolveSchema(schemaName, projectRoot);
|
|
503
508
|
const graph = ArtifactGraph.fromSchema(schema);
|
|
504
|
-
const schemaDir = getSchemaDir(schemaName);
|
|
505
|
-
// Determine
|
|
506
|
-
const { getUserSchemasDir } = await import('../core/artifact-graph/resolver.js');
|
|
509
|
+
const schemaDir = getSchemaDir(schemaName, projectRoot);
|
|
510
|
+
// Determine the source (project, user, or package)
|
|
511
|
+
const { getUserSchemasDir, getProjectSchemasDir, } = await import('../core/artifact-graph/resolver.js');
|
|
512
|
+
const projectSchemasDir = getProjectSchemasDir(projectRoot);
|
|
507
513
|
const userSchemasDir = getUserSchemasDir();
|
|
508
|
-
|
|
514
|
+
let source;
|
|
515
|
+
if (schemaDir.startsWith(projectSchemasDir)) {
|
|
516
|
+
source = 'project';
|
|
517
|
+
}
|
|
518
|
+
else if (schemaDir.startsWith(userSchemasDir)) {
|
|
519
|
+
source = 'user';
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
source = 'package';
|
|
523
|
+
}
|
|
509
524
|
const templates = graph.getAllArtifacts().map((artifact) => ({
|
|
510
525
|
artifactId: artifact.id,
|
|
511
526
|
templatePath: path.join(schemaDir, 'templates', artifact.template),
|
|
512
|
-
source
|
|
527
|
+
source,
|
|
513
528
|
}));
|
|
514
529
|
spinner.stop();
|
|
515
530
|
if (options.json) {
|
|
@@ -521,7 +536,7 @@ async function templatesCommand(options) {
|
|
|
521
536
|
return;
|
|
522
537
|
}
|
|
523
538
|
console.log(`Schema: ${schemaName}`);
|
|
524
|
-
console.log(`Source: ${
|
|
539
|
+
console.log(`Source: ${source}`);
|
|
525
540
|
console.log();
|
|
526
541
|
for (const t of templates) {
|
|
527
542
|
console.log(`${t.artifactId}:`);
|
|
@@ -541,15 +556,15 @@ async function newChangeCommand(name, options) {
|
|
|
541
556
|
if (!validation.valid) {
|
|
542
557
|
throw new Error(validation.error);
|
|
543
558
|
}
|
|
559
|
+
const projectRoot = process.cwd();
|
|
544
560
|
// Validate schema if provided
|
|
545
561
|
if (options.schema) {
|
|
546
|
-
validateSchemaExists(options.schema);
|
|
562
|
+
validateSchemaExists(options.schema, projectRoot);
|
|
547
563
|
}
|
|
548
564
|
const schemaDisplay = options.schema ? ` with schema '${options.schema}'` : '';
|
|
549
565
|
const spinner = ora(`Creating change '${name}'${schemaDisplay}...`).start();
|
|
550
566
|
try {
|
|
551
|
-
const
|
|
552
|
-
await createChange(projectRoot, name, { schema: options.schema });
|
|
567
|
+
const result = await createChange(projectRoot, name, { schema: options.schema });
|
|
553
568
|
// If description provided, create README.md with description
|
|
554
569
|
if (options.description) {
|
|
555
570
|
const { promises: fs } = await import('fs');
|
|
@@ -557,8 +572,7 @@ async function newChangeCommand(name, options) {
|
|
|
557
572
|
const readmePath = path.join(changeDir, 'README.md');
|
|
558
573
|
await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
|
|
559
574
|
}
|
|
560
|
-
|
|
561
|
-
spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${schemaUsed})`);
|
|
575
|
+
spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${result.schema})`);
|
|
562
576
|
}
|
|
563
577
|
catch (error) {
|
|
564
578
|
spinner.fail(`Failed to create change '${name}'`);
|
|
@@ -667,6 +681,118 @@ ${template.content}
|
|
|
667
681
|
console.log(chalk.green(' ✓ ' + file));
|
|
668
682
|
}
|
|
669
683
|
console.log();
|
|
684
|
+
// Config creation section
|
|
685
|
+
console.log('━'.repeat(70));
|
|
686
|
+
console.log();
|
|
687
|
+
console.log(chalk.bold('📋 Project Configuration (Optional)'));
|
|
688
|
+
console.log();
|
|
689
|
+
console.log('Configure project defaults for OpenSpec workflows.');
|
|
690
|
+
console.log();
|
|
691
|
+
// Check if config already exists
|
|
692
|
+
const configPath = path.join(projectRoot, 'openspec', 'config.yaml');
|
|
693
|
+
const configYmlPath = path.join(projectRoot, 'openspec', 'config.yml');
|
|
694
|
+
const configExists = fs.existsSync(configPath) || fs.existsSync(configYmlPath);
|
|
695
|
+
if (configExists) {
|
|
696
|
+
// Config already exists, skip creation
|
|
697
|
+
console.log(chalk.blue('ℹ️ openspec/config.yaml already exists. Skipping config creation.'));
|
|
698
|
+
console.log();
|
|
699
|
+
console.log(' To update config, edit openspec/config.yaml manually or:');
|
|
700
|
+
console.log(' 1. Delete openspec/config.yaml');
|
|
701
|
+
console.log(' 2. Run openspec artifact-experimental-setup again');
|
|
702
|
+
console.log();
|
|
703
|
+
}
|
|
704
|
+
else if (!process.stdin.isTTY) {
|
|
705
|
+
// Non-interactive mode (CI, automation, piped input)
|
|
706
|
+
console.log(chalk.blue('ℹ️ Skipping config prompts (non-interactive mode)'));
|
|
707
|
+
console.log();
|
|
708
|
+
console.log(' To create config manually, add openspec/config.yaml with:');
|
|
709
|
+
console.log(chalk.dim(' schema: spec-driven'));
|
|
710
|
+
console.log();
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
// Prompt for config creation
|
|
714
|
+
try {
|
|
715
|
+
const configResult = await promptForConfig(projectRoot);
|
|
716
|
+
if (configResult.createConfig && configResult.schema) {
|
|
717
|
+
// Build config object
|
|
718
|
+
const config = {
|
|
719
|
+
schema: configResult.schema,
|
|
720
|
+
context: configResult.context,
|
|
721
|
+
rules: configResult.rules,
|
|
722
|
+
};
|
|
723
|
+
// Serialize to YAML
|
|
724
|
+
const yamlContent = serializeConfig(config);
|
|
725
|
+
// Write config file
|
|
726
|
+
try {
|
|
727
|
+
await FileSystemUtils.writeFile(configPath, yamlContent);
|
|
728
|
+
console.log();
|
|
729
|
+
console.log(chalk.green('✓ Created openspec/config.yaml'));
|
|
730
|
+
console.log();
|
|
731
|
+
console.log('━'.repeat(70));
|
|
732
|
+
console.log();
|
|
733
|
+
console.log(chalk.bold('📖 Config created at: openspec/config.yaml'));
|
|
734
|
+
// Display summary
|
|
735
|
+
const contextLines = config.context ? config.context.split('\n').length : 0;
|
|
736
|
+
const rulesCount = config.rules ? Object.keys(config.rules).length : 0;
|
|
737
|
+
console.log(` • Default schema: ${chalk.cyan(config.schema)}`);
|
|
738
|
+
if (contextLines > 0) {
|
|
739
|
+
console.log(` • Project context: ${chalk.cyan(`Added (${contextLines} lines)`)}`);
|
|
740
|
+
}
|
|
741
|
+
if (rulesCount > 0) {
|
|
742
|
+
console.log(` • Rules: ${chalk.cyan(`${rulesCount} artifact${rulesCount > 1 ? 's' : ''} configured`)}`);
|
|
743
|
+
}
|
|
744
|
+
console.log();
|
|
745
|
+
// Usage examples
|
|
746
|
+
console.log(chalk.bold('Usage:'));
|
|
747
|
+
console.log(' • New changes automatically use this schema');
|
|
748
|
+
console.log(' • Context injected into all artifact instructions');
|
|
749
|
+
console.log(' • Rules applied to matching artifacts');
|
|
750
|
+
console.log();
|
|
751
|
+
// Git commit suggestion
|
|
752
|
+
console.log(chalk.bold('To share with team:'));
|
|
753
|
+
console.log(chalk.dim(' git add openspec/config.yaml .claude/'));
|
|
754
|
+
console.log(chalk.dim(' git commit -m "Setup OpenSpec experimental workflow with project config"'));
|
|
755
|
+
console.log();
|
|
756
|
+
}
|
|
757
|
+
catch (writeError) {
|
|
758
|
+
// Handle file write errors
|
|
759
|
+
console.error();
|
|
760
|
+
console.error(chalk.red('✗ Failed to write openspec/config.yaml'));
|
|
761
|
+
console.error(chalk.dim(` ${writeError.message}`));
|
|
762
|
+
console.error();
|
|
763
|
+
console.error('Fallback: Create config manually:');
|
|
764
|
+
console.error(chalk.dim(' 1. Create openspec/config.yaml'));
|
|
765
|
+
console.error(chalk.dim(' 2. Copy the following content:'));
|
|
766
|
+
console.error();
|
|
767
|
+
console.error(chalk.dim(yamlContent));
|
|
768
|
+
console.error();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
// User chose not to create config
|
|
773
|
+
console.log();
|
|
774
|
+
console.log(chalk.blue('ℹ️ Skipped config creation.'));
|
|
775
|
+
console.log(' You can create openspec/config.yaml manually later.');
|
|
776
|
+
console.log();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (promptError) {
|
|
780
|
+
if (isExitPromptError(promptError)) {
|
|
781
|
+
// User cancelled (Ctrl+C)
|
|
782
|
+
console.log();
|
|
783
|
+
console.log(chalk.blue('ℹ️ Config creation cancelled'));
|
|
784
|
+
console.log(' Skills and commands already created');
|
|
785
|
+
console.log(' Run setup again to create config later');
|
|
786
|
+
console.log();
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
// Unexpected error
|
|
790
|
+
throw promptError;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
console.log('━'.repeat(70));
|
|
795
|
+
console.log();
|
|
670
796
|
console.log(chalk.bold('📖 Usage:'));
|
|
671
797
|
console.log();
|
|
672
798
|
console.log(' ' + chalk.cyan('Skills') + ' work automatically in compatible editors:');
|
|
@@ -699,7 +825,8 @@ ${template.content}
|
|
|
699
825
|
}
|
|
700
826
|
}
|
|
701
827
|
async function schemasCommand(options) {
|
|
702
|
-
const
|
|
828
|
+
const projectRoot = process.cwd();
|
|
829
|
+
const schemas = listSchemasWithInfo(projectRoot);
|
|
703
830
|
if (options.json) {
|
|
704
831
|
console.log(JSON.stringify(schemas, null, 2));
|
|
705
832
|
return;
|
|
@@ -707,7 +834,13 @@ async function schemasCommand(options) {
|
|
|
707
834
|
console.log('Available schemas:');
|
|
708
835
|
console.log();
|
|
709
836
|
for (const schema of schemas) {
|
|
710
|
-
|
|
837
|
+
let sourceLabel = '';
|
|
838
|
+
if (schema.source === 'project') {
|
|
839
|
+
sourceLabel = chalk.cyan(' (project)');
|
|
840
|
+
}
|
|
841
|
+
else if (schema.source === 'user') {
|
|
842
|
+
sourceLabel = chalk.dim(' (user override)');
|
|
843
|
+
}
|
|
711
844
|
console.log(` ${chalk.bold(schema.name)}${sourceLabel}`);
|
|
712
845
|
console.log(` ${schema.description}`);
|
|
713
846
|
console.log(` Artifacts: ${schema.artifacts.join(' → ')}`);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execSync, execFileSync } from 'child_process';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
/**
|
|
6
|
+
* Check if gh CLI is installed and available in PATH
|
|
7
|
+
* Uses platform-appropriate command: 'where' on Windows, 'which' on Unix/macOS
|
|
8
|
+
*/
|
|
9
|
+
function isGhInstalled() {
|
|
10
|
+
try {
|
|
11
|
+
const command = process.platform === 'win32' ? 'where gh' : 'which gh';
|
|
12
|
+
execSync(command, { stdio: 'pipe' });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if gh CLI is authenticated
|
|
21
|
+
*/
|
|
22
|
+
function isGhAuthenticated() {
|
|
23
|
+
try {
|
|
24
|
+
execSync('gh auth status', { stdio: 'pipe' });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get OpenSpec version from package.json
|
|
33
|
+
*/
|
|
34
|
+
function getVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const { version } = require('../../package.json');
|
|
37
|
+
return version;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get platform name
|
|
45
|
+
*/
|
|
46
|
+
function getPlatform() {
|
|
47
|
+
return os.platform();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get current timestamp in ISO format
|
|
51
|
+
*/
|
|
52
|
+
function getTimestamp() {
|
|
53
|
+
return new Date().toISOString();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate metadata footer for feedback
|
|
57
|
+
*/
|
|
58
|
+
function generateMetadata() {
|
|
59
|
+
const version = getVersion();
|
|
60
|
+
const platform = getPlatform();
|
|
61
|
+
const timestamp = getTimestamp();
|
|
62
|
+
return `---
|
|
63
|
+
Submitted via OpenSpec CLI
|
|
64
|
+
- Version: ${version}
|
|
65
|
+
- Platform: ${platform}
|
|
66
|
+
- Timestamp: ${timestamp}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Format the feedback title
|
|
70
|
+
*/
|
|
71
|
+
function formatTitle(message) {
|
|
72
|
+
return `Feedback: ${message}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format the full feedback body
|
|
76
|
+
*/
|
|
77
|
+
function formatBody(bodyText) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (bodyText) {
|
|
80
|
+
parts.push(bodyText);
|
|
81
|
+
parts.push(''); // Empty line before metadata
|
|
82
|
+
}
|
|
83
|
+
parts.push(generateMetadata());
|
|
84
|
+
return parts.join('\n');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generate a pre-filled GitHub issue URL for manual submission
|
|
88
|
+
*/
|
|
89
|
+
function generateManualSubmissionUrl(title, body) {
|
|
90
|
+
const repo = 'Fission-AI/OpenSpec';
|
|
91
|
+
const encodedTitle = encodeURIComponent(title);
|
|
92
|
+
const encodedBody = encodeURIComponent(body);
|
|
93
|
+
const encodedLabels = encodeURIComponent('feedback');
|
|
94
|
+
return `https://github.com/${repo}/issues/new?title=${encodedTitle}&body=${encodedBody}&labels=${encodedLabels}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Display formatted feedback content for manual submission
|
|
98
|
+
*/
|
|
99
|
+
function displayFormattedFeedback(title, body) {
|
|
100
|
+
console.log('\n--- FORMATTED FEEDBACK ---');
|
|
101
|
+
console.log(`Title: ${title}`);
|
|
102
|
+
console.log(`Labels: feedback`);
|
|
103
|
+
console.log('\nBody:');
|
|
104
|
+
console.log(body);
|
|
105
|
+
console.log('--- END FEEDBACK ---\n');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Submit feedback via gh CLI
|
|
109
|
+
* Uses execFileSync to prevent shell injection vulnerabilities
|
|
110
|
+
*/
|
|
111
|
+
function submitViaGhCli(title, body) {
|
|
112
|
+
try {
|
|
113
|
+
const result = execFileSync('gh', [
|
|
114
|
+
'issue',
|
|
115
|
+
'create',
|
|
116
|
+
'--repo',
|
|
117
|
+
'Fission-AI/OpenSpec',
|
|
118
|
+
'--title',
|
|
119
|
+
title,
|
|
120
|
+
'--body',
|
|
121
|
+
body,
|
|
122
|
+
'--label',
|
|
123
|
+
'feedback',
|
|
124
|
+
], { encoding: 'utf-8', stdio: 'pipe' });
|
|
125
|
+
const issueUrl = result.trim();
|
|
126
|
+
console.log(`\n✓ Feedback submitted successfully!`);
|
|
127
|
+
console.log(`Issue URL: ${issueUrl}\n`);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Display the error output from gh CLI
|
|
131
|
+
if (error.stderr) {
|
|
132
|
+
console.error(error.stderr.toString());
|
|
133
|
+
}
|
|
134
|
+
else if (error.message) {
|
|
135
|
+
console.error(error.message);
|
|
136
|
+
}
|
|
137
|
+
// Exit with the same code as gh CLI
|
|
138
|
+
process.exit(error.status ?? 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Handle fallback when gh CLI is not available or not authenticated
|
|
143
|
+
*/
|
|
144
|
+
function handleFallback(title, body, reason) {
|
|
145
|
+
if (reason === 'missing') {
|
|
146
|
+
console.log('⚠️ GitHub CLI not found. Manual submission required.');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.log('⚠️ GitHub authentication required. Manual submission required.');
|
|
150
|
+
}
|
|
151
|
+
displayFormattedFeedback(title, body);
|
|
152
|
+
const manualUrl = generateManualSubmissionUrl(title, body);
|
|
153
|
+
console.log('Please submit your feedback manually:');
|
|
154
|
+
console.log(manualUrl);
|
|
155
|
+
if (reason === 'unauthenticated') {
|
|
156
|
+
console.log('\nTo auto-submit in the future: gh auth login');
|
|
157
|
+
}
|
|
158
|
+
// Exit with success code (fallback is successful)
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Feedback command implementation
|
|
163
|
+
*/
|
|
164
|
+
export class FeedbackCommand {
|
|
165
|
+
async execute(message, options) {
|
|
166
|
+
// Format title and body once for all code paths
|
|
167
|
+
const title = formatTitle(message);
|
|
168
|
+
const body = formatBody(options?.body);
|
|
169
|
+
// Check if gh CLI is installed
|
|
170
|
+
if (!isGhInstalled()) {
|
|
171
|
+
handleFallback(title, body, 'missing');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Check if gh CLI is authenticated
|
|
175
|
+
if (!isGhAuthenticated()) {
|
|
176
|
+
handleFallback(title, body, 'unauthenticated');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Submit via gh CLI
|
|
180
|
+
submitViaGhCli(title, body);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=feedback.js.map
|