a2acalling 0.6.55 → 0.6.57
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-INSTALL.md +2 -0
- package/bin/cli.js +56 -0
- package/package.json +1 -1
- package/scripts/cleanup.js +251 -0
- package/scripts/install-skills.js +123 -11
- package/scripts/postinstall.js +45 -5
- package/scripts/preuninstall.js +68 -0
package/CLAUDE-INSTALL.md
CHANGED
package/bin/cli.js
CHANGED
|
@@ -2711,6 +2711,62 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
2711
2711
|
console.log('Removing database... ⏭️');
|
|
2712
2712
|
}
|
|
2713
2713
|
|
|
2714
|
+
// ── Remove installed skill files using the shared cleanup module ──────
|
|
2715
|
+
//
|
|
2716
|
+
// The postinstall script writes .a2a-manifest.json listing every file it
|
|
2717
|
+
// installed. We delegate to the shared cleanupProjectFiles() function which
|
|
2718
|
+
// handles CLAUDE.md section removal, file deletion, and empty directory
|
|
2719
|
+
// cleanup. This is the same logic used by the npm preuninstall hook,
|
|
2720
|
+
// ensuring both uninstall paths behave identically.
|
|
2721
|
+
//
|
|
2722
|
+
// We check multiple candidate directories for the manifest because `a2a
|
|
2723
|
+
// uninstall` might be run from a different directory than the one where
|
|
2724
|
+
// the package was originally installed.
|
|
2725
|
+
const manifestCandidates = [
|
|
2726
|
+
process.env.INIT_CWD,
|
|
2727
|
+
process.cwd(),
|
|
2728
|
+
].filter(Boolean);
|
|
2729
|
+
// Deduplicate paths (INIT_CWD and cwd may be identical)
|
|
2730
|
+
const uniqueDirs = [...new Set(manifestCandidates.map(d => path.resolve(d)))];
|
|
2731
|
+
|
|
2732
|
+
let projectCleaned = false;
|
|
2733
|
+
for (const candidateDir of uniqueDirs) {
|
|
2734
|
+
if (fs.existsSync(path.join(candidateDir, '.a2a-manifest.json'))) {
|
|
2735
|
+
process.stdout.write('Removing installed skill files... ');
|
|
2736
|
+
try {
|
|
2737
|
+
const { cleanupProjectFiles } = require('../scripts/cleanup');
|
|
2738
|
+
const cleanResult = cleanupProjectFiles(candidateDir);
|
|
2739
|
+
const hasErrors = cleanResult.errors.length > 0;
|
|
2740
|
+
console.log(hasErrors ? '⚠️' : '✅');
|
|
2741
|
+
if (cleanResult.removed.length > 0) {
|
|
2742
|
+
for (const f of cleanResult.removed) {
|
|
2743
|
+
console.log(` - ${f}`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (cleanResult.preserved.length > 0) {
|
|
2747
|
+
for (const f of cleanResult.preserved) {
|
|
2748
|
+
console.log(` ~ ${f}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
if (hasErrors) {
|
|
2752
|
+
for (const e of cleanResult.errors) {
|
|
2753
|
+
console.error(` ! ${e}`);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
projectCleaned = true;
|
|
2757
|
+
} catch (err) {
|
|
2758
|
+
console.log('⚠️');
|
|
2759
|
+
console.error(` Cleanup error: ${err.message}`);
|
|
2760
|
+
}
|
|
2761
|
+
break; // Only clean up once — first manifest found wins
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
if (!projectCleaned) {
|
|
2765
|
+
// No manifest found in any candidate directory. This is expected when
|
|
2766
|
+
// `a2a uninstall` is run after `npm uninstall` (preuninstall already cleaned).
|
|
2767
|
+
console.log('Removing installed skill files... ⏭️ (no manifest found)');
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2714
2770
|
// Remove native macOS app if present
|
|
2715
2771
|
if (os.platform() === 'darwin') {
|
|
2716
2772
|
const appCandidates = [
|
package/package.json
CHANGED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Shared cleanup function used by both:
|
|
3
|
+
// 1. npm preuninstall hook (scripts/preuninstall.js)
|
|
4
|
+
// 2. `a2a uninstall` CLI command (bin/cli.js)
|
|
5
|
+
//
|
|
6
|
+
// Reads .a2a-manifest.json to determine which files to remove.
|
|
7
|
+
// CLAUDE.md section removal uses the same boundary markers as the
|
|
8
|
+
// merge logic in installSkills() — "# A2A Calling" start marker
|
|
9
|
+
// and "<!-- END A2A CALLING SECTION -->" end marker.
|
|
10
|
+
//
|
|
11
|
+
// Returns { removed: string[], preserved: string[], errors: string[] }
|
|
12
|
+
// so callers can print a meaningful summary.
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// These markers must match the ones used in install-skills.js merge logic.
|
|
19
|
+
// If they diverge, cleanup will fail to find the section boundaries.
|
|
20
|
+
const A2A_SECTION_START = '# A2A Calling';
|
|
21
|
+
const A2A_SECTION_END = '<!-- END A2A CALLING SECTION -->';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* cleanupProjectFiles — manifest-driven removal of all postinstall artifacts
|
|
25
|
+
*
|
|
26
|
+
* Reads .a2a-manifest.json from targetDir, then removes every file listed in
|
|
27
|
+
* the manifest. CLAUDE.md gets special handling: only the A2A section is
|
|
28
|
+
* removed, preserving any user content before/after. Empty directories
|
|
29
|
+
* (.claude/commands/, .claude/) are cleaned up if no non-a2a files remain.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} targetDir - The project directory containing .a2a-manifest.json
|
|
32
|
+
* @returns {{ removed: string[], preserved: string[], errors: string[] }}
|
|
33
|
+
*/
|
|
34
|
+
function cleanupProjectFiles(targetDir) {
|
|
35
|
+
const result = { removed: [], preserved: [], errors: [] };
|
|
36
|
+
const manifestPath = path.join(targetDir, '.a2a-manifest.json');
|
|
37
|
+
|
|
38
|
+
// If manifest doesn't exist, skip cleanup gracefully.
|
|
39
|
+
// This happens for manual installs, old versions, or projects where the
|
|
40
|
+
// manifest was already cleaned up. Better to leave files than crash.
|
|
41
|
+
if (!fs.existsSync(manifestPath)) {
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let manifest;
|
|
46
|
+
try {
|
|
47
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Corrupt or unreadable manifest — can't determine what to clean up.
|
|
50
|
+
// Return gracefully so npm uninstall doesn't fail.
|
|
51
|
+
result.errors.push(`.a2a-manifest.json: could not parse (${err.message})`);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const files = manifest.files || [];
|
|
56
|
+
|
|
57
|
+
for (const entry of files) {
|
|
58
|
+
// Skip the manifest itself — we remove it last, after all other files
|
|
59
|
+
if (entry.path === '.a2a-manifest.json') continue;
|
|
60
|
+
|
|
61
|
+
const filePath = path.join(targetDir, entry.path);
|
|
62
|
+
|
|
63
|
+
// If a file listed in manifest doesn't exist on disk, skip it silently.
|
|
64
|
+
// This handles cases where the user manually deleted files, or a previous
|
|
65
|
+
// partial cleanup already removed some files.
|
|
66
|
+
if (!fs.existsSync(filePath)) continue;
|
|
67
|
+
|
|
68
|
+
// ── CLAUDE.md: section removal, not whole-file deletion ──────────────
|
|
69
|
+
//
|
|
70
|
+
// The user may have their own project-specific content in CLAUDE.md.
|
|
71
|
+
// We only remove the A2A section (bounded by start/end markers), leaving
|
|
72
|
+
// everything else intact.
|
|
73
|
+
if (entry.path === 'CLAUDE.md') {
|
|
74
|
+
try {
|
|
75
|
+
const cleaned = removeA2ASectionFromClaudeMd(filePath);
|
|
76
|
+
if (cleaned === 'deleted') {
|
|
77
|
+
result.removed.push('CLAUDE.md (was A2A-only, deleted entirely)');
|
|
78
|
+
} else if (cleaned === 'trimmed') {
|
|
79
|
+
result.preserved.push('CLAUDE.md (A2A section removed, user content preserved)');
|
|
80
|
+
} else {
|
|
81
|
+
// 'no-section' — CLAUDE.md exists but has no A2A section.
|
|
82
|
+
// This shouldn't happen if the manifest says it was installed, but
|
|
83
|
+
// could occur if the user manually edited it. Leave it alone.
|
|
84
|
+
result.preserved.push('CLAUDE.md (no A2A section found, left unchanged)');
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
result.errors.push(`CLAUDE.md: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── All other files: delete entirely ─────────────────────────────────
|
|
93
|
+
try {
|
|
94
|
+
fs.rmSync(filePath, { force: true });
|
|
95
|
+
result.removed.push(entry.path);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// If file permissions prevent deletion, log warning and continue.
|
|
98
|
+
// We don't want a single permission error to abort the entire cleanup.
|
|
99
|
+
result.errors.push(`${entry.path}: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Empty directory cleanup ──────────────────────────────────────────────
|
|
104
|
+
//
|
|
105
|
+
// After removing .claude/commands/a2a-*.md and .claude/a2a-skill-reference.md,
|
|
106
|
+
// the directories may be empty. We clean them up to avoid leaving empty dirs,
|
|
107
|
+
// but ONLY if they contain no non-a2a files (user's own commands, settings, etc.).
|
|
108
|
+
cleanupEmptyDir(path.join(targetDir, '.claude', 'commands'), result);
|
|
109
|
+
cleanupEmptyDir(path.join(targetDir, '.claude'), result);
|
|
110
|
+
cleanupEmptyDir(path.join(targetDir, '.codex'), result);
|
|
111
|
+
|
|
112
|
+
// ── Remove the install log ───────────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// .a2a-install.log is written by postinstall.js as a convenience log.
|
|
115
|
+
// It's listed in the manifest but we also try to remove it explicitly
|
|
116
|
+
// in case the manifest entry was missing (belt and suspenders).
|
|
117
|
+
const logPath = path.join(targetDir, '.a2a-install.log');
|
|
118
|
+
if (fs.existsSync(logPath)) {
|
|
119
|
+
try {
|
|
120
|
+
fs.rmSync(logPath, { force: true });
|
|
121
|
+
// Only add to removed list if not already tracked from manifest iteration
|
|
122
|
+
if (!result.removed.includes('.a2a-install.log')) {
|
|
123
|
+
result.removed.push('.a2a-install.log');
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
result.errors.push(`.a2a-install.log: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Remove the manifest itself last ──────────────────────────────────────
|
|
131
|
+
//
|
|
132
|
+
// The manifest is the cleanup driver, so we remove it after everything else.
|
|
133
|
+
// If this fails, the manifest is left behind — which is acceptable since
|
|
134
|
+
// a stale manifest is harmless and helps debug failed cleanups.
|
|
135
|
+
try {
|
|
136
|
+
fs.rmSync(manifestPath, { force: true });
|
|
137
|
+
result.removed.push('.a2a-manifest.json');
|
|
138
|
+
} catch (err) {
|
|
139
|
+
result.errors.push(`.a2a-manifest.json: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* removeA2ASectionFromClaudeMd — surgically removes the A2A section from CLAUDE.md
|
|
147
|
+
*
|
|
148
|
+
* Three outcomes:
|
|
149
|
+
* - 'deleted' : File was entirely A2A content, deleted the file
|
|
150
|
+
* - 'trimmed' : A2A section removed, remaining user content preserved
|
|
151
|
+
* - 'no-section' : No A2A section found (file left unchanged)
|
|
152
|
+
*
|
|
153
|
+
* Edge case: CLAUDE.md becomes empty after A2A section removal.
|
|
154
|
+
* If the only content was the A2A section, the file would be left as
|
|
155
|
+
* whitespace-only, which is confusing. Delete it entirely instead.
|
|
156
|
+
*
|
|
157
|
+
* Edge case: Legacy installs without end marker.
|
|
158
|
+
* Pre-A2A-34 installs wrote the A2A section without the
|
|
159
|
+
* <!-- END A2A CALLING SECTION --> marker. For these, we fall back
|
|
160
|
+
* to removing from "# A2A Calling" to EOF, same as the old merge
|
|
161
|
+
* replacement behavior. This means any user content added after the
|
|
162
|
+
* A2A section in a legacy install will be lost — but this is the
|
|
163
|
+
* same behavior they already had on upgrade, so it's not a regression.
|
|
164
|
+
*/
|
|
165
|
+
function removeA2ASectionFromClaudeMd(filePath) {
|
|
166
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
167
|
+
|
|
168
|
+
// If the A2A section start marker isn't present, nothing to remove
|
|
169
|
+
if (!content.includes(A2A_SECTION_START)) {
|
|
170
|
+
return 'no-section';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sectionStart = content.indexOf(A2A_SECTION_START);
|
|
174
|
+
const endMarkerIndex = content.indexOf(A2A_SECTION_END, sectionStart);
|
|
175
|
+
|
|
176
|
+
let before, after;
|
|
177
|
+
|
|
178
|
+
if (endMarkerIndex !== -1) {
|
|
179
|
+
// End marker found — extract content before and after the bounded section.
|
|
180
|
+
// trimEnd/trimStart collapse the whitespace gap left by the removed section.
|
|
181
|
+
const sectionEnd = endMarkerIndex + A2A_SECTION_END.length;
|
|
182
|
+
before = content.slice(0, sectionStart).trimEnd();
|
|
183
|
+
after = content.slice(sectionEnd).trimStart();
|
|
184
|
+
} else {
|
|
185
|
+
// Legacy install without end marker — remove from header to EOF.
|
|
186
|
+
// Everything after "# A2A Calling" is considered part of the A2A section.
|
|
187
|
+
before = content.slice(0, sectionStart).trimEnd();
|
|
188
|
+
after = '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Reassemble the file from the non-A2A portions
|
|
192
|
+
let cleaned;
|
|
193
|
+
if (before && after) {
|
|
194
|
+
// Content exists both before and after — join with double newline
|
|
195
|
+
cleaned = before + '\n\n' + after;
|
|
196
|
+
} else if (before) {
|
|
197
|
+
cleaned = before;
|
|
198
|
+
} else if (after) {
|
|
199
|
+
cleaned = after;
|
|
200
|
+
} else {
|
|
201
|
+
cleaned = '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Collapse any triple+ blank lines into double blank lines.
|
|
205
|
+
// This prevents ugly whitespace gaps where the A2A section used to be.
|
|
206
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
|
|
207
|
+
|
|
208
|
+
// Edge case: CLAUDE.md becomes empty after A2A section removal.
|
|
209
|
+
// If the only content was the A2A section, the file would be left as
|
|
210
|
+
// whitespace-only, which is confusing. Delete it entirely instead.
|
|
211
|
+
if (!cleaned.trim()) {
|
|
212
|
+
fs.rmSync(filePath, { force: true });
|
|
213
|
+
return 'deleted';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Write the cleaned content back, ensuring the file ends with a newline
|
|
217
|
+
fs.writeFileSync(filePath, cleaned.trimEnd() + '\n');
|
|
218
|
+
return 'trimmed';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* cleanupEmptyDir — removes a directory if it exists and contains no files.
|
|
223
|
+
*
|
|
224
|
+
* After removing A2A skill files, directories like .claude/commands/ may be
|
|
225
|
+
* empty. We remove them to avoid clutter, but ONLY if no user files remain.
|
|
226
|
+
* This prevents accidentally deleting directories that contain the user's
|
|
227
|
+
* own Claude Code commands or settings.
|
|
228
|
+
*/
|
|
229
|
+
function cleanupEmptyDir(dirPath, result) {
|
|
230
|
+
try {
|
|
231
|
+
if (!fs.existsSync(dirPath)) return;
|
|
232
|
+
|
|
233
|
+
// Check if directory stat confirms it's actually a directory
|
|
234
|
+
const stat = fs.statSync(dirPath);
|
|
235
|
+
if (!stat.isDirectory()) return;
|
|
236
|
+
|
|
237
|
+
const entries = fs.readdirSync(dirPath);
|
|
238
|
+
|
|
239
|
+
// Only remove if the directory is completely empty.
|
|
240
|
+
// Any remaining files (user's own commands, .gitkeep, etc.) mean we keep it.
|
|
241
|
+
if (entries.length === 0) {
|
|
242
|
+
fs.rmdirSync(dirPath);
|
|
243
|
+
result.removed.push(dirPath.split(path.sep).slice(-2).join('/') + '/ (empty directory)');
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// Directory cleanup is best-effort. Failures here are non-fatal —
|
|
247
|
+
// an empty directory is harmless, just slightly untidy.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = { cleanupProjectFiles };
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A2A Skill Installer
|
|
3
3
|
*
|
|
4
|
-
* Copies Claude Code commands, CLAUDE.md context,
|
|
5
|
-
* target project directory. Idempotent: skips files
|
|
6
|
-
* identical content.
|
|
4
|
+
* Copies Claude Code commands, CLAUDE.md context, SKILL.md reference, and
|
|
5
|
+
* Codex AGENTS.md into a target project directory. Idempotent: skips files
|
|
6
|
+
* that already exist with identical content.
|
|
7
7
|
*
|
|
8
8
|
* CLAUDE.md is the key file — Claude Code reads it automatically, giving the
|
|
9
9
|
* agent full context about the a2a CLI, native app, and onboarding flow
|
|
10
10
|
* immediately after npm install.
|
|
11
|
+
*
|
|
12
|
+
* SKILL.md is the deep reference for A2A — invite formatting templates,
|
|
13
|
+
* incoming call handling, disclosure manifest flow, and protocol details.
|
|
14
|
+
* We copy it to .claude/ so Claude Code can discover it naturally without
|
|
15
|
+
* having to grep through node_modules. The .claude/ directory is already
|
|
16
|
+
* used for slash commands, so this is a natural home for reference docs.
|
|
17
|
+
*
|
|
18
|
+
* Unlike CLAUDE.md (which is auto-loaded), the skill reference file is only
|
|
19
|
+
* read when the agent explicitly looks in .claude/ — so it serves as opt-in
|
|
20
|
+
* deep reference rather than always-loaded context (avoiding token bloat).
|
|
11
21
|
*/
|
|
12
22
|
|
|
13
23
|
const fs = require('fs');
|
|
@@ -15,9 +25,18 @@ const path = require('path');
|
|
|
15
25
|
|
|
16
26
|
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
17
27
|
|
|
28
|
+
// Section delimiter used to bound the A2A block inside CLAUDE.md.
|
|
29
|
+
// This allows safe replacement of ONLY the A2A content when updating,
|
|
30
|
+
// preserving any user content before or after the A2A section.
|
|
31
|
+
const A2A_SECTION_END_MARKER = '<!-- END A2A CALLING SECTION -->';
|
|
32
|
+
|
|
18
33
|
const SKILL_FILES = [
|
|
19
34
|
// CLAUDE.md — gives Claude Code instant context about the a2a CLI
|
|
20
35
|
{ src: 'CLAUDE-INSTALL.md', dest: 'CLAUDE.md', mergeKey: '# A2A Calling' },
|
|
36
|
+
// SKILL.md — deep reference for A2A (invite formatting, call handling, etc.)
|
|
37
|
+
// Copied to .claude/ so Claude Code discovers it naturally without grepping
|
|
38
|
+
// node_modules. This is opt-in context: only loaded when the agent looks.
|
|
39
|
+
{ src: 'SKILL.md', dest: '.claude/a2a-skill-reference.md' },
|
|
21
40
|
// Claude Code slash commands
|
|
22
41
|
{ src: '.claude/commands/a2a-call.md', dest: '.claude/commands/a2a-call.md' },
|
|
23
42
|
{ src: '.claude/commands/a2a-invite.md', dest: '.claude/commands/a2a-invite.md' },
|
|
@@ -31,6 +50,20 @@ const SKILL_FILES = [
|
|
|
31
50
|
function installSkills(targetDir, options = {}) {
|
|
32
51
|
const result = { installed: [], skipped: [], errors: [] };
|
|
33
52
|
|
|
53
|
+
// Install manifest: .a2a-manifest.json
|
|
54
|
+
//
|
|
55
|
+
// After installing skill files, we write a manifest recording every file
|
|
56
|
+
// we touched, what action was taken, and the package version. This serves
|
|
57
|
+
// three purposes:
|
|
58
|
+
// 1. Transparency — user can see exactly what the package added
|
|
59
|
+
// 2. Clean uninstall — `a2a uninstall` reads the manifest to remove files
|
|
60
|
+
// 3. Upgrade tracking — on re-install, we can compare versions and
|
|
61
|
+
// only update files that actually changed
|
|
62
|
+
//
|
|
63
|
+
// The manifest is written to the project root (same level as CLAUDE.md)
|
|
64
|
+
// so it's easy to find.
|
|
65
|
+
const manifestEntries = [];
|
|
66
|
+
|
|
34
67
|
for (const file of SKILL_FILES) {
|
|
35
68
|
const srcPath = path.join(PACKAGE_ROOT, file.src);
|
|
36
69
|
const destPath = path.join(targetDir, file.dest);
|
|
@@ -38,6 +71,7 @@ function installSkills(targetDir, options = {}) {
|
|
|
38
71
|
try {
|
|
39
72
|
if (!fs.existsSync(srcPath)) {
|
|
40
73
|
result.errors.push({ file: file.src, error: 'Source file not found' });
|
|
74
|
+
manifestEntries.push({ path: file.dest, action: 'error', detail: 'Source file not found' });
|
|
41
75
|
continue;
|
|
42
76
|
}
|
|
43
77
|
|
|
@@ -46,47 +80,125 @@ function installSkills(targetDir, options = {}) {
|
|
|
46
80
|
if (fs.existsSync(destPath)) {
|
|
47
81
|
const existing = fs.readFileSync(destPath, 'utf8');
|
|
48
82
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
83
|
+
// ── CLAUDE.md merge strategy ──────────────────────────────────────
|
|
84
|
+
//
|
|
85
|
+
// The a2acalling package installs context into the project's CLAUDE.md so
|
|
86
|
+
// Claude Code has immediate awareness of the CLI, commands, and native app.
|
|
87
|
+
// But projects often have their own CLAUDE.md with project-specific instructions.
|
|
88
|
+
//
|
|
89
|
+
// To avoid clobbering user content, we use a section-delimited merge:
|
|
90
|
+
// 1. If CLAUDE.md doesn't exist: write CLAUDE-INSTALL.md as-is
|
|
91
|
+
// 2. If CLAUDE.md exists WITHOUT an A2A section: append at the end
|
|
92
|
+
// 3. If CLAUDE.md exists WITH an A2A section: replace only between
|
|
93
|
+
// "# A2A Calling" and "<!-- END A2A CALLING SECTION -->"
|
|
94
|
+
//
|
|
95
|
+
// The end marker ensures user content after the A2A block is preserved.
|
|
96
|
+
// Legacy installs without the marker fall back to replace-to-EOF (old behavior)
|
|
97
|
+
// but the marker is added during the update so future updates are safe.
|
|
51
98
|
if (file.mergeKey) {
|
|
52
99
|
if (existing.includes(file.mergeKey)) {
|
|
53
|
-
// A2A section already present —
|
|
100
|
+
// A2A section already present — find its boundaries
|
|
54
101
|
const sectionStart = existing.indexOf(file.mergeKey);
|
|
55
|
-
|
|
102
|
+
|
|
103
|
+
// Check if the end marker exists to determine section boundaries.
|
|
104
|
+
// If found, we only replace the bounded section, preserving any
|
|
105
|
+
// user content after the marker. If not found (legacy installs
|
|
106
|
+
// that predate the marker), we replace from the header to EOF
|
|
107
|
+
// and add the marker so future updates are safe.
|
|
108
|
+
const endMarkerIndex = existing.indexOf(A2A_SECTION_END_MARKER, sectionStart);
|
|
109
|
+
|
|
110
|
+
let existingSection;
|
|
111
|
+
let after = '';
|
|
112
|
+
|
|
113
|
+
if (endMarkerIndex !== -1) {
|
|
114
|
+
// End marker found — extract only the bounded A2A section
|
|
115
|
+
const sectionEnd = endMarkerIndex + A2A_SECTION_END_MARKER.length;
|
|
116
|
+
existingSection = existing.slice(sectionStart, sectionEnd);
|
|
117
|
+
// Preserve everything after the end marker (user's trailing content)
|
|
118
|
+
after = existing.slice(sectionEnd);
|
|
119
|
+
} else {
|
|
120
|
+
// Legacy install without end marker — take everything from header to EOF
|
|
121
|
+
// The new content includes the marker, so future updates will be safe
|
|
122
|
+
existingSection = existing.slice(sectionStart);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Compare the existing A2A section with the new source content.
|
|
126
|
+
// Skip the update if they're identical (idempotent behavior).
|
|
56
127
|
if (!options.force && existingSection.trim() === srcContent.trim()) {
|
|
57
128
|
result.skipped.push(file.dest);
|
|
129
|
+
manifestEntries.push({ path: file.dest, action: 'skipped' });
|
|
58
130
|
continue;
|
|
59
131
|
}
|
|
60
|
-
|
|
132
|
+
|
|
133
|
+
// Replace only the A2A section, preserving content before and after
|
|
61
134
|
const before = existing.slice(0, sectionStart).trimEnd();
|
|
62
|
-
|
|
135
|
+
let merged = before ? before + '\n\n' + srcContent : srcContent;
|
|
136
|
+
// Re-attach any trailing content that was after the end marker
|
|
137
|
+
if (after) {
|
|
138
|
+
merged = merged.trimEnd() + '\n' + after;
|
|
139
|
+
}
|
|
63
140
|
fs.writeFileSync(destPath, merged);
|
|
64
141
|
result.installed.push(file.dest + ' (updated A2A section)');
|
|
142
|
+
manifestEntries.push({ path: file.dest, action: 'updated A2A section' });
|
|
65
143
|
} else {
|
|
66
|
-
// Existing CLAUDE.md without A2A section — append
|
|
144
|
+
// Existing CLAUDE.md without A2A section — append at the end
|
|
67
145
|
const merged = existing.trimEnd() + '\n\n' + srcContent;
|
|
68
146
|
fs.writeFileSync(destPath, merged);
|
|
69
147
|
result.installed.push(file.dest + ' (appended A2A section)');
|
|
148
|
+
manifestEntries.push({ path: file.dest, action: 'appended A2A section' });
|
|
70
149
|
}
|
|
71
150
|
continue;
|
|
72
151
|
}
|
|
73
152
|
|
|
74
|
-
// Standard mode: skip if identical
|
|
153
|
+
// Standard mode: skip if identical (non-merge files)
|
|
75
154
|
if (!options.force && existing === srcContent) {
|
|
76
155
|
result.skipped.push(file.dest);
|
|
156
|
+
manifestEntries.push({ path: file.dest, action: 'skipped' });
|
|
77
157
|
continue;
|
|
78
158
|
}
|
|
159
|
+
} else {
|
|
160
|
+
// File doesn't exist yet — will be created below
|
|
79
161
|
}
|
|
80
162
|
|
|
81
163
|
// Create directory and write file
|
|
82
164
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
83
165
|
fs.writeFileSync(destPath, srcContent);
|
|
84
166
|
result.installed.push(file.dest);
|
|
167
|
+
manifestEntries.push({ path: file.dest, action: 'created' });
|
|
85
168
|
} catch (err) {
|
|
86
169
|
result.errors.push({ file: file.dest, error: err.message });
|
|
170
|
+
manifestEntries.push({ path: file.dest, action: 'error', detail: err.message });
|
|
87
171
|
}
|
|
88
172
|
}
|
|
89
173
|
|
|
174
|
+
// ── Write install manifest ────────────────────────────────────────────
|
|
175
|
+
//
|
|
176
|
+
// The manifest records every file we touched so the user (or `a2a uninstall`)
|
|
177
|
+
// knows exactly what was installed. We include the manifest itself in the list
|
|
178
|
+
// for completeness.
|
|
179
|
+
try {
|
|
180
|
+
const pkg = require('../package.json');
|
|
181
|
+
const manifestPath = path.join(targetDir, '.a2a-manifest.json');
|
|
182
|
+
|
|
183
|
+
// Add the manifest file itself to the entries list
|
|
184
|
+
manifestEntries.push({ path: '.a2a-manifest.json', action: 'created' });
|
|
185
|
+
// Add the install log file (written by postinstall.js)
|
|
186
|
+
manifestEntries.push({ path: '.a2a-install.log', action: 'created' });
|
|
187
|
+
|
|
188
|
+
const manifest = {
|
|
189
|
+
version: pkg.version,
|
|
190
|
+
installed_at: new Date().toISOString(),
|
|
191
|
+
files: manifestEntries,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Non-fatal — the manifest is a convenience for cleanup, not required
|
|
197
|
+
// for the skill files to work. Failure here (e.g., read-only dir)
|
|
198
|
+
// should not break the install.
|
|
199
|
+
result.errors.push({ file: '.a2a-manifest.json', error: err.message });
|
|
200
|
+
}
|
|
201
|
+
|
|
90
202
|
return result;
|
|
91
203
|
}
|
|
92
204
|
|
package/scripts/postinstall.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
|
|
24
24
|
if (process.env.DOCKER) process.exit(0);
|
|
25
25
|
|
|
26
|
+
const fs = require('fs');
|
|
26
27
|
const path = require('path');
|
|
27
28
|
const os = require('os');
|
|
28
29
|
const { spawnSync } = require('child_process');
|
|
@@ -70,6 +71,22 @@ function printGettingStarted() {
|
|
|
70
71
|
const isMac = os.platform() === 'darwin';
|
|
71
72
|
const pkg = require('../package.json');
|
|
72
73
|
|
|
74
|
+
// ── Short critical stderr output ──────────────────────────────────────
|
|
75
|
+
//
|
|
76
|
+
// npm v7+ suppresses stdout/stderr from postinstall scripts unless
|
|
77
|
+
// --foreground-scripts is used. We print a minimal critical line to stderr
|
|
78
|
+
// (which npm sometimes still shows) AND write the full banner to a log file
|
|
79
|
+
// so that Claude Code or a human can always find the getting-started info.
|
|
80
|
+
const shortLines = [
|
|
81
|
+
`a2acalling v${pkg.version} installed.`,
|
|
82
|
+
'Next step: a2a quickstart',
|
|
83
|
+
'Skills installed: /a2a-setup, /a2a-call, /a2a-contacts, /a2a-invite, /a2a-status',
|
|
84
|
+
];
|
|
85
|
+
// Print the short critical summary to stderr — this is the most likely
|
|
86
|
+
// output to survive npm's output suppression in v7+
|
|
87
|
+
console.error(shortLines.join('\n'));
|
|
88
|
+
|
|
89
|
+
// ── Full verbose banner ───────────────────────────────────────────────
|
|
73
90
|
const lines = [
|
|
74
91
|
'',
|
|
75
92
|
'╔══════════════════════════════════════════════════════════════╗',
|
|
@@ -176,9 +193,32 @@ function printGettingStarted() {
|
|
|
176
193
|
'',
|
|
177
194
|
);
|
|
178
195
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
console.log(
|
|
196
|
+
const fullBanner = lines.join('\n');
|
|
197
|
+
|
|
198
|
+
// Print the full banner to stdout for contexts where it's visible
|
|
199
|
+
// (e.g., --foreground-scripts, direct script execution, agent contexts)
|
|
200
|
+
console.log(fullBanner);
|
|
201
|
+
|
|
202
|
+
// ── Write full banner to a log file ─────────────────────────────────
|
|
203
|
+
//
|
|
204
|
+
// Even when npm suppresses terminal output, the agent or human can read
|
|
205
|
+
// this file to get the full getting-started info. We write to the project
|
|
206
|
+
// root (initCwd) so it's next to CLAUDE.md and easy to discover.
|
|
207
|
+
try {
|
|
208
|
+
const logPath = path.join(initCwd, '.a2a-install.log');
|
|
209
|
+
const logContent = [
|
|
210
|
+
`a2acalling v${pkg.version} — install summary`,
|
|
211
|
+
`Installed at: ${new Date().toISOString()}`,
|
|
212
|
+
`Target directory: ${initCwd}`,
|
|
213
|
+
`Global install: ${isGlobal}`,
|
|
214
|
+
'',
|
|
215
|
+
...shortLines,
|
|
216
|
+
'',
|
|
217
|
+
fullBanner,
|
|
218
|
+
].join('\n');
|
|
219
|
+
fs.writeFileSync(logPath, logContent);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
// Non-fatal — the log file is a convenience, not a requirement.
|
|
222
|
+
// This can fail if the target directory is read-only (e.g., system dirs).
|
|
223
|
+
}
|
|
184
224
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// npm preuninstall hook — manifest-driven cleanup
|
|
5
|
+
//
|
|
6
|
+
// When `npm uninstall a2acalling` runs, npm calls this script BEFORE
|
|
7
|
+
// removing the package from node_modules. We read .a2a-manifest.json
|
|
8
|
+
// to find every file our postinstall created and remove them cleanly.
|
|
9
|
+
//
|
|
10
|
+
// CLAUDE.md gets special handling: we only remove the A2A section
|
|
11
|
+
// (between "# A2A Calling" and "<!-- END A2A CALLING SECTION -->"),
|
|
12
|
+
// preserving any project-specific content the user added before/after.
|
|
13
|
+
//
|
|
14
|
+
// If the manifest doesn't exist (manual install, old version), we
|
|
15
|
+
// skip cleanup gracefully — better to leave files than crash the uninstall.
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
// Skip in CI environments — cleanup is not needed in ephemeral containers
|
|
19
|
+
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
|
|
20
|
+
if (process.env.DOCKER) process.exit(0);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { cleanupProjectFiles } = require('./cleanup');
|
|
25
|
+
|
|
26
|
+
// INIT_CWD is set by npm to the directory where `npm uninstall` was run.
|
|
27
|
+
// This is the project root where postinstall placed the skill files.
|
|
28
|
+
// Falls back to cwd() which should also be the project root during npm lifecycle.
|
|
29
|
+
const targetDir = process.env.INIT_CWD || process.cwd();
|
|
30
|
+
|
|
31
|
+
const result = cleanupProjectFiles(targetDir);
|
|
32
|
+
|
|
33
|
+
// Print a short summary so the user (or agent) knows what was cleaned up.
|
|
34
|
+
// This output may be suppressed by npm v7+ unless --foreground-scripts is used,
|
|
35
|
+
// but it's still useful for debugging and for agents that capture stderr.
|
|
36
|
+
if (result.removed.length > 0) {
|
|
37
|
+
console.error(`a2acalling: cleaned up ${result.removed.length} file(s):`);
|
|
38
|
+
for (const f of result.removed) {
|
|
39
|
+
console.error(` - ${f}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (result.preserved.length > 0) {
|
|
43
|
+
for (const f of result.preserved) {
|
|
44
|
+
console.error(` ~ ${f}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (result.errors.length > 0) {
|
|
48
|
+
console.error(` Warnings (${result.errors.length}):`);
|
|
49
|
+
for (const e of result.errors) {
|
|
50
|
+
console.error(` ! ${e}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (result.removed.length === 0 && result.preserved.length === 0 && result.errors.length === 0) {
|
|
54
|
+
// No manifest found or manifest was empty — nothing to clean up.
|
|
55
|
+
// This is expected for fresh installs that never ran postinstall,
|
|
56
|
+
// or for projects where cleanup was already done via `a2a uninstall`.
|
|
57
|
+
console.error('a2acalling: no install manifest found, skipping project cleanup.');
|
|
58
|
+
console.error(' Tip: run `a2a uninstall` to also remove server config and database.');
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// CRITICAL: Never crash the npm uninstall process.
|
|
62
|
+
// If our cleanup fails for any reason, npm should still be able to remove
|
|
63
|
+
// the package from node_modules. A failed cleanup leaves orphaned files,
|
|
64
|
+
// which is annoying but not harmful. A crashed uninstall is much worse.
|
|
65
|
+
console.error(`a2acalling: cleanup warning — ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(0);
|