@shirayner/ace 0.1.1-snapshot.6 → 0.1.2

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.en-US.md CHANGED
@@ -16,15 +16,20 @@
16
16
 
17
17
  ```
18
18
  $ ace init
19
- ? Your role: Fullstack Developer
20
- ? Preset: full
21
- core installed
22
- ✔ rules installed
23
- plugin installed (ace:auto-goal, ace:coding, ...)
24
- hookify installed
25
- hooks installed
26
- memory installed
27
- Done! Your AI coding environment is ready.
19
+ ◇ ace v0.1.2
20
+
21
+ ◇ Installed to ~/.claude/
22
+
23
+ │ ◆ Core Config 2 files
24
+ │ ◆ Rules 8 files
25
+ │ ◆ Plugin installed
26
+ │ ◆ Hooks 1 file
27
+ │ ◆ Safety Guards 7 files
28
+ │ ◆ Memory 2 files
29
+
30
+ ◆ 20 installed
31
+
32
+ └ Done. Go to your project and run ace spec init.
28
33
  ```
29
34
 
30
35
  ### Spec 驱动开发
package/README.md CHANGED
@@ -45,15 +45,31 @@ ace init
45
45
 
46
46
  ```bash
47
47
  $ ace init
48
- ? 选择你的角色: Fullstack Developer
49
- ? 选择安装预设: full (完整功能)
50
- Core 核心配置已安装
51
- ✓ 8 条认知规则已部署
52
- 4 AI Skills 已激活 (ace:auto-goal, ace:coding, ...)
53
- Hookify 安全守卫已启用
54
- 角色钩子脚本已配置
55
- 记忆系统已初始化
56
- Done! 你的 AI 开发环境已就绪。
48
+ ◇ ace v0.1.2
49
+
50
+ ◇ Installed to ~/.claude/
51
+
52
+ │ ◆ Core Config 2 files
53
+ │ ◆ Rules 8 files
54
+ │ ◆ Plugin installed
55
+ │ ◆ Hooks 1 file
56
+ │ ◆ Safety Guards 7 files
57
+ │ ◆ Memory 2 files
58
+
59
+ ◆ 20 installed
60
+
61
+ ┌ Next steps
62
+ │ Get started
63
+ │ 1. cd <your-project> && ace spec init
64
+ │ 2. Open Claude Code, type: /opsx:propose
65
+
66
+ │ Customize
67
+ │ Change role edit ~/.claude/memory/user_profile.md
68
+ │ Adjust rules edit ~/.claude/rules/ace/
69
+ │ Safety guards edit ~/.claude/hookify.ace.*.local.md
70
+ │ Verify setup ace doctor
71
+
72
+ └ Done. Go to your project and run ace spec init.
57
73
  ```
58
74
 
59
75
  ### Spec Coding 完整流程
@@ -146,30 +162,6 @@ All systems operational.
146
162
 
147
163
  ---
148
164
 
149
- ## 📦 安装预设
150
-
151
- | 组件 | `full` | `safe` | `minimal` |
152
- | ------------------------------------------ | :------: | :------: | :---------: |
153
- | **Core** (CLAUDE.md + settings.json) | ✅ | ✅ | ✅ |
154
- | **Rules** (8 条认知与代码质量规则) | ✅ | ✅ | ✅ |
155
- | **Plugin** (4 个 Skills) | ✅ | ✅ | ✅ |
156
- | **Hooks** (角色相关脚本) | ✅ | ❌ | ❌ |
157
- | **Hookify** (3 个安全守卫) | ✅ | ✅ | ❌ |
158
- | **Memory** (模板 + 开发者画像) | ✅ | ✅ | ❌ |
159
-
160
- ```bash
161
- # 完整功能(推荐)
162
- ace init --preset full
163
-
164
- # 安全优先(适合团队协作)
165
- ace init --preset safe
166
-
167
- # 最小安装(仅核心功能)
168
- ace init --preset minimal
169
- ```
170
-
171
- ---
172
-
173
165
  ## 🎓 核心设计理念
174
166
 
175
167
  ACE 的设计融合了多学科的深层洞察:
@@ -283,7 +275,8 @@ claude
283
275
 
284
276
  ACE 遵循**零侵入**原则:
285
277
 
