repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
pi/agent/skills/optional/quickshell/bin/qs-scaffold.mjs
Raw#!/usr/bin/env node
// qs-scaffold.mjs: scaffold a new Quickshell config from an example.
// Cross-platform: no shell, no POSIX assumptions. Pure Node builtins.
//
// Usage:
// qs-scaffold <name> [--from <example-name>]
//
// Default example: 02-per-monitor-bar (canonical per-monitor shape).
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SKILL_ROOT = join(__dirname, "..");
const EXAMPLES_DIR = join(SKILL_ROOT, "examples");
const DEFAULT_EXAMPLE = "02-per-monitor-bar";
function usage() {
process.stderr.write("usage: qs-scaffold <name> [--from <example-name>]\n");
process.stderr.write(
" example-name: e.g. 01-hello-bar, 07-composite-bar (omit the .qml)\n",
);
}
function parseArgs(argv) {
const out = { name: null, from: DEFAULT_EXAMPLE };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === "-h" || a === "--help") {
usage();
process.exit(0);
}
if (a === "--from") {
const v = argv[++i];
if (!v) {
process.stderr.write("qs-scaffold: --from requires a value\n");
process.exit(1);
}
out.from = v;
continue;
}
if (a.startsWith("--")) {
process.stderr.write(`qs-scaffold: unknown flag ${a}\n`);
process.exit(1);
}
if (!out.name) {
out.name = a;
continue;
}
process.stderr.write(`qs-scaffold: unexpected positional arg ${a}\n`);
process.exit(1);
}
return out;
}
function xdgConfigHome() {
if (process.env.XDG_CONFIG_HOME) return process.env.XDG_CONFIG_HOME;
if (process.env.HOME) return join(process.env.HOME, ".config");
// Windows fallback.
if (process.env.APPDATA) return process.env.APPDATA;
process.stderr.write(
"qs-scaffold: cannot determine config dir (set XDG_CONFIG_HOME or HOME)\n",
);
process.exit(1);
}
const { name, from } = parseArgs(process.argv);
if (!name) {
usage();
process.exit(1);
}
function requirePathSegment(label, value) {
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
process.stderr.write(
`qs-scaffold: ${label} must be a single path segment: ${value}\n`,
);
process.exit(1);
}
}
requirePathSegment("name", name);
requirePathSegment("example name", from);
// Validate example dir.
const exampleDir = join(EXAMPLES_DIR, from);
if (!existsSync(exampleDir) || !statSync(exampleDir).isDirectory()) {
process.stderr.write(
`qs-scaffold: example not found: ${from} (looked in ${EXAMPLES_DIR})\n`,
);
process.exit(1);
}
const configRoot = join(xdgConfigHome(), "quickshell");
const target = join(configRoot, name);
if (existsSync(target)) {
process.stderr.write(`qs-scaffold: target already exists: ${target}\n`);
process.exit(1);
}
mkdirSync(target, { recursive: true });
// Copy the example dir's QML + README. Skip hidden / non-source files.
for (const entry of readdirSync(exampleDir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
if (entry.name.startsWith(".")) continue;
if (!(entry.name.endsWith(".qml") || entry.name.endsWith(".md"))) continue;
copyFileSync(join(exampleDir, entry.name), join(target, entry.name));
}
// Touch .qmlls.ini for LSP.
writeFileSync(join(target, ".qmlls.ini"), "");
process.stdout.write(`Scaffolded ${target}\n`);
process.stdout.write(`Run: qs -c ${name}\n`);