Unify Wiki and Studio and remove deployment scaffolding
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
try {
|
||||
const port = process.env.PORT || '3000';
|
||||
const [health, ready] = await Promise.all([
|
||||
fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(5000) }),
|
||||
fetch(`http://127.0.0.1:${port}/api/ready`, { signal: AbortSignal.timeout(5000) }),
|
||||
]);
|
||||
const data = await health.json();
|
||||
if (!health.ok || !ready.ok || data.service !== 'prompt' || data.release !== (process.env.RELEASE_SHA || 'development')) process.exit(1);
|
||||
} catch { process.exit(1); }
|
||||
@@ -1,38 +0,0 @@
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {join,dirname} from 'node:path';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {requiredPackImages} from './verify-character-pack.mjs';
|
||||
const root=join(dirname(fileURLToPath(import.meta.url)),'..','seed','characters');
|
||||
const base=process.env.PROMPT_IMPORT_BASE_URL||'http://127.0.0.1:3000';
|
||||
const media=JSON.parse(await readFile(join(root,'..','..','src','content','managed-media.json'),'utf8'));
|
||||
const packFiles=pack=>Object.entries(media).filter(([name])=>name.startsWith(`apps/waterfall-wiki/seed/characters/${pack.directory}/`)).sort(([a],[b])=>a.localeCompare(b));
|
||||
const packs=JSON.parse(await readFile(join(root,'packs.json'),'utf8'));
|
||||
async function verify(pack){
|
||||
let cursor='',post;const seen=new Set();
|
||||
do{const response=await fetch(base+'/api/feed?'+new URLSearchParams({section:'images',...(cursor?{cursor}:{})}));if(!response.ok)throw Error('Cannot verify character feed: '+response.status);const page=await response.json();post=page.posts.find(p=>p.slug===pack.slug);cursor=page.nextCursor||'';if(cursor&&seen.has(cursor))throw Error('Character feed cursor did not advance');seen.add(cursor);}while(!post&&cursor);
|
||||
if(!post)throw Error('Character pack missing: '+pack.slug);
|
||||
const files=packFiles(pack);if(files.length!==pack.count)throw Error('Seed file count mismatch: '+pack.slug);
|
||||
const hashes=files.map(([,asset])=>asset.sha256);
|
||||
const originals=requiredPackImages(hashes,post.assets.filter(a=>a.kind==='image'));
|
||||
for(const image of originals){const media=await fetch(`${base}/api/media/${image.id}`,{method:'HEAD'});if(!media.ok||media.headers.get('content-type')!=='image/png')throw Error('Original PNG unavailable');}
|
||||
console.log(`Verified: /p/${pack.slug} (${originals.length} original PNGs preserved; editorial covers allowed)`);
|
||||
}
|
||||
for(const pack of packs){
|
||||
const existing=await fetch(`${base}/p/${pack.slug}`);
|
||||
if(existing.ok){console.log(`Already imported: /p/${pack.slug}`);await verify(pack);continue;}
|
||||
if(existing.status!==404)throw new Error(`Cannot check pack ${pack.slug}: ${existing.status}`);
|
||||
const files=packFiles(pack);
|
||||
if(files.length!==pack.count)throw new Error(`Expected ${pack.count} PNGs for ${pack.slug}, got ${files.length}`);
|
||||
const data=new FormData();data.set('title',pack.title);data.set('slug',pack.slug);data.set('description',pack.description);
|
||||
for(const [name,asset] of files){
|
||||
const response=await fetch(`${base}/api/media/${asset.id}`);if(!response.ok)throw Error('Cannot read migrated original: '+name);
|
||||
const bytes=Buffer.from(await response.arrayBuffer());if(bytes.length!==asset.bytes||createHash('sha256').update(bytes).digest('hex')!==asset.sha256)throw Error('Original checksum mismatch: '+name);
|
||||
data.append('files',new Blob([bytes],{type:'image/png'}),name.split('/').pop());
|
||||
}
|
||||
const response=await fetch(`${base}/api/skills/street-people`,{method:'POST',body:data});
|
||||
if(response.status!==201&&response.status!==409)throw new Error(`Import ${pack.slug}: ${response.status} ${await response.text()}`);
|
||||
if(response.status===409&&!(await fetch(`${base}/p/${pack.slug}`)).ok)throw new Error(`Conflicting import ${pack.slug}`);
|
||||
console.log(`Imported: /p/${pack.slug} (${files.length} transparent PNGs)`);
|
||||
await verify(pack);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { cp,mkdir,access } from 'node:fs/promises';
|
||||
await mkdir('.next/standalone/apps/waterfall-wiki/.next',{recursive:true});
|
||||
await cp('.next/static','.next/standalone/apps/waterfall-wiki/.next/static',{recursive:true});
|
||||
try{await access('public');await cp('public','.next/standalone/apps/waterfall-wiki/public',{recursive:true});}catch(e){if(e.code!=='ENOENT')throw e;}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { db } from "../src/lib/db";
|
||||
import { publishLearningSeries } from "../src/lib/publish-learning-series";
|
||||
publishLearningSeries().then(result=>console.log(`Published learning series: ${result.published.join(", ")}; one shared cover: ${result.coverId}`))
|
||||
.catch(error=>{console.error("Learning series publication failed:",error instanceof Error?error.message:"Unknown error");process.exitCode=1;})
|
||||
.finally(()=>db().end());
|
||||
@@ -1,65 +0,0 @@
|
||||
import {writeFile,unlink} from 'node:fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { spawn } from "node:child_process";
|
||||
import assert from "node:assert/strict";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { createServer } from "node:net";
|
||||
import { once } from "node:events";
|
||||
|
||||
async function availablePort() {
|
||||
const server = createServer();
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const port = server.address().port;
|
||||
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
||||
return String(port);
|
||||
}
|
||||
|
||||
const port = process.env.SMOKE_PORT || await availablePort();
|
||||
const release = "smoke-test";
|
||||
const previewFixture='.next/standalone/apps/waterfall-wiki/public/__smoke-preview.png';
|
||||
await writeFile(previewFixture,await sharp({create:{width:48,height:48,channels:3,background:'#eee'}}).png().toBuffer());
|
||||
const child = spawn(process.execPath, [".next/standalone/apps/waterfall-wiki/server.js"], {
|
||||
// This loopback-only child checks retained resources without production credentials.
|
||||
// Production keeps failing closed; the bypass requires both explicit test settings.
|
||||
env: { ...process.env, AUTH_CONFIG_FILE: "", AUTH_DISABLED_FOR_TESTS: "true", PORT: port, HOSTNAME: "127.0.0.1", RELEASE_SHA: release },
|
||||
stdio: "inherit",
|
||||
});
|
||||
try {
|
||||
let ready = false;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (child.exitCode !== null) throw new Error(`Server exited: ${child.exitCode}`);
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { status: "ok", service: "prompt", release });
|
||||
ready = true;
|
||||
break;
|
||||
} catch { await setTimeout(500); }
|
||||
}
|
||||
assert.ok(ready, "Standalone production server must be healthy");
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/health`, { method: "POST" });
|
||||
assert.equal(response.status, 405);
|
||||
const indexResponse=await fetch(`http://127.0.0.1:${port}/api/wiki-index`);
|
||||
assert.equal(indexResponse.status,200);
|
||||
const indexText=await indexResponse.text();
|
||||
const id=indexText.match(/\/explore\/([a-f0-9]{24})/)?.[1];assert.ok(id);
|
||||
const agent=await fetch(`http://127.0.0.1:${port}/explore/${id}?agent_mode=True`);
|
||||
assert.equal(agent.status,200);assert.match(agent.headers.get('content-type'),/text\/plain/);const agentText=await agent.text();assert.match(agentText,/^# /);assert.doesNotMatch(agentText,/^\s*(?:<!doctype html|<html)/i);
|
||||
const preview=await fetch(`http://127.0.0.1:${port}/api/preview?source=${encodeURIComponent('/__smoke-preview.png')}&width=480`);
|
||||
assert.equal(preview.status,200);assert.match(preview.headers.get('content-type'),/image\/webp/);assert.ok((await preview.arrayBuffer()).byteLength<=100000);
|
||||
const home=await fetch(`http://127.0.0.1:${port}/`).then(r=>r.text());
|
||||
for(const href of ['/graphic','/infinite_canvas','/account?next=/workspace'])assert.ok(home.includes(`href="${href}"`),href);
|
||||
for(const [legacy,canonical] of [['/studio/graphic','/graphic'],['/studio/infinite_canvas','/infinite_canvas']]){
|
||||
const redirect=await fetch(`http://127.0.0.1:${port}${legacy}`,{redirect:'manual'});
|
||||
assert.equal(redirect.status,308);assert.equal(redirect.headers.get('location'),canonical);
|
||||
}
|
||||
console.log("Production smoke test passed, including landing entry points and canonical redirects");
|
||||
} finally {
|
||||
await unlink(previewFixture);
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
const exited = once(child, "exit");
|
||||
child.kill("SIGTERM");
|
||||
await exited;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/** Extra editorial covers are allowed; every original seed image must remain attached. */
|
||||
export function requiredPackImages(hashes,images){
|
||||
const remaining=[...images],originals=[];
|
||||
for(const hash of hashes){const index=remaining.findIndex(image=>image.sha256===hash);if(index<0)throw Error('Original seed image missing from character pack');originals.push(...remaining.splice(index,1));}
|
||||
return originals;
|
||||
}
|
||||
Reference in New Issue
Block a user