graphonomous 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 +210 -0
- package/bin/graphonomous.js +158 -0
- package/package.json +50 -0
- package/scripts/download-release-asset.js +274 -0
- package/scripts/postinstall.js +221 -0
- package/scripts/resolve-platform.js +79 -0
- package/vendor/.gitkeep +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# graphonomous (npm wrapper)
|
|
2
|
+
|
|
3
|
+
This package provides an npm-friendly launcher for the Graphonomous MCP server CLI.
|
|
4
|
+
|
|
5
|
+
It installs (or reuses) a platform-specific `graphonomous` binary and exposes:
|
|
6
|
+
|
|
7
|
+
- `graphonomous ...`
|
|
8
|
+
- `npx graphonomous ...`
|
|
9
|
+
|
|
10
|
+
The underlying server communicates over **STDIO**, so it works well with MCP-capable editors/clients (for example Zed custom context servers).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this package does
|
|
15
|
+
|
|
16
|
+
- Detects your OS/arch (`darwin|linux` + `x64|arm64`)
|
|
17
|
+
- Downloads a matching release asset at install time
|
|
18
|
+
- Installs the OTP release command path under `vendor/<platform>-<arch>/graphonomous/bin/graphonomous` when available
|
|
19
|
+
- Creates/uses `vendor/<platform>-<arch>/graphonomous` as the launcher target for consistent execution
|
|
20
|
+
- Runs the resolved Graphonomous command with all arguments passed through
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Node.js `>= 18`
|
|
27
|
+
- Supported platforms:
|
|
28
|
+
- macOS: `x64`, `arm64`
|
|
29
|
+
- Linux: `x64`, `arm64`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
### Global install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm i -g graphonomous
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then run:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
graphonomous --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### One-off execution
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npx -y graphonomous --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Local project install
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm i graphonomous
|
|
57
|
+
npx graphonomous --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Run examples
|
|
63
|
+
|
|
64
|
+
Start Graphonomous MCP server with a local DB path:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
graphonomous --db ~/.graphonomous/knowledge.db --embedder-backend fallback
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Safe laptop-oriented defaults:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
graphonomous \
|
|
74
|
+
--db ~/.graphonomous/knowledge.db \
|
|
75
|
+
--embedder-backend fallback \
|
|
76
|
+
--log-level info
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Zed configuration example
|
|
82
|
+
|
|
83
|
+
In Zed settings JSON:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"context_servers": {
|
|
88
|
+
"graphonomous": {
|
|
89
|
+
"command": "graphonomous",
|
|
90
|
+
"args": ["--db", "~/.graphonomous/knowledge.db", "--embedder-backend", "fallback"],
|
|
91
|
+
"env": {
|
|
92
|
+
"GRAPHONOMOUS_EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If you prefer not to install globally:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"context_servers": {
|
|
104
|
+
"graphonomous": {
|
|
105
|
+
"command": "npx",
|
|
106
|
+
"args": ["-y", "graphonomous", "--db", "~/.graphonomous/knowledge.db", "--embedder-backend", "fallback"],
|
|
107
|
+
"env": {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Release asset override instructions
|
|
116
|
+
|
|
117
|
+
The installer supports override environment variables for custom repos/tags/asset hosting.
|
|
118
|
+
|
|
119
|
+
### Override GitHub owner/repo/tag
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
GRAPHONOMOUS_GITHUB_OWNER=my-org \
|
|
123
|
+
GRAPHONOMOUS_GITHUB_REPO=graphonomous \
|
|
124
|
+
GRAPHONOMOUS_RELEASE_TAG=v0.1.0 \
|
|
125
|
+
npm i graphonomous
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Override version used for asset naming
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
GRAPHONOMOUS_VERSION=0.1.0 npm i graphonomous
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Use custom release base URL (bypass GitHub release URL construction)
|
|
135
|
+
|
|
136
|
+
`GRAPHONOMOUS_RELEASE_BASE_URL` should point to a directory containing assets named like:
|
|
137
|
+
|
|
138
|
+
`graphonomous-v<version>-<platform>-<arch>.tar.gz`
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
GRAPHONOMOUS_RELEASE_BASE_URL=https://downloads.example.com/graphonomous \
|
|
144
|
+
GRAPHONOMOUS_VERSION=0.1.0 \
|
|
145
|
+
npm i graphonomous
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Private release download token
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
GRAPHONOMOUS_GITHUB_TOKEN=ghp_xxx npm i graphonomous
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
(You can also use `GITHUB_TOKEN`.)
|
|
155
|
+
|
|
156
|
+
### Skip, force, and tune download behavior
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
# Skip download entirely
|
|
160
|
+
GRAPHONOMOUS_SKIP_DOWNLOAD=1 npm i graphonomous
|
|
161
|
+
|
|
162
|
+
# Force re-download even if binary exists
|
|
163
|
+
GRAPHONOMOUS_FORCE_DOWNLOAD=1 npm i graphonomous
|
|
164
|
+
|
|
165
|
+
# Timeout and redirect controls
|
|
166
|
+
GRAPHONOMOUS_DOWNLOAD_TIMEOUT_MS=120000 \
|
|
167
|
+
GRAPHONOMOUS_DOWNLOAD_MAX_REDIRECTS=10 \
|
|
168
|
+
npm i graphonomous
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Runtime command override
|
|
174
|
+
|
|
175
|
+
You can bypass installed vendor binaries/release layout and point directly to a custom executable:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
GRAPHONOMOUS_BINARY_PATH=/absolute/path/to/graphonomous graphonomous --help
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Troubleshooting
|
|
184
|
+
|
|
185
|
+
### Binary not found after install
|
|
186
|
+
Try reinstalling or rebuilding:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
npm rebuild graphonomous
|
|
190
|
+
# or
|
|
191
|
+
npm i graphonomous@latest
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Unsupported platform message
|
|
195
|
+
Current prebuilt targets are Linux/macOS + x64/arm64.
|
|
196
|
+
|
|
197
|
+
### Permission issue on command path
|
|
198
|
+
Reinstall the package, or manually set executable bit on unix-like systems:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
chmod +x node_modules/graphonomous/vendor/<target>/graphonomous
|
|
202
|
+
chmod +x node_modules/graphonomous/vendor/<target>/graphonomous/bin/graphonomous
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Source of truth
|
|
208
|
+
|
|
209
|
+
The npm package is a distribution wrapper around the Graphonomous Elixir CLI.
|
|
210
|
+
Core implementation and release process live in the Graphonomous repository.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* npm bin launcher for Graphonomous.
|
|
6
|
+
*
|
|
7
|
+
* This script resolves the installed platform-specific Graphonomous command in:
|
|
8
|
+
* npm/vendor/<platform>-<arch>/graphonomous/bin/graphonomous
|
|
9
|
+
*
|
|
10
|
+
* For OTP release assets, it executes the release command through:
|
|
11
|
+
* eval "Graphonomous.CLI.main(System.argv())"
|
|
12
|
+
* and passes through all CLI arguments.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const { spawn } = require("child_process");
|
|
18
|
+
const {
|
|
19
|
+
resolvePlatform,
|
|
20
|
+
isSupportedPlatform,
|
|
21
|
+
} = require("../scripts/resolve-platform");
|
|
22
|
+
|
|
23
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
24
|
+
const VENDOR_ROOT = path.join(PACKAGE_ROOT, "vendor");
|
|
25
|
+
|
|
26
|
+
function fail(message, code = 1) {
|
|
27
|
+
console.error(`[graphonomous] ${message}`);
|
|
28
|
+
process.exit(code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fileExists(p) {
|
|
32
|
+
try {
|
|
33
|
+
fs.accessSync(p, fs.constants.F_OK);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isExecutable(p) {
|
|
41
|
+
try {
|
|
42
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveBinaryPath() {
|
|
50
|
+
const override = process.env.GRAPHONOMOUS_BINARY_PATH;
|
|
51
|
+
if (override && override.trim()) {
|
|
52
|
+
const candidate = path.resolve(override.trim());
|
|
53
|
+
if (!fileExists(candidate)) {
|
|
54
|
+
fail(
|
|
55
|
+
`GRAPHONOMOUS_BINARY_PATH is set but file does not exist: ${candidate}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!isSupportedPlatform(process.platform, process.arch)) {
|
|
62
|
+
fail(
|
|
63
|
+
`Unsupported platform "${process.platform}-${process.arch}". ` +
|
|
64
|
+
`Supported targets: darwin/linux + x64/arm64.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const info = resolvePlatform({
|
|
69
|
+
platform: process.platform,
|
|
70
|
+
arch: process.arch,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Preferred layout for OTP release assets.
|
|
74
|
+
const releaseCommandPath = path.join(
|
|
75
|
+
VENDOR_ROOT,
|
|
76
|
+
info.target,
|
|
77
|
+
"graphonomous",
|
|
78
|
+
"bin",
|
|
79
|
+
info.exeName,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (fileExists(releaseCommandPath)) {
|
|
83
|
+
return releaseCommandPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Backward-compatible fallback for single-file binary assets.
|
|
87
|
+
const binaryPath = path.join(VENDOR_ROOT, info.target, info.exeName);
|
|
88
|
+
if (fileExists(binaryPath)) {
|
|
89
|
+
return binaryPath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fail(
|
|
93
|
+
`Installed Graphonomous command not found.\n` +
|
|
94
|
+
`Checked:\n` +
|
|
95
|
+
` - ${releaseCommandPath}\n` +
|
|
96
|
+
` - ${binaryPath}\n` +
|
|
97
|
+
`Try reinstalling package or rerunning install scripts:\n` +
|
|
98
|
+
` npm rebuild graphonomous\n` +
|
|
99
|
+
`or\n` +
|
|
100
|
+
` npm i graphonomous@latest`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function run() {
|
|
105
|
+
const binaryPath = resolveBinaryPath();
|
|
106
|
+
|
|
107
|
+
// Best-effort chmod for unix-like systems if executable bit is missing.
|
|
108
|
+
if (process.platform !== "win32" && !isExecutable(binaryPath)) {
|
|
109
|
+
try {
|
|
110
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore; spawn will report if it still cannot execute
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
const otpReleasePathPattern = new RegExp(
|
|
118
|
+
`[\\\\/]graphonomous[\\\\/]bin[\\\\/]graphonomous$`,
|
|
119
|
+
);
|
|
120
|
+
const spawnArgs = otpReleasePathPattern.test(binaryPath)
|
|
121
|
+
? ["eval", "Graphonomous.CLI.main(System.argv())", ...args]
|
|
122
|
+
: args;
|
|
123
|
+
|
|
124
|
+
const child = spawn(binaryPath, spawnArgs, {
|
|
125
|
+
stdio: "inherit",
|
|
126
|
+
env: process.env,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
child.on("error", (err) => {
|
|
130
|
+
fail(`Failed to start binary at ${binaryPath}: ${err.message}`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
child.on("exit", (code, signal) => {
|
|
134
|
+
if (signal) {
|
|
135
|
+
// Mirror termination signal behavior.
|
|
136
|
+
process.kill(process.pid, signal);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.exit(typeof code === "number" ? code : 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Forward common termination signals to child.
|
|
144
|
+
const forward = (sig) => {
|
|
145
|
+
if (!child.killed) {
|
|
146
|
+
try {
|
|
147
|
+
child.kill(sig);
|
|
148
|
+
} catch {
|
|
149
|
+
// no-op
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
process.on("SIGINT", () => forward("SIGINT"));
|
|
155
|
+
process.on("SIGTERM", () => forward("SIGTERM"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graphonomous",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "npm wrapper for the Graphonomous MCP server CLI",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "Ampersandbox",
|
|
8
|
+
"homepage": "https://github.com/ampersandbox/graphonomous#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ampersandbox/graphonomous.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ampersandbox/graphonomous/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "commonjs",
|
|
17
|
+
"bin": {
|
|
18
|
+
"graphonomous": "bin/graphonomous.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"scripts",
|
|
23
|
+
"vendor",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"postinstall": "node ./scripts/postinstall.js",
|
|
28
|
+
"start": "node ./bin/graphonomous.js --help"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"graphonomous",
|
|
34
|
+
"ai",
|
|
35
|
+
"agent",
|
|
36
|
+
"elixir",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"os": [
|
|
43
|
+
"darwin",
|
|
44
|
+
"linux"
|
|
45
|
+
],
|
|
46
|
+
"cpu": [
|
|
47
|
+
"x64",
|
|
48
|
+
"arm64"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const fsp = require("fs/promises");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
const https = require("https");
|
|
9
|
+
const { pipeline } = require("stream/promises");
|
|
10
|
+
const { spawn } = require("child_process");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
13
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
14
|
+
|
|
15
|
+
function normalizeTag(tag) {
|
|
16
|
+
if (!tag || typeof tag !== "string") {
|
|
17
|
+
throw new Error("A release tag is required (e.g. v0.1.0).");
|
|
18
|
+
}
|
|
19
|
+
return tag.startsWith("v") ? tag : `v${tag}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildGitHubReleaseAssetUrl({ owner, repo, tag, assetName }) {
|
|
23
|
+
if (!owner) throw new Error("Missing GitHub owner.");
|
|
24
|
+
if (!repo) throw new Error("Missing GitHub repo.");
|
|
25
|
+
if (!assetName) throw new Error("Missing release asset name.");
|
|
26
|
+
const normalizedTag = normalizeTag(tag);
|
|
27
|
+
return `https://github.com/${owner}/${repo}/releases/download/${normalizedTag}/${assetName}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ensureDir(dirPath) {
|
|
31
|
+
return fsp.mkdir(dirPath, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function streamRequestToFile(url, destinationPath, opts = {}) {
|
|
35
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
36
|
+
const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
37
|
+
const headers = { ...(opts.headers || {}) };
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const visited = [];
|
|
41
|
+
let settled = false;
|
|
42
|
+
|
|
43
|
+
const onError = (err) => {
|
|
44
|
+
if (!settled) {
|
|
45
|
+
settled = true;
|
|
46
|
+
reject(err);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const requestUrl = (targetUrl, redirectsLeft) => {
|
|
51
|
+
visited.push(targetUrl);
|
|
52
|
+
|
|
53
|
+
if (redirectsLeft < 0) {
|
|
54
|
+
onError(
|
|
55
|
+
new Error(
|
|
56
|
+
`Too many redirects while downloading release asset. Visited: ${visited.join(" -> ")}`
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parsed = new URL(targetUrl);
|
|
63
|
+
const client = parsed.protocol === "http:" ? http : https;
|
|
64
|
+
|
|
65
|
+
const req = client.get(
|
|
66
|
+
targetUrl,
|
|
67
|
+
{
|
|
68
|
+
headers: {
|
|
69
|
+
"user-agent": "graphonomous-npm-installer",
|
|
70
|
+
...headers
|
|
71
|
+
},
|
|
72
|
+
timeout: timeoutMs
|
|
73
|
+
},
|
|
74
|
+
async (res) => {
|
|
75
|
+
const status = res.statusCode || 0;
|
|
76
|
+
|
|
77
|
+
// Follow redirects
|
|
78
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
79
|
+
res.resume();
|
|
80
|
+
const nextUrl = new URL(res.headers.location, targetUrl).toString();
|
|
81
|
+
requestUrl(nextUrl, redirectsLeft - 1);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (status < 200 || status >= 300) {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
88
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
89
|
+
onError(
|
|
90
|
+
new Error(
|
|
91
|
+
`Failed downloading asset. HTTP ${status}. URL=${targetUrl}${
|
|
92
|
+
body ? `\nResponse: ${body.slice(0, 2000)}` : ""
|
|
93
|
+
}`
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await ensureDir(path.dirname(destinationPath));
|
|
101
|
+
await pipeline(res, fs.createWriteStream(destinationPath));
|
|
102
|
+
if (!settled) {
|
|
103
|
+
settled = true;
|
|
104
|
+
resolve({ finalUrl: targetUrl, destinationPath });
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
onError(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
req.on("timeout", () => {
|
|
113
|
+
req.destroy(new Error(`Download timed out after ${timeoutMs}ms`));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
req.on("error", onError);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
requestUrl(url, maxRedirects);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runTarExtract({ archivePath, destinationDir, stripComponents = 0 }) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const args = ["-xzf", archivePath, "-C", destinationDir];
|
|
126
|
+
if (stripComponents > 0) {
|
|
127
|
+
args.push(`--strip-components=${stripComponents}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const child = spawn("tar", args, {
|
|
131
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
let stderr = "";
|
|
135
|
+
child.stderr.on("data", (chunk) => {
|
|
136
|
+
stderr += chunk.toString("utf8");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
child.on("error", (err) => {
|
|
140
|
+
reject(
|
|
141
|
+
new Error(
|
|
142
|
+
`Failed to start tar process. Ensure 'tar' is available on this system.\n${err.message}`
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.on("close", (code) => {
|
|
148
|
+
if (code === 0) {
|
|
149
|
+
resolve();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
reject(
|
|
154
|
+
new Error(
|
|
155
|
+
`Failed to extract archive with tar (exit code ${code}).` +
|
|
156
|
+
(stderr ? `\n${stderr.trim()}` : "")
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function fileExists(filePath) {
|
|
164
|
+
try {
|
|
165
|
+
await fsp.access(filePath, fs.constants.F_OK);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Downloads and extracts a release asset archive (tar.gz), then optionally marks
|
|
174
|
+
* the expected executable as executable.
|
|
175
|
+
*
|
|
176
|
+
* Options:
|
|
177
|
+
* - url: direct asset URL (optional if owner/repo/tag/assetName are provided)
|
|
178
|
+
* - owner, repo, tag, assetName: used to build GitHub release URL
|
|
179
|
+
* - destinationDir: where the archive should be extracted (required)
|
|
180
|
+
* - archivePath: optional explicit temp archive path
|
|
181
|
+
* - stripComponents: optional tar --strip-components value (default 0)
|
|
182
|
+
* - executableName: optional executable to chmod +x after extraction
|
|
183
|
+
* - timeoutMs: download timeout (default 60s)
|
|
184
|
+
* - maxRedirects: max redirects (default 5)
|
|
185
|
+
* - token: optional GitHub token for private assets
|
|
186
|
+
* - keepArchive: keep downloaded archive on disk (default false)
|
|
187
|
+
* - logger: optional logger object ({ info, warn, error })
|
|
188
|
+
*/
|
|
189
|
+
async function downloadAndExtractReleaseAsset(options = {}) {
|
|
190
|
+
const logger = options.logger || console;
|
|
191
|
+
const destinationDir = options.destinationDir;
|
|
192
|
+
|
|
193
|
+
if (!destinationDir) {
|
|
194
|
+
throw new Error("downloadAndExtractReleaseAsset: destinationDir is required.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const stripComponents = Number.isInteger(options.stripComponents)
|
|
198
|
+
? options.stripComponents
|
|
199
|
+
: 0;
|
|
200
|
+
|
|
201
|
+
const assetUrl =
|
|
202
|
+
options.url ||
|
|
203
|
+
buildGitHubReleaseAssetUrl({
|
|
204
|
+
owner: options.owner,
|
|
205
|
+
repo: options.repo,
|
|
206
|
+
tag: options.tag,
|
|
207
|
+
assetName: options.assetName
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const archivePath =
|
|
211
|
+
options.archivePath ||
|
|
212
|
+
path.join(
|
|
213
|
+
os.tmpdir(),
|
|
214
|
+
`graphonomous-${Date.now()}-${Math.random().toString(16).slice(2)}.tar.gz`
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const headers = {};
|
|
218
|
+
if (options.token && typeof options.token === "string") {
|
|
219
|
+
headers.authorization = `Bearer ${options.token}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await ensureDir(destinationDir);
|
|
223
|
+
|
|
224
|
+
logger.info?.(`Downloading Graphonomous release asset: ${assetUrl}`);
|
|
225
|
+
await streamRequestToFile(assetUrl, archivePath, {
|
|
226
|
+
timeoutMs: options.timeoutMs,
|
|
227
|
+
maxRedirects: options.maxRedirects,
|
|
228
|
+
headers
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
logger.info?.(`Extracting release asset into: ${destinationDir}`);
|
|
232
|
+
await runTarExtract({
|
|
233
|
+
archivePath,
|
|
234
|
+
destinationDir,
|
|
235
|
+
stripComponents
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
let executablePath = null;
|
|
239
|
+
if (options.executableName) {
|
|
240
|
+
executablePath = path.join(destinationDir, options.executableName);
|
|
241
|
+
const exists = await fileExists(executablePath);
|
|
242
|
+
if (!exists) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Expected executable "${options.executableName}" not found after extraction at ${executablePath}.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Ensure executable bit is present on unix-like systems
|
|
249
|
+
await fsp.chmod(executablePath, 0o755);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!options.keepArchive) {
|
|
253
|
+
try {
|
|
254
|
+
await fsp.unlink(archivePath);
|
|
255
|
+
} catch {
|
|
256
|
+
// best-effort cleanup
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
assetUrl,
|
|
262
|
+
archivePath,
|
|
263
|
+
destinationDir,
|
|
264
|
+
executablePath
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
normalizeTag,
|
|
270
|
+
buildGitHubReleaseAssetUrl,
|
|
271
|
+
streamRequestToFile,
|
|
272
|
+
runTarExtract,
|
|
273
|
+
downloadAndExtractReleaseAsset
|
|
274
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// npm postinstall script for Graphonomous.
|
|
2
|
+
// Downloads the platform-specific Graphonomous release binary into npm/vendor/<target>/.
|
|
3
|
+
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const fsp = require("fs/promises");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { resolvePlatform, isSupportedPlatform } = require("./resolve-platform");
|
|
10
|
+
const { downloadAndExtractReleaseAsset } = require("./download-release-asset");
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
13
|
+
const VENDOR_ROOT = path.join(PACKAGE_ROOT, "vendor");
|
|
14
|
+
|
|
15
|
+
function readPackageJson() {
|
|
16
|
+
const pkgPath = path.join(PACKAGE_ROOT, "package.json");
|
|
17
|
+
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function envFlag(name, defaultValue = false) {
|
|
22
|
+
const raw = process.env[name];
|
|
23
|
+
if (raw == null) return defaultValue;
|
|
24
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
25
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getConfig(pkg) {
|
|
29
|
+
const owner = process.env.GRAPHONOMOUS_GITHUB_OWNER || "ampersandbox";
|
|
30
|
+
const repo = process.env.GRAPHONOMOUS_GITHUB_REPO || "graphonomous";
|
|
31
|
+
const version = process.env.GRAPHONOMOUS_VERSION || pkg.version;
|
|
32
|
+
const tag =
|
|
33
|
+
process.env.GRAPHONOMOUS_RELEASE_TAG || `v${version.replace(/^v/, "")}`;
|
|
34
|
+
const token =
|
|
35
|
+
process.env.GRAPHONOMOUS_GITHUB_TOKEN || process.env.GITHUB_TOKEN || null;
|
|
36
|
+
const force = envFlag("GRAPHONOMOUS_FORCE_DOWNLOAD", false);
|
|
37
|
+
const skip = envFlag("GRAPHONOMOUS_SKIP_DOWNLOAD", false);
|
|
38
|
+
const timeoutMs = Number(
|
|
39
|
+
process.env.GRAPHONOMOUS_DOWNLOAD_TIMEOUT_MS || 60_000,
|
|
40
|
+
);
|
|
41
|
+
const maxRedirects = Number(
|
|
42
|
+
process.env.GRAPHONOMOUS_DOWNLOAD_MAX_REDIRECTS || 5,
|
|
43
|
+
);
|
|
44
|
+
const releaseBaseUrl = process.env.GRAPHONOMOUS_RELEASE_BASE_URL || null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
owner,
|
|
48
|
+
repo,
|
|
49
|
+
version: version.replace(/^v/, ""),
|
|
50
|
+
tag,
|
|
51
|
+
token,
|
|
52
|
+
force,
|
|
53
|
+
skip,
|
|
54
|
+
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : 60_000,
|
|
55
|
+
maxRedirects: Number.isFinite(maxRedirects) ? maxRedirects : 5,
|
|
56
|
+
releaseBaseUrl,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function exists(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
await fsp.access(filePath, fs.constants.F_OK);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildAssetName(version, target) {
|
|
70
|
+
return `graphonomous-v${version}-${target}.tar.gz`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildCustomAssetUrl(baseUrl, assetName) {
|
|
74
|
+
const trimmed = String(baseUrl).replace(/\/+$/, "");
|
|
75
|
+
return `${trimmed}/${assetName}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolveInstalledCommandPath(targetDir, exeName) {
|
|
79
|
+
const direct = path.join(targetDir, exeName);
|
|
80
|
+
if (await exists(direct)) return direct;
|
|
81
|
+
|
|
82
|
+
const otpDirect = path.join(targetDir, "bin", exeName);
|
|
83
|
+
if (await exists(otpDirect)) return otpDirect;
|
|
84
|
+
|
|
85
|
+
const entries = await fsp.readdir(targetDir, { withFileTypes: true });
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (!entry.isDirectory()) continue;
|
|
88
|
+
const nested = path.join(targetDir, entry.name, "bin", exeName);
|
|
89
|
+
if (await exists(nested)) return nested;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Could not locate installed Graphonomous command after extraction. ` +
|
|
94
|
+
`Expected one of: ${direct}, ${otpDirect}, or <release>/bin/${exeName}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function installCommandShim({ shimPath, sourceCommandPath }) {
|
|
99
|
+
const shimDir = path.dirname(shimPath);
|
|
100
|
+
const relativeTarget = path
|
|
101
|
+
.relative(shimDir, sourceCommandPath)
|
|
102
|
+
.replace(/\\/g, "/");
|
|
103
|
+
|
|
104
|
+
const script = `#!/usr/bin/env sh
|
|
105
|
+
set -eu
|
|
106
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
107
|
+
exec "$SCRIPT_DIR/${relativeTarget}" "$@"
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
await fsp.writeFile(shimPath, script, { encoding: "utf8", mode: 0o755 });
|
|
111
|
+
await fsp.chmod(shimPath, 0o755);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function writeInstallMetadata(targetDir, metadata) {
|
|
115
|
+
const metadataPath = path.join(targetDir, ".install-metadata.json");
|
|
116
|
+
await fsp.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const pkg = readPackageJson();
|
|
121
|
+
const cfg = getConfig(pkg);
|
|
122
|
+
|
|
123
|
+
if (cfg.skip) {
|
|
124
|
+
console.log(
|
|
125
|
+
"[graphonomous] postinstall skipped (GRAPHONOMOUS_SKIP_DOWNLOAD is set).",
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isSupportedPlatform(process.platform, process.arch)) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[graphonomous] Unsupported platform for prebuilt binaries: ${process.platform}-${process.arch}.`,
|
|
133
|
+
);
|
|
134
|
+
console.warn(
|
|
135
|
+
"[graphonomous] Install continues without binary. You can build Graphonomous manually from source.",
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const platformInfo = resolvePlatform({
|
|
141
|
+
platform: process.platform,
|
|
142
|
+
arch: process.arch,
|
|
143
|
+
version: cfg.version,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const targetDir = path.join(VENDOR_ROOT, platformInfo.target);
|
|
147
|
+
const binaryPath = path.join(targetDir, platformInfo.exeName);
|
|
148
|
+
const assetName = buildAssetName(cfg.version, platformInfo.target);
|
|
149
|
+
|
|
150
|
+
if (!cfg.force && (await exists(binaryPath))) {
|
|
151
|
+
console.log(
|
|
152
|
+
`[graphonomous] Binary already present at ${binaryPath}; skipping download.`,
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const useCustomBaseUrl = Boolean(cfg.releaseBaseUrl);
|
|
158
|
+
const directUrl = useCustomBaseUrl
|
|
159
|
+
? buildCustomAssetUrl(cfg.releaseBaseUrl, assetName)
|
|
160
|
+
: null;
|
|
161
|
+
|
|
162
|
+
console.log(
|
|
163
|
+
`[graphonomous] Installing Graphonomous binary for ${platformInfo.target} (version ${cfg.version})...`,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await downloadAndExtractReleaseAsset({
|
|
167
|
+
url: directUrl || undefined,
|
|
168
|
+
owner: cfg.owner,
|
|
169
|
+
repo: cfg.repo,
|
|
170
|
+
tag: cfg.tag,
|
|
171
|
+
assetName,
|
|
172
|
+
destinationDir: targetDir,
|
|
173
|
+
stripComponents: 0,
|
|
174
|
+
timeoutMs: cfg.timeoutMs,
|
|
175
|
+
maxRedirects: cfg.maxRedirects,
|
|
176
|
+
token: cfg.token,
|
|
177
|
+
keepArchive: false,
|
|
178
|
+
logger: console,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const installedCommandPath = await resolveInstalledCommandPath(
|
|
182
|
+
targetDir,
|
|
183
|
+
platformInfo.exeName,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (installedCommandPath !== binaryPath) {
|
|
187
|
+
await installCommandShim({
|
|
188
|
+
shimPath: binaryPath,
|
|
189
|
+
sourceCommandPath: installedCommandPath,
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
await fsp.chmod(binaryPath, 0o755);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await writeInstallMetadata(targetDir, {
|
|
196
|
+
packageName: pkg.name,
|
|
197
|
+
packageVersion: pkg.version,
|
|
198
|
+
resolvedVersion: cfg.version,
|
|
199
|
+
tag: cfg.tag,
|
|
200
|
+
target: platformInfo.target,
|
|
201
|
+
binaryPath,
|
|
202
|
+
installedCommandPath,
|
|
203
|
+
installedAt: new Date().toISOString(),
|
|
204
|
+
source: useCustomBaseUrl
|
|
205
|
+
? "custom_release_base_url"
|
|
206
|
+
: `github:${cfg.owner}/${cfg.repo}`,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
console.log(`[graphonomous] Installed binary: ${binaryPath}`);
|
|
210
|
+
if (installedCommandPath !== binaryPath) {
|
|
211
|
+
console.log(
|
|
212
|
+
`[graphonomous] Installed command source: ${installedCommandPath}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch((err) => {
|
|
218
|
+
console.error("[graphonomous] postinstall failed.");
|
|
219
|
+
console.error(err && err.stack ? err.stack : err);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve Node.js runtime platform/architecture into Graphonomous binary artifact metadata.
|
|
5
|
+
*
|
|
6
|
+
* Supported targets:
|
|
7
|
+
* - darwin-x64
|
|
8
|
+
* - darwin-arm64
|
|
9
|
+
* - linux-x64
|
|
10
|
+
* - linux-arm64
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PLATFORM_MAP = {
|
|
14
|
+
darwin: "darwin",
|
|
15
|
+
linux: "linux"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ARCH_MAP = {
|
|
19
|
+
x64: "x64",
|
|
20
|
+
arm64: "arm64"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function normalizeVersion(version) {
|
|
24
|
+
if (!version || typeof version !== "string") {
|
|
25
|
+
throw new Error("A package version string is required.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return version.startsWith("v") ? version.slice(1) : version;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolvePlatform(opts = {}) {
|
|
32
|
+
const rawPlatform = opts.platform || process.platform;
|
|
33
|
+
const rawArch = opts.arch || process.arch;
|
|
34
|
+
const version = opts.version ? normalizeVersion(opts.version) : null;
|
|
35
|
+
|
|
36
|
+
const platform = PLATFORM_MAP[rawPlatform];
|
|
37
|
+
const arch = ARCH_MAP[rawArch];
|
|
38
|
+
|
|
39
|
+
if (!platform) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unsupported platform "${rawPlatform}". Supported platforms: ${Object.keys(PLATFORM_MAP).join(", ")}.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!arch) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Unsupported architecture "${rawArch}". Supported architectures: ${Object.keys(ARCH_MAP).join(", ")}.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const target = `${platform}-${arch}`;
|
|
52
|
+
|
|
53
|
+
const result = {
|
|
54
|
+
platform,
|
|
55
|
+
arch,
|
|
56
|
+
target,
|
|
57
|
+
exeName: platform === "windows" ? "graphonomous.exe" : "graphonomous",
|
|
58
|
+
archiveExt: "tar.gz"
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (version) {
|
|
62
|
+
result.version = version;
|
|
63
|
+
result.tag = `v${version}`;
|
|
64
|
+
result.archiveName = `graphonomous-v${version}-${target}.${result.archiveExt}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isSupportedPlatform(platform = process.platform, arch = process.arch) {
|
|
71
|
+
return Boolean(PLATFORM_MAP[platform] && ARCH_MAP[arch]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
PLATFORM_MAP,
|
|
76
|
+
ARCH_MAP,
|
|
77
|
+
resolvePlatform,
|
|
78
|
+
isSupportedPlatform
|
|
79
|
+
};
|
package/vendor/.gitkeep
ADDED
|
File without changes
|