a2acalling 0.6.74 → 0.6.75

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.
Files changed (118) hide show
  1. package/.a2a-manifest.json +2 -2
  2. package/.c8rc.json +16 -0
  3. package/.node-version +1 -0
  4. package/.serena/project.yml +126 -0
  5. package/ARCHITECTURE.md +11 -0
  6. package/CONVENTIONS.md +9 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/favicon.png +0 -0
  10. package/coverage/index.html +146 -0
  11. package/coverage/prettify.css +1 -0
  12. package/coverage/prettify.js +2 -0
  13. package/coverage/sort-arrow-sprite.png +0 -0
  14. package/coverage/sorter.js +210 -0
  15. package/coverage/src/index.html +131 -0
  16. package/coverage/src/index.js.html +313 -0
  17. package/coverage/src/lib/agent-card.js.html +418 -0
  18. package/coverage/src/lib/call-monitor.js.html +700 -0
  19. package/coverage/src/lib/callbook.js.html +1183 -0
  20. package/coverage/src/lib/claude-subagent.js.html +2173 -0
  21. package/coverage/src/lib/client.js.html +2134 -0
  22. package/coverage/src/lib/config.js.html +1525 -0
  23. package/coverage/src/lib/conversation-driver.js.html +1909 -0
  24. package/coverage/src/lib/conversations.js.html +2575 -0
  25. package/coverage/src/lib/crypto.js.html +424 -0
  26. package/coverage/src/lib/dashboard-events.js.html +724 -0
  27. package/coverage/src/lib/disclosure.js.html +2461 -0
  28. package/coverage/src/lib/external-ip.js.html +718 -0
  29. package/coverage/src/lib/index.html +506 -0
  30. package/coverage/src/lib/invite-host.js.html +754 -0
  31. package/coverage/src/lib/local-request.js.html +292 -0
  32. package/coverage/src/lib/logger.js.html +2116 -0
  33. package/coverage/src/lib/openclaw-integration.js.html +1102 -0
  34. package/coverage/src/lib/pid-file.js.html +394 -0
  35. package/coverage/src/lib/port-scanner.js.html +334 -0
  36. package/coverage/src/lib/prompt-template.js.html +1150 -0
  37. package/coverage/src/lib/runtime-adapter.js.html +2188 -0
  38. package/coverage/src/lib/summarizer.js.html +553 -0
  39. package/coverage/src/lib/summary-formatter.js.html +589 -0
  40. package/coverage/src/lib/summary-prompt.js.html +694 -0
  41. package/coverage/src/lib/tokens.js.html +2689 -0
  42. package/coverage/src/lib/turn-timeout.js.html +241 -0
  43. package/coverage/src/lib/update-checker.js.html +364 -0
  44. package/coverage/src/lib/update-manager.js.html +1024 -0
  45. package/coverage/src/routes/a2a.js.html +3724 -0
  46. package/coverage/src/routes/callbook.js.html +511 -0
  47. package/coverage/src/routes/dashboard.js.html +4819 -0
  48. package/coverage/src/routes/index.html +146 -0
  49. package/coverage/src/server.js.html +3622 -0
  50. package/coverage/tmp/coverage-1605378-1772576706365-0.json +1 -0
  51. package/coverage/tmp/coverage-1605384-1772576607459-0.json +1 -0
  52. package/coverage/tmp/coverage-1605410-1772576631155-0.json +1 -0
  53. package/coverage/tmp/coverage-1606942-1772576636869-0.json +1 -0
  54. package/coverage/tmp/coverage-1607004-1772576637454-0.json +1 -0
  55. package/coverage/tmp/coverage-1607044-1772576637876-0.json +1 -0
  56. package/coverage/tmp/coverage-1607096-1772576638356-0.json +1 -0
  57. package/coverage/tmp/coverage-1607145-1772576638777-0.json +1 -0
  58. package/coverage/tmp/coverage-1607201-1772576639277-0.json +1 -0
  59. package/coverage/tmp/coverage-1607247-1772576639755-0.json +1 -0
  60. package/coverage/tmp/coverage-1607317-1772576640083-0.json +1 -0
  61. package/coverage/tmp/coverage-1607381-1772576640465-0.json +1 -0
  62. package/coverage/tmp/coverage-1607446-1772576640868-0.json +1 -0
  63. package/coverage/tmp/coverage-1607501-1772576641662-0.json +1 -0
  64. package/coverage/tmp/coverage-1607534-1772576641565-0.json +1 -0
  65. package/coverage/tmp/coverage-1607627-1772576641871-0.json +1 -0
  66. package/coverage/tmp/coverage-1607665-1772576642172-0.json +1 -0
  67. package/coverage/tmp/coverage-1607714-1772576642577-0.json +1 -0
  68. package/coverage/tmp/coverage-1607788-1772576643466-0.json +1 -0
  69. package/coverage/tmp/coverage-1607924-1772576644678-0.json +1 -0
  70. package/coverage/tmp/coverage-1607978-1772576645154-0.json +1 -0
  71. package/coverage/tmp/coverage-1608035-1772576645564-0.json +1 -0
  72. package/coverage/tmp/coverage-1608106-1772576645967-0.json +1 -0
  73. package/coverage/tmp/coverage-1608179-1772576648656-0.json +1 -0
  74. package/coverage/tmp/coverage-1608196-1772576647367-0.json +1 -0
  75. package/coverage/tmp/coverage-1608217-1772576648557-0.json +1 -0
  76. package/coverage/tmp/coverage-1608256-1772576651378-0.json +1 -0
  77. package/coverage/tmp/coverage-1608265-1772576650058-0.json +1 -0
  78. package/coverage/tmp/coverage-1608289-1772576651358-0.json +1 -0
  79. package/coverage/tmp/coverage-1608591-1772576660465-0.json +1 -0
  80. package/coverage/tmp/coverage-1608648-1772576659272-0.json +1 -0
  81. package/coverage/tmp/coverage-1608665-1772576660374-0.json +1 -0
  82. package/coverage/tmp/coverage-1608677-1772576661268-0.json +1 -0
  83. package/coverage/tmp/coverage-1608684-1772576663968-0.json +1 -0
  84. package/coverage/tmp/coverage-1608692-1772576662575-0.json +1 -0
  85. package/coverage/tmp/coverage-1608701-1772576663873-0.json +1 -0
  86. package/coverage/tmp/coverage-1608718-1772576666674-0.json +1 -0
  87. package/coverage/tmp/coverage-1608725-1772576665463-0.json +1 -0
  88. package/coverage/tmp/coverage-1608738-1772576666577-0.json +1 -0
  89. package/coverage/tmp/coverage-1608753-1772576669664-0.json +1 -0
  90. package/coverage/tmp/coverage-1608763-1772576668275-0.json +1 -0
  91. package/coverage/tmp/coverage-1608771-1772576669563-0.json +1 -0
  92. package/coverage/tmp/coverage-1608828-1772576676574-0.json +1 -0
  93. package/coverage/tmp/coverage-1609244-1772576675272-0.json +1 -0
  94. package/coverage/tmp/coverage-1609342-1772576676478-0.json +1 -0
  95. package/coverage/tmp/coverage-1609450-1772576686954-0.json +1 -0
  96. package/coverage/tmp/coverage-1609841-1772576685466-0.json +1 -0
  97. package/coverage/tmp/coverage-1609925-1772576686855-0.json +1 -0
  98. package/coverage/tmp/coverage-1610399-1772576692469-0.json +1 -0
  99. package/coverage/tmp/coverage-1611283-1772576703062-0.json +1 -0
  100. package/coverage/tmp/coverage-1611294-1772576703755-0.json +1 -0
  101. package/docs/plans/2026-03-03-a2a-91-macos-packaging-plan.md +144 -0
  102. package/docs/signing-setup.md +49 -0
  103. package/native/macos/certs/appldevcert.cer +0 -0
  104. package/native/macos/src-tauri/binaries/.gitkeep +0 -0
  105. package/native/macos/src-tauri/capabilities/default.json +11 -1
  106. package/native/macos/src-tauri/entitlements.plist +14 -0
  107. package/native/macos/src-tauri/src/discovery.rs +14 -3
  108. package/native/macos/src-tauri/src/health.rs +4 -0
  109. package/native/macos/src-tauri/src/lib.rs +52 -11
  110. package/native/macos/src-tauri/src/server.rs +262 -26
  111. package/native/macos/src-tauri/tauri.conf.json +13 -4
  112. package/package.json +7 -2
  113. package/pkg.config.json +14 -0
  114. package/scripts/build-standalone.sh +106 -0
  115. package/scripts/smoke-test-standalone.sh +101 -0
  116. package/scripts/sync-version.sh +28 -0
  117. package/scripts/verify-app-bundle.sh +34 -0
  118. package/.maestro/inbox/release-workflow-spam.md +0 -25
