@backtomyfuture/exchange-cli 0.2.5 → 0.2.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.
@@ -1,9 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawn } = require('child_process');
4
+ const crypto = require('crypto');
4
5
  const fs = require('fs');
5
6
  const path = require('path');
6
7
 
8
+ const startTime = process.hrtime.bigint ? process.hrtime.bigint() : Date.now();
9
+
10
+ function getRequestId() {
11
+ for (let i = 2; i < process.argv.length; i++) {
12
+ const arg = process.argv[i];
13
+ if (arg.startsWith('--request-id=')) {
14
+ const val = arg.slice('--request-id='.length).trim();
15
+ if (val) return val;
16
+ }
17
+ if (arg === '--request-id' && i + 1 < process.argv.length) {
18
+ const val = process.argv[i + 1].trim();
19
+ if (val) return val;
20
+ }
21
+ }
22
+ if (typeof crypto.randomUUID === 'function') {
23
+ return crypto.randomUUID();
24
+ }
25
+ return '00000000-0000-0000-0000-000000000000';
26
+ }
27
+
7
28
  const PLATFORM_PACKAGES = {
8
29
  'darwin-arm64': '@backtomyfuture/exchange-cli-darwin-arm64',
9
30
  'darwin-x64': '@backtomyfuture/exchange-cli-darwin-x64',
@@ -24,12 +45,21 @@ function renderError(message, code = 'BINARY_NOT_FOUND', exitCode = 1) {
24
45
  if (isText) {
25
46
  console.error(`Error [${code}]: ${message}`);
26
47
  } else {
48
+ const requestId = getRequestId();
49
+ const elapsedMs = process.hrtime.bigint
50
+ ? Number(process.hrtime.bigint() - startTime) / 1e6
51
+ : Date.now() - startTime;
27
52
  console.log(
28
53
  JSON.stringify({
29
54
  ok: false,
30
55
  error: message,
31
56
  code: code,
32
57
  retryable: false,
58
+ request_id: requestId,
59
+ meta: {
60
+ request_id: requestId,
61
+ elapsed_ms: Math.round(elapsedMs * 100) / 100,
62
+ },
33
63
  })
34
64
  );
35
65
  }
@@ -46,6 +76,15 @@ function getBinaryPath() {
46
76
  1
47
77
  );
48
78
  }
79
+ try {
80
+ fs.accessSync(customBin, fs.constants.X_OK);
81
+ } catch {
82
+ renderError(
83
+ `exchange-cli: binary at EXCHANGE_CLI_BINARY is not executable: ${customBin}`,
84
+ 'BINARY_SPAWN_FAILED',
85
+ 1
86
+ );
87
+ }
49
88
  return customBin;
50
89
  }
51
90
 
