diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89be2ee3..caa5af55 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,13 +203,17 @@ jobs: retention-days: 7 if-no-files-found: error - # code.pythinker.com redeploys via Dokploy autodeploy on push to main (app - # Pythinker/code builds apps/site/Dockerfile from the repo). That autodeploy - # fires on the `ci: release packages` push — which STARTS the release — while - # apps/site/scripts/build-cdn.mjs reads the version from npm's dist-tag, which - # only moves when the publish FINISHES. The first build therefore bakes in the - # previous version and nothing rebuilds it, so the release stays invisible to - # every installed client. This job fires a second deploy after the publish. + # code.pythinker.com is built from the private repository PyModel/pythinker-site, + # which owns the site source and its build-cdn.mjs. Dokploy autodeploys on push + # to THAT repository, so nothing in this repository triggers a CDN rebuild — this + # job is the only thing that does after a release. + # + # It has to exist because the CDN resolves the version it advertises from npm's + # dist-tag, which only moves when the publish FINISHES. Without this job a + # release would publish to npm and the CDN would keep serving the previous + # version until someone happened to push to the site repository, leaving the + # release invisible to every installed client. This job fires the deploy that + # picks the new dist-tag up. # # It must run after publish-native-assets: latest.json only gets its # per-platform `platforms` block once the native zips exist on the release. diff --git a/apps/desktop/tests/packaging-config.spec.ts b/apps/desktop/tests/packaging-config.spec.ts index 62c764ce..604336e3 100644 --- a/apps/desktop/tests/packaging-config.spec.ts +++ b/apps/desktop/tests/packaging-config.spec.ts @@ -73,23 +73,11 @@ describe('desktop packaging configuration', () => { }) }) - it('keeps desktop download URLs derived from their published release version', () => { - const siteSource = readFileSync(resolve(repositoryRoot, 'apps/site/src/App.vue'), 'utf8') - const desktopVersionMatch = siteSource.match(/const DESKTOP_VERSION = '([^']+)'/) - - expect(desktopVersionMatch).not.toBeNull() - expect(desktopVersionMatch![1]).not.toBe('') - expect(siteSource).not.toContain('Pythinker-0.1.0-arm64.dmg') - expect(siteSource).not.toContain('Pythinker-0.1.0-x64-Setup.exe') - expect(siteSource).toContain('Pythinker-${DESKTOP_VERSION}-arm64.dmg') - expect(siteSource).toContain('Pythinker-${DESKTOP_VERSION}-x64-Setup.exe') - expect(siteSource).toContain('releases/download/v${DESKTOP_VERSION}') - expect(siteSource).not.toContain('releases/download/v0.1.0') - - const desktopShowcaseMatch = siteSource.match(/
/) - expect(desktopShowcaseMatch).not.toBeNull() - expect(desktopShowcaseMatch![0]).toContain('/brand/windows11.svg') - }) + // A test here used to read the site's App.vue and assert that the download + // buttons on code.pythinker.com interpolated the desktop release version + // instead of hardcoding one. The site moved to the private repository + // PyModel/pythinker-site, so nothing in this repository can see those links + // any more and the check has to live there. It is not covered here. it('maps the staged Host node_modules directory as the copy root', () => { expect(desktopPackage.build.extraResources).toEqual(expect.arrayContaining([ diff --git a/apps/site/.gitignore b/apps/site/.gitignore deleted file mode 100644 index ce19795c..00000000 --- a/apps/site/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -cdn-dist -!Dockerfile diff --git a/apps/site/Caddyfile b/apps/site/Caddyfile deleted file mode 100644 index 6f429fa5..00000000 --- a/apps/site/Caddyfile +++ /dev/null @@ -1,24 +0,0 @@ -:80 { - root * /srv - - # Keep HTML as the default and exclude explicit Markdown q=0 requests. - @markdown { - path / - header_regexp Accept (?i)(?:^|,)\s*text/markdown\s*(?:;\s*q=(?:0\.[0-9]*[1-9][0-9]*|1(?:\.0*)?))?\s*(?:,|$) - } - - header Content-Signal "ai-train=no, search=yes, ai-input=yes" - header / { - Link "; rel=\"describedby\"; type=\"text/html\", ; rel=\"alternate\"; type=\"text/markdown\"" - Vary "Accept" - } - header @markdown Content-Type "text/markdown; charset=utf-8" - header /robots.txt Content-Type "text/plain; charset=utf-8" - header /sitemap.xml Content-Type "application/xml; charset=utf-8" - rewrite @markdown /index.md - try_files {path} /index.html - - file_server - header /pythinker-code/* Cache-Control "no-cache" - header /assets/* Cache-Control "public, max-age=31536000, immutable" -} diff --git a/apps/site/Dockerfile b/apps/site/Dockerfile deleted file mode 100644 index 7567ffff..00000000 --- a/apps/site/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:26-alpine AS build - -RUN apk add --no-cache zip -WORKDIR /repo -COPY pnpm-workspace.yaml ./ -COPY apps/site apps/site -COPY apps/pythinker-code/package.json apps/pythinker-code/package.json -COPY apps/pythinker-web/public/install.sh apps/pythinker-web/public/install.sh -COPY apps/pythinker-web/public/install.ps1 apps/pythinker-web/public/install.ps1 -COPY plugins/cdn plugins/cdn -COPY packages/agent-core/src/tools/support/rg-locator.ts packages/agent-core/src/tools/support/rg-locator.ts -RUN cd apps/site && npm install --no-audit --no-fund && npm run build && node scripts/build-cdn.mjs --out /cdn - -FROM caddy:2-alpine - -COPY --from=build /cdn /srv -COPY apps/site/Caddyfile /etc/caddy/Caddyfile diff --git a/apps/site/index.html b/apps/site/index.html deleted file mode 100644 index 68dda94e..00000000 --- a/apps/site/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Pythinker Code, the terminal-native AI engineering agent - - -
- - - diff --git a/apps/site/package.json b/apps/site/package.json deleted file mode 100644 index 712a1655..00000000 --- a/apps/site/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@pymodel/site", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "build:cdn": "node scripts/build-cdn.mjs --out cdn-dist", - "clean": "rm -rf dist", - "preview": "vite preview" - }, - "dependencies": { - "vue": "^3.5.40" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.2.4", - "vite": "^6.4.3" - } -} diff --git a/apps/site/public/_headers b/apps/site/public/_headers deleted file mode 100644 index 5711b8e8..00000000 --- a/apps/site/public/_headers +++ /dev/null @@ -1,10 +0,0 @@ -/install.sh - Content-Type: text/x-shellscript; charset=utf-8 - Cache-Control: public, max-age=300, s-maxage=900, stale-if-error=86400 - -/install.ps1 - Content-Type: text/plain; charset=utf-8 - Cache-Control: public, max-age=300, s-maxage=900, stale-if-error=86400 - -/releases/* - Cache-Control: public, max-age=31536000, immutable diff --git a/apps/site/public/apple-touch-icon.png b/apps/site/public/apple-touch-icon.png deleted file mode 100644 index 626623e3..00000000 Binary files a/apps/site/public/apple-touch-icon.png and /dev/null differ diff --git a/apps/site/public/bimi-logo.svg b/apps/site/public/bimi-logo.svg deleted file mode 100644 index 4fbf2dc5..00000000 --- a/apps/site/public/bimi-logo.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - Pythinker - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/site/public/brand/anthropic.svg b/apps/site/public/brand/anthropic.svg deleted file mode 100644 index c917480d..00000000 --- a/apps/site/public/brand/anthropic.svg +++ /dev/null @@ -1 +0,0 @@ -Anthropic \ No newline at end of file diff --git a/apps/site/public/brand/apple.svg b/apps/site/public/brand/apple.svg deleted file mode 100644 index 6a25a115..00000000 --- a/apps/site/public/brand/apple.svg +++ /dev/null @@ -1 +0,0 @@ -Apple \ No newline at end of file diff --git a/apps/site/public/brand/aws.svg b/apps/site/public/brand/aws.svg deleted file mode 100644 index af7b7415..00000000 --- a/apps/site/public/brand/aws.svg +++ /dev/null @@ -1,3 +0,0 @@ - - aws - diff --git a/apps/site/public/brand/deepseek.svg b/apps/site/public/brand/deepseek.svg deleted file mode 100644 index e19a4597..00000000 --- a/apps/site/public/brand/deepseek.svg +++ /dev/null @@ -1 +0,0 @@ -DeepSeek \ No newline at end of file diff --git a/apps/site/public/brand/gemini.svg b/apps/site/public/brand/gemini.svg deleted file mode 100644 index 62681dfa..00000000 --- a/apps/site/public/brand/gemini.svg +++ /dev/null @@ -1 +0,0 @@ -Gemini \ No newline at end of file diff --git a/apps/site/public/brand/github.svg b/apps/site/public/brand/github.svg deleted file mode 100644 index 0f8cec74..00000000 --- a/apps/site/public/brand/github.svg +++ /dev/null @@ -1 +0,0 @@ -GitHub \ No newline at end of file diff --git a/apps/site/public/brand/groq.svg b/apps/site/public/brand/groq.svg deleted file mode 100644 index 52c2a828..00000000 --- a/apps/site/public/brand/groq.svg +++ /dev/null @@ -1,3 +0,0 @@ - - Groq - diff --git a/apps/site/public/brand/icon.svg b/apps/site/public/brand/icon.svg deleted file mode 100644 index 4454e2df..00000000 --- a/apps/site/public/brand/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/site/public/brand/jetbrains.svg b/apps/site/public/brand/jetbrains.svg deleted file mode 100644 index 67a2ffea..00000000 --- a/apps/site/public/brand/jetbrains.svg +++ /dev/null @@ -1 +0,0 @@ -JetBrains \ No newline at end of file diff --git a/apps/site/public/brand/lmstudio.svg b/apps/site/public/brand/lmstudio.svg deleted file mode 100644 index 3e689b5a..00000000 --- a/apps/site/public/brand/lmstudio.svg +++ /dev/null @@ -1 +0,0 @@ -LM Studio \ No newline at end of file diff --git a/apps/site/public/brand/mcp.svg b/apps/site/public/brand/mcp.svg deleted file mode 100644 index e6814de7..00000000 --- a/apps/site/public/brand/mcp.svg +++ /dev/null @@ -1 +0,0 @@ -ModelContextProtocol \ No newline at end of file diff --git a/apps/site/public/brand/meta.svg b/apps/site/public/brand/meta.svg deleted file mode 100644 index d9a1631d..00000000 --- a/apps/site/public/brand/meta.svg +++ /dev/null @@ -1 +0,0 @@ -Meta \ No newline at end of file diff --git a/apps/site/public/brand/mistral.svg b/apps/site/public/brand/mistral.svg deleted file mode 100644 index 6388f65a..00000000 --- a/apps/site/public/brand/mistral.svg +++ /dev/null @@ -1 +0,0 @@ -Mistral AI \ No newline at end of file diff --git a/apps/site/public/brand/npm.svg b/apps/site/public/brand/npm.svg deleted file mode 100644 index e04468d9..00000000 --- a/apps/site/public/brand/npm.svg +++ /dev/null @@ -1 +0,0 @@ -npm \ No newline at end of file diff --git a/apps/site/public/brand/ollama.svg b/apps/site/public/brand/ollama.svg deleted file mode 100644 index 06947b8d..00000000 --- a/apps/site/public/brand/ollama.svg +++ /dev/null @@ -1 +0,0 @@ -Ollama \ No newline at end of file diff --git a/apps/site/public/brand/openai.svg b/apps/site/public/brand/openai.svg deleted file mode 100644 index ebbdab0e..00000000 --- a/apps/site/public/brand/openai.svg +++ /dev/null @@ -1 +0,0 @@ -OpenAI \ No newline at end of file diff --git a/apps/site/public/brand/pythinker_animated.svg b/apps/site/public/brand/pythinker_animated.svg deleted file mode 100644 index bf23b5bc..00000000 --- a/apps/site/public/brand/pythinker_animated.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/site/public/brand/visualstudiocode.svg b/apps/site/public/brand/visualstudiocode.svg deleted file mode 100644 index 6a8b474f..00000000 --- a/apps/site/public/brand/visualstudiocode.svg +++ /dev/null @@ -1 +0,0 @@ -Visual Studio Code \ No newline at end of file diff --git a/apps/site/public/brand/windows11.svg b/apps/site/public/brand/windows11.svg deleted file mode 100644 index d421f8aa..00000000 --- a/apps/site/public/brand/windows11.svg +++ /dev/null @@ -1 +0,0 @@ -Windows 11 \ No newline at end of file diff --git a/apps/site/public/code/oauth-success.html b/apps/site/public/code/oauth-success.html deleted file mode 100644 index 7d5dde65..00000000 --- a/apps/site/public/code/oauth-success.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Signed in to Pythinker - - - -
- -

Pythinker Code

-
- -
-

You're logged in to Pythinker

-

You can close this tab and return to Pythinker.

-
- - diff --git a/apps/site/public/favicon.ico b/apps/site/public/favicon.ico deleted file mode 100644 index 5887d0bb..00000000 Binary files a/apps/site/public/favicon.ico and /dev/null differ diff --git a/apps/site/public/favicon.svg b/apps/site/public/favicon.svg deleted file mode 100644 index 4454e2df..00000000 --- a/apps/site/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/site/public/icon-192.png b/apps/site/public/icon-192.png deleted file mode 100644 index bc17dcdd..00000000 Binary files a/apps/site/public/icon-192.png and /dev/null differ diff --git a/apps/site/public/icon-512.png b/apps/site/public/icon-512.png deleted file mode 100644 index 596d244a..00000000 Binary files a/apps/site/public/icon-512.png and /dev/null differ diff --git a/apps/site/public/icon.svg b/apps/site/public/icon.svg deleted file mode 100644 index 4454e2df..00000000 --- a/apps/site/public/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/site/public/index.md b/apps/site/public/index.md deleted file mode 100644 index ba4ffc21..00000000 --- a/apps/site/public/index.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Pythinker Code, the terminal-native AI engineering agent -description: Install Pythinker Code, an open-source AI coding agent for your terminal. ---- - -# Pythinker Code - -**Think first, then code.** - -Pythinker Code is an open-source AI engineering agent for your terminal. It reads your repository, edits files, runs commands, and iterates until the job is done. - -It is free, MIT licensed, and available for macOS, Linux, and Windows. - -## Install - -### macOS or Linux - -```sh -curl -fsSL https://code.pythinker.com/pythinker-code/install.sh | bash -``` - -### Windows PowerShell - -```powershell -irm https://code.pythinker.com/pythinker-code/install.ps1 | iex -``` - -### Homebrew - -```sh -brew install pymodel/tap/pythinker-code -``` - -### Nix - -```sh -nix run github:PyModel/pythinker-code -``` - -### npm - -```sh -npm install -g @pymodel/pythinker-code -``` - -Verify the installation with `pythinker --version`. - -## Quick start - -1. Run `pythinker` in your project. -2. Authenticate with `/login` using Pythinker Code OAuth or an API key. -3. Describe an engineering task and review the agent's work. - -## Capabilities - -- Run subagents in parallel from one iterative agent loop. -- Connect to editors through Agent Client Protocol (ACP). -- Load Model Context Protocol (MCP) servers with `/mcp-config`. -- Use reusable repository skills and lifecycle hooks. -- Configure Pythinker models or other compatible LLM APIs. - -## Resources - -- [Documentation](https://pymodel.github.io/pythinker-code/) -- [Getting started](https://pymodel.github.io/pythinker-code/guides/getting-started) -- [Configuration](https://pymodel.github.io/pythinker-code/configuration/config-files) -- [Command reference](https://pymodel.github.io/pythinker-code/reference/pythinker-command) -- [GitHub repository](https://github.com/PyModel/pythinker-code) -- [npm package](https://www.npmjs.com/package/@pymodel/pythinker-code) diff --git a/apps/site/public/og.jpg b/apps/site/public/og.jpg deleted file mode 100644 index 01ad47a9..00000000 Binary files a/apps/site/public/og.jpg and /dev/null differ diff --git a/apps/site/public/pythinker_animated.svg b/apps/site/public/pythinker_animated.svg deleted file mode 100644 index bf23b5bc..00000000 --- a/apps/site/public/pythinker_animated.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/site/public/pythinker_desktop.png b/apps/site/public/pythinker_desktop.png deleted file mode 100644 index 33641c50..00000000 Binary files a/apps/site/public/pythinker_desktop.png and /dev/null differ diff --git a/apps/site/public/pythinker_desktop.webm b/apps/site/public/pythinker_desktop.webm deleted file mode 100644 index a7f30fb2..00000000 Binary files a/apps/site/public/pythinker_desktop.webm and /dev/null differ diff --git a/apps/site/public/pythinker_desktop.webp b/apps/site/public/pythinker_desktop.webp deleted file mode 100644 index 5a4e2590..00000000 Binary files a/apps/site/public/pythinker_desktop.webp and /dev/null differ diff --git a/apps/site/public/robots.txt b/apps/site/public/robots.txt deleted file mode 100644 index cbe11854..00000000 --- a/apps/site/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://pythinker.com/sitemap.xml diff --git a/apps/site/public/sitemap.xml b/apps/site/public/sitemap.xml deleted file mode 100644 index 797d8174..00000000 --- a/apps/site/public/sitemap.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - https://code.pythinker.com/ - - diff --git a/apps/site/public/vscode_img.jpeg b/apps/site/public/vscode_img.jpeg deleted file mode 100644 index 6cd8d4b9..00000000 Binary files a/apps/site/public/vscode_img.jpeg and /dev/null differ diff --git a/apps/site/scripts/build-cdn.mjs b/apps/site/scripts/build-cdn.mjs deleted file mode 100644 index ba49dcef..00000000 --- a/apps/site/scripts/build-cdn.mjs +++ /dev/null @@ -1,258 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { createHash } from 'node:crypto'; -import { access, cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; -import { basename, dirname, join, parse, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -async function findRepoRoot() { - let dir = dirname(fileURLToPath(import.meta.url)); - while (dir !== parse(dir).root) { - try { - await access(join(dir, 'pnpm-workspace.yaml')); - return dir; - } catch { - dir = dirname(dir); - } - } - throw new Error('Could not find repository root containing pnpm-workspace.yaml'); -} - -function parseArgs() { - const args = process.argv.slice(2); - let out; - let skipRg = false; - for (let index = 0; index < args.length; index += 1) { - if (args[index] === '--out') out = args[++index]; - else if (args[index] === '--skip-rg') skipRg = true; - else throw new Error(`Unknown argument: ${args[index]}`); - } - if (!out) throw new Error('Usage: build-cdn.mjs --out [--skip-rg]'); - return { out, skipRg }; -} - -const RELEASE_VERSION = /^\d+\.\d+\.\d+$/; - -/** - * Resolve the version this CDN advertises from a *published* release, never from - * the working tree. - * - * `apps/pythinker-code/package.json` is bumped by the `ci: release packages` - * merge and this site autodeploys on every push to main, so deriving the - * manifest from it advertised the next version the moment that merge landed — - * before the npm publish and the GitHub release assets existed, and permanently - * when the publish never ran at all. Installed clients then polled GitHub for - * assets that did not exist (~6 minutes per launch, every launch). The npm - * dist-tag is the publish barrier, so it is the only safe source. - * - * `publishedAt` comes from npm's own publish timestamp: stamping build time made - * every unrelated site deploy re-anchor the clients' rollout eligibility window. - * - * A registry read failure throws on purpose: a failed image build leaves the - * previous container serving the last good manifest, which is the safe outcome. - */ -async function resolvePublishedRelease(packageName) { - const pinned = process.env.PYTHINKER_CDN_VERSION?.trim(); - if (pinned) { - // The override answers to the same shape rule as the registry path below. - // A client rejects a manifest whose `version` is not semver, so an - // unusable override has to stop the build instead of publishing a - // latest.json that every installed client fails to parse. - if (!RELEASE_VERSION.test(pinned)) { - throw new Error(`PYTHINKER_CDN_VERSION is not a release version: ${pinned}`); - } - // Build time is the only timestamp available for a manual pin; the - // registry path below requires npm's own and never stamps one. - return { version: pinned, publishedAt: new Date().toISOString() }; - } - const view = JSON.parse( - execFileSync( - 'npm', - ['view', packageName, 'dist-tags', 'time', '--json', '--registry=https://registry.npmjs.org'], - { encoding: 'utf8', timeout: 60_000 }, - ), - ); - const version = view['dist-tags']?.latest; - if (typeof version !== 'string' || !RELEASE_VERSION.test(version)) { - throw new Error( - `npm dist-tag latest for ${packageName} is not a release version: ${String(version)}`, - ); - } - const publishedAt = view.time?.[version]; - // Stamping build time here is the bug this function documents: it would move - // the rollout anchor on every unrelated site deploy. Unreadable registry - // metadata is a failed read, and a failed read must fail the build. - if (typeof publishedAt !== 'string' || !Number.isFinite(Date.parse(publishedAt))) { - throw new Error( - `npm has no usable publish time for ${packageName}@${version}: ${String(publishedAt)}`, - ); - } - return { version, publishedAt }; -} - -const RELEASE_DOWNLOAD_BASE = 'https://github.com/PyModel/pythinker-code/releases/download'; - -// Set to a version string only when an older client can no longer work against -// the current services; it makes every client below it take the update without -// waiting for its rollout batch. Reset to null once that release is the floor. -const MIN_REQUIRED_VERSION = null; - -/** - * Resolve the per-platform native artifacts for a published version. - * - * The release already carries a `manifest.json` asset written by - * `apps/pythinker-code/scripts/native/produce-manifest.mjs`, so this only turns - * `{ filename, checksum }` into `{ url, sha256 }`. Naming the artifact in - * `latest.json` is what lets a client answer "is there anything to download for - * my platform" from the manifest alone, instead of guessing an asset URL and - * polling GitHub for six minutes when the guess is wrong. - * - * Returns null — never throws — when the release shipped no native manifest. - * An npm-only release is legitimate, and a site build must not fail because of - * it; clients that see no `platforms` key keep their previous behaviour. - */ -async function resolvePlatformArtifacts(version) { - const base = `${RELEASE_DOWNLOAD_BASE}/%40pymodel%2Fpythinker-code%40${version}`; - let native; - try { - const response = await fetch(`${base}/manifest.json`, { signal: AbortSignal.timeout(30_000) }); - if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`); - native = await response.json(); - } catch (error) { - console.log(`no native manifest for ${version} (${error.message}); omitting platforms`); - return null; - } - if (native?.version !== version) { - console.log(`native manifest reports ${native?.version}, expected ${version}; omitting platforms`); - return null; - } - const platforms = {}; - for (const [target, entry] of Object.entries(native.platforms ?? {})) { - if (typeof entry?.filename !== 'string' || !/^[a-f0-9]{64}$/.test(entry?.checksum ?? '')) { - throw new Error(`Malformed native manifest entry for ${target}`); - } - platforms[target] = { url: `${base}/${entry.filename}`, sha256: entry.checksum }; - } - return Object.keys(platforms).length > 0 ? platforms : null; -} - -async function copyPlugins(repoRoot, cdnRoot) { - const source = join(repoRoot, 'plugins/cdn'); - const destination = join(cdnRoot, 'pythinker-code/plugins'); - await mkdir(destination, { recursive: true }); - await cp(join(source, 'marketplace.json'), join(destination, 'marketplace.json')); - for (const channel of ['official', 'curated']) { - const channelSource = join(source, channel); - let entries; - try { - entries = await readdir(channelSource, { withFileTypes: true }); - } catch (error) { - if (error.code === 'ENOENT') continue; - throw error; - } - const channelDestination = join(destination, channel); - await mkdir(channelDestination, { recursive: true }); - for (const entry of entries) { - if (!entry.isFile()) continue; - await cp(join(channelSource, entry.name), join(channelDestination, entry.name)); - } - } -} - -async function downloadRipgrep(repoRoot, cdnRoot) { - const locator = await readFile( - join(repoRoot, 'packages/agent-core/src/tools/support/rg-locator.ts'), - 'utf8', - ); - const pairs = [...locator.matchAll(/'(ripgrep-[^']+\.(?:tar\.gz|zip))':\s*'([a-f0-9]{64})'/g)] - .map((match) => ({ filename: match[1], sha256: match[2] })); - if (pairs.length !== 6) { - throw new Error(`Expected 6 ripgrep archive checksums, found ${pairs.length}`); - } - const versions = new Set(pairs.map(({ filename }) => { - const match = /^ripgrep-(\d+\.\d+\.\d+)-/.exec(filename); - if (!match) throw new Error(`Could not parse ripgrep version from ${filename}`); - return match[1]; - })); - if (versions.size !== 1) throw new Error('Ripgrep archive versions do not match'); - const [version] = versions; - const destination = join(cdnRoot, 'pythinker-code/rg'); - await mkdir(destination, { recursive: true }); - - for (const { filename, sha256 } of pairs) { - try { - const response = await fetch( - `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`, - ); - if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`); - const bytes = Buffer.from(await response.arrayBuffer()); - const actual = createHash('sha256').update(bytes).digest('hex'); - if (actual !== sha256) throw new Error(`SHA-256 mismatch: expected ${sha256}, got ${actual}`); - await writeFile(join(destination, filename), bytes); - } catch (error) { - throw new Error(`Failed to download ${filename}: ${error.message}`, { cause: error }); - } - } -} - -async function countFiles(dir) { - let count = 0; - for (const entry of await readdir(dir, { withFileTypes: true })) { - count += entry.isDirectory() ? await countFiles(join(dir, entry.name)) : 1; - } - return count; -} - -const { out, skipRg } = parseArgs(); -const repoRoot = await findRepoRoot(); -const packageJson = JSON.parse( - await readFile(join(repoRoot, 'apps/pythinker-code/package.json'), 'utf8'), -); -const packageName = packageJson.name; -if (typeof packageName !== 'string' || packageName.trim() === '') { - throw new Error('apps/pythinker-code/package.json has no name'); -} -const { version, publishedAt } = await resolvePublishedRelease(packageName); - -const siteDist = join(repoRoot, 'apps/site/dist'); -await access(join(siteDist, 'index.html')); -const expectedLeaf = basename(out); -const outDir = resolve(repoRoot, out); -if ( - outDir === parse(outDir).root || - outDir === repoRoot || - expectedLeaf === '' || - basename(outDir) !== expectedLeaf -) { - throw new Error(`Refusing unsafe output path: ${outDir}`); -} - -await rm(outDir, { recursive: true, force: true }); -await cp(siteDist, outDir, { recursive: true }); -const channelRoot = join(outDir, 'pythinker-code'); -await mkdir(channelRoot, { recursive: true }); -// Plain-text `/latest`: install.sh reads it for a fresh install, and clients -// shipped before latest.json existed still poll it. Current clients read only -// the manifest, so this file must keep being written but must never become the -// place a new field lands. -await writeFile(join(channelRoot, 'latest'), `${version}\n`); -const platforms = await resolvePlatformArtifacts(version); -await writeFile(join(channelRoot, 'latest.json'), `${JSON.stringify({ - version, - publishedAt, - minRequiredVersion: MIN_REQUIRED_VERSION ?? undefined, - platforms: platforms ?? undefined, - rollout: [], -}, null, 2)}\n`); -// One checked-in installer, served at both paths. `apps/site/public/` used to -// hold its own byte-identical copy, which meant a one-sided edit shipped -// silently; the site root keeps working because the file is placed here at -// build time instead of being duplicated in the tree. -for (const name of ['install.sh', 'install.ps1']) { - const source = join(repoRoot, 'apps/pythinker-web/public', name); - await cp(source, join(channelRoot, name)); - await cp(source, join(outDir, name)); -} -await copyPlugins(repoRoot, outDir); -if (!skipRg) await downloadRipgrep(repoRoot, outDir); - -console.log(`version ${version}, ${await countFiles(outDir)} files, rg ${skipRg ? 'skipped' : 'included'}`); diff --git a/apps/site/src/App.vue b/apps/site/src/App.vue deleted file mode 100644 index f11a78c9..00000000 --- a/apps/site/src/App.vue +++ /dev/null @@ -1,1991 +0,0 @@ - - - - - diff --git a/apps/site/src/components/AgentLoop.vue b/apps/site/src/components/AgentLoop.vue deleted file mode 100644 index 77d8147a..00000000 --- a/apps/site/src/components/AgentLoop.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/apps/site/src/components/InstallCommand.vue b/apps/site/src/components/InstallCommand.vue deleted file mode 100644 index a3eca682..00000000 --- a/apps/site/src/components/InstallCommand.vue +++ /dev/null @@ -1,368 +0,0 @@ - - - - - diff --git a/apps/site/src/components/LegacyDownloadsPopup.vue b/apps/site/src/components/LegacyDownloadsPopup.vue deleted file mode 100644 index a1740150..00000000 --- a/apps/site/src/components/LegacyDownloadsPopup.vue +++ /dev/null @@ -1,190 +0,0 @@ - - - - - diff --git a/apps/site/src/components/ParticleField.vue b/apps/site/src/components/ParticleField.vue deleted file mode 100644 index e5622b84..00000000 --- a/apps/site/src/components/ParticleField.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - - - diff --git a/apps/site/src/components/PythinkerMascot.vue b/apps/site/src/components/PythinkerMascot.vue deleted file mode 100644 index fe98453e..00000000 --- a/apps/site/src/components/PythinkerMascot.vue +++ /dev/null @@ -1,136 +0,0 @@ - - - - - diff --git a/apps/site/src/install-channels.js b/apps/site/src/install-channels.js deleted file mode 100644 index 9c04b9ee..00000000 --- a/apps/site/src/install-channels.js +++ /dev/null @@ -1,7 +0,0 @@ -export const INSTALL_CHANNELS = [ - { id: 'unix', label: 'Mac / Linux', command: 'curl -fsSL https://code.pythinker.com/pythinker-code/install.sh | bash', icon: '/brand/apple.svg' }, - { id: 'windows', label: 'Windows', command: 'irm https://code.pythinker.com/pythinker-code/install.ps1 | iex', icon: '/brand/windows11.svg' }, - { id: 'brew', label: 'Homebrew', command: 'brew install pymodel/tap/pythinker-code' }, - { id: 'nix', label: 'Nix', command: 'nix run github:PyModel/pythinker-code' }, - { id: 'npm', label: 'npm', command: 'npm install -g @pymodel/pythinker-code', icon: '/brand/npm.svg' }, -]; diff --git a/apps/site/src/main.js b/apps/site/src/main.js deleted file mode 100644 index 08e4395e..00000000 --- a/apps/site/src/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { createApp } from 'vue'; -import App from './App.vue'; -import './style.css'; -import { registerWebMcpTools } from './webmcp'; - -createApp(App).mount('#app'); -registerWebMcpTools(); diff --git a/apps/site/src/style.css b/apps/site/src/style.css deleted file mode 100644 index 60c968a4..00000000 --- a/apps/site/src/style.css +++ /dev/null @@ -1,290 +0,0 @@ -:root { - --canvas: #ffffff; - --surface-1: #f7f7f8; - --surface-2: #efeff1; - --surface-3: #e4e4e7; - --hairline: #e5e7eb; - --hairline-soft: #f0f0f2; - --ink: #111113; - --ink-muted: #55565c; - --ink-subtle: #8b8d94; - --accent: #2b89ff; - --grid-line: rgba(43, 137, 255, 0.055); - --bubble-blue: rgba(43, 137, 255, 0.18); - --bubble-cyan: rgba(92, 206, 255, 0.14); - --pill-dark: #111113; - --pill-dark-ink: #ffffff; - --terminal-bg: #10263c; - --terminal-band: #17324c; - --radius: 18px; - --radius-sm: 12px; - --radius-pill: 9999px; - --shadow-card: 0 24px 70px -42px rgba(17, 17, 19, 0.35); - --shadow-terminal: 0 26px 70px -42px rgba(16, 38, 60, 0.86), inset 0 1px 0 rgba(255, 255, 255, 0.08); - - /* Existing component aliases mapped to the design system. */ - --hairline-strong: var(--surface-3); - --r-xs: 4px; - --r-sm: var(--radius-sm); - --r-md: var(--radius-sm); - --r-lg: var(--radius); - --r-xxl: var(--radius); - --r-pill: var(--radius-pill); - color-scheme: light; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', helvetica, arial, sans-serif; - font-feature-settings: 'cv11', 'ss01'; - font-synthesis: none; -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; -} - -html { - scroll-behavior: smooth; -} - -body { - position: relative; - min-width: 320px; - min-height: 100vh; - overflow-x: clip; - background-color: var(--canvas); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath d='M48 0H0v48' fill='none' stroke='%23111113' stroke-opacity='0.045' stroke-width='1'/%3E%3C/svg%3E"); - color: var(--ink); - font-size: 17px; - font-weight: 400; - line-height: 1.55; -} - -body::before, -body::after { - position: fixed; - z-index: 0; - width: clamp(340px, 34vw, 520px); - border: 1px solid rgba(43, 137, 255, 0.1); - border-radius: 50%; - aspect-ratio: 1; - content: ''; - pointer-events: none; - will-change: transform; -} - -body::before { - top: 12vh; - left: clamp(-300px, -18vw, -190px); - background: - radial-gradient(circle at 64% 32%, rgba(255, 255, 255, 0.78) 0 4%, transparent 5%), - radial-gradient(circle at 58% 46%, var(--bubble-cyan), var(--bubble-blue) 42%, rgba(43, 137, 255, 0.035) 67%, transparent 72%); - box-shadow: - 112px 300px 0 -190px rgba(92, 206, 255, 0.13), - 188px -38px 0 -214px rgba(43, 137, 255, 0.12), - 32px 30px 100px rgba(43, 137, 255, 0.08); - animation: bubble-drift-left 26s ease-in-out -8s infinite; -} - -body::after { - right: clamp(-310px, -19vw, -200px); - bottom: 8vh; - background: - radial-gradient(circle at 32% 30%, rgba(255, 255, 255, 0.72) 0 3.5%, transparent 4.5%), - radial-gradient(circle at 42% 44%, rgba(92, 206, 255, 0.16), rgba(43, 137, 255, 0.16) 44%, rgba(43, 137, 255, 0.03) 68%, transparent 73%); - box-shadow: - -128px -236px 0 -202px rgba(92, 206, 255, 0.12), - -194px 54px 0 -218px rgba(43, 137, 255, 0.12), - -28px 30px 100px rgba(43, 137, 255, 0.08); - animation: bubble-drift-right 31s ease-in-out -17s infinite; -} - -@keyframes bubble-drift-left { - 0%, - 100% { - transform: translate3d(0, 0, 0) rotate(0deg); - } - - 36% { - transform: translate3d(14px, -18px, 0) rotate(1.5deg); - } - - 72% { - transform: translate3d(-8px, 12px, 0) rotate(-1deg); - } -} - -@keyframes bubble-drift-right { - 0%, - 100% { - transform: translate3d(0, 0, 0) rotate(0deg); - } - - 42% { - transform: translate3d(-16px, 14px, 0) rotate(-1.4deg); - } - - 76% { - transform: translate3d(10px, -16px, 0) rotate(0.9deg); - } -} - -#app { - position: relative; - z-index: 1; - min-height: 100vh; -} - -button { - font: inherit; -} - -button, -a { - -webkit-tap-highlight-color: transparent; -} - -a { - color: inherit; - text-decoration: none; -} - -p a { - color: var(--accent); -} - -p a:hover { - color: var(--accent); -} - -code { - font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace; -} - -::selection { - background: var(--surface-3); - color: var(--ink); -} - -:where(a, button):focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} - -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.container { - width: min(100%, 1120px); - margin-inline: auto; - padding-inline: 24px; -} - -.section { - margin-top: 96px; -} - -.eyebrow { - margin-bottom: 14px; - color: var(--ink-subtle); - font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace; - font-size: 0.78rem; - font-weight: 500; - line-height: 1.4; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.reveal { - opacity: 0; - transform: translateY(12px); - transition: opacity 0.5s ease, transform 0.5s ease; -} - -.reveal.is-visible { - opacity: 1; - transform: none; -} - -.display-md { - font-size: clamp(28px, 3.5vw, 40px); - font-weight: 600; - line-height: 1.19; - letter-spacing: -0.025em; -} - -.button { - display: inline-flex; - min-height: 44px; - align-items: center; - justify-content: center; - padding: 10px 18px; - border: 0; - border-radius: var(--r-pill); - cursor: pointer; - font-size: 14px; - font-weight: 600; - line-height: 1.4; - transition: background-color 120ms ease, color 120ms ease; -} - -.button-primary { - background: var(--pill-dark); - color: var(--pill-dark-ink); -} - -.button-primary:hover { - background: var(--ink-muted); -} - -.button-secondary { - border: 1px solid var(--hairline); - background: var(--canvas); - color: var(--ink); -} - -.button-secondary:hover { - background: var(--surface-1); -} - -@media (min-width: 1024px) { - .container { - padding-inline: 48px; - } -} - -@media (max-width: 640px) { - body::before, - body::after { - opacity: 0.72; - } - - .section { - margin-top: 48px; - } -} - -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - scroll-behavior: auto !important; - animation: none !important; - transition: none !important; - } - - .reveal { - opacity: 1; - transform: none; - transition: none; - } -} diff --git a/apps/site/src/webmcp.js b/apps/site/src/webmcp.js deleted file mode 100644 index 9731fad6..00000000 --- a/apps/site/src/webmcp.js +++ /dev/null @@ -1,42 +0,0 @@ -import { INSTALL_CHANNELS } from './install-channels'; - -const installCommandTool = { - name: 'get-install-command', - description: 'Get the official Pythinker Code installation command for a supported platform or package manager.', - inputSchema: { - type: 'object', - properties: { - channel: { - type: 'string', - description: 'Installation channel.', - enum: INSTALL_CHANNELS.map(({ id }) => id), - }, - }, - required: ['channel'], - additionalProperties: false, - }, - execute({ channel }) { - const selected = INSTALL_CHANNELS.find(({ id }) => id === channel); - if (!selected) throw new Error(`Unsupported installation channel: ${channel}`); - - return { - content: [{ type: 'text', text: `${selected.label}: ${selected.command}` }], - }; - }, -}; - -export function registerWebMcpTools(modelContext = navigator.modelContext) { - if (!modelContext) return false; - - if (typeof modelContext.provideContext === 'function') { - modelContext.provideContext({ tools: [installCommandTool] }); - return true; - } - - if (typeof modelContext.registerTool === 'function') { - modelContext.registerTool(installCommandTool); - return true; - } - - return false; -} diff --git a/apps/site/vite.config.js b/apps/site/vite.config.js deleted file mode 100644 index 387d02e9..00000000 --- a/apps/site/vite.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; - -const pkg = JSON.parse( - readFileSync(new URL('../pythinker-code/package.json', import.meta.url), 'utf8'), -); - -export default defineConfig({ - plugins: [vue()], - build: { target: 'es2022' }, - define: { __PYTHINKER_VERSION__: JSON.stringify(pkg.version) }, -}); diff --git a/packages/oauth/src/oauth-pages.ts b/packages/oauth/src/oauth-pages.ts index 84a051c5..d18a9b94 100644 --- a/packages/oauth/src/oauth-pages.ts +++ b/packages/oauth/src/oauth-pages.ts @@ -50,7 +50,8 @@ const ERROR_BADGE_SVG = '' + ''; -// Palette and card treatment mirror apps/site/src/style.css (light canvas, +// Palette and card treatment mirror src/style.css in the private site +// repository PyModel/pythinker-site (light canvas, // grid background, hairline card, blue accent). Inline-only: the Inter/mono // families fall back to system fonts because this page must work offline. const GRID_BG =