Skip to content

Commit 6194940

Browse files
Minidoracatclaude
andcommitted
✨ 同步原版 v0.17.2
新增功能: - openspec-tw config 命令(全域配置管理、XDG 支援) - Shell Completions 系統(Zsh 自動補全、Oh-my-zsh 整合) Bug 修復: - 修復 --no-interactive 在 validate 命令的問題 - 修復 pre-commit hooks 掛起問題(動態導入) - 修復 Windows 環境測試相容性問題 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e993288 commit 6194940

18 files changed

Lines changed: 2607 additions & 16 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@minidoracat/openspec-tw": minor
3+
---
4+
5+
同步原版 v0.17.2
6+
7+
**新增功能:**
8+
- `openspec-tw config` 命令 - 全域配置管理(XDG 支援)
9+
- `config path` - 顯示配置檔案位置
10+
- `config list` - 列出所有設定
11+
- `config get/set/unset` - 讀寫配置值
12+
- `config reset` - 重設為預設值
13+
- `config edit` - 用 $EDITOR 編輯
14+
- Shell Completions 系統 - Zsh 自動補全(Oh-my-zsh 整合)
15+
- `completion generate [shell]` - 生成補全腳本
16+
- `completion install [shell]` - 安裝補全
17+
- `completion uninstall [shell]` - 解除安裝
18+
19+
**Bug 修復:**
20+
- 修復 `--no-interactive` 在 validate 命令的問題
21+
- 修復 pre-commit hooks 掛起問題(動態導入 @inquirer/prompts
22+
23+
**測試修復:**
24+
- 修復 Windows 環境測試相容性問題

src/cli/index.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { createRequire } from 'module';
33
import ora from 'ora';
44
import path from 'path';
55
import { promises as fs } from 'fs';
6-
import { InitCommand } from '../core/init.js';
76
import { AI_TOOLS } from '../core/config.js';
87
import { UpdateCommand } from '../core/update.js';
98
import { ListCommand } from '../core/list.js';
@@ -13,6 +12,8 @@ import { registerSpecCommand } from '../commands/spec.js';
1312
import { ChangeCommand } from '../commands/change.js';
1413
import { ValidateCommand } from '../commands/validate.js';
1514
import { ShowCommand } from '../commands/show.js';
15+
import { CompletionCommand } from '../commands/completion.js';
16+
import { registerConfigCommand } from '../commands/config.js';
1617

1718
const program = new Command();
1819
const require = createRequire(import.meta.url);
@@ -29,7 +30,7 @@ program.option('--no-color', '停用彩色輸出');
2930
// Apply global flags before any command runs
3031
program.hook('preAction', (thisCommand) => {
3132
const opts = thisCommand.opts();
32-
if (opts.noColor) {
33+
if (opts.color === false) {
3334
process.env.NO_COLOR = '1';
3435
}
3536
});
@@ -62,6 +63,7 @@ program
6263
}
6364
}
6465

66+
const { InitCommand } = await import('../core/init.js');
6567
const initCommand = new InitCommand({
6668
tools: options?.tools,
6769
});
@@ -199,6 +201,7 @@ program
199201
});
200202

201203
registerSpecCommand(program);
204+
registerConfigCommand(program);
202205

203206
// Top-level validate command
204207
program
@@ -250,4 +253,67 @@ program
250253
}
251254
});
252255

256+
// Completion command with subcommands
257+
const completionCmd = program
258+
.command('completion')
259+
.description('管理 OpenSpec CLI 的 shell 自動補全');
260+
261+
completionCmd
262+
.command('generate [shell]')
263+
.description('產生指定 shell 的補全腳本(輸出到標準輸出)')
264+
.action(async (shell?: string) => {
265+
try {
266+
const completionCommand = new CompletionCommand();
267+
await completionCommand.generate({ shell });
268+
} catch (error) {
269+
console.log();
270+
ora().fail(`Error: ${(error as Error).message}`);
271+
process.exit(1);
272+
}
273+
});
274+
275+
completionCmd
276+
.command('install [shell]')
277+
.description('安裝指定 shell 的補全腳本')
278+
.option('--verbose', '顯示詳細安裝輸出')
279+
.action(async (shell?: string, options?: { verbose?: boolean }) => {
280+
try {
281+
const completionCommand = new CompletionCommand();
282+
await completionCommand.install({ shell, verbose: options?.verbose });
283+
} catch (error) {
284+
console.log();
285+
ora().fail(`Error: ${(error as Error).message}`);
286+
process.exit(1);
287+
}
288+
});
289+
290+
completionCmd
291+
.command('uninstall [shell]')
292+
.description('解除安裝指定 shell 的補全腳本')
293+
.option('-y, --yes', '跳過確認提示')
294+
.action(async (shell?: string, options?: { yes?: boolean }) => {
295+
try {
296+
const completionCommand = new CompletionCommand();
297+
await completionCommand.uninstall({ shell, yes: options?.yes });
298+
} catch (error) {
299+
console.log();
300+
ora().fail(`Error: ${(error as Error).message}`);
301+
process.exit(1);
302+
}
303+
});
304+
305+
// Hidden command for machine-readable completion data
306+
program
307+
.command('__complete <type>', { hidden: true })
308+
.description('輸出機器可讀格式的補全資料(內部使用)')
309+
.action(async (type: string) => {
310+
try {
311+
const completionCommand = new CompletionCommand();
312+
await completionCommand.complete({ type });
313+
} catch (error) {
314+
// Silently fail for graceful shell completion experience
315+
process.exitCode = 1;
316+
}
317+
});
318+
253319
program.parse();

