spec-canon 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -66,14 +66,34 @@ npx spec-canon --help
66
66
  → 阅读 [guide/04_new_project_sop.md](docs/guide/04_new_project_sop.md),然后使用 `spec-canon prompt guide` 查看决策树
67
67
 
68
68
  ### 我要在已有项目中开发新需求
69
- 使用 `spec-canon prompt list --stage daily` 查看日常提示词,按 Step 0 → Step 5 顺序执行
69
+ 先运行 `spec-canon change start --goal ...` 建立 active change,再按 Step 0 → Step 5 执行
70
70
 
71
71
  ### 我要为已有项目引入 SDD
72
72
  → 阅读 [guide/05_iterative_project_sop.md](docs/guide/05_iterative_project_sop.md) 的冷启动策略,然后使用 `spec-canon prompt list --stage iterative`
73
73
 
74
+ ### 我要给已初始化项目补齐新版本骨架
75
+ → 运行 `spec-canon sync`,只补缺失文件,不覆盖已有内容
76
+
77
+ ## 常用命令
78
+
79
+ ```bash
80
+ spec-canon init
81
+ spec-canon init --domain auth
82
+ spec-canon sync # 给已初始化项目安全补齐新骨架
83
+ spec-canon change start -g @docs/prd.md
84
+ spec-canon change status
85
+ spec-canon change clear
86
+ spec-canon prompt list
87
+ spec-canon prompt show <id>
88
+ spec-canon prompt guide
89
+ ```
90
+
91
+ `sync` 会在项目内写入 `spec-canon/.template-manifest.json`,用于记录当前骨架状态,为后续更智能的升级能力预留基础。
92
+
74
93
  ## 文档结构
75
94
 
76
95
  ```
96
+ ROADMAP.md ← CLI 演进计划(单一来源)
77
97
  docs/
78
98
  ├── SOP.md ← 导航索引(入口)
79
99
  ├── guide/ ← 方法论(一次性阅读)
@@ -89,6 +109,10 @@ docs/
89
109
  └── templates/ ← Spec 模板(7 个)
90
110
  ```
91
111
 
112
+ ## ROADMAP
113
+
114
+ 见 [ROADMAP.md](ROADMAP.md)。当前版本已支持安全补齐,后续版本会在 manifest 基础上逐步增加“只刷新未被修改的文件”和 `--check` 等能力。
115
+
92
116
  ## 为什么叫 SpecCanon
93
117
 
