diff --git a/README.md b/README.md index 7b6c7354..ec55265d 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,52 @@

- + - - - OpenSpec logo + + + OpenSplx logo - +

Spec-driven development for AI coding assistants.

- CI - npm version + Fork of OpenSpec node version License: MIT - Conventional Commits - Discord

- OpenSpec dashboard preview + OpenSplx dashboard preview

+# OpenSplx + +> **Fork Notice:** OpenSplx is a community fork of [OpenSpec](https://github.com/Fission-AI/OpenSpec). +> It adds the `plx` command alias and extended features while maintaining full compatibility +> with the original OpenSpec workflow. + +## What's Different in OpenSplx + +| Feature | OpenSpec | OpenSplx | +|---------|----------|----------| +| Command | `openspec` | `openspec` + `plx` alias | +| Install | `npm i -g @fission-ai/openspec` | Clone & `npm link` (local) | + +### Quick Start (OpenSplx) + +```bash +git clone https://github.com/appyboypov/OpenSplx.git +cd OpenSplx +pnpm install && pnpm build +npm link +plx --version # or openspec --version +``` + +--- + +
+Original OpenSpec Documentation (click to expand) +

Follow @0xTab on X for updates ยท Join the OpenSpec Discord for help and questions.

@@ -379,3 +404,5 @@ Run `openspec update` whenever someone switches tools so your agents pick up the ## License MIT + +
diff --git a/assets/opensplx_pixel_dark.svg b/assets/opensplx_pixel_dark.svg new file mode 100644 index 00000000..40d18f7e --- /dev/null +++ b/assets/opensplx_pixel_dark.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/opensplx_pixel_light.svg b/assets/opensplx_pixel_light.svg new file mode 100644 index 00000000..9f9c273f --- /dev/null +++ b/assets/opensplx_pixel_light.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/plx.js b/bin/plx.js new file mode 100755 index 00000000..75996ba5 --- /dev/null +++ b/bin/plx.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import '../dist/cli/index.js'; diff --git a/package.json b/package.json index 9ad409fe..c0cc11be 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ } }, "bin": { - "openspec": "./bin/openspec.js" + "openspec": "./bin/openspec.js", + "plx": "./bin/plx.js" }, "files": [ "dist", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index bfe6e123..b10bfdec 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -18,6 +18,16 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +/** + * Detect the command name from the invocation path + */ +function getCommandName() { + const scriptPath = process.argv[1] || ''; + const scriptName = path.basename(scriptPath).replace(/\.js$/, ''); + // Default to 'openspec' for postinstall context + return scriptName === 'plx' ? 'plx' : 'openspec'; +} + /** * Check if we should skip installation */ @@ -72,7 +82,7 @@ async function installCompletions(shell) { // Check if shell is supported if (!CompletionFactory.isSupported(shell)) { - console.log(`\nTip: Run 'openspec completion install' for shell completions`); + console.log(`\nTip: Run 'openspec completion install' or 'plx completion install' for shell completions`); return; } @@ -99,11 +109,11 @@ async function installCompletions(shell) { } } else { // Installation failed, show tip for manual install - console.log(`\nTip: Run 'openspec completion install' for shell completions`); + console.log(`\nTip: Run 'openspec completion install' or 'plx completion install' for shell completions`); } } catch (error) { // Fail gracefully - show tip for manual install - console.log(`\nTip: Run 'openspec completion install' for shell completions`); + console.log(`\nTip: Run 'openspec completion install' or 'plx completion install' for shell completions`); } } @@ -127,7 +137,7 @@ async function main() { // Detect shell const shell = await detectShell(); if (!shell) { - console.log(`\nTip: Run 'openspec completion install' for shell completions`); + console.log(`\nTip: Run 'openspec completion install' or 'plx completion install' for shell completions`); return; } @@ -136,7 +146,7 @@ async function main() { } catch (error) { // Fail gracefully - never break npm install // Show tip for manual install - console.log(`\nTip: Run 'openspec completion install' for shell completions`); + console.log(`\nTip: Run 'openspec completion install' or 'plx completion install' for shell completions`); } } diff --git a/src/cli/index.ts b/src/cli/index.ts index 0cd5885f..a58e8b9c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -15,12 +15,15 @@ import { ValidateCommand } from '../commands/validate.js'; import { ShowCommand } from '../commands/show.js'; import { CompletionCommand } from '../commands/completion.js'; +// Import command name detection utility +import { commandName } from '../utils/command-name.js'; + const program = new Command(); const require = createRequire(import.meta.url); const { version } = require('../../package.json'); program - .name('openspec') + .name(commandName) .description('AI-native system for spec-driven development') .version(version); diff --git a/src/commands/completion.ts b/src/commands/completion.ts index 2baedc0c..f57f81f4 100644 --- a/src/commands/completion.ts +++ b/src/commands/completion.ts @@ -5,6 +5,7 @@ import { COMMAND_REGISTRY } from '../core/completions/command-registry.js'; import { detectShell, SupportedShell } from '../utils/shell-detection.js'; import { CompletionProvider } from '../core/completions/completion-provider.js'; import { getArchivedChangeIds } from '../utils/item-discovery.js'; +import { commandName } from '../utils/command-name.js'; interface GenerateOptions { shell?: string; @@ -59,7 +60,7 @@ export class CompletionCommand { // No shell specified and cannot auto-detect console.error('Error: Could not auto-detect shell. Please specify shell explicitly.'); - console.error(`Usage: openspec completion ${operationName} [shell]`); + console.error(`Usage: ${commandName} completion ${operationName} [shell]`); console.error(`Currently supported: ${CompletionFactory.getSupportedShells().join(', ')}`); process.exitCode = 1; return null; @@ -115,7 +116,7 @@ export class CompletionCommand { */ private async generateForShell(shell: SupportedShell): Promise { const generator = CompletionFactory.createGenerator(shell); - const script = generator.generate(COMMAND_REGISTRY); + const script = generator.generate(COMMAND_REGISTRY, commandName); console.log(script); } @@ -126,11 +127,11 @@ export class CompletionCommand { const generator = CompletionFactory.createGenerator(shell); const installer = CompletionFactory.createInstaller(shell); - const spinner = ora(`Installing ${shell} completion script...`).start(); + const spinner = ora(`Installing ${shell} completion script for ${commandName}...`).start(); try { // Generate the completion script - const script = generator.generate(COMMAND_REGISTRY); + const script = generator.generate(COMMAND_REGISTRY, commandName); // Install it const result = await installer.install(script); @@ -180,7 +181,7 @@ export class CompletionCommand { // Prompt for confirmation unless --yes flag is provided if (!skipConfirmation) { const confirmed = await confirm({ - message: 'Remove OpenSpec configuration from ~/.zshrc?', + message: `Remove ${commandName} configuration from ~/.zshrc?`, default: false, }); diff --git a/src/core/completions/generators/zsh-generator.ts b/src/core/completions/generators/zsh-generator.ts index 765fd5b4..75f39ae4 100644 --- a/src/core/completions/generators/zsh-generator.ts +++ b/src/core/completions/generators/zsh-generator.ts @@ -1,8 +1,8 @@ import { CompletionGenerator, CommandDefinition, FlagDefinition } from '../types.js'; /** - * Generates Zsh completion scripts for the OpenSpec CLI. - * Follows Zsh completion system conventions using the _openspec function. + * Generates Zsh completion scripts for the OpenSpec/OpenSplx CLI. + * Follows Zsh completion system conventions. */ export class ZshGenerator implements CompletionGenerator { readonly shell = 'zsh' as const; @@ -11,20 +11,21 @@ export class ZshGenerator implements CompletionGenerator { * Generate a Zsh completion script * * @param commands - Command definitions to generate completions for + * @param commandName - The CLI command name (defaults to 'openspec') * @returns Zsh completion script as a string */ - generate(commands: CommandDefinition[]): string { + generate(commands: CommandDefinition[], commandName: string = 'openspec'): string { const script: string[] = []; // Header comment - script.push('#compdef openspec'); + script.push(`#compdef ${commandName}`); script.push(''); - script.push('# Zsh completion script for OpenSpec CLI'); + script.push(`# Zsh completion script for ${commandName} CLI`); script.push('# Auto-generated - do not edit manually'); script.push(''); // Main completion function - script.push('_openspec() {'); + script.push(`_${commandName}() {`); script.push(' local context state line'); script.push(' typeset -A opt_args'); script.push(''); @@ -48,7 +49,7 @@ export class ZshGenerator implements CompletionGenerator { // Command dispatch logic script.push(' case $state in'); script.push(' command)'); - script.push(' _describe "openspec command" commands'); + script.push(` _describe "${commandName} command" commands`); script.push(' ;;'); script.push(' args)'); script.push(' case $words[1] in'); @@ -56,7 +57,7 @@ export class ZshGenerator implements CompletionGenerator { // Generate completion for each command for (const cmd of commands) { script.push(` ${cmd.name})`); - script.push(` _openspec_${this.sanitizeFunctionName(cmd.name)}`); + script.push(` _${commandName}_${this.sanitizeFunctionName(cmd.name)}`); script.push(' ;;'); } @@ -68,15 +69,15 @@ export class ZshGenerator implements CompletionGenerator { // Generate individual command completion functions for (const cmd of commands) { - script.push(...this.generateCommandFunction(cmd)); + script.push(...this.generateCommandFunction(cmd, commandName)); script.push(''); } // Add dynamic completion helper functions - script.push(...this.generateDynamicCompletionHelpers()); + script.push(...this.generateDynamicCompletionHelpers(commandName)); // Register the completion function - script.push('compdef _openspec openspec'); + script.push(`compdef _${commandName} ${commandName}`); script.push(''); return script.join('\n'); @@ -128,44 +129,44 @@ export class ZshGenerator implements CompletionGenerator { /** * Generate dynamic completion helper functions for change and spec IDs */ - private generateDynamicCompletionHelpers(): string[] { + private generateDynamicCompletionHelpers(commandName: string): string[] { const lines: string[] = []; lines.push('# Dynamic completion helpers'); lines.push(''); // Helper function for completing change IDs - lines.push('# Use openspec __complete to get available changes'); - lines.push('_openspec_complete_changes() {'); + lines.push(`# Use ${commandName} __complete to get available changes`); + lines.push(`_${commandName}_complete_changes() {`); lines.push(' local -a changes'); lines.push(' while IFS=$\'\\t\' read -r id desc; do'); lines.push(' changes+=("$id:$desc")'); - lines.push(' done < <(openspec __complete changes 2>/dev/null)'); + lines.push(` done < <(${commandName} __complete changes 2>/dev/null)`); lines.push(' _describe "change" changes'); lines.push('}'); lines.push(''); // Helper function for completing spec IDs - lines.push('# Use openspec __complete to get available specs'); - lines.push('_openspec_complete_specs() {'); + lines.push(`# Use ${commandName} __complete to get available specs`); + lines.push(`_${commandName}_complete_specs() {`); lines.push(' local -a specs'); lines.push(' while IFS=$\'\\t\' read -r id desc; do'); lines.push(' specs+=("$id:$desc")'); - lines.push(' done < <(openspec __complete specs 2>/dev/null)'); + lines.push(` done < <(${commandName} __complete specs 2>/dev/null)`); lines.push(' _describe "spec" specs'); lines.push('}'); lines.push(''); // Helper function for completing both changes and specs lines.push('# Get both changes and specs'); - lines.push('_openspec_complete_items() {'); + lines.push(`_${commandName}_complete_items() {`); lines.push(' local -a items'); lines.push(' while IFS=$\'\\t\' read -r id desc; do'); lines.push(' items+=("$id:$desc")'); - lines.push(' done < <(openspec __complete changes 2>/dev/null)'); + lines.push(` done < <(${commandName} __complete changes 2>/dev/null)`); lines.push(' while IFS=$\'\\t\' read -r id desc; do'); lines.push(' items+=("$id:$desc")'); - lines.push(' done < <(openspec __complete specs 2>/dev/null)'); + lines.push(` done < <(${commandName} __complete specs 2>/dev/null)`); lines.push(' _describe "item" items'); lines.push('}'); lines.push(''); @@ -176,8 +177,8 @@ export class ZshGenerator implements CompletionGenerator { /** * Generate completion function for a specific command */ - private generateCommandFunction(cmd: CommandDefinition): string[] { - const funcName = `_openspec_${this.sanitizeFunctionName(cmd.name)}`; + private generateCommandFunction(cmd: CommandDefinition, commandName: string): string[] { + const funcName = `_${commandName}_${this.sanitizeFunctionName(cmd.name)}`; const lines: string[] = []; lines.push(`${funcName}() {`); @@ -216,7 +217,7 @@ export class ZshGenerator implements CompletionGenerator { for (const subcmd of cmd.subcommands) { lines.push(` ${subcmd.name})`); - lines.push(` _openspec_${this.sanitizeFunctionName(cmd.name)}_${this.sanitizeFunctionName(subcmd.name)}`); + lines.push(` _${commandName}_${this.sanitizeFunctionName(cmd.name)}_${this.sanitizeFunctionName(subcmd.name)}`); lines.push(' ;;'); } @@ -234,7 +235,7 @@ export class ZshGenerator implements CompletionGenerator { // Add positional argument completion if (cmd.acceptsPositional) { - const positionalSpec = this.generatePositionalSpec(cmd.positionalType); + const positionalSpec = this.generatePositionalSpec(cmd.positionalType, commandName); lines.push(' ' + positionalSpec); } else { // Remove trailing backslash from last flag @@ -250,7 +251,7 @@ export class ZshGenerator implements CompletionGenerator { if (cmd.subcommands) { for (const subcmd of cmd.subcommands) { lines.push(''); - lines.push(...this.generateSubcommandFunction(cmd.name, subcmd)); + lines.push(...this.generateSubcommandFunction(cmd.name, subcmd, commandName)); } } @@ -260,8 +261,8 @@ export class ZshGenerator implements CompletionGenerator { /** * Generate completion function for a subcommand */ - private generateSubcommandFunction(parentName: string, subcmd: CommandDefinition): string[] { - const funcName = `_openspec_${this.sanitizeFunctionName(parentName)}_${this.sanitizeFunctionName(subcmd.name)}`; + private generateSubcommandFunction(parentName: string, subcmd: CommandDefinition, commandName: string): string[] { + const funcName = `_${commandName}_${this.sanitizeFunctionName(parentName)}_${this.sanitizeFunctionName(subcmd.name)}`; const lines: string[] = []; lines.push(`${funcName}() {`); @@ -274,7 +275,7 @@ export class ZshGenerator implements CompletionGenerator { // Add positional argument completion if (subcmd.acceptsPositional) { - const positionalSpec = this.generatePositionalSpec(subcmd.positionalType); + const positionalSpec = this.generatePositionalSpec(subcmd.positionalType, commandName); lines.push(' ' + positionalSpec); } else { // Remove trailing backslash from last flag @@ -326,14 +327,14 @@ export class ZshGenerator implements CompletionGenerator { /** * Generate positional argument specification */ - private generatePositionalSpec(positionalType?: string): string { + private generatePositionalSpec(positionalType?: string, commandName: string = 'openspec'): string { switch (positionalType) { case 'change-id': - return "'*: :_openspec_complete_changes'"; + return `'*: :_${commandName}_complete_changes'`; case 'spec-id': - return "'*: :_openspec_complete_specs'"; + return `'*: :_${commandName}_complete_specs'`; case 'change-or-spec-id': - return "'*: :_openspec_complete_items'"; + return `'*: :_${commandName}_complete_items'`; case 'path': return "'*:path:_files'"; case 'shell': diff --git a/src/core/completions/types.ts b/src/core/completions/types.ts index fef908b6..4d426e1b 100644 --- a/src/core/completions/types.ts +++ b/src/core/completions/types.ts @@ -84,7 +84,8 @@ export interface CompletionGenerator { * Generate the completion script content * * @param commands - Command definitions to generate completions for + * @param commandName - The CLI command name (e.g., 'openspec' or 'plx') * @returns The shell-specific completion script as a string */ - generate(commands: CommandDefinition[]): string; + generate(commands: CommandDefinition[], commandName?: string): string; } diff --git a/src/utils/command-name.ts b/src/utils/command-name.ts new file mode 100644 index 00000000..150d5ef7 --- /dev/null +++ b/src/utils/command-name.ts @@ -0,0 +1,16 @@ +import path from 'path'; + +/** + * Detect the CLI command name from the invocation path. + * Returns 'plx' if invoked via plx, otherwise defaults to 'openspec'. + */ +export function getCommandName(): string { + const scriptPath = process.argv[1] || ''; + const scriptName = path.basename(scriptPath).replace(/\.js$/, ''); + return scriptName === 'plx' ? 'plx' : 'openspec'; +} + +/** + * The detected command name for the current invocation. + */ +export const commandName = getCommandName(); diff --git a/test/core/completions/generators/zsh-generator.test.ts b/test/core/completions/generators/zsh-generator.test.ts index 74bef2ac..67682122 100644 --- a/test/core/completions/generators/zsh-generator.test.ts +++ b/test/core/completions/generators/zsh-generator.test.ts @@ -32,7 +32,7 @@ describe('ZshGenerator', () => { const script = generator.generate(commands); expect(script).toContain('#compdef openspec'); - expect(script).toContain('# Zsh completion script for OpenSpec CLI'); + expect(script).toContain('# Zsh completion script for openspec CLI'); expect(script).toContain('_openspec() {'); });