pushci 1.3.1 → 1.4.1

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 (2) hide show
  1. package/bin/pushci.js +126 -40
  2. package/package.json +1 -1
package/bin/pushci.js CHANGED
@@ -1,30 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ // pushci npm shim. Resolution order: local dev build → bundled
3
+ // bin/pushci-<os>-<arch> → pushci on PATH → download from GitHub
4
+ // Releases → go build → install help. VERSION reads from package.json
5
+ // so the shim and npm package never drift. See CLAUDE.md
6
+ // "Release & Distribution" for the full pipeline.
7
+
2
8
  const { execSync, spawn } = require('child_process');
3
9
  const fs = require('fs');
4
10
  const path = require('path');
5
11
  const os = require('os');
6
12
 
7
- const VERSION = '1.1.0';
13
+ const pkg = require('../package.json');
14
+ const VERSION = pkg.version;
15
+ const REPO = 'finsavvyai/push-ci.dev';
16
+
8
17
  const BINARY_NAME = os.platform() === 'win32' ? 'pushci.exe' : 'pushci';
9
- const CDN = 'https://pushci-releases.broad-dew-49ad.workers.dev';
10
18
 
11
- const PLATFORM_MAP = {
12
- darwin: 'darwin', linux: 'linux', win32: 'windows',
13
- };
14
- const ARCH_MAP = {
15
- x64: 'amd64', arm64: 'arm64',
16
- };
19
+ const PLATFORM_MAP = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
20
+ const ARCH_MAP = { x64: 'amd64', arm64: 'arm64' };
17
21
 
18
22
  function getBinaryPath() {
23
+ // 1. Local dev build at the package root (set by `go build` during
24
+ // development or by goreleaser-like bundling).
19
25
  const local = path.join(__dirname, '..', BINARY_NAME);
20
- if (fs.existsSync(local)) return local;
26
+ if (fs.existsSync(local) && isValidBinary(local)) return local;
27
+
28
+ // 2. Bundled platform-specific binary shipped inside the npm
29
+ // tarball at bin/pushci-<os>-<arch>[.exe]. This is the
30
+ // fastest path — zero network, works offline, deterministic.
31
+ // It's also the only reliable path for users who don't have
32
+ // GitHub Releases access (corporate proxies, air-gapped CI).
33
+ const plat = PLATFORM_MAP[os.platform()];
34
+ const arch = ARCH_MAP[os.arch()];
35
+ if (plat && arch) {
36
+ const winExt = os.platform() === 'win32' ? '.exe' : '';
37
+ const bundled = path.join(__dirname, `pushci-${plat}-${arch}${winExt}`);
38
+ if (fs.existsSync(bundled) && isValidBinary(bundled)) return bundled;
39
+ }
21
40
 
41
+ // 3. Existing install on PATH (Homebrew, go install, curl
42
+ // installer). The `which` result may be this same shim, so
43
+ // validate it's a real binary before trusting it.
22
44
  try {
23
45
  const cmd = os.platform() === 'win32' ? 'where' : 'which';
24
46
  const found = execSync(`${cmd} pushci`, { encoding: 'utf8' }).trim();
25
- if (found) return found;
47
+ if (found && found !== __filename && isValidBinary(found)) return found;
26
48
  } catch (_) {}
27
49
 
50
+ // 4-6. Download, build, or give up.
28
51
  return downloadOrBuild();
29
52
  }
30
53
 
