@backtomyfuture/exchange-cli 0.2.6 → 0.2.8

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);
@@ -33,39 +59,47 @@ function copyDirRecursive(srcDir, dstDir) {
33
59
  let inMemoryLayoutChecked = false;
34
60
 
35
61
  function linkOrCopyFile(src, dst) {
36
- fs.mkdirSync(path.dirname(dst), { recursive: true });
37
- // Try hardlink first (fast, zero extra disk space, works on same filesystem)
38
62
  try {
39
- fs.linkSync(src, dst);
40
- return;
41
- } catch {
42
- // Fall back to relative symlink
63
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
64
+ // Try hardlink first (fast, zero extra disk space, works on same filesystem)
43
65
  try {
44
- const rel = path.relative(path.dirname(dst), src);
45
- fs.symlinkSync(rel, dst);
66
+ fs.linkSync(src, dst);
46
67
  return;
47
68
  } catch {
48
- // Fall back to file copy
49
- fs.copyFileSync(src, dst);
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.
50
84
  }
51
- }
52
- try {
53
- const stat = fs.statSync(src);
54
- fs.chmodSync(dst, stat.mode & 0o777);
55
85
  } catch {
56
- // Best effort only.
86
+ // Best effort only: dst dir may be read-only.
57
87
  }
58
88
  }
59
89
 
60
90
  function linkOrCopyDir(srcDir, dstDir) {
61
- fs.mkdirSync(path.dirname(dstDir), { recursive: true });
62
- // Try symlink first for directories (like Versions/Current -> 3.12)
63
91
  try {
64
- const rel = path.relative(path.dirname(dstDir), srcDir);
65
- fs.symlinkSync(rel, dstDir, 'junction');
66
- return;
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
+ }
67
101
  } catch {
68
- copyDirRecursive(srcDir, dstDir);
102
+ // Best effort only: dst dir may be read-only.
69
103
  }
70
104
  }
71
105
 
@@ -73,16 +107,23 @@ function linkOrCopyIfMissing(src, dst, logger) {
73
107
  if (fs.existsSync(dst) || !fs.existsSync(src)) {
74
108
  return false;
75
109
  }
76
- const stat = fs.statSync(src);
77
- if (stat.isDirectory()) {
78
- linkOrCopyDir(src, dst);
79
- } else {
80
- linkOrCopyFile(src, dst);
81
- }
82
- if (logger) {
83
- 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;
84
126
  }
85
- return true;
86
127
  }
87
128
 
88
129
  function resolveFrameworkVersionDir(internalDir) {
@@ -124,7 +165,8 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
124
165
  }
125
166
 
126
167
  const markerPath = path.join(internalDir, '.runtime_layout_ok');
127
- if (fs.existsSync(markerPath)) {
168
+ const userCacheMarkerPath = getUserCacheMarkerPath(internalDir);
169
+ if (fs.existsSync(markerPath) || fs.existsSync(userCacheMarkerPath)) {
128
170
  inMemoryLayoutChecked = true;
129
171
  return { changed: false };
130
172
  }
@@ -158,10 +200,21 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
158
200
  // Best effort only.
159
201
  }
160
202
 
203
+ let markerWritten = false;
161
204
  try {
162
205
  fs.writeFileSync(markerPath, '');
206
+ markerWritten = true;
163
207
  } catch {
164
- // 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
+ }
165
218
  }
166
219
 
167
220
  inMemoryLayoutChecked = true;
@@ -170,4 +223,5 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
170
223
 
171
224
  module.exports = {
172
225
  ensureDarwinArm64RuntimeLayout,
226
+ getUserCacheMarkerPath,
173
227
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtomyfuture/exchange-cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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.6",
17
- "@backtomyfuture/exchange-cli-darwin-x64": "0.2.6",
18
- "@backtomyfuture/exchange-cli-linux-x64": "0.2.6",
19
- "@backtomyfuture/exchange-cli-linux-arm64": "0.2.6",
20
- "@backtomyfuture/exchange-cli-win32-x64": "0.2.6",
21
- "@backtomyfuture/exchange-cli-win32-ia32": "0.2.6"
16
+ "@backtomyfuture/exchange-cli-darwin-arm64": "0.2.8",
17
+ "@backtomyfuture/exchange-cli-darwin-x64": "0.2.8",
18
+ "@backtomyfuture/exchange-cli-linux-x64": "0.2.8",
19
+ "@backtomyfuture/exchange-cli-linux-arm64": "0.2.8",
20
+ "@backtomyfuture/exchange-cli-win32-x64": "0.2.8",
21
+ "@backtomyfuture/exchange-cli-win32-ia32": "0.2.8"
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": {