theseus/bundled-addons/pithos/core/cli.js
Local Dev 4ad95b3c7a Theseus: bundle the Pithos add-on
Ships Pithos (the s3d control panel) as a built-in extension: the dock
menu opens it in a tab served from a loopback port, and "Stop s3d"
shuts the gateway down. Generated by Pithos/scripts/build-theseus-addon.mjs.

yaml is vendored under vendor/yaml/lib rather than node_modules/ or
dist/, because Theseus git-ignores both and a clean-worktree release
build would otherwise ship the add-on without its only dependency.
2026-10-03 19:03:05 +02:00

93 lines
3.7 KiB
JavaScript

// Typed wrappers over s3d's user/key subcommands. s3d has no HTTP API for
// these; the CLI opens the same SQLite file the daemon uses (WAL mode, so it
// is safe while the daemon is running).
import { spawn } from 'node:child_process';
export class CliError extends Error {}
function check({ code, stdout, stderr }) {
if (code !== 0) throw new CliError((stderr || stdout).trim() || `s3d exited with code ${code}`);
return stdout;
}
export function makeCli(daemon) {
return {
// Whether this s3d holds an app key. `s3d login` reports an existing
// registration before it reads any input; with stdin closed an
// unregistered instance hits EOF at the first prompt and exits.
async registration() {
const { stdout } = await new Promise((resolve, reject) => {
const child = spawn(daemon.binary, ['login'], {
cwd: daemon.cwd(), env: daemon.env(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
});
let out = '';
child.stdout.on('data', (d) => { out += d; });
child.stderr.on('data', (d) => { out += d; });
const timer = setTimeout(() => child.kill(), 15_000);
child.on('error', reject);
child.on('exit', () => { clearTimeout(timer); resolve({ stdout: out.replace(/\x1b\[[0-9;]*m/g, '') }); });
});
const m = stdout.match(/already registered with (\S+?)\.?\s*$/m);
return m ? { registered: true, indexerUrl: m[1] } : { registered: false };
},
async version() {
const out = check(await daemon.run(['version']));
const m = out.match(/s3d v?(\S+)/);
const commit = out.match(/Commit:\s*(\S+)/);
return { version: m ? m[1] : null, commit: commit ? commit[1] : null, raw: out.trim() };
},
async listUsers() {
const out = check(await daemon.run(['users', 'list']));
if (/^No users found/m.test(out)) return [];
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
},
async createUser(name) {
validateName(name);
check(await daemon.run(['users', 'create', name]));
},
async deleteUser(name) {
validateName(name);
check(await daemon.run(['users', 'delete', name]));
},
// [{ accessKeyId, secretKey, user }]
async listKeys(user) {
const out = check(await daemon.run(user ? ['keys', 'list', user] : ['keys', 'list']));
if (/^No access keys found/m.test(out)) return [];
return out.split(/\r?\n/).filter(Boolean).map((line) => {
const [accessKeyId, secretKey, ...rest] = line.split('\t');
return { accessKeyId, secretKey, user: rest.join('\t') };
});
},
async createKey(user, { accessKey, secretKey } = {}) {
validateName(user);
const args = ['keys', 'create'];
if (accessKey || secretKey) args.push('--access-key', accessKey || '', '--secret-key', secretKey || '');
args.push(user);
const out = check(await daemon.run(args));
const ak = out.match(/Access Key:\s*(\S+)/);
const sk = out.match(/Secret Key:\s*(\S+)/);
if (!ak || !sk) throw new CliError(`unexpected output from s3d keys create: ${out.trim()}`);
return { accessKeyId: ak[1], secretKey: sk[1], user };
},
async deleteKey(accessKeyId) {
if (!/^[\x21-\x7e]{1,128}$/.test(accessKeyId)) throw new CliError('invalid access key id');
check(await daemon.run(['keys', 'delete', accessKeyId]));
},
};
}
// Users are passed as argv, never through a shell, but a leading '-' would be
// read as a flag by s3d's parser.
function validateName(name) {
if (typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._@-]{0,63}$/.test(name)) {
throw new CliError('user names are 1-64 characters: letters, digits, . _ @ -, not starting with a symbol');
}
}