utem-cli 0.1.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/README.md +74 -0
- package/bin/utem.js +43 -0
- package/lib/install.js +120 -0
- package/lib/platform.js +114 -0
- package/package.json +50 -0
- package/scripts/lint.js +45 -0
- package/scripts/postinstall.js +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# utem-cli
|
|
2
|
+
|
|
3
|
+
npm distribution channel for the **UTEM CLI** (`utem`) — the control-plane
|
|
4
|
+
command-line interface for the [UTEM security platform](https://utem.innavoto.com).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
# one-off
|
|
8
|
+
npx utem-cli --help
|
|
9
|
+
|
|
10
|
+
# global install
|
|
11
|
+
npm i -g utem-cli
|
|
12
|
+
utem --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What this package is (and isn't)
|
|
16
|
+
|
|
17
|
+
The UTEM CLI itself is a **pure-Python** application published on PyPI as
|
|
18
|
+
[`utem`](https://pypi.org/project/utem/) (the wheel is
|
|
19
|
+
`utem-<version>-py3-none-any.whl` — architecture independent). It is also
|
|
20
|
+
distributed via Homebrew (`Innavoto/utem-homebrew-tap`).
|
|
21
|
+
|
|
22
|
+
This npm package is a **thin wrapper**, not a reimplementation. On install it:
|
|
23
|
+
|
|
24
|
+
1. Locates a Python 3.10+ interpreter on your `PATH`.
|
|
25
|
+
2. Creates an isolated virtualenv inside the installed package (`.venv/`).
|
|
26
|
+
3. `pip install`s the pinned `utem` release from PyPI into that venv.
|
|
27
|
+
|
|
28
|
+
The `utem` command shipped by this package then execs the CLI inside that
|
|
29
|
+
venv, forwarding all arguments, stdio and exit codes. Your global Python
|
|
30
|
+
environment is never modified.
|
|
31
|
+
|
|
32
|
+
### Requirement: Python 3.10+
|
|
33
|
+
|
|
34
|
+
Because the CLI is a Python program, this wrapper **requires Python 3.10 or
|
|
35
|
+
newer on your `PATH`** (the same requirement the Homebrew formula declares via
|
|
36
|
+
`depends_on "python3"`). If Python is missing, install fails soft with an
|
|
37
|
+
actionable message and the CLI is provisioned lazily on first `utem` run.
|
|
38
|
+
|
|
39
|
+
On Debian/Ubuntu you may also need `python3-venv` (`apt install python3-venv`).
|
|
40
|
+
|
|
41
|
+
### Design tradeoff
|
|
42
|
+
|
|
43
|
+
Two common patterns exist for shipping a CLI on npm:
|
|
44
|
+
|
|
45
|
+
| Approach | Used here? | Why / why not |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| **Prebuilt-binary downloader** (esbuild style: postinstall fetches a per-OS/arch native binary from GitHub Releases, checksum-verified) | No | The UTEM CLI has **no native binary** and **no per-platform GitHub release assets** — it is pure Python. There is nothing arch-specific to download. |
|
|
48
|
+
| **`optionalDependencies` per-platform packages** | No | Same reason — there are no platform-specific artifacts to split into sub-packages. |
|
|
49
|
+
| **PyPI-install shim** (provision the real package into an isolated venv) | **Yes** | The canonical artifact is a pure-Python wheel already on PyPI; installing it into an isolated venv is the simplest correct wrapper and requires no new binary build pipeline. |
|
|
50
|
+
|
|
51
|
+
If the CLI is later shipped as self-contained native binaries (e.g. via
|
|
52
|
+
PyInstaller) attached to versioned GitHub Releases, this wrapper can be
|
|
53
|
+
switched to the checksum-verified downloader model without changing its public
|
|
54
|
+
interface.
|
|
55
|
+
|
|
56
|
+
## Environment variables
|
|
57
|
+
|
|
58
|
+
| Variable | Effect |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `UTEM_SKIP_POSTINSTALL=1` | Skip provisioning during `npm install` (CI / air-gapped). The CLI is provisioned lazily on first run. |
|
|
61
|
+
|
|
62
|
+
CLI auth (`UTEM_API_URL`, `UTEM_TOKEN`, `UTEM_TENANT_ID`) is documented in the
|
|
63
|
+
[main CLI README](https://github.com/Innavoto/utem-cli#readme).
|
|
64
|
+
|
|
65
|
+
## Versioning
|
|
66
|
+
|
|
67
|
+
This wrapper's version tracks the `utem` PyPI release it installs
|
|
68
|
+
(currently **0.1.0**). Publishing a new wrapper version bumps the pinned CLI
|
|
69
|
+
version in lockstep.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Proprietary — © Innavoto India Pvt Ltd. See the
|
|
74
|
+
[main repository](https://github.com/Innavoto/utem-cli).
|
package/bin/utem.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `utem` launcher. Resolves the CLI provisioned into the package-local venv
|
|
6
|
+
* and execs it, forwarding argv, stdio, exit code and signals. If the venv
|
|
7
|
+
* isn't present yet (e.g. installed with --ignore-scripts), it provisions on
|
|
8
|
+
* first run.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
const { ensureInstalled } = require('../lib/install');
|
|
13
|
+
|
|
14
|
+
function fail(result) {
|
|
15
|
+
console.error('[utem] Unable to launch the UTEM CLI.');
|
|
16
|
+
console.error(`[utem] Reason: ${result.reason}`);
|
|
17
|
+
console.error(`[utem] ${result.detail}`);
|
|
18
|
+
process.exit(127);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = ensureInstalled({ silent: true });
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
// Retry once, this time surfacing installer output for diagnostics.
|
|
24
|
+
const verbose = ensureInstalled({ silent: false, force: true });
|
|
25
|
+
if (!verbose.ok) fail(verbose);
|
|
26
|
+
result.utemExe = verbose.utemExe;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const child = spawnSync(result.utemExe, process.argv.slice(2), {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (child.error) {
|
|
34
|
+
console.error(`[utem] Failed to execute ${result.utemExe}: ${child.error.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Propagate a terminating signal as the conventional 128+signal exit code.
|
|
39
|
+
if (child.signal) {
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.exit(child.status === null ? 1 : child.status);
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provision the UTEM CLI (`utem`) from PyPI into an isolated virtualenv that
|
|
5
|
+
* lives inside this installed npm package. Shared by the postinstall script
|
|
6
|
+
* and the lazy fallback in bin/utem.js (for `npm i --ignore-scripts`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
const {
|
|
13
|
+
PINNED_CLI_VERSION,
|
|
14
|
+
MIN_PYTHON,
|
|
15
|
+
pythonCandidates,
|
|
16
|
+
parsePythonVersion,
|
|
17
|
+
isSupportedPython,
|
|
18
|
+
venvLayout,
|
|
19
|
+
} = require('./platform');
|
|
20
|
+
|
|
21
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
22
|
+
|
|
23
|
+
function run(cmd, args, opts) {
|
|
24
|
+
return spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find the first Python interpreter on PATH that satisfies the CLI's minimum
|
|
29
|
+
* version (3.10+).
|
|
30
|
+
* @returns {{exe:string, version:{major:number,minor:number,patch:number}}|null}
|
|
31
|
+
*/
|
|
32
|
+
function findSupportedPython() {
|
|
33
|
+
for (const exe of pythonCandidates()) {
|
|
34
|
+
const res = run(exe, ['--version']);
|
|
35
|
+
if (res.error || res.status !== 0) continue;
|
|
36
|
+
const version = parsePythonVersion((res.stdout || '') + (res.stderr || ''));
|
|
37
|
+
if (isSupportedPython(version)) return { exe, version };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure the `utem` CLI is installed into the package-local venv.
|
|
44
|
+
* Never throws — always returns a result object so callers can decide how
|
|
45
|
+
* loudly to fail (postinstall warns; the bin shim errors hard).
|
|
46
|
+
*
|
|
47
|
+
* @param {{silent?:boolean, force?:boolean}} [opts]
|
|
48
|
+
* @returns {{ok:boolean, utemExe?:string, reason?:string, detail?:string}}
|
|
49
|
+
*/
|
|
50
|
+
function ensureInstalled(opts = {}) {
|
|
51
|
+
const { silent = false, force = false } = opts;
|
|
52
|
+
const layout = venvLayout(PKG_ROOT);
|
|
53
|
+
|
|
54
|
+
if (!force && fs.existsSync(layout.utemExe)) {
|
|
55
|
+
return { ok: true, utemExe: layout.utemExe };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const py = findSupportedPython();
|
|
59
|
+
if (!py) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
reason: 'no-python',
|
|
63
|
+
detail:
|
|
64
|
+
`No Python >= ${MIN_PYTHON.major}.${MIN_PYTHON.minor} found on PATH. ` +
|
|
65
|
+
`The UTEM CLI is a Python application; install Python 3.10+ ` +
|
|
66
|
+
`(https://www.python.org/downloads/) and reinstall, or install the CLI ` +
|
|
67
|
+
`directly with: pipx install utem==${PINNED_CLI_VERSION}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stdio = silent ? 'ignore' : 'inherit';
|
|
72
|
+
|
|
73
|
+
// 1. Create the isolated venv.
|
|
74
|
+
const mkvenv = run(py.exe, ['-m', 'venv', layout.venvDir], { stdio });
|
|
75
|
+
if (mkvenv.status !== 0) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: 'venv-failed',
|
|
79
|
+
detail:
|
|
80
|
+
`Failed to create a virtualenv with "${py.exe} -m venv". On Debian/Ubuntu ` +
|
|
81
|
+
`you may need the python3-venv package (apt install python3-venv).`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Install the pinned CLI from PyPI into the venv.
|
|
86
|
+
// --disable-pip-version-check keeps output clean; pin the exact version.
|
|
87
|
+
const pip = run(
|
|
88
|
+
layout.pythonExe,
|
|
89
|
+
[
|
|
90
|
+
'-m',
|
|
91
|
+
'pip',
|
|
92
|
+
'install',
|
|
93
|
+
'--disable-pip-version-check',
|
|
94
|
+
'--no-input',
|
|
95
|
+
`utem==${PINNED_CLI_VERSION}`,
|
|
96
|
+
],
|
|
97
|
+
{ stdio }
|
|
98
|
+
);
|
|
99
|
+
if (pip.status !== 0) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: 'pip-failed',
|
|
103
|
+
detail:
|
|
104
|
+
`pip failed to install utem==${PINNED_CLI_VERSION} from PyPI. Check network ` +
|
|
105
|
+
`access to pypi.org, then reinstall.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(layout.utemExe)) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
reason: 'entrypoint-missing',
|
|
113
|
+
detail: `Install completed but the utem entry point was not found at ${layout.utemExe}.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { ok: true, utemExe: layout.utemExe };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { ensureInstalled, findSupportedPython, PKG_ROOT };
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform / Python resolution for the UTEM CLI npm wrapper.
|
|
5
|
+
*
|
|
6
|
+
* The UTEM CLI (`utem`) is a *pure-Python* package published on PyPI as
|
|
7
|
+
* `utem` (wheel: utem-<version>-py3-none-any.whl — arch independent). This
|
|
8
|
+
* wrapper therefore does NOT download a per-arch native binary; instead it
|
|
9
|
+
* provisions the PyPI package into an isolated virtualenv co-located with the
|
|
10
|
+
* installed npm package, and shims the `utem` entry point to it.
|
|
11
|
+
*
|
|
12
|
+
* All functions here are pure and unit-testable with `node --test`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The UTEM CLI release the wrapper pins to. Kept in lockstep with the
|
|
20
|
+
* `version` in this package.json and the `utem` PyPI release. There is no
|
|
21
|
+
* GitHub *binary* release for this CLI (it is pure Python), so "the latest
|
|
22
|
+
* release tag" is the PyPI version 0.1.0.
|
|
23
|
+
*/
|
|
24
|
+
const PINNED_CLI_VERSION = '0.1.0';
|
|
25
|
+
|
|
26
|
+
/** Minimum Python the CLI supports (pyproject: requires-python >=3.10). */
|
|
27
|
+
const MIN_PYTHON = Object.freeze({ major: 3, minor: 10 });
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Candidate Python interpreter executables to probe, most-specific first.
|
|
31
|
+
* @param {string} [platform] process.platform override (for testing)
|
|
32
|
+
* @returns {string[]}
|
|
33
|
+
*/
|
|
34
|
+
function pythonCandidates(platform) {
|
|
35
|
+
const plat = platform || process.platform;
|
|
36
|
+
if (plat === 'win32') {
|
|
37
|
+
// `py` is the Windows launcher; `python` is the usual PATH name.
|
|
38
|
+
return ['python3', 'python', 'py'];
|
|
39
|
+
}
|
|
40
|
+
return ['python3', 'python'];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse the output of `python --version` (e.g. "Python 3.10.4").
|
|
45
|
+
* @param {string} out
|
|
46
|
+
* @returns {{major:number,minor:number,patch:number}|null}
|
|
47
|
+
*/
|
|
48
|
+
function parsePythonVersion(out) {
|
|
49
|
+
if (typeof out !== 'string') return null;
|
|
50
|
+
const m = out.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
51
|
+
if (!m) return null;
|
|
52
|
+
return {
|
|
53
|
+
major: Number(m[1]),
|
|
54
|
+
minor: Number(m[2]),
|
|
55
|
+
patch: m[3] ? Number(m[3]) : 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Is a parsed Python version >= the CLI's minimum (3.10)?
|
|
61
|
+
* @param {{major:number,minor:number}|null} v
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function isSupportedPython(v) {
|
|
65
|
+
if (!v) return false;
|
|
66
|
+
if (v.major !== MIN_PYTHON.major) return v.major > MIN_PYTHON.major;
|
|
67
|
+
return v.minor >= MIN_PYTHON.minor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compute the venv layout for a given package root + platform. Windows uses
|
|
72
|
+
* `Scripts\` and `.exe`; POSIX uses `bin/`.
|
|
73
|
+
* @param {string} pkgRoot absolute path to the installed npm package root
|
|
74
|
+
* @param {string} [platform] process.platform override (for testing)
|
|
75
|
+
* @returns {{venvDir:string,binDir:string,pythonExe:string,utemExe:string}}
|
|
76
|
+
*/
|
|
77
|
+
function venvLayout(pkgRoot, platform) {
|
|
78
|
+
const plat = platform || process.platform;
|
|
79
|
+
const venvDir = path.join(pkgRoot, '.venv');
|
|
80
|
+
if (plat === 'win32') {
|
|
81
|
+
const binDir = path.join(venvDir, 'Scripts');
|
|
82
|
+
return {
|
|
83
|
+
venvDir,
|
|
84
|
+
binDir,
|
|
85
|
+
pythonExe: path.join(binDir, 'python.exe'),
|
|
86
|
+
utemExe: path.join(binDir, 'utem.exe'),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const binDir = path.join(venvDir, 'bin');
|
|
90
|
+
return {
|
|
91
|
+
venvDir,
|
|
92
|
+
binDir,
|
|
93
|
+
pythonExe: path.join(binDir, 'python'),
|
|
94
|
+
utemExe: path.join(binDir, 'utem'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Human-readable platform/arch label for diagnostics.
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function platformLabel() {
|
|
103
|
+
return `${process.platform}/${process.arch} (node ${process.version}, ${os.release()})`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
PINNED_CLI_VERSION,
|
|
108
|
+
MIN_PYTHON,
|
|
109
|
+
pythonCandidates,
|
|
110
|
+
parsePythonVersion,
|
|
111
|
+
isSupportedPython,
|
|
112
|
+
venvLayout,
|
|
113
|
+
platformLabel,
|
|
114
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "utem-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "npm wrapper for the UTEM CLI (utem) — control plane for the UTEM security platform. Installs the official PyPI package into an isolated environment.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"utem",
|
|
7
|
+
"security",
|
|
8
|
+
"cli",
|
|
9
|
+
"sast",
|
|
10
|
+
"sca",
|
|
11
|
+
"code-security",
|
|
12
|
+
"innavoto"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Innavoto/utem-cli#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Innavoto/utem-cli/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "SEE LICENSE IN README.md",
|
|
19
|
+
"author": "Innavoto Technologies <engineering@innavoto.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Innavoto/utem-cli.git",
|
|
23
|
+
"directory": "npm"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"utem": "bin/utem.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"darwin",
|
|
33
|
+
"linux",
|
|
34
|
+
"win32"
|
|
35
|
+
],
|
|
36
|
+
"files": [
|
|
37
|
+
"bin/",
|
|
38
|
+
"lib/",
|
|
39
|
+
"scripts/",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"postinstall": "node scripts/postinstall.js",
|
|
44
|
+
"test": "node --test test/*.test.js",
|
|
45
|
+
"lint": "node scripts/lint.js"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/scripts/lint.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zero-dependency lint: syntax-checks every shipped/source .js file with
|
|
5
|
+
* `node --check`. Avoids pulling eslint (and a network install) into a thin
|
|
6
|
+
* wrapper package while still catching parse errors in CI and pre-publish.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(__dirname, '..');
|
|
14
|
+
const dirs = ['bin', 'lib', 'scripts', 'test'];
|
|
15
|
+
|
|
16
|
+
/** @returns {string[]} */
|
|
17
|
+
function collect() {
|
|
18
|
+
const files = [];
|
|
19
|
+
for (const d of dirs) {
|
|
20
|
+
const abs = path.join(root, d);
|
|
21
|
+
if (!fs.existsSync(abs)) continue;
|
|
22
|
+
for (const name of fs.readdirSync(abs)) {
|
|
23
|
+
if (name.endsWith('.js')) files.push(path.join(abs, name));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let failed = 0;
|
|
30
|
+
for (const file of collect()) {
|
|
31
|
+
const res = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' });
|
|
32
|
+
const rel = path.relative(root, file);
|
|
33
|
+
if (res.status === 0) {
|
|
34
|
+
console.log(`ok ${rel}`);
|
|
35
|
+
} else {
|
|
36
|
+
failed += 1;
|
|
37
|
+
console.error(`FAIL ${rel}\n${res.stderr}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (failed > 0) {
|
|
42
|
+
console.error(`\n${failed} file(s) failed lint.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
console.log('\nAll files passed node --check.');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* postinstall: provision the UTEM CLI from PyPI into a package-local venv.
|
|
5
|
+
*
|
|
6
|
+
* Design choice: this NEVER hard-fails the enclosing `npm install`. A failed
|
|
7
|
+
* postinstall (e.g. no Python on PATH) exits 0 with a loud, actionable
|
|
8
|
+
* warning; the `utem` bin shim will retry the install lazily on first run and
|
|
9
|
+
* error clearly if it still can't provision. This keeps `npm i -g` from
|
|
10
|
+
* aborting a user's whole toolchain over an optional shim.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { PINNED_CLI_VERSION, platformLabel } = require('../lib/platform');
|
|
14
|
+
const { ensureInstalled } = require('../lib/install');
|
|
15
|
+
|
|
16
|
+
// Escape hatches for CI / air-gapped installs.
|
|
17
|
+
if (process.env.UTEM_SKIP_POSTINSTALL === '1') {
|
|
18
|
+
console.log('[utem] UTEM_SKIP_POSTINSTALL=1 set — skipping CLI provisioning.');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(
|
|
23
|
+
`[utem] Provisioning UTEM CLI ${PINNED_CLI_VERSION} from PyPI on ${platformLabel()} ...`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const result = ensureInstalled({ silent: false });
|
|
27
|
+
|
|
28
|
+
if (result.ok) {
|
|
29
|
+
console.log(`[utem] Installed. Run "utem --help" to get started.`);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Warn, but do not fail the install.
|
|
34
|
+
console.warn('');
|
|
35
|
+
console.warn('[utem] WARNING: could not provision the UTEM CLI during install.');
|
|
36
|
+
console.warn(`[utem] Reason: ${result.reason}`);
|
|
37
|
+
console.warn(`[utem] ${result.detail}`);
|
|
38
|
+
console.warn(
|
|
39
|
+
'[utem] The first time you run `utem`, provisioning will be retried automatically.'
|
|
40
|
+
);
|
|
41
|
+
console.warn('');
|
|
42
|
+
process.exit(0);
|