src/commands/completion.ts

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import ora from 'ora';
2+
import { CompletionFactory } from '../core/completions/factory.js';
3+
import { COMMAND_REGISTRY } from '../core/completions/command-registry.js';
4+
import { detectShell, SupportedShell } from '../utils/shell-detection.js';
5+
import { CompletionProvider } from '../core/completions/completion-provider.js';
6+
import { getArchivedChangeIds } from '../utils/item-discovery.js';
7+
8+
interface GenerateOptions {
9+
shell?: string;
10+
}
11+
12+
interface InstallOptions {
13+
shell?: string;
14+
verbose?: boolean;
15+
}
16+
17+
interface UninstallOptions {
18+
shell?: string;
19+
yes?: boolean;
20+
}
21+
22+
interface CompleteOptions {
23+
type: string;
24+
}
25+
26+
/**
27+
* Command for managing shell completions for OpenSpec CLI
28+
*/
29+
export class CompletionCommand {
30+
private completionProvider: CompletionProvider;
31+
32+
constructor() {
33+
this.completionProvider = new CompletionProvider();
34+
}
35+
/**
36+
* Resolve shell parameter or exit with error
37+
*
38+
* @param shell - The shell parameter (may be undefined)
39+
* @param operationName - Name of the operation (for error messages)
40+
* @returns Resolved shell or null if should exit
41+
*/
42+
private resolveShellOrExit(shell: string | undefined, operationName: string): SupportedShell | null {
43+
const normalizedShell = this.normalizeShell(shell);
44+
45+
if (!normalizedShell) {
46+
const detectionResult = detectShell();
47+
48+
if (detectionResult.shell && CompletionFactory.isSupported(detectionResult.shell)) {
49+
return detectionResult.shell;
50+
}
51+
52+
// Shell was detected but not supported
53+
if (detectionResult.detected && !detectionResult.shell) {
54+
console.error(`錯誤:Shell「${detectionResult.detected}」尚未支援。目前支援:${CompletionFactory.getSupportedShells().join(', ')}`);
55+
process.exitCode = 1;
56+
return null;
57+
}
58+
59+
// No shell specified and cannot auto-detect
60+
console.error('錯誤:無法自動偵測 shell。請明確指定 shell。');
61+
console.error(`用法:openspec-tw completion ${operationName} [shell]`);
62+
console.error(`目前支援:${CompletionFactory.getSupportedShells().join(', ')}`);
63+
process.exitCode = 1;
64+
return null;
65+
}
66+
67+
if (!CompletionFactory.isSupported(normalizedShell)) {
68+
console.error(`錯誤:Shell「${normalizedShell}」尚未支援。目前支援:${CompletionFactory.getSupportedShells().join(', ')}`);
69+
process.exitCode = 1;
70+
return null;
71+
}
72+
73+
return normalizedShell;
74+
}
75+
76+
/**
77+
* Generate completion script and output to stdout
78+
*
79+
* @param options - Options for generation (shell type)
80+
*/
81+
async generate(options: GenerateOptions = {}): Promise<void> {
82+
const shell = this.resolveShellOrExit(options.shell, 'generate');
83+
if (!shell) return;
84+
85+
await this.generateForShell(shell);
86+
}
87+
88+
/**
89+
* Install completion script to the appropriate location
90+
*
91+
* @param options - Options for installation (shell type, verbose output)
92+
*/
93+
async install(options: InstallOptions = {}): Promise<void> {
94+
const shell = this.resolveShellOrExit(options.shell, 'install');
95+
if (!shell) return;
96+
97+
await this.installForShell(shell, options.verbose || false);
98+
}
99+
100+
/**
101+
* Uninstall completion script from the installation location
102+
*
103+
* @param options - Options for uninstallation (shell type, yes flag)
104+
*/
105+
async uninstall(options: UninstallOptions = {}): Promise<void> {
106+
const shell = this.resolveShellOrExit(options.shell, 'uninstall');
107+
if (!shell) return;
108+
109+
await this.uninstallForShell(shell, options.yes || false);
110+
}
111+
112+
/**
113+
* Generate completion script for a specific shell
114+
*/
115+
private async generateForShell(shell: SupportedShell): Promise<void> {
116+
const generator = CompletionFactory.createGenerator(shell);
117+
const script = generator.generate(COMMAND_REGISTRY);
118+
console.log(script);
119+
}
120+
121+
/**
122+
* Install completion script for a specific shell
123+
*/
124+
private async installForShell(shell: SupportedShell, verbose: boolean): Promise<void> {
125+
const generator = CompletionFactory.createGenerator(shell);
126+
const installer = CompletionFactory.createInstaller(shell);
127+
128+
const spinner = ora(`正在安裝 ${shell} 補全腳本...`).start();
129+
130+
try {
131+
// Generate the completion script
132+
const script = generator.generate(COMMAND_REGISTRY);
133+
134+
// Install it
135+
const result = await installer.install(script);
136+
137+
spinner.stop();
138+
139+
if (result.success) {
140+
console.log(`✓ ${result.message}`);
141+
142+
if (verbose && result.installedPath) {
143+
console.log(` 安裝至:${result.installedPath}`);
144+
if (result.backupPath) {
145+
console.log(` 已建立備份:${result.backupPath}`);
146+
}
147+
if (result.zshrcConfigured) {
148+
console.log(` ~/.zshrc 已自動配置`);
149+
}
150+
}
151+
152+
// Print instructions (only shown if .zshrc wasn't auto-configured)
153+
if (result.instructions && result.instructions.length > 0) {
154+
console.log('');
155+
for (const instruction of result.instructions) {
156+
console.log(instruction);
157+
}
158+
} else if (result.zshrcConfigured) {
159+
console.log('');
160+
console.log('重新啟動您的 shell 或執行:exec zsh');
161+
}
162+
} else {
163+
console.error(`✗ ${result.message}`);
164+
process.exitCode = 1;
165+
}
166+
} catch (error) {
167+
spinner.stop();
168+
console.error(`✗ 安裝補全腳本失敗:${error instanceof Error ? error.message : String(error)}`);
169+
process.exitCode = 1;
170+
}
171+
}
172+
173+
/**
174+
* Uninstall completion script for a specific shell
175+
*/
176+
private async uninstallForShell(shell: SupportedShell, skipConfirmation: boolean): Promise<void> {
177+
const installer = CompletionFactory.createInstaller(shell);
178+
179+
// Prompt for confirmation unless --yes flag is provided
180+
if (!skipConfirmation) {
181+
const { confirm } = await import('@inquirer/prompts');
182+
const confirmed = await confirm({
183+
message: '從 ~/.zshrc 移除 OpenSpec 配置?',
184+
default: false,
185+
});
186+
187+
if (!confirmed) {
188+
console.log('解除安裝已取消。');
189+
return;
190+
}
191+
}
192+
193+
const spinner = ora(`正在解除安裝 ${shell} 補全腳本...`).start();
194+
195+
try {
196+
const result = await installer.uninstall();
197+
198+
spinner.stop();
199+
200+
if (result.success) {
201+
console.log(`✓ ${result.message}`);
202+
} else {
203+
console.error(`✗ ${result.message}`);
204+
process.exitCode = 1;
205+
}
206+
} catch (error) {
207+
spinner.stop();
208+
console.error(`✗ 解除安裝補全腳本失敗:${error instanceof Error ? error.message : String(error)}`);
209+
process.exitCode = 1;
210+
}
211+
}
212+
213+
/**
214+
* Output machine-readable completion data for shell consumption
215+
* Format: tab-separated "id\tdescription" per line
216+
*
217+
* @param options - Options specifying completion type
218+
*/
219+
async complete(options: CompleteOptions): Promise<void> {
220+
const type = options.type.toLowerCase();
221+
222+
try {
223+
switch (type) {
224+
case 'changes': {
225+
const changeIds = await this.completionProvider.getChangeIds();
226+
for (const id of changeIds) {
227+
console.log(`${id}\tactive change`);
228+
}
229+
break;
230+
}
231+
case 'specs': {
232+
const specIds = await this.completionProvider.getSpecIds();
233+
for (const id of specIds) {
234+
console.log(`${id}\tspecification`);
235+
}
236+
break;
237+
}
238+
case 'archived-changes': {
239+
const archivedIds = await getArchivedChangeIds();
240+
for (const id of archivedIds) {
241+
console.log(`${id}\tarchived change`);
242+
}
243+
break;
244+
}
245+
default:
246+
// Invalid type - silently exit with no output for graceful shell completion failure
247+
process.exitCode = 1;
248+
break;
249+
}
250+
} catch {
251+
// Silently fail for graceful shell completion experience
252+
process.exitCode = 1;
253+
}
254+
}
255+
256+
/**
257+
* Normalize shell parameter to lowercase
258+
*/
259+
private normalizeShell(shell?: string): string | undefined {
260+
return shell?.toLowerCase();
261+
}
262+
}

0 commit comments

Comments
 (0)