// Create the per-user root under DATA_PATH for the given accounts. // // bun scripts/provision-user-dirs.ts a@b.com c@d.com // DRY_RUN=1 bun scripts/provision-user-dirs.ts a@b.com // // Takes emails as arguments rather than reading the user table: the directory layout does not depend // on the database, and keeping the DB out means this runs with nothing else up. At invite time the // caller already knows the email. // // This is the directory skeleton ONLY. It is deliberately not a revival of the old provision.ts, which // also seeded shell rc files and wrote a generated CLAUDE.md + settings.json describing the user's // Docker container. That architecture is gone. What survives from it is the useful part: a user has a // root, and a home inside it. // // Idempotent — an existing directory is left exactly as it is, so re-running is safe. import { mkdirSync, existsSync } from 'node:fs'; import { join } from 'node:path'; const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); // Mirrors the shape the owner's root grew organically. Most of these are also created on demand by // whichever feature owns them (attachments, dashboards, email_accounts…), so pre-creating them buys // legibility more than function — the tree shows what a user has without having to use it first. // // `home` is the exception, and the reason this exists at all: nothing creates it today. getOwnerHomeDir // returns process.env.HOME_DIR whenever it is set, which it always is on a real install, so the // per-user home has never actually been reached. It is where a non-owner's sessions will run. const USER_DIRS = [ 'home', 'attachments', 'cache', 'dashboards', 'email_accounts', 'general_chat_sessions', 'logs', 'sidecar', ] as const; const DRY_RUN = process.env.DRY_RUN === '1'; const emails = process.argv.slice(2).filter(Boolean); if (!emails.length) { console.error('Usage: bun scripts/provision-user-dirs.ts [email...]'); process.exit(2); } // A stray argument would create a junk directory next to real user roots, and it would look like a // user. Cheap to refuse. const invalid = emails.filter((e) => !/^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/.test(e)); if (invalid.length) { console.error(`Not valid email addresses: ${invalid.join(', ')}`); process.exit(2); } console.log(`DATA_PATH = ${DATA_PATH}`); console.log(`${DRY_RUN ? 'Would provision' : 'Provisioning'} ${emails.length} user root(s)\n`); for (const email of emails) { const root = join(DATA_PATH, email); console.log(`${email}${existsSync(root) ? '' : ' [new root]'}`); for (const dir of USER_DIRS) { const path = join(root, dir); if (existsSync(path)) { console.log(` · ${dir} (exists)`); continue; } if (!DRY_RUN) mkdirSync(path, { recursive: true }); console.log(` ${DRY_RUN ? '+' : '✓'} ${dir}`); } console.log(''); }