skill-any-code 1.0.0 → 1.0.1

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.
Files changed (68) hide show
  1. package/dist/adapters/command.schemas.js +18 -0
  2. package/dist/application/analysis.app.service.js +264 -0
  3. package/dist/application/bootstrap.js +21 -0
  4. package/dist/application/services/llm.analysis.service.js +170 -0
  5. package/dist/common/config.js +213 -0
  6. package/dist/common/constants.js +11 -0
  7. package/dist/common/errors.js +37 -0
  8. package/dist/common/logger.js +77 -0
  9. package/dist/common/types.js +2 -0
  10. package/dist/common/ui.js +201 -0
  11. package/dist/common/utils.js +117 -0
  12. package/dist/domain/index.js +17 -0
  13. package/dist/domain/interfaces.js +2 -0
  14. package/dist/domain/services/analysis.service.js +696 -0
  15. package/dist/domain/services/incremental.service.js +81 -0
  16. package/dist/infrastructure/blacklist.service.js +71 -0
  17. package/dist/infrastructure/cache/file.hash.cache.js +140 -0
  18. package/dist/infrastructure/git/git.service.js +159 -0
  19. package/dist/infrastructure/git.service.js +157 -0
  20. package/dist/infrastructure/index.service.js +108 -0
  21. package/dist/infrastructure/llm/llm.usage.tracker.js +58 -0
  22. package/dist/infrastructure/llm/openai.client.js +141 -0
  23. package/{src/infrastructure/llm/prompt.template.ts → dist/infrastructure/llm/prompt.template.js} +31 -36
  24. package/dist/infrastructure/llm.service.js +61 -0
  25. package/dist/infrastructure/skill/skill.generator.js +83 -0
  26. package/{src/infrastructure/skill/templates/resolve.script.ts → dist/infrastructure/skill/templates/resolve.script.js} +18 -15
  27. package/dist/infrastructure/skill/templates/skill.md.template.js +47 -0
  28. package/dist/infrastructure/splitter/code.splitter.js +137 -0
  29. package/dist/infrastructure/storage.service.js +409 -0
  30. package/dist/infrastructure/worker-pool/parse.worker.impl.js +137 -0
  31. package/dist/infrastructure/worker-pool/parse.worker.js +43 -0
  32. package/dist/infrastructure/worker-pool/worker-pool.service.js +171 -0
  33. package/package.json +5 -1
  34. package/jest.config.js +0 -27
  35. package/src/adapters/command.schemas.ts +0 -21
  36. package/src/application/analysis.app.service.ts +0 -272
  37. package/src/application/bootstrap.ts +0 -35
  38. package/src/application/services/llm.analysis.service.ts +0 -237
  39. package/src/cli.ts +0 -297
  40. package/src/common/config.ts +0 -209
  41. package/src/common/constants.ts +0 -8
  42. package/src/common/errors.ts +0 -34
  43. package/src/common/logger.ts +0 -82
  44. package/src/common/types.ts +0 -385
  45. package/src/common/ui.ts +0 -228
  46. package/src/common/utils.ts +0 -81
  47. package/src/domain/index.ts +0 -1
  48. package/src/domain/interfaces.ts +0 -188
  49. package/src/domain/services/analysis.service.ts +0 -735
  50. package/src/domain/services/incremental.service.ts +0 -50
  51. package/src/index.ts +0 -6
  52. package/src/infrastructure/blacklist.service.ts +0 -37
  53. package/src/infrastructure/cache/file.hash.cache.ts +0 -119
  54. package/src/infrastructure/git/git.service.ts +0 -120
  55. package/src/infrastructure/git.service.ts +0 -121
  56. package/src/infrastructure/index.service.ts +0 -94
  57. package/src/infrastructure/llm/llm.usage.tracker.ts +0 -65
  58. package/src/infrastructure/llm/openai.client.ts +0 -162
  59. package/src/infrastructure/llm.service.ts +0 -70
  60. package/src/infrastructure/skill/skill.generator.ts +0 -53
  61. package/src/infrastructure/skill/templates/skill.md.template.ts +0 -45
  62. package/src/infrastructure/splitter/code.splitter.ts +0 -176
  63. package/src/infrastructure/storage.service.ts +0 -413
  64. package/src/infrastructure/worker-pool/parse.worker.impl.ts +0 -135
  65. package/src/infrastructure/worker-pool/parse.worker.ts +0 -9
  66. package/src/infrastructure/worker-pool/worker-pool.service.ts +0 -173
  67. package/tsconfig.json +0 -24
  68. package/tsconfig.test.json +0 -5