286
- - **智能合并** — 与现有配置共存,从不覆盖
278
+ - **智能合并** — CLAUDE.md 使用标记区块替换,settings.json 深度合并,用户配置始终保留
279
+ - **ACE 文件自动覆盖** — rules/ace/*、hooks/* 等 ACE 自有文件升级时自动更新,无需用户决策
287
280
  - **自动备份** — 首次安装前创建完整快照
288
281
  - **干净卸载** — `ace uninstall` 一键恢复原始状态
289
282
  - **命名空间隔离** — 所有文件使用 `ace/` 前缀,避免冲突
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shirayner/ace",
3
- "version": "0.1.1-snapshot.6",
3
+ "version": "0.1.2",
4
4
  "description": "AI Coding Environment - One command to set up your Claude Code harness",
5
5
  "bin": {
6
6
  "ace": "./bin/ace.js"
@@ -2,7 +2,7 @@ import * as p from '@clack/prompts';
2
2
  import path from 'path';
3
3
  import fs from 'fs-extra';
4
4
  import { createRequire } from 'module';
5
- import { PRESETS, COMPONENTS, CLAUDE_DIR, TEMPLATES_DIR } from '../core/constants.js';
5
+ import { PRESETS, COMPONENTS, CLAUDE_DIR, TEMPLATES_DIR, isAceOwnedFile } from '../core/constants.js';
6
6
  import { Installer } from '../core/installer.js';
7
7
  import { mergeClaudeMd } from '../core/merger.js';
8
8
 
@@ -196,8 +196,10 @@ async function buildInstallPreview(installer, components) {
196
196
  if (await fs.pathExists(srcDir)) {
197
197
  const files = (await fs.readdir(srcDir)).filter(f => f.endsWith('.md'));
198
198
  for (const f of files) {
199
- if (await fs.pathExists(path.join(destDir, f))) {
200
- preview.conflict.push(path.join(component.rulesDir, f).replace(/\\/g, '/'));
199
+ const relativePath = path.join(component.rulesDir, f).replace(/\\/g, '/');
200
+ // ACE-owned files are overwritten directly, not shown as conflicts
201
+ if (await fs.pathExists(path.join(destDir, f)) && !isAceOwnedFile(relativePath)) {
202
+ preview.conflict.push(relativePath);
201
203
  }
202
204
  }
203
205
  }
@@ -207,7 +209,8 @@ async function buildInstallPreview(installer, components) {
207
209
  for (const file of component.conditional) {
208
210
  if (file.roles?.includes(installer.role)) {
209
211
  const destPath = path.join(installer.targetDir, file.dest);
210
- if (await fs.pathExists(destPath)) {
212
+ // ACE-owned files are overwritten directly, not shown as conflicts
213
+ if (await fs.pathExists(destPath) && !isAceOwnedFile(file.dest)) {
211
214
  preview.conflict.push(file.dest);
212
215
  }
213
216
  }
@@ -67,6 +67,36 @@ export const SPEC_TEMPLATE_FILES = [
67
67
  'retrospective-template.md',
68
68
  ];
69
69
 
70
+ /**
71
+ * Patterns for files owned by ACE - these are overwritten directly on init without prompting.
72
+ * Used to identify ACE-owned content in rules/, hooks/, and hookify/.
73
+ */
74
+ export const ACE_OWNED_PATTERNS = [
75
+ /^rules\/ace\//, // rules/ace/*.md
76
+ /^hooks\/ace\./, // hooks/ace.*.sh
77
+ /^hookify\.ace\./, // hookify.ace.*.local.md
78
+ ];
79
+
80
+ /**
81
+ * Check if a file path (relative to ~/.claude/) is owned by ACE.
82
+ * @param {string} relativePath - Path like 'rules/ace/thinking.md' or 'hooks/ace.java-compile-check.sh'
83
+ * @returns {boolean}
84
+ */
85
+ export function isAceOwnedFile(relativePath) {
86
+ return ACE_OWNED_PATTERNS.some(pattern => pattern.test(relativePath));
87
+ }
88
+
89
+ /**
90
+ * Check if an @reference path is owned by ACE.
91
+ * @param {string} refPath - Reference path like '~/.claude/rules/ace/thinking.md' or '~/.claude/hooks/ace.java-compile-check.sh'
92
+ * @returns {boolean}
93
+ */
94
+ export function isAceOwnedRef(refPath) {
95
+ // Remove the ~/.claude/ prefix if present
96
+ const relativePath = refPath.replace(/^~\/\.claude\//, '');
97
+ return isAceOwnedFile(relativePath);
98
+ }
99
+
70
100
  export const COMPONENTS = {
71
101
  core: {
72
102
  description: 'Core config (CLAUDE.md + settings.json)',
@@ -97,13 +127,13 @@ export const COMPONENTS = {
97
127
  description: 'Safety guard rules (block dangerous ops, protect secrets, safe git, code quality, require verification)',
98
128
  required: false,
99
129
  files: [
100
- { src: 'hookify/ace.hookify.block-dangerous-ops.local.md', dest: 'hooks/ace.hookify.block-dangerous-ops.local.md' },
101
- { src: 'hookify/ace.hookify.protect-secrets.local.md', dest: 'hooks/ace.hookify.protect-secrets.local.md' },
102
- { src: 'hookify/ace.hookify.safe-git-commands.local.md', dest: 'hooks/ace.hookify.safe-git-commands.local.md' },
103
- { src: 'hookify/ace.hookify.code-quality-gate.local.md', dest: 'hooks/ace.hookify.code-quality-gate.local.md' },
104
- { src: 'hookify/ace.hookify.require-verification.local.md', dest: 'hooks/ace.hookify.require-verification.local.md' },
105
- { src: 'hookify/hookify.dangerous-commands.local.md', dest: 'hooks/hookify.dangerous-commands.local.md' },
106
- { src: 'hookify/hookify.sensitive-data.local.md', dest: 'hooks/hookify.sensitive-data.local.md' },
130
+ { src: 'hookify/hookify.ace.block-dangerous-ops.local.md', dest: 'hookify.ace.block-dangerous-ops.local.md' },
131
+ { src: 'hookify/hookify.ace.protect-secrets.local.md', dest: 'hookify.ace.protect-secrets.local.md' },
132
+ { src: 'hookify/hookify.ace.safe-git-commands.local.md', dest: 'hookify.ace.safe-git-commands.local.md' },
133
+ { src: 'hookify/hookify.ace.code-quality-gate.local.md', dest: 'hookify.ace.code-quality-gate.local.md' },
134
+ { src: 'hookify/hookify.ace.require-verification.local.md', dest: 'hookify.ace.require-verification.local.md' },
135
+ { src: 'hookify/hookify.ace.dangerous-commands.local.md', dest: 'hookify.ace.dangerous-commands.local.md' },
136
+ { src: 'hookify/hookify.ace.sensitive-data.local.md', dest: 'hookify.ace.sensitive-data.local.md' },
107
137
  ],
108
138
  },
109
139
  memory: {
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
5
  import {
6
- CLAUDE_DIR, TEMPLATES_DIR, COMPONENTS,
6
+ CLAUDE_DIR, TEMPLATES_DIR, COMPONENTS, isAceOwnedFile,
7
7
  PLUGIN_SRC_DIR, PLUGIN_CACHE_DIR, INSTALLED_PLUGINS_FILE,
8
8
  KNOWN_MARKETPLACES_FILE, MARKETPLACE_DIR, MARKETPLACE_NAME,
9
9
  PLUGIN_KEY, PLUGIN_NAME,
@@ -255,22 +255,25 @@ export class Installer {
255
255
  }
256
256
 
257
257
  if (exists && !this.force) {
258
- if (fileSpec.merge === 'claude-md') {
258
+ // ACE-owned files are always overwritten (no user prompt needed)
259
+ if (isAceOwnedFile(fileSpec.dest)) {
260
+ // fall through to install (overwrite)
261
+ } else if (fileSpec.merge === 'claude-md') {
259
262
  await this.mergeClaudeMdFile(srcPath, destPath, fileSpec);
260
263
  return;
261
- }
262
- if (fileSpec.merge === 'settings-json') {
264
+ } else if (fileSpec.merge === 'settings-json') {
263
265
  await this.mergeSettingsJsonFile(srcPath, destPath, fileSpec);
264
266
  return;
265
- }
266
- // Check per-component resolution
267
- const resolution = this.resolutions[componentName];
268
- if (resolution === 'overwrite') {
269
- // fall through to install
270
267
  } else {
271
- // default: skip
272
- this.results.skipped.push(fileSpec.dest);
273
- return;
268
+ // Check per-component resolution
269
+ const resolution = this.resolutions[componentName];
270
+ if (resolution === 'overwrite') {
271
+ // fall through to install
272
+ } else {
273
+ // default: skip
274
+ this.results.skipped.push(fileSpec.dest);
275
+ return;
276
+ }
274
277
  }
275
278
  }
276
279
 
@@ -1,19 +1,146 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import deepmerge from 'deepmerge';
4
+ import { isAceOwnedRef } from './constants.js';
4
5
 
5
6
  /**
6
- * Merge CLAUDE.md: append missing @references from template into existing file.
7
- * Preserves all existing content, only adds new @references.
7
+ * Marker constants for ACE-managed sections in CLAUDE.md
8
+ */
9
+ const ACE_MANAGED_START = '<!-- ace:managed:start -->';
10
+ const ACE_MANAGED_END = '<!-- ace:managed:end -->';
11
+
12
+ /**
13
+ * Merge CLAUDE.md using marker-based section replacement.
14
+ *
15
+ * Strategy:
16
+ * 1. If both existing and template have ace:managed markers, replace the section
17
+ * between markers with template content, while preserving content outside markers.
18
+ * 2. Clean up any ACE-owned references outside the managed section (obsolete refs).
19
+ * 3. If markers are missing or incomplete, fall back to append-only strategy for
20
+ * backward compatibility.
21
+ *
22
+ * @param {string} existingContent - Current CLAUDE.md content
23
+ * @param {string} templateContent - Template CLAUDE.md content
24
+ * @returns {{content: string, added: string[], removed: string[]}} Merged content and change list
8
25
  */
9
26
  export function mergeClaudeMd(existingContent, templateContent) {
27
+ // Check if both files have complete marker sections
28
+ const existingHasMarkers = hasCompleteMarkers(existingContent);
29
+ const templateHasMarkers = hasCompleteMarkers(templateContent);
30
+
31
+ if (existingHasMarkers && templateHasMarkers) {
32
+ return mergeWithMarkers(existingContent, templateContent);
33
+ }
34
+
35
+ // Fall back to legacy append strategy
36
+ return mergeWithAppend(existingContent, templateContent);
37
+ }
38
+
39
+ /**
40
+ * Check if content has both start and end markers.
41
+ */
42
+ function hasCompleteMarkers(content) {
43
+ const hasStart = content.includes(ACE_MANAGED_START);
44
+ const hasEnd = content.includes(ACE_MANAGED_END);
45
+ return hasStart && hasEnd;
46
+ }
47
+
48
+ /**
49
+ * Merge using marker-based section replacement.
50
+ * Replaces content between ace:managed markers and cleans up obsolete ACE refs.
51
+ */
52
+ function mergeWithMarkers(existingContent, templateContent) {
53
+ // Extract the managed section from template
54
+ const templateManaged = extractManagedSection(templateContent);
55
+
56
+ // Get all ACE-owned refs from template (these are the current/active ones)
57
+ const templateRefs = extractRefs(templateManaged);
58
+
59
+ // Replace the managed section in existing content
60
+ let result = replaceManagedSection(existingContent, templateManaged);
61
+
62
+ // Clean up any obsolete ACE refs outside the managed section
63
+ const removed = [];
64
+ const lines = result.split('\n');
65
+ const cleanedLines = lines.map(line => {
66
+ const refs = extractRefs(line);
67
+ const hasObsoleteAceRef = refs.some(ref => {
68
+ // Check if this is an ACE-owned ref that's NOT in the new template
69
+ if (isAceOwnedRef(ref)) {
70
+ const refWithAt = `@${ref}`;
71
+ if (!templateRefs.includes(refWithAt)) {
72
+ removed.push(ref);
73
+ return true; // This line has an obsolete ref
74
+ }
75
+ }
76
+ return false;
77
+ });
78
+
79
+ // Return null to mark for removal, otherwise keep line
80
+ return hasObsoleteAceRef ? null : line;
81
+ }).filter(line => line !== null);
82
+
83
+ result = cleanedLines.join('\n');
84
+
85
+ // Get the new refs that were added (in the managed section)
86
+ const existingRefs = extractRefs(existingContent);
87
+ const added = templateRefs
88
+ .map(ref => ref.replace(/^@/, ''))
89
+ .filter(ref => !existingRefs.includes(ref));
90
+
91
+ return { content: result, added, removed };
92
+ }
93
+
94
+ /**
95
+ * Extract content between ace:managed markers.
96
+ */
97
+ function extractManagedSection(content) {
98
+ const startIdx = content.indexOf(ACE_MANAGED_START);
99
+ const endIdx = content.indexOf(ACE_MANAGED_END);
100
+
101
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
102
+ return content;
103
+ }
104
+
105
+ return content.slice(startIdx, endIdx + ACE_MANAGED_END.length);
106
+ }
107
+
108
+ /**
109
+ * Replace the managed section in existing content with new managed content.
110
+ */
111
+ function replaceManagedSection(existingContent, newManagedContent) {
112
+ const startIdx = existingContent.indexOf(ACE_MANAGED_START);
113
+ const endIdx = existingContent.indexOf(ACE_MANAGED_END);
114
+
115
+ if (startIdx === -1 || endIdx === -1) {
116
+ return existingContent;
117
+ }
118
+
119
+ const before = existingContent.slice(0, startIdx);
120
+ const after = existingContent.slice(endIdx + ACE_MANAGED_END.length);
121
+
122
+ // Normalize whitespace: ensure single newline separation
123
+ return before.trimEnd() + '\n' + newManagedContent + '\n' + after.trimStart();
124
+ }
125
+
126
+ /**
127
+ * Legacy merge strategy: append missing @references from template.
128
+ * Used for backward compatibility when markers are not present.
129
+ */
130
+ function mergeWithAppend(existingContent, templateContent) {
10
131
  const existingRefs = extractRefs(existingContent);
11
132
  const templateRefs = extractRefs(templateContent);
12
133
 
13
- const missingRefs = templateRefs.filter(ref => !existingRefs.includes(ref));
134
+ // Filter out ACE-owned refs from existing (we'll add current ones from template)
135
+ // This provides some cleanup even in legacy mode
136
+ const userRefs = existingRefs.filter(ref => !isAceOwnedRef(ref));
137
+ const templateRefsBare = templateRefs.map(ref => ref.replace(/^@/, ''));
138
+
139
+ // Find refs in template but not in user's refs
140
+ const missingRefs = templateRefsBare.filter(ref => !userRefs.includes(ref));
14
141
 
15
142
  if (missingRefs.length === 0) {
16
- return { content: existingContent, added: [] };
143
+ return { content: existingContent, added: [], removed: [] };
17
144
  }
18
145
 
19
146
  // Append missing references at the end with a section marker
@@ -32,15 +159,22 @@ export function mergeClaudeMd(existingContent, templateContent) {
32
159
  return {
33
160
  content: existingContent.trimEnd() + '\n' + appendSection,
34
161
  added: missingRefs,
162
+ removed: [],
35
163
  };
36
164
  }
37
165
 
166
+ /**
167
+ * Extract @reference paths from content.
168
+ */
38
169
  function extractRefs(content) {
39
170
  const refPattern = /@~?\/?\.?claude\/[^\s)]+/g;
40
171
  const matches = content.match(refPattern) || [];
41
- return matches.map(ref => ref.replace(/^@/, ''));
172
+ return matches;
42
173
  }
43
174
 
175
+ /**
176
+ * Find the full line containing a reference.
177
+ */
44
178
  function findRefLine(content, ref) {
45
179
  const lines = content.split('\n');
46
180
  return lines.find(line => line.includes(ref));
@@ -64,11 +64,6 @@ export class SpecInstaller {
64
64
  async runOpenspecInit() {
65
65
  if (this.skipOpenspec) return;
66
66
 
67
- if (await fs.pathExists(this.openspecDir) && !this.force) {
68
- this.results.skipped.push('openspec/ (already exists)');
69
- return;
70
- }
71
-
72
67
  if (this.dryRun) {
73
68
  this.results.installed.push('openspec/ (via openspec init)');
74
69
  return;
@@ -1,5 +1,6 @@
1
1
  # 全局配置索引
2
2
 
3
+ <!-- ace:managed:start -->
3
4
  ## 核心原则
4
5
  - @~/.claude/rules/ace/thinking.md - 深度思考原则(序验深广辨简)
5
6
 
@@ -15,11 +16,6 @@
15
16
  ## 质量控制
16
17
  - @~/.claude/rules/ace/memory-policy.md - Memory 质量策略
17
18
 
18
- ## Hookify 规则
19
- - @~/.claude/hooks/ace.hookify.block-dangerous-ops.local.md - 阻止危险操作
20
- - @~/.claude/hooks/ace.hookify.protect-secrets.local.md - 保护敏感信息
21
- - @~/.claude/hooks/ace.hookify.safe-git-commands.local.md - Git 安全命令
22
- - @~/.claude/hooks/ace.hookify.code-quality-gate.local.md - 代码质量门禁
23
-
24
19
  ## 交互规则
25
20
  - @~/.claude/rules/ace/interactive-clarify.md - 交互式澄清规则
21
+ <!-- ace:managed:end -->