fundamentum-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +96 -0
  2. package/install.js +205 -0
  3. package/package.json +66 -0
  4. package/run.js +80 -0
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # fundamentum-cli
2
+
3
+ **One command to make your GitHub repo look (and behave) like a pro open-source project.**
4
+
5
+ [npm](https://www.npmjs.com/package/fundamentum-cli) · [GitHub](https://github.com/jpvelasco/fundamentum)
6
+
7
+ Fundamentum bootstraps and hardens GitHub repos for OSS collaboration — community health files, branch protection, secret scanning, and starter CI workflows. You bring the code, it brings the polish.
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/jpvelasco/fundamentum/actions/workflows/ci.yml"><img src="https://github.com/jpvelasco/fundamentum/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
+ <a href="https://github.com/jpvelasco/fundamentum/releases/latest"><img src="https://img.shields.io/github/v/release/jpvelasco/fundamentum" alt="Release"></a>
12
+ <a href="https://www.npmjs.com/package/fundamentum-cli"><img src="https://img.shields.io/npm/v/fundamentum-cli" alt="npm"></a>
13
+ <a href="https://www.npmjs.com/package/fundamentum-cli"><img src="https://img.shields.io/npm/dm/fundamentum-cli" alt="npm downloads"></a>
14
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
15
+ </p>
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -g fundamentum-cli
21
+ ```
22
+
23
+ Try without a global install:
24
+
25
+ ```bash
26
+ npx fundamentum-cli --version
27
+ npx fundamentum-cli apply OWNER/REPO
28
+ ```
29
+
30
+ Works on **macOS**, **Linux**, and **Windows** — `x64` and `arm64`. Postinstall downloads the matching prebuilt binary from GitHub Releases with embedded SHA-256 verification.
31
+
32
+ > **npm 9+ / Ubuntu 26+ note:** npm's `allow-scripts` security policy may block the postinstall
33
+ > script, so the binary won't be downloaded at install time. No worries — if `fundamentum` is invoked
34
+ > without a binary present it detects this and downloads automatically. You can also trigger it
35
+ > manually:
36
+ > ```bash
37
+ > node $(npm root -g)/fundamentum-cli/install.js
38
+ > ```
39
+
40
+ ## Quickstart
41
+
42
+ ```bash
43
+ # Harden an existing repo
44
+ fundamentum apply OWNER/REPO
45
+
46
+ # Create a new repo + harden it in one shot
47
+ fundamentum init OWNER/REPO
48
+
49
+ # Preview everything before touching anything
50
+ fundamentum --dry-run apply OWNER/REPO
51
+
52
+ # Apply file changes via a pull request
53
+ fundamentum --pr apply OWNER/REPO
54
+ ```
55
+
56
+ That's it. A repo that went from bare-bones to release-ready in under a minute.
57
+
58
+ ## What you get
59
+
60
+ - **Community health files** — `CONTRIBUTING.md`, `CODEOWNERS`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, issue + PR templates, `dependabot.yml`
61
+ - **Branch protection** — modern rulesets on `main` (PR-only pushes, CODEOWNERS review, required checks, no force-push)
62
+ - **Security** — secret scanning + push protection, Dependabot alerts, CodeQL
63
+ - **Starter workflows** — CI, coverage, and CodeQL pipelines that work out of the box
64
+ - **Idempotent by design** — re-running is always safe
65
+
66
+ ## Why fundamentum?
67
+
68
+ | Manual setup | fundamentum |
69
+ |---------------|-------------|
70
+ | 20+ files and settings across 6 GitHub pages | One command |
71
+ | "Which templates does everyone use?" | Opinionated, proven defaults |
72
+ | Copy-paste from another repo, hope it's current | Rendered fresh, version-controlled |
73
+ | "Did I forget CODEOWNERS?" | Full summary table before you apply |
74
+ | Private repo? Pro account? | Graceful fallbacks for free-tier |
75
+
76
+ Built for solo devs and small teams who want their repos to feel like they were made by someone who cared.
77
+
78
+ ## Commands
79
+
80
+ | Command | Purpose |
81
+ |---------|---------|
82
+ | `apply OWNER/REPO` | Harden an existing repo |
83
+ | `init OWNER/REPO` | Create a new repo, then harden it |
84
+ | `--dry-run` | Preview the full plan without applying |
85
+ | `--pr` | Batch file changes into a pull request |
86
+ | `--no-overwrite` | Only add missing files, never touch existing ones |
87
+
88
+ ## Prerequisites
89
+
90
+ A GitHub token with `repo` scope (classic) or Contents + Metadata + Administration (fine-grained) — via `GITHUB_TOKEN` or `--token`.
91
+
92
+ ## License
93
+
94
+ MIT — see [LICENSE](https://github.com/jpvelasco/fundamentum/blob/main/LICENSE).
95
+
96
+ fundamentum is an independent project, not affiliated with GitHub.
package/install.js ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ // nosemgrep: all — npm install shim, not application code
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const https = require("https");
8
+ const crypto = require("crypto");
9
+ const { spawnSync } = require("child_process");
10
+
11
+ const REPO = "jpvelasco/fundamentum";
12
+ const MAX_REDIRECTS = 5;
13
+ const DOWNLOAD_TIMEOUT_MS = 30000;
14
+
15
+ const RELEASE_HOSTS = new Set([
16
+ "github.com",
17
+ "objects.githubusercontent.com",
18
+ "github-releases.githubusercontent.com",
19
+ ]);
20
+
21
+ const PLATFORM_MAP = {
22
+ linux: "linux",
23
+ darwin: "darwin",
24
+ win32: "windows",
25
+ };
26
+
27
+ const ARCH_MAP = {
28
+ x64: "amd64",
29
+ arm64: "arm64",
30
+ };
31
+
32
+ function getArchiveName(version, platform, arch) {
33
+ const os = PLATFORM_MAP[platform];
34
+ const cpu = ARCH_MAP[arch];
35
+ if (!os || !cpu) {
36
+ throw new Error(`Unsupported platform: ${platform}/${arch}`);
37
+ }
38
+ const ext = platform === "win32" ? "zip" : "tar.gz";
39
+ return `fundamentum_${version}_${os}_${cpu}.${ext}`;
40
+ }
41
+
42
+ function download(url, redirectCount = 0) {
43
+ return new Promise((resolve, reject) => {
44
+ if (redirectCount > MAX_REDIRECTS) {
45
+ return reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`));
46
+ }
47
+
48
+ const req = https.get(url, { timeout: DOWNLOAD_TIMEOUT_MS }, (res) => {
49
+ if (
50
+ res.statusCode >= 300 &&
51
+ res.statusCode < 400 &&
52
+ res.headers.location
53
+ ) {
54
+ const nextUrl = new URL(res.headers.location, url);
55
+ if (
56
+ !RELEASE_HOSTS.has(nextUrl.hostname) &&
57
+ !nextUrl.hostname.endsWith(".githubusercontent.com")
58
+ ) {
59
+ return reject(
60
+ new Error(`Refusing redirect to unexpected host: ${nextUrl.hostname}`)
61
+ );
62
+ }
63
+ res.resume();
64
+ return download(nextUrl.href, redirectCount + 1).then(resolve, reject);
65
+ }
66
+ if (res.statusCode !== 200) {
67
+ return reject(
68
+ new Error(`Download failed: HTTP ${res.statusCode} for ${url}`)
69
+ );
70
+ }
71
+ const chunks = [];
72
+ res.on("data", (chunk) => chunks.push(chunk));
73
+ res.on("end", () => resolve(Buffer.concat(chunks)));
74
+ res.on("error", reject);
75
+ });
76
+
77
+ req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
78
+ req.destroy(new Error(`Timed out downloading ${url}`));
79
+ });
80
+ req.on("error", reject);
81
+ });
82
+ }
83
+
84
+ function verifyChecksum(buffer, archiveName, pkg) {
85
+ const expected = pkg.binaryChecksums?.[archiveName] || null;
86
+ if (!expected) {
87
+ // A published package always has checksums embedded by the release
88
+ // workflow. A missing entry means asset-naming drift or tampering —
89
+ // fail closed rather than run an unverified binary.
90
+ if (pkg.version !== "0.0.0") {
91
+ throw new Error(
92
+ `No checksum found for ${archiveName}. Refusing to install an unverified binary.`
93
+ );
94
+ }
95
+ console.log("fundamentum: dev version, skipping checksum verification");
96
+ return;
97
+ }
98
+
99
+ const actual = crypto.createHash("sha256").update(buffer).digest("hex");
100
+ if (actual !== expected) {
101
+ throw new Error(
102
+ `Checksum mismatch for ${archiveName}\n` +
103
+ ` Expected: ${expected}\n` +
104
+ ` Actual: ${actual}`
105
+ );
106
+ }
107
+ console.log("fundamentum: checksum verified (SHA-256)");
108
+ }
109
+
110
+ // Escape a string for use inside PowerShell single quotes.
111
+ // PowerShell single-quoted strings only need '' to represent a literal '.
112
+ function psEscape(s) {
113
+ return s.replace(/'/g, "''");
114
+ }
115
+
116
+ function spawnOrFail(cmd, args, label) {
117
+ const result = spawnSync(cmd, args, { stdio: "pipe" });
118
+ if (result.error) {
119
+ throw new Error(`${label}: ${result.error.message}`);
120
+ }
121
+ if (result.status !== 0) {
122
+ const stderr = result.stderr ? result.stderr.toString().trim() : "";
123
+ throw new Error(
124
+ `${label} exited with code ${result.status}${stderr ? ": " + stderr : ""}`
125
+ );
126
+ }
127
+ }
128
+
129
+ function extract(buffer, archiveName, binDir) {
130
+ const tmpDir = path.join(__dirname, ".tmp-install");
131
+ fs.mkdirSync(tmpDir, { recursive: true });
132
+
133
+ const archivePath = path.join(tmpDir, archiveName);
134
+ fs.writeFileSync(archivePath, buffer);
135
+
136
+ try {
137
+ if (archiveName.endsWith(".zip")) {
138
+ if (process.platform === "win32") {
139
+ spawnOrFail(
140
+ "powershell",
141
+ [
142
+ "-NoProfile",
143
+ "-Command",
144
+ `Expand-Archive -Force -Path '${psEscape(
145
+ archivePath
146
+ )}' -DestinationPath '${psEscape(tmpDir)}'`,
147
+ ],
148
+ "Expand-Archive"
149
+ );
150
+ } else {
151
+ spawnOrFail("unzip", ["-o", archivePath, "-d", tmpDir], "unzip");
152
+ }
153
+ } else {
154
+ spawnOrFail("tar", ["-xzf", archivePath, "-C", tmpDir], "tar");
155
+ }
156
+
157
+ // Find the binary in the extracted files
158
+ const binaryName = process.platform === "win32" ? "fundamentum.exe" : "fundamentum";
159
+ const extractedBinary = path.join(tmpDir, binaryName);
160
+
161
+ if (!fs.existsSync(extractedBinary)) {
162
+ throw new Error(`Binary ${binaryName} not found in archive`);
163
+ }
164
+
165
+ fs.mkdirSync(binDir, { recursive: true });
166
+ const destBinary = path.join(binDir, binaryName);
167
+ fs.copyFileSync(extractedBinary, destBinary);
168
+
169
+ if (process.platform !== "win32") {
170
+ fs.chmodSync(destBinary, 0o755);
171
+ }
172
+ } finally {
173
+ fs.rmSync(tmpDir, { recursive: true, force: true });
174
+ }
175
+ }
176
+
177
+ async function main() {
178
+ const pkg = JSON.parse(
179
+ fs.readFileSync(path.join(__dirname, "package.json"), "utf8")
180
+ );
181
+ const version = pkg.version;
182
+ if (version === "0.0.0") {
183
+ console.error("fundamentum: skipping binary download for development version");
184
+ return;
185
+ }
186
+
187
+ const archiveName = getArchiveName(version, process.platform, process.arch);
188
+ const url = `https://github.com/${REPO}/releases/download/v${version}/${archiveName}`;
189
+ const binDir = path.join(__dirname, "bin");
190
+
191
+ console.log(`fundamentum: downloading ${archiveName}...`);
192
+ const buffer = await download(url);
193
+
194
+ verifyChecksum(buffer, archiveName, pkg);
195
+
196
+ console.log("fundamentum: extracting binary...");
197
+ extract(buffer, archiveName, binDir);
198
+
199
+ console.log("fundamentum: installed successfully");
200
+ }
201
+
202
+ main().catch((err) => {
203
+ console.error(`fundamentum: installation failed: ${err.message}`);
204
+ process.exit(1);
205
+ });
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "fundamentum-cli",
3
+ "version": "0.1.0",
4
+ "description": "Bootstrap and harden GitHub repos for OSS collaboration — community files, branch protection, security features, and starter workflows in one command.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jpvelasco/fundamentum.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/jpvelasco/fundamentum/issues"
12
+ },
13
+ "homepage": "https://github.com/jpvelasco/fundamentum#readme",
14
+ "bin": {
15
+ "fundamentum": "run.js"
16
+ },
17
+ "scripts": {
18
+ "postinstall": "node install.js"
19
+ },
20
+ "os": [
21
+ "linux",
22
+ "darwin",
23
+ "win32"
24
+ ],
25
+ "cpu": [
26
+ "x64",
27
+ "arm64"
28
+ ],
29
+ "keywords": [
30
+ "github",
31
+ "github-repo",
32
+ "repo-hardening",
33
+ "repository-security",
34
+ "branch-protection",
35
+ "codeowners",
36
+ "code-of-conduct",
37
+ "contributing",
38
+ "community-files",
39
+ "open-source",
40
+ "oss",
41
+ "best-practices",
42
+ "security",
43
+ "dependabot",
44
+ "codeql",
45
+ "secret-scanning",
46
+ "templates",
47
+ "scaffolding",
48
+ "automation",
49
+ "devops",
50
+ "cli",
51
+ "github-api"
52
+ ],
53
+ "files": [
54
+ "README.md",
55
+ "install.js",
56
+ "run.js",
57
+ "bin/"
58
+ ],
59
+ "binaryChecksums": {
60
+ "fundamentum_0.1.0_darwin_amd64.tar.gz": "9d7be6fbfee6414005932547e1cc2cef54580739a5a265573942c80bb5fd28d9",
61
+ "fundamentum_0.1.0_darwin_arm64.tar.gz": "e50920a5522c9739899e30aa8f9414ac8a66bfcea4b91ef98e2c38f407d54b75",
62
+ "fundamentum_0.1.0_linux_amd64.tar.gz": "1772182f6c1d6a13b0e23bafb01ce89a8b9316895b778ce7a38ce2a9409fff16",
63
+ "fundamentum_0.1.0_linux_arm64.tar.gz": "5506f3002cdd4fad7ee641dfb023672c80d0e8934602906e9c8bb35a643da4ab",
64
+ "fundamentum_0.1.0_windows_amd64.zip": "ba5d12e8a0dbcae168adf7f68141910e0536c186f2921adba000a5b486f68853"
65
+ }
66
+ }
package/run.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // nosemgrep: all — npm run shim, not application code
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { spawn, spawnSync } = require("child_process");
8
+
9
+ const binaryName = process.platform === "win32" ? "fundamentum.exe" : "fundamentum";
10
+ const binaryPath = path.join(__dirname, "bin", binaryName);
11
+
12
+ function runBinary() {
13
+ const child = spawn(binaryPath, process.argv.slice(2), {
14
+ stdio: "inherit",
15
+ });
16
+
17
+ // Forward signals so the Go binary shuts down cleanly
18
+ ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
19
+ process.on(sig, () => child.kill(sig));
20
+ });
21
+
22
+ child.on("error", (err) => {
23
+ if (err.code === "ENOENT") {
24
+ console.error(
25
+ `fundamentum: binary not found at ${binaryPath}\n` +
26
+ "Try running: node " +
27
+ path.join(__dirname, "install.js")
28
+ );
29
+ } else {
30
+ console.error(`fundamentum: failed to start: ${err.message}`);
31
+ }
32
+ process.exit(1);
33
+ });
34
+
35
+ child.on("exit", (code, signal) => {
36
+ process.exit(signal ? 1 : code || 0);
37
+ });
38
+ }
39
+
40
+ // If the binary is missing, postinstall was likely blocked by npm's allow-scripts
41
+ // security policy. Attempt a lazy download, but the package dir may be root-owned
42
+ // (e.g. sudo npm install -g). In that case, surface a clear recovery message.
43
+ if (!fs.existsSync(binaryPath)) {
44
+ const installScript = path.join(__dirname, "install.js");
45
+
46
+ // Check if we can write to the package dir before attempting the download.
47
+ let canWrite = false;
48
+ try {
49
+ fs.accessSync(__dirname, fs.constants.W_OK);
50
+ canWrite = true;
51
+ } catch (_) {
52
+ // directory is not writable by current user
53
+ }
54
+
55
+ if (!canWrite) {
56
+ console.error(
57
+ "fundamentum: binary not found and the package directory is not writable.\n" +
58
+ "Run the installer with the same permissions used for npm install:\n" +
59
+ " sudo node " + installScript
60
+ );
61
+ process.exit(1);
62
+ }
63
+
64
+ console.error(
65
+ "fundamentum: binary not found — postinstall was likely blocked by npm allow-scripts."
66
+ );
67
+ console.error("fundamentum: downloading binary now...");
68
+ const result = spawnSync(process.execPath, [installScript], {
69
+ stdio: "inherit",
70
+ });
71
+ if (result.error || result.status !== 0) {
72
+ console.error(
73
+ "fundamentum: download failed. Manual recovery:\n" +
74
+ " node " + installScript
75
+ );
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ runBinary();