@@ -1,55 +1,287 @@
1
1
  use serde::Serialize;
2
2
  use std::process::Command;
3
+ use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering};
4
+ use std::sync::Mutex;
5
+ use std::time::{Duration, Instant};
6
+ use tauri::{Emitter, Manager};
7
+ use tauri_plugin_shell::process::{CommandChild, CommandEvent};
8
+ use tauri_plugin_shell::ShellExt;
9
+
10
+ const MAX_CRASH_COUNT: u32 = 5;
11
+ const STABLE_THRESHOLD: Duration = Duration::from_secs(60);
12
+ const MAX_BACKOFF_MS: u64 = 30_000;
3
13
 
4
14
  #[derive(Debug, Serialize)]
5
15
  pub struct StartResult {
6
16
  pub success: bool,
7
17
  pub message: String,
18
+ pub port: Option<u16>,
19
+ pub source: String, // "sidecar" | "external" | "none"
8
20
  }
9
21
 
10
- /// Find the `a2a` CLI binary
11
- fn find_a2a_binary() -> Option<String> {
12
- // Check common locations
13
- let candidates = [
14
- "a2a", // In PATH
15
- ];
16
-
17
- for candidate in &candidates {
18
- let result = Command::new("which")
19
- .arg(candidate)
20
- .output();
21
-
22
- if let Ok(output) = result {
23
- if output.status.success() {
24
- let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
25
- if !path.is_empty() {
26
- return Some(path);
22
+ /// Holds the sidecar child process, port, and crash recovery state.
23
+ pub struct SidecarState {
24
+ pub child: Mutex<Option<CommandChild>>,
25
+ pub port: AtomicU16,
26
+ pub crash_count: AtomicU32,
27
+ pub shutting_down: AtomicBool,
28
+ last_start: Mutex<Option<Instant>>,
29
+ }
30
+
31
+ impl SidecarState {
32
+ pub fn new() -> Self {
33
+ SidecarState {
34
+ child: Mutex::new(None),
35
+ port: AtomicU16::new(0),
36
+ crash_count: AtomicU32::new(0),
37
+ shutting_down: AtomicBool::new(false),
38
+ last_start: Mutex::new(None),
39
+ }
40
+ }
41
+
42
+ pub fn port(&self) -> u16 {
43
+ self.port.load(Ordering::Relaxed)
44
+ }
45
+ }
46
+
47
+ /// Calculate exponential backoff: 1s, 2s, 4s, 8s, 16s (capped at 30s).
48
+ fn backoff_ms(crash_count: u32) -> u64 {
49
+ let exponent = crash_count.saturating_sub(1).min(4);
50
+ let ms = 1000u64 << exponent;
51
+ ms.min(MAX_BACKOFF_MS)
52
+ }
53
+
54
+ /// Pick a port for the sidecar: prefer config, then OS-assigned.
55
+ fn pick_port() -> u16 {
56
+ let config_ports = crate::discovery::read_config_ports();
57
+ if let Some(&port) = config_ports.first() {
58
+ return port;
59
+ }
60
+ // Let the OS pick an available port
61
+ std::net::TcpListener::bind("127.0.0.1:0")
62
+ .and_then(|l| l.local_addr())
63
+ .map(|addr| addr.port())
64
+ .unwrap_or(3001)
65
+ }
66
+
67
+ /// Start the A2A server via Tauri sidecar (bundled binary).
68
+ pub fn start_sidecar(app: &tauri::AppHandle) -> StartResult {
69
+ let port = pick_port();
70
+ let port_str = port.to_string();
71
+
72
+ let sidecar_cmd = match app.shell().sidecar("a2a-server") {
73
+ Ok(cmd) => cmd,
74
+ Err(_) => return start_external_server(port),
75
+ };
76
+
77
+ let (rx, child) = match sidecar_cmd
78
+ .env("PORT", &port_str)
79
+ .spawn()
80
+ {
81
+ Ok(pair) => pair,
82
+ Err(_) => return start_external_server(port),
83
+ };
84
+
85
+ // Store the child handle and record start time
86
+ if let Some(state) = app.try_state::<SidecarState>() {
87
+ state.port.store(port, Ordering::Relaxed);
88
+ if let Ok(mut guard) = state.child.lock() {
89
+ *guard = Some(child);
90
+ }
91
+ if let Ok(mut guard) = state.last_start.lock() {
92
+ *guard = Some(Instant::now());
93
+ }
94
+ }
95
+
96
+ // Monitor sidecar stdout/stderr and detect exit for crash recovery
97
+ spawn_sidecar_monitor(app.clone(), rx);
98
+
99
+ StartResult {
100
+ success: true,
101
+ message: format!("Sidecar server starting on port {}...", port),
102
+ port: Some(port),
103
+ source: "sidecar".to_string(),
104
+ }
105
+ }
106
+
107
+ /// Monitor sidecar output and process exit for crash recovery.
108
+ fn spawn_sidecar_monitor(
109
+ app: tauri::AppHandle,
110
+ mut rx: tokio::sync::mpsc::Receiver<CommandEvent>,
111
+ ) {
112
+ tauri::async_runtime::spawn(async move {
113
+ while let Some(event) = rx.recv().await {
114
+ match event {
115
+ CommandEvent::Stdout(line) => {
116
+ let text = String::from_utf8_lossy(&line);
117
+ let _ = app.emit(
118
+ "sidecar-log",
119
+ serde_json::json!({
120
+ "stream": "stdout",
121
+ "line": text.trim_end()
122
+ }),
123
+ );
124
+ }
125
+ CommandEvent::Stderr(line) => {
126
+ let text = String::from_utf8_lossy(&line);
127
+ let _ = app.emit(
128
+ "sidecar-log",
129
+ serde_json::json!({
130
+ "stream": "stderr",
131
+ "line": text.trim_end()
132
+ }),
133
+ );
134
+ }
135
+ CommandEvent::Terminated(payload) => {
136
+ handle_sidecar_exit(&app, payload.code);
137
+ break;
138
+ }
139
+ CommandEvent::Error(msg) => {
140
+ let _ = app.emit(
141
+ "sidecar-log",
142
+ serde_json::json!({
143
+ "stream": "stderr",
144
+ "line": format!("[sidecar error] {}", msg)
145
+ }),
146
+ );
27
147
  }
148
+ _ => {}
28
149
  }
29
150
  }
151
+ });
152
+ }
153
+
154
+ /// Handle unexpected sidecar exit with auto-restart and exponential backoff.
155
+ fn handle_sidecar_exit(app: &tauri::AppHandle, exit_code: Option<i32>) {
156
+ let state = match app.try_state::<SidecarState>() {
157
+ Some(s) => s,
158
+ None => return,
159
+ };
160
+
161
+ // Don't restart during intentional shutdown
162
+ if state.shutting_down.load(Ordering::Relaxed) {
163
+ return;
30
164
  }
31
165
 
166
+ // Clear the dead child from state
167
+ if let Ok(mut guard) = state.child.lock() {
168
+ guard.take();
169
+ }
170
+
171
+ // Reset crash counter if server ran long enough (60s of stable operation)
172
+ if let Ok(guard) = state.last_start.lock() {
173
+ if let Some(start_time) = *guard {
174
+ if start_time.elapsed() >= STABLE_THRESHOLD {
175
+ state.crash_count.store(0, Ordering::Relaxed);
176
+ }
177
+ }
178
+ }
179
+
180
+ let crashes = state.crash_count.fetch_add(1, Ordering::Relaxed) + 1;
181
+
182
+ crate::health::set_disconnected();
183
+
184
+ let _ = app.emit(
185
+ "server-status",
186
+ serde_json::json!({
187
+ "connected": false,
188
+ "crashed": true,
189
+ "crashCount": crashes,
190
+ "exitCode": exit_code
191
+ }),
192
+ );
193
+
194
+ // Stop restarting after too many consecutive crashes
195
+ if crashes >= MAX_CRASH_COUNT {
196
+ let _ = app.emit(
197
+ "server-status",
198
+ serde_json::json!({
199
+ "connected": false,
200
+ "crashed": true,
201
+ "crashCount": crashes,
202
+ "fatal": true,
203
+ "message": "Server crashed too many times. Use View > Restart Server to try again."
204
+ }),
205
+ );
206
+ return;
207
+ }
208
+
209
+ // Schedule restart with exponential backoff
210
+ let delay = backoff_ms(crashes);
211
+ let app_clone = app.clone();
212
+ tauri::async_runtime::spawn(async move {
213
+ tokio::time::sleep(Duration::from_millis(delay)).await;
214
+
215
+ if let Some(st) = app_clone.try_state::<SidecarState>() {
216
+ if st.shutting_down.load(Ordering::Relaxed) {
217
+ return;
218
+ }
219
+ }
220
+
221
+ let result = start_sidecar(&app_clone);
222
+ if result.success {
223
+ if let Some(port) = result.port {
224
+ crate::health::set_connected(port);
225
+ }
226
+ }
227
+ });
228
+ }
229
+
230
+ /// Restart the sidecar — kills existing process, resets crash counter, starts fresh.
231
+ pub fn restart_sidecar(app: &tauri::AppHandle) -> StartResult {
232
+ kill_sidecar(app);
233
+
234
+ if let Some(state) = app.try_state::<SidecarState>() {
235
+ state.crash_count.store(0, Ordering::Relaxed);
236
+ state.shutting_down.store(false, Ordering::Relaxed);
237
+ }
238
+
239
+ start_sidecar(app)
240
+ }
241
+
242
+ /// Kill the running sidecar process (called on app exit or restart).
243
+ pub fn kill_sidecar(app: &tauri::AppHandle) {
244
+ if let Some(state) = app.try_state::<SidecarState>() {
245
+ state.shutting_down.store(true, Ordering::Relaxed);
246
+ if let Ok(mut guard) = state.child.lock() {
247
+ if let Some(child) = guard.take() {
248
+ let _ = child.kill();
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ // ── External CLI fallback ──
255
+
256
+ /// Find the `a2a` CLI binary on PATH
257
+ fn find_a2a_binary() -> Option<String> {
258
+ let result = Command::new("which").arg("a2a").output();
259
+ if let Ok(output) = result {
260
+ if output.status.success() {
261
+ let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
262
+ if !path.is_empty() {
263
+ return Some(path);
264
+ }
265
+ }
266
+ }
32
267
  None
33
268
  }
34
269
 
35
- /// Start the a2a server as a detached process
36
- pub fn start_server() -> StartResult {
270
+ /// Start the A2A server via external CLI (fallback when sidecar unavailable).
271
+ fn start_external_server(port: u16) -> StartResult {
37
272
  let binary = match find_a2a_binary() {
38
273
  Some(b) => b,
39
274
  None => {
40
275
  return StartResult {
41
276
  success: false,
42
- message: "Could not find 'a2a' CLI. Is a2acalling installed? Run: npm install -g a2acalling".to_string(),
277
+ message: "Could not find bundled sidecar or 'a2a' CLI. Is a2acalling installed? Run: npm install -g a2acalling".to_string(),
278
+ port: None,
279
+ source: "none".to_string(),
43
280
  };
44
281
  }
45
282
  };
46
283
 
47
- let port = crate::discovery::read_config_ports()
48
- .first()
49
- .copied()
50
- .unwrap_or(3001);
51
284
  let port_str = port.to_string();
52
-
53
285
  let result = Command::new(&binary)
54
286
  .args(["server", "--port", &port_str])
55
287
  .stdout(std::process::Stdio::null())
@@ -60,11 +292,15 @@ pub fn start_server() -> StartResult {
60
292
  match result {
61
293
  Ok(_child) => StartResult {
62
294
  success: true,
63
- message: format!("Server starting on port {}...", port),
295
+ message: format!("External server starting on port {}...", port),
296
+ port: Some(port),
297
+ source: "external".to_string(),
64
298
  },
65
299
  Err(err) => StartResult {
66
300
  success: false,
67
301
  message: format!("Failed to start server: {}", err),
302
+ port: None,
303
+ source: "none".to_string(),
68
304
  },
69
305
  }
70
306
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
3
3
  "productName": "A2A Callbook",
4
- "version": "0.1.0",
4
+ "version": "0.6.74",
5
5
  "identifier": "com.openclaw.a2a-callbook",
6
6
  "build": {
7
7
  "frontendDist": "../index.html"
@@ -20,7 +20,13 @@
20
20
  },
21
21
  "bundle": {
22
22
  "active": true,
23
- "targets": ["dmg", "app"],
23
+ "targets": [
24
+ "dmg",
25
+ "app"
26
+ ],
27
+ "externalBin": [
28
+ "binaries/a2a-server"
29
+ ],
24
30
  "icon": [
25
31
  "icons/32x32.png",
26
32
  "icons/128x128.png",
@@ -28,13 +34,16 @@
28
34
  ],
29
35
  "macOS": {
30
36
  "minimumSystemVersion": "12.0",
31
- "frameworks": []
37
+ "frameworks": [],
38
+ "entitlements": "entitlements.plist"
32
39
  }
33
40
  },
34
41
  "plugins": {
35
42
  "deep-link": {
36
43
  "desktop": {
37
- "schemes": ["a2a"]
44
+ "schemes": [
45
+ "a2a"
46
+ ]
38
47
  }
39
48
  }
40
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.74",
3
+ "version": "0.6.75",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,9 +11,12 @@
11
11
  "postinstall": "node scripts/postinstall.js",
12
12
  "start": "node src/server.js",
13
13
  "test": "node test/run.js",
14
+ "test:coverage": "c8 node test/run.js",
14
15
  "lint": "biome lint src/",
15
16
  "lint:check": "biome check src/",
16
- "knip": "knip"
17
+ "knip": "knip",
18
+ "build:standalone": "bash scripts/build-standalone.sh",
19
+ "build:sync-version": "bash scripts/sync-version.sh"
17
20
  },
18
21
  "keywords": [
19
22
  "openclaw",
@@ -41,7 +44,9 @@
41
44
  "express": "^4.21.0"
42
45
  },
43
46
  "devDependencies": {
47
+ "@yao-pkg/pkg": "^6.0.0",
44
48
  "@biomejs/biome": "^2.4.4",
49
+ "c8": "^11.0.0",
45
50
  "eslint": "^10.0.2",
46
51
  "eslint-plugin-sonarjs": "^4.0.0",
47
52
  "knip": "^5.85.0"
@@ -0,0 +1,14 @@
1
+ {
2
+ "pkg": {
3
+ "scripts": [
4
+ "src/**/*.js",
5
+ "bin/cli.js"
6
+ ],
7
+ "assets": [
8
+ "src/dashboard/public/**/*",
9
+ "node_modules/better-sqlite3/build/Release/better_sqlite3.node",
10
+ "node_modules/better-sqlite3/prebuilds/**/*"
11
+ ],
12
+ "outputPath": "native/macos/src-tauri/binaries"
13
+ }
14
+ }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # Build standalone Node.js binary for the A2A server using @yao-pkg/pkg.
3
+ # Usage:
4
+ # scripts/build-standalone.sh # Build for current arch
5
+ # scripts/build-standalone.sh --universal # Build macOS universal binary (aarch64 + x86_64)
6
+ # scripts/build-standalone.sh --arch arm64 # Build for specific arch
7
+ # scripts/build-standalone.sh --arch x64 # Build for specific arch
8
+ #
9
+ # Output: native/macos/src-tauri/binaries/a2a-server-<target-triple>
10
+ # Requires: Node.js 20+, npm, macOS (for native binary builds)
11
+
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
16
+ OUTPUT_DIR="$PROJECT_DIR/native/macos/src-tauri/binaries"
17
+ PKG_TARGET_NODE="node20"
18
+ BINARY_NAME="a2a-server"
19
+
20
+ cd "$PROJECT_DIR"
21
+
22
+ # ── Parse args ──
23
+ UNIVERSAL=false
24
+ TARGET_ARCH=""
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --universal) UNIVERSAL=true; shift ;;
28
+ --arch) TARGET_ARCH="$2"; shift 2 ;;
29
+ *) echo "Unknown option: $1" >&2; exit 2 ;;
30
+ esac
31
+ done
32
+
33
+ # ── Detect platform ──
34
+ OS="$(uname -s)"
35
+ if [[ "$OS" != "Darwin" ]]; then
36
+ echo "Warning: This build script is designed for macOS. Binary will target macOS regardless." >&2
37
+ fi
38
+
39
+ NATIVE_ARCH="$(uname -m)"
40
+ case "$NATIVE_ARCH" in
41
+ arm64|aarch64) NATIVE_ARCH="arm64" ;;
42
+ x86_64) NATIVE_ARCH="x64" ;;
43
+ *) echo "Unsupported architecture: $NATIVE_ARCH" >&2; exit 1 ;;
44
+ esac
45
+
46
+ # ── Ensure @yao-pkg/pkg is available ──
47
+ if ! npx --no-install pkg --version >/dev/null 2>&1; then
48
+ echo "Installing @yao-pkg/pkg..."
49
+ npm install --save-dev @yao-pkg/pkg
50
+ fi
51
+
52
+ # ── Ensure production dependencies are built ──
53
+ echo "Rebuilding native modules..."
54
+ npm rebuild better-sqlite3
55
+
56
+ # ── Build function ──
57
+ build_arch() {
58
+ local arch="$1"
59
+ local target="${PKG_TARGET_NODE}-macos-${arch}"
60
+ local triple
61
+
62
+ case "$arch" in
63
+ arm64) triple="aarch64-apple-darwin" ;;
64
+ x64) triple="x86_64-apple-darwin" ;;
65
+ *) echo "Unsupported arch: $arch" >&2; return 1 ;;
66
+ esac
67
+
68
+ local output_path="$OUTPUT_DIR/${BINARY_NAME}-${triple}"
69
+
70
+ echo "Building standalone binary: $target → $output_path"
71
+
72
+ npx pkg src/server.js \
73
+ --config pkg.config.json \
74
+ --target "$target" \
75
+ --output "$output_path" \
76
+ --compress GZip
77
+
78
+ chmod +x "$output_path"
79
+ echo "Built: $output_path ($(du -h "$output_path" | cut -f1))"
80
+ }
81
+
82
+ # ── Execute builds ──
83
+ mkdir -p "$OUTPUT_DIR"
84
+
85
+ if $UNIVERSAL; then
86
+ echo "=== Building macOS universal binary ==="
87
+ build_arch "arm64"
88
+ build_arch "x64"
89
+
90
+ ARM_BIN="$OUTPUT_DIR/${BINARY_NAME}-aarch64-apple-darwin"
91
+ X64_BIN="$OUTPUT_DIR/${BINARY_NAME}-x86_64-apple-darwin"
92
+ UNIVERSAL_BIN="$OUTPUT_DIR/${BINARY_NAME}-universal-apple-darwin"
93
+
94
+ echo "Creating universal binary with lipo..."
95
+ lipo -create "$ARM_BIN" "$X64_BIN" -output "$UNIVERSAL_BIN"
96
+ chmod +x "$UNIVERSAL_BIN"
97
+ echo "Universal: $UNIVERSAL_BIN ($(du -h "$UNIVERSAL_BIN" | cut -f1))"
98
+ elif [[ -n "$TARGET_ARCH" ]]; then
99
+ build_arch "$TARGET_ARCH"
100
+ else
101
+ build_arch "$NATIVE_ARCH"
102
+ fi
103
+
104
+ echo ""
105
+ echo "=== Build complete ==="
106
+ ls -lh "$OUTPUT_DIR"/${BINARY_NAME}-* 2>/dev/null || echo "(no binaries found)"
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # A2A-97: fast smoke coverage for packaged standalone server startup + SQLite-backed routes.
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
7
+ BINARY_PATH="${1:-}"
8
+
9
+ if [[ -z "$BINARY_PATH" ]]; then
10
+ for candidate in \
11
+ "$PROJECT_DIR/native/macos/src-tauri/binaries/a2a-server-universal-apple-darwin" \
12
+ "$PROJECT_DIR/native/macos/src-tauri/binaries/a2a-server-aarch64-apple-darwin" \
13
+ "$PROJECT_DIR/native/macos/src-tauri/binaries/a2a-server-x86_64-apple-darwin"
14
+ do
15
+ if [[ -x "$candidate" ]]; then
16
+ BINARY_PATH="$candidate"
17
+ break
18
+ fi
19
+ done
20
+ fi
21
+
22
+ if [[ -z "$BINARY_PATH" || ! -x "$BINARY_PATH" ]]; then
23
+ echo "Standalone binary not found or not executable: ${BINARY_PATH:-<unset>}" >&2
24
+ exit 1
25
+ fi
26
+
27
+ TMP_ROOT="$(mktemp -d)"
28
+ CONFIG_DIR="$TMP_ROOT/config"
29
+ LOG_FILE="$TMP_ROOT/server.log"
30
+ mkdir -p "$CONFIG_DIR"
31
+
32
+ PORT="$((
33
+ $(node -e "const net=require('net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")
34
+ ))"
35
+
36
+ PID=""
37
+ cleanup() {
38
+ if [[ -n "$PID" ]] && kill -0 "$PID" 2>/dev/null; then
39
+ kill "$PID" 2>/dev/null || true
40
+ wait "$PID" 2>/dev/null || true
41
+ fi
42
+ rm -rf "$TMP_ROOT"
43
+ }
44
+ trap cleanup EXIT
45
+
46
+ fail() {
47
+ echo "Smoke test failed: $1" >&2
48
+ if [[ -f "$LOG_FILE" ]]; then
49
+ echo "---- server log tail (last 50 lines) ----" >&2
50
+ tail -n 50 "$LOG_FILE" >&2 || true
51
+ fi
52
+ exit 1
53
+ }
54
+
55
+ A2A_RUNTIME=test \
56
+ NO_AUTO_UPDATE=1 \
57
+ A2A_CONFIG_DIR="$CONFIG_DIR" \
58
+ OPENCLAW_CONFIG_DIR="$CONFIG_DIR" \
59
+ "$BINARY_PATH" "$PORT" >"$LOG_FILE" 2>&1 &
60
+ PID="$!"
61
+
62
+ health_ok=0
63
+ for _ in $(seq 1 60); do
64
+ if curl -fsS "http://127.0.0.1:${PORT}/api/a2a/status" >/dev/null 2>&1; then
65
+ health_ok=1
66
+ break
67
+ fi
68
+ sleep 1
69
+ done
70
+
71
+ if [[ "$health_ok" -ne 1 ]]; then
72
+ fail "server did not become healthy within 60 seconds"
73
+ fi
74
+
75
+ # A2A-97: hit conversation list endpoint to exercise SQLite-backed dashboard storage path.
76
+ curl -fsS "http://127.0.0.1:${PORT}/api/a2a/dashboard/calls" >/dev/null \
77
+ || fail "dashboard calls endpoint failed (possible SQLite/native binding issue)"
78
+
79
+ create_resp="$(
80
+ curl -fsS -X POST "http://127.0.0.1:${PORT}/api/a2a/dashboard/invites" \
81
+ -H 'content-type: application/json' \
82
+ -d '{"name":"CI Smoke","tier":"public","expires":"1h"}'
83
+ )"
84
+
85
+ invite_id="$(node -e "const payload=JSON.parse(process.argv[1]); process.stdout.write(payload.token && payload.token.id ? payload.token.id : '');" "$create_resp")"
86
+ if [[ -z "$invite_id" ]]; then
87
+ fail "invite creation returned no token id"
88
+ fi
89
+
90
+ curl -fsS -X POST "http://127.0.0.1:${PORT}/api/a2a/dashboard/invites/${invite_id}/revoke" >/dev/null \
91
+ || fail "invite revoke failed"
92
+
93
+ if ! kill -0 "$PID" 2>/dev/null; then
94
+ fail "server exited unexpectedly during smoke checks"
95
+ fi
96
+
97
+ kill "$PID" 2>/dev/null || true
98
+ wait "$PID" 2>/dev/null || true
99
+ PID=""
100
+
101
+ echo "Standalone smoke test passed for ${BINARY_PATH}"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6
+ TAURI_CONF="$PROJECT_DIR/native/macos/src-tauri/tauri.conf.json"
7
+
8
+ PACKAGE_VERSION="$(node -p "require('$PROJECT_DIR/package.json').version")"
9
+
10
+ # A2A-95: force native bundle version to match npm package version for release parity.
11
+ node - "$TAURI_CONF" "$PACKAGE_VERSION" <<'NODE'
12
+ const fs = require('fs');
13
+ const tauriConfPath = process.argv[2];
14
+ const nextVersion = process.argv[3];
15
+
16
+ const raw = fs.readFileSync(tauriConfPath, 'utf8');
17
+ const config = JSON.parse(raw);
18
+ const prevVersion = config.version;
19
+
20
+ if (prevVersion === nextVersion) {
21
+ console.log(`Tauri version already in sync: ${nextVersion}`);
22
+ process.exit(0);
23
+ }
24
+
25
+ config.version = nextVersion;
26
+ fs.writeFileSync(tauriConfPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
27
+ console.log(`Updated tauri.conf.json version: ${prevVersion} -> ${nextVersion}`);
28
+ NODE
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # A2A-97: verify packaged app has sidecar + expected metadata before release upload.
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
7
+ APP_PATH="${1:-$PROJECT_DIR/native/macos/src-tauri/target/universal-apple-darwin/release/bundle/macos/A2A Callbook.app}"
8
+
9
+ fail() {
10
+ echo "App bundle verification failed: $1" >&2
11
+ if [[ -d "$APP_PATH/Contents/MacOS" ]]; then
12
+ echo "---- Contents/MacOS ----" >&2
13
+ ls -la "$APP_PATH/Contents/MacOS" >&2 || true
14
+ fi
15
+ exit 1
16
+ }
17
+
18
+ [[ -d "$APP_PATH" ]] || fail "missing app bundle at $APP_PATH"
19
+ [[ -f "$APP_PATH/Contents/Info.plist" ]] || fail "missing Info.plist"
20
+ [[ -f "$APP_PATH/Contents/Resources/icon.icns" ]] || fail "missing icon.icns"
21
+
22
+ if ! ls "$APP_PATH/Contents/MacOS"/a2a-server-* >/dev/null 2>&1; then
23
+ fail "missing bundled a2a-server sidecar binary"
24
+ fi
25
+
26
+ EXPECTED_VERSION="$(node -p "require('$PROJECT_DIR/package.json').version")"
27
+ ACTUAL_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PATH/Contents/Info.plist" 2>/dev/null || true)"
28
+ [[ -n "$ACTUAL_VERSION" ]] || fail "unable to read CFBundleShortVersionString"
29
+
30
+ if [[ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]]; then
31
+ fail "bundle version mismatch (expected $EXPECTED_VERSION, got $ACTUAL_VERSION)"
32
+ fi
33
+
34
+ echo "App bundle verification passed for $APP_PATH"