feat: add migrate-v2.sh — standalone v1 → v2 migration script
New entry point: `bash migrate-v2.sh` from the v2 checkout. Replaces the old setup-embedded migration flow with a standalone 4-phase script + rewritten Claude skill for the interactive parts. Phase 0: Bootstrap (Node/pnpm/deps via setup.sh) + find v1 Phase 1: Core state (env, DB, groups, sessions, tasks) Phase 2: Channels (clack multiselect, auth copy, code install) Phase 3: Infrastructure (OneCLI, auth, Docker, skills, container build) Service switchover: stop v1 → start v2 → test → keep or revert Phase 4: Handoff → exec claude "/migrate-from-v1" The skill handles: owner seeding, access policy, CLAUDE.local.md cleanup, container config validation, fork customization porting. Key fixes found during testing: - triggerToEngage: requires_trigger=0 must override non-empty pattern - unknown_sender_policy defaults to 'public' (strict drops all msgs before owner is seeded) - Service revert must stop v2 (parse unit name from step log, not early tsx one-liner that can fail) - Session continuity: copy JSONL from -workspace-group/ to -workspace-agent/ and write continuation:claude into outbound.db - container_config.additionalMounts written directly to container.json (same shape in v1 and v2) - EXIT trap writes handoff.json; explicit write_handoff before exec Includes migrate-v2-reset.sh for dev iteration and docs/migration-dev.md for testing/debugging reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
134
setup/migrate-v2/channel-auth.ts
Normal file
134
setup/migrate-v2/channel-auth.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* migrate-v2 step: channel-auth
|
||||
*
|
||||
* Copy channel auth state from v1 to v2 for selected channels.
|
||||
* Handles both env keys and on-disk auth files (Baileys, Matrix, etc.)
|
||||
* per the CHANNEL_AUTH_REGISTRY.
|
||||
*
|
||||
* Usage: pnpm exec tsx setup/migrate-v2/channel-auth.ts <v1-path> <channel1> [channel2...]
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { CHANNEL_AUTH_REGISTRY } from '../migrate-v1/shared.js';
|
||||
|
||||
function parseEnv(filePath: string): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
if (!fs.existsSync(filePath)) return out;
|
||||
for (const line of fs.readFileSync(filePath, 'utf-8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
out.set(trimmed.slice(0, eq).trim(), trimmed.slice(eq + 1));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function appendEnvKey(envPath: string, key: string, value: string): boolean {
|
||||
const existing = parseEnv(envPath);
|
||||
if (existing.has(key)) return false;
|
||||
|
||||
let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
||||
if (content && !content.endsWith('\n')) content += '\n';
|
||||
content += `${key}=${value}\n`;
|
||||
fs.writeFileSync(envPath, content);
|
||||
return true;
|
||||
}
|
||||
|
||||
function copyGlob(v1Root: string, v2Root: string, relativePath: string): string[] {
|
||||
const src = path.join(v1Root, relativePath);
|
||||
if (!fs.existsSync(src)) return [];
|
||||
|
||||
const copied: string[] = [];
|
||||
const stat = fs.statSync(src);
|
||||
|
||||
if (stat.isFile()) {
|
||||
const dst = path.join(v2Root, relativePath);
|
||||
if (!fs.existsSync(dst)) {
|
||||
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
||||
fs.copyFileSync(src, dst);
|
||||
copied.push(relativePath);
|
||||
}
|
||||
} else if (stat.isDirectory()) {
|
||||
const dst = path.join(v2Root, relativePath);
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const sub = path.join(relativePath, entry.name);
|
||||
copied.push(...copyGlob(v1Root, v2Root, sub));
|
||||
}
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const args = process.argv.slice(2);
|
||||
const v1Path = args[0];
|
||||
const channels = args.slice(1);
|
||||
|
||||
if (!v1Path || channels.length === 0) {
|
||||
console.error('Usage: tsx setup/migrate-v2/channel-auth.ts <v1-path> <channel1> [channel2...]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const v1EnvPath = path.join(v1Path, '.env');
|
||||
const v2EnvPath = path.join(process.cwd(), '.env');
|
||||
const v1Env = parseEnv(v1EnvPath);
|
||||
|
||||
let envKeysCopied = 0;
|
||||
let filesCopied = 0;
|
||||
let channelsProcessed = 0;
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const channel of channels) {
|
||||
const spec = CHANNEL_AUTH_REGISTRY[channel];
|
||||
if (!spec) {
|
||||
// Unknown channel — just try copying env keys with common naming
|
||||
channelsProcessed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy env keys
|
||||
for (const key of spec.v1EnvKeys) {
|
||||
const value = v1Env.get(key);
|
||||
if (value) {
|
||||
if (appendEnvKey(v2EnvPath, key, value)) {
|
||||
envKeysCopied++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check required v2 keys — report missing ones
|
||||
const v2Env = parseEnv(v2EnvPath);
|
||||
for (const req of spec.requiredV2Keys) {
|
||||
if (!v2Env.has(req.key)) {
|
||||
missing.push(`${channel}:${req.key} (${req.where})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy on-disk auth files
|
||||
for (const candidate of spec.candidatePaths) {
|
||||
const copied = copyGlob(v1Path, process.cwd(), candidate);
|
||||
filesCopied += copied.length;
|
||||
}
|
||||
|
||||
channelsProcessed++;
|
||||
}
|
||||
|
||||
// Sync to data/env/env
|
||||
if (fs.existsSync(v2EnvPath)) {
|
||||
const containerEnvDir = path.join(process.cwd(), 'data', 'env');
|
||||
try {
|
||||
fs.mkdirSync(containerEnvDir, { recursive: true });
|
||||
fs.copyFileSync(v2EnvPath, path.join(containerEnvDir, 'env'));
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
console.log(`OK:channels=${channelsProcessed},env_keys=${envKeysCopied},files=${filesCopied}`);
|
||||
if (missing.length > 0) {
|
||||
console.log(`MISSING:${missing.join(',')}`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user