@shirayner/ace 0.1.1-snapshot.6 → 0.1.1-snapshot.7

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/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.1-snapshot.7",
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 -->