@accio-ai/ecommerce-cli 0.0.1-rc.1 → 0.0.2-rc.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/CHANGELOG.md +27 -0
- package/README.md +4 -2
- package/bin/ecomctl.cjs +32 -12
- package/checksums.json +7 -7
- package/json-file.cjs +21 -0
- package/package.json +9 -7
- package/postinstall.cjs +292 -30
- package/targets.json +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
- 新增多 Plugin SQLite 初始化 Go 接口:每份构建期内置 `init_config` 统一声明 Plugin、物理表、DDL 与 DML,Assets Loader 提供领域 Config,Storage Adapter 为 07 Core 生成内部同名表注册;使用库内初始化状态、Plugin 级 checksum、只读快速路径和 `BEGIN IMMEDIATE` 实现重复调用、并发调用与失败重试幂等,并通过 `setup`、`doctor`、业务命令兜底和 npm postinstall 接入。
|
|
10
|
+
- 新增 CLI 声明式参数关系,支持 `at_least_one`、`exactly_one` 和 `mutually_exclusive`;同一份 metadata 驱动注册校验、运行时校验、Help 和 Schema。
|
|
11
|
+
- 强化跨平台发布与 SQLite 恢复:统一兼容 JSON UTF-8 BOM/CRLF、避免 Windows npm shell 转义、拒绝 SQLite UNC/设备路径,并验证异常退出后的 WAL 恢复。
|
|
12
|
+
- 新增 SQLite Core:统一多 Plugin 数据库路径与表绑定、连接池、访问模式、事务、参数化 SQL、缓存、资源释放和稳定错误边界;上层无需直接依赖 `database/sql`。
|
|
13
|
+
- 将 CGo-free SQLite 引擎直接链接进 `ecomctl`,并增加真实内存数据库验证;用户无需额外安装系统 `sqlite3`,数据库由 `setup`、npm postinstall 或首次业务命令按需初始化。
|
|
9
14
|
- 新增相互隔离的正式版与 RC 发布入口;RC 自动使用 `-rc.N` 版本和 npm `rc` dist-tag,正式版固定使用 `latest`,两条通道共享完整 Windows 签名和安装验证门禁。
|
|
10
15
|
- 将根目录 `VERSION` 设为唯一发布版本源;npm staging 和 Go release build 自动注入版本,不再要求同步修改 `package.json` 与 Go 源码。
|
|
11
16
|
- 简化 Windows 签名交接:默认强制验证 Authenticode 与原始 payload,signer 证书 SHA-256 指纹改为可选加强项,不再要求每次发布手工输入。
|
|
@@ -13,6 +18,28 @@
|
|
|
13
18
|
- 将公网 npm 包名统一为 `@accio-ai/ecommerce-cli`,固定使用 `accio-ai` scope、public access 和 npmjs registry。
|
|
14
19
|
- 将 npm 分发改为 1 个主包加 6 个按 `os`/`cpu` 限制的平台包;安装时只下载当前平台二进制,发布时平台包先于主包。
|
|
15
20
|
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- 将 Source Import 统一为 append-only insert;每次 Excel/JSON 导入生成带自增 ID 的独立事实记录,不按业务字段覆盖历史数据。
|
|
24
|
+
- 将查数导出统一为 raw `scopes` 协议;按节点时返回 `scopes[].nodes[].dataset[]`,按指标时返回 `scopes[].dataset[]`,每条数据保留原始事实行字段。
|
|
25
|
+
- 将查数导出命令的共享 flag 和输入转换上提到 `dataquery/cli`,消除叶子 CLI Adapter 之间的横向依赖。
|
|
26
|
+
- 新增叶子 CLI、Assets 和 Storage 依赖方向门禁,防止分层在后续扩展中回退。
|
|
27
|
+
- 将六平台矩阵收敛到单一 manifest,launcher、staging、publish 和测试共享同一份目标定义。
|
|
28
|
+
- 将 GoReleaser 流程收敛为 raw binary build;npm publish 只提交已经完成隔离安装 smoke 的精确 tarball。
|
|
29
|
+
- 为五个非 Linux amd64 目标增加条件式原生 GitLab runner job,并在 tag pipeline 强制要求完整 runner 配置。
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- 修复 npm smoke 忽略 `ECOMCTL_DIST_DIR`、可能误用旧产物的问题。
|
|
34
|
+
- Windows 签名替换改为整批事务;任一文件或 receipt 失败时回滚已经替换的二进制。
|
|
35
|
+
- npm 发布支持按 tarball integrity 安全续跑,避免部分平台包发布成功后无法继续同一版本。
|
|
36
|
+
- registry 在注册时冻结 adapter metadata,避免装配完成后命令描述发生漂移。
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
|
|
40
|
+
- 下线未注册的 Dashboard Render Service、CLI Adapter、DTO、测试和文档,统一使用按节点或指标导出。
|
|
41
|
+
- 删除没有外部分发目标的 GoReleaser archive、archive checksum、GPG checksum 签名和 Windows zip 重写路径。
|
|
42
|
+
|
|
16
43
|
## [0.0.1] - 2026-08-31
|
|
17
44
|
|
|
18
45
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ This is the platform-independent launcher package for `ecomctl`.
|
|
|
4
4
|
|
|
5
5
|
It declares six exact-version optional dependencies. npm uses each dependency's `os` and `cpu` fields to install only the matching platform package, so users do not download binaries for other operating systems or architectures.
|
|
6
6
|
|
|
7
|
-
The platform package contains one statically compiled binary; this main package contains only the Node.js launcher, install checksum verification, README, and changelog.
|
|
7
|
+
The platform package contains one statically compiled binary; this main package contains only the Node.js launcher, the shared target manifest, install checksum verification, README, and changelog. After checksum verification, `postinstall` best-effort initializes the local SQLite databases for a normal user. It does not read or write account state, credentials, user configuration, Agent skills, telemetry, or remote services.
|
|
8
8
|
|
|
9
9
|
Supported targets:
|
|
10
10
|
|
|
@@ -12,4 +12,6 @@ Supported targets:
|
|
|
12
12
|
- Linux: arm64 and x64
|
|
13
13
|
- Windows: arm64 and x64
|
|
14
14
|
|
|
15
|
-
Run `ecomctl
|
|
15
|
+
Database setup is best-effort. Installs that disable lifecycle scripts, use an unsafe service identity, or encounter a database warning can initialize later with `ecomctl setup`; database-backed domain commands also perform the same idempotent initialization before their leaf operation. Run `ecomctl doctor` after installation to verify the local environment.
|
|
16
|
+
|
|
17
|
+
The launcher invokes the platform binary with an argument array and no command shell. Release scripts invoke npm on Windows through `node.exe` and `npm-cli.js`. Package JSON readers accept a UTF-8 BOM and CRLF, while the executable launcher itself is packaged only with an LF shebang and no BOM.
|
package/bin/ecomctl.cjs
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
const childProcess = require("node:child_process");
|
|
6
6
|
const fs = require("node:fs");
|
|
7
7
|
const path = require("node:path");
|
|
8
|
+
const { readJSONFile } = require("../json-file.cjs");
|
|
8
9
|
|
|
9
|
-
const TARGETS = Object.freeze(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const TARGETS = Object.freeze(
|
|
11
|
+
Object.fromEntries(
|
|
12
|
+
readJSONFile(path.join(__dirname, "..", "targets.json")).map((target) => [
|
|
13
|
+
target.key,
|
|
14
|
+
Object.freeze({ binary: target.binary }),
|
|
15
|
+
]),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
17
18
|
|
|
18
19
|
const SIGNAL_EXIT_CODES = Object.freeze({
|
|
19
20
|
SIGHUP: 129,
|
|
@@ -31,7 +32,7 @@ function platformKey(platform = process.platform, architecture = process.arch) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function basePackageName(packageRoot) {
|
|
34
|
-
const manifest =
|
|
35
|
+
const manifest = readJSONFile(path.join(packageRoot, "package.json"));
|
|
35
36
|
return manifest.name;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -70,6 +71,16 @@ function resolveBinaryPath(
|
|
|
70
71
|
);
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function childSignalFor(platform, signal, childMissedOriginalSignal = false) {
|
|
75
|
+
if (platform !== "win32") {
|
|
76
|
+
return signal;
|
|
77
|
+
}
|
|
78
|
+
if (signal === "SIGTERM" || childMissedOriginalSignal) {
|
|
79
|
+
return "SIGTERM";
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
function run(args = process.argv.slice(2)) {
|
|
74
85
|
const packageRoot = path.resolve(__dirname, "..");
|
|
75
86
|
let key;
|
|
@@ -109,7 +120,7 @@ function run(args = process.argv.slice(2)) {
|
|
|
109
120
|
? ["SIGINT", "SIGTERM", "SIGBREAK"]
|
|
110
121
|
: ["SIGHUP", "SIGINT", "SIGTERM"];
|
|
111
122
|
|
|
112
|
-
function forwardSignal(signal) {
|
|
123
|
+
function forwardSignal(signal, childMissedOriginalSignal = false) {
|
|
113
124
|
forwardedSignal = signal;
|
|
114
125
|
if (child === null) {
|
|
115
126
|
pendingSignal = signal;
|
|
@@ -118,8 +129,16 @@ function run(args = process.argv.slice(2)) {
|
|
|
118
129
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
119
130
|
return;
|
|
120
131
|
}
|
|
132
|
+
const childSignal = childSignalFor(
|
|
133
|
+
process.platform,
|
|
134
|
+
signal,
|
|
135
|
+
childMissedOriginalSignal,
|
|
136
|
+
);
|
|
137
|
+
if (childSignal === null) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
121
140
|
try {
|
|
122
|
-
child.kill(
|
|
141
|
+
child.kill(childSignal);
|
|
123
142
|
} catch (error) {
|
|
124
143
|
console.error(`ecomctl: failed to forward ${signal}: ${error.message}`);
|
|
125
144
|
}
|
|
@@ -142,7 +161,7 @@ function run(args = process.argv.slice(2)) {
|
|
|
142
161
|
if (pendingSignal !== null) {
|
|
143
162
|
const signal = pendingSignal;
|
|
144
163
|
pendingSignal = null;
|
|
145
|
-
forwardSignal(signal);
|
|
164
|
+
forwardSignal(signal, true);
|
|
146
165
|
}
|
|
147
166
|
|
|
148
167
|
child.on("error", (error) => {
|
|
@@ -171,6 +190,7 @@ module.exports = {
|
|
|
171
190
|
SIGNAL_EXIT_CODES,
|
|
172
191
|
TARGETS,
|
|
173
192
|
basePackageName,
|
|
193
|
+
childSignalFor,
|
|
174
194
|
platformKey,
|
|
175
195
|
platformPackageName,
|
|
176
196
|
resolveBinaryPath,
|
package/checksums.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.2-rc.1",
|
|
3
3
|
"binaries": {
|
|
4
4
|
"darwin-arm64": {
|
|
5
5
|
"package": "@accio-ai/ecommerce-cli-darwin-arm64",
|
|
6
6
|
"file": "ecomctl",
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "d0807e36731b60e03a842fb2f8c3357b4f6d0fad88d8473ef84d2cf935e12e62"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
10
|
"package": "@accio-ai/ecommerce-cli-darwin-x64",
|
|
11
11
|
"file": "ecomctl",
|
|
12
|
-
"sha256": "
|
|
12
|
+
"sha256": "009b17424ea301e2db01c957b075f686a3b4426c66f4cddc9b99dc7ae857ef64"
|
|
13
13
|
},
|
|
14
14
|
"linux-arm64": {
|
|
15
15
|
"package": "@accio-ai/ecommerce-cli-linux-arm64",
|
|
16
16
|
"file": "ecomctl",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "ee53210dfddc8aa70e48232091f72515aeae0fed53f2d9992e03771a13d394a2"
|
|
18
18
|
},
|
|
19
19
|
"linux-x64": {
|
|
20
20
|
"package": "@accio-ai/ecommerce-cli-linux-x64",
|
|
21
21
|
"file": "ecomctl",
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "8eb6a2569d72d98912335ec3b8535eed5ce254d0e4f6edd7b9379462c5f6ca0e"
|
|
23
23
|
},
|
|
24
24
|
"win32-arm64": {
|
|
25
25
|
"package": "@accio-ai/ecommerce-cli-win32-arm64",
|
|
26
26
|
"file": "ecomctl.exe",
|
|
27
|
-
"sha256": "
|
|
27
|
+
"sha256": "e953a631f1f0f55d6f7cfaf05e819770a97f1909d336443e3beb7b2203bf5538"
|
|
28
28
|
},
|
|
29
29
|
"win32-x64": {
|
|
30
30
|
"package": "@accio-ai/ecommerce-cli-win32-x64",
|
|
31
31
|
"file": "ecomctl.exe",
|
|
32
|
-
"sha256": "
|
|
32
|
+
"sha256": "98ba2b83588a8cf07eb34de1812c658e322186e0d2e5c5b07a89e65fe9612d2e"
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|
package/json-file.cjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
|
|
5
|
+
function stripUTF8BOM(content) {
|
|
6
|
+
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseJSON(content, source = "JSON input") {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(stripUTF8BOM(content));
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new Error(`invalid JSON in ${source}: ${error.message}`, { cause: error });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJSONFile(filePath) {
|
|
18
|
+
return parseJSON(fs.readFileSync(filePath, "utf8"), String(filePath));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { parseJSON, readJSONFile, stripUTF8BOM };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@accio-ai/ecommerce-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2-rc.1",
|
|
4
4
|
"description": "Local, statically compiled ecommerce automation toolkit",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -18,16 +18,18 @@
|
|
|
18
18
|
"postinstall": "node postinstall.cjs"
|
|
19
19
|
},
|
|
20
20
|
"optionalDependencies": {
|
|
21
|
-
"@accio-ai/ecommerce-cli-darwin-arm64": "0.0.
|
|
22
|
-
"@accio-ai/ecommerce-cli-darwin-x64": "0.0.
|
|
23
|
-
"@accio-ai/ecommerce-cli-linux-arm64": "0.0.
|
|
24
|
-
"@accio-ai/ecommerce-cli-linux-x64": "0.0.
|
|
25
|
-
"@accio-ai/ecommerce-cli-win32-arm64": "0.0.
|
|
26
|
-
"@accio-ai/ecommerce-cli-win32-x64": "0.0.
|
|
21
|
+
"@accio-ai/ecommerce-cli-darwin-arm64": "0.0.2-rc.1",
|
|
22
|
+
"@accio-ai/ecommerce-cli-darwin-x64": "0.0.2-rc.1",
|
|
23
|
+
"@accio-ai/ecommerce-cli-linux-arm64": "0.0.2-rc.1",
|
|
24
|
+
"@accio-ai/ecommerce-cli-linux-x64": "0.0.2-rc.1",
|
|
25
|
+
"@accio-ai/ecommerce-cli-win32-arm64": "0.0.2-rc.1",
|
|
26
|
+
"@accio-ai/ecommerce-cli-win32-x64": "0.0.2-rc.1"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin",
|
|
30
|
+
"json-file.cjs",
|
|
30
31
|
"postinstall.cjs",
|
|
32
|
+
"targets.json",
|
|
31
33
|
"checksums.json",
|
|
32
34
|
"README.md",
|
|
33
35
|
"CHANGELOG.md"
|
package/postinstall.cjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
|
+
const childProcess = require("node:child_process");
|
|
6
7
|
const fs = require("node:fs");
|
|
8
|
+
const os = require("node:os");
|
|
7
9
|
const path = require("node:path");
|
|
8
10
|
const {
|
|
9
11
|
basePackageName,
|
|
@@ -11,49 +13,309 @@ const {
|
|
|
11
13
|
platformPackageName,
|
|
12
14
|
resolveBinaryPath,
|
|
13
15
|
} = require("./bin/ecomctl.cjs");
|
|
16
|
+
const { readJSONFile } = require("./json-file.cjs");
|
|
14
17
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const DATA_HOME_ENVIRONMENT = "ECOMCTL_DATA_HOME";
|
|
19
|
+
const INIT_ARGS = Object.freeze(["setup", "--output", "json"]);
|
|
20
|
+
const INIT_TIMEOUT_MS = 30_000;
|
|
21
|
+
const INIT_MAX_BUFFER_BYTES = 1 << 20;
|
|
22
|
+
const MANUAL_INIT_COMMAND = "ecomctl setup";
|
|
23
|
+
|
|
24
|
+
function main() {
|
|
25
|
+
const packageRoot = __dirname;
|
|
26
|
+
let binaryPath;
|
|
27
|
+
let key;
|
|
28
|
+
let packageName;
|
|
29
|
+
let platformPackage;
|
|
30
|
+
try {
|
|
31
|
+
key = platformKey();
|
|
32
|
+
packageName = basePackageName(packageRoot);
|
|
33
|
+
platformPackage = platformPackageName(packageName);
|
|
34
|
+
binaryPath = resolveBinaryPath(packageRoot);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!key) {
|
|
37
|
+
fail(error.message);
|
|
38
|
+
}
|
|
39
|
+
fail(`platform package ${platformPackage || key} is missing; reinstall ${packageName || "the CLI package"}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(binaryPath)) {
|
|
43
|
+
fail(`binary is missing from ${platformPackage}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const checksums = readJSONFile(path.join(packageRoot, "checksums.json"));
|
|
47
|
+
const expected = checksums.binaries[key]?.sha256;
|
|
48
|
+
if (!expected) {
|
|
49
|
+
fail(`checksum is missing for ${key}`);
|
|
50
|
+
}
|
|
51
|
+
if (checksums.binaries[key]?.package !== platformPackage) {
|
|
52
|
+
fail(`checksum package does not match ${platformPackage}`);
|
|
53
|
+
}
|
|
54
|
+
const actual = crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex");
|
|
55
|
+
if (actual !== expected) {
|
|
56
|
+
fail(`binary checksum mismatch for ${key}`);
|
|
57
|
+
}
|
|
58
|
+
if (process.platform !== "win32") {
|
|
59
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`ecomctl ${checksums.version} installed from ${platformPackage}`);
|
|
63
|
+
initializeDatabase(binaryPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function initializeDatabase(binaryPath, options = {}) {
|
|
67
|
+
const emitWarning = options.warn || warning;
|
|
68
|
+
const decision = initializationDecision(options);
|
|
69
|
+
if (!decision.run) {
|
|
70
|
+
emitWarning(`database initialization skipped (code=${decision.reason}); ${recoveryInstruction(decision.reason)}`);
|
|
71
|
+
return { status: "skipped", reason: decision.reason };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const spawnSync = options.spawnSync || childProcess.spawnSync;
|
|
75
|
+
const childEnvironment = { ...decision.environment };
|
|
76
|
+
if (decision.dataRoot !== undefined) {
|
|
77
|
+
childEnvironment[DATA_HOME_ENVIRONMENT] = decision.dataRoot;
|
|
78
|
+
}
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = spawnSync(binaryPath, INIT_ARGS, {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
env: childEnvironment,
|
|
84
|
+
killSignal: "SIGKILL",
|
|
85
|
+
maxBuffer: INIT_MAX_BUFFER_BYTES,
|
|
86
|
+
shell: false,
|
|
87
|
+
timeout: INIT_TIMEOUT_MS,
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
});
|
|
90
|
+
} catch {
|
|
91
|
+
emitWarning(`database initialization failed (code=execution_failed); run "${MANUAL_INIT_COMMAND}" after installation`);
|
|
92
|
+
return { status: "failed", reason: "execution_failed" };
|
|
93
|
+
}
|
|
94
|
+
if (result.error) {
|
|
95
|
+
const reason = result.error.code === "ETIMEDOUT" ? "timeout" : "execution_failed";
|
|
96
|
+
emitWarning(`database initialization failed (code=${reason}); run "${MANUAL_INIT_COMMAND}" after installation`);
|
|
97
|
+
return { status: "failed", reason };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (result.status !== 0) {
|
|
101
|
+
const failure = initializationFailure(result.stderr);
|
|
102
|
+
emitWarning(`database initialization failed (${failure}); run "${MANUAL_INIT_COMMAND}" after installation`);
|
|
103
|
+
return { status: "failed", reason: failure };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const response = parseJSON(result.stdout);
|
|
107
|
+
const domains = response?.data?.domains;
|
|
108
|
+
if (
|
|
109
|
+
result.stderr !== "" ||
|
|
110
|
+
response?.success !== true ||
|
|
111
|
+
!Array.isArray(domains) ||
|
|
112
|
+
domains.length === 0 ||
|
|
113
|
+
!validInitializationDomains(domains)
|
|
114
|
+
) {
|
|
115
|
+
emitWarning(`database initialization failed (code=invalid_response); ${recoveryInstruction("invalid_response")}`);
|
|
116
|
+
return { status: "failed", reason: "invalid_response" };
|
|
117
|
+
}
|
|
118
|
+
return { status: "initialized", domains };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validInitializationDomains(domains) {
|
|
122
|
+
const domainKeys = new Set();
|
|
123
|
+
for (const domain of domains) {
|
|
124
|
+
const tableNames = Array.isArray(domain?.tables) ? new Set(domain.tables) : null;
|
|
125
|
+
if (
|
|
126
|
+
domain === null ||
|
|
127
|
+
typeof domain !== "object" ||
|
|
128
|
+
!isDomainKey(domain.domain) ||
|
|
129
|
+
(domain.status !== "created" && domain.status !== "already_initialized") ||
|
|
130
|
+
!Array.isArray(domain.tables) ||
|
|
131
|
+
domain.tables.length === 0 ||
|
|
132
|
+
domain.tables.some((table) => typeof table !== "string" || !/^[a-z][a-z0-9_]{0,62}$/.test(table)) ||
|
|
133
|
+
tableNames.size !== domain.tables.length ||
|
|
134
|
+
domainKeys.has(domain.domain)
|
|
135
|
+
) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
domainKeys.add(domain.domain);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isDomainKey(value) {
|
|
144
|
+
return typeof value === "string" && /^[a-z][a-z0-9-]*$/.test(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function initializationDecision(options = {}) {
|
|
148
|
+
const environment = options.env || process.env;
|
|
149
|
+
const platform = options.platform || process.platform;
|
|
150
|
+
const configuredRoot = environment[DATA_HOME_ENVIRONMENT];
|
|
151
|
+
if (configuredRoot) {
|
|
152
|
+
if (!isAbsoluteLocalPath(configuredRoot, platform)) {
|
|
153
|
+
return { run: false, reason: "invalid_data_home" };
|
|
154
|
+
}
|
|
155
|
+
return { run: true, dataRoot: normalizePath(configuredRoot, platform), environment };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (platform === "win32") {
|
|
159
|
+
const username = hasOwn(options, "username") ? options.username : currentUsername();
|
|
160
|
+
const home = hasOwn(options, "home") ? options.home : currentHome();
|
|
161
|
+
if (isWindowsServiceAccount(username) || isWindowsServiceProfile(home)) {
|
|
162
|
+
return { run: false, reason: "service_account" };
|
|
163
|
+
}
|
|
164
|
+
if (!isAbsoluteLocalPath(home, platform)) {
|
|
165
|
+
return { run: false, reason: "unreliable_user_home" };
|
|
166
|
+
}
|
|
167
|
+
return { run: true, environment };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const uid = hasOwn(options, "uid") ? options.uid : currentID("getuid");
|
|
171
|
+
const euid = hasOwn(options, "euid") ? options.euid : currentID("geteuid");
|
|
172
|
+
if (uid === 0 || euid === 0) {
|
|
173
|
+
return { run: false, reason: "privileged_account" };
|
|
174
|
+
}
|
|
175
|
+
if (environment.SUDO_UID || environment.SUDO_USER || environment.SUDO_GID) {
|
|
176
|
+
return { run: false, reason: "sudo_identity" };
|
|
177
|
+
}
|
|
178
|
+
const home = hasOwn(options, "home") ? options.home : currentHome();
|
|
179
|
+
if (!Number.isInteger(uid) || !Number.isInteger(euid) || !isAbsoluteLocalPath(home, platform)) {
|
|
180
|
+
return { run: false, reason: "unreliable_user_home" };
|
|
28
181
|
}
|
|
29
|
-
|
|
182
|
+
return { run: true, environment };
|
|
30
183
|
}
|
|
31
184
|
|
|
32
|
-
|
|
33
|
-
|
|
185
|
+
function hasOwn(object, property) {
|
|
186
|
+
return Object.prototype.hasOwnProperty.call(object, property);
|
|
34
187
|
}
|
|
35
188
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
189
|
+
function isAbsoluteLocalPath(value, platform) {
|
|
190
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (platform === "win32") {
|
|
194
|
+
return path.win32.isAbsolute(value) && !isWindowsNetworkOrDevicePath(value);
|
|
195
|
+
}
|
|
196
|
+
return path.posix.isAbsolute(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizePath(value, platform) {
|
|
200
|
+
return platform === "win32" ? path.win32.normalize(value) : path.posix.normalize(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isWindowsNetworkOrDevicePath(value) {
|
|
204
|
+
const normalized = value.replaceAll("/", "\\");
|
|
205
|
+
const lower = normalized.toLowerCase();
|
|
206
|
+
return (
|
|
207
|
+
normalized.startsWith("\\\\") ||
|
|
208
|
+
lower.startsWith("\\??\\") ||
|
|
209
|
+
lower.startsWith("\\device\\") ||
|
|
210
|
+
lower.startsWith("\\global??\\")
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isWindowsServiceAccount(username) {
|
|
215
|
+
if (typeof username !== "string") {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
const account = username.replaceAll("/", "\\").split("\\").at(-1).trim().toLowerCase();
|
|
219
|
+
return new Set(["system", "local service", "localservice", "network service", "networkservice"]).has(account);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isWindowsServiceProfile(home) {
|
|
223
|
+
if (typeof home !== "string") {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
const normalized = home.replaceAll("\\", "/").toLowerCase().replace(/\/+$/, "");
|
|
227
|
+
return normalized.endsWith("/windows/system32/config/systemprofile") || normalized.includes("/windows/serviceprofiles/");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function currentID(method) {
|
|
231
|
+
try {
|
|
232
|
+
return typeof process[method] === "function" ? process[method]() : undefined;
|
|
233
|
+
} catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function currentHome() {
|
|
239
|
+
try {
|
|
240
|
+
return os.homedir();
|
|
241
|
+
} catch {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
42
244
|
}
|
|
43
|
-
|
|
44
|
-
|
|
245
|
+
|
|
246
|
+
function currentUsername() {
|
|
247
|
+
try {
|
|
248
|
+
return os.userInfo().username;
|
|
249
|
+
} catch {
|
|
250
|
+
return "";
|
|
251
|
+
}
|
|
45
252
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
253
|
+
|
|
254
|
+
function initializationFailure(stderr) {
|
|
255
|
+
const response = parseJSON(stderr);
|
|
256
|
+
const code = safeToken(response?.error?.reason) || safeToken(response?.error?.code) || "initialization_failed";
|
|
257
|
+
const failures = Array.isArray(response?.data?.domains)
|
|
258
|
+
? response.data.domains
|
|
259
|
+
.filter((domain) => domain?.error_code)
|
|
260
|
+
.slice(0, 10)
|
|
261
|
+
.map((domain) => {
|
|
262
|
+
const domainKey = safeToken(domain?.domain) || "unknown";
|
|
263
|
+
const itemCode = safeToken(domain?.error_code) || "initialization_failed";
|
|
264
|
+
return `${domainKey}:${itemCode}`;
|
|
265
|
+
})
|
|
266
|
+
: [];
|
|
267
|
+
return failures.length === 0 ? `code=${code}` : `code=${code}, domains=${failures.join(",")}`;
|
|
49
268
|
}
|
|
50
|
-
|
|
51
|
-
|
|
269
|
+
|
|
270
|
+
function recoveryInstruction(reason) {
|
|
271
|
+
if (reason === "invalid_data_home") {
|
|
272
|
+
return `set ${DATA_HOME_ENVIRONMENT} to an absolute local path or unset it, then run "${MANUAL_INIT_COMMAND}"`;
|
|
273
|
+
}
|
|
274
|
+
if (["privileged_account", "sudo_identity", "service_account", "unreliable_user_home"].includes(reason)) {
|
|
275
|
+
return `run "${MANUAL_INIT_COMMAND}" after installation from the intended user account`;
|
|
276
|
+
}
|
|
277
|
+
return `run "${MANUAL_INIT_COMMAND}" after installation`;
|
|
52
278
|
}
|
|
53
279
|
|
|
54
|
-
|
|
280
|
+
function safeToken(value) {
|
|
281
|
+
return typeof value === "string" && /^[a-z][a-z0-9_-]{0,127}$/.test(value) ? value : "";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseJSON(value) {
|
|
285
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
return JSON.parse(value);
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function warning(message) {
|
|
296
|
+
console.error(`ecomctl: warning: ${message}`);
|
|
297
|
+
}
|
|
55
298
|
|
|
56
299
|
function fail(message) {
|
|
57
300
|
console.error(`ecomctl: ${message}`);
|
|
58
301
|
process.exit(1);
|
|
59
302
|
}
|
|
303
|
+
|
|
304
|
+
if (require.main === module) {
|
|
305
|
+
main();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
DATA_HOME_ENVIRONMENT,
|
|
310
|
+
INIT_ARGS,
|
|
311
|
+
INIT_MAX_BUFFER_BYTES,
|
|
312
|
+
INIT_TIMEOUT_MS,
|
|
313
|
+
initializeDatabase,
|
|
314
|
+
initializationDecision,
|
|
315
|
+
initializationFailure,
|
|
316
|
+
isAbsoluteLocalPath,
|
|
317
|
+
isWindowsNetworkOrDevicePath,
|
|
318
|
+
isWindowsServiceAccount,
|
|
319
|
+
isWindowsServiceProfile,
|
|
320
|
+
main,
|
|
321
|
+
};
|
package/targets.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "goos": "darwin", "goarch": "arm64", "key": "darwin-arm64", "os": "darwin", "cpu": "arm64", "binary": "ecomctl" },
|
|
3
|
+
{ "goos": "darwin", "goarch": "amd64", "key": "darwin-x64", "os": "darwin", "cpu": "x64", "binary": "ecomctl" },
|
|
4
|
+
{ "goos": "linux", "goarch": "arm64", "key": "linux-arm64", "os": "linux", "cpu": "arm64", "binary": "ecomctl" },
|
|
5
|
+
{ "goos": "linux", "goarch": "amd64", "key": "linux-x64", "os": "linux", "cpu": "x64", "binary": "ecomctl" },
|
|
6
|
+
{ "goos": "windows", "goarch": "arm64", "key": "win32-arm64", "os": "win32", "cpu": "arm64", "binary": "ecomctl.exe" },
|
|
7
|
+
{ "goos": "windows", "goarch": "amd64", "key": "win32-x64", "os": "win32", "cpu": "x64", "binary": "ecomctl.exe" }
|
|
8
|
+
]
|