Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,18 @@
{
"name": "@toolctrl/create-toolctrl",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"create-toolctrl": "./src/create.js"
},
"exports": {
".": "./src/create.js"
},
"scripts": {
"test": "vitest run src"
},
"devDependencies": {
"vitest": "^3.2.4"
}
}
@@ -0,0 +1,185 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const PACKAGE_DIR = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const REPO_ROOT = path.resolve(PACKAGE_DIR, "..", "..");
const PROFILES = new Set(["core", "saas", "full"]);
const EXCLUDED_DIRS = new Set([
".bg-shell",
".claude",
".git",
".react-router",
".turbo",
".next",
".tc",
"build",
"coverage",
"dist",
"node_modules",
]);
function isSecretOrRuntimeFile(relativePath) {
const base = path.basename(relativePath);
if (/^\.env($|\.test$|\.production$|\.local$)/.test(base)) {
return true;
}
if (/token|secret|credential/i.test(base) && !base.endsWith(".example")) {
return true;
}
return base === ".tc-id";
}
function shouldCopy(source, sourceRoot = REPO_ROOT) {
const relativePath = path.relative(sourceRoot, source);
if (!relativePath) {
return true;
}
if (isSecretOrRuntimeFile(relativePath)) {
return false;
}
return !relativePath.split(path.sep).some((part) => EXCLUDED_DIRS.has(part));
}
function slugifyPackageName(appName) {
const slug = appName
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug || "toolctrl-app";
}
function modulesForProfile(profile) {
if (profile === "core") {
return ["core", "auth-tenancy"];
}
if (profile === "saas") {
return ["core", "auth-tenancy", "admin-app-shell", "billing", "transactional-mail", "rows-entities-runtime"];
}
return ["core", "auth-tenancy", "admin-app-shell", "billing", "transactional-mail", "rows-entities-runtime", "plugin-candidates"];
}
async function updateJsonFile(filePath, updater) {
const data = JSON.parse(await fs.readFile(filePath, "utf8"));
const updated = updater(data);
await fs.writeFile(filePath, `${JSON.stringify(updated, null, 2)}\n`);
}
async function rewriteEnvExample(targetDir, appName) {
const envPath = path.join(targetDir, ".env.example");
let content = await fs.readFile(envPath, "utf8");
content = content.replace(/^APP_NAME=.*$/m, `APP_NAME=${appName}`);
await fs.writeFile(envPath, content);
}
async function writeScaffoldReadme(targetDir, appName, profile) {
const readmePath = path.join(targetDir, "README.md");
const original = await fs.readFile(readmePath, "utf8");
const header = `# ${appName}\n\nGenerated from the local ToolCTRL boilerplate with the \`${profile}\` module profile.\n\n`;
await fs.writeFile(readmePath, `${header}${original}`);
}
async function writeScaffoldProfile(targetDir, appName, profile) {
const profilePath = path.join(targetDir, "docs", "feature-manifests", "scaffold-profile.json");
await fs.mkdir(path.dirname(profilePath), { recursive: true });
await fs.writeFile(
profilePath,
`${JSON.stringify(
{
id: "scaffold-profile",
title: "Scaffold Profile",
appName,
profile,
classification: profile === "core" ? "core" : profile === "saas" ? "default-enabled" : "optional-inclusive",
modules: modulesForProfile(profile),
generatedAt: new Date(0).toISOString(),
},
null,
2
)}\n`
);
}
export async function createToolctrlProject({ targetDir, appName, profile = "saas", sourceDir = REPO_ROOT }) {
if (!targetDir) {
throw new Error("targetDir is required.");
}
if (!PROFILES.has(profile)) {
throw new Error(`profile must be one of: ${[...PROFILES].join(", ")}.`);
}
const resolvedTarget = path.resolve(targetDir);
const resolvedSource = path.resolve(sourceDir);
const finalAppName = appName || path.basename(resolvedTarget);
await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
const existing = await fs.readdir(resolvedTarget).catch((error) => {
if (error.code === "ENOENT") {
return [];
}
throw error;
});
if (existing.length > 0) {
throw new Error(`Target directory is not empty: ${resolvedTarget}`);
}
await fs.cp(resolvedSource, resolvedTarget, {
recursive: true,
filter: (source) => shouldCopy(source, resolvedSource),
});
await updateJsonFile(path.join(resolvedTarget, "package.json"), (data) => ({
...data,
name: slugifyPackageName(finalAppName),
private: true,
}));
await rewriteEnvExample(resolvedTarget, finalAppName);
await writeScaffoldReadme(resolvedTarget, finalAppName, profile);
await writeScaffoldProfile(resolvedTarget, finalAppName, profile);
return {
targetDir: resolvedTarget,
appName: finalAppName,
packageName: slugifyPackageName(finalAppName),
profile,
};
}
function parseArgs(argv) {
const args = [...argv];
const options = { targetDir: "", appName: "", profile: "saas" };
while (args.length > 0) {
const arg = args.shift();
if (arg === "--name") {
options.appName = args.shift() ?? "";
} else if (arg === "--profile") {
options.profile = args.shift() ?? "saas";
} else if (!options.targetDir) {
options.targetDir = arg ?? "";
} else {
throw new Error(`Unexpected argument: ${arg}`);
}
}
return options;
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (!options.targetDir) {
throw new Error("Usage: create-toolctrl <target-dir> [--name AppName] [--profile core|saas|full]");
}
const result = await createToolctrlProject(options);
// eslint-disable-next-line no-console -- CLI output is the user-visible smoke result.
console.log(`Created ${result.appName} at ${result.targetDir} (${result.profile})`);
}
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (invokedDirectly) {
main().catch((error) => {
// eslint-disable-next-line no-console -- CLI must report fatal usage/setup errors.
console.error(error.message);
process.exit(1);
});
}
@@ -0,0 +1,75 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createToolctrlProject } from "./create.js";
const tempRoots = [];
async function exists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function collectFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await collectFiles(fullPath)));
} else {
files.push(fullPath);
}
}
return files;
}
async function createSourceFixture(rootDir) {
const sourceDir = path.join(rootDir, "source");
await fs.mkdir(path.join(sourceDir, "apps", "web", "build"), { recursive: true });
await fs.mkdir(path.join(sourceDir, "docs", "feature-manifests"), { recursive: true });
await fs.mkdir(path.join(sourceDir, ".git"), { recursive: true });
await fs.writeFile(path.join(sourceDir, "package.json"), JSON.stringify({ name: "toolctrl-boilerplate", private: true, scripts: {} }, null, 2));
await fs.writeFile(path.join(sourceDir, ".env.example"), "APP_NAME=ToolCTRL\nSESSION_SECRET=\n");
await fs.writeFile(path.join(sourceDir, ".env"), "SESSION_SECRET=secret\n");
await fs.writeFile(path.join(sourceDir, "README.md"), "# ToolCTRL\n");
await fs.writeFile(path.join(sourceDir, "apps", "web", "build", "artifact.js"), "build");
await fs.writeFile(path.join(sourceDir, ".git", "HEAD"), "ref: main\n");
return sourceDir;
}
afterEach(async () => {
await Promise.all(tempRoots.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
describe("createToolctrlProject", () => {
it("scaffolds a local workspace without secrets or build artifacts", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolctrl-create-"));
tempRoots.push(tempRoot);
const sourceDir = await createSourceFixture(tempRoot);
const targetDir = path.join(tempRoot, "acme-ops");
const result = await createToolctrlProject({ sourceDir, targetDir, appName: "Acme Ops", profile: "core" });
const packageJson = JSON.parse(await fs.readFile(path.join(targetDir, "package.json"), "utf8"));
const envExample = await fs.readFile(path.join(targetDir, ".env.example"), "utf8");
const profile = JSON.parse(await fs.readFile(path.join(targetDir, "docs", "feature-manifests", "scaffold-profile.json"), "utf8"));
const files = (await collectFiles(targetDir)).map((file) => path.relative(targetDir, file));
expect(result).toMatchObject({ appName: "Acme Ops", packageName: "acme-ops", profile: "core" });
expect(packageJson.name).toBe("acme-ops");
expect(envExample).toContain("APP_NAME=Acme Ops");
expect(profile).toMatchObject({ appName: "Acme Ops", profile: "core", modules: ["core", "auth-tenancy"] });
expect(await exists(path.join(targetDir, ".git"))).toBe(false);
expect(await exists(path.join(targetDir, "node_modules"))).toBe(false);
expect(await exists(path.join(targetDir, "apps", "web", "build"))).toBe(false);
expect(await exists(path.join(targetDir, ".env"))).toBe(false);
expect(files.some((file) => /(^|\/)\.env($|\.test$|\.local$|\.production$)/.test(file))).toBe(false);
expect(files.some((file) => /(^|\/)(build|dist|coverage)(\/|$)/.test(file))).toBe(false);
});
});