@hagicode/hagiscript 0.1.6 → 0.1.8
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 +144 -201
- package/bin/runtime +16 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/pm2-commands.d.ts +2 -0
- package/dist/commands/pm2-commands.js +54 -0
- package/dist/commands/pm2-commands.js.map +1 -0
- package/dist/commands/runtime-commands.d.ts +2 -0
- package/dist/commands/runtime-commands.js +136 -0
- package/dist/commands/runtime-commands.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/command-launch.js +1 -0
- package/dist/runtime/command-launch.js.map +1 -1
- package/dist/runtime/pm2-manager.d.ts +54 -0
- package/dist/runtime/pm2-manager.js +258 -0
- package/dist/runtime/pm2-manager.js.map +1 -0
- package/dist/runtime/runtime-executor.d.ts +45 -0
- package/dist/runtime/runtime-executor.js +153 -0
- package/dist/runtime/runtime-executor.js.map +1 -0
- package/dist/runtime/runtime-manager.d.ts +79 -0
- package/dist/runtime/runtime-manager.js +651 -0
- package/dist/runtime/runtime-manager.js.map +1 -0
- package/dist/runtime/runtime-manifest.d.ts +77 -0
- package/dist/runtime/runtime-manifest.js +277 -0
- package/dist/runtime/runtime-manifest.js.map +1 -0
- package/dist/runtime/runtime-paths.d.ts +31 -0
- package/dist/runtime/runtime-paths.js +77 -0
- package/dist/runtime/runtime-paths.js.map +1 -0
- package/dist/runtime/runtime-state.d.ts +45 -0
- package/dist/runtime/runtime-state.js +82 -0
- package/dist/runtime/runtime-state.js.map +1 -0
- package/package.json +9 -5
- package/runtime/lib/runtime-script-lib.mjs +531 -0
- package/runtime/manifest.yaml +136 -0
- package/runtime/scripts/configure-code-server.mjs +14 -0
- package/runtime/scripts/configure-omniroute.mjs +16 -0
- package/runtime/scripts/install-code-server.mjs +53 -0
- package/runtime/scripts/install-dotnet.mjs +24 -0
- package/runtime/scripts/install-node.mjs +4 -0
- package/runtime/scripts/install-npm-packages.mjs +4 -0
- package/runtime/scripts/install-omniroute.mjs +60 -0
- package/runtime/scripts/remove-npm-packages.mjs +4 -0
- package/runtime/scripts/update-npm-packages.mjs +4 -0
- package/runtime/scripts/verify-dotnet.mjs +10 -0
- package/runtime/scripts/verify-node.mjs +4 -0
- package/runtime/templates/code-server-config.yaml +5 -0
- package/runtime/templates/omniroute-config.yaml +4 -0
- package/README_cn.md +0 -266
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
export class RuntimeStateError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "RuntimeStateError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function createInitialRuntimeState(manifest, paths) {
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: 1,
|
|
12
|
+
runtime: {
|
|
13
|
+
name: manifest.runtime.name,
|
|
14
|
+
version: manifest.runtime.version,
|
|
15
|
+
manifestPath: manifest.manifestPath
|
|
16
|
+
},
|
|
17
|
+
managedRoot: paths.root,
|
|
18
|
+
managedPaths: paths,
|
|
19
|
+
components: {},
|
|
20
|
+
lastOperation: null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function readRuntimeState(statePath) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(await readFile(statePath, "utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (isMissingFileError(error)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
throw new RuntimeStateError(`Failed to read runtime state ${statePath}: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
return validateRuntimeState(parsed, statePath);
|
|
36
|
+
}
|
|
37
|
+
export async function writeRuntimeState(statePath, state) {
|
|
38
|
+
await mkdir(dirname(statePath), { recursive: true });
|
|
39
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
export function mergeRuntimeState(manifest, paths, state) {
|
|
42
|
+
const nextState = state ?? createInitialRuntimeState(manifest, paths);
|
|
43
|
+
return {
|
|
44
|
+
...nextState,
|
|
45
|
+
runtime: {
|
|
46
|
+
name: manifest.runtime.name,
|
|
47
|
+
version: manifest.runtime.version,
|
|
48
|
+
manifestPath: manifest.manifestPath
|
|
49
|
+
},
|
|
50
|
+
managedRoot: paths.root,
|
|
51
|
+
managedPaths: paths,
|
|
52
|
+
components: { ...nextState.components }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function validateRuntimeState(value, statePath) {
|
|
56
|
+
if (!isRecord(value)) {
|
|
57
|
+
throw new RuntimeStateError(`Runtime state ${statePath} must be a JSON object.`);
|
|
58
|
+
}
|
|
59
|
+
if (value.schemaVersion !== 1) {
|
|
60
|
+
throw new RuntimeStateError(`Runtime state ${statePath} has unsupported schemaVersion ${String(value.schemaVersion)}.`);
|
|
61
|
+
}
|
|
62
|
+
if (!isRecord(value.runtime) || typeof value.runtime.name !== "string" || typeof value.runtime.version !== "string") {
|
|
63
|
+
throw new RuntimeStateError(`Runtime state ${statePath} is missing runtime metadata.`);
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(value.managedPaths)) {
|
|
66
|
+
throw new RuntimeStateError(`Runtime state ${statePath} is missing managedPaths.`);
|
|
67
|
+
}
|
|
68
|
+
if (!isRecord(value.components)) {
|
|
69
|
+
throw new RuntimeStateError(`Runtime state ${statePath} is missing components.`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function isRecord(value) {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
function isMissingFileError(error) {
|
|
77
|
+
return (error instanceof Error &&
|
|
78
|
+
"code" in error &&
|
|
79
|
+
typeof error.code === "string" &&
|
|
80
|
+
error.code === "ENOENT");
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=runtime-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-state.js","sourceRoot":"","sources":["../../src/runtime/runtime-state.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagiscript",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Scoped npm package foundation for Hagiscript language tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/HagiCode-org/hagiscript#readme",
|
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
|
-
"hagiscript": "dist/cli.js"
|
|
23
|
+
"hagiscript": "dist/cli.js",
|
|
24
|
+
"hagicode-runtime": "bin/runtime"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"bin",
|
|
29
|
+
"runtime",
|
|
30
|
+
"README.md"
|
|
29
31
|
],
|
|
30
32
|
"scripts": {
|
|
31
33
|
"build": "tsc -p tsconfig.json",
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"format:check": "prettier --check .",
|
|
36
38
|
"lint": "eslint .",
|
|
37
39
|
"integration:installed-runtime": "node scripts/integration-installed-runtime.mjs",
|
|
40
|
+
"integration:runtime-management": "node scripts/integration-runtime-management.mjs",
|
|
38
41
|
"pack:check": "node scripts/verify-package.mjs",
|
|
39
42
|
"publish:check-prereqs": "node scripts/verify-npm-publish-prereqs.mjs",
|
|
40
43
|
"publish:prepare-dev-version": "node scripts/prepare-dev-version.mjs",
|
|
@@ -59,7 +62,8 @@
|
|
|
59
62
|
"dependencies": {
|
|
60
63
|
"commander": "^14.0.1",
|
|
61
64
|
"execa": "^9.6.1",
|
|
62
|
-
"semver": "^7.7.4"
|
|
65
|
+
"semver": "^7.7.4",
|
|
66
|
+
"yaml": "^2.8.4"
|
|
63
67
|
},
|
|
64
68
|
"devDependencies": {
|
|
65
69
|
"@eslint/js": "^9.38.0",
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { createReadStream, createWriteStream } from "node:fs"
|
|
3
|
+
import { access, chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import process from "node:process"
|
|
7
|
+
import { createGunzip } from "node:zlib"
|
|
8
|
+
import { pipeline } from "node:stream/promises"
|
|
9
|
+
|
|
10
|
+
export function readRuntimeScriptContext() {
|
|
11
|
+
return {
|
|
12
|
+
runtimeRoot: requiredEnv("HAGISCRIPT_RUNTIME_ROOT"),
|
|
13
|
+
runtimeHome: requiredEnv("HAGICODE_RUNTIME_HOME"),
|
|
14
|
+
runtimeDataHome: requiredEnv("HAGICODE_RUNTIME_DATA_HOME"),
|
|
15
|
+
binDir: requiredEnv("HAGISCRIPT_RUNTIME_BIN_DIR"),
|
|
16
|
+
configDir: requiredEnv("HAGISCRIPT_RUNTIME_CONFIG_DIR"),
|
|
17
|
+
logsDir: requiredEnv("HAGISCRIPT_RUNTIME_LOGS_DIR"),
|
|
18
|
+
dataDir: requiredEnv("HAGISCRIPT_RUNTIME_DATA_DIR"),
|
|
19
|
+
statePath: requiredEnv("HAGISCRIPT_RUNTIME_STATE_PATH"),
|
|
20
|
+
componentName: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_NAME"),
|
|
21
|
+
componentType: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_TYPE"),
|
|
22
|
+
componentRoot: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_ROOT"),
|
|
23
|
+
componentConfigDir: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_CONFIG_DIR"),
|
|
24
|
+
componentDataDir: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_DATA_DIR"),
|
|
25
|
+
componentLogsDir: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_LOGS_DIR"),
|
|
26
|
+
componentPm2Home: requiredEnv("HAGISCRIPT_RUNTIME_COMPONENT_PM2_HOME"),
|
|
27
|
+
templateDir: requiredEnv("HAGISCRIPT_RUNTIME_TEMPLATE_DIR"),
|
|
28
|
+
componentVersion: process.env.HAGISCRIPT_RUNTIME_COMPONENT_VERSION?.trim() || null,
|
|
29
|
+
vendoredRepository: process.env.HAGISCRIPT_RUNTIME_VENDORED_REPOSITORY?.trim() || "HagiCode-org/vendered",
|
|
30
|
+
vendoredTag:
|
|
31
|
+
process.env.HAGISCRIPT_RUNTIME_VENDORED_TAG?.trim() || "v2026.0506.0029",
|
|
32
|
+
vendoredBaseUrl: process.env.HAGISCRIPT_RUNTIME_VENDORED_BASE_URL?.trim() || "https://github.com",
|
|
33
|
+
phase: process.env.HAGISCRIPT_RUNTIME_PHASE?.trim() || "install",
|
|
34
|
+
purge: process.env.HAGISCRIPT_RUNTIME_PURGE === "1"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function ensureDirectory(directory) {
|
|
39
|
+
await mkdir(directory, { recursive: true })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function writeJsonFile(filePath, value) {
|
|
43
|
+
await ensureDirectory(path.dirname(filePath))
|
|
44
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function writeComponentMarker(context, extra = {}) {
|
|
48
|
+
const markerPath = path.join(context.componentRoot, ".hagicode-runtime.json")
|
|
49
|
+
await writeJsonFile(markerPath, {
|
|
50
|
+
component: context.componentName,
|
|
51
|
+
type: context.componentType,
|
|
52
|
+
version: context.componentVersion,
|
|
53
|
+
phase: context.phase,
|
|
54
|
+
runtimeRoot: context.runtimeRoot,
|
|
55
|
+
generatedAt: new Date().toISOString(),
|
|
56
|
+
...extra
|
|
57
|
+
})
|
|
58
|
+
return markerPath
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function materializeTemplate(templateName, destinationPath, variables) {
|
|
62
|
+
const templatePath = path.join(readRuntimeScriptContext().templateDir, templateName)
|
|
63
|
+
const template = await readFile(templatePath, "utf8")
|
|
64
|
+
let rendered = template
|
|
65
|
+
|
|
66
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
67
|
+
rendered = rendered.replaceAll(`{{${key}}}`, String(value))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await ensureDirectory(path.dirname(destinationPath))
|
|
71
|
+
await writeFile(destinationPath, rendered, "utf8")
|
|
72
|
+
return destinationPath
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function writeNodeEntrypoint(filePath, message) {
|
|
76
|
+
await ensureDirectory(path.dirname(filePath))
|
|
77
|
+
await writeFile(
|
|
78
|
+
filePath,
|
|
79
|
+
`#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(message)} + "\\n")\n`,
|
|
80
|
+
"utf8"
|
|
81
|
+
)
|
|
82
|
+
await makeExecutable(filePath)
|
|
83
|
+
return filePath
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeCommandWrapper(binDir, commandName, scriptPath) {
|
|
87
|
+
await ensureDirectory(binDir)
|
|
88
|
+
const nodeWrapperPath = path.join(binDir, process.platform === "win32" ? "node.cmd" : "node")
|
|
89
|
+
|
|
90
|
+
if (process.platform === "win32") {
|
|
91
|
+
const wrapperPath = path.join(binDir, `${commandName}.cmd`)
|
|
92
|
+
const relativeTarget = path.relative(path.dirname(wrapperPath), scriptPath).replaceAll("/", "\\")
|
|
93
|
+
const relativeNode = path.relative(path.dirname(wrapperPath), nodeWrapperPath).replaceAll("/", "\\")
|
|
94
|
+
await writeFile(
|
|
95
|
+
wrapperPath,
|
|
96
|
+
`@echo off\r\nset "HAGISCRIPT_NODE=%~dp0\\${relativeNode}"\r\nif exist "%HAGISCRIPT_NODE%" (\r\n "%HAGISCRIPT_NODE%" "%~dp0\\${relativeTarget}" %*\r\n) else (\r\n node "%~dp0\\${relativeTarget}" %*\r\n)\r\n`,
|
|
97
|
+
"utf8"
|
|
98
|
+
)
|
|
99
|
+
return wrapperPath
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const wrapperPath = path.join(binDir, commandName)
|
|
103
|
+
const relativeTarget = path.relative(path.dirname(wrapperPath), scriptPath).replaceAll("\\", "/")
|
|
104
|
+
const relativeNode = path.relative(path.dirname(wrapperPath), nodeWrapperPath).replaceAll("\\", "/")
|
|
105
|
+
await writeFile(
|
|
106
|
+
wrapperPath,
|
|
107
|
+
`#!/usr/bin/env sh
|
|
108
|
+
node_cmd="$(dirname "$0")/${relativeNode}"
|
|
109
|
+
if [ -x "$node_cmd" ]; then
|
|
110
|
+
exec "$node_cmd" "$(dirname "$0")/${relativeTarget}" "$@"
|
|
111
|
+
fi
|
|
112
|
+
exec node "$(dirname "$0")/${relativeTarget}" "$@"
|
|
113
|
+
`,
|
|
114
|
+
"utf8"
|
|
115
|
+
)
|
|
116
|
+
await makeExecutable(wrapperPath)
|
|
117
|
+
return wrapperPath
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function installVendoredPackage(context, options) {
|
|
121
|
+
const { repository, baseUrl } = parseGitHubRepositoryConfig(
|
|
122
|
+
context.vendoredRepository,
|
|
123
|
+
context.vendoredBaseUrl
|
|
124
|
+
)
|
|
125
|
+
const releaseTag = context.vendoredTag
|
|
126
|
+
const platform = normalizeVendoredPlatform(process.platform)
|
|
127
|
+
const arch = normalizeVendoredArchitecture(process.arch)
|
|
128
|
+
const assetName = buildVendoredAssetName({
|
|
129
|
+
packageName: options.packageName,
|
|
130
|
+
releaseTag,
|
|
131
|
+
platform,
|
|
132
|
+
arch
|
|
133
|
+
})
|
|
134
|
+
const assetUrl = buildVendoredAssetUrl(baseUrl, repository, releaseTag, assetName)
|
|
135
|
+
|
|
136
|
+
const stagingRoot = await mkdtemp(path.join(tmpdir(), `hagiscript-vendored-${options.packageName}-`))
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const archivePath = path.join(stagingRoot, assetName)
|
|
140
|
+
const extractRoot = path.join(stagingRoot, "extract")
|
|
141
|
+
await downloadVendoredAsset(assetUrl, archivePath)
|
|
142
|
+
const extractedRoot = await extractVendoredArchive(
|
|
143
|
+
archivePath,
|
|
144
|
+
extractRoot,
|
|
145
|
+
path.extname(assetName).toLowerCase() === ".zip" ? "zip" : "tar.gz"
|
|
146
|
+
)
|
|
147
|
+
await replaceDirectory(extractedRoot, options.prefixRoot)
|
|
148
|
+
|
|
149
|
+
const entrypointPath = path.join(options.prefixRoot, options.entrypointRelativePath)
|
|
150
|
+
await access(entrypointPath)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
entrypointPath,
|
|
154
|
+
releaseRepository: repository,
|
|
155
|
+
releaseTag,
|
|
156
|
+
releaseName: releaseTag.replace(/^v/u, ""),
|
|
157
|
+
releaseUrl: `${baseUrl}/${repository}/releases/tag/${encodeURIComponent(releaseTag)}`,
|
|
158
|
+
releaseAssetName: assetName,
|
|
159
|
+
releaseAssetUrl: assetUrl
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
await rm(stagingRoot, { recursive: true, force: true })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function writeManagedPackageLauncher(filePath, options) {
|
|
167
|
+
await ensureDirectory(path.dirname(filePath))
|
|
168
|
+
const configLoader =
|
|
169
|
+
options.serviceKind === "omniroute"
|
|
170
|
+
? `
|
|
171
|
+
const runtimeConfig = await loadOmnirouteConfig()
|
|
172
|
+
const runtimeEnv = {
|
|
173
|
+
...(runtimeConfig.dataDir ? { DATA_DIR: runtimeConfig.dataDir } : {}),
|
|
174
|
+
...(runtimeConfig.logDir ? { LOG_DIR: runtimeConfig.logDir } : {}),
|
|
175
|
+
...(runtimeConfig.port ? { PORT: runtimeConfig.port } : {})
|
|
176
|
+
}
|
|
177
|
+
`
|
|
178
|
+
: ""
|
|
179
|
+
const configHelpers =
|
|
180
|
+
options.serviceKind === "omniroute"
|
|
181
|
+
? `
|
|
182
|
+
async function loadOmnirouteConfig() {
|
|
183
|
+
try {
|
|
184
|
+
const [{ readFile }, { parse }] = await Promise.all([
|
|
185
|
+
import("node:fs/promises"),
|
|
186
|
+
import("yaml")
|
|
187
|
+
])
|
|
188
|
+
const loaded = parse(await readFile(configPath, "utf8"))
|
|
189
|
+
const listen = typeof loaded?.listen === "string" ? loaded.listen : ""
|
|
190
|
+
const portMatch = listen.match(/:(\\d+)$/u)
|
|
191
|
+
return {
|
|
192
|
+
dataDir: typeof loaded?.dataDir === "string" ? loaded.dataDir : ${JSON.stringify(
|
|
193
|
+
options.defaultEnv?.DATA_DIR ?? ""
|
|
194
|
+
)},
|
|
195
|
+
logDir: typeof loaded?.logDir === "string" ? loaded.logDir : ${JSON.stringify(
|
|
196
|
+
options.defaultEnv?.LOG_DIR ?? ""
|
|
197
|
+
)},
|
|
198
|
+
port: portMatch?.[1] ?? ${JSON.stringify(options.defaultEnv?.PORT ?? "")}
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
dataDir: ${JSON.stringify(options.defaultEnv?.DATA_DIR ?? "")},
|
|
203
|
+
logDir: ${JSON.stringify(options.defaultEnv?.LOG_DIR ?? "")},
|
|
204
|
+
port: ${JSON.stringify(options.defaultEnv?.PORT ?? "")}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
`
|
|
209
|
+
: ""
|
|
210
|
+
|
|
211
|
+
await writeFile(
|
|
212
|
+
filePath,
|
|
213
|
+
`#!/usr/bin/env node
|
|
214
|
+
import { spawn } from "node:child_process"
|
|
215
|
+
|
|
216
|
+
const entrypointPath = ${JSON.stringify(options.entrypointPath)}
|
|
217
|
+
const configPath = ${JSON.stringify(options.configPath)}
|
|
218
|
+
const baseArgs = ${JSON.stringify(options.baseArgs ?? [])}
|
|
219
|
+
const baseEnv = ${JSON.stringify(options.defaultEnv ?? {})}
|
|
220
|
+
${configLoader}
|
|
221
|
+
const child = spawn(
|
|
222
|
+
process.execPath,
|
|
223
|
+
[entrypointPath, ...baseArgs, ...process.argv.slice(2)],
|
|
224
|
+
{
|
|
225
|
+
stdio: "inherit",
|
|
226
|
+
env: {
|
|
227
|
+
...process.env,
|
|
228
|
+
...baseEnv,
|
|
229
|
+
${options.serviceKind === "omniroute" ? "...runtimeEnv" : ""}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const forwardSignal = (signal) => {
|
|
235
|
+
if (!child.killed) {
|
|
236
|
+
child.kill(signal)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"))
|
|
241
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"))
|
|
242
|
+
|
|
243
|
+
child.on("exit", (code, signal) => {
|
|
244
|
+
if (signal) {
|
|
245
|
+
process.kill(process.pid, signal)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.exit(code ?? 0)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
child.on("error", (error) => {
|
|
253
|
+
process.stderr.write(String(error?.stack ?? error) + "\\n")
|
|
254
|
+
process.exit(1)
|
|
255
|
+
})
|
|
256
|
+
${configHelpers}
|
|
257
|
+
`,
|
|
258
|
+
"utf8"
|
|
259
|
+
)
|
|
260
|
+
await makeExecutable(filePath)
|
|
261
|
+
return filePath
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function requiredEnv(name) {
|
|
265
|
+
const value = process.env[name]?.trim()
|
|
266
|
+
|
|
267
|
+
if (!value) {
|
|
268
|
+
throw new Error(`Missing runtime script environment variable: ${name}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return value
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function makeExecutable(filePath) {
|
|
275
|
+
if (process.platform === "win32") {
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await chmod(filePath, 0o755)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function runManagedCommand(command, args, options = {}) {
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
const child = spawn(command, args, {
|
|
285
|
+
env: options.env,
|
|
286
|
+
cwd: options.cwd,
|
|
287
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
288
|
+
})
|
|
289
|
+
let stdout = ""
|
|
290
|
+
let stderr = ""
|
|
291
|
+
|
|
292
|
+
child.stdout.on("data", (chunk) => {
|
|
293
|
+
stdout += chunk.toString()
|
|
294
|
+
})
|
|
295
|
+
child.stderr.on("data", (chunk) => {
|
|
296
|
+
stderr += chunk.toString()
|
|
297
|
+
})
|
|
298
|
+
child.on("error", reject)
|
|
299
|
+
child.on("exit", (code, signal) => {
|
|
300
|
+
if (code === 0) {
|
|
301
|
+
resolve({ stdout, stderr })
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
reject(
|
|
306
|
+
new Error(
|
|
307
|
+
`Managed command failed: ${command} ${args.join(" ")} (code=${code ?? "null"}, signal=${signal ?? "null"})\n${stderr || stdout}`.trim()
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseGitHubRepositoryConfig(repositoryValue, baseUrlValue) {
|
|
315
|
+
const trimmedRepository = String(repositoryValue || "").trim()
|
|
316
|
+
const repository =
|
|
317
|
+
trimmedRepository.startsWith("https://github.com/")
|
|
318
|
+
? trimmedRepository
|
|
319
|
+
.replace(/^https:\/\/github\.com\//u, "")
|
|
320
|
+
.replace(/\.git$/u, "")
|
|
321
|
+
.replace(/\/+$/u, "")
|
|
322
|
+
: trimmedRepository
|
|
323
|
+
|
|
324
|
+
if (!/^[^/]+\/[^/]+$/u.test(repository)) {
|
|
325
|
+
throw new Error(`Invalid vendored GitHub repository: ${trimmedRepository}`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
repository,
|
|
330
|
+
baseUrl: String(baseUrlValue || "https://github.com").replace(/\/+$/u, "")
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildVendoredAssetName(options) {
|
|
335
|
+
const releaseVersion = normalizeVendoredReleaseVersion(options.releaseTag)
|
|
336
|
+
return `${options.packageName}-${releaseVersion}-${options.platform}-${options.arch}${getVendoredArchiveExtension(options.platform)}`
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildVendoredAssetUrl(baseUrl, repository, releaseTag, assetName) {
|
|
340
|
+
return `${baseUrl}/${repository}/releases/download/${encodeURIComponent(releaseTag)}/${encodeURIComponent(assetName)}`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeVendoredReleaseVersion(releaseTag) {
|
|
344
|
+
const normalized = String(releaseTag || "").trim().replace(/^v/u, "")
|
|
345
|
+
if (!normalized) {
|
|
346
|
+
throw new Error(`Invalid vendored release tag: ${String(releaseTag)}`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return normalized
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function normalizeVendoredPlatform(platform) {
|
|
353
|
+
switch (platform) {
|
|
354
|
+
case "darwin":
|
|
355
|
+
return "macos"
|
|
356
|
+
case "win32":
|
|
357
|
+
return "windows"
|
|
358
|
+
default:
|
|
359
|
+
return "linux"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeVendoredArchitecture(arch) {
|
|
364
|
+
switch (String(arch).toLowerCase()) {
|
|
365
|
+
case "x64":
|
|
366
|
+
return "amd64"
|
|
367
|
+
case "arm64":
|
|
368
|
+
case "aarch64":
|
|
369
|
+
return "arm64"
|
|
370
|
+
default:
|
|
371
|
+
throw new Error(`Unsupported vendored runtime architecture: ${arch}`)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getVendoredArchiveExtension(platform) {
|
|
376
|
+
return platform === "windows" ? ".zip" : ".tar.gz"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function downloadVendoredAsset(url, destinationPath) {
|
|
380
|
+
const response = await globalThis.fetch(url)
|
|
381
|
+
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new Error(`Failed to download vendored asset ${url}: HTTP ${response.status}`)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!response.body) {
|
|
387
|
+
throw new Error(`Failed to download vendored asset ${url}: empty response body.`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await ensureDirectory(path.dirname(destinationPath))
|
|
391
|
+
const file = createWriteStream(destinationPath)
|
|
392
|
+
|
|
393
|
+
const reader = response.body.getReader()
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
while (true) {
|
|
397
|
+
const { done, value } = await reader.read()
|
|
398
|
+
if (done) {
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!file.write(value)) {
|
|
403
|
+
await new Promise((resolve) => file.once("drain", resolve))
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await new Promise((resolve, reject) =>
|
|
408
|
+
file.end((error) => (error ? reject(error) : resolve()))
|
|
409
|
+
)
|
|
410
|
+
} finally {
|
|
411
|
+
reader.releaseLock()
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function extractVendoredArchive(archivePath, stagingDirectory, archiveKind) {
|
|
416
|
+
await rm(stagingDirectory, { recursive: true, force: true })
|
|
417
|
+
await mkdir(stagingDirectory, { recursive: true })
|
|
418
|
+
|
|
419
|
+
if (archiveKind === "zip") {
|
|
420
|
+
await extractZipArchive(archivePath, stagingDirectory)
|
|
421
|
+
} else {
|
|
422
|
+
await extractTarGzArchive(archivePath, stagingDirectory)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const entries = await readdir(stagingDirectory, { withFileTypes: true })
|
|
426
|
+
const directories = entries.filter((entry) => entry.isDirectory())
|
|
427
|
+
|
|
428
|
+
if (directories.length !== 1) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Expected exactly one extracted vendored root in ${stagingDirectory}, found ${directories.length}.`
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return path.join(stagingDirectory, directories[0].name)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function extractZipArchive(archivePath, destination) {
|
|
438
|
+
if (process.platform === "win32") {
|
|
439
|
+
await runManagedCommand("powershell.exe", [
|
|
440
|
+
"-NoLogo",
|
|
441
|
+
"-NoProfile",
|
|
442
|
+
"-Command",
|
|
443
|
+
`Expand-Archive -Path '${escapePowerShell(archivePath.replaceAll("/", "\\"))}' -DestinationPath '${escapePowerShell(destination.replaceAll("/", "\\"))}' -Force`
|
|
444
|
+
])
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
await runManagedCommand("unzip", ["-q", archivePath, "-d", destination])
|
|
450
|
+
} catch {
|
|
451
|
+
await runManagedCommand("bsdtar", ["-xf", archivePath, "-C", destination])
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function extractTarGzArchive(archivePath, destination) {
|
|
456
|
+
try {
|
|
457
|
+
await runManagedCommand("tar", ["-xzf", archivePath, "-C", destination])
|
|
458
|
+
} catch {
|
|
459
|
+
await extractTarGzWithNode(archivePath, destination)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function extractTarGzWithNode(archivePath, destination) {
|
|
464
|
+
const tarPath = `${archivePath}.tar`
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await pipeline(
|
|
468
|
+
createReadStream(archivePath),
|
|
469
|
+
createGunzip(),
|
|
470
|
+
createWriteStream(tarPath)
|
|
471
|
+
)
|
|
472
|
+
await extractTarBuffer(await readFile(tarPath), destination)
|
|
473
|
+
} finally {
|
|
474
|
+
await rm(tarPath, { force: true }).catch(() => undefined)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function extractTarBuffer(buffer, destination) {
|
|
479
|
+
let offset = 0
|
|
480
|
+
|
|
481
|
+
while (offset + 512 <= buffer.length) {
|
|
482
|
+
const header = buffer.subarray(offset, offset + 512)
|
|
483
|
+
if (header.every((byte) => byte === 0)) {
|
|
484
|
+
break
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const name = header.toString("utf8", 0, 100).replace(/\0.*$/u, "")
|
|
488
|
+
const sizeText = header
|
|
489
|
+
.toString("utf8", 124, 136)
|
|
490
|
+
.replace(/\0.*$/u, "")
|
|
491
|
+
.trim()
|
|
492
|
+
const typeFlag = header.toString("utf8", 156, 157)
|
|
493
|
+
const size = parseInt(sizeText || "0", 8)
|
|
494
|
+
const outputPath = safeArchiveJoin(destination, name)
|
|
495
|
+
|
|
496
|
+
if (typeFlag === "5") {
|
|
497
|
+
await mkdir(outputPath, { recursive: true })
|
|
498
|
+
} else if (typeFlag === "0" || typeFlag === "") {
|
|
499
|
+
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
500
|
+
await writeFile(outputPath, buffer.subarray(offset + 512, offset + 512 + size))
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
offset += 512 + Math.ceil(size / 512) * 512
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function safeArchiveJoin(root, entryName) {
|
|
508
|
+
const normalized = path.join(root, entryName)
|
|
509
|
+
const relativePath = path.relative(root, normalized)
|
|
510
|
+
|
|
511
|
+
if (
|
|
512
|
+
!entryName ||
|
|
513
|
+
relativePath.startsWith("..") ||
|
|
514
|
+
relativePath.includes(`${path.sep}..${path.sep}`) ||
|
|
515
|
+
path.isAbsolute(entryName)
|
|
516
|
+
) {
|
|
517
|
+
throw new Error(`Vendored archive entry escapes the extraction root: ${entryName}`)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return normalized
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function replaceDirectory(sourceDirectory, targetDirectory) {
|
|
524
|
+
await rm(targetDirectory, { recursive: true, force: true })
|
|
525
|
+
await ensureDirectory(path.dirname(targetDirectory))
|
|
526
|
+
await rename(sourceDirectory, targetDirectory)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function escapePowerShell(value) {
|
|
530
|
+
return value.replaceAll("'", "''")
|
|
531
|
+
}
|