@backtomyfuture/exchange-cli 0.2.4 → 0.2.6

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execFileSync } = require('child_process');
3
+ const { spawn } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
@@ -16,32 +16,65 @@ const PLATFORM_PACKAGES = {
16
16
  const platformKey = `${process.platform}-${process.arch}`;
17
17
  const ext = process.platform === 'win32' ? '.exe' : '';
18
18
 
19
+ function renderError(message, code = 'BINARY_NOT_FOUND', exitCode = 1) {
20
+ const isText =
21
+ process.argv.includes('--format=text') ||
22
+ (process.argv.indexOf('--format') !== -1 &&
23
+ process.argv[process.argv.indexOf('--format') + 1] === 'text');
24
+ if (isText) {
25
+ console.error(`Error [${code}]: ${message}`);
26
+ } else {
27
+ console.log(
28
+ JSON.stringify({
29
+ ok: false,
30
+ error: message,
31
+ code: code,
32
+ retryable: false,
33
+ })
34
+ );
35
+ }
36
+ process.exit(exitCode);
37
+ }
38
+
19
39
  function getBinaryPath() {
20
40
  if (process.env.EXCHANGE_CLI_BINARY) {
21
- return process.env.EXCHANGE_CLI_BINARY;
41
+ const customBin = process.env.EXCHANGE_CLI_BINARY;
42
+ if (!fs.existsSync(customBin)) {
43
+ renderError(
44
+ `exchange-cli: binary not found at EXCHANGE_CLI_BINARY: ${customBin}`,
45
+ 'BINARY_NOT_FOUND',
46
+ 1
47
+ );
48
+ }
49
+ return customBin;
22
50
  }
23
51
 
24
52
  const pkg = PLATFORM_PACKAGES[platformKey];
25
53
  if (!pkg) {
26
- console.error(`exchange-cli: unsupported platform ${platformKey}`);
27
- process.exit(1);
54
+ renderError(`exchange-cli: unsupported platform ${platformKey}`, 'PLATFORM_NOT_SUPPORTED', 1);
28
55
  }
29
56
 
30
57
  try {
31
58
  return require.resolve(`${pkg}/bin/exchange-cli${ext}`);
32
59
  } catch {
33
- const modPath = path.join(
34
- path.dirname(require.resolve(`${pkg}/package.json`)),
35
- `bin/exchange-cli${ext}`
36
- );
37
- if (fs.existsSync(modPath)) {
38
- return modPath;
60
+ try {
61
+ const modPath = path.join(
62
+ path.dirname(require.resolve(`${pkg}/package.json`)),
63
+ `bin/exchange-cli${ext}`
64
+ );
65
+ if (fs.existsSync(modPath)) {
66
+ return modPath;
67
+ }
68
+ } catch {
69
+ // Ignore require failure
39
70
  }
40
71
  }
41
72
 
42
- console.error(`exchange-cli: binary not found for ${platformKey}`);
43
- console.error('Try: npm install --force @backtomyfuture/exchange-cli');
44
- process.exit(1);
73
+ renderError(
74
+ `exchange-cli: binary not found for ${platformKey}. Try: npm install --force @backtomyfuture/exchange-cli`,
75
+ 'BINARY_NOT_FOUND',
76
+ 1
77
+ );
45
78
  }
46
79
 
47
80
  try {
@@ -50,13 +83,48 @@ try {
50
83
  const { ensureDarwinArm64RuntimeLayout } = require('./runtime-layout');
51
84
  ensureDarwinArm64RuntimeLayout(binaryPath);
52
85
  }
53
- execFileSync(binaryPath, process.argv.slice(2), {
86
+
87
+ const child = spawn(binaryPath, process.argv.slice(2), {
54
88
  stdio: 'inherit',
55
89
  env: { ...process.env },
56
90
  });
91
+
92
+ child.on('error', (err) => {
93
+ renderError(
94
+ `exchange-cli: failed to spawn binary: ${err.message}`,
95
+ 'BINARY_NOT_FOUND',
96
+ 1
97
+ );
98
+ });
99
+
100
+ const forwardSignal = (signal) => {
101
+ if (child && !child.killed && child.pid) {
102
+ try {
103
+ child.kill(signal);
104
+ } catch {
105
+ // Child already exited
106
+ }
107
+ }
108
+ };
109
+
110
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
111
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
112
+ process.on('SIGHUP', () => forwardSignal('SIGHUP'));
113
+
114
+ child.on('exit', (code, signal) => {
115
+ if (signal) {
116
+ process.removeAllListeners('SIGINT');
117
+ process.removeAllListeners('SIGTERM');
118
+ process.removeAllListeners('SIGHUP');
119
+ try {
120
+ process.kill(process.pid, signal);
121
+ } catch {
122
+ process.exit(128 + 15);
123
+ }
124
+ } else {
125
+ process.exit(code ?? 0);
126
+ }
127
+ });
57
128
  } catch (e) {
58
- if (e && e.status != null) {
59
- process.exit(e.status);
60
- }
61
- throw e;
129
+ renderError(`exchange-cli wrapper error: ${e.message || e}`, 'WRAPPER_ERROR', 1);
62
130
  }
@@ -30,15 +30,54 @@ function copyDirRecursive(srcDir, dstDir) {
30
30
  }
31
31
  }
32
32
 
33
- function copyIfMissing(src, dst, logger) {
33
+ let inMemoryLayoutChecked = false;
34
+
35
+ 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
+ try {
39
+ fs.linkSync(src, dst);
40
+ return;
41
+ } catch {
42
+ // Fall back to relative symlink
43
+ try {
44
+ const rel = path.relative(path.dirname(dst), src);
45
+ fs.symlinkSync(rel, dst);
46
+ return;
47
+ } catch {
48
+ // Fall back to file copy
49
+ fs.copyFileSync(src, dst);
50
+ }
51
+ }
52
+ try {
53
+ const stat = fs.statSync(src);
54
+ fs.chmodSync(dst, stat.mode & 0o777);
55
+ } catch {
56
+ // Best effort only.
57
+ }
58
+ }
59
+
60
+ function linkOrCopyDir(srcDir, dstDir) {
61
+ fs.mkdirSync(path.dirname(dstDir), { recursive: true });
62
+ // Try symlink first for directories (like Versions/Current -> 3.12)
63
+ try {
64
+ const rel = path.relative(path.dirname(dstDir), srcDir);
65
+ fs.symlinkSync(rel, dstDir, 'junction');
66
+ return;
67
+ } catch {
68
+ copyDirRecursive(srcDir, dstDir);
69
+ }
70
+ }
71
+
72
+ function linkOrCopyIfMissing(src, dst, logger) {
34
73
  if (fs.existsSync(dst) || !fs.existsSync(src)) {
35
74
  return false;
36
75
  }
37
76
  const stat = fs.statSync(src);
38
77
  if (stat.isDirectory()) {
39
- copyDirRecursive(src, dst);
78
+ linkOrCopyDir(src, dst);
40
79
  } else {
41
- copyFileWithMode(src, dst);
80
+ linkOrCopyFile(src, dst);
42
81
  }
43
82
  if (logger) {
44
83
  logger(`exchange-cli: repaired missing runtime path ${path.basename(dst)}`);
@@ -70,17 +109,29 @@ function resolveFrameworkVersionDir(internalDir) {
70
109
  }
71
110
 
72
111
  function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
112
+ if (inMemoryLayoutChecked) {
113
+ return { changed: false };
114
+ }
73
115
  if (!(process.platform === 'darwin' && process.arch === 'arm64')) {
116
+ inMemoryLayoutChecked = true;
74
117
  return { changed: false };
75
118
  }
76
119
  const binDir = path.dirname(binaryPath);
77
120
  const internalDir = path.join(binDir, '_internal');
78
121
  if (!fs.existsSync(internalDir)) {
122
+ inMemoryLayoutChecked = true;
123
+ return { changed: false };
124
+ }
125
+
126
+ const markerPath = path.join(internalDir, '.runtime_layout_ok');
127
+ if (fs.existsSync(markerPath)) {
128
+ inMemoryLayoutChecked = true;
79
129
  return { changed: false };
80
130
  }
81
131
 
82
132
  const frameworkVersionDir = resolveFrameworkVersionDir(internalDir);
83
133
  if (!frameworkVersionDir) {
134
+ inMemoryLayoutChecked = true;
84
135
  return { changed: false };
85
136
  }
86
137
  const sourcePython = path.join(frameworkVersionDir, 'Python');
@@ -98,7 +149,7 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
98
149
 
99
150
  let changed = false;
100
151
  for (const target of targets) {
101
- changed = copyIfMissing(target.src, target.dst, logger) || changed;
152
+ changed = linkOrCopyIfMissing(target.src, target.dst, logger) || changed;
102
153
  }
103
154
 
104
155
  try {
@@ -106,6 +157,14 @@ function ensureDarwinArm64RuntimeLayout(binaryPath, logger = null) {
106
157
  } catch {
107
158
  // Best effort only.
108
159
  }
160
+
161
+ try {
162
+ fs.writeFileSync(markerPath, '');
163
+ } catch {
164
+ // Best effort only.
165
+ }
166
+
167
+ inMemoryLayoutChecked = true;
109
168
  return { changed };
110
169
  }
111
170
 
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "@backtomyfuture/exchange-cli",
3
- "version": "0.2.4",
4
- "description": "On-premises Exchange Server CLI for email, calendar, tasks, and contacts.",
3
+ "version": "0.2.6",
4
+ "description": "Cross-platform CLI for on-premises Microsoft Exchange Server",
5
5
  "bin": {
6
- "exchange-cli": "bin/exchange-cli.js"
6
+ "exchange-cli": "./bin/exchange-cli.js"
7
7
  },
8
8
  "scripts": {
9
- "postinstall": "node install.js"
9
+ "postinstall": "node ./install.js"
10
10
  },
11
11
  "files": [
12
12
  "bin/",
13
13
  "install.js"
14
14
  ],
15
15
  "optionalDependencies": {
16
- "@backtomyfuture/exchange-cli-darwin-arm64": "0.2.4",
17
- "@backtomyfuture/exchange-cli-darwin-x64": "0.2.4",
18
- "@backtomyfuture/exchange-cli-linux-x64": "0.2.4",
19
- "@backtomyfuture/exchange-cli-linux-arm64": "0.2.4",
20
- "@backtomyfuture/exchange-cli-win32-x64": "0.2.4",
21
- "@backtomyfuture/exchange-cli-win32-ia32": "0.2.4"
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"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=14"