@fission-ai/openspec 0.15.0 → 0.17.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 +21 -12
- package/dist/cli/index.js +67 -2
- package/dist/commands/change.js +4 -3
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +221 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/show.js +3 -2
- package/dist/commands/spec.js +4 -3
- package/dist/commands/validate.js +21 -2
- package/dist/core/archive.js +4 -1
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +362 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +51 -0
- package/dist/core/completions/factory.js +57 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
- package/dist/core/completions/generators/zsh-generator.js +319 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/types.d.ts +78 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.js +8 -6
- package/dist/core/configurators/iflow.d.ts +8 -0
- package/dist/core/configurators/iflow.js +15 -0
- package/dist/core/configurators/registry.js +3 -0
- package/dist/core/configurators/slash/antigravity.d.ts +9 -0
- package/dist/core/configurators/slash/antigravity.js +23 -0
- package/dist/core/configurators/slash/gemini.d.ts +3 -6
- package/dist/core/configurators/slash/gemini.js +4 -50
- package/dist/core/configurators/slash/iflow.d.ts +9 -0
- package/dist/core/configurators/slash/iflow.js +37 -0
- package/dist/core/configurators/slash/opencode.js +0 -3
- package/dist/core/configurators/slash/qwen.d.ts +3 -8
- package/dist/core/configurators/slash/qwen.js +11 -36
- package/dist/core/configurators/slash/registry.js +6 -0
- package/dist/core/configurators/slash/toml-base.d.ts +10 -0
- package/dist/core/configurators/slash/toml-base.js +53 -0
- package/dist/core/global-config.d.ts +29 -0
- package/dist/core/global-config.js +87 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/init.js +7 -0
- package/dist/core/templates/slash-command-templates.js +2 -1
- package/dist/utils/file-system.js +19 -3
- package/dist/utils/interactive.d.ts +12 -1
- package/dist/utils/interactive.js +7 -2
- package/dist/utils/item-discovery.d.ts +1 -0
- package/dist/utils/item-discovery.js +23 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +147 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FileSystemUtils } from '../../../utils/file-system.js';
|
|
2
|
+
import { SlashCommandConfigurator } from './base.js';
|
|
3
|
+
import { OPENSPEC_MARKERS } from '../../config.js';
|
|
4
|
+
export class TomlSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
5
|
+
getFrontmatter(_id) {
|
|
6
|
+
// TOML doesn't use separate frontmatter - it's all in one structure
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
// Override to generate TOML format with markers inside the prompt field
|
|
10
|
+
async generateAll(projectPath, _openspecDir) {
|
|
11
|
+
const createdOrUpdated = [];
|
|
12
|
+
for (const target of this.getTargets()) {
|
|
13
|
+
const body = this.getBody(target.id);
|
|
14
|
+
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
|
|
15
|
+
if (await FileSystemUtils.fileExists(filePath)) {
|
|
16
|
+
await this.updateBody(filePath, body);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const tomlContent = this.generateTOML(target.id, body);
|
|
20
|
+
await FileSystemUtils.writeFile(filePath, tomlContent);
|
|
21
|
+
}
|
|
22
|
+
createdOrUpdated.push(target.path);
|
|
23
|
+
}
|
|
24
|
+
return createdOrUpdated;
|
|
25
|
+
}
|
|
26
|
+
generateTOML(id, body) {
|
|
27
|
+
const description = this.getDescription(id);
|
|
28
|
+
// TOML format with triple-quoted string for multi-line prompt
|
|
29
|
+
// Markers are inside the prompt value
|
|
30
|
+
return `description = "${description}"
|
|
31
|
+
|
|
32
|
+
prompt = """
|
|
33
|
+
${OPENSPEC_MARKERS.start}
|
|
34
|
+
${body}
|
|
35
|
+
${OPENSPEC_MARKERS.end}
|
|
36
|
+
"""
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
// Override updateBody to handle TOML format
|
|
40
|
+
async updateBody(filePath, body) {
|
|
41
|
+
const content = await FileSystemUtils.readFile(filePath);
|
|
42
|
+
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
|
|
43
|
+
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
|
|
44
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
45
|
+
throw new Error(`Missing OpenSpec markers in ${filePath}`);
|
|
46
|
+
}
|
|
47
|
+
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
|
|
48
|
+
const after = content.slice(endIndex);
|
|
49
|
+
const updatedContent = `${before}\n${body}\n${after}`;
|
|
50
|
+
await FileSystemUtils.writeFile(filePath, updatedContent);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=toml-base.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const GLOBAL_CONFIG_DIR_NAME = "openspec";
|
|
2
|
+
export declare const GLOBAL_CONFIG_FILE_NAME = "config.json";
|
|
3
|
+
export interface GlobalConfig {
|
|
4
|
+
featureFlags?: Record<string, boolean>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Gets the global configuration directory path following XDG Base Directory Specification.
|
|
8
|
+
*
|
|
9
|
+
* - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
|
|
10
|
+
* - Unix/macOS fallback: ~/.config/openspec/
|
|
11
|
+
* - Windows fallback: %APPDATA%/openspec/
|
|
12
|
+
*/
|
|
13
|
+
export declare function getGlobalConfigDir(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Gets the path to the global config file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getGlobalConfigPath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Loads the global configuration from disk.
|
|
20
|
+
* Returns default configuration if file doesn't exist or is invalid.
|
|
21
|
+
* Merges loaded config with defaults to ensure new fields are available.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getGlobalConfig(): GlobalConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Saves the global configuration to disk.
|
|
26
|
+
* Creates the config directory if it doesn't exist.
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveGlobalConfig(config: GlobalConfig): void;
|
|
29
|
+
//# sourceMappingURL=global-config.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
// Constants
|
|
5
|
+
export const GLOBAL_CONFIG_DIR_NAME = 'openspec';
|
|
6
|
+
export const GLOBAL_CONFIG_FILE_NAME = 'config.json';
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
featureFlags: {}
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Gets the global configuration directory path following XDG Base Directory Specification.
|
|
12
|
+
*
|
|
13
|
+
* - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
|
|
14
|
+
* - Unix/macOS fallback: ~/.config/openspec/
|
|
15
|
+
* - Windows fallback: %APPDATA%/openspec/
|
|
16
|
+
*/
|
|
17
|
+
export function getGlobalConfigDir() {
|
|
18
|
+
// XDG_CONFIG_HOME takes precedence on all platforms when explicitly set
|
|
19
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
20
|
+
if (xdgConfigHome) {
|
|
21
|
+
return path.join(xdgConfigHome, GLOBAL_CONFIG_DIR_NAME);
|
|
22
|
+
}
|
|
23
|
+
const platform = os.platform();
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
// Windows: use %APPDATA%
|
|
26
|
+
const appData = process.env.APPDATA;
|
|
27
|
+
if (appData) {
|
|
28
|
+
return path.join(appData, GLOBAL_CONFIG_DIR_NAME);
|
|
29
|
+
}
|
|
30
|
+
// Fallback for Windows if APPDATA is not set
|
|
31
|
+
return path.join(os.homedir(), 'AppData', 'Roaming', GLOBAL_CONFIG_DIR_NAME);
|
|
32
|
+
}
|
|
33
|
+
// Unix/macOS fallback: ~/.config
|
|
34
|
+
return path.join(os.homedir(), '.config', GLOBAL_CONFIG_DIR_NAME);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gets the path to the global config file.
|
|
38
|
+
*/
|
|
39
|
+
export function getGlobalConfigPath() {
|
|
40
|
+
return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE_NAME);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Loads the global configuration from disk.
|
|
44
|
+
* Returns default configuration if file doesn't exist or is invalid.
|
|
45
|
+
* Merges loaded config with defaults to ensure new fields are available.
|
|
46
|
+
*/
|
|
47
|
+
export function getGlobalConfig() {
|
|
48
|
+
const configPath = getGlobalConfigPath();
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(configPath)) {
|
|
51
|
+
return { ...DEFAULT_CONFIG };
|
|
52
|
+
}
|
|
53
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
54
|
+
const parsed = JSON.parse(content);
|
|
55
|
+
// Merge with defaults (loaded values take precedence)
|
|
56
|
+
return {
|
|
57
|
+
...DEFAULT_CONFIG,
|
|
58
|
+
...parsed,
|
|
59
|
+
// Deep merge featureFlags
|
|
60
|
+
featureFlags: {
|
|
61
|
+
...DEFAULT_CONFIG.featureFlags,
|
|
62
|
+
...(parsed.featureFlags || {})
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Log warning for parse errors, but not for missing files
|
|
68
|
+
if (error instanceof SyntaxError) {
|
|
69
|
+
console.error(`Warning: Invalid JSON in ${configPath}, using defaults`);
|
|
70
|
+
}
|
|
71
|
+
return { ...DEFAULT_CONFIG };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Saves the global configuration to disk.
|
|
76
|
+
* Creates the config directory if it doesn't exist.
|
|
77
|
+
*/
|
|
78
|
+
export function saveGlobalConfig(config) {
|
|
79
|
+
const configDir = getGlobalConfigDir();
|
|
80
|
+
const configPath = getGlobalConfigPath();
|
|
81
|
+
// Create directory if it doesn't exist
|
|
82
|
+
if (!fs.existsSync(configDir)) {
|
|
83
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=global-config.js.map
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig } from './global-config.js';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
package/dist/core/init.js
CHANGED
|
@@ -567,6 +567,13 @@ export class InitCommand {
|
|
|
567
567
|
}
|
|
568
568
|
console.log();
|
|
569
569
|
console.log(PALETTE.midGray('Use `openspec update` to refresh shared OpenSpec instructions in the future.'));
|
|
570
|
+
// Show restart instruction if any tools were configured
|
|
571
|
+
if (created.length > 0 || refreshed.length > 0) {
|
|
572
|
+
console.log();
|
|
573
|
+
console.log(PALETTE.white('Important: Restart your IDE'));
|
|
574
|
+
console.log(PALETTE.midGray('Slash commands are loaded at startup. Please restart your coding assistant'));
|
|
575
|
+
console.log(PALETTE.midGray('to ensure the new /openspec commands appear in your command palette.'));
|
|
576
|
+
}
|
|
570
577
|
// Get the selected tool name(s) for display
|
|
571
578
|
const toolName = this.formatToolNames(selectedTools);
|
|
572
579
|
console.log();
|
|
@@ -2,7 +2,8 @@ const baseGuardrails = `**Guardrails**
|
|
|
2
2
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
|
3
3
|
- Keep changes tightly scoped to the requested outcome.
|
|
4
4
|
- Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`;
|
|
5
|
-
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files
|
|
5
|
+
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
|
6
|
+
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.`;
|
|
6
7
|
const proposalSteps = `**Steps**
|
|
7
8
|
1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
|
8
9
|
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
1
|
+
import { promises as fs, constants as fsConstants } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
function isMarkerOnOwnLine(content, markerIndex, markerLength) {
|
|
4
4
|
let leftIndex = markerIndex - 1;
|
|
@@ -72,11 +72,27 @@ export class FileSystemUtils {
|
|
|
72
72
|
if (!stats.isFile()) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
// On Windows, stats.mode doesn't reliably indicate write permissions.
|
|
76
|
+
// Use fs.access with W_OK to check actual write permissions cross-platform.
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(filePath, fsConstants.W_OK);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
76
84
|
}
|
|
77
85
|
catch (error) {
|
|
78
86
|
if (error.code === 'ENOENT') {
|
|
79
|
-
|
|
87
|
+
// File doesn't exist; check if we can write to the parent directory
|
|
88
|
+
const parentDir = path.dirname(filePath);
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(parentDir, fsConstants.W_OK);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
97
|
console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
|
|
82
98
|
return false;
|
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
type InteractiveOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Explicit "disable prompts" flag passed by internal callers.
|
|
4
|
+
*/
|
|
5
|
+
noInteractive?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Commander-style negated option: `--no-interactive` sets this to false.
|
|
8
|
+
*/
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function isInteractive(value?: boolean | InteractiveOptions): boolean;
|
|
12
|
+
export {};
|
|
2
13
|
//# sourceMappingURL=interactive.d.ts.map
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
if (
|
|
1
|
+
function resolveNoInteractive(value) {
|
|
2
|
+
if (typeof value === 'boolean')
|
|
3
|
+
return value;
|
|
4
|
+
return value?.noInteractive === true || value?.interactive === false;
|
|
5
|
+
}
|
|
6
|
+
export function isInteractive(value) {
|
|
7
|
+
if (resolveNoInteractive(value))
|
|
3
8
|
return false;
|
|
4
9
|
if (process.env.OPEN_SPEC_INTERACTIVE === '0')
|
|
5
10
|
return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function getActiveChangeIds(root?: string): Promise<string[]>;
|
|
2
2
|
export declare function getSpecIds(root?: string): Promise<string[]>;
|
|
3
|
+
export declare function getArchivedChangeIds(root?: string): Promise<string[]>;
|
|
3
4
|
//# sourceMappingURL=item-discovery.d.ts.map
|
|
@@ -46,4 +46,27 @@ export async function getSpecIds(root = process.cwd()) {
|
|
|
46
46
|
}
|
|
47
47
|
return result.sort();
|
|
48
48
|
}
|
|
49
|
+
export async function getArchivedChangeIds(root = process.cwd()) {
|
|
50
|
+
const archivePath = path.join(root, 'openspec', 'changes', 'archive');
|
|
51
|
+
try {
|
|
52
|
+
const entries = await fs.readdir(archivePath, { withFileTypes: true });
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
56
|
+
continue;
|
|
57
|
+
const proposalPath = path.join(archivePath, entry.name, 'proposal.md');
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(proposalPath);
|
|
60
|
+
result.push(entry.name);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// skip directories without proposal.md
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result.sort();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
49
72
|
//# sourceMappingURL=item-discovery.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported shell types for completion generation
|
|
3
|
+
*/
|
|
4
|
+
export type SupportedShell = 'zsh' | 'bash' | 'fish' | 'powershell';
|
|
5
|
+
/**
|
|
6
|
+
* Result of shell detection
|
|
7
|
+
*/
|
|
8
|
+
export interface ShellDetectionResult {
|
|
9
|
+
/** The detected shell if supported, otherwise undefined */
|
|
10
|
+
shell: SupportedShell | undefined;
|
|
11
|
+
/** The raw shell name detected (even if unsupported), or undefined if nothing detected */
|
|
12
|
+
detected: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Detects the current user's shell based on environment variables
|
|
16
|
+
*
|
|
17
|
+
* @returns Detection result with supported shell and raw detected name
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectShell(): ShellDetectionResult;
|
|
20
|
+
//# sourceMappingURL=shell-detection.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects the current user's shell based on environment variables
|
|
3
|
+
*
|
|
4
|
+
* @returns Detection result with supported shell and raw detected name
|
|
5
|
+
*/
|
|
6
|
+
export function detectShell() {
|
|
7
|
+
// Try SHELL environment variable first (Unix-like systems)
|
|
8
|
+
const shellPath = process.env.SHELL;
|
|
9
|
+
if (shellPath) {
|
|
10
|
+
const shellName = shellPath.toLowerCase();
|
|
11
|
+
if (shellName.includes('zsh')) {
|
|
12
|
+
return { shell: 'zsh', detected: 'zsh' };
|
|
13
|
+
}
|
|
14
|
+
if (shellName.includes('bash')) {
|
|
15
|
+
return { shell: 'bash', detected: 'bash' };
|
|
16
|
+
}
|
|
17
|
+
if (shellName.includes('fish')) {
|
|
18
|
+
return { shell: 'fish', detected: 'fish' };
|
|
19
|
+
}
|
|
20
|
+
// Shell detected but not supported
|
|
21
|
+
// Extract shell name from path (e.g., /bin/tcsh -> tcsh)
|
|
22
|
+
const match = shellPath.match(/\/([^/]+)$/);
|
|
23
|
+
const detectedName = match ? match[1] : shellPath;
|
|
24
|
+
return { shell: undefined, detected: detectedName };
|
|
25
|
+
}
|
|
26
|
+
// Check for PowerShell on Windows
|
|
27
|
+
// PSModulePath is a reliable PowerShell-specific environment variable
|
|
28
|
+
if (process.env.PSModulePath || process.platform === 'win32') {
|
|
29
|
+
const comspec = process.env.COMSPEC?.toLowerCase();
|
|
30
|
+
// If PSModulePath exists, we're definitely in PowerShell
|
|
31
|
+
if (process.env.PSModulePath) {
|
|
32
|
+
return { shell: 'powershell', detected: 'powershell' };
|
|
33
|
+
}
|
|
34
|
+
// On Windows without PSModulePath, we might be in cmd.exe
|
|
35
|
+
if (comspec?.includes('cmd.exe')) {
|
|
36
|
+
return { shell: undefined, detected: 'cmd.exe' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { shell: undefined, detected: undefined };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=shell-detection.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
34
|
"bin",
|
|
35
|
+
"scripts/postinstall.js",
|
|
35
36
|
"!dist/**/*.test.js",
|
|
36
37
|
"!dist/**/__tests__",
|
|
37
38
|
"!dist/**/*.map"
|
|
@@ -62,6 +63,8 @@
|
|
|
62
63
|
"test:watch": "vitest",
|
|
63
64
|
"test:ui": "vitest --ui",
|
|
64
65
|
"test:coverage": "vitest --coverage",
|
|
66
|
+
"test:postinstall": "node scripts/postinstall.js",
|
|
67
|
+
"postinstall": "node scripts/postinstall.js",
|
|
65
68
|
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
66
69
|
"release": "pnpm run release:ci",
|
|
67
70
|
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for auto-installing shell completions
|
|
5
|
+
*
|
|
6
|
+
* This script runs automatically after npm install unless:
|
|
7
|
+
* - CI=true environment variable is set
|
|
8
|
+
* - OPENSPEC_NO_COMPLETIONS=1 environment variable is set
|
|
9
|
+
* - dist/ directory doesn't exist (dev setup scenario)
|
|
10
|
+
*
|
|
11
|
+
* The script never fails npm install - all errors are caught and handled gracefully.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if we should skip installation
|
|
23
|
+
*/
|
|
24
|
+
function shouldSkipInstallation() {
|
|
25
|
+
// Skip in CI environments
|
|
26
|
+
if (process.env.CI === 'true' || process.env.CI === '1') {
|
|
27
|
+
return { skip: true, reason: 'CI environment detected' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Skip if user opted out
|
|
31
|
+
if (process.env.OPENSPEC_NO_COMPLETIONS === '1') {
|
|
32
|
+
return { skip: true, reason: 'OPENSPEC_NO_COMPLETIONS=1 set' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { skip: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if dist/ directory exists
|
|
40
|
+
*/
|
|
41
|
+
async function distExists() {
|
|
42
|
+
const distPath = path.join(__dirname, '..', 'dist');
|
|
43
|
+
try {
|
|
44
|
+
const stat = await fs.stat(distPath);
|
|
45
|
+
return stat.isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect the user's shell
|
|
53
|
+
*/
|
|
54
|
+
async function detectShell() {
|
|
55
|
+
try {
|
|
56
|
+
const { detectShell } = await import('../dist/utils/shell-detection.js');
|
|
57
|
+
const result = detectShell();
|
|
58
|
+
return result.shell;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Fail silently if detection module doesn't exist
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install completions for the detected shell
|
|
67
|
+
*/
|
|
68
|
+
async function installCompletions(shell) {
|
|
69
|
+
try {
|
|
70
|
+
const { CompletionFactory } = await import('../dist/core/completions/factory.js');
|
|
71
|
+
const { COMMAND_REGISTRY } = await import('../dist/core/completions/command-registry.js');
|
|
72
|
+
|
|
73
|
+
// Check if shell is supported
|
|
74
|
+
if (!CompletionFactory.isSupported(shell)) {
|
|
75
|
+
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generate completion script
|
|
80
|
+
const generator = CompletionFactory.createGenerator(shell);
|
|
81
|
+
const script = generator.generate(COMMAND_REGISTRY);
|
|
82
|
+
|
|
83
|
+
// Install completion script
|
|
84
|
+
const installer = CompletionFactory.createInstaller(shell);
|
|
85
|
+
const result = await installer.install(script);
|
|
86
|
+
|
|
87
|
+
if (result.success) {
|
|
88
|
+
// Show success message based on installation type
|
|
89
|
+
if (result.isOhMyZsh) {
|
|
90
|
+
console.log(`✓ Shell completions installed`);
|
|
91
|
+
console.log(` Restart shell: exec zsh`);
|
|
92
|
+
} else if (result.zshrcConfigured) {
|
|
93
|
+
console.log(`✓ Shell completions installed and configured`);
|
|
94
|
+
console.log(` Restart shell: exec zsh`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`✓ Shell completions installed to ~/.zsh/completions/`);
|
|
97
|
+
console.log(` Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)`);
|
|
98
|
+
console.log(` Then: exec zsh`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Installation failed, show tip for manual install
|
|
102
|
+
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Fail gracefully - show tip for manual install
|
|
106
|
+
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Main function
|
|
112
|
+
*/
|
|
113
|
+
async function main() {
|
|
114
|
+
try {
|
|
115
|
+
// Check if we should skip
|
|
116
|
+
const skipCheck = shouldSkipInstallation();
|
|
117
|
+
if (skipCheck.skip) {
|
|
118
|
+
// Silent skip - no output
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if dist/ exists (skip silently if not - expected during dev setup)
|
|
123
|
+
if (!(await distExists())) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Detect shell
|
|
128
|
+
const shell = await detectShell();
|
|
129
|
+
if (!shell) {
|
|
130
|
+
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Install completions
|
|
135
|
+
await installCompletions(shell);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Fail gracefully - never break npm install
|
|
138
|
+
// Show tip for manual install
|
|
139
|
+
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Run main and handle any unhandled errors
|
|
144
|
+
main().catch(() => {
|
|
145
|
+
// Silent failure - never break npm install
|
|
146
|
+
process.exit(0);
|
|
147
|
+
});
|