94
118
  **Canon**(正典)——在 SpecCanon 体系中,`domain_spec.md` 是系统行为的权威来源。每次变更开发完成后,关键信息回填到 Domain Spec,使其成为持续演化的系统正典。
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerChangeCommand(program: Command): void;
@@ -0,0 +1,160 @@
1
+ import { join } from 'node:path';
2
+ import { ActiveChangeError, clearActiveChange, ensureSddRoot, getActiveChangeStatus, getChangeDir, readActiveChange, writeActiveChange, } from '../utils/active-change.js';
3
+ import { getChangeSeq, getNextChangeSeq, isValidChangeName } from '../utils/change.js';
4
+ import { ensureDir } from '../utils/fs.js';
5
+ import { logger } from '../utils/logger.js';
6
+ export function registerChangeCommand(program) {
7
+ const change = program
8
+ .command('change')
9
+ .description('管理当前 active change');
10
+ change
11
+ .command('start')
12
+ .description('启动或确认当前 change')
13
+ .option('-c, --change <name>', '已确认的变更目录名')
14
+ .option('-g, --goal <content>', '变更目标 / 问题描述 / PRD 引用')
15
+ .action(async (opts) => {
16
+ await runStart(opts);
17
+ });
18
+ change
19
+ .command('status')
20
+ .description('查看当前 active change 状态')
21
+ .action(async () => {
22
+ await runStatus();
23
+ });
24
+ change
25
+ .command('clear')
26
+ .description('清除当前 active change 状态')
27
+ .action(async () => {
28
+ await runClear();
29
+ });
30
+ }
31
+ async function runStart(opts) {
32
+ try {
33
+ await ensureSddRoot();
34
+ }
35
+ catch (error) {
36
+ handleActiveChangeError(error);
37
+ return;
38
+ }
39
+ if (!opts.goal) {
40
+ logger.error('change start 需要指定 -g, --goal');
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ if (opts.change && !isValidChangeName(opts.change)) {
45
+ logger.error('变更目录名必须符合 <type>-<seq>-<slug> 规则');
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const existing = await safeReadActiveChange();
50
+ if (existing instanceof Error)
51
+ return;
52
+ if (existing) {
53
+ const canConfirmPendingChange = !existing.change &&
54
+ !!opts.change &&
55
+ existing.goal === opts.goal;
56
+ const isIdempotent = existing.goal === opts.goal &&
57
+ existing.change === opts.change;
58
+ if (canConfirmPendingChange) {
59
+ await ensureDir(getChangeDir(opts.change, process.cwd()));
60
+ await writeActiveChange({
61
+ ...existing,
62
+ change: opts.change,
63
+ });
64
+ logger.success(`已确认 active change:${opts.change}`);
65
+ return;
66
+ }
67
+ if (isIdempotent) {
68
+ logger.info('当前 active change 已是目标状态,无需重复启动');
69
+ return;
70
+ }
71
+ logger.error('已有 active change,请先运行 spec-canon change clear');
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ const changeSeq = opts.change
76
+ ? getChangeSeq(opts.change)
77
+ : await getNextChangeSeq();
78
+ const changesDir = join(process.cwd(), 'spec-canon', 'changes');
79
+ await ensureDir(changesDir);
80
+ if (opts.change) {
81
+ await ensureDir(getChangeDir(opts.change, process.cwd()));
82
+ }
83
+ await writeActiveChange({
84
+ goal: opts.goal,
85
+ change: opts.change,
86
+ changeSeq,
87
+ createdAt: new Date().toISOString(),
88
+ });
89
+ logger.success('已写入 active change');
90
+ if (opts.change) {
91
+ logger.info(`change: ${opts.change}`);
92
+ }
93
+ else {
94
+ logger.info(`change 序号: ${changeSeq}`);
95
+ logger.info('下一步:运行 spec-canon prompt show ctx,确认建议的 change 名后,可再次执行 change start -g <goal> -c <change>');
96
+ }
97
+ }
98
+ async function runStatus() {
99
+ try {
100
+ await ensureSddRoot();
101
+ const active = await readActiveChange();
102
+ if (!active) {
103
+ logger.info('当前没有 active change');
104
+ return;
105
+ }
106
+ const status = await getActiveChangeStatus(active);
107
+ console.log(`change: ${active.change ?? '(待命名)'}`);
108
+ console.log(`goal: ${active.goal}`);
109
+ console.log(`seq: ${active.changeSeq}`);
110
+ console.log(`createdAt: ${active.createdAt}`);
111
+ logger.blank();
112
+ console.log('Spec 进度:');
113
+ console.log(` 00_context: ${mark(status.progress.context)}`);
114
+ console.log(` 01_requirement: ${mark(status.progress.requirement)}`);
115
+ console.log(` 02_interface: ${mark(status.progress.interface)}`);
116
+ console.log(` 03_implementation: ${mark(status.progress.implementation)}`);
117
+ console.log(` 04_test_spec: ${mark(status.progress.testSpec)}`);
118
+ console.log(` AI_CHANGELOG: ${mark(status.progress.changelogUpdated)}`);
119
+ console.log(` domain-sync: ${mark(status.progress.domainSynced)}`);
120
+ logger.blank();
121
+ console.log(`推荐下一步: ${status.nextStep}`);
122
+ }
123
+ catch (error) {
124
+ handleActiveChangeError(error);
125
+ }
126
+ }
127
+ async function runClear() {
128
+ try {
129
+ await ensureSddRoot();
130
+ const cleared = await clearActiveChange();
131
+ if (!cleared) {
132
+ logger.info('当前没有 active change,无需清除');
133
+ return;
134
+ }
135
+ logger.success('已清除 active change');
136
+ }
137
+ catch (error) {
138
+ handleActiveChangeError(error);
139
+ }
140
+ }
141
+ async function safeReadActiveChange() {
142
+ try {
143
+ return await readActiveChange();
144
+ }
145
+ catch (error) {
146
+ handleActiveChangeError(error);
147
+ return error;
148
+ }
149
+ }
150
+ function handleActiveChangeError(error) {
151
+ if (error instanceof ActiveChangeError) {
152
+ logger.error(error.message);
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+ throw error;
157
+ }
158
+ function mark(value) {
159
+ return value ? '✅' : '—';
160
+ }
@@ -1,8 +1,8 @@
1
1
  import { join, resolve } from 'node:path';
2
- import { loadTemplate, SPEC_TEMPLATES } from '../templates/index.js';
3
- import { ensureDir, fileExists, writeFileSafe, } from '../utils/fs.js';
2
+ import { loadTemplate } from '../templates/index.js';
3
+ import { fileExists, } from '../utils/fs.js';
4
4
  import { logger } from '../utils/logger.js';
5
- const SDD_ROOT = 'spec-canon';
5
+ import { SDD_ROOT, TEMPLATE_MANIFEST, buildManagedFiles, ensureBaseScaffoldDirs, reconcileManagedFile, writeTemplateManifest, } from '../utils/scaffold.js';
6
6
  export function registerInitCommand(program) {
7
7
  program
8
8
  .command('init')
@@ -27,42 +27,25 @@ async function runInit(opts) {
27
27
  logger.info(`初始化 SDD 文档结构到 ${targetDir}`);
28
28
  logger.blank();
29
29
  const created = [];
30
- // 1. 创建目录树
31
- const dirs = [
32
- join(sddDir, 'domains'),
33
- join(sddDir, 'changes'),
34
- join(sddDir, 'decisions'),
35
- join(sddDir, 'skills'),
36
- join(sddDir, 'templates'),
37
- ];
38
- for (const dir of dirs) {
39
- await ensureDir(dir);
40
- }
41
- // 2. 写入 .gitkeep
42
- await writeFileSafe(join(sddDir, 'changes', '.gitkeep'), '');
43
- created.push(`${SDD_ROOT}/changes/.gitkeep`);
44
- // 3. 写入 7 个 Spec 模板
45
- for (const name of SPEC_TEMPLATES) {
46
- const content = await loadTemplate(name);
47
- await writeFileSafe(join(sddDir, 'templates', name), content);
48
- created.push(`${SDD_ROOT}/templates/${name}`);
30
+ const statuses = [];
31
+ await ensureBaseScaffoldDirs(targetDir);
32
+ const managedFiles = await buildManagedFiles(targetDir, {
33
+ includeClaude: true,
34
+ extraDomains: opts.domain ? [opts.domain] : [],
35
+ });
36
+ for (const file of managedFiles) {
37
+ const overwriteExisting = file.relativePath !== 'CLAUDE.md';
38
+ const status = await reconcileManagedFile(targetDir, file, {
39
+ overwriteExisting,
40
+ });
41
+ statuses.push(status);
42
+ if (status.state === 'created') {
43
+ created.push(file.relativePath);
44
+ }
49
45
  }
50
- // 4. 写入 AI_CHANGELOG.md
51
- const changelogContent = await loadTemplate('AI_CHANGELOG.md');
52
- await writeFileSafe(join(sddDir, 'decisions', 'AI_CHANGELOG.md'), changelogContent);
53
- created.push(`${SDD_ROOT}/decisions/AI_CHANGELOG.md`);
54
- // 5. 写入 SKILL.md
55
- const skillContent = await loadTemplate('skill.md');
56
- await writeFileSafe(join(sddDir, 'skills', 'SKILL.md'), skillContent);
57
- created.push(`${SDD_ROOT}/skills/SKILL.md`);
58
- // 6. 写入 domains/README.md
59
- const domainsReadme = await loadTemplate('domains-readme.md');
60
- await writeFileSafe(join(sddDir, 'domains', 'README.md'), domainsReadme);
61
- created.push(`${SDD_ROOT}/domains/README.md`);
62
- // 7. 处理 CLAUDE.md
63
- const claudeMdPath = join(targetDir, 'CLAUDE.md');
64
46
  const sddSection = await loadTemplate('claude-md-section.md');
65
- if (await fileExists(claudeMdPath)) {
47
+ const claudeStatus = statuses.find((status) => status.relativePath === 'CLAUDE.md');
48
+ if (claudeStatus?.state !== 'created') {
66
49
  logger.warn('CLAUDE.md 已存在,请手动将以下 SDD 协议段添加到文件中:');
67
50
  logger.blank();
68
51
  logger.dim('─'.repeat(60));
@@ -71,21 +54,20 @@ async function runInit(opts) {
71
54
  logger.blank();
72
55
  }
73
56
  else {
74
- const claudeMdContent = `# 项目:[项目名称]\n[一句话描述项目做什么]\n\n## 技术栈\n- [语言/框架/数据库/中间件]\n\n${sddSection}`;
75
- await writeFileSafe(claudeMdPath, claudeMdContent);
76
- created.push('CLAUDE.md');
77
57
  logger.success('已创建 CLAUDE.md(含 SDD 协议段)');
78
58
  }
79
- // 8. 处理 --domain
80
59
  if (opts.domain) {
81
- const domainDir = join(sddDir, 'domains', opts.domain);
82
- await ensureDir(domainDir);
83
- const domainSpecContent = await loadTemplate('domain_spec.md');
84
- await writeFileSafe(join(domainDir, 'domain_spec.md'), domainSpecContent);
85
- created.push(`${SDD_ROOT}/domains/${opts.domain}/domain_spec.md`);
86
60
  logger.success(`已创建业务域: ${opts.domain}`);
87
61
  }
88
- // 9. 输出摘要
62
+ const manifestAbsolutePath = join(targetDir, SDD_ROOT, TEMPLATE_MANIFEST);
63
+ const manifestExisted = await fileExists(manifestAbsolutePath);
64
+ const manifestPath = await writeTemplateManifest(targetDir, await buildManagedFiles(targetDir, {
65
+ includeClaude: true,
66
+ includeExistingDomains: true,
67
+ }), statuses);
68
+ if (!manifestExisted) {
69
+ created.push(manifestPath);
70
+ }
89
71
  logger.blank();
90
72
  logger.success(`SDD 初始化完成!共创建 ${created.length} 个文件:`);
91
73
  logger.blank();
@@ -1,7 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { ActiveChangeError, readActiveChange } from '../utils/active-change.js';
3
4
  import { findPrompt, getPromptsDir, listPrompts, loadPromptContent, } from '../prompts/catalog.js';
4
5
  import { renderPrompt } from '../prompts/renderer.js';
6
+ import { getNextChangeSeq } from '../utils/change.js';
5
7
  import { logger } from '../utils/logger.js';
6
8
  const STAGE_LABELS = {
7
9
  daily: '日常开发 (daily)',
@@ -25,8 +27,7 @@ export function registerPromptCommand(program) {
25
27
  .description('输出指定提示词')
26
28
  .option('-c, --change <name>', '变更目录名(替换 {{CHANGE}})')
27
29
  .option('-d, --domain <name>', '业务域名称(替换 {{DOMAIN}})')
28
- .option('-m, --module <path>', '目标模块路径(替换 {{MODULE}})')
29
- .option('-p, --prd <content>', 'PRD 内容/文件引用(替换 {{PRD}})')
30
+ .option('-g, --goal <content>', '变更目标/问题描述/PRD 引用(替换 {{GOAL}})')
30
31
  .option('--raw', '输出原始模板(不替换变量)', false)
31
32
  .action(async (id, opts) => {
32
33
  await runShow(id, opts);
@@ -105,30 +106,18 @@ async function runShow(id, opts) {
105
106
  process.stdout.write(template);
106
107
  return;
107
108
  }
108
- const variables = {};
109
- if (opts.change)
110
- variables['CHANGE'] = opts.change;
111
- if (opts.domain)
112
- variables['DOMAIN'] = opts.domain;
113
- if (opts.module)
114
- variables['MODULE'] = opts.module;
115
- if (opts.prd)
116
- variables['PRD'] = opts.prd;
109
+ const variables = await resolveVariables(entry.id, opts);
110
+ if (!variables)
111
+ return;
117
112
  const { text, unreplaced } = renderPrompt(template, variables);
118
113
  // 提示词 → stdout
119
114
  process.stdout.write(text);
120
- // ctx 提示词缺少 -d/-m 时的专属提示 → stderr
121
- if (entry.id === 'ctx' && !opts.domain && !opts.module) {
122
- console.error();
123
- console.error('⚠ ctx 提示词需要指定 -d(从 domain_spec)或 -m(从代码库)');
124
- }
125
115
  // 未替换变量警告 → stderr
126
116
  if (unreplaced.length > 0) {
127
117
  const varOpts = {
128
118
  CHANGE: '-c, --change',
129
119
  DOMAIN: '-d, --domain',
130
- MODULE: '-m, --module',
131
- PRD: '-p, --prd',
120
+ GOAL: '-g, --goal',
132
121
  };
133
122
  console.error();
134
123
  console.error("--------------------------------");
@@ -143,3 +132,62 @@ async function runGuide() {
143
132
  const content = await readFile(guidePath, 'utf-8');
144
133
  process.stdout.write(content);
145
134
  }
135
+ function requiresGoal(id) {
136
+ return id === 'ctx' || id === 'req';
137
+ }
138
+ function requiresChange(id) {
139
+ return [
140
+ 'req',
141
+ 'iface',
142
+ 'impl-spec',
143
+ 'test-spec',
144
+ 'impl',
145
+ 'review',
146
+ 'domain-sync',
147
+ ].includes(id);
148
+ }
149
+ function canUseActiveChange(id) {
150
+ return id === 'ctx' || requiresChange(id);
151
+ }
152
+ async function resolveVariables(id, opts) {
153
+ let active = null;
154
+ const shouldReadActive = (!opts.goal && requiresGoal(id)) ||
155
+ (!opts.change && canUseActiveChange(id));
156
+ if (shouldReadActive) {
157
+ try {
158
+ active = await readActiveChange();
159
+ }
160
+ catch (error) {
161
+ if (error instanceof ActiveChangeError) {
162
+ logger.error(error.message);
163
+ process.exitCode = 1;
164
+ return null;
165
+ }
166
+ throw error;
167
+ }
168
+ }
169
+ const goal = opts.goal ?? active?.goal;
170
+ const change = opts.change ?? active?.change;
171
+ if (requiresGoal(id) && !goal) {
172
+ logger.error(`提示词 "${id}" 需要指定 -g, --goal,或先运行 spec-canon change start`);
173
+ process.exitCode = 1;
174
+ return null;
175
+ }
176
+ if (requiresChange(id) && !change) {
177
+ logger.error(`提示词 "${id}" 需要指定 -c, --change,或先运行 spec-canon change start`);
178
+ process.exitCode = 1;
179
+ return null;
180
+ }
181
+ const variables = {};
182
+ if (goal)
183
+ variables['GOAL'] = goal;
184
+ if (change)
185
+ variables['CHANGE'] = change;
186
+ if (opts.domain)
187
+ variables['DOMAIN'] = opts.domain;
188
+ if (id === 'ctx') {
189
+ variables['CHANGE_SEQ'] =
190
+ active?.changeSeq ?? await getNextChangeSeq();
191
+ }
192
+ return variables;
193
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSyncCommand(program: Command): void;
@@ -0,0 +1,51 @@
1
+ import { resolve } from 'node:path';
2
+ import { ScaffoldSyncError, syncScaffold, } from '../utils/scaffold.js';
3
+ import { logger } from '../utils/logger.js';
4
+ export function registerSyncCommand(program) {
5
+ program
6
+ .command('sync')
7
+ .description('安全补齐已初始化项目的 SDD 文档结构')
8
+ .option('-d, --dir <path>', '目标项目目录', process.cwd())
9
+ .action(async (opts) => {
10
+ await runSync(opts);
11
+ });
12
+ }
13
+ async function runSync(opts) {
14
+ const targetDir = resolve(opts.dir);
15
+ logger.info(`同步 SDD 文档结构到 ${targetDir}`);
16
+ logger.blank();
17
+ try {
18
+ const report = await syncScaffold(targetDir);
19
+ logger.success('SDD 同步完成');
20
+ logger.blank();
21
+ console.log(`created: ${report.created.length}`);
22
+ console.log(`up_to_date: ${report.upToDate.length}`);
23
+ console.log(`preserved: ${report.preserved.length}`);
24
+ console.log(`manual_review: ${report.manualReview.length}`);
25
+ logger.blank();
26
+ if (report.created.length > 0) {
27
+ logger.success('新增文件:');
28
+ for (const file of report.created) {
29
+ logger.dim(` ${file}`);
30
+ }
31
+ logger.blank();
32
+ }
33
+ if (report.manualReview.length > 0) {
34
+ logger.warn('需要人工比对:');
35
+ for (const item of report.manualReview) {
36
+ logger.dim(` ${item.path}`);
37
+ logger.dim(` ${item.reason}`);
38
+ }
39
+ logger.blank();
40
+ }
41
+ logger.info(`已更新模板状态清单:${report.manifestPath}`);
42
+ }
43
+ catch (error) {
44
+ if (error instanceof ScaffoldSyncError) {
45
+ logger.error(error.message);
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ throw error;
50
+ }
51
+ }
package/dist/index.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import { Command } from 'commander';
2
+ import { registerChangeCommand } from './commands/change.js';
2
3
  import { registerInitCommand } from './commands/init.js';
3
4
  import { registerPromptCommand } from './commands/prompt.js';
5
+ import { registerSyncCommand } from './commands/sync.js';
6
+ import { CLI_VERSION } from './version.js';
4
7
  const program = new Command();
5
8
  program
6
9
  .name('spec-canon')
7
10
  .description('CLI toolkit for Spec-Driven Development (SDD)')
8
- .version('0.1.2');
11
+ .version(CLI_VERSION);
9
12
  registerInitCommand(program);
13
+ registerChangeCommand(program);
10
14
  registerPromptCommand(program);
15
+ registerSyncCommand(program);
11
16
  program.parse();
@@ -1,10 +1,10 @@
1
- const IF_BLOCK = /\{\{#if\s+([A-Z_]+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
1
+ const IF_BLOCK = /\{\{#if\s+([A-Z_]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g;
2
2
  const VAR_PATTERN = /\{\{([A-Z_]+)\}\}/g;
3
3
  export function renderPrompt(template, variables) {
4
- // 1. 处理条件块:保留已提供变量的块内容,移除未提供的
4
+ // 1. 处理条件块:保留已提供变量的块内容,否则使用 else 分支(如有)
5
5
  const afterBlocks = template
6
- .replace(IF_BLOCK, (_match, name, body) => {
7
- return name in variables ? body : '';
6
+ .replace(IF_BLOCK, (_match, name, body, elseBody) => {
7
+ return name in variables ? body : (elseBody ?? '');
8
8
  })
9
9
  .replace(/\n{3,}/g, '\n\n') // 压缩连续空行
10
10
  .trim();
@@ -0,0 +1,30 @@
1
+ export interface ActiveChange {
2
+ goal: string;
3
+ change?: string;
4
+ changeSeq: string;
5
+ createdAt: string;
6
+ }
7
+ export interface ActiveChangeProgress {
8
+ context: boolean;
9
+ requirement: boolean;
10
+ interface: boolean;
11
+ implementation: boolean;
12
+ testSpec: boolean;
13
+ changelogUpdated: boolean;
14
+ domainSynced: boolean;
15
+ }
16
+ export interface ActiveChangeStatus {
17
+ active: ActiveChange;
18
+ progress: ActiveChangeProgress;
19
+ nextStep: 'ctx' | 'req' | 'iface' | 'impl-spec' | 'test-spec' | 'impl / review' | 'domain-sync' | 'change clear';
20
+ }
21
+ export declare class ActiveChangeError extends Error {
22
+ }
23
+ export declare function getSddRoot(rootDir?: string): string;
24
+ export declare function getActiveChangePath(rootDir?: string): string;
25
+ export declare function getChangeDir(change: string, rootDir?: string): string;
26
+ export declare function ensureSddRoot(rootDir?: string): Promise<void>;
27
+ export declare function readActiveChange(rootDir?: string): Promise<ActiveChange | null>;
28
+ export declare function writeActiveChange(activeChange: ActiveChange, rootDir?: string): Promise<void>;
29
+ export declare function clearActiveChange(rootDir?: string): Promise<boolean>;
30
+ export declare function getActiveChangeStatus(active: ActiveChange, rootDir?: string): Promise<ActiveChangeStatus>;