diff --git a/scripts/provision-user-dirs.ts b/scripts/provision-user-dirs.ts new file mode 100644 index 00000000..403a69f9 --- /dev/null +++ b/scripts/provision-user-dirs.ts @@ -0,0 +1,73 @@ +// 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(''); +}