tdd-ai 0.4.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 (3) hide show
  1. package/install.js +224 -0
  2. package/package.json +25 -0
  3. package/run.js +50 -0
package/install.js ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview Postinstall script for the tdd-ai npm package.
5
+ *
6
+ * Runs automatically after `npm install`. Downloads the pre-built tdd-ai
7
+ * Go binary from GitHub Releases for the user's platform and architecture.
8
+ *
9
+ * Uses zero npm dependencies — only Node.js built-ins (https, zlib, fs, path).
10
+ *
11
+ * Supported platforms: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64, win32-arm64
12
+ */
13
+
14
+ "use strict";
15
+
16
+ const { execFileSync } = require("child_process");
17
+ const fs = require("fs");
18
+ const https = require("https");
19
+ const path = require("path");
20
+ const zlib = require("zlib");
21
+
22
+ /** @const {string} GitHub owner/repo for release downloads */
23
+ const REPO = "mauricioTechDev/tdd-ai";
24
+
25
+ /** @const {string} Name of the Go binary to download */
26
+ const BINARY_NAME = "tdd-ai";
27
+
28
+ /** @const {Object<string, string>} Maps Node.js process.platform to GoReleaser OS names */
29
+ const PLATFORM_MAP = {
30
+ darwin: "darwin",
31
+ linux: "linux",
32
+ win32: "windows",
33
+ };
34
+
35
+ /** @const {Object<string, string>} Maps Node.js process.arch to GoReleaser architecture names */
36
+ const ARCH_MAP = {
37
+ x64: "amd64",
38
+ arm64: "arm64",
39
+ };
40
+
41
+ /**
42
+ * Detects the current platform and architecture, mapped to GoReleaser naming.
43
+ * @returns {{ platform: string, arch: string }} GoReleaser-compatible OS and arch names
44
+ * @throws {Error} If the current platform/arch combination is not supported
45
+ */
46
+ function getPlatformInfo() {
47
+ const platform = PLATFORM_MAP[process.platform];
48
+ const arch = ARCH_MAP[process.arch];
49
+
50
+ if (!platform || !arch) {
51
+ throw new Error(
52
+ `Unsupported platform: ${process.platform}-${process.arch}. ` +
53
+ `Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64, win32-arm64`
54
+ );
55
+ }
56
+
57
+ return { platform, arch };
58
+ }
59
+
60
+ /**
61
+ * Builds the GitHub Releases download URL for a specific version and platform.
62
+ * @param {string} version - Semver version without "v" prefix (e.g. "0.4.0")
63
+ * @param {string} platform - GoReleaser OS name (darwin, linux, windows)
64
+ * @param {string} arch - GoReleaser arch name (amd64, arm64)
65
+ * @returns {string} Full download URL for the release archive
66
+ */
67
+ function getDownloadUrl(version, platform, arch) {
68
+ const ext = platform === "windows" ? "zip" : "tar.gz";
69
+ return `https://github.com/${REPO}/releases/download/v${version}/${BINARY_NAME}_${version}_${platform}_${arch}.${ext}`;
70
+ }
71
+
72
+ /**
73
+ * Downloads a file from the given URL, following up to 5 redirects.
74
+ * GitHub Releases URLs redirect to a CDN (objects.githubusercontent.com),
75
+ * so redirect following is required.
76
+ * @param {string} url - The URL to download
77
+ * @returns {Promise<Buffer>} The downloaded file contents as a Buffer
78
+ * @throws {Error} If the download fails or too many redirects occur
79
+ */
80
+ function downloadFile(url) {
81
+ return new Promise((resolve, reject) => {
82
+ const follow = (url, redirects) => {
83
+ if (redirects > 5) return reject(new Error("Too many redirects"));
84
+
85
+ https
86
+ .get(url, (res) => {
87
+ if (
88
+ res.statusCode >= 300 &&
89
+ res.statusCode < 400 &&
90
+ res.headers.location
91
+ ) {
92
+ return follow(res.headers.location, redirects + 1);
93
+ }
94
+ if (res.statusCode !== 200) {
95
+ return reject(
96
+ new Error(`Download failed: HTTP ${res.statusCode} from ${url}`)
97
+ );
98
+ }
99
+
100
+ const chunks = [];
101
+ res.on("data", (chunk) => chunks.push(chunk));
102
+ res.on("end", () => resolve(Buffer.concat(chunks)));
103
+ res.on("error", reject);
104
+ })
105
+ .on("error", reject);
106
+ };
107
+
108
+ follow(url, 0);
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Extracts a binary from a gzipped tar archive (.tar.gz).
114
+ * Uses a minimal tar parser that reads 512-byte headers to locate the binary,
115
+ * avoiding the need for an external `tar` dependency.
116
+ * @param {Buffer} buffer - The gzipped tar archive contents
117
+ * @param {string} destDir - Directory to write the extracted binary to
118
+ * @param {string} binaryName - Name of the binary file to find in the archive
119
+ * @returns {string} Absolute path to the extracted binary
120
+ * @throws {Error} If the binary is not found in the archive
121
+ */
122
+ function extractTarGz(buffer, destDir, binaryName) {
123
+ const tar = zlib.gunzipSync(buffer);
124
+
125
+ let offset = 0;
126
+ while (offset < tar.length) {
127
+ const header = tar.slice(offset, offset + 512);
128
+ if (header.every((b) => b === 0)) break;
129
+
130
+ const name = header.toString("utf8", 0, 100).replace(/\0/g, "");
131
+ const sizeStr = header
132
+ .toString("utf8", 124, 136)
133
+ .replace(/\0/g, "")
134
+ .trim();
135
+ const size = parseInt(sizeStr, 8) || 0;
136
+
137
+ offset += 512;
138
+
139
+ if (name === binaryName || name.endsWith("/" + binaryName)) {
140
+ const data = tar.slice(offset, offset + size);
141
+ const destPath = path.join(destDir, binaryName);
142
+ fs.writeFileSync(destPath, data);
143
+ fs.chmodSync(destPath, 0o755);
144
+ return destPath;
145
+ }
146
+
147
+ offset += Math.ceil(size / 512) * 512;
148
+ }
149
+
150
+ throw new Error(`Binary '${binaryName}' not found in archive`);
151
+ }
152
+
153
+ /**
154
+ * Extracts a binary from a zip archive (used for Windows releases).
155
+ * Uses PowerShell on Windows or the `unzip` command on other platforms.
156
+ * @param {Buffer} buffer - The zip archive contents
157
+ * @param {string} destDir - Directory to extract into
158
+ * @param {string} binaryName - Name of the binary (without .exe extension)
159
+ * @returns {string} Absolute path to the extracted .exe binary
160
+ * @throws {Error} If the binary is not found after extraction
161
+ */
162
+ function extractZip(buffer, destDir, binaryName) {
163
+ const tmpZip = path.join(destDir, "tmp.zip");
164
+ fs.writeFileSync(tmpZip, buffer);
165
+
166
+ try {
167
+ if (process.platform === "win32") {
168
+ execFileSync("powershell", [
169
+ "-NoProfile",
170
+ "-Command",
171
+ `Expand-Archive -Path '${tmpZip}' -DestinationPath '${destDir}' -Force`,
172
+ ]);
173
+ } else {
174
+ execFileSync("unzip", ["-o", tmpZip, "-d", destDir]);
175
+ }
176
+ } finally {
177
+ fs.unlinkSync(tmpZip);
178
+ }
179
+
180
+ const destPath = path.join(destDir, binaryName + ".exe");
181
+ if (!fs.existsSync(destPath)) {
182
+ throw new Error(`Binary '${binaryName}.exe' not found after extraction`);
183
+ }
184
+ return destPath;
185
+ }
186
+
187
+ /**
188
+ * Entry point. Reads the version from package.json, detects the platform,
189
+ * downloads the matching binary from GitHub Releases, and extracts it
190
+ * to ./bin/. Skips download if the binary already exists.
191
+ */
192
+ async function main() {
193
+ const pkg = require("./package.json");
194
+ const version = pkg.version;
195
+ const { platform, arch } = getPlatformInfo();
196
+ const binDir = path.join(__dirname, "bin");
197
+
198
+ const ext = platform === "windows" ? ".exe" : "";
199
+ const binaryPath = path.join(binDir, BINARY_NAME + ext);
200
+ if (fs.existsSync(binaryPath)) {
201
+ console.log(`tdd-ai binary already exists at ${binaryPath}`);
202
+ return;
203
+ }
204
+
205
+ const url = getDownloadUrl(version, platform, arch);
206
+ console.log(`Downloading tdd-ai v${version} for ${platform}-${arch}...`);
207
+
208
+ fs.mkdirSync(binDir, { recursive: true });
209
+
210
+ const buffer = await downloadFile(url);
211
+
212
+ if (platform === "windows") {
213
+ extractZip(buffer, binDir, BINARY_NAME);
214
+ } else {
215
+ extractTarGz(buffer, binDir, BINARY_NAME);
216
+ }
217
+
218
+ console.log(`tdd-ai v${version} installed successfully`);
219
+ }
220
+
221
+ main().catch((err) => {
222
+ console.error(`Failed to install tdd-ai: ${err.message}`);
223
+ process.exit(1);
224
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "tdd-ai",
3
+ "version": "0.4.0",
4
+ "description": "TDD guardrails for AI coding agents",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mauricioTechDev/tdd-ai.git"
9
+ },
10
+ "homepage": "https://github.com/mauricioTechDev/tdd-ai",
11
+ "bin": {
12
+ "tdd-ai": "run.js"
13
+ },
14
+ "scripts": {
15
+ "postinstall": "node install.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=16"
19
+ },
20
+ "files": [
21
+ "install.js",
22
+ "run.js",
23
+ "README.md"
24
+ ]
25
+ }
package/run.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview CLI wrapper for the tdd-ai Go binary.
5
+ *
6
+ * This is the entry point registered in package.json's "bin" field.
7
+ * npm symlinks this script so that `tdd-ai` is available as a command.
8
+ *
9
+ * It spawns the Go binary with inherited stdio so that terminal detection
10
+ * (used by tdd-ai for auto JSON/text format switching) works correctly.
11
+ * Exit codes from the Go binary are propagated to the caller.
12
+ */
13
+
14
+ "use strict";
15
+
16
+ const { execFileSync } = require("child_process");
17
+ const path = require("path");
18
+ const fs = require("fs");
19
+
20
+ const BINARY_NAME = "tdd-ai";
21
+
22
+ /**
23
+ * Resolves the path to the downloaded Go binary.
24
+ * @returns {string} Absolute path to the tdd-ai binary
25
+ * @throws {Error} If the binary does not exist (postinstall may not have run)
26
+ */
27
+ function getBinaryPath() {
28
+ const ext = process.platform === "win32" ? ".exe" : "";
29
+ const binPath = path.join(__dirname, "bin", BINARY_NAME + ext);
30
+
31
+ if (!fs.existsSync(binPath)) {
32
+ throw new Error(
33
+ `tdd-ai binary not found at ${binPath}. ` +
34
+ `Try reinstalling: npm install -g tdd-ai`
35
+ );
36
+ }
37
+
38
+ return binPath;
39
+ }
40
+
41
+ try {
42
+ execFileSync(getBinaryPath(), process.argv.slice(2), {
43
+ stdio: "inherit",
44
+ });
45
+ } catch (err) {
46
+ if (err.status !== undefined) {
47
+ process.exit(err.status);
48
+ }
49
+ throw err;
50
+ }