@fork-hu/cli2 0.0.0-stage → 1.0.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 +115 -3
- package/dist/fork.mjs +603 -0
- package/package.json +49 -6
package/README.md
CHANGED
|
@@ -1,3 +1,115 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Fork szerver telepítése – @fork-hu/cli2 1.x
|
|
2
|
+
|
|
3
|
+
Helyszíni (Windows) telepítés és frissítés a privát `fork-hu/fork-releases` repó GitHub Release-eiből. A régi, 0.2.x-es CLI-vel készült telepítéseket a [Régi telepítés átvétele](#régi-telepítés-átvétele) rész veszi át.
|
|
4
|
+
|
|
5
|
+
## Előfeltételek
|
|
6
|
+
|
|
7
|
+
- Windows 10 vagy 11, **rendszergazdaként** megnyitott PowerShell.
|
|
8
|
+
- [Node.js 24 LTS](https://nodejs.org/). A szolgáltatás azzal a Node-dal fut, amellyel a CLI-t futtatod.
|
|
9
|
+
- MySQL 8.4 a `fork` adatbázissal és a `fork`/`fork` felhasználóval.
|
|
10
|
+
- Az adatkönyvtár létrehozásakor be kell állítani: `lower_case_table_names=1`, `sql_mode=""`.
|
|
11
|
+
- Új helyszínen az adatbázist továbbra is az alap-dumpból kell létrehozni: üres adatbázison a szerver nem indul el.
|
|
12
|
+
- A Forktól kapott **`.forkdeploy`** fájl. Ebben van a domain, a tanúsítvány és a helyszíni release-token (`releaseToken`).
|
|
13
|
+
- Szabad 80-as és 443-as port, internet-hozzáférés a `github.com`-hoz.
|
|
14
|
+
|
|
15
|
+
## 1. A CLI telepítése
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
npm i -g @fork-hu/cli2
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Frissíteni ugyanezzel a paranccsal lehet. A parancs neve `fork2`. A régi, 0.2.x-es CLI (`@fork-hu/cli`, `fork` parancs) egymás mellett megmaradhat vele, és a beállításaik is külön helyen vannak.
|
|
22
|
+
|
|
23
|
+
## 2. Első telepítés
|
|
24
|
+
|
|
25
|
+
1. Hozd létre a telepítési mappát (például `C:\fork-deploy`), és másold bele a `.forkdeploy` fájlt.
|
|
26
|
+
2. Futtasd az initet. Ellenőrzi és kiírja a tanúsítványokat, a CA-t felveszi a Root tárolóba, beírja a hosts-bejegyzést, és elmenti a release-tokent. Egy összesítő után megerősítést kér.
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
fork2 init C:\fork-deploy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. Ha az adatbázis nem a `127.0.0.1:3306`-on, `fork`/`fork` felhasználóval érhető el, hozd létre a `C:\fork-deploy\data\config.local.json` fájlt, például:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{ "database": { "port": 3307, "password": "..." } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
4. Telepítés: letöltés, aláírás- és hash-ellenőrzés, `npm ci`, Windows-szolgáltatás, indítás, egészség-ellenőrzés.
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
fork2 install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Béta verzióhoz `fork2 install beta`, egy adott verzióhoz `fork2 install v1.6.0`.
|
|
45
|
+
|
|
46
|
+
5. Ellenőrzés: futtasd a `fork2 status` parancsot, majd nyisd meg böngészőben a `https://<domain>/admin` oldalt. A szerver első induláskor saját titkokat generál, ezért a rendelő terminálokat párosítani kell.
|
|
47
|
+
|
|
48
|
+
## Frissítés és visszaállítás
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
fork2 update
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Az alapértelmezett csatorna a `latest`; `fork2 update beta` a bétát, `fork2 update v1.6.0` egy adott verziót telepít.
|
|
55
|
+
- Az új verzió a régi mellé kerül. Ha 90 másodpercen belül nem válaszol, a CLI magától visszaáll az előzőre.
|
|
56
|
+
- Kézi visszaállás az előző verzióra:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
fork2 rollback
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Régi telepítés átvétele
|
|
63
|
+
|
|
64
|
+
0.2.x-es CLI-vel készült telepítésnél (például `C:\fork-deploy\server`):
|
|
65
|
+
|
|
66
|
+
1. Telepítsd a Node.js 24-et és az új CLI-t (1. lépés).
|
|
67
|
+
2. Ha a `.forkdeploy` fájlban még nincs `releaseToken`, add meg a tokent:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
fork2 token <token>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. Futtasd az átvételt. Eltávolítja a régi szolgáltatást, a régi `server` mappát `server-legacy-<dátum>` néven félreteszi, és telepíti az új verziót.
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
fork2 migrate
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. **Minden terminált újra kell párosítani**, mert az új szerver saját JWT-titkot használ.
|
|
80
|
+
|
|
81
|
+
## Üzemeltetés
|
|
82
|
+
|
|
83
|
+
| Parancs | Mit csinál |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `fork2 status` | telepített és elérhető verziók |
|
|
86
|
+
| `fork2 start` / `stop` / `restart` | a „Fork Server” Windows-szolgáltatás kezelése |
|
|
87
|
+
| `fork2 logs` | a szolgáltatás naplói |
|
|
88
|
+
| `fork2 token <token>` | lejárt vagy visszavont release-token cseréje |
|
|
89
|
+
| `fork2 uninstall` | a szolgáltatás eltávolítása (a `releases\` és a `data\` megmarad) |
|
|
90
|
+
| `fork2 config` | a CLI beállításfájljának helye (`%APPDATA%\fork\cli.json`) |
|
|
91
|
+
|
|
92
|
+
A telepítési mappa szerkezete:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
C:\fork-deploy\
|
|
96
|
+
ssl\ tanúsítványok a .forkdeploy-ból
|
|
97
|
+
releases\<tag>\ az aktív és az előző verzió
|
|
98
|
+
server junction az aktív verzióra
|
|
99
|
+
data\ .fork-secrets.json, access.log, config.local.json, sslcert -> ..\ssl
|
|
100
|
+
launcher\ ezt futtatja a szolgáltatás; naplók: launcher\daemon\
|
|
101
|
+
state.json aktív és előző verzió
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Mentés:** az adatbázis és a `data\.fork-secrets.json`. Ha ez a fájl elvész, minden terminált újra kell párosítani.
|
|
105
|
+
|
|
106
|
+
## Hibaelhárítás
|
|
107
|
+
|
|
108
|
+
| Hiba | Teendő |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `GitHub 401` / `404 … check the release token` | A token lejárt, visszavonták, vagy nincs olvasási joga a `fork-releases`-re: `fork2 token <új token>`. |
|
|
111
|
+
| `signature is invalid` / `is corrupted` | A letöltött csomag nem a Forktól jött, vagy megsérült; a CLI nem telepíti. Próbáld újra, és ha ismétlődik, jelezd. |
|
|
112
|
+
| `Update … failed, … is running again` | Az új verzió nem indult el, a régi fut. Az okot a `fork2 logs` mutatja (gyakran DB-kapcsolat vagy foglalt port). |
|
|
113
|
+
| `needs administrator rights` | Rendszergazdaként megnyitott PowerShellből futtasd. |
|
|
114
|
+
| `Node.js 24+ is required` | Telepítsd a Node.js 24 LTS-t, utána futtasd újra a parancsot. |
|
|
115
|
+
| `A legacy installation is in the way` | 0.2.x-es telepítés van a mappában: `fork2 migrate`. |
|
package/dist/fork.mjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync as renameSync2, statSync } from "fs";
|
|
6
|
+
import { createRequire as createRequire2 } from "module";
|
|
7
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
8
|
+
import { createInterface } from "readline/promises";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
|
|
15
|
+
// src/release-key.ts
|
|
16
|
+
var RELEASE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
17
|
+
MCowBQYDK2VwAyEAbZ7I1PbVBqlG5QZGJCDr51GARB4oAAWKseckv3UIgek=
|
|
18
|
+
-----END PUBLIC KEY-----
|
|
19
|
+
`;
|
|
20
|
+
var DEFAULT_RELEASE_REPO = "fork-hu/fork-releases";
|
|
21
|
+
|
|
22
|
+
// src/config.ts
|
|
23
|
+
var defaults = {
|
|
24
|
+
releaseRepo: DEFAULT_RELEASE_REPO,
|
|
25
|
+
healthUrl: "https://127.0.0.1/api/status"
|
|
26
|
+
};
|
|
27
|
+
var configPath = () => process.env.FORK_CLI_CONFIG ?? join(process.env.APPDATA ?? join(homedir(), ".config"), "fork", "cli.json");
|
|
28
|
+
var loadConfig = () => {
|
|
29
|
+
const file = configPath();
|
|
30
|
+
if (!existsSync(file)) return { ...defaults };
|
|
31
|
+
return { ...defaults, ...JSON.parse(readFileSync(file, "utf8")) };
|
|
32
|
+
};
|
|
33
|
+
var saveConfig = (config) => {
|
|
34
|
+
const file = configPath();
|
|
35
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
36
|
+
writeFileSync(file, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
|
|
37
|
+
};
|
|
38
|
+
var legacyInstallLocation = () => {
|
|
39
|
+
const file = join(process.env.APPDATA ?? join(homedir(), ".config"), "fork-cli-nodejs", "Config", "config.json");
|
|
40
|
+
if (!existsSync(file)) return void 0;
|
|
41
|
+
return JSON.parse(readFileSync(file, "utf8"))["install-location"];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/deploy.ts
|
|
45
|
+
import { X509Certificate, createPrivateKey } from "crypto";
|
|
46
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
47
|
+
import { join as join2 } from "path";
|
|
48
|
+
var findForkDeploy = (dir) => {
|
|
49
|
+
const name2 = existsSync2(dir) ? readdirSync(dir).find((f) => f.endsWith(".forkdeploy")) : void 0;
|
|
50
|
+
return name2 ? join2(dir, name2) : void 0;
|
|
51
|
+
};
|
|
52
|
+
var readForkDeploy = (file) => {
|
|
53
|
+
const data = JSON.parse(readFileSync2(file, "utf8"));
|
|
54
|
+
for (const field of ["domain", "cert", "key", "ca"]) {
|
|
55
|
+
if (!data[field]) throw new Error(`${file} has no "${field}"`);
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
};
|
|
59
|
+
var describeForkDeploy = (deploy) => {
|
|
60
|
+
const cert = new X509Certificate(deploy.cert);
|
|
61
|
+
const ca = new X509Certificate(deploy.ca);
|
|
62
|
+
return {
|
|
63
|
+
domain: deploy.domain,
|
|
64
|
+
subject: cert.subject.replace(/\n/g, ", "),
|
|
65
|
+
san: cert.subjectAltName ?? "none",
|
|
66
|
+
validTo: new Date(cert.validTo),
|
|
67
|
+
keyMatches: cert.checkPrivateKey(createPrivateKey(deploy.key)),
|
|
68
|
+
signedByCa: cert.verify(ca.publicKey),
|
|
69
|
+
hasReleaseToken: Boolean(deploy.releaseToken)
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
var writeSslFiles = (sslDir, deploy) => {
|
|
73
|
+
mkdirSync2(sslDir, { recursive: true });
|
|
74
|
+
writeFileSync2(join2(sslDir, "cert.pem"), deploy.cert);
|
|
75
|
+
writeFileSync2(join2(sslDir, "key.pem"), deploy.key, { mode: 384 });
|
|
76
|
+
writeFileSync2(join2(sslDir, "privkey.pem"), deploy.key, { mode: 384 });
|
|
77
|
+
writeFileSync2(join2(sslDir, "ca.pem"), deploy.ca);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/layout.ts
|
|
81
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readlinkSync, rmdirSync, symlinkSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
82
|
+
import { join as join3, resolve } from "path";
|
|
83
|
+
var paths = (root) => ({
|
|
84
|
+
root,
|
|
85
|
+
ssl: join3(root, "ssl"),
|
|
86
|
+
releases: join3(root, "releases"),
|
|
87
|
+
release: (tag) => join3(root, "releases", tag),
|
|
88
|
+
server: join3(root, "server"),
|
|
89
|
+
data: join3(root, "data"),
|
|
90
|
+
downloads: join3(root, "downloads"),
|
|
91
|
+
launcher: join3(root, "launcher"),
|
|
92
|
+
launcherScript: join3(root, "launcher", "run.cjs"),
|
|
93
|
+
state: join3(root, "state.json")
|
|
94
|
+
});
|
|
95
|
+
var readState = (p) => existsSync3(p.state) ? JSON.parse(readFileSync3(p.state, "utf8")) : {};
|
|
96
|
+
var writeState = (p, state) => writeFileSync3(p.state, JSON.stringify(state, null, " ") + "\n");
|
|
97
|
+
var isLink = (path) => {
|
|
98
|
+
try {
|
|
99
|
+
return lstatSync(path).isSymbolicLink();
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var removeLink = (path) => {
|
|
105
|
+
if (!isLink(path)) return;
|
|
106
|
+
try {
|
|
107
|
+
unlinkSync(path);
|
|
108
|
+
} catch {
|
|
109
|
+
rmdirSync(path);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var pointLink = (link, target) => {
|
|
113
|
+
removeLink(link);
|
|
114
|
+
if (existsSync3(link)) throw new Error(`${link} exists and is not a link; run "fork2 migrate" for a legacy installation`);
|
|
115
|
+
symlinkSync(resolve(target), link, "junction");
|
|
116
|
+
};
|
|
117
|
+
var linkTarget = (link) => isLink(link) ? resolve(readlinkSync(link)) : void 0;
|
|
118
|
+
var isLegacyLayout = (p) => existsSync3(p.server) && !isLink(p.server);
|
|
119
|
+
var LAUNCHER = `// Generated by @fork-hu/cli2. The Windows service runs this file, so it lives outside the release
|
|
120
|
+
// folders: it switches to the data directory and loads the active release through the "server" junction.
|
|
121
|
+
const path = require('node:path')
|
|
122
|
+
process.chdir(path.join(__dirname, '..', 'data'))
|
|
123
|
+
require(path.join(__dirname, '..', 'server', 'main.js'))
|
|
124
|
+
`;
|
|
125
|
+
var ensureBaseLayout = (p) => {
|
|
126
|
+
for (const dir of [p.releases, p.data, p.downloads, p.launcher]) mkdirSync3(dir, { recursive: true });
|
|
127
|
+
writeFileSync3(p.launcherScript, LAUNCHER);
|
|
128
|
+
const sslLink = join3(p.data, "sslcert");
|
|
129
|
+
if (existsSync3(p.ssl) && !isLink(sslLink)) symlinkSync(p.ssl, sslLink, "junction");
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/log.ts
|
|
133
|
+
import { styleText } from "util";
|
|
134
|
+
var log = {
|
|
135
|
+
info: (...parts) => console.log(...parts),
|
|
136
|
+
step: (text) => console.log(styleText("cyan", "\u203A ") + text),
|
|
137
|
+
success: (text) => console.log(styleText("green", "\u2714 ") + text),
|
|
138
|
+
warn: (text) => console.warn(styleText("yellow", "! ") + text),
|
|
139
|
+
error: (text) => console.error(styleText("red", "\u2716 ") + text)
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/server.ts
|
|
143
|
+
import { spawn } from "child_process";
|
|
144
|
+
import { copyFileSync, existsSync as existsSync4, readdirSync as readdirSync2, renameSync, rmSync } from "fs";
|
|
145
|
+
import { request as request2 } from "https";
|
|
146
|
+
import { join as join4 } from "path";
|
|
147
|
+
import AdmZip from "adm-zip";
|
|
148
|
+
|
|
149
|
+
// src/github.ts
|
|
150
|
+
import { createWriteStream } from "fs";
|
|
151
|
+
import { Readable } from "stream";
|
|
152
|
+
import { pipeline } from "stream/promises";
|
|
153
|
+
var GitHubError = class extends Error {
|
|
154
|
+
status;
|
|
155
|
+
constructor(message, status) {
|
|
156
|
+
super(message);
|
|
157
|
+
this.status = status;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var API = "https://api.github.com";
|
|
161
|
+
var request = async (fetchImpl, token, path, accept = "application/vnd.github+json") => {
|
|
162
|
+
const response = await fetchImpl(API + path, {
|
|
163
|
+
headers: {
|
|
164
|
+
Accept: accept,
|
|
165
|
+
Authorization: `Bearer ${token}`,
|
|
166
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
167
|
+
"User-Agent": "fork-cli"
|
|
168
|
+
},
|
|
169
|
+
redirect: "follow"
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const hint = response.status === 401 || response.status === 404 ? " (check the release token: it needs read access to the release repository)" : "";
|
|
173
|
+
throw new GitHubError(`GitHub ${response.status} for ${path}${hint}`, response.status);
|
|
174
|
+
}
|
|
175
|
+
return response;
|
|
176
|
+
};
|
|
177
|
+
var toRelease = (r) => ({ tag: r.tag_name, prerelease: r.prerelease, assets: r.assets });
|
|
178
|
+
var resolveRelease = async (repo, token, selector, fetchImpl = fetch) => {
|
|
179
|
+
if (selector === "latest") {
|
|
180
|
+
const response2 = await request(fetchImpl, token, `/repos/${repo}/releases/latest`);
|
|
181
|
+
return toRelease(await response2.json());
|
|
182
|
+
}
|
|
183
|
+
if (selector === "beta") {
|
|
184
|
+
const response2 = await request(fetchImpl, token, `/repos/${repo}/releases?per_page=30`);
|
|
185
|
+
const releases = (await response2.json()).filter((r) => !r.draft);
|
|
186
|
+
if (!releases.length) throw new GitHubError(`No releases in ${repo}`, 404);
|
|
187
|
+
return toRelease(releases[0]);
|
|
188
|
+
}
|
|
189
|
+
if (!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(selector)) {
|
|
190
|
+
throw new Error(`Unknown release "${selector}": use latest, beta or a version tag such as v1.6.0`);
|
|
191
|
+
}
|
|
192
|
+
const response = await request(fetchImpl, token, `/repos/${repo}/releases/tags/${selector}`);
|
|
193
|
+
return toRelease(await response.json());
|
|
194
|
+
};
|
|
195
|
+
var findAsset = (release, name2) => {
|
|
196
|
+
const asset = release.assets.find((a) => a.name === name2);
|
|
197
|
+
if (!asset) throw new Error(`Release ${release.tag} has no ${name2}`);
|
|
198
|
+
return asset;
|
|
199
|
+
};
|
|
200
|
+
var downloadAsset = async (repo, token, asset, destination, fetchImpl = fetch) => {
|
|
201
|
+
const response = await request(fetchImpl, token, `/repos/${repo}/releases/assets/${asset.id}`, "application/octet-stream");
|
|
202
|
+
if (!response.body) throw new Error(`Empty download for ${asset.name}`);
|
|
203
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
|
|
204
|
+
};
|
|
205
|
+
var downloadText = async (repo, token, asset, fetchImpl = fetch) => {
|
|
206
|
+
const response = await request(fetchImpl, token, `/repos/${repo}/releases/assets/${asset.id}`, "application/octet-stream");
|
|
207
|
+
return response.text();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/verify.ts
|
|
211
|
+
import { createHash, createPublicKey, verify } from "crypto";
|
|
212
|
+
import { createReadStream } from "fs";
|
|
213
|
+
var verifySignature = (content, signatureBase64, publicKeyPem) => verify(null, Buffer.from(content, "utf8"), createPublicKey(publicKeyPem), Buffer.from(signatureBase64.trim(), "base64"));
|
|
214
|
+
var parseChecksums = (content) => {
|
|
215
|
+
const sums = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const line of content.split(/\r?\n/)) {
|
|
217
|
+
const match = /^([0-9a-f]{64}) [ *](.+)$/.exec(line.trim());
|
|
218
|
+
if (match) sums.set(match[2], match[1]);
|
|
219
|
+
}
|
|
220
|
+
return sums;
|
|
221
|
+
};
|
|
222
|
+
var sha256File = async (file) => {
|
|
223
|
+
const hash = createHash("sha256");
|
|
224
|
+
for await (const chunk of createReadStream(file)) hash.update(chunk);
|
|
225
|
+
return hash.digest("hex");
|
|
226
|
+
};
|
|
227
|
+
var verifyRelease = async (options) => {
|
|
228
|
+
if (!verifySignature(options.sums, options.signature, options.publicKey)) {
|
|
229
|
+
throw new Error("SHA256SUMS signature is invalid: the release was not signed by Fork, refusing to install");
|
|
230
|
+
}
|
|
231
|
+
const expected = parseChecksums(options.sums).get(options.fileName);
|
|
232
|
+
if (!expected) throw new Error(`SHA256SUMS does not list ${options.fileName}`);
|
|
233
|
+
const actual = await sha256File(options.file);
|
|
234
|
+
if (actual !== expected) throw new Error(`${options.fileName} is corrupted (sha256 ${actual}, expected ${expected})`);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/server.ts
|
|
238
|
+
var archiveName = (tag) => `fork-server-${tag}.zip`;
|
|
239
|
+
var npmInstall = (dir) => new Promise((resolve3, reject) => {
|
|
240
|
+
const lock = existsSync4(join4(dir, "package-lock.json"));
|
|
241
|
+
const command2 = `npm ${lock ? "ci" : "install"} --omit=dev --no-audit --no-fund`;
|
|
242
|
+
const child = spawn(command2, { cwd: dir, shell: true, stdio: "inherit" });
|
|
243
|
+
child.on("error", reject);
|
|
244
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`${command2} failed (exit code ${code})`)));
|
|
245
|
+
});
|
|
246
|
+
var httpHealthCheck = (url, timeoutMs = 9e4) => async () => {
|
|
247
|
+
const deadline = Date.now() + timeoutMs;
|
|
248
|
+
let last = "no answer";
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
const status = await new Promise((resolve3) => {
|
|
251
|
+
const req = request2(url, { rejectUnauthorized: false, timeout: 5e3 }, (res) => {
|
|
252
|
+
res.resume();
|
|
253
|
+
resolve3(res.statusCode ?? 0);
|
|
254
|
+
});
|
|
255
|
+
req.on("error", (err) => resolve3(err.message));
|
|
256
|
+
req.on("timeout", () => req.destroy(new Error("timeout")));
|
|
257
|
+
req.end();
|
|
258
|
+
});
|
|
259
|
+
if (status === 200) return;
|
|
260
|
+
last = String(status);
|
|
261
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
262
|
+
}
|
|
263
|
+
throw new Error(`Health check of ${url} failed: ${last}`);
|
|
264
|
+
};
|
|
265
|
+
var ServerManager = class {
|
|
266
|
+
p;
|
|
267
|
+
deps;
|
|
268
|
+
constructor(deps) {
|
|
269
|
+
this.deps = deps;
|
|
270
|
+
this.p = paths(deps.root);
|
|
271
|
+
}
|
|
272
|
+
current() {
|
|
273
|
+
return readState(this.p).current;
|
|
274
|
+
}
|
|
275
|
+
resolve(selector) {
|
|
276
|
+
return resolveRelease(this.deps.repo, this.deps.token, selector, this.deps.fetchImpl);
|
|
277
|
+
}
|
|
278
|
+
/** Downloads, verifies and unpacks a release into releases/<tag> (no service changes). */
|
|
279
|
+
async prepare(release) {
|
|
280
|
+
const target = this.p.release(release.tag);
|
|
281
|
+
if (existsSync4(join4(target, "main.js")) && existsSync4(join4(target, "node_modules"))) {
|
|
282
|
+
log.step(`${release.tag} is already unpacked`);
|
|
283
|
+
return target;
|
|
284
|
+
}
|
|
285
|
+
const name2 = archiveName(release.tag);
|
|
286
|
+
const archive = join4(this.p.downloads, name2);
|
|
287
|
+
const { repo, token, fetchImpl } = this.deps;
|
|
288
|
+
log.step(`Downloading ${name2}`);
|
|
289
|
+
await downloadAsset(repo, token, findAsset(release, name2), archive, fetchImpl);
|
|
290
|
+
const sums = await downloadText(repo, token, findAsset(release, "SHA256SUMS"), fetchImpl);
|
|
291
|
+
const signature = await downloadText(repo, token, findAsset(release, "SHA256SUMS.sig"), fetchImpl);
|
|
292
|
+
log.step("Verifying signature and checksum");
|
|
293
|
+
await verifyRelease({ sums, signature, publicKey: this.deps.publicKey ?? RELEASE_PUBLIC_KEY, file: archive, fileName: name2 });
|
|
294
|
+
log.step(`Unpacking to ${target}`);
|
|
295
|
+
const staging = target + ".partial";
|
|
296
|
+
rmSync(staging, { recursive: true, force: true });
|
|
297
|
+
new AdmZip(archive).extractAllTo(staging, true);
|
|
298
|
+
log.step("Installing dependencies");
|
|
299
|
+
await this.deps.installDependencies(staging);
|
|
300
|
+
rmSync(target, { recursive: true, force: true });
|
|
301
|
+
renameSync(staging, target);
|
|
302
|
+
rmSync(archive, { force: true });
|
|
303
|
+
return target;
|
|
304
|
+
}
|
|
305
|
+
/** Site specific runtime overrides live in data/ and are copied next to main.js of every release. */
|
|
306
|
+
applySiteConfig(releaseDir) {
|
|
307
|
+
const local = join4(this.p.data, "config.local.json");
|
|
308
|
+
if (existsSync4(local)) copyFileSync(local, join4(releaseDir, "config.local.json"));
|
|
309
|
+
}
|
|
310
|
+
/** First installation: layout, service, start. */
|
|
311
|
+
async install(selector) {
|
|
312
|
+
if (this.current()) throw new Error(`Fork server ${this.current()} is already installed; use "fork2 update"`);
|
|
313
|
+
const release = await this.resolve(selector);
|
|
314
|
+
ensureBaseLayout(this.p);
|
|
315
|
+
const dir = await this.prepare(release);
|
|
316
|
+
this.applySiteConfig(dir);
|
|
317
|
+
pointLink(this.p.server, dir);
|
|
318
|
+
log.step("Installing the Windows service");
|
|
319
|
+
await this.deps.service.install();
|
|
320
|
+
await this.deps.service.start();
|
|
321
|
+
await this.deps.healthCheck();
|
|
322
|
+
writeState(this.p, { current: release.tag });
|
|
323
|
+
log.success(`Fork server ${release.tag} is running`);
|
|
324
|
+
return release.tag;
|
|
325
|
+
}
|
|
326
|
+
/** Switches to another release; goes back to the previous one if the new one does not come up. */
|
|
327
|
+
async update(selector, { force = false } = {}) {
|
|
328
|
+
const state = readState(this.p);
|
|
329
|
+
if (!state.current) throw new Error('No Fork server installed; use "fork2 install"');
|
|
330
|
+
const release = await this.resolve(selector);
|
|
331
|
+
if (release.tag === state.current && !force) {
|
|
332
|
+
log.success(`Already on ${release.tag}`);
|
|
333
|
+
return release.tag;
|
|
334
|
+
}
|
|
335
|
+
ensureBaseLayout(this.p);
|
|
336
|
+
const dir = await this.prepare(release);
|
|
337
|
+
this.applySiteConfig(dir);
|
|
338
|
+
await this.switchTo(release.tag, state.current);
|
|
339
|
+
this.prune();
|
|
340
|
+
return release.tag;
|
|
341
|
+
}
|
|
342
|
+
async rollback() {
|
|
343
|
+
const state = readState(this.p);
|
|
344
|
+
if (!state.previous || !existsSync4(this.p.release(state.previous))) throw new Error("No previous release to roll back to");
|
|
345
|
+
await this.switchTo(state.previous, state.current);
|
|
346
|
+
return state.previous;
|
|
347
|
+
}
|
|
348
|
+
async switchTo(tag, from) {
|
|
349
|
+
const { service } = this.deps;
|
|
350
|
+
log.step(`Switching ${from} -> ${tag}`);
|
|
351
|
+
await service.stop();
|
|
352
|
+
pointLink(this.p.server, this.p.release(tag));
|
|
353
|
+
try {
|
|
354
|
+
await service.start();
|
|
355
|
+
await this.deps.healthCheck();
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log.error(`${tag} did not start (${err.message}); going back to ${from}`);
|
|
358
|
+
await service.stop().catch(() => void 0);
|
|
359
|
+
pointLink(this.p.server, this.p.release(from));
|
|
360
|
+
await service.start();
|
|
361
|
+
throw new Error(`Update to ${tag} failed, ${from} is running again`);
|
|
362
|
+
}
|
|
363
|
+
writeState(this.p, { current: tag, previous: from });
|
|
364
|
+
log.success(`Fork server ${tag} is running (previous: ${from})`);
|
|
365
|
+
}
|
|
366
|
+
/** Deletes old releases, keeping the active and the previous one (plus `keep` more). */
|
|
367
|
+
prune() {
|
|
368
|
+
const state = readState(this.p);
|
|
369
|
+
const keepTags = new Set([state.current, state.previous].filter(Boolean));
|
|
370
|
+
const active = linkTarget(this.p.server);
|
|
371
|
+
const others = readdirSync2(this.p.releases).filter((t) => !keepTags.has(t) && !t.endsWith(".partial"));
|
|
372
|
+
const extra = this.deps.keep ?? 0;
|
|
373
|
+
for (const tag of others.sort().reverse().slice(extra)) {
|
|
374
|
+
const dir = this.p.release(tag);
|
|
375
|
+
if (dir === active) continue;
|
|
376
|
+
rmSync(dir, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async uninstall() {
|
|
380
|
+
await this.deps.service.stop().catch(() => void 0);
|
|
381
|
+
await this.deps.service.uninstall();
|
|
382
|
+
rmSync(this.p.state, { force: true });
|
|
383
|
+
log.success("Windows service removed; releases/ and data/ were kept");
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/service.ts
|
|
388
|
+
import { createRequire } from "module";
|
|
389
|
+
var SERVICE_NAME = "Fork Server";
|
|
390
|
+
var waitFor = (svc, action, done) => new Promise((resolve3, reject) => {
|
|
391
|
+
for (const event of done) svc.on(event, () => resolve3());
|
|
392
|
+
svc.on("error", (err) => reject(err instanceof Error ? err : new Error(String(err))));
|
|
393
|
+
action();
|
|
394
|
+
});
|
|
395
|
+
var windowsService = (script, cwd) => {
|
|
396
|
+
const require2 = createRequire(import.meta.url);
|
|
397
|
+
const { Service } = require2("node-windows");
|
|
398
|
+
const create = () => new Service({ name: SERVICE_NAME, description: "Fork \xC9ttermi Program szerver", script, workingDirectory: cwd });
|
|
399
|
+
return {
|
|
400
|
+
install: () => {
|
|
401
|
+
const s = create();
|
|
402
|
+
return waitFor(s, () => s.install(), ["install", "alreadyinstalled"]);
|
|
403
|
+
},
|
|
404
|
+
uninstall: () => {
|
|
405
|
+
const s = create();
|
|
406
|
+
return waitFor(s, () => s.uninstall(), ["uninstall", "alreadyuninstalled"]);
|
|
407
|
+
},
|
|
408
|
+
start: () => {
|
|
409
|
+
const s = create();
|
|
410
|
+
return waitFor(s, () => s.start(), ["start"]);
|
|
411
|
+
},
|
|
412
|
+
stop: () => {
|
|
413
|
+
const s = create();
|
|
414
|
+
return waitFor(s, () => s.stop(), ["stop", "alreadystopped"]);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// src/main.ts
|
|
420
|
+
var MIN_NODE_MAJOR = 24;
|
|
421
|
+
var usage = `Usage: fork2 <command>
|
|
422
|
+
|
|
423
|
+
init [folder] set up a site from its .forkdeploy file (certificates, hosts entry, release token)
|
|
424
|
+
install [release] install the server (release: latest | beta | v1.6.0; default latest)
|
|
425
|
+
update [release] update to another release; rolls back by itself if it does not start
|
|
426
|
+
rollback go back to the previous release
|
|
427
|
+
status installed and available releases
|
|
428
|
+
start | stop | restart control the Windows service
|
|
429
|
+
logs print the service logs
|
|
430
|
+
migrate take over an installation made by the legacy 0.2.x CLI
|
|
431
|
+
uninstall remove the Windows service (keeps releases/ and data/)
|
|
432
|
+
token <token> replace the release repository token
|
|
433
|
+
config print the configuration file location`;
|
|
434
|
+
var ask = async (question, fallback = "") => {
|
|
435
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
436
|
+
const answer = (await rl.question(fallback ? `${question} [${fallback}] ` : `${question} `)).trim();
|
|
437
|
+
rl.close();
|
|
438
|
+
return answer || fallback;
|
|
439
|
+
};
|
|
440
|
+
var confirm = async (question) => /^(i|igen|y|yes)$/i.test(await ask(`${question} (i/n)`, "i"));
|
|
441
|
+
var isElevated = () => process.platform !== "win32" || spawnSync("net", ["session"], { stdio: "ignore" }).status === 0;
|
|
442
|
+
var requireElevated = () => {
|
|
443
|
+
if (!isElevated()) throw new Error('This command needs administrator rights: run it from a terminal opened "as administrator"');
|
|
444
|
+
};
|
|
445
|
+
var requireNode = () => {
|
|
446
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
447
|
+
if (major < MIN_NODE_MAJOR) {
|
|
448
|
+
throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required (this is ${process.version}); the service runs on the same Node.js as this command`);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var requireSetup = (config) => {
|
|
452
|
+
if (!config.installLocation) throw new Error('Not initialised: run "fork2 init" first');
|
|
453
|
+
if (!config.releaseToken) throw new Error('No release token: run "fork2 token <token>" or "fork2 init" with a .forkdeploy that contains one');
|
|
454
|
+
return { root: config.installLocation, token: config.releaseToken };
|
|
455
|
+
};
|
|
456
|
+
var manager = (config) => {
|
|
457
|
+
const { root, token } = requireSetup(config);
|
|
458
|
+
const p = paths(root);
|
|
459
|
+
return new ServerManager({
|
|
460
|
+
root,
|
|
461
|
+
repo: config.releaseRepo,
|
|
462
|
+
token,
|
|
463
|
+
service: windowsService(p.launcherScript, p.data),
|
|
464
|
+
healthCheck: httpHealthCheck(config.healthUrl),
|
|
465
|
+
installDependencies: npmInstall
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
var serviceOf = (config) => {
|
|
469
|
+
const p = paths(requireSetup(config).root);
|
|
470
|
+
return windowsService(p.launcherScript, p.data);
|
|
471
|
+
};
|
|
472
|
+
var commands = {
|
|
473
|
+
async init([folder]) {
|
|
474
|
+
requireElevated();
|
|
475
|
+
const config = loadConfig();
|
|
476
|
+
const root = resolve2(folder ?? await ask("Install location:", config.installLocation ?? "C:\\fork-deploy"));
|
|
477
|
+
const file = findForkDeploy(root);
|
|
478
|
+
if (!file) throw new Error(`No .forkdeploy file in ${root}`);
|
|
479
|
+
const deploy = readForkDeploy(file);
|
|
480
|
+
const info = describeForkDeploy(deploy);
|
|
481
|
+
log.info(`Domain: ${info.domain}`);
|
|
482
|
+
log.info(`Certificate: ${info.subject}`);
|
|
483
|
+
log.info(`SAN: ${info.san}`);
|
|
484
|
+
log.info(`Valid until: ${info.validTo.toLocaleDateString("hu-HU")}`);
|
|
485
|
+
log.info(`Private key: ${info.keyMatches ? "OK" : "DOES NOT MATCH"}`);
|
|
486
|
+
log.info(`CA signature: ${info.signedByCa ? "OK" : "INVALID"}`);
|
|
487
|
+
log.info(`Release token: ${info.hasReleaseToken ? "present" : 'missing (add it later with "fork2 token")'}`);
|
|
488
|
+
if (!info.keyMatches || !info.signedByCa) throw new Error("The .forkdeploy file is not valid");
|
|
489
|
+
if (!await confirm("Proceed?")) return;
|
|
490
|
+
const p = paths(root);
|
|
491
|
+
writeSslFiles(p.ssl, deploy);
|
|
492
|
+
log.success("SSL files written");
|
|
493
|
+
const certutil = spawnSync("certutil", ["-addstore", "Root", join5(p.ssl, "ca.pem")], { stdio: "inherit" });
|
|
494
|
+
if (certutil.status === 0) log.success("CA added to the Root store");
|
|
495
|
+
else log.warn("Could not add the CA to the Root store");
|
|
496
|
+
createRequire2(import.meta.url)("hostile").set("127.0.0.1", deploy.domain);
|
|
497
|
+
log.success(`hosts: ${deploy.domain} -> 127.0.0.1`);
|
|
498
|
+
saveConfig({
|
|
499
|
+
...config,
|
|
500
|
+
installLocation: root,
|
|
501
|
+
domain: deploy.domain,
|
|
502
|
+
releaseToken: deploy.releaseToken ?? config.releaseToken,
|
|
503
|
+
releaseRepo: deploy.releaseRepo ?? config.releaseRepo
|
|
504
|
+
});
|
|
505
|
+
log.success(`Initialised; next: "fork2 install"`);
|
|
506
|
+
},
|
|
507
|
+
async install([selector = "latest"]) {
|
|
508
|
+
requireElevated();
|
|
509
|
+
requireNode();
|
|
510
|
+
const config = loadConfig();
|
|
511
|
+
if (isLegacyLayout(paths(requireSetup(config).root))) throw new Error('A legacy installation is in the way: run "fork2 migrate"');
|
|
512
|
+
await manager(config).install(selector);
|
|
513
|
+
},
|
|
514
|
+
async update([selector = "latest", flag]) {
|
|
515
|
+
requireElevated();
|
|
516
|
+
requireNode();
|
|
517
|
+
await manager(loadConfig()).update(selector, { force: flag === "--force" });
|
|
518
|
+
},
|
|
519
|
+
async rollback() {
|
|
520
|
+
requireElevated();
|
|
521
|
+
await manager(loadConfig()).rollback();
|
|
522
|
+
},
|
|
523
|
+
async status() {
|
|
524
|
+
const config = loadConfig();
|
|
525
|
+
const m = manager(config);
|
|
526
|
+
log.info(`Install location: ${m.p.root}`);
|
|
527
|
+
log.info(`Installed: ${m.current() ?? "none"}`);
|
|
528
|
+
for (const channel of ["latest", "beta"]) {
|
|
529
|
+
const release = await m.resolve(channel).catch((err) => ({ tag: `unavailable (${err.message})` }));
|
|
530
|
+
log.info(`Available ${channel.padEnd(7)} ${release.tag}`);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
async start() {
|
|
534
|
+
requireElevated();
|
|
535
|
+
await serviceOf(loadConfig()).start();
|
|
536
|
+
},
|
|
537
|
+
async stop() {
|
|
538
|
+
requireElevated();
|
|
539
|
+
await serviceOf(loadConfig()).stop();
|
|
540
|
+
},
|
|
541
|
+
async restart(args2) {
|
|
542
|
+
await commands.stop(args2);
|
|
543
|
+
await commands.start(args2);
|
|
544
|
+
},
|
|
545
|
+
async logs() {
|
|
546
|
+
const p = paths(requireSetup(loadConfig()).root);
|
|
547
|
+
for (const file of ["forkserver.out.log", "forkserver.err.log"]) {
|
|
548
|
+
const path = join5(p.launcher, "daemon", file);
|
|
549
|
+
log.info(`===== ${path}`);
|
|
550
|
+
log.info(existsSync5(path) ? readFileSync4(path, "utf8") : "(missing)");
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
async migrate() {
|
|
554
|
+
requireElevated();
|
|
555
|
+
requireNode();
|
|
556
|
+
const config = loadConfig();
|
|
557
|
+
const root = config.installLocation ?? legacyInstallLocation();
|
|
558
|
+
if (!root) throw new Error('No installation found; use "fork2 init"');
|
|
559
|
+
const p = paths(root);
|
|
560
|
+
if (!isLegacyLayout(p)) {
|
|
561
|
+
log.success("Nothing to migrate");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
log.warn("The new server generates its own JWT secret: every terminal has to be paired again after this");
|
|
565
|
+
if (!await confirm(`Migrate the installation in ${root}?`)) return;
|
|
566
|
+
log.step("Removing the legacy service");
|
|
567
|
+
await windowsService(join5(p.server, "main.js"), p.server).stop().catch(() => void 0);
|
|
568
|
+
await windowsService(join5(p.server, "main.js"), p.server).uninstall();
|
|
569
|
+
const backup = `${p.server}-legacy-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
570
|
+
renameSync2(p.server, backup);
|
|
571
|
+
log.success(`Legacy server moved to ${backup}`);
|
|
572
|
+
saveConfig({ ...config, installLocation: root });
|
|
573
|
+
if (!loadConfig().releaseToken) {
|
|
574
|
+
log.warn('Set the release token with "fork2 token <token>", then run "fork2 install"');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
await manager(loadConfig()).install("latest");
|
|
578
|
+
},
|
|
579
|
+
async uninstall() {
|
|
580
|
+
requireElevated();
|
|
581
|
+
await manager(loadConfig()).uninstall();
|
|
582
|
+
},
|
|
583
|
+
async token([token]) {
|
|
584
|
+
if (!token) throw new Error("Usage: fork2 token <token>");
|
|
585
|
+
saveConfig({ ...loadConfig(), releaseToken: token });
|
|
586
|
+
log.success("Release token saved");
|
|
587
|
+
},
|
|
588
|
+
async config() {
|
|
589
|
+
const file = configPath();
|
|
590
|
+
log.info(file, existsSync5(file) ? `(${statSync(file).size} bytes)` : "(not created yet)");
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
var [name, ...args] = process.argv.slice(2);
|
|
594
|
+
var command = name ? commands[name] : void 0;
|
|
595
|
+
if (!command) {
|
|
596
|
+
log.info(usage);
|
|
597
|
+
process.exitCode = name ? 1 : 0;
|
|
598
|
+
} else {
|
|
599
|
+
command(args).catch((err) => {
|
|
600
|
+
log.error(err.message);
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
});
|
|
603
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@fork-hu/cli2",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@fork-hu/cli2",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Installs and updates the Fork server on-premise from GitHub Releases",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"author": "Fork <admin@fork.hu> (https://fork.hu)",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"fork",
|
|
9
|
+
"Fork"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/fork-hu/fork-2026-refactor.git",
|
|
14
|
+
"directory": "packages/cli"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"fork2": "dist/fork.mjs"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"os": [
|
|
24
|
+
"win32"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"dev": "node --experimental-strip-types src/main.ts",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
36
|
+
"prepublishOnly": "tsup"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"adm-zip": "0.6.1",
|
|
40
|
+
"hostile": "1.4.0",
|
|
41
|
+
"node-windows": "1.0.0-beta.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/adm-zip": "0.6.0",
|
|
45
|
+
"@types/node": "24.9.1",
|
|
46
|
+
"tsup": "8.5.1",
|
|
47
|
+
"typescript": "5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|