import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { FILES, RELEASE } from './publish-five-views.mjs'; import { buildIdentityFunctions } from '../server/identity/build-release.mjs'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const sha = (b) => crypto.createHash('sha256').update(b).digest('hex'); const baseline = JSON.parse(fs.readFileSync(path.join(root, 'deploy/five-views-production-baseline-20261103.json'))); const prior = new Map(baseline.records.map((r) => [r.path, r])); const manifest = { version: 'ksf.five-views.publish.v1', release: RELEASE, homepageSha256: prior.get('index.html').rawSha256, files: FILES.map((name) => ({ path: name, bytes: fs.statSync(path.join(root, name)).size, sha256: sha(fs.readFileSync(path.join(root, name))), allowMissing: !prior.get(name)?.present, priorNormalizedSha256: prior.get(name)?.present ? [prior.get(name).normalizedSha256] : [] })), preserved: ['src/services/board-service.js', 'src/components/kf-opportunity-card.js', 'src/pages/view-pool.js', 'src/core/report.js', 'src/core/evidence/chain.js'] .map((name) => { const normalized = sha(Buffer.from(fs.readFileSync(path.join(root, name), 'utf8').replaceAll('\r\n', '\n'))); if (normalized !== prior.get(name)?.normalizedSha256) throw new Error('Published repair would change: ' + name); return { path: name, normalizedSha256: normalized }; }), }; fs.writeFileSync(path.join(root, 'deploy/five-views-files-20261103.json'), JSON.stringify(manifest, null, 2)); if (!process.argv.includes('--manifest-only')) { const index = process.argv.indexOf('--out'); if (index < 0 || !process.argv[index + 1]) throw new Error('Use --out ABSOLUTE_NEW_DIRECTORY or --manifest-only'); const out = process.argv[index + 1]; if (!path.isAbsolute(out) || fs.existsSync(out) || out === root) throw new Error('Output must be a new absolute directory'); fs.mkdirSync(out, { recursive: true }); const copy = (source, target = source) => { const dest = path.join(out, target); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(path.join(root, source), dest); }; for (const file of FILES) copy(file, 'payload/' + file); for (const file of ['scripts/publish-five-views.mjs', 'deploy/five-views-files-20261103.json', 'deploy/five-views-production-baseline-20261103.json', 'docs/showcase/prd.md', 'docs/showcase/schema.md', 'docs/showcase/api.md', 'docs/showcase/design.md', 'docs/showcase/test.md', 'docs/showcase/deploy.md', 'docs/showcase/validation-result.json']) copy(file); const identityIndex = process.argv.indexOf('--identity-base'); if (identityIndex >= 0) { const uploadIndex = process.argv.indexOf('--identity-upload-bases'); const functions = buildIdentityFunctions(JSON.parse(fs.readFileSync(process.argv[identityIndex + 1], 'utf8')), { uploadBases: uploadIndex < 0 ? [] : JSON.parse(fs.readFileSync(process.argv[uploadIndex + 1], 'utf8')), }); const dist = path.join(out, 'server/identity/dist'); fs.mkdirSync(dist, { recursive: true }); for (const entry of functions) fs.writeFileSync(path.join(dist, entry.name + '.js'), entry.code); fs.writeFileSync(path.join(dist, 'manifest.json'), JSON.stringify({ version: 'ksf.wecom-identity.20261103.v1', functions: functions.map(({ code, ...rest }) => rest) }, null, 2)); for (const file of ['wecom-directory.mjs', 'employee-handlers.mjs', 'build-release.mjs', 'manage.mjs', 'scheduled-sync.mjs', 'directory.example.json', 'platform.example.json']) copy('server/identity/' + file); copy('docs/wecom-identity-integration-20261103.md'); } const all = fs.readdirSync(out, { recursive: true }).filter((p) => fs.statSync(path.join(out, p)).isFile()).sort(); fs.writeFileSync(path.join(out, 'FILES.sha256'), all.map((p) => sha(fs.readFileSync(path.join(out, p))) + ' ' + p.replaceAll('\\', '/')).join('\n') + '\n'); console.log(JSON.stringify({ ok: true, release: RELEASE, out, payloadFiles: FILES.length, files: all.length + 1, deployed: false })); } else console.log(JSON.stringify({ ok: true, payloadFiles: FILES.length, manifestOnly: true }));