coda-backend 0.0.0-stage → 0.2.246
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.
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm owns the package; the standard installer owns the backend. This foreground launcher checks for updates
|
|
3
|
+
// and restarts through the updated npm entry point. The machine's process manager keeps the command running.
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
6
|
+
import { homedir, tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const script = fileURLToPath(import.meta.url);
|
|
11
|
+
const readJSON = file => JSON.parse(readFileSync(file, 'utf8'));
|
|
12
|
+
const log = message => console.log(`coda-backend: ${message}`);
|
|
13
|
+
function run(file, args) {
|
|
14
|
+
return new Promise((done, reject) => {
|
|
15
|
+
const child = spawn(file, args, { stdio: 'inherit', detached: true });
|
|
16
|
+
child.once('error', reject);
|
|
17
|
+
child.once('close', code => code === 0 ? done() : reject(new Error(`${file} 失败(${code})`)));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function checkForUpdate(directory, pkg, { signal, fetch = globalThis.fetch, execute = run } = {}) {
|
|
22
|
+
const { repository, channel } = pkg.coda;
|
|
23
|
+
const response = await fetch(`https://api.github.com/repos/${repository}/releases/latest`, {
|
|
24
|
+
signal: AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(30_000)]),
|
|
25
|
+
headers: { 'User-Agent': 'coda-backend', Accept: 'application/vnd.github+json' },
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) throw new Error(`检查更新失败:HTTP ${response.status}`);
|
|
28
|
+
const release = await response.json();
|
|
29
|
+
const offered = (channel === 'preview' ? /^v(\d+\.\d+\.\d+)-preview$/ : /^v(\d+\.\d+\.\d+)$/).exec(release.tag_name)?.[1];
|
|
30
|
+
if (release.draft || release.prerelease || !offered) throw new Error('发布不属于本后端的更新通道');
|
|
31
|
+
const current = pkg.version.replace(/-preview$/, '').split('.').map(Number);
|
|
32
|
+
if (!(offered.split('.').map((part, i) => Number(part) - current[i]).find(value => value !== 0) > 0)) return false;
|
|
33
|
+
const url = `https://github.com/${repository}/releases/download/${release.tag_name}/coda-backend.tgz`;
|
|
34
|
+
if (!release.assets?.some(asset => asset.name === 'coda-backend.tgz' && asset.browser_download_url === url))
|
|
35
|
+
throw new Error('发布没有 npm 安装包');
|
|
36
|
+
// Keep the same global prefix even if the process manager's npm config has changed.
|
|
37
|
+
const prefix = resolve(directory, '../../..');
|
|
38
|
+
if (join(prefix, 'lib/node_modules/coda-backend') !== directory) throw new Error('自动更新需要通过 npm install -g 安装');
|
|
39
|
+
log(`更新到 ${release.tag_name}`);
|
|
40
|
+
await execute('npm', ['install', '--global', '--prefix', prefix, '--no-audit', '--no-fund', url]);
|
|
41
|
+
signal?.throwIfAborted();
|
|
42
|
+
// Prepare dependencies while the current backend still serves. Let the installer finish its own cleanup on stop.
|
|
43
|
+
await execute(process.execPath, [join(directory, 'bin/coda-backend.mjs'), 'install']);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runBackend(root, { autoUpdate = true, check, firstCheck = 60_000, interval = 6 * 60 * 60_000,
|
|
48
|
+
signals = process, report = log } = {}) {
|
|
49
|
+
const config = readJSON(join(root, 'installation.json'));
|
|
50
|
+
const child = spawn(config.supervisor ?? config.node, config.supervisor ? ['--root', root]
|
|
51
|
+
: [join(root, 'backend-service.mjs'), 'run', '--root', root], { stdio: 'inherit', detached: true });
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
let timer, deadline, restart = false;
|
|
54
|
+
const stop = () => {
|
|
55
|
+
clearTimeout(timer); controller.abort(); child.kill('SIGTERM');
|
|
56
|
+
deadline ??= setTimeout(() => child.kill('SIGKILL'), 40_000);
|
|
57
|
+
};
|
|
58
|
+
const shutdown = () => { restart = false; stop(); };
|
|
59
|
+
const poll = async () => {
|
|
60
|
+
try {
|
|
61
|
+
if (await check({ signal: controller.signal }) && !controller.signal.aborted) { restart = true; stop(); }
|
|
62
|
+
} catch (error) { if (!controller.signal.aborted) report(`自动更新未完成:${error.message};下次继续检查`); }
|
|
63
|
+
if (!controller.signal.aborted) timer = setTimeout(poll, interval);
|
|
64
|
+
};
|
|
65
|
+
signals.on('SIGTERM', shutdown); signals.on('SIGINT', shutdown);
|
|
66
|
+
if (autoUpdate) timer = setTimeout(poll, firstCheck);
|
|
67
|
+
try {
|
|
68
|
+
const code = await new Promise((done, reject) => {
|
|
69
|
+
child.once('error', reject); child.once('close', code => done(code ?? 1));
|
|
70
|
+
});
|
|
71
|
+
return { code, restart };
|
|
72
|
+
} finally {
|
|
73
|
+
controller.abort(); clearTimeout(timer); clearTimeout(deadline);
|
|
74
|
+
signals.removeListener('SIGTERM', shutdown); signals.removeListener('SIGINT', shutdown);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
const directory = dirname(dirname(script)), payload = join(directory, 'payload');
|
|
80
|
+
const version = readFileSync(join(payload, 'version'), 'utf8').trim(), root = join(homedir(), '.local/share/coda');
|
|
81
|
+
const args = process.argv.slice(2), command = args[0] ?? 'run';
|
|
82
|
+
if (command === '--version' || command === 'version') { console.log(version); return; }
|
|
83
|
+
if (!['run', 'install'].includes(command) || args.slice(1).some(arg => arg !== '--no-auto-update')) {
|
|
84
|
+
console.log('coda-backend run [--no-auto-update]:在前台运行 Coda 后端,默认定期自动更新;交给本机的进程管理器常驻。');
|
|
85
|
+
process.exitCode = ['help', '--help'].includes(command) ? 0 : 2; return;
|
|
86
|
+
}
|
|
87
|
+
if (Number(process.versions.node.split('.')[0]) < 24) throw new Error(`需要 Node.js 24 或更新,现在是 ${process.version}`);
|
|
88
|
+
if (spawnSync('git', ['--version'], { stdio: 'ignore' }).status !== 0) throw new Error('需要 Git,请先安装');
|
|
89
|
+
const pkg = readJSON(join(directory, 'package.json'));
|
|
90
|
+
const configFile = join(root, 'installation.json'), existing = existsSync(configFile) ? readJSON(configFile) : null;
|
|
91
|
+
if (existing?.manager === 'coda-app') throw new Error('此后端由 Coda App 管理,请从该 App 更新');
|
|
92
|
+
if (existing && (existing.channel ?? 'stable') !== pkg.coda.channel) throw new Error('此后端属于另一个更新通道');
|
|
93
|
+
const installed = existsSync(join(root, 'backend-version')) ? readFileSync(join(root, 'backend-version'), 'utf8').trim() : null;
|
|
94
|
+
if (installed !== version) {
|
|
95
|
+
log(`安装后端 ${version} 到 ${root}`);
|
|
96
|
+
const stage = mkdtempSync(join(tmpdir(), 'coda-backend-'));
|
|
97
|
+
try {
|
|
98
|
+
await run('tar', ['-xzf', join(payload, 'backend.tar.gz'), '-C', stage]);
|
|
99
|
+
const install = [join(stage, 'scripts/backend-service.mjs'), 'install', '--no-start', '--source', join(stage, 'backend'),
|
|
100
|
+
'--package-version', version, '--channel', pkg.coda.channel];
|
|
101
|
+
const parent = join(payload, `supervisor-${process.platform}-${process.arch}.tar.gz`);
|
|
102
|
+
if (existsSync(parent)) {
|
|
103
|
+
await run('tar', ['-xzf', parent, '-C', stage]);
|
|
104
|
+
install.push('--supervisor', join(stage, 'coda-supervisor'));
|
|
105
|
+
}
|
|
106
|
+
await run(process.execPath, install);
|
|
107
|
+
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
108
|
+
}
|
|
109
|
+
if (command === 'install') return;
|
|
110
|
+
const result = await runBackend(root, {
|
|
111
|
+
autoUpdate: !args.includes('--no-auto-update') && process.env.CODA_NPM_AUTO_UPDATE !== '0',
|
|
112
|
+
check: options => checkForUpdate(directory, pkg, options),
|
|
113
|
+
});
|
|
114
|
+
if (result.restart) process.execve(process.execPath, [process.execPath, script, ...args], process.env);
|
|
115
|
+
process.exitCode = result.code;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === script) main().catch(error => { console.error(`coda-backend: ${error.message}`); process.exitCode = 1; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coda-backend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.246",
|
|
4
|
+
"description": "Coda 后端,由本机的进程管理器常驻运行",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coda-backend": "bin/coda-backend.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=24"
|
|
11
|
+
},
|
|
12
|
+
"coda": {
|
|
13
|
+
"repository": "damaged-soda/coda-releases",
|
|
14
|
+
"channel": "stable"
|
|
15
|
+
},
|
|
16
|
+
"os": [
|
|
17
|
+
"linux",
|
|
18
|
+
"darwin"
|
|
19
|
+
],
|
|
20
|
+
"license": "UNLICENSED"
|
|
21
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/payload/version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
20261024102151-f6c29c95e7ce-f363269f46f6
|
package/README.md
DELETED