@@ -1,237 +0,0 @@
1
- import type { ILLMClient, IFileSplitter, IAnalysisCache } from '../../domain/interfaces';
2
- import type { FileAnalysis, LLMConfig, FileChunkAnalysis } from '../../common/types';
3
- import {
4
- FILE_STRUCTURE_PROMPT,
5
- FILE_DESCRIPTION_PROMPT,
6
- FILE_SUMMARY_PROMPT,
7
- PARSE_RETRY_HINT,
8
- CHUNK_ANALYSIS_PROMPT,
9
- DIRECTORY_DESCRIPTION_PROMPT,
10
- DIRECTORY_SUMMARY_PROMPT
11
- } from '../../infrastructure/llm/prompt.template';
12
- import Mustache from 'mustache';
13
- import { AppError, ErrorCode } from '../../common/errors';
14
- import { FileHashCache } from '../../infrastructure/cache/file.hash.cache';
15
- import path from 'path';
16
-
17
- /** 从 LLM 返回中解析单字段:支持 {"key": "value"} 或纯字符串 */
18
- function parseSingleField(content: string, field: 'description' | 'summary'): string {
19
- const trimmed = content.trim();
20
- try {
21
- const o = JSON.parse(trimmed);
22
- if (o && typeof o[field] === 'string') return o[field];
23
- } catch {
24
- // 非 JSON 则整体视为该字段内容
25
- }
26
- return trimmed || '';
27
- }
28
-
29
- export class LLMAnalysisService {
30
- private llmClient: ILLMClient;
31
- private fileSplitter: IFileSplitter;
32
- private cache: IAnalysisCache;
33
- private config: LLMConfig;
34
-
35
- constructor(
36
- llmClient: ILLMClient,
37
- fileSplitter: IFileSplitter,
38
- cache: IAnalysisCache,
39
- config: LLMConfig
40
- ) {
41
- this.llmClient = llmClient;
42
- this.fileSplitter = fileSplitter;
43
- this.cache = cache;
44
- this.config = config;
45
- }
46
-
47
- async analyzeFile(filePath: string, fileContent: string, fileHash: string): Promise<FileAnalysis> {
48
- // 先查缓存
49
- if (this.config.cache_enabled) {
50
- const cachedResult = await this.cache.get(fileHash);
51
- if (cachedResult) {
52
- cachedResult.path = filePath;
53
- cachedResult.lastAnalyzedAt = new Date().toISOString();
54
- return cachedResult;
55
- }
56
- }
57
-
58
- let result: FileAnalysis;
59
-
60
- // 检查文件大小是否超过上下文窗口
61
- if (fileContent.length > this.config.context_window_size * 0.8) {
62
- // 大文件分片解析
63
- result = await this.analyzeLargeFile(filePath, fileContent);
64
- } else {
65
- // 小文件直接解析
66
- result = await this.analyzeSmallFile(filePath, fileContent);
67
- }
68
-
69
- // 保存缓存
70
- if (this.config.cache_enabled) {
71
- await this.cache.set(fileHash, result);
72
- }
73
-
74
- result.path = filePath;
75
- result.lastAnalyzedAt = new Date().toISOString();
76
- return result;
77
- }
78
-
79
- /** 三步协议(需求 10.5.3 / 10.9.1):结构 → 功能描述 → 概述,程序组装为完整 FileAnalysis;某次解析失败仅重试当次(10.9.2)。 */
80
- private async analyzeSmallFile(filePath: string, fileContent: string): Promise<FileAnalysis> {
81
- const opts = { temperature: 0.1 };
82
-
83
- // 第一步:仅提取结构(classes / globalVariables / globalFunctions),不包含基础信息
84
- const structure = await this.callWithParseRetry(
85
- Mustache.render(FILE_STRUCTURE_PROMPT, { filePath, fileContent }),
86
- opts,
87
- (content) => {
88
- const o = JSON.parse(content);
89
- return {
90
- classes: Array.isArray(o.classes) ? o.classes : [],
91
- functions: Array.isArray(o.functions) ? o.functions : []
92
- };
93
- }
94
- );
95
-
96
- const structureJson = JSON.stringify(structure, null, 2);
97
-
98
- // 第二步:仅生成功能描述
99
- const description = await this.callWithParseRetry(
100
- Mustache.render(FILE_DESCRIPTION_PROMPT, { structureJson }),
101
- opts,
102
- (content) => parseSingleField(content, 'description')
103
- );
104
-
105
- // 第三步:仅生成概述
106
- const summary = await this.callWithParseRetry(
107
- Mustache.render(FILE_SUMMARY_PROMPT, { structureJson, description }),
108
- opts,
109
- (content) => parseSingleField(content, 'summary')
110
- );
111
-
112
- // 基础信息由程序侧负责(设计文档 13.2.2)
113
- const name = path.basename(filePath);
114
- const language = this.detectLanguage(filePath);
115
- const linesOfCode = fileContent.split(/\r?\n/).length;
116
-
117
- return {
118
- type: 'file',
119
- path: filePath,
120
- name,
121
- language,
122
- linesOfCode,
123
- dependencies: [],
124
- description,
125
- summary,
126
- classes: structure.classes,
127
- functions: structure.functions,
128
- lastAnalyzedAt: new Date().toISOString(),
129
- commitHash: ''
130
- };
131
- }
132
-
133
- private detectLanguage(filePath: string): string {
134
- const ext = path.extname(filePath).toLowerCase();
135
- switch (ext) {
136
- case '.ts':
137
- case '.tsx':
138
- return 'TypeScript';
139
- case '.js':
140
- case '.jsx':
141
- return 'JavaScript';
142
- case '.py':
143
- return 'Python';
144
- case '.java':
145
- return 'Java';
146
- case '.go':
147
- return 'Go';
148
- case '.cs':
149
- return 'C#';
150
- default:
151
- return '';
152
- }
153
- }
154
-
155
- /** 单次调用:解析失败则仅重试该次一次,不重做已成功步骤(需求 10.9.2)。 */
156
- private async callWithParseRetry<T>(
157
- prompt: string,
158
- options: { temperature?: number },
159
- parseFn: (content: string) => T
160
- ): Promise<T> {
161
- let lastError: unknown;
162
- for (let attempt = 0; attempt < 2; attempt++) {
163
- try {
164
- const response = await this.llmClient.call(
165
- attempt === 1 ? prompt + PARSE_RETRY_HINT : prompt,
166
- { ...options, retries: 0 }
167
- );
168
- return parseFn(response.content);
169
- } catch (e) {
170
- lastError = e;
171
- }
172
- }
173
- throw new AppError(
174
- ErrorCode.LLM_RESPONSE_PARSE_FAILED,
175
- `Failed to parse LLM response after retry: ${(lastError as Error)?.message}`,
176
- lastError
177
- );
178
- }
179
-
180
- private async analyzeLargeFile(filePath: string, fileContent: string): Promise<FileAnalysis> {
181
- // 分片
182
- const chunks = await this.fileSplitter.split(fileContent, this.config.context_window_size * 0.7);
183
-
184
- // 并行解析所有分片(仅提取结构)
185
- const chunkAnalysisPromises = chunks.map(async (chunk) => {
186
- const prompt = Mustache.render(CHUNK_ANALYSIS_PROMPT, {
187
- filePath,
188
- chunkId: chunk.id,
189
- chunkContent: chunk.content,
190
- context: chunk.context || ''
191
- });
192
-
193
- const response = await this.llmClient.call(prompt, { temperature: 0.1 });
194
-
195
- const parsed = JSON.parse(response.content);
196
- return {
197
- chunkId: chunk.id,
198
- classes: Array.isArray(parsed.classes) ? parsed.classes : [],
199
- functions: Array.isArray(parsed.functions) ? parsed.functions : []
200
- } as FileChunkAnalysis;
201
- });
202
-
203
- const chunkResults = await Promise.all(chunkAnalysisPromises);
204
-
205
- // 合并分片结果
206
- return this.fileSplitter.merge(chunkResults, filePath);
207
- }
208
-
209
- /**
210
- * 目录两步协议(需求 10.6.3 / 10.9.3):基于子项精简信息先生成 description,再生成 summary。
211
- */
212
- async analyzeDirectory(
213
- childrenDirs: Array<{ name: string; summary: string; description: string }>,
214
- childrenFiles: Array<{ name: string; summary: string; description: string }>
215
- ): Promise<{ description: string; summary: string }> {
216
- const opts = { temperature: 0.1 };
217
- const payload = { childrenDirs, childrenFiles };
218
- const childrenJson = JSON.stringify(payload, null, 2);
219
-
220
- // 第一步:功能描述
221
- const description = await this.callWithParseRetry(
222
- Mustache.render(DIRECTORY_DESCRIPTION_PROMPT, { childrenJson }),
223
- opts,
224
- (content) => parseSingleField(content, 'description')
225
- );
226
-
227
- // 第二步:概述
228
- const summary = await this.callWithParseRetry(
229
- Mustache.render(DIRECTORY_SUMMARY_PROMPT, { description, childrenJson }),
230
- opts,
231
- (content) => parseSingleField(content, 'summary')
232
- );
233
-
234
- return { description, summary };
235
- }
236
-
237
- }
package/src/cli.ts DELETED
@@ -1,297 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import pc from 'picocolors';
4
- import fs from 'fs-extra';
5
- import path from 'path';
6
- import { version } from '../package.json';
7
- import { AnalysisAppService } from './application/analysis.app.service';
8
- import { configManager } from './common/config';
9
- import { logger } from './common/logger';
10
- import { cliRenderer } from './common/ui';
11
- import type { TokenUsageStats } from './common/types';
12
- import { AppError, ErrorCode } from './common/errors';
13
- import { LocalStorageService } from './infrastructure/storage.service';
14
- import { GitService } from './infrastructure/git.service';
15
- import { generateProjectSlug } from './common/utils';
16
- import { DEFAULT_CONCURRENCY } from './common/constants';
17
-
18
- const program = new Command();
19
-
20
- // 主命令配置
21
- program
22
- .name('skill-any-code')
23
- .alias('sac')
24
- .description('Skill Any Code: a CLI for large codebase understanding and analysis')
25
- .version(version, '-v, --version', 'Show version number')
26
- .helpOption('-h, --help', 'Show help information')
27
- .option('--log-level <level>', 'Log level: debug/info/warn/error')
28
- .option('--path <path>', 'Project root path to analyze', process.cwd())
29
- .option('-m, --mode <mode>', 'Analysis mode: full/incremental/auto', 'auto')
30
- .option('-d, --depth <number>', 'Max directory depth (-1 = unlimited)', '-1')
31
- .option('-C, --concurrency <number>', 'Max concurrent workers (default: CPU*2, capped by analyze.max_concurrency)')
32
- .option('--output-dir <path>', 'Custom output directory for results')
33
- .option('--skills-providers <list>', 'Comma-separated AI tool providers (opencode/cursor/claude/codex)')
34
- .option('--no-skills', 'Skip skill generation')
35
- // LLM相关参数
36
- .option('--llm-base-url <url>', 'LLM API base URL')
37
- .option('--llm-api-key <key>', 'LLM API key')
38
- .option('--llm-model <model>', 'LLM model name')
39
- .option('--llm-temperature <number>', 'LLM temperature (0-2)', parseFloat)
40
- .option('--llm-max-tokens <number>', 'LLM max output tokens', parseInt)
41
- .option('--llm-timeout <ms>', 'LLM request timeout (ms)', parseInt)
42
- .option('--llm-max-retries <number>', 'LLM max retries', parseInt)
43
- .option('--llm-retry-delay <ms>', 'LLM retry delay (ms)', parseInt)
44
- .option('--llm-context-window-size <number>', 'LLM context window size', parseInt)
45
- .option('--no-llm-cache', 'Disable LLM result cache')
46
- .option('--llm-cache-dir <path>', 'LLM cache directory')
47
- .option('--clear-cache', 'Clear existing LLM cache before analyzing');
48
-
49
- // init 子命令:显式初始化配置文件(V2.5)
50
- program
51
- .command('init')
52
- .description('Initialize or reset the config file')
53
- .action(async () => {
54
- try {
55
- const resolvedPath = '~/.config/skill-any-code/config.yaml';
56
- const fsPath = resolvedPath.replace('~', process.env.HOME || process.env.USERPROFILE || '');
57
- const exists = await fs.pathExists(fsPath);
58
-
59
- await configManager.init();
60
- logger.success(exists ? `Config file reset: ${fsPath}` : `Config file created: ${fsPath}`);
61
- process.exit(0);
62
- } catch (error) {
63
- logger.error('Failed to initialize config', error as Error);
64
- process.exit(1);
65
- }
66
- });
67
-
68
- // 默认解析(不带子命令时直接执行)
69
- program.action(async () => {
70
- const os = require('os');
71
- try {
72
- // 加载配置(V2.5:配置未初始化时直接失败,提示先执行 init)
73
- let config;
74
- try {
75
- config = await configManager.load();
76
- } catch (e: any) {
77
- if (e instanceof AppError && e.code === ErrorCode.CONFIG_NOT_INITIALIZED) {
78
- process.stderr.write(
79
- `Config is not initialized. Run "skill-any-code init" to create: ~/.config/skill-any-code/config.yaml\n`,
80
- );
81
- process.exit(1);
82
- return;
83
- }
84
- throw e;
85
- }
86
- const cliLogLevel = program.opts().logLevel as any;
87
- if (cliLogLevel) {
88
- logger.setLevel(cliLogLevel);
89
- }
90
-
91
- // 合并LLM命令行参数
92
- const options = program.opts();
93
- if (options.llmBaseUrl) config.llm.base_url = options.llmBaseUrl;
94
- if (options.llmApiKey) config.llm.api_key = options.llmApiKey;
95
- if (options.llmModel) config.llm.model = options.llmModel;
96
- if (options.llmTemperature !== undefined) config.llm.temperature = options.llmTemperature;
97
- if (options.llmMaxTokens !== undefined) config.llm.max_tokens = options.llmMaxTokens;
98
- if (options.llmTimeout !== undefined) config.llm.timeout = options.llmTimeout;
99
- if (options.llmMaxRetries !== undefined) config.llm.max_retries = options.llmMaxRetries;
100
- if (options.llmRetryDelay !== undefined) config.llm.retry_delay = options.llmRetryDelay;
101
- if (options.llmContextWindowSize !== undefined) config.llm.context_window_size = options.llmContextWindowSize;
102
- if (options.llmCache !== undefined) config.llm.cache_enabled = options.llmCache;
103
- if (options.llmCacheDir) config.llm.cache_dir = options.llmCacheDir;
104
-
105
- // 处理清空缓存选项
106
- if (options.clearCache) {
107
- const { FileHashCache } = await import('./infrastructure/cache/file.hash.cache');
108
- const homeDir = os.homedir();
109
- const cache = new FileHashCache({
110
- cacheDir: config.llm.cache_dir.replace(/^~(?=\/|\\|$)/, homeDir),
111
- maxSizeMb: config.llm.cache_max_size_mb,
112
- });
113
- await cache.clear();
114
- logger.info('LLM cache cleared');
115
- }
116
-
117
- // 默认并发:CPU*2,但不超过配置中的 analyze.max_concurrency
118
- const cpuBasedConcurrency = DEFAULT_CONCURRENCY;
119
- const configuredMax = Number(config.analyze.max_concurrency ?? DEFAULT_CONCURRENCY);
120
- const defaultConcurrency =
121
- configuredMax > 0 ? Math.min(cpuBasedConcurrency, configuredMax) : cpuBasedConcurrency;
122
-
123
- const analysisParams = {
124
- path: options.path,
125
- mode: options.mode as any,
126
- depth: Number(options.depth),
127
- concurrency: options.concurrency !== undefined ? Number(options.concurrency) : defaultConcurrency,
128
- outputDir: options.outputDir || config.global.output_dir,
129
- llmConfig: config.llm,
130
- skillsProviders: options.skillsProviders
131
- ? options.skillsProviders.split(',').map((s: string) => s.trim().toLowerCase())
132
- : undefined,
133
- noSkills: options.skills === false,
134
- };
135
-
136
- // V2.5:解析前执行 LLM 连接可用性校验,失败则立即退出(需求文档 13.4.2 / 测试文档 ST-LLM-CONNECT-001)
137
- const { OpenAIClient } = await import('./infrastructure/llm/openai.client');
138
- const llmClient = new OpenAIClient(config.llm);
139
- logger.info(
140
- `LLM client initialized. Testing connectivity and config (url=${config.llm.base_url}, model=${config.llm.model})`,
141
- );
142
- try {
143
- await llmClient.testConnection(config.llm);
144
- } catch (e: any) {
145
- const detail = e?.message || String(e);
146
- process.stderr.write(`LLM connectivity/config validation failed: ${detail}\n`);
147
- process.exit(1);
148
- }
149
-
150
- const analysisService = new AnalysisAppService();
151
-
152
- // 在 analyze 生命周期内,将所有 logger 输出通过 CLI 渲染器固定到进度块下方,
153
- // 避免产生额外的进度/对象/Tokens 区域块。
154
- logger.setSink((line) => {
155
- cliRenderer.logBelow(line);
156
- });
157
-
158
- const paramsWithProgress = {
159
- ...analysisParams,
160
- onTotalKnown: (total: number) => {
161
- cliRenderer.setTotal(total);
162
- },
163
- onProgress: (done: number, total: number, current?: { path: string }) => {
164
- cliRenderer.updateProgress(done, total, current?.path, analysisParams.concurrency);
165
- },
166
- onTokenUsageSnapshot: (stats: TokenUsageStats) => {
167
- cliRenderer.updateTokens(stats);
168
- },
169
- onScanProgress: (scannedFiles: number) => {
170
- cliRenderer.updateScanProgress(scannedFiles);
171
- },
172
- };
173
-
174
- let result;
175
- // 执行解析(V2.4+:不再在 CLI 中做交互式错误处理,所有 LLM 错误由应用层统一抛出)
176
- result = await analysisService.runAnalysis(paramsWithProgress);
177
-
178
- if (result.success) {
179
- const files = result.data?.analyzedFilesCount || 0;
180
- const dirs = (result as any).data?.analyzedDirsCount || 0;
181
- const objects = files + dirs;
182
- logger.success(
183
- `Analysis completed. Processed ${objects} object(s)`,
184
- );
185
- const summaryPath = result.data?.summaryPath || '';
186
- const summaryLabel = summaryPath ? `Entry file: ${path.basename(summaryPath)}` : 'Entry file: index.md';
187
- logger.success(`Project analysis result. ${summaryLabel}`);
188
- const usage = result.data?.tokenUsage;
189
- if (usage) {
190
- logger.info(
191
- `LLM calls: ${usage.totalCalls}, prompt tokens: ${usage.totalPromptTokens}, ` +
192
- `completion tokens: ${usage.totalCompletionTokens}, total tokens: ${usage.totalTokens}`
193
- );
194
- }
195
- } else {
196
- logger.error(`Analysis failed: ${result.message}`);
197
- if (result.errors && result.errors.length > 0) {
198
- result.errors.forEach(err => logger.error(`- ${err.path}: ${err.message}`));
199
- }
200
- process.exit(1);
201
- }
202
- } catch (error) {
203
- const err = error as any;
204
- // V2.5:LLM 连接/配置校验失败时统一输出明确前缀,满足 ST-LLM-CONNECT-001/002/003
205
- if (err && err.code && (
206
- err.code === ErrorCode.LLM_INVALID_CONFIG ||
207
- err.code === ErrorCode.LLM_CALL_FAILED ||
208
- err.code === ErrorCode.LLM_TIMEOUT
209
- )) {
210
- const detail = err.message || '';
211
- process.stderr.write(`LLM connectivity/config validation failed: ${detail}\n`);
212
- } else if (err instanceof AppError) {
213
- logger.error(`Execution failed: ${err.message}`, err);
214
- } else {
215
- logger.error('Execution failed', err as Error);
216
- }
217
- process.exit(1);
218
- }
219
- });
220
-
221
- // resolve 子命令(V2.6:根据相对路径推导分析结果 Markdown 路径,不依赖索引)
222
- program
223
- .command('resolve')
224
- .description('Resolve the analysis result Markdown path for a file/directory')
225
- .argument('<relative-path>', 'Relative path of the file/directory (from project root)')
226
- .option('-p, --project <path>', 'Project root path', process.cwd())
227
- .action(async (relativePath: string, options: { project?: string }) => {
228
- try {
229
- logger.setLevel(program.opts().logLevel as any);
230
- const projectRoot = options.project || process.cwd();
231
-
232
- const DEFAULT_OUTPUT_DIR = '.skill-any-code-result'
233
-
234
- const normalizeRel = (input: string): { rel: string; rawHadTrailingSlash: boolean } => {
235
- const raw = (input || '').trim()
236
- const rawPosix = raw.replace(/\\/g, '/')
237
- const rawHadTrailingSlash = rawPosix.endsWith('/') && rawPosix.length > 1
238
- let rel = rawPosix
239
- while (rel.startsWith('./')) rel = rel.slice(2)
240
- if (rel.endsWith('/') && rel.length > 1) rel = rel.slice(0, -1)
241
- if (rel === '') rel = '.'
242
- return { rel, rawHadTrailingSlash }
243
- }
244
-
245
- const { rel, rawHadTrailingSlash } = normalizeRel(relativePath)
246
- const targetAbs = path.resolve(projectRoot, rel)
247
- if (!(await fs.pathExists(targetAbs))) {
248
- process.stdout.write('N/A\n')
249
- process.exit(0)
250
- return
251
- }
252
-
253
- const stat = await fs.stat(targetAbs)
254
- const isDir = stat.isDirectory() || rawHadTrailingSlash || rel === '.'
255
-
256
- let mdRel: string
257
- if (isDir) {
258
- mdRel =
259
- rel === '.'
260
- ? path.posix.join(DEFAULT_OUTPUT_DIR, 'index.md')
261
- : path.posix.join(DEFAULT_OUTPUT_DIR, rel, 'index.md')
262
- } else {
263
- const parsed = path.posix.parse(rel)
264
- const dirPart = parsed.dir
265
- const name =
266
- parsed.name === 'index' && parsed.ext
267
- ? `index${parsed.ext}.md`
268
- : `${parsed.name}.md`
269
- mdRel = dirPart ? path.posix.join(DEFAULT_OUTPUT_DIR, dirPart, name) : path.posix.join(DEFAULT_OUTPUT_DIR, name)
270
- }
271
-
272
- const mdAbs = path.resolve(projectRoot, mdRel)
273
- if (await fs.pathExists(mdAbs)) {
274
- process.stdout.write(mdRel + '\n')
275
- } else {
276
- process.stdout.write('N/A\n')
277
- }
278
- process.exit(0);
279
- } catch (error: unknown) {
280
- const msg = error instanceof Error ? error.message : String(error);
281
- process.stderr.write(`Resolve failed: ${msg}\n`);
282
- process.exit(1);
283
- }
284
- });
285
-
286
- // 全局错误处理
287
- program.showSuggestionAfterError();
288
- program.configureHelp({
289
- sortSubcommands: true,
290
- sortOptions: true,
291
- });
292
-
293
- // 解析命令行参数
294
- program.parseAsync(process.argv).catch((error) => {
295
- console.error(pc.red(`\nExecution failed: ${error.message}`));
296
- process.exit(1);
297
- });