// 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 keeps its contents, so re-running is safe. It does NOT keep its // mode: provisionUserDirs re-asserts 711/700 on every run, tolerating the EPERM from a `home` that has // since become the member's own. Re-running is therefore also the repair. import { existsSync } from 'node:fs'; import { join } from 'node:path'; // The list and DATA_PATH itself come from the platform rather than being restated here. The owner's // create-account handler provisions the same skeleton, and a script that drifted from it would produce // accounts that differ by how they were made. Importing data-path.ts pulls in no database and no server. // provisionUserDirs rather than a local mkdir loop, and that is the whole point of this script: it used // to import DATA_PATH and USER_DIRS from this very module and then reimplement the loop WITHOUT the // chmod, so a hand-provisioned account got 755 where a UI-provisioned one got 711/700. Nothing was ever // created that way here, but "two paths that claim to produce identical accounts" is exactly the drift // the comment above already warned about โ€” while doing it. import { DATA_PATH, USER_DIRS, provisionUserDirs } from '../src/servers/data-path'; 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]'}`); // Probed BEFORE provisioning, because afterwards everything exists and the log would say so. const existing = new Set(USER_DIRS.filter((dir) => existsSync(join(root, dir)))); // Skipped entirely under DRY_RUN rather than called: provisionUserDirs has no dry-run mode, and giving // it one to serve this script would put a branch in the real provisioning path for a script's benefit. if (!DRY_RUN) provisionUserDirs(email); for (const dir of USER_DIRS) { if (existing.has(dir)) console.log(` ยท ${dir} (exists${DRY_RUN ? '' : ', mode re-asserted'})`); else console.log(` ${DRY_RUN ? '+' : 'โœ“'} ${dir}`); } console.log(''); }