Skip to content

Commit d6f5566

Browse files
committed
feat(init): 优化初始化流程,集成 OpenSpec 并自动下载技能
- 移除本地 contract 模板目录及相关构建逻辑 - 自动检测并安装 OpenSpec 依赖 - 静默执行 openspec init 避免重复提示 - 为支持的 IDE 下载 tml-docs-spec-generate 技能 - 自动创建标准化的 docs 目录结构 - 更新工具适配器接口以支持初始化后操作
1 parent 3f2b6e2 commit d6f5566

17 files changed

Lines changed: 255 additions & 30 deletions

File tree

TMLSPEC-cli/package-lock.json

Lines changed: 22 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

TMLSPEC-cli/package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@
88
},
99
"files": [
1010
"bin",
11-
"contract",
1211
"dist",
1312
"src/core/commands",
1413
"README.md"
1514
],
1615
"scripts": {
17-
"prebuild": "node ./scripts/sync-contract.mjs",
1816
"build": "tsc -p tsconfig.json",
1917
"dev": "tsx src/cli/index.ts",
20-
"prepack": "node ./scripts/sync-contract.mjs",
2118
"start": "node ./bin/tml-spec.js"
2219
},
2320
"keywords": [
@@ -35,9 +32,11 @@
3532
},
3633
"dependencies": {
3734
"@inquirer/prompts": "^7.8.4",
38-
"commander": "^13.1.0"
35+
"commander": "^13.1.0",
36+
"degit": "^2.8.4"
3937
},
4038
"devDependencies": {
39+
"@types/degit": "^2.8.6",
4140
"@types/node": "^24.0.0",
4241
"tsx": "^4.20.3",
4342
"typescript": "^5.9.2"

TMLSPEC-cli/src/core/adapters/trae.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import path from 'node:path';
12
import { buildProjectMarkdownCommand, buildRequirementMarkdownCommand } from '../command-files.js';
23
import { buildNamespacedCommandPath, COMMAND_NAMESPACE } from '../command-paths.js';
34
import type { ToolAdapter } from '../types.js';
5+
import { downloadSkill } from '../../utils/fs.js';
46

57
export const traeAdapter: ToolAdapter = {
68
tool: {
@@ -20,5 +22,15 @@ export const traeAdapter: ToolAdapter = {
2022
content: buildRequirementMarkdownCommand()
2123
}
2224
];
25+
},
26+
async postInit(projectRoot: string) {
27+
const destPath = path.join(projectRoot, '.trae/skills/tml-docs-spec-generate');
28+
console.log('Downloading tml-docs-spec-generate skill for Trae...');
29+
try {
30+
await downloadSkill('Time-Machine-Lab/TML-Skills/skills/tml-docs-spec-generate', destPath);
31+
console.log('Skill downloaded successfully to .trae/skills/tml-docs-spec-generate');
32+
} catch (error) {
33+
console.error('Failed to download skill for Trae:', error);
34+
}
2335
}
2436
};

TMLSPEC-cli/src/core/init.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { checkbox, confirm, input } from '@inquirer/prompts';
44
import { getAdapter } from './adapters/index.js';
55
import { TOOL_OPTIONS } from './catalog.js';
66
import type { InitAnswers, ToolId } from './types.js';
7-
import { copyDirectory, writeTextFile } from '../utils/fs.js';
7+
import { scaffoldDirectories, writeTextFile } from '../utils/fs.js';
8+
import { checkCommandExists, runCommand } from '../utils/exec.js';
89

910
const currentDir = path.dirname(fileURLToPath(import.meta.url));
1011
const packageRoot = path.resolve(currentDir, '../..');
11-
const bundledContractDirectory = path.join(packageRoot, 'contract');
1212