@@ -92,7 +131,7 @@ try {
92
131
  child.on('error', (err) => {
93
132
  renderError(
94
133
  `exchange-cli: failed to spawn binary: ${err.message}`,
95
- 'BINARY_NOT_FOUND',
134
+ 'BINARY_SPAWN_FAILED',
96
135
  1
97
136
  );
98
137
  });
@@ -2,9 +2,35 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ const crypto = require('crypto');
5
6
  const fs = require('fs');
7
+ const os = require('os');
6
8
  const path = require('path');
7
9
 
10
+ function getUserCacheMarkerPath(internalDir) {
11
+ const homeDir = os.homedir();
12
+ let cacheBase;
13
+ if (process.platform === 'darwin') {
14
+ cacheBase = path.join(homeDir, 'Library', 'Caches', 'exchange-cli');
15
+ } else if (process.platform === 'win32') {
16
+ cacheBase = path.join(
17
+ process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
18
+ 'exchange-cli'
19
+ );
20
+ } else {
21
+ cacheBase = path.join(
22
+ process.env.XDG_CACHE_HOME || path.join(homeDir, '.cache'),
23
+ 'exchange-cli'
24
+ );
25
+ }
26
+ const hash = crypto
27
+ .createHash('sha256')
28
+ .update(path.resolve(internalDir))
29
+ .digest('hex')
30
+ .slice(0, 16);
31
+ return path.join(cacheBase, `runtime_layout_${hash}.ok`);
32
+ }
33
+
8
34
  function copyFileWithMode(src, dst) {
9
35
  fs.mkdirSync(path.dirname(dst), { recursive: true });
10
36
  fs.copyFileSync(src, dst);
@@ -30,20 +56,74 @@ function copyDirRecursive(srcDir, dstDir) {
30
56
  }
31
57
  }
32
58
 
33
- function copyIfMissing(src, dst, logger) {
59
+ let inMemoryLayoutChecked = false;
60
+
61
+ function linkOrCopyFile(src, dst) {
62
+ try {
63
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
64
+ // Try hardlink first (fast, zero extra disk space, works on same filesystem)
65
+ try {
66
+ fs.linkSync(src, dst);
67
+ return;
68
+ } catch {
69
+ // Fall back to relative symlink
70
+ try {
71
+ const rel = path.relative(path.dirname(dst), src);
72
+ fs.symlinkSync(rel, dst);
73
+ return;
74
+ } catch {
75
+ // Fall back to file copy
76
+ fs.copyFileSync(src, dst);
77
+ }
78
+ }
79
+ try {
80
+ const stat = fs.statSync(src);
81
+ fs.chmodSync(dst, stat.mode & 0o777);
82
+ } catch {
83
+ // Best effort only.
84
+ }
85
+ } catch {
86
+ // Best effort only: dst dir may be read-only.
87
+ }
88
+ }
89
+
90
+ function linkOrCopyDir(srcDir, dstDir) {
91
+ try {
92
+ fs.mkdirSync(path.dirname(dstDir), { recursive: true });
93
+ // Try symlink first for directories (like Versions/Current -> 3.12)
94
+ try {
95
+ const rel = path.relative(path.dirname(dstDir), srcDir);
96
+ fs.symlinkSync(rel, dstDir, 'junction');
97
+ return;
98
+ } catch {
99
+ copyDirRecursive(srcDir, dstDir);
100
+ }
101
+ } catch {
102
+ // Best effort only: dst dir may be read-only.
103
+ }
104
+ }
105
+
106
+ function linkOrCopyIfMissing(src, dst, logger) {
34
107
  if (fs.existsSync(dst) || !fs.existsSync(src)) {
35
108
  return false;
36
109
  }
37
- const stat = fs.statSync(src);
38
- if (stat.isDirectory()) {
39
- copyDirRecursive(src, dst);
40
- } else {
41
- copyFileWithMode(src, dst);
42
- }
43
- if (logger) {
44
- logger(`exchange-cli: repaired missing runtime path ${path.basename(dst)}`);
110
+ try {
111
+ const stat = fs.statSync(src);
112
+ if (stat.isDirectory()) {
113
+ linkOrCopyDir(src, dst);
114
+ } else {
115
+ linkOrCopyFile(src, dst);
116
+ }
117
+ if (fs.existsSync(dst)) {
118
+ if (logger) {
119
+ logger(`exchange-cli: repaired missing runtime path ${path.basename(dst)}`);
120
+ }
121
+ return true;
122
+ }
123
+ return false;
124
+ } catch {
125
+ return false;
45
126
  }
46
- return true;
47
127
  }
48
128
 
49
129
  function resolveFrameworkVersionDir(internalDir) {
@@ -70,22 +150,30 @@ function resolveFrameworkVersionDir(internalDir) {
70
150
  }
71
151
 
72
152
  function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
153
+ if (inMemoryLayoutChecked) {
154
+ return { changed: false };
155
+ }
73
156
  if (!(process.platform === 'darwin' && process.arch === 'arm64')) {
157
+ inMemoryLayoutChecked = true;
74
158
  return { changed: false };
75
159
  }
76
160
  const binDir = path.dirname(binaryPath);
77
161
  const internalDir = path.join(binDir, '_internal');
78
162
  if (!fs.existsSync(internalDir)) {
163
+ inMemoryLayoutChecked = true;
79
164
  return { changed: false };
80
165
  }
81
166
 
82
167
  const markerPath = path.join(internalDir, '.runtime_layout_ok');
83
- if (fs.existsSync(markerPath)) {
168
+ const userCacheMarkerPath = getUserCacheMarkerPath(internalDir);
169
+ if (fs.existsSync(markerPath) || fs.existsSync(userCacheMarkerPath)) {
170
+ inMemoryLayoutChecked = true;
84
171
  return { changed: false };
85
172
  }
86
173
 
87
174
  const frameworkVersionDir = resolveFrameworkVersionDir(internalDir);
88
175
  if (!frameworkVersionDir) {
176
+ inMemoryLayoutChecked = true;
89
177
  return { changed: false };
90
178
  }
91
179
  const sourcePython = path.join(frameworkVersionDir, 'Python');
@@ -103,7 +191,7 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
103
191
 
104
192
  let changed = false;
105
193
  for (const target of targets) {
106
- changed = copyIfMissing(target.src, target.dst, logger) || changed;
194
+ changed = linkOrCopyIfMissing(target.src, target.dst, logger) || changed;
107
195
  }
108
196
 
109
197
  try {
@@ -112,15 +200,28 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
112
200
  // Best effort only.
113
201
  }
114
202
 
203
+ let markerWritten = false;
115
204
  try {
116
205
  fs.writeFileSync(markerPath, '');
206
+ markerWritten = true;
117
207
  } catch {
118
- // Best effort only.
208
+ // Best effort only: internalDir may be read-only in system installations.
209
+ }
210
+
211
+ if (!markerWritten) {
212
+ try {
213
+ fs.mkdirSync(path.dirname(userCacheMarkerPath), { recursive: true });
214
+ fs.writeFileSync(userCacheMarkerPath, '');
215
+ } catch {
216
+ // Best effort only.
217
+ }
119
218
  }
120
219
 
220
+ inMemoryLayoutChecked = true;
121
221
  return { changed };
122
222
  }
123
223
 
124
224
  module.exports = {
125
225
  ensureDarwinArm64RuntimeLayout,
226
+ getUserCacheMarkerPath,
126
227
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtomyfuture/exchange-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Cross-platform CLI for on-premises Microsoft Exchange Server",
5
5
  "bin": {
6
6
  "exchange-cli": "./bin/exchange-cli.js"
@@ -13,17 +13,24 @@
13
13
  "install.js"
14
14
  ],
15
15
  "optionalDependencies": {
16
- "@backtomyfuture/exchange-cli-darwin-arm64": "0.2.5",
17
- "@backtomyfuture/exchange-cli-darwin-x64": "0.2.5",
18
- "@backtomyfuture/exchange-cli-linux-x64": "0.2.5",
19
- "@backtomyfuture/exchange-cli-linux-arm64": "0.2.5",
20
- "@backtomyfuture/exchange-cli-win32-x64": "0.2.5",
21
- "@backtomyfuture/exchange-cli-win32-ia32": "0.2.5"
16
+ "@backtomyfuture/exchange-cli-darwin-arm64": "0.2.7",
17
+ "@backtomyfuture/exchange-cli-darwin-x64": "0.2.7",
18
+ "@backtomyfuture/exchange-cli-linux-x64": "0.2.7",
19
+ "@backtomyfuture/exchange-cli-linux-arm64": "0.2.7",
20
+ "@backtomyfuture/exchange-cli-win32-x64": "0.2.7",
21
+ "@backtomyfuture/exchange-cli-win32-ia32": "0.2.7"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=14"
25
25
  },
26
- "keywords": ["exchange", "exchange-server", "ews", "cli", "email", "ai-agent"],
26
+ "keywords": [
27
+ "exchange",
28
+ "exchange-server",
29
+ "ews",
30
+ "cli",
31
+ "email",
32
+ "ai-agent"
33
+ ],
27
34
  "license": "Apache-2.0",
28
35
  "homepage": "https://github.com/backtomyfuture/exchange-cli#readme",
29
36
  "repository": {