@aware-aeco/cli 0.89.1 → 0.91.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aware-aeco/cli",
3
- "version": "0.89.1",
3
+ "version": "0.91.0",
4
4
  "description": "AWARE CLI — open-source agentic substrate for AECO",
5
5
  "homepage": "https://github.com/aware-aeco/aware",
6
6
  "repository": {
@@ -1,9 +1,45 @@
1
1
  #!/usr/bin/env node
2
+ const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { spawnSync } = require('child_process');
4
5
 
6
+ const PKG_VERSION = require('../../package.json').version;
7
+ const { requiredFiles } = require('../payload');
5
8
  const binaryName = process.platform === 'win32' ? 'aware.exe' : 'aware';
6
- const binary = path.join(__dirname, '..', '..', 'binaries', binaryName);
9
+ const binariesDir = path.join(__dirname, '..', '..', 'binaries');
10
+ let binary = path.join(binariesDir, binaryName);
11
+
12
+ if (!fs.existsSync(binary)) {
13
+ // Rescue a partial install: postinstall ≤0.90.0 could leave the extracted
14
+ // versioned folder unpromoted (#287) — run the binary from there. Pinned to
15
+ // this package's version so a leftover from another release never runs, and
16
+ // only a COMPLETE payload qualifies — the CLI resolves its sidecar/roslyn
17
+ // companions next to the executable, so a half-extracted folder would run
18
+ // but fail confusingly later.
19
+ const exeSuffix = process.platform === 'win32' ? '.exe' : '';
20
+ const complete = (dir) =>
21
+ requiredFiles(exeSuffix).every((f) => fs.existsSync(path.join(dir, f)));
22
+ const candidate = fs.existsSync(binariesDir)
23
+ ? fs.readdirSync(binariesDir)
24
+ .filter((name) => name.startsWith(`aware-${PKG_VERSION}-`))
25
+ .map((name) => path.join(binariesDir, name))
26
+ .filter(complete)
27
+ .map((dir) => path.join(dir, binaryName))[0]
28
+ : undefined;
29
+ if (candidate) {
30
+ binary = candidate;
31
+ } else {
32
+ // Never fail silently (#287) — say what is missing and how to repair it.
33
+ console.error(`aware: binary not found at ${binary}`);
34
+ console.error('The npm postinstall step did not complete on this machine. To repair, run:');
35
+ console.error(` node "${path.join(__dirname, '..', 'postinstall.js')}"`);
36
+ console.error('or download a release manually: https://github.com/aware-aeco/aware/releases');
37
+ process.exit(1);
38
+ }
39
+ }
7
40
 
8
41
  const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
42
+ if (result.error) {
43
+ console.error(`aware: failed to run ${binary}: ${result.error.message}`);
44
+ }
9
45
  process.exit(result.status ?? 1);
@@ -0,0 +1,23 @@
1
+ // Shared payload contract for postinstall.js and bin/aware.js (#287).
2
+ //
3
+ // The files without which the CLI (or its aware-roslyn companion host) cannot
4
+ // start. aware-roslyn ships as a self-contained FOLDER (hundreds of framework
5
+ // DLLs); exhaustively manifesting it is disproportionate here — these four are
6
+ // the startup-critical set (apphost, app dll, runtime config, deps graph), and
7
+ // the extraction paths themselves fail loudly on any entry error, so a payload
8
+ // passing this check with an arbitrary missing middle DLL requires the
9
+ // extractor to have lied about its exit code.
10
+ const path = require('path');
11
+
12
+ function requiredFiles(exeSuffix) {
13
+ return [
14
+ `aware${exeSuffix}`,
15
+ `aware-sidecar${exeSuffix}`,
16
+ path.join('aware-roslyn', `aware-roslyn${exeSuffix}`),
17
+ path.join('aware-roslyn', 'aware-roslyn.dll'),
18
+ path.join('aware-roslyn', 'aware-roslyn.runtimeconfig.json'),
19
+ path.join('aware-roslyn', 'aware-roslyn.deps.json'),
20
+ ];
21
+ }
22
+
23
+ module.exports = { requiredFiles };
@@ -2,7 +2,8 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const https = require('https');
5
- const { execSync } = require('child_process');
5
+ const { execFileSync } = require('child_process');
6
+ const { requiredFiles } = require('./payload');
6
7
 
7
8
  const PKG_VERSION = require('../package.json').version;
8
9
  const REPO = 'aware-aeco/aware';
@@ -61,22 +62,73 @@ function download(srcUrl, dest, depth = 0) {
61
62
  await download(url, tmpFile);
62
63
 
63
64
  console.log('[aware-npm] extracting...');
64
- if (target.archive === 'zip') {
65
- execSync(`powershell -NoProfile -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${binariesDir}' -Force"`, { stdio: 'inherit' });
65
+ // bsdtar handles both .zip and .tar.gz, exits non-zero on real failures, and
66
+ // (unlike PS 5.1 Expand-Archive) never half-extracts silently (#287). Use the
67
+ // System32 copy on Windows — a GNU tar earlier on PATH (Git Bash) can't read zip.
68
+ const winTar = path.join(process.env.WINDIR || 'C:\\Windows', 'System32', 'tar.exe');
69
+ if (process.platform === 'win32' && !fs.existsSync(winTar)) {
70
+ // Server 2016 / Win10 <1803 ship no inbox bsdtar — fall back to
71
+ // Expand-Archive. Its silent half-extraction mode is defused by the
72
+ // shim-target check below, which turns it into a loud failure (#287).
73
+ // $ErrorActionPreference='Stop' promotes Expand-Archive's non-terminating
74
+ // extraction errors to a non-zero exit, so a partial payload throws here
75
+ // instead of surfacing later.
76
+ execFileSync('powershell', [
77
+ '-NoProfile', '-Command',
78
+ `$ErrorActionPreference='Stop'; Expand-Archive -LiteralPath '${tmpFile}' -DestinationPath '${binariesDir}' -Force`,
79
+ ], { stdio: 'inherit' });
66
80
  } else {
67
- execSync(`tar -xzf "${tmpFile}" -C "${binariesDir}"`, { stdio: 'inherit' });
81
+ const tar = process.platform === 'win32' ? winTar : 'tar';
82
+ // Explicit -z for tar.gz: minimal POSIX tar builds don't autodetect gzip.
83
+ const flags = target.archive === 'zip' ? '-xf' : '-xzf';
84
+ execFileSync(tar, [flags, tmpFile, '-C', binariesDir], { stdio: 'inherit' });
68
85
  }
69
- fs.unlinkSync(tmpFile);
86
+ // Cleanup is cosmetic — an AV scan holding the fresh archive must not abort
87
+ // an install that already extracted successfully (#287).
88
+ try { fs.unlinkSync(tmpFile); } catch { /* locked archive; leave it */ }
70
89
 
71
90
  // Promote binaries from <binariesDir>/aware-<version>-<rid>/ to <binariesDir>/
72
91
  const extractedDir = path.join(binariesDir, `aware-${PKG_VERSION}-${target.rid}`);
73
92
  if (fs.existsSync(extractedDir)) {
74
93
  for (const file of fs.readdirSync(extractedDir)) {
75
- fs.renameSync(path.join(extractedDir, file), path.join(binariesDir, file));
94
+ const src = path.join(extractedDir, file);
95
+ const dest = path.join(binariesDir, file);
96
+ // renameSync replaces existing FILES atomically (MOVEFILE_REPLACE_EXISTING
97
+ // on Windows); only a directory dest (aware-roslyn) must be moved aside
98
+ // first. Keep it until the replacement lands so an interrupted repair
99
+ // can't destroy a working install (#287 review).
100
+ let backup = null;
101
+ if (fs.existsSync(dest) && fs.statSync(dest).isDirectory()) {
102
+ backup = `${dest}.old-${process.pid}`;
103
+ fs.rmSync(backup, { recursive: true, force: true });
104
+ fs.renameSync(dest, backup);
105
+ }
106
+ try {
107
+ fs.renameSync(src, dest);
108
+ } catch (err) {
109
+ if (backup) fs.renameSync(backup, dest); // restore the old payload
110
+ throw err;
111
+ }
112
+ // The replacement already landed — a locked obsolete backup (AV handle)
113
+ // must not abort the remaining promotions.
114
+ if (backup) {
115
+ try { fs.rmSync(backup, { recursive: true, force: true }); } catch { /* best-effort */ }
116
+ }
76
117
  }
77
118
  fs.rmdirSync(extractedDir);
78
119
  }
79
120
 
121
+ // The shim resolves <binariesDir>/aware(.exe), and the CLI needs its
122
+ // sidecar + roslyn companions next to it. Anything missing is a broken
123
+ // install — fail loudly here instead of letting every later `aware` call
124
+ // exit 1 in silence (#287). Also catches Expand-Archive's silent
125
+ // half-extraction on the no-inbox-tar fallback path.
126
+ const required = requiredFiles(target.ext);
127
+ const missing = required.filter((f) => !fs.existsSync(path.join(binariesDir, f)));
128
+ if (missing.length) {
129
+ throw new Error(`extraction finished but ${missing.join(', ')} missing from ${binariesDir}`);
130
+ }
131
+
80
132
  if (process.platform !== 'win32') {
81
133
  fs.chmodSync(path.join(binariesDir, `aware${target.ext}`), 0o755);
82
134
  fs.chmodSync(path.join(binariesDir, `aware-sidecar${target.ext}`), 0o755);