feat: split core views and add Windows portable auto-update
- split the core page into Home and Version Management - add responsive layouts and navigation - implement portable update checks, progress, verification, replacement, and rollback - generate Windows update packages and release manifests - add updater, navigation, and release workflow tests
This commit is contained in:
1 parent
879ebd2dc7
commit
462766d0d2
17 files changed
+2116
-197
No files matched your search
@@ -0,0 +1,21 @@
|
||||
export type PortableUpdateManifestAsset = {
|
||||
url: string;
|
||||
sha256: string;
|
||||
sizeBytes: number;
|
||||
};
|
||||
|
||||
export type PortableUpdateManifest = {
|
||||
schemaVersion: number;
|
||||
version: string;
|
||||
publishedAt: string;
|
||||
releaseUrl: string;
|
||||
assets: Record<string, PortableUpdateManifestAsset>;
|
||||
};
|
||||
|
||||
export function generatePortableUpdateManifest(options: {
|
||||
directory?: string;
|
||||
output?: string;
|
||||
repository?: string;
|
||||
tag?: string;
|
||||
publishedAt?: string;
|
||||
}): Promise<PortableUpdateManifest>;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
export async function generatePortableUpdateManifest({
|
||||
directory,
|
||||
output,
|
||||
repository,
|
||||
tag: rawTag,
|
||||
publishedAt = new Date().toISOString(),
|
||||
}) {
|
||||
const resolvedDirectory = resolve(directory ?? 'artifacts');
|
||||
const resolvedOutput = resolve(output ?? join(resolvedDirectory, 'portable-update-windows.json'));
|
||||
const resolvedRepository = repository ?? 'router-for-me/EasyCLIProxyAPI';
|
||||
const normalizedRawTag = String(rawTag ?? '').trim();
|
||||
const tag = normalizedRawTag.startsWith('v') ? normalizedRawTag : `v${normalizedRawTag}`;
|
||||
const version = tag.slice(1);
|
||||
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||
|
||||
if (!semverPattern.test(version)) {
|
||||
throw new Error(`Invalid release tag: ${normalizedRawTag}`);
|
||||
}
|
||||
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(resolvedRepository)) {
|
||||
throw new Error(`Invalid GitHub repository: ${resolvedRepository}`);
|
||||
}
|
||||
if (Number.isNaN(Date.parse(publishedAt))) {
|
||||
throw new Error(`Invalid publishedAt: ${publishedAt}`);
|
||||
}
|
||||
|
||||
const assets = {};
|
||||
for (const arch of ['amd64', 'aarch64']) {
|
||||
const filename = `EasyCLIProxyAPI-update-${tag}-Windows-${arch}.zip`;
|
||||
const path = join(resolvedDirectory, filename);
|
||||
const [contents, metadata] = await Promise.all([readFile(path), stat(path)]);
|
||||
if (!metadata.isFile() || metadata.size === 0) {
|
||||
throw new Error(`Portable updater asset is empty or not a file: ${filename}`);
|
||||
}
|
||||
assets[`windows-${arch}`] = {
|
||||
url: `https://github.com/${resolvedRepository}/releases/download/${tag}/${filename}`,
|
||||
sha256: createHash('sha256').update(contents).digest('hex'),
|
||||
sizeBytes: metadata.size,
|
||||
};
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
version,
|
||||
publishedAt,
|
||||
releaseUrl: `https://github.com/${resolvedRepository}/releases/tag/${tag}`,
|
||||
assets,
|
||||
};
|
||||
|
||||
await writeFile(resolvedOutput, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 2) {
|
||||
args.set(process.argv[index], process.argv[index + 1]);
|
||||
}
|
||||
const directory = resolve(args.get('--directory') ?? 'artifacts');
|
||||
const output = resolve(args.get('--output') ?? join(directory, 'portable-update-windows.json'));
|
||||
const manifest = await generatePortableUpdateManifest({
|
||||
directory,
|
||||
output,
|
||||
repository: args.get('--repository'),
|
||||
tag: args.get('--tag'),
|
||||
});
|
||||
console.log(`Generated ${basename(output)} for v${manifest.version}`);
|
||||
}
|
||||
|
||||
const entryPoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
|
||||
if (import.meta.url === entryPoint) {
|
||||
await main();
|
||||
}
|
||||
@@ -21,6 +21,12 @@ if (!existsSync(binary)) throw new Error(`GUI binary not found: ${binary}`);
|
||||
const rawVersion = (await readFile(join(root, 'core-version.txt'), 'utf8')).trim();
|
||||
if (!/^v?\d+(?:\.\d+)+$/.test(rawVersion)) throw new Error(`Invalid core-version.txt: ${rawVersion}`);
|
||||
const version = rawVersion.replace(/^v/i, '');
|
||||
const packageMetadata = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
||||
const appVersion = String(packageMetadata.version ?? '').trim();
|
||||
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||
if (!semverPattern.test(appVersion)) {
|
||||
throw new Error(`Invalid package.json version: ${appVersion}`);
|
||||
}
|
||||
const extension = targetOS === 'windows' ? 'zip' : 'tar.gz';
|
||||
const assetName = `CLIProxyAPI_${version}_${targetOS}_${targetArch}.${extension}`;
|
||||
const sourceDir = join(root, 'cpa-core');
|
||||
@@ -59,6 +65,14 @@ await rm(legacyOutputBinary, { force: true });
|
||||
await copyFile(binary, outputBinary);
|
||||
if (targetOS !== 'windows') await chmod(outputBinary, 0o755);
|
||||
await copyFile(join(root, 'core-version.txt'), join(output, 'core-version.txt'));
|
||||
await writeFile(join(output, 'portable-app.json'), `${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
application: 'EasyCLIProxyAPI',
|
||||
version: appVersion,
|
||||
platform: targetOS,
|
||||
arch: targetArch,
|
||||
autoUpdate: targetOS === 'windows',
|
||||
}, null, 2)}\n`);
|
||||
|
||||
const coreOutput = join(output, 'cpa-core');
|
||||
await rm(coreOutput, { recursive: true, force: true });
|
||||
|
||||
Reference in new issue
Block a user