1313
interface InitOverrides {
1414
projectRoot?: string;
@@ -78,6 +78,19 @@ async function collectAnswers(overrides: InitOverrides): Promise<InitAnswers> {
7878

7979
export async function runInit(overrides: InitOverrides = {}): Promise<void> {
8080
const answers = await collectAnswers(overrides);
81+
82+
console.log('检查 OpenSpec 依赖...');
83+
const hasOpenSpec = await checkCommandExists('openspec');
84+
if (!hasOpenSpec) {
85+
console.log('未检测到 OpenSpec,正在全局安装 @fission-ai/openspec@latest...');
86+
try {
87+
await runCommand('npm install -g @fission-ai/openspec@latest');
88+
console.log('OpenSpec 安装完成。');
89+
} catch (error) {
90+
console.error('OpenSpec 安装失败,请稍后手动安装。');
91+
}
92+
}
93+
8194
let writtenFiles = 0;
8295
let skippedFiles = 0;
8396

@@ -94,23 +107,37 @@ export async function runInit(overrides: InitOverrides = {}): Promise<void> {
94107
skippedFiles += 1;
95108
}
96109
}
110+
111+
if (adapter.postInit) {
112+
await adapter.postInit(answers.projectRoot);
113+
}
114+
}
115+
116+
try {
117+
console.log('执行 OpenSpec 初始化...');
118+
await runCommand(`openspec init --tools ${answers.tools.join(',')} "${answers.projectRoot}" --force`);
119+
console.log('OpenSpec 初始化完成。');
120+
} catch (error) {
121+
console.error('OpenSpec 初始化时发生错误。');
97122
}
98123

99-
const contractTargetPath = path.join(answers.projectRoot, 'contract');
100-
const contractCopied = await copyDirectory(bundledContractDirectory, contractTargetPath, answers.force);
124+
await scaffoldDirectories(answers.projectRoot, [
125+
'docs/api',
126+
'docs/sql',
127+
'docs/project',
128+
'docs/project/domain'
129+
]);
101130

102131
const relativeRoot = path.relative(process.cwd(), answers.projectRoot) || '.';
103132
console.log('TML Spec 命令已初始化。');
104133
console.log(`项目根目录: ${relativeRoot}`);
105134
console.log(`已选择工具: ${answers.tools.join(', ')}`);
106135
console.log(`写入文件数: ${writtenFiles}`);
107136
console.log(`跳过文件数: ${skippedFiles}`);
108-
console.log(`contract 模板目录: ${contractCopied ? '已写入项目根目录' : '已存在,未覆盖'}`);
109137
console.log('下一步:');
110138
console.log('1. 在你的 IDE 中打开生成的命令或 prompt 文件。');
111-
console.log('2. 在项目根目录下查看 contract 模板目录,并按需补充团队规范。');
112-
console.log('3. 使用 tml-spec 命名空间下的 project 命令处理项目级文档工作。');
113-
console.log('4. 使用 tml-spec 命名空间下的 requirement 命令处理需求级工作,并路由到 openspec。');
139+
console.log('2. 使用 tml-spec 命名空间下的 project 命令处理项目级文档工作。');
140+
console.log('3. 使用 tml-spec 命名空间下的 requirement 命令处理需求级工作,并路由到 openspec。');
114141
}
115142

116143
export function parseInitOverrides(options: {

TMLSPEC-cli/src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export interface GeneratedCommandFile {
2121
export interface ToolAdapter {
2222
tool: ToolOption;
2323
generateFiles(): GeneratedCommandFile[];
24+
postInit?(projectRoot: string): Promise<void>;
2425
}

TMLSPEC-cli/src/utils/exec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { exec } from 'node:child_process';
2+
import { promisify } from 'node:util';
3+
4+
const execAsync = promisify(exec);
5+
6+
export async function checkCommandExists(command: string): Promise<boolean> {
7+
try {
8+
await execAsync(`${command} --version`);
9+
return true;
10+
} catch {
11+
return false;
12+
}
13+
}
14+
15+
export async function runCommand(command: string, options = {}): Promise<void> {
16+
const { stdout, stderr } = await execAsync(command, options);
17+
if (stdout) console.log(stdout.trim());
18+
if (stderr) console.error(stderr.trim());
19+
}

TMLSPEC-cli/src/utils/fs.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { access, cp, mkdir, rm, writeFile } from 'node:fs/promises';
22
import path from 'node:path';
3+
import degit from 'degit';
34

45
export async function ensureDirectory(dirPath: string): Promise<void> {
56
await mkdir(dirPath, { recursive: true });
@@ -24,20 +25,22 @@ export async function writeTextFile(filePath: string, content: string, overwrite
2425
return true;
2526
}
2627

27-
export async function copyDirectory(sourcePath: string, targetPath: string, overwrite = true): Promise<boolean> {
28-
if (!await pathExists(sourcePath)) {
29-
throw new Error(`Source directory does not exist: ${sourcePath}`);
30-
}
31-
32-
if (!overwrite && await pathExists(targetPath)) {
33-
return false;
34-
}
35-
36-
if (overwrite && await pathExists(targetPath)) {
37-
await rm(targetPath, { recursive: true, force: true });
28+
export async function scaffoldDirectories(projectRoot: string, directories: string[]): Promise<void> {
29+
for (const dir of directories) {
30+
const fullPath = path.join(projectRoot, dir);
31+
await ensureDirectory(fullPath);
32+
const gitkeepPath = path.join(fullPath, '.gitkeep');
33+
if (!await pathExists(gitkeepPath)) {
34+
await writeFile(gitkeepPath, '', 'utf8');
35+
}
3836
}
37+
}
3938

40-
await ensureDirectory(path.dirname(targetPath));
41-
await cp(sourcePath, targetPath, { recursive: true, force: overwrite });
42-
return true;
39+
export async function downloadSkill(repositoryPath: string, destPath: string): Promise<void> {
40+
const emitter = degit(repositoryPath, {
41+
cache: false,
42+
force: true,
43+
verbose: true
44+
});
45+
await emitter.clone(destPath);
4346
}

TMLSPEC-cli/test-dir/docs/api/.gitkeep

Whitespace-only changes.

TMLSPEC-cli/test-dir/docs/project/.gitkeep

Whitespace-only changes.

TMLSPEC-cli/test-dir/docs/project/domain/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)