sillyspec 3.9.0 → 3.10.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/.claude/skills/sillyspec-archive/SKILL.md +17 -0
- package/.claude/skills/sillyspec-auto/SKILL.md +78 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +17 -0
- package/{templates/commit.md → .claude/skills/sillyspec-commit/SKILL.md} +32 -47
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -0
- package/.claude/skills/sillyspec-doctor/SKILL.md +27 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +17 -0
- package/.claude/skills/sillyspec-explore/SKILL.md +105 -0
- package/.claude/skills/sillyspec-export/SKILL.md +53 -0
- package/.claude/skills/sillyspec-init/SKILL.md +170 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +17 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +17 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +17 -0
- package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
- package/.claude/skills/sillyspec-scan/SKILL.md +17 -0
- package/.claude/skills/sillyspec-state/SKILL.md +54 -0
- package/.claude/skills/sillyspec-status/SKILL.md +17 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +17 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
- package/README.md +19 -11
- package/SKILL.md +15 -10
- package/package.json +7 -9
- package/packages/dashboard/dist/assets/index-BcM2J-hv.css +1 -0
- package/packages/dashboard/dist/assets/index-DpLHK4jv.js +7446 -0
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/packages/dashboard/dist/prototype-overview.html +256 -0
- package/packages/dashboard/package-lock.json +226 -6
- package/packages/dashboard/package.json +8 -5
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/public/prototype-dashboard.html +836 -0
- package/packages/dashboard/public/prototype-overview.html +256 -0
- package/packages/dashboard/server/executor.js +1 -1
- package/packages/dashboard/server/index.js +341 -113
- package/packages/dashboard/server/parser.js +442 -30
- package/packages/dashboard/server/watcher.js +214 -134
- package/packages/dashboard/src/App.vue +475 -71
- package/packages/dashboard/src/components/ActionBar.vue +36 -43
- package/packages/dashboard/src/components/CommandPalette.vue +45 -66
- package/packages/dashboard/src/components/DetailPanel.vue +68 -53
- package/packages/dashboard/src/components/DocPreview.vue +257 -0
- package/packages/dashboard/src/components/DocTree.vue +114 -0
- package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
- package/packages/dashboard/src/components/LogStream.vue +13 -33
- package/packages/dashboard/src/components/PipelineStage.vue +8 -8
- package/packages/dashboard/src/components/PipelineView.vue +99 -45
- package/packages/dashboard/src/components/ProjectCard.vue +187 -0
- package/packages/dashboard/src/components/ProjectList.vue +103 -45
- package/packages/dashboard/src/components/ProjectOverview.vue +152 -0
- package/packages/dashboard/src/components/StageBadge.vue +13 -13
- package/packages/dashboard/src/components/StepCard.vue +15 -15
- package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
- package/packages/dashboard/src/composables/useDashboard.js +48 -6
- package/packages/dashboard/src/composables/useKeyboard.js +6 -4
- package/packages/dashboard/src/composables/useLayout.js +131 -0
- package/packages/dashboard/src/main.js +4 -1
- package/packages/dashboard/src/style.css +17 -17
- package/src/index.js +141 -22
- package/src/init.js +93 -231
- package/src/migrate.js +117 -0
- package/src/progress.js +460 -0
- package/src/run.js +635 -0
- package/src/setup.js +2 -72
- package/src/stages/archive.js +54 -0
- package/src/stages/brainstorm.js +264 -0
- package/src/stages/doctor.js +303 -0
- package/src/stages/execute.js +287 -0
- package/src/stages/explore.js +34 -0
- package/src/stages/index.js +28 -0
- package/src/stages/plan.js +354 -0
- package/src/stages/propose.js +115 -0
- package/src/stages/quick.js +64 -0
- package/src/stages/scan.js +141 -0
- package/src/stages/status.js +65 -0
- package/src/stages/verify.js +135 -0
- package/.sillyspec/changes/dashboard/design.md +0 -219
- package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
- package/.sillyspec/specs/2026-04-05-dashboard-design.md +0 -206
- package/dist/steps/brainstorm/01-load-context.md +0 -30
- package/dist/steps/brainstorm/02-reuse-check.md +0 -6
- package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
- package/dist/steps/brainstorm/04-module-split.md +0 -23
- package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
- package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
- package/dist/steps/brainstorm/07-present-design.md +0 -3
- package/dist/steps/brainstorm/08-write-design.md +0 -21
- package/dist/steps/brainstorm/09-self-review.md +0 -15
- package/dist/steps/brainstorm/10-user-confirm.md +0 -3
- package/dist/steps/brainstorm/11-output-spec.md +0 -7
- package/dist/steps/brainstorm/manifest.yaml +0 -26
- package/dist/steps/execute/01-load-context.md +0 -41
- package/dist/steps/execute/02-scan-conventions.md +0 -47
- package/dist/steps/execute/03-skill-mcp.md +0 -19
- package/dist/steps/execute/04-assign-task.md +0 -22
- package/dist/steps/execute/04b-prompt-template.md +0 -54
- package/dist/steps/execute/05-write-test.md +0 -7
- package/dist/steps/execute/06-write-code.md +0 -8
- package/dist/steps/execute/07-run-test.md +0 -26
- package/dist/steps/execute/08-fix-issues.md +0 -28
- package/dist/steps/execute/09-next-task.md +0 -33
- package/dist/steps/execute/manifest.yaml +0 -28
- package/dist/steps/plan/01-load-context.md +0 -22
- package/dist/steps/plan/02-anchor-confirm.md +0 -1
- package/dist/steps/plan/03-expand-tasks.md +0 -33
- package/dist/steps/plan/04-mark-order.md +0 -15
- package/dist/steps/plan/05-e2e-planning.md +0 -17
- package/dist/steps/plan/06-self-check.md +0 -16
- package/dist/steps/plan/07-save.md +0 -1
- package/dist/steps/plan/manifest.yaml +0 -18
- package/dist/steps/scan/01-env-detect.md +0 -51
- package/dist/steps/scan/02-tech-stack.md +0 -16
- package/dist/steps/scan/03-conventions.md +0 -16
- package/dist/steps/scan/04-structure.md +0 -19
- package/dist/steps/scan/05-quality.md +0 -18
- package/dist/steps/scan/06-complete.md +0 -49
- package/dist/steps/scan/manifest.yaml +0 -16
- package/dist/steps/verify/01-load-specs.md +0 -28
- package/dist/steps/verify/02-check-tasks.md +0 -1
- package/dist/steps/verify/03-check-design.md +0 -6
- package/dist/steps/verify/04-run-tests.md +0 -7
- package/dist/steps/verify/05-e2e-tests.md +0 -27
- package/dist/steps/verify/05b-e2e-fix.md +0 -33
- package/dist/steps/verify/06-code-quality.md +0 -25
- package/dist/steps/verify/07-lint-check.md +0 -27
- package/dist/steps/verify/08-output-report.md +0 -14
- package/dist/steps/verify/manifest.yaml +0 -22
- package/docs/.vitepress/config.mts +0 -45
- package/docs/.vitepress/dist/404.html +0 -25
- package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
- package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
- package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
- package/docs/.vitepress/dist/hashmap.json +0 -1
- package/docs/.vitepress/dist/index.html +0 -28
- package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
- package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
- package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
- package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
- package/docs/.vitepress/dist/sillyspec/install.html +0 -32
- package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
- package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
- package/docs/.vitepress/dist/vp-icons.css +0 -1
- package/docs/index.md +0 -34
- package/docs/sillyspec/commands.md +0 -218
- package/docs/sillyspec/dashboard.md +0 -51
- package/docs/sillyspec/file-io.md +0 -34
- package/docs/sillyspec/getting-started.md +0 -61
- package/docs/sillyspec/install.md +0 -51
- package/docs/sillyspec/lifecycle.md +0 -146
- package/docs/sillyspec/structure.md +0 -62
- package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
- package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
- package/src/step.js +0 -543
- package/templates/archive.md +0 -120
- package/templates/brainstorm.md +0 -170
- package/templates/continue.md +0 -32
- package/templates/execute.md +0 -304
- package/templates/explore.md +0 -59
- package/templates/export.md +0 -21
- package/templates/init.md +0 -61
- package/templates/plan.md +0 -146
- package/templates/quick.md +0 -135
- package/templates/scan-quick.md +0 -49
- package/templates/scan.md +0 -156
- package/templates/skills/playwright-e2e/SKILL.md +0 -340
- package/templates/status.md +0 -75
- package/templates/verify.md +0 -236
- package/templates/workspace-sync.md +0 -99
- package/templates/workspace.md +0 -70
- /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
- /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
- /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
- /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
package/src/init.js
CHANGED
|
@@ -1,61 +1,36 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync,
|
|
2
|
-
import { join, resolve, dirname } from 'path';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
5
|
+
import { ProgressManager } from './progress.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import ora from 'ora';
|
|
8
7
|
|
|
9
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
9
|
const __dirname = dirname(__filename);
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
// ── 递归复制目录 ──
|
|
12
|
+
function copyDirSync(src, dst) {
|
|
13
|
+
mkdirSync(dst, { recursive: true });
|
|
14
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
15
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
16
|
+
const srcPath = join(src, entry.name);
|
|
17
|
+
const dstPath = join(dst, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
copyDirSync(srcPath, dstPath);
|
|
20
|
+
} else if (entry.name.endsWith('.md')) {
|
|
21
|
+
writeFileSync(dstPath, readFileSync(srcPath));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
// ── 元数据映射 ──
|
|
14
27
|
|
|
15
|
-
const DESCRIPTIONS = {
|
|
16
|
-
init: '绿地项目初始化 — 深度提问、调研、需求文档、路线图',
|
|
17
|
-
scan: '代码库扫描 — 支持快速扫描和深度扫描两阶段',
|
|
18
|
-
explore: '自由思考模式 — 讨论、画图、调研,不写代码',
|
|
19
|
-
brainstorm: '需求探索 — 结构化头脑风暴,生成设计文档(创建性工作前必用)',
|
|
20
|
-
plan: '编写实现计划 — 任务拆分与实现路径',
|
|
21
|
-
execute: '波次执行 — 子代理并行 + 强制 TDD + 两阶段审查',
|
|
22
|
-
verify: '验证实现 — 对照规范检查 + 测试套件',
|
|
23
|
-
archive: '归档变更 — 规范沉淀,可追溯',
|
|
24
|
-
commit: '智能提交 — 自动收集变更信息,生成 commit message',
|
|
25
|
-
status: '查看项目进度和状态',
|
|
26
|
-
continue: '自动判断并执行下一步',
|
|
27
|
-
state: '查看当前工作状态 — 显示 STATE.md 内容',
|
|
28
|
-
|
|
29
|
-
quick: '快速任务 — 跳过完整流程,直接做',
|
|
30
|
-
workspace: '工作区管理 — 初始化、管理多项目工作区,查看子项目状态',
|
|
31
|
-
export: '导出成功方案为可复用模板',
|
|
32
|
-
};
|
|
33
28
|
|
|
34
|
-
const ARG_HINTS = {
|
|
35
|
-
init: '[项目名]',
|
|
36
|
-
scan: '[可选:指定区域,如 \'api\' 或 \'auth\'] [--deep 深度扫描]',
|
|
37
|
-
explore: '[探索主题]',
|
|
38
|
-
brainstorm: '[需求或想法描述]',
|
|
39
|
-
|
|
40
|
-
plan: '[计划名]',
|
|
41
|
-
execute: '[任务编号或 \'all\']',
|
|
42
|
-
verify: '[可选:指定验证范围]',
|
|
43
|
-
archive: '[变更名]',
|
|
44
|
-
commit: '[可选:自定义 commit message]',
|
|
45
|
-
status: '',
|
|
46
|
-
continue: '',
|
|
47
|
-
state: '[可选备注]',
|
|
48
|
-
|
|
49
|
-
quick: '[任务描述]',
|
|
50
|
-
workspace: '[可选:add/remove/status/info]',
|
|
51
|
-
export: '<change-name> [--to <path>]',
|
|
52
|
-
};
|
|
53
29
|
|
|
54
|
-
const VALID_TOOLS = ['claude', '
|
|
30
|
+
const VALID_TOOLS = ['claude', 'cursor', 'openclaw', 'codex', 'gemini', 'opencode'];
|
|
55
31
|
|
|
56
32
|
const TOOL_LABELS = {
|
|
57
33
|
claude: 'Claude Code',
|
|
58
|
-
claude_skills: 'Claude Skills',
|
|
59
34
|
cursor: 'Cursor',
|
|
60
35
|
openclaw: 'OpenClaw',
|
|
61
36
|
codex: 'OpenAI Codex (通过 AGENTS.md)',
|
|
@@ -63,8 +38,6 @@ const TOOL_LABELS = {
|
|
|
63
38
|
opencode: 'OpenCode (通过 INSTRUCTIONS.md)',
|
|
64
39
|
};
|
|
65
40
|
|
|
66
|
-
|
|
67
|
-
// 指令文件工具:注入规范引用到指令文件
|
|
68
41
|
const INSTRUCTION_TOOLS = ['codex', 'gemini', 'opencode'];
|
|
69
42
|
|
|
70
43
|
const INSTRUCTION_FILE_MAP = {
|
|
@@ -78,83 +51,16 @@ const INJECTION_CONTENT = `## SillySpec — 规范驱动开发
|
|
|
78
51
|
在执行开发任务时,遵循以下规范:
|
|
79
52
|
|
|
80
53
|
### 代码规范
|
|
81
|
-
- 写代码前先读取 \`.sillyspec/
|
|
54
|
+
- 写代码前先读取 \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\`(代码风格)和 \`.sillyspec/docs/<project>/scan/ARCHITECTURE.md\`(架构)
|
|
82
55
|
- 调用已有方法前,用 grep 确认方法存在,不许编造
|
|
83
|
-
- 遵循 \`.sillyspec/
|
|
56
|
+
- 遵循 \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\` 中的代码风格
|
|
84
57
|
|
|
85
58
|
### 工作流程
|
|
86
|
-
- 读取 \`.sillyspec/
|
|
59
|
+
- 读取 \`.sillyspec/.runtime/progress.json\` 确认当前阶段(使用 \`sillyspec progress show\`)
|
|
87
60
|
- 各阶段产出文件位于 \`.sillyspec/changes/<变更名>/\` 下
|
|
88
|
-
- 详细流程参考模板文件:\`.sillyspec/.templates/\`(brainstorm.md, plan.md, execute.md 等)
|
|
89
61
|
`;
|
|
90
62
|
|
|
91
|
-
// ──
|
|
92
|
-
|
|
93
|
-
function generateClaude(projectDir, name, desc, body, argHint, version) {
|
|
94
|
-
const outDir = join(projectDir, '.claude', 'commands', 'sillyspec');
|
|
95
|
-
mkdirSync(outDir, { recursive: true });
|
|
96
|
-
writeFileSync(join(outDir, `${name}.md`),
|
|
97
|
-
`---
|
|
98
|
-
description: ${desc}
|
|
99
|
-
argument-hint: "${argHint}"
|
|
100
|
-
version: "${version}"
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
${body}`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function generateClaudeSkills(projectDir, name, desc, body, argHint, version) {
|
|
108
|
-
const outDir = join(projectDir, '.claude', 'skills', `sillyspec-${name}`);
|
|
109
|
-
mkdirSync(outDir, { recursive: true });
|
|
110
|
-
writeFileSync(join(outDir, 'SKILL.md'),
|
|
111
|
-
`---
|
|
112
|
-
name: sillyspec:${name}
|
|
113
|
-
description: ${desc}
|
|
114
|
-
version: "${version}"
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
${body}`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function generateCursor(projectDir, name, desc, body, argHint, version) {
|
|
122
|
-
const outDir = join(projectDir, '.cursor', 'commands');
|
|
123
|
-
mkdirSync(outDir, { recursive: true });
|
|
124
|
-
writeFileSync(join(outDir, `sillyspec-${name}.md`),
|
|
125
|
-
`---
|
|
126
|
-
name: /sillyspec-${name}
|
|
127
|
-
id: sillyspec-${name}
|
|
128
|
-
description: ${desc}
|
|
129
|
-
version: "${version}"
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
${body}`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function generateOpenclaw(projectDir, name, desc, body, argHint, version) {
|
|
137
|
-
const outDir = join(projectDir, '.openclaw', 'skills', `sillyspec-${name}`);
|
|
138
|
-
mkdirSync(outDir, { recursive: true });
|
|
139
|
-
writeFileSync(join(outDir, 'SKILL.md'),
|
|
140
|
-
`---
|
|
141
|
-
name: sillyspec:${name}
|
|
142
|
-
description: ${desc}
|
|
143
|
-
version: "${version}"
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
${body}`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const GENERATORS = {
|
|
151
|
-
claude: generateClaude,
|
|
152
|
-
claude_skills: generateClaudeSkills,
|
|
153
|
-
cursor: generateCursor,
|
|
154
|
-
openclaw: generateOpenclaw,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// ── 指令文件注入 ──
|
|
63
|
+
// ── 注入指令文件 ──
|
|
158
64
|
|
|
159
65
|
function injectInstructions(tool, projectDir) {
|
|
160
66
|
const fileName = INSTRUCTION_FILE_MAP[tool];
|
|
@@ -180,7 +86,6 @@ function injectInstructions(tool, projectDir) {
|
|
|
180
86
|
function detectTools(projectDir) {
|
|
181
87
|
const found = [];
|
|
182
88
|
if (existsSync(join(projectDir, '.claude'))) found.push('claude');
|
|
183
|
-
if (existsSync(join(projectDir, '.claude', 'skills'))) found.push('claude_skills');
|
|
184
89
|
if (existsSync(join(projectDir, '.cursor'))) found.push('cursor');
|
|
185
90
|
if (existsSync(join(projectDir, '.openclaw'))) found.push('openclaw');
|
|
186
91
|
if (existsSync(join(projectDir, 'AGENTS.md'))) found.push('codex');
|
|
@@ -198,22 +103,33 @@ function isTTY() {
|
|
|
198
103
|
|
|
199
104
|
// ── 核心安装逻辑 ──
|
|
200
105
|
|
|
201
|
-
async function doInstall(projectDir, tools,
|
|
106
|
+
async function doInstall(projectDir, tools, subprojects = []) {
|
|
202
107
|
// 创建基础目录
|
|
203
|
-
// .sillyspec/
|
|
204
|
-
// .sillyspec/
|
|
205
|
-
// .sillyspec/
|
|
206
|
-
// .sillyspec/
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
108
|
+
// .sillyspec/projects/ → 项目注册表
|
|
109
|
+
// .sillyspec/docs/<name>/ → 统一文档中心
|
|
110
|
+
// .sillyspec/knowledge/ → 跨项目共享知识库
|
|
111
|
+
// .sillyspec/.runtime/ → progress (gitignored)
|
|
112
|
+
|
|
113
|
+
// 注册当前项目到 projects/
|
|
114
|
+
const projectName = basename(projectDir) || 'project';
|
|
115
|
+
const projectsDir = join(projectDir, '.sillyspec', 'projects');
|
|
116
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
117
|
+
const projectYamlPath = join(projectsDir, `${projectName}.yaml`);
|
|
118
|
+
if (!existsSync(projectYamlPath)) {
|
|
119
|
+
writeFileSync(projectYamlPath, `name: ${projectName}\npath: .\nstatus: active\n`);
|
|
214
120
|
}
|
|
215
121
|
|
|
216
|
-
//
|
|
122
|
+
// 创建 .sillyspec/docs/<projectName>/scan/ 子目录(代码扫描结果)
|
|
123
|
+
const scanDir = join(projectDir, '.sillyspec', 'docs', projectName, 'scan');
|
|
124
|
+
mkdirSync(scanDir, { recursive: true });
|
|
125
|
+
const gitkeepPath = join(scanDir, '.gitkeep');
|
|
126
|
+
if (!existsSync(gitkeepPath)) writeFileSync(gitkeepPath, '');
|
|
127
|
+
|
|
128
|
+
// 创建 shared/workspace 目录
|
|
129
|
+
mkdirSync(join(projectDir, '.sillyspec', 'shared'), { recursive: true });
|
|
130
|
+
mkdirSync(join(projectDir, '.sillyspec', 'workspace'), { recursive: true });
|
|
131
|
+
|
|
132
|
+
// 创建知识库骨架
|
|
217
133
|
const knowledgeDir = join(projectDir, '.sillyspec', 'knowledge');
|
|
218
134
|
mkdirSync(knowledgeDir, { recursive: true });
|
|
219
135
|
const indexPath = join(knowledgeDir, 'INDEX.md');
|
|
@@ -231,6 +147,13 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
|
|
|
231
147
|
mkdirSync(join(runtimeDir, sub), { recursive: true });
|
|
232
148
|
}
|
|
233
149
|
|
|
150
|
+
// 创建初始 progress.json
|
|
151
|
+
const progressPath = join(runtimeDir, 'progress.json');
|
|
152
|
+
if (!existsSync(progressPath)) {
|
|
153
|
+
const pm = new ProgressManager();
|
|
154
|
+
pm.init(projectDir);
|
|
155
|
+
}
|
|
156
|
+
|
|
234
157
|
// 创建初始 user-inputs.md
|
|
235
158
|
const inputsPath = join(runtimeDir, 'user-inputs.md');
|
|
236
159
|
if (!existsSync(inputsPath)) {
|
|
@@ -238,7 +161,7 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
|
|
|
238
161
|
}
|
|
239
162
|
|
|
240
163
|
const gitignorePath = join(projectDir, '.gitignore');
|
|
241
|
-
const ignoreRules = ['.sillyspec/
|
|
164
|
+
const ignoreRules = ['.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml', '.sillyspec/.runtime/'];
|
|
242
165
|
if (existsSync(gitignorePath)) {
|
|
243
166
|
const content = readFileSync(gitignorePath, 'utf8');
|
|
244
167
|
let updated = content.trimEnd();
|
|
@@ -252,83 +175,44 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
|
|
|
252
175
|
writeFileSync(gitignorePath, ignoreRules.join('\n') + '\n');
|
|
253
176
|
}
|
|
254
177
|
|
|
255
|
-
//
|
|
256
|
-
const templateFiles = readdirSync(TEMPLATE_DIR).filter(f => f.endsWith('.md'));
|
|
257
|
-
let count = 0;
|
|
258
|
-
|
|
178
|
+
// 注入指令文件(codex/gemini/opencode)
|
|
259
179
|
for (let i = 0; i < tools.length; i++) {
|
|
260
180
|
const toolName = tools[i];
|
|
261
|
-
const label = TOOL_LABELS[toolName] || toolName;
|
|
262
|
-
|
|
263
181
|
if (INSTRUCTION_TOOLS.includes(toolName)) {
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
injectInstructions(toolName, projectDir);
|
|
267
|
-
// 复制模板文件到 .sillyspec/.templates/
|
|
268
|
-
const templatesSourceDir = join(TEMPLATE_DIR);
|
|
269
|
-
const templatesDir = join(projectDir, '.sillyspec', '.templates');
|
|
270
|
-
if (!existsSync(templatesDir)) {
|
|
271
|
-
mkdirSync(templatesDir, { recursive: true });
|
|
272
|
-
for (const file of readdirSync(templatesSourceDir)) {
|
|
273
|
-
if (file.endsWith('.md')) {
|
|
274
|
-
copyFileSync(join(templatesSourceDir, file), join(templatesDir, file));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
spinner.succeed(`${label} 完成`);
|
|
279
|
-
count++;
|
|
280
|
-
} catch (err) {
|
|
281
|
-
spinner.fail(`${label} 失败: ${err.message}`);
|
|
282
|
-
throw err;
|
|
283
|
-
}
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const spinner = ora(`安装 ${label}... (${i + 1}/${tools.length})`).start();
|
|
288
|
-
try {
|
|
289
|
-
const gen = GENERATORS[toolName];
|
|
290
|
-
const ver = getVersion();
|
|
291
|
-
for (const file of templateFiles) {
|
|
292
|
-
const name = file.replace('.md', '');
|
|
293
|
-
const desc = DESCRIPTIONS[name] || `SillySpec ${name}`;
|
|
294
|
-
const argHint = ARG_HINTS[name] || '';
|
|
295
|
-
const body = readFileSync(join(TEMPLATE_DIR, file), 'utf8');
|
|
296
|
-
gen(projectDir, name, desc, body, argHint, ver);
|
|
297
|
-
count++;
|
|
298
|
-
}
|
|
299
|
-
spinner.succeed(`${label} 完成`);
|
|
300
|
-
} catch (err) {
|
|
301
|
-
spinner.fail(`${label} 失败: ${err.message}`);
|
|
302
|
-
throw err;
|
|
182
|
+
injectInstructions(toolName, projectDir);
|
|
303
183
|
}
|
|
304
184
|
}
|
|
305
185
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
)
|
|
186
|
+
// 复制 skills 到各工具目录
|
|
187
|
+
const skillToolDirs = {
|
|
188
|
+
claude: '.claude/skills',
|
|
189
|
+
codex: '.codex/skills',
|
|
190
|
+
openclaw: '.openclaw/skills',
|
|
191
|
+
opencode: '.opencode/skills',
|
|
192
|
+
}
|
|
193
|
+
const skillsSource = join(__dirname, '..', '.claude', 'skills');
|
|
194
|
+
if (existsSync(skillsSource)) {
|
|
195
|
+
const sillyspecSkills = readdirSync(skillsSource).filter(f => f.startsWith('sillyspec-') && statSync(join(skillsSource, f)).isDirectory());
|
|
196
|
+
if (sillyspecSkills.length > 0) {
|
|
197
|
+
for (const [tool, dir] of Object.entries(skillToolDirs)) {
|
|
198
|
+
if (!tools.includes(tool)) continue
|
|
199
|
+
const targetDir = join(projectDir, dir)
|
|
200
|
+
mkdirSync(targetDir, { recursive: true })
|
|
201
|
+
for (const skill of sillyspecSkills) {
|
|
202
|
+
copyDirSync(join(skillsSource, skill), join(targetDir, skill))
|
|
203
|
+
}
|
|
204
|
+
console.log(chalk.green(` ✓ ${TOOL_LABELS[tool]} skills 已同步 (${sillyspecSkills.length} 个)`))
|
|
320
205
|
}
|
|
321
206
|
}
|
|
207
|
+
} else {
|
|
208
|
+
console.log(chalk.yellow(' ⚠ 未找到 skills 目录(npm 包内无 .claude/skills/),跳过同步'));
|
|
322
209
|
}
|
|
323
|
-
|
|
324
|
-
return count;
|
|
325
210
|
}
|
|
326
211
|
|
|
327
212
|
// ── 安装完成总结 ──
|
|
328
213
|
|
|
329
|
-
function showSummary(version, tools
|
|
214
|
+
function showSummary(version, tools) {
|
|
330
215
|
const toolLabels = tools.map(t => TOOL_LABELS[t] || t);
|
|
331
|
-
const mode = isWorkspace ? '多项目工作区' : '单项目';
|
|
332
216
|
|
|
333
217
|
console.log('');
|
|
334
218
|
console.log(chalk.green(' ═══════════════════════════════════════'));
|
|
@@ -336,20 +220,13 @@ function showSummary(version, tools, isWorkspace, count) {
|
|
|
336
220
|
console.log(chalk.green(' ═══════════════════════════════════════'));
|
|
337
221
|
console.log('');
|
|
338
222
|
console.log(` 已安装工具: ${chalk.cyan(toolLabels.join(', '))}`);
|
|
339
|
-
console.log(` 模式: ${chalk.yellow(mode)}`);
|
|
340
|
-
console.log('');
|
|
341
|
-
console.log(` 📄 ${count} 个命令已就绪`);
|
|
342
223
|
console.log(' 📁 .sillyspec/ — 项目规范目录');
|
|
343
224
|
console.log('');
|
|
344
|
-
console.log('
|
|
345
|
-
console.log('
|
|
346
|
-
console.log('
|
|
347
|
-
console.log(' 自由思考:' + chalk.bold('/sillyspec:explore "你的想法"'));
|
|
348
|
-
console.log('');
|
|
349
|
-
console.log(chalk.gray(' 重启你的 AI 工具以使 slash commands 生效。'));
|
|
225
|
+
console.log(' 下一步:使用 AI 技能开始工作');
|
|
226
|
+
console.log(' OpenClaw: ' + chalk.bold('/sillyspec:brainstorm'));
|
|
227
|
+
console.log(' Claude Code: ' + chalk.bold('/sillyspec:brainstorm'));
|
|
350
228
|
console.log('');
|
|
351
229
|
console.log(chalk.dim(' 💡 推荐安装 MCP 工具增强 AI 能力:sillyspec setup'));
|
|
352
|
-
console.log(chalk.dim(' Context7 — 查最新文档 | grep.app — 搜开源实现 | Chrome DevTools — 浏览器自动化'));
|
|
353
230
|
console.log('');
|
|
354
231
|
}
|
|
355
232
|
|
|
@@ -367,7 +244,7 @@ export function getVersion() {
|
|
|
367
244
|
// ── 主命令 ──
|
|
368
245
|
|
|
369
246
|
export async function cmdInit(projectDir, options = {}) {
|
|
370
|
-
const { tool,
|
|
247
|
+
const { tool, interactive } = options;
|
|
371
248
|
const version = getVersion();
|
|
372
249
|
|
|
373
250
|
// ── 交互式模式(--interactive 或 -i)──
|
|
@@ -399,20 +276,11 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
399
276
|
validate: (answer) => answer.length > 0 || '至少选择一个工具',
|
|
400
277
|
});
|
|
401
278
|
|
|
402
|
-
//
|
|
403
|
-
const isWorkspace = await select({
|
|
404
|
-
message: '选择项目模式',
|
|
405
|
-
choices: [
|
|
406
|
-
{ name: '单项目模式', value: 'false' },
|
|
407
|
-
{ name: '多项目工作区', value: 'true' },
|
|
408
|
-
],
|
|
409
|
-
}) === 'true';
|
|
410
|
-
|
|
411
|
-
// 工作区子项目引导
|
|
279
|
+
// 子项目引导(仅交互模式)
|
|
412
280
|
let subprojects = [];
|
|
413
|
-
|
|
281
|
+
{
|
|
414
282
|
console.log('');
|
|
415
|
-
console.log(chalk.yellow('📋
|
|
283
|
+
console.log(chalk.yellow('📋 添加子项目'));
|
|
416
284
|
console.log(chalk.dim(' 子项目是工作区中的独立项目目录(如 frontend/、backend/)'));
|
|
417
285
|
console.log('');
|
|
418
286
|
|
|
@@ -475,8 +343,8 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
475
343
|
}
|
|
476
344
|
|
|
477
345
|
console.log('');
|
|
478
|
-
|
|
479
|
-
showSummary(version, selectedTools
|
|
346
|
+
await doInstall(projectDir, selectedTools, subprojects);
|
|
347
|
+
showSummary(version, selectedTools);
|
|
480
348
|
return;
|
|
481
349
|
}
|
|
482
350
|
|
|
@@ -494,23 +362,17 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
494
362
|
tools = detectTools(projectDir);
|
|
495
363
|
}
|
|
496
364
|
|
|
497
|
-
|
|
365
|
+
await doInstall(projectDir, tools);
|
|
498
366
|
|
|
499
367
|
console.log('');
|
|
500
368
|
console.log(chalk.green(` ✅ SillySpec v${version} 安装完成!`));
|
|
501
369
|
console.log('');
|
|
502
|
-
console.log(` 📄 ${count} 个命令已就绪`);
|
|
503
370
|
console.log(' 📁 .sillyspec/ — 项目规范目录');
|
|
504
371
|
console.log('');
|
|
505
|
-
console.log('
|
|
506
|
-
console.log(`
|
|
507
|
-
console.log(`
|
|
508
|
-
console.log(` 自由探索 → ${chalk.bold('/sillyspec:explore "你的想法"')}`);
|
|
509
|
-
if (workspace) {
|
|
510
|
-
console.log(` 管理子项目 → ${chalk.bold('/sillyspec:workspace add')}`);
|
|
511
|
-
}
|
|
372
|
+
console.log(' 下一步:使用 AI 技能开始工作');
|
|
373
|
+
console.log(` OpenClaw: ${chalk.bold('/sillyspec:brainstorm')}`);
|
|
374
|
+
console.log(` Claude Code: ${chalk.bold('/sillyspec:brainstorm')}`);
|
|
512
375
|
console.log('');
|
|
513
376
|
console.log(chalk.dim(' 💡 增强能力:sillyspec setup(安装 MCP 工具)'));
|
|
514
|
-
console.log(chalk.dim(' 💡 完整配置:sillyspec init --interactive'));
|
|
515
377
|
console.log('');
|
|
516
378
|
}
|
package/src/migrate.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync } from 'fs';
|
|
2
|
+
import { basename, join, resolve } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Migrate old .sillyspec/ structure to unified .sillyspec/docs/<project>/ structure
|
|
7
|
+
* @param {string} projectDir - Path to the project directory
|
|
8
|
+
*/
|
|
9
|
+
export function migrateDocs(projectDir) {
|
|
10
|
+
const sillyspecDir = join(projectDir, '.sillyspec');
|
|
11
|
+
if (!existsSync(sillyspecDir)) {
|
|
12
|
+
console.error('❌ .sillyspec/ 目录不存在');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Determine project name from projects/*.yaml or directory name
|
|
17
|
+
let projectName = basename(resolve(projectDir));
|
|
18
|
+
const projectsDir = join(sillyspecDir, 'projects');
|
|
19
|
+
if (existsSync(projectsDir)) {
|
|
20
|
+
const yamlFiles = readdirSync(projectsDir).filter(f => f.endsWith('.yaml'));
|
|
21
|
+
if (yamlFiles.length > 0) {
|
|
22
|
+
projectName = yamlFiles[0].replace('.yaml', '');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan(`📦 迁移项目: ${projectName}`));
|
|
27
|
+
console.log('');
|
|
28
|
+
|
|
29
|
+
const docsBase = join(sillyspecDir, 'docs', projectName);
|
|
30
|
+
let migrated = 0;
|
|
31
|
+
|
|
32
|
+
// 1. codebase/ → .sillyspec/docs/<project>/scan/
|
|
33
|
+
const codebaseDir = join(sillyspecDir, 'codebase');
|
|
34
|
+
if (existsSync(codebaseDir)) {
|
|
35
|
+
const targetDir = join(docsBase, 'scan');
|
|
36
|
+
mkdirSync(targetDir, { recursive: true });
|
|
37
|
+
const files = readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const src = join(codebaseDir, file);
|
|
40
|
+
const dest = join(targetDir, file);
|
|
41
|
+
if (!existsSync(dest)) {
|
|
42
|
+
copyFileSync(src, dest);
|
|
43
|
+
console.log(chalk.green(' ✅') + ` scan/${file}`);
|
|
44
|
+
migrated++;
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.yellow(' ⏭️') + ` scan/${file} (已存在)`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. specs/ is deprecated — designs live in changes/<变更名>/design.md
|
|
52
|
+
|
|
53
|
+
// 3. changes/archive/ → .sillyspec/docs/<project>/archive/
|
|
54
|
+
const archiveDir = join(sillyspecDir, 'changes', 'archive');
|
|
55
|
+
if (existsSync(archiveDir)) {
|
|
56
|
+
const targetDir = join(docsBase, 'archive');
|
|
57
|
+
mkdirSync(targetDir, { recursive: true });
|
|
58
|
+
const entries = readdirSync(archiveDir);
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const src = join(archiveDir, entry);
|
|
61
|
+
const dest = join(targetDir, entry);
|
|
62
|
+
if (!existsSync(dest)) {
|
|
63
|
+
copyFileSync(src, dest);
|
|
64
|
+
console.log(chalk.green(' ✅') + ` archive/${entry}`);
|
|
65
|
+
migrated++;
|
|
66
|
+
} else {
|
|
67
|
+
console.log(chalk.yellow(' ⏭️') + ` archive/${entry} (已存在)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. knowledge/ → .sillyspec/docs/<project>/archive/ (append knowledge files)
|
|
73
|
+
const knowledgeDir = join(sillyspecDir, 'knowledge');
|
|
74
|
+
if (existsSync(knowledgeDir)) {
|
|
75
|
+
const targetDir = join(docsBase, 'archive');
|
|
76
|
+
mkdirSync(targetDir, { recursive: true });
|
|
77
|
+
const files = readdirSync(knowledgeDir).filter(f => f.endsWith('.md'));
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const src = join(knowledgeDir, file);
|
|
80
|
+
const dest = join(targetDir, `knowledge-${file}`);
|
|
81
|
+
if (!existsSync(dest)) {
|
|
82
|
+
copyFileSync(src, dest);
|
|
83
|
+
console.log(chalk.green(' ✅') + ` archive/knowledge-${file}`);
|
|
84
|
+
migrated++;
|
|
85
|
+
} else {
|
|
86
|
+
console.log(chalk.yellow(' ⏭️') + ` archive/knowledge-${file} (已存在)`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. quicklog/ → .sillyspec/docs/<project>/quicklog/
|
|
92
|
+
const quicklogDir = join(sillyspecDir, 'quicklog');
|
|
93
|
+
if (existsSync(quicklogDir)) {
|
|
94
|
+
const targetDir = join(docsBase, 'quicklog');
|
|
95
|
+
mkdirSync(targetDir, { recursive: true });
|
|
96
|
+
const files = readdirSync(quicklogDir).filter(f => f.endsWith('.md'));
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
const src = join(quicklogDir, file);
|
|
99
|
+
const dest = join(targetDir, file);
|
|
100
|
+
if (!existsSync(dest)) {
|
|
101
|
+
copyFileSync(src, dest);
|
|
102
|
+
console.log(chalk.green(' ✅') + ` quicklog/${file}`);
|
|
103
|
+
migrated++;
|
|
104
|
+
} else {
|
|
105
|
+
console.log(chalk.yellow(' ⏭️') + ` quicklog/${file} (已存在)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('');
|
|
111
|
+
if (migrated > 0) {
|
|
112
|
+
console.log(chalk.green(` ✅ 迁移完成,共迁移 ${migrated} 个文件`));
|
|
113
|
+
console.log(chalk.dim(' 旧文件保留在原位,确认无误后可手动删除'));
|
|
114
|
+
} else {
|
|
115
|
+
console.log(chalk.yellow(' 没有需要迁移的文件'));
|
|
116
|
+
}
|
|
117
|
+
}
|