feat(landing): add Atlas landing page with Astro static site
Implement the full landing page for atlas.atomstorm.ai per the PRD at Docs/Execution/Landing-Page-PRD-2026-03-14.md. Includes design spec, bilingual Astro site, CI pipeline, and all assets. Key deliverables: - DESIGN.md: 7-section Atom Web Design specification - Astro 5.x static site with 15 components and 11 page sections - Bilingual i18n (zh/en) with path-based routing - Build-time GitHub release manifest integration - Release channel state machine (Stable/Prerelease/Coming Soon) - CSS design tokens mapped from AtlasBrand.swift - Self-hosted fonts (Space Grotesk, Instrument Sans, IBM Plex Mono) - OG social sharing images for both locales - GitHub Actions workflow for GitHub Pages deployment - Zero client JS, 227KB page weight, 17/17 quality checks passed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
Apps/LandingSite/scripts/fetch-release.ts
Normal file
142
Apps/LandingSite/scripts/fetch-release.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Build-time script: Fetches latest release from GitHub API
|
||||
* and writes release-manifest.json for the landing page.
|
||||
*
|
||||
* Usage: tsx scripts/fetch-release.ts
|
||||
* Env: GITHUB_TOKEN (optional, increases rate limit)
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const OWNER = 'nicekid1';
|
||||
const REPO = 'CleanMyPc';
|
||||
const OUTPUT = join(__dirname, '..', 'src', 'data', 'release-manifest.json');
|
||||
|
||||
interface ReleaseManifest {
|
||||
channel: 'stable' | 'prerelease' | 'none';
|
||||
version: string | null;
|
||||
publishedAt: string | null;
|
||||
releaseUrl: string | null;
|
||||
assets: {
|
||||
dmg: string | null;
|
||||
zip: string | null;
|
||||
pkg: string | null;
|
||||
sha256: string | null;
|
||||
};
|
||||
gatekeeperWarning: boolean;
|
||||
installNote: string | null;
|
||||
tagName: string | null;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
function findAsset(assets: any[], suffix: string): string | null {
|
||||
const asset = assets.find((a: any) =>
|
||||
a.name.toLowerCase().endsWith(suffix.toLowerCase())
|
||||
);
|
||||
return asset?.browser_download_url ?? null;
|
||||
}
|
||||
|
||||
async function fetchRelease(): Promise<ReleaseManifest> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'atlas-landing-build',
|
||||
};
|
||||
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
// Try latest release first, then fall back to all releases
|
||||
let release: any = null;
|
||||
|
||||
try {
|
||||
const latestRes = await fetch(
|
||||
`https://api.github.com/repos/${OWNER}/${REPO}/releases/latest`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (latestRes.ok) {
|
||||
release = await latestRes.json();
|
||||
}
|
||||
} catch {
|
||||
// Ignore — try all releases next
|
||||
}
|
||||
|
||||
if (!release) {
|
||||
try {
|
||||
const allRes = await fetch(
|
||||
`https://api.github.com/repos/${OWNER}/${REPO}/releases?per_page=5`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (allRes.ok) {
|
||||
const releases = await allRes.json();
|
||||
if (Array.isArray(releases) && releases.length > 0) {
|
||||
release = releases[0];
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Will return "none" channel
|
||||
}
|
||||
}
|
||||
|
||||
if (!release) {
|
||||
return {
|
||||
channel: 'none',
|
||||
version: null,
|
||||
publishedAt: null,
|
||||
releaseUrl: `https://github.com/${OWNER}/${REPO}`,
|
||||
assets: { dmg: null, zip: null, pkg: null, sha256: null },
|
||||
gatekeeperWarning: false,
|
||||
installNote: null,
|
||||
tagName: null,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
const isPrerelease = release.prerelease === true;
|
||||
const version = (release.tag_name ?? '').replace(/^V/i, '') || null;
|
||||
const assets = release.assets ?? [];
|
||||
|
||||
return {
|
||||
channel: isPrerelease ? 'prerelease' : 'stable',
|
||||
version,
|
||||
publishedAt: release.published_at ?? null,
|
||||
releaseUrl: release.html_url ?? `https://github.com/${OWNER}/${REPO}/releases`,
|
||||
assets: {
|
||||
dmg: findAsset(assets, '.dmg'),
|
||||
zip: findAsset(assets, '.zip'),
|
||||
pkg: findAsset(assets, '.pkg'),
|
||||
sha256: findAsset(assets, '.sha256'),
|
||||
},
|
||||
gatekeeperWarning: isPrerelease,
|
||||
installNote: isPrerelease
|
||||
? 'This build is development-signed. macOS Gatekeeper may require "Open Anyway" or a right-click "Open" flow.'
|
||||
: null,
|
||||
tagName: release.tag_name ?? null,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Fetching release data for ${OWNER}/${REPO}...`);
|
||||
|
||||
try {
|
||||
const manifest = await fetchRelease();
|
||||
writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2) + '\n');
|
||||
console.log(`Wrote ${OUTPUT}`);
|
||||
console.log(` channel: ${manifest.channel}`);
|
||||
console.log(` version: ${manifest.version ?? '(none)'}`);
|
||||
console.log(` gatekeeperWarning: ${manifest.gatekeeperWarning}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch release, using fallback:', err);
|
||||
// The build will use release-fallback.json via the data loader
|
||||
process.exit(0); // Don't fail the build
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user