Files
platform/scripts/provision-user-dirs.ts
T
pastilhasandClaude Opus 5 c4b262dc51 provision-user-dirs script now sets modes, by calling the real provisioner
The script imported DATA_PATH and USER_DIRS from data-path.ts and then
reimplemented the mkdir loop without the chmod that module's own
provisionUserDirs applies — 711 on the account root, 700 on each subdir. A
hand-provisioned account would have come out 755 at the default umask, where a
UI-provisioned one is closed. Its header comment warned about exactly this
drift while being an instance of it.

Nothing was ever created that way here: every account on this box was made by
the create-account handler and is correctly 711/700. This is a latent
divergence between two paths that claim to produce identical accounts, not
damage to repair.

Calls provisionUserDirs(email) instead. Two consequences worth naming:
re-running now re-asserts modes on existing directories, so it is also the
repair; and DRY_RUN skips the call outright rather than gaining a dry-run mode
in the real provisioning path to serve a script.

Verified: dry run creates nothing, a fresh root comes out 711/700, and a
directory chmod'd to 755 is restored on the next run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 02:07:56 +00:00

67 lines
3.4 KiB
TypeScript

// 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> [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('');
}