@@ -37,31 +60,16 @@ function downloadOrBuild() {
37
60
  if (fs.existsSync(target) && isValidBinary(target)) return target;
38
61
 
39
62
  if (plat && arch) {
40
- const url = `${CDN}/v${VERSION}/pushci-${plat}-${arch}${ext}`;
41
- console.log(`Downloading pushci v${VERSION}...`);
42
- try {
43
- execSync(`curl -sfL --retry 2 -o "${target}" "${url}"`, { timeout: 30000 });
44
- if (isValidBinary(target)) {
45
- fs.chmodSync(target, 0o755);
46
- return target;
47
- }
48
- try { fs.unlinkSync(target); } catch (_) {}
49
- } catch (_) {}
63
+ if (downloadFromReleases(plat, arch, target)) {
64
+ return target;
65
+ }
50
66
  }
51
67
 
52
- try {
53
- execSync('go version', { stdio: 'ignore' });
54
- console.log('Building pushci from source...');
55
- const out = path.join(os.tmpdir(), BINARY_NAME);
56
- execSync(`go build -o "${out}" ./cmd/pushci`, {
57
- cwd: path.join(__dirname, '..'),
58
- stdio: 'inherit',
59
- timeout: 120000,
60
- });
61
- if (fs.existsSync(out)) return out;
62
- } catch (_) {}
68
+ if (buildFromSource(target)) {
69
+ return target;
70
+ }
63
71
 
64
- // If invoked from a git hook, don't block the push
72
+ // If invoked from a git hook, don't block the push.
65
73
  if (process.env.GIT_DIR) {
66
74
  console.error('pushci: binary unavailable, skipping checks.');
67
75
  process.exit(0);
@@ -70,6 +78,81 @@ function downloadOrBuild() {
70
78
  process.exit(1);
71
79
  }
72
80
 
81
+ // downloadFromReleases pulls the goreleaser archive for the current
82
+ // platform from GitHub Releases, extracts the binary, and moves it
83
+ // into place. Returns true on success, false on any failure so the
84
+ // caller can fall through to buildFromSource.
85
+ function downloadFromReleases(plat, arch, target) {
86
+ const isWin = os.platform() === 'win32';
87
+ const archiveExt = isWin ? 'zip' : 'tar.gz';
88
+ const archiveName = `pushci_${VERSION}_${plat}_${arch}.${archiveExt}`;
89
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
90
+
91
+ const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pushci-install-'));
92
+ const archivePath = path.join(scratchDir, archiveName);
93
+
94
+ console.log(`Downloading pushci v${VERSION} from GitHub Releases...`);
95
+ try {
96
+ execSync(
97
+ `curl -sfL --retry 2 --retry-delay 1 -o "${archivePath}" "${url}"`,
98
+ { timeout: 60000 },
99
+ );
100
+ } catch (_) {
101
+ fs.rmSync(scratchDir, { recursive: true, force: true });
102
+ return false;
103
+ }
104
+
105
+ try {
106
+ if (isWin) {
107
+ // tar has shipped with Windows 10 (1803+) — `tar -xf` handles
108
+ // .zip the same as .tar.gz so we avoid a PowerShell branch.
109
+ execSync(`tar -xf "${archivePath}" -C "${scratchDir}"`, {
110
+ timeout: 30000,
111
+ });
112
+ } else {
113
+ execSync(`tar -xzf "${archivePath}" -C "${scratchDir}"`, {
114
+ timeout: 30000,
115
+ });
116
+ }
117
+ const extracted = path.join(scratchDir, BINARY_NAME);
118
+ if (!fs.existsSync(extracted) || !isValidBinary(extracted)) {
119
+ fs.rmSync(scratchDir, { recursive: true, force: true });
120
+ return false;
121
+ }
122
+ fs.copyFileSync(extracted, target);
123
+ if (!isWin) fs.chmodSync(target, 0o755);
124
+ fs.rmSync(scratchDir, { recursive: true, force: true });
125
+ return true;
126
+ } catch (_) {
127
+ fs.rmSync(scratchDir, { recursive: true, force: true });
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // buildFromSource is the Go-install fallback. Only runs when download
133
+ // fails AND the user has Go on PATH. Useful for air-gapped dev setups.
134
+ function buildFromSource(target) {
135
+ try {
136
+ execSync('go version', { stdio: 'ignore' });
137
+ } catch (_) {
138
+ return false;
139
+ }
140
+ console.log('Downloading failed, building pushci from source...');
141
+ try {
142
+ execSync(`go build -o "${target}" ./cmd/pushci`, {
143
+ cwd: path.join(__dirname, '..'),
144
+ stdio: 'inherit',
145
+ timeout: 180000,
146
+ });
147
+ return fs.existsSync(target) && isValidBinary(target);
148
+ } catch (_) {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ // isValidBinary sanity-checks the magic bytes so a half-downloaded
154
+ // archive doesn't pass for a real binary. ELF (Linux), Mach-O
155
+ // (darwin little- and big-endian), PE (Windows).
73
156
  function isValidBinary(filepath) {
74
157
  try {
75
158
  const stat = fs.statSync(filepath);
@@ -78,12 +161,14 @@ function isValidBinary(filepath) {
78
161
  const fd = fs.openSync(filepath, 'r');
79
162
  fs.readSync(fd, buf, 0, 4, 0);
80
163
  fs.closeSync(fd);
81
- if (buf[0] === 0x7f && buf[1] === 0x45) return true;
82
- if (buf[0] === 0xcf && buf[1] === 0xfa) return true;
83
- if (buf[0] === 0xfe && buf[1] === 0xed) return true;
84
- if (buf[0] === 0x4d && buf[1] === 0x5a) return true;
164
+ if (buf[0] === 0x7f && buf[1] === 0x45) return true; // ELF
165
+ if (buf[0] === 0xcf && buf[1] === 0xfa) return true; // Mach-O (64-bit LE)
166
+ if (buf[0] === 0xfe && buf[1] === 0xed) return true; // Mach-O (32-bit BE)
167
+ if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE (MZ)
85
168
  return false;
86
- } catch (_) { return false; }
169
+ } catch (_) {
170
+ return false;
171
+ }
87
172
  }
88
173
 
89
174
  function printInstallHelp() {
@@ -95,12 +180,13 @@ function printInstallHelp() {
95
180
  console.error(' brew install finsavvyai/tap/pushci');
96
181
  console.error(' go install github.com/finsavvyai/pushci/cmd/pushci@latest');
97
182
  console.error('');
98
- console.error("Don't have npm/npx? Install Node.js first:");
183
+ console.error("Don't have Node installed?");
99
184
  console.error(' macOS: brew install node');
100
- console.error(' Linux: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs');
185
+ console.error(' Linux: see https://nodejs.org/en/download/package-manager');
101
186
  console.error(' Windows: https://nodejs.org');
102
187
  console.error('');
103
- console.error('Or install Go: https://go.dev/dl');
188
+ console.error('Offline? Install Go and the shim will build from source:');
189
+ console.error(' https://go.dev/dl');
104
190
  }
105
191
 
106
192
  const binary = getBinaryPath();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushci",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "AI-native CI/CD that runs on your machine. Zero config, zero cost. 33 languages, 69 skills, Tailscale mesh, blast radius analysis.",
5
5
  "bin": {
6
6
  "pushci": "bin/pushci.js"