@aionui/officecli 1.0.102 → 1.0.122-test.1

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/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # @aionui/officecli
1
+ # officecli
2
2
 
3
- Placeholder package. Work in progress.
3
+ CLI for reading and writing Office documents (`.docx`, `.xlsx`, `.pptx`) via a
4
+ document DOM API.
5
+
6
+ ```bash
7
+ npm install -g @officecli/officecli
8
+ # or run without installing:
9
+ npx @officecli/officecli --help
10
+ ```
11
+
12
+ On install, the native binary for your platform (macOS / Linux / Windows,
13
+ x64 / arm64) is downloaded from the official release mirror
14
+ (`d.officecli.ai`, with GitHub Releases as a fallback) and verified against
15
+ its published `SHA256SUMS`. macOS builds are Developer ID signed and notarized.
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ officecli create report.docx
21
+ officecli add report.docx /body --type paragraph --prop text="Hello"
22
+ officecli get report.docx '/body/p[1]'
23
+ officecli --help
24
+ ```
25
+
26
+ ## Notes
27
+
28
+ - Supported platforms: macOS (arm64/x64), Linux glibc & musl/Alpine
29
+ (arm64/x64), Windows (arm64/x64).
30
+ - Set `OFFICECLI_SKIP_BINARY_DOWNLOAD=1` to skip the download during
31
+ `npm install` (the binary is then fetched on first run).
32
+ - Source, issues and full docs:
33
+ <https://github.com/iOfficeAI/OfficeCLI>
34
+
35
+ Licensed under Apache-2.0.
package/install.js ADDED
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ // postinstall entry point. Downloads the platform binary up-front so the first
4
+ // `officecli` call is instant. A failure here is non-fatal: the bin shim
5
+ // (bin/officecli.js) lazily downloads on first run, so an offline/proxied
6
+ // install still leaves a working command once connectivity returns. Set
7
+ // OFFICECLI_SKIP_BINARY_DOWNLOAD=1 to skip the download entirely.
8
+
9
+ if (process.env.OFFICECLI_SKIP_BINARY_DOWNLOAD) {
10
+ process.stderr.write('[officecli] OFFICECLI_SKIP_BINARY_DOWNLOAD set, skipping binary download.\n');
11
+ process.exit(0);
12
+ }
13
+
14
+ require('./lib/install-binary')
15
+ .ensureBinary()
16
+ .catch(function (err) {
17
+ process.stderr.write('[officecli] postinstall could not fetch the binary: ' + err.message + '\n');
18
+ process.stderr.write('[officecli] it will be downloaded on first run instead.\n');
19
+ // Exit 0 so `npm install` succeeds; the shim retries lazily.
20
+ process.exit(0);
21
+ });
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ // Shared installer logic for the @officecli/officecli (and @aionui/officecli)
4
+ // npm packages. The package itself ships no native code: on install it fetches
5
+ // the platform binary from the SAME release mirror install.sh uses
6
+ // (d.officecli.ai primary, GitHub releases fallback), pinned to the IMMUTABLE
7
+ // versioned path so a freshly-published release never collides with a CDN-cached
8
+ // `latest`. Asset names and the mirror/fallback order mirror install.sh exactly.
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const https = require('https');
14
+ const crypto = require('crypto');
15
+ const { execSync } = require('child_process');
16
+
17
+ const REPO = 'iOfficeAI/OfficeCLI';
18
+ const MIRROR_BASE = 'https://d.officecli.ai';
19
+ const GITHUB_BASE = 'https://github.com/' + REPO;
20
+
21
+ // The package version is set to the release version at publish time, so the
22
+ // release tag we download from is derived directly from it (immutable, never
23
+ // stale). A prerelease/build suffix (e.g. 1.0.122-test.1) maps to the same
24
+ // binary release v1.0.122 — strip everything after the first '-' or '+'.
25
+ const VERSION = require('../package.json').version;
26
+ const TAG = 'v' + VERSION.split('+')[0].split('-')[0];
27
+
28
+ const PKG_ROOT = path.join(__dirname, '..');
29
+ // Native binary lives under vendor/, NOT bin/: the repo's root .gitignore
30
+ // ignores `bin/`, and keeping the download target out of bin/ avoids any
31
+ // collision with the launcher shim.
32
+ const BIN_DIR = path.join(PKG_ROOT, 'vendor');
33
+
34
+ function log(msg) {
35
+ // postinstall output goes to stderr so it never pollutes a command's stdout.
36
+ process.stderr.write('[officecli] ' + msg + '\n');
37
+ }
38
+
39
+ // musl detection, mirroring install.sh's gnu-vs-musl branch. process.report
40
+ // exposes glibcVersionRuntime on a glibc system; its absence (plus the Alpine
41
+ // marker / `ldd` text) means musl.
42
+ function isMusl() {
43
+ if (process.platform !== 'linux') return false;
44
+ try {
45
+ const report = process.report && process.report.getReport();
46
+ const header = report && report.header;
47
+ if (header && header.glibcVersionRuntime) return false;
48
+ if (header && header.glibcVersionRuntime === undefined) {
49
+ // No glibc runtime reported — treat as musl, but confirm below.
50
+ }
51
+ } catch (_) { /* fall through to filesystem/ldd probes */ }
52
+ try {
53
+ if (fs.existsSync('/etc/alpine-release')) return true;
54
+ } catch (_) { /* ignore */ }
55
+ try {
56
+ const out = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });
57
+ if (/musl/i.test(out)) return true;
58
+ } catch (_) { /* ignore */ }
59
+ // Default to glibc when nothing positively indicates musl.
60
+ return false;
61
+ }
62
+
63
+ function detectAsset() {
64
+ const platform = process.platform;
65
+ const arch = process.arch;
66
+ if (platform === 'darwin') {
67
+ if (arch === 'arm64') return 'officecli-mac-arm64';
68
+ if (arch === 'x64') return 'officecli-mac-x64';
69
+ } else if (platform === 'linux') {
70
+ const musl = isMusl();
71
+ if (arch === 'x64') return musl ? 'officecli-linux-alpine-x64' : 'officecli-linux-x64';
72
+ if (arch === 'arm64') return musl ? 'officecli-linux-alpine-arm64' : 'officecli-linux-arm64';
73
+ } else if (platform === 'win32') {
74
+ if (arch === 'x64') return 'officecli-win-x64.exe';
75
+ if (arch === 'arm64') return 'officecli-win-arm64.exe';
76
+ }
77
+ throw new Error(
78
+ 'Unsupported platform: ' + platform + ' ' + arch +
79
+ '. Download manually from ' + GITHUB_BASE + '/releases'
80
+ );
81
+ }
82
+
83
+ function binaryName() {
84
+ return process.platform === 'win32' ? 'officecli.exe' : 'officecli';
85
+ }
86
+
87
+ function binaryPath() {
88
+ return path.join(BIN_DIR, binaryName());
89
+ }
90
+
91
+ function assetUrls(asset) {
92
+ // Mirror first (issues surface fast), GitHub fallback — same order as
93
+ // install.sh. Both use the immutable /releases/download/<tag>/ path.
94
+ return [
95
+ MIRROR_BASE + '/releases/download/' + TAG + '/' + asset,
96
+ GITHUB_BASE + '/releases/download/' + TAG + '/' + asset
97
+ ];
98
+ }
99
+
100
+ function sumsUrls() {
101
+ return [
102
+ MIRROR_BASE + '/releases/download/' + TAG + '/SHA256SUMS',
103
+ GITHUB_BASE + '/releases/download/' + TAG + '/SHA256SUMS'
104
+ ];
105
+ }
106
+
107
+ function httpGet(url, onResponse, onError, redirects) {
108
+ redirects = redirects || 0;
109
+ if (redirects > 10) {
110
+ onError(new Error('Too many redirects for ' + url));
111
+ return;
112
+ }
113
+ const req = https.get(
114
+ url,
115
+ { headers: { 'User-Agent': 'officecli-npm-installer' } },
116
+ function (res) {
117
+ const code = res.statusCode;
118
+ if (code >= 300 && code < 400 && res.headers.location) {
119
+ res.resume();
120
+ const next = new URL(res.headers.location, url).toString();
121
+ httpGet(next, onResponse, onError, redirects + 1);
122
+ return;
123
+ }
124
+ if (code !== 200) {
125
+ res.resume();
126
+ onError(new Error('HTTP ' + code + ' for ' + url));
127
+ return;
128
+ }
129
+ onResponse(res);
130
+ }
131
+ );
132
+ req.on('error', onError);
133
+ req.setTimeout(300000, function () {
134
+ req.destroy(new Error('Timeout downloading ' + url));
135
+ });
136
+ }
137
+
138
+ function fetchToFile(url, dest) {
139
+ return new Promise(function (resolve, reject) {
140
+ httpGet(
141
+ url,
142
+ function (res) {
143
+ const tmp = dest + '.download';
144
+ const out = fs.createWriteStream(tmp);
145
+ res.pipe(out);
146
+ out.on('error', reject);
147
+ out.on('finish', function () {
148
+ out.close(function () {
149
+ try {
150
+ fs.renameSync(tmp, dest);
151
+ resolve();
152
+ } catch (e) {
153
+ reject(e);
154
+ }
155
+ });
156
+ });
157
+ },
158
+ reject
159
+ );
160
+ });
161
+ }
162
+
163
+ function fetchBuffer(url) {
164
+ return new Promise(function (resolve, reject) {
165
+ httpGet(
166
+ url,
167
+ function (res) {
168
+ const chunks = [];
169
+ res.on('data', function (c) { chunks.push(c); });
170
+ res.on('end', function () { resolve(Buffer.concat(chunks)); });
171
+ res.on('error', reject);
172
+ },
173
+ reject
174
+ );
175
+ });
176
+ }
177
+
178
+ async function verifyChecksum(asset, file) {
179
+ let sums = null;
180
+ for (const url of sumsUrls()) {
181
+ try {
182
+ sums = (await fetchBuffer(url)).toString('utf8');
183
+ break;
184
+ } catch (_) { /* try next source */ }
185
+ }
186
+ if (!sums) {
187
+ log(' SHA256SUMS not available, skipping checksum verification.');
188
+ return;
189
+ }
190
+ // SHA256SUMS rows are "<hex> <name>" (sha256sum text mode). Match the
191
+ // filename column EXACTLY (a leading '*' marks binary mode), never a
192
+ // substring — same rule as install.sh / the C# self-updater.
193
+ let expected = null;
194
+ for (const line of sums.split('\n')) {
195
+ const parts = line.trim().split(/\s+/);
196
+ if (parts.length >= 2) {
197
+ const name = parts[1].replace(/^\*/, '');
198
+ if (name === asset) { expected = parts[0]; break; }
199
+ }
200
+ }
201
+ if (!expected) {
202
+ log(' ' + asset + ' not listed in SHA256SUMS, skipping verification.');
203
+ return;
204
+ }
205
+ const actual = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
206
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
207
+ throw new Error('Checksum mismatch for ' + asset + ' (expected ' + expected + ', got ' + actual + ')');
208
+ }
209
+ log(' checksum verified.');
210
+ }
211
+
212
+ // Download the platform binary into bin/ if it is not already present.
213
+ // Idempotent: a non-empty binary is treated as already installed (the package
214
+ // version pins the release, so existence is sufficient).
215
+ async function ensureBinary() {
216
+ const dest = binaryPath();
217
+ if (fs.existsSync(dest) && fs.statSync(dest).size > 0) {
218
+ return dest;
219
+ }
220
+ fs.mkdirSync(BIN_DIR, { recursive: true });
221
+ const asset = detectAsset();
222
+ let lastErr = null;
223
+ for (const url of assetUrls(asset)) {
224
+ try {
225
+ log('Downloading ' + asset + ' (' + TAG + ') from ' + url + ' ...');
226
+ await fetchToFile(url, dest);
227
+ await verifyChecksum(asset, dest);
228
+ if (process.platform !== 'win32') {
229
+ fs.chmodSync(dest, 0o755);
230
+ }
231
+ log('OfficeCLI ' + VERSION + ' installed.');
232
+ return dest;
233
+ } catch (e) {
234
+ lastErr = e;
235
+ try { fs.rmSync(dest, { force: true }); } catch (_) { /* ignore */ }
236
+ log(' failed: ' + e.message);
237
+ }
238
+ }
239
+ throw new Error(
240
+ 'Could not download OfficeCLI binary (' + asset + ' @ ' + TAG + '). ' +
241
+ 'Last error: ' + (lastErr && lastErr.message) +
242
+ '. Install manually from ' + GITHUB_BASE + '/releases'
243
+ );
244
+ }
245
+
246
+ module.exports = {
247
+ ensureBinary: ensureBinary,
248
+ binaryPath: binaryPath,
249
+ detectAsset: detectAsset,
250
+ VERSION: VERSION,
251
+ TAG: TAG
252
+ };
package/officecli.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Launcher shim. npm links this as the `officecli` command. It execs the native
5
+ // binary fetched by postinstall, forwarding argv, stdio and the exit code. If
6
+ // the binary is missing (postinstall was skipped or failed offline), it is
7
+ // downloaded lazily on this first run.
8
+ //
9
+ // This lives at the package root, NOT under bin/, on purpose: the repo's root
10
+ // .gitignore ignores `bin/`, which would silently drop a bin/ shim from the
11
+ // published tarball.
12
+
13
+ const fs = require('fs');
14
+ const { spawnSync } = require('child_process');
15
+ const installer = require('./lib/install-binary');
16
+
17
+ async function main() {
18
+ let bin = installer.binaryPath();
19
+ if (!fs.existsSync(bin)) {
20
+ try {
21
+ bin = await installer.ensureBinary();
22
+ } catch (err) {
23
+ process.stderr.write('[officecli] ' + err.message + '\n');
24
+ process.exit(1);
25
+ }
26
+ }
27
+ const res = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' });
28
+ if (res.error) {
29
+ process.stderr.write('[officecli] failed to launch binary: ' + res.error.message + '\n');
30
+ process.exit(1);
31
+ }
32
+ // Signal-terminated child: surface a non-zero exit rather than a null status.
33
+ if (res.signal) {
34
+ process.exit(1);
35
+ }
36
+ process.exit(res.status === null ? 1 : res.status);
37
+ }
38
+
39
+ main();
package/package.json CHANGED
@@ -1,7 +1,50 @@
1
1
  {
2
2
  "name": "@aionui/officecli",
3
- "version": "1.0.102",
4
- "description": "OfficeCli — placeholder, work in progress.",
3
+ "version": "1.0.122-test.1",
4
+ "description": "OfficeCli — CLI for reading and writing Office documents (.docx, .xlsx, .pptx) via a document DOM API. The native binary is fetched on install for your platform.",
5
5
  "license": "Apache-2.0",
6
- "author": "goworm"
6
+ "author": "goworm",
7
+ "homepage": "https://github.com/iOfficeAI/OfficeCLI",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/iOfficeAI/OfficeCLI.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/iOfficeAI/OfficeCLI/issues"
14
+ },
15
+ "keywords": [
16
+ "office",
17
+ "docx",
18
+ "xlsx",
19
+ "pptx",
20
+ "word",
21
+ "excel",
22
+ "powerpoint",
23
+ "ooxml",
24
+ "cli"
25
+ ],
26
+ "bin": {
27
+ "officecli": "officecli.js"
28
+ },
29
+ "scripts": {
30
+ "postinstall": "node install.js"
31
+ },
32
+ "files": [
33
+ "officecli.js",
34
+ "lib/install-binary.js",
35
+ "install.js",
36
+ "README.md"
37
+ ],
38
+ "os": [
39
+ "darwin",
40
+ "linux",
41
+ "win32"
42
+ ],
43
+ "cpu": [
44
+ "x64",
45
+ "arm64"
46
+ ],
47
+ "engines": {
48
+ "node": ">=14"
49
+ }
7
50
  }