depa-codument 0.4.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.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/package.json +63 -0
- package/src/cli/commands/archive.ts +519 -0
- package/src/cli/commands/decisions.ts +123 -0
- package/src/cli/commands/engineering.ts +105 -0
- package/src/cli/commands/init.ts +54 -0
- package/src/cli/commands/list.ts +73 -0
- package/src/cli/commands/modeling.ts +105 -0
- package/src/cli/commands/show.ts +238 -0
- package/src/cli/commands/status.ts +140 -0
- package/src/cli/commands/upgrade-track.ts +385 -0
- package/src/cli/commands/upgrade-workspace.ts +138 -0
- package/src/cli/commands/validate.ts +330 -0
- package/src/cli/engineering/config.ts +68 -0
- package/src/cli/engineering/lint.ts +58 -0
- package/src/cli/engineering/merge.ts +172 -0
- package/src/cli/engineering/registry.ts +230 -0
- package/src/cli/engineering/schema.ts +126 -0
- package/src/cli/engineering/validate.ts +286 -0
- package/src/cli/index.ts +136 -0
- package/src/cli/modeling/config.ts +68 -0
- package/src/cli/modeling/lint.ts +58 -0
- package/src/cli/modeling/merge.ts +172 -0
- package/src/cli/modeling/registry.ts +229 -0
- package/src/cli/modeling/schema.ts +160 -0
- package/src/cli/modeling/validate.ts +282 -0
- package/src/cli/utils/index.ts +941 -0
- package/src/cli/utils/install.ts +291 -0
- package/src/cli/utils/spec-xml.ts +673 -0
- package/src/cli/utils/track-time.ts +75 -0
- package/src/cli/utils/vfs.ts +102 -0
- package/src/templates/codument/README.md +59 -0
- package/src/templates/codument/attractors/product.md +17 -0
- package/src/templates/codument/attractors/project.md +10 -0
- package/src/templates/codument/backlog/README.md +33 -0
- package/src/templates/codument/config/attractor-profiles.xml +31 -0
- package/src/templates/codument/config/engineering.xml +22 -0
- package/src/templates/codument/config/modeling.xml +22 -0
- package/src/templates/codument/config/operation-hooks.xml +55 -0
- package/src/templates/codument/memory/README.md +13 -0
- package/src/templates/codument/missions/README.md +125 -0
- package/src/templates/codument/sop/README.md +14 -0
- package/src/templates/codument/std/AGENTS.md +82 -0
- package/src/templates/codument/std/attractors/depa-attractor.md +572 -0
- package/src/templates/codument/std/attractors/knowledge-tiers.md +128 -0
- package/src/templates/codument/std/attractors/model-driven-docs.md +293 -0
- package/src/templates/codument/std/attractors/project-memory.md +48 -0
- package/src/templates/codument/std/docs-impl-fractal/index.md +110 -0
- package/src/templates/codument/std/docs-modeling-fractal/index.md +156 -0
- package/src/templates/codument/std/kernel-pointer.md +19 -0
- package/src/templates/codument/std/operations/README.md +30 -0
- package/src/templates/codument/std/operations/_operation-spec.md +41 -0
- package/src/templates/codument/std/operations/archive-mission.md +66 -0
- package/src/templates/codument/std/operations/archive-track.md +238 -0
- package/src/templates/codument/std/operations/artifact-sync.md +172 -0
- package/src/templates/codument/std/operations/discuss-phase.md +214 -0
- package/src/templates/codument/std/operations/discuss.md +87 -0
- package/src/templates/codument/std/operations/docs-bootstrap.md +148 -0
- package/src/templates/codument/std/operations/gap-loop.md +301 -0
- package/src/templates/codument/std/operations/impl-mission.md +167 -0
- package/src/templates/codument/std/operations/impl-quick.md +79 -0
- package/src/templates/codument/std/operations/impl-track.md +537 -0
- package/src/templates/codument/std/operations/migrate.md +337 -0
- package/src/templates/codument/std/operations/plan-mission.md +230 -0
- package/src/templates/codument/std/operations/plan-track-wave.md +231 -0
- package/src/templates/codument/std/operations/plan-track.md +579 -0
- package/src/templates/codument/std/operations/revise-track.md +136 -0
- package/src/templates/codument/std/operations/validate.md +339 -0
- package/src/templates/codument/std/operations/verify.md +184 -0
- package/src/templates/codument/std/root-agents.md +39 -0
- package/src/templates/codument/std/sop/questioning.md +98 -0
- package/src/templates/codument/std/sop/tdd.md +26 -0
- package/src/templates/codument/std/sop/validation.md +25 -0
- package/src/templates/codument/std/sop/wave-exec.md +42 -0
- package/src/templates/codument/std/sop/workflow.md +35 -0
- package/src/templates/codument/std/spec/behavior-delta.md +36 -0
- package/src/templates/codument/std/spec/behavior-registry.md +42 -0
- package/src/templates/codument/std/spec/engineering-delta.md +68 -0
- package/src/templates/codument/std/spec/engineering-node-schema.md +86 -0
- package/src/templates/codument/std/spec/engineering-registry.md +82 -0
- package/src/templates/codument/std/spec/flow-notation.md +93 -0
- package/src/templates/codument/std/spec/folder-manifest.md +99 -0
- package/src/templates/codument/std/spec/mission-xml-spec.md +249 -0
- package/src/templates/codument/std/spec/modeling-delta.md +85 -0
- package/src/templates/codument/std/spec/modeling-node-schema.md +183 -0
- package/src/templates/codument/std/spec/modeling-registry.md +49 -0
- package/src/templates/codument/std/spec/track-xml-spec.md +272 -0
- package/src/templates/codument/std/spec/xnl-format.md +301 -0
- package/src/templates/codument/workflows/README.md +15 -0
- package/src/templates/manifest.ts +177 -0
- package/src/templates/skills/README.md +38 -0
- package/src/templates/skills/codument-archive/SKILL.md +17 -0
- package/src/templates/skills/codument-archive-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-archive-track/SKILL.md +17 -0
- package/src/templates/skills/codument-artifact-sync/SKILL.md +17 -0
- package/src/templates/skills/codument-code-quality-score/SKILL.md +67 -0
- package/src/templates/skills/codument-decision-tree/SKILL.md +40 -0
- package/src/templates/skills/codument-discuss/SKILL.md +17 -0
- package/src/templates/skills/codument-discuss-phase/SKILL.md +17 -0
- package/src/templates/skills/codument-docs-bootstrap/SKILL.md +17 -0
- package/src/templates/skills/codument-gap-loop/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-quick/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-track/SKILL.md +17 -0
- package/src/templates/skills/codument-implement/SKILL.md +14 -0
- package/src/templates/skills/codument-migrate/SKILL.md +17 -0
- package/src/templates/skills/codument-modeling-engineering-e2e/SKILL.md +74 -0
- package/src/templates/skills/codument-plan-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-plan-track/SKILL.md +17 -0
- package/src/templates/skills/codument-plan-track-wave/SKILL.md +17 -0
- package/src/templates/skills/codument-revise-track/SKILL.md +17 -0
- package/src/templates/skills/codument-track/SKILL.md +14 -0
- package/src/templates/skills/codument-validate/SKILL.md +17 -0
- package/src/templates/skills/codument-verify/SKILL.md +17 -0
- package/src/types/text-assets.d.ts +9 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { TEMPLATE_FILES } from '../../templates/manifest';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pure text-copy install of the embedded templates (src/templates/) into a
|
|
8
|
+
* project. No template rendering, no per-agent prompt variants, no interactive
|
|
9
|
+
* prompts — init / upgrade-workspace are just file copies.
|
|
10
|
+
*
|
|
11
|
+
* Template layout (paths are relative to src/templates/):
|
|
12
|
+
* codument/** → <workspace>/codument/** (workspace files)
|
|
13
|
+
* skills/<name>/SKILL.md → <agent skills dir>/<name>/SKILL.md (agent skill shells)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Where each coding agent looks for installable skills. Destination only — the skill content is identical for every agent. */
|
|
17
|
+
const SUPPORTED_AGENTS = ['claude', 'codeflicker', 'eidolon', 'opencode', 'sparrow', 'codex'] as const;
|
|
18
|
+
export type CLITool = typeof SUPPORTED_AGENTS[number];
|
|
19
|
+
|
|
20
|
+
export const SKILLS_DIR_BY_AGENT: Record<CLITool, string> = {
|
|
21
|
+
claude: path.join('.claude', 'skills'),
|
|
22
|
+
codeflicker: path.join('.codeflicker', 'skills'),
|
|
23
|
+
eidolon: path.join('.eidolon', 'skills'),
|
|
24
|
+
opencode: path.join('.opencode', 'skills'),
|
|
25
|
+
sparrow: path.join('.sparrow', 'skills'),
|
|
26
|
+
codex: path.join(os.homedir(), '.codex', 'skills'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const AGENTS_BEGIN = '<!-- codument:begin -->';
|
|
30
|
+
const AGENTS_END = '<!-- codument:end -->';
|
|
31
|
+
const CODUMENT_GITIGNORE_RULES = [
|
|
32
|
+
'codument/**/analysis',
|
|
33
|
+
'codument/**/reports',
|
|
34
|
+
];
|
|
35
|
+
const DEPRECATED_SKILLS = [
|
|
36
|
+
'codument-execute-wave',
|
|
37
|
+
'codument-init',
|
|
38
|
+
'codument-plan-schedule',
|
|
39
|
+
'codument-plan-wave',
|
|
40
|
+
'codument-status',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const AGENTS_MANAGED_BODY = `# Codument Instructions
|
|
44
|
+
|
|
45
|
+
打开 \`@/codument/std/AGENTS.md\`,当请求:
|
|
46
|
+
- 涉及 planning / proposal / track(proposal、behavior、change、plan、track、implement)
|
|
47
|
+
- 新增能力、破坏性变更、架构/模式调整、或较大性能/安全工作
|
|
48
|
+
- 表述模糊、需要权威规范再动手
|
|
49
|
+
- 补充需求属于某进行中 track 范围
|
|
50
|
+
|
|
51
|
+
从 \`@/codument/std/AGENTS.md\` 了解:如何创建并应用变更(track 三阶段)、behavior 增量 / track.xml 格式、项目结构与工作流。
|
|
52
|
+
|
|
53
|
+
快速路由:
|
|
54
|
+
- 项目工程约束 / 代码边界 / 技术取舍:\`@/codument/attractors/project.md\`
|
|
55
|
+
- 产品目标 / 用户价值 / 范围取舍:\`@/codument/attractors/product.md\`
|
|
56
|
+
- 信息该落哪层 / 何时晋升 / 冲突谁赢:\`@/codument/std/attractors/knowledge-tiers.md\`
|
|
57
|
+
- docs/modeling 与 docs/impl 写法 / 路由 / frontmatter:\`@/codument/std/attractors/model-driven-docs.md\`
|
|
58
|
+
- 长期记忆 lessons / incidents / patterns / summaries:\`@/codument/std/attractors/project-memory.md\`
|
|
59
|
+
|
|
60
|
+
保留本受管块,'codument upgrade-workspace' 会刷新它。`;
|
|
61
|
+
|
|
62
|
+
export interface ResolvedSkillsTarget {
|
|
63
|
+
agent: CLITool | string;
|
|
64
|
+
skillsDir: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const CLI_TOOLS_CONFIG_FILE = path.join('codument', 'config', 'cli-tools.json');
|
|
68
|
+
const LEGACY_STATE_FILE = path.join('codument', 'state.json');
|
|
69
|
+
|
|
70
|
+
function isCliTool(value: string): value is CLITool {
|
|
71
|
+
return (SUPPORTED_AGENTS as readonly string[]).includes(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function uniqueAgents(values: string[]): CLITool[] {
|
|
75
|
+
const seen = new Set<CLITool>();
|
|
76
|
+
const agents: CLITool[] = [];
|
|
77
|
+
for (const value of values) {
|
|
78
|
+
const agent = value.trim().toLowerCase();
|
|
79
|
+
if (!isCliTool(agent) || seen.has(agent)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
seen.add(agent);
|
|
83
|
+
agents.push(agent);
|
|
84
|
+
}
|
|
85
|
+
return agents;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseAgents(value: string | undefined, fallback: CLITool[] = ['claude']): CLITool[] {
|
|
89
|
+
const rawAgents = value
|
|
90
|
+
? value.split(',').map((agent) => agent.trim().toLowerCase()).filter(Boolean)
|
|
91
|
+
: fallback;
|
|
92
|
+
const agents = uniqueAgents(rawAgents);
|
|
93
|
+
return agents.length > 0 ? agents : fallback;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Resolve agent skill destinations from `--agent <name[,name]>` (default: claude). */
|
|
97
|
+
export function resolveSkillsTargets(
|
|
98
|
+
options: Record<string, string | boolean>,
|
|
99
|
+
fallbackAgents: CLITool[] = ['claude'],
|
|
100
|
+
): ResolvedSkillsTarget[] {
|
|
101
|
+
const explicit = typeof options['skills-dir'] === 'string' ? String(options['skills-dir']) : undefined;
|
|
102
|
+
const agents = parseAgents(typeof options['agent'] === 'string' ? String(options['agent']) : undefined, fallbackAgents);
|
|
103
|
+
if (explicit) {
|
|
104
|
+
return [{ agent: agents.join(','), skillsDir: explicit }];
|
|
105
|
+
}
|
|
106
|
+
return agents.map((agent) => ({ agent, skillsDir: SKILLS_DIR_BY_AGENT[agent] }));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Resolve the first agent skills destination. Kept for single-target callers. */
|
|
110
|
+
export function resolveSkillsTarget(options: Record<string, string | boolean>): ResolvedSkillsTarget {
|
|
111
|
+
return resolveSkillsTargets(options)[0];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readCliToolsFile(file: string, key: 'tools' | 'cli_tools'): CLITool[] {
|
|
115
|
+
try {
|
|
116
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<string, unknown>;
|
|
117
|
+
const raw = state[key];
|
|
118
|
+
return Array.isArray(raw)
|
|
119
|
+
? uniqueAgents(raw.map((tool) => String(tool)))
|
|
120
|
+
: [];
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function readCliToolsConfig(): CLITool[] {
|
|
127
|
+
if (fs.existsSync(CLI_TOOLS_CONFIG_FILE)) {
|
|
128
|
+
return readCliToolsFile(CLI_TOOLS_CONFIG_FILE, 'tools');
|
|
129
|
+
}
|
|
130
|
+
if (fs.existsSync(LEGACY_STATE_FILE)) {
|
|
131
|
+
return readCliToolsFile(LEGACY_STATE_FILE, 'cli_tools');
|
|
132
|
+
}
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function writeCliToolsConfig(tools: CLITool[]): void {
|
|
137
|
+
fs.mkdirSync(path.dirname(CLI_TOOLS_CONFIG_FILE), { recursive: true });
|
|
138
|
+
const config = {
|
|
139
|
+
tools: [...tools],
|
|
140
|
+
updated_at: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
fs.writeFileSync(CLI_TOOLS_CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function ensureCodumentGitignoreRules(file = '.gitignore'): number {
|
|
146
|
+
if (!fs.existsSync(file)) {
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const existing = fs.readFileSync(file, 'utf-8');
|
|
151
|
+
const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
152
|
+
const missing = CODUMENT_GITIGNORE_RULES.filter((rule) => !lines.has(rule));
|
|
153
|
+
if (missing.length === 0) {
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
158
|
+
fs.writeFileSync(file, `${existing}${prefix}${missing.join('\n')}\n`, 'utf-8');
|
|
159
|
+
return missing.length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function writeFile(dest: string, content: string): void {
|
|
163
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
164
|
+
fs.writeFileSync(dest, content.endsWith('\n') ? content : `${content}\n`, 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface InstallResult {
|
|
168
|
+
workspaceWritten: number;
|
|
169
|
+
workspaceSkipped: number;
|
|
170
|
+
skillsWritten: number;
|
|
171
|
+
skillsRemoved: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface SkillInstallResult {
|
|
175
|
+
skillsWritten: number;
|
|
176
|
+
skillsRemoved: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface InstallOptions {
|
|
180
|
+
skillsDir: string;
|
|
181
|
+
/** Overwrite the codument/std/** subtree (true for upgrade-workspace; false for init which preserves existing files). */
|
|
182
|
+
overwriteStd: boolean;
|
|
183
|
+
/** Overwrite every codument/** file regardless (init --force). */
|
|
184
|
+
force?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Copy embedded templates into the project.
|
|
189
|
+
* - skills/** → always overwritten (generated shells).
|
|
190
|
+
* - codument/std/** → overwritten when overwriteStd|force, else written only if missing.
|
|
191
|
+
* - other codument/** → written only if missing (preserves user content), unless force.
|
|
192
|
+
*/
|
|
193
|
+
export function installTemplates(opts: InstallOptions): InstallResult {
|
|
194
|
+
const result: InstallResult = {
|
|
195
|
+
workspaceWritten: 0,
|
|
196
|
+
workspaceSkipped: 0,
|
|
197
|
+
skillsWritten: 0,
|
|
198
|
+
skillsRemoved: cleanupDeprecatedSkills(opts.skillsDir),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
for (const file of TEMPLATE_FILES) {
|
|
202
|
+
if (file.path.startsWith('skills/')) {
|
|
203
|
+
const dest = path.join(opts.skillsDir, file.path.slice('skills/'.length));
|
|
204
|
+
writeFile(dest, file.content);
|
|
205
|
+
result.skillsWritten++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!file.path.startsWith('codument/')) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const dest = file.path; // relative to cwd (= workspace)
|
|
214
|
+
const isStd = file.path.startsWith('codument/std/');
|
|
215
|
+
const overwrite = opts.force || (isStd && opts.overwriteStd);
|
|
216
|
+
|
|
217
|
+
if (!overwrite && fs.existsSync(dest)) {
|
|
218
|
+
result.workspaceSkipped++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
writeFile(dest, file.content);
|
|
222
|
+
result.workspaceWritten++;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function installSkillTemplates(skillsDir: string): SkillInstallResult {
|
|
229
|
+
const result: SkillInstallResult = { skillsWritten: 0, skillsRemoved: cleanupDeprecatedSkills(skillsDir) };
|
|
230
|
+
for (const file of TEMPLATE_FILES) {
|
|
231
|
+
if (!file.path.startsWith('skills/')) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const dest = path.join(skillsDir, file.path.slice('skills/'.length));
|
|
235
|
+
writeFile(dest, file.content);
|
|
236
|
+
result.skillsWritten++;
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function cleanupDeprecatedSkills(skillsDir: string): number {
|
|
242
|
+
let removed = 0;
|
|
243
|
+
for (const skill of DEPRECATED_SKILLS) {
|
|
244
|
+
const dir = path.join(skillsDir, skill);
|
|
245
|
+
if (!fs.existsSync(dir)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
249
|
+
removed++;
|
|
250
|
+
}
|
|
251
|
+
return removed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Inject / refresh the codument managed pointer block in the project-root AGENTS.md.
|
|
256
|
+
* Matches the managed block case-insensitively and across legacy marker words
|
|
257
|
+
* (codument:begin/end and CODUMENT:START/END), replacing the first occurrence and
|
|
258
|
+
* removing any duplicates so re-running never stacks blocks.
|
|
259
|
+
*/
|
|
260
|
+
const MANAGED_BLOCK_RE = /<!--\s*codument:(?:begin|start)\s*-->[\s\S]*?<!--\s*codument:end\s*-->/gi;
|
|
261
|
+
|
|
262
|
+
export function injectAgentsBlock(): void {
|
|
263
|
+
const block = `${AGENTS_BEGIN}\n\n${AGENTS_MANAGED_BODY}\n\n${AGENTS_END}`;
|
|
264
|
+
const file = 'AGENTS.md';
|
|
265
|
+
|
|
266
|
+
if (!fs.existsSync(file)) {
|
|
267
|
+
fs.writeFileSync(file, `${block}\n`, 'utf-8');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const existing = fs.readFileSync(file, 'utf-8');
|
|
272
|
+
|
|
273
|
+
if (MANAGED_BLOCK_RE.test(existing)) {
|
|
274
|
+
MANAGED_BLOCK_RE.lastIndex = 0;
|
|
275
|
+
let replaced = false;
|
|
276
|
+
const updated = existing
|
|
277
|
+
.replace(MANAGED_BLOCK_RE, () => {
|
|
278
|
+
if (replaced) return ''; // drop duplicate managed blocks
|
|
279
|
+
replaced = true;
|
|
280
|
+
return block;
|
|
281
|
+
})
|
|
282
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
283
|
+
.replace(/^\n+/, '');
|
|
284
|
+
fs.writeFileSync(file, updated, 'utf-8');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// No (recognizable) existing block — prepend.
|
|
289
|
+
const updated = existing.trim().length > 0 ? `${block}\n\n${existing}` : `${block}\n`;
|
|
290
|
+
fs.writeFileSync(file, updated, 'utf-8');
|
|
291
|
+
}
|