pushci 1.4.2 → 1.4.3

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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/bin/pushci.js CHANGED
@@ -1,9 +1,21 @@
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.
2
+ // pushci npm shim.
3
+ //
4
+ // Resolution order for the Go binary:
5
+ // 0. PUSHCI_BINARY env var explicit user override (offline
6
+ // sandboxes, air-gapped CI, corporate proxies)
7
+ // 1. Local dev build at <pkg>/pushci (for pnpm link and goreleaser
8
+ // snapshot runs during development)
9
+ // 2. Bundled platform binary shipped in the npm tarball at
10
+ // bin/pushci-<os>-<arch>[.exe] — the canonical prod path
11
+ // 3. Existing pushci on PATH (Homebrew, go install, curl installer)
12
+ // 4. Download from GitHub Releases into os.tmpdir()
13
+ // 5. `go build` from source if Go is installed
14
+ // 6. Last resort: print install help with offline guidance
15
+ //
16
+ // VERSION reads from package.json so the shim never drifts from the
17
+ // npm package version. See CLAUDE.md "Release & Distribution" for
18
+ // the full pipeline.
7
19
 
8
20
  const { execSync, spawn } = require('child_process');
9
21
  const fs = require('fs');
@@ -13,23 +25,53 @@ const os = require('os');
13
25
  const pkg = require('../package.json');
14
26
  const VERSION = pkg.version;
15
27
  const REPO = 'finsavvyai/push-ci.dev';
28
+ const GO_MODULE = 'github.com/finsavvyai/push-ci.dev/cmd/pushci';
16
29
 
17
30
  const BINARY_NAME = os.platform() === 'win32' ? 'pushci.exe' : 'pushci';
18
-
19
31
  const PLATFORM_MAP = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
20
32
  const ARCH_MAP = { x64: 'amd64', arm64: 'arm64' };
21
33
 
22
- function getBinaryPath() {
23
- // 1. Local dev build at the package root (set by `go build` during
24
- // development or by goreleaser-like bundling).
34
+ // Early short-circuit: `pushci version` / `--version` / `-v` should
35
+ // never trigger a 60s binary download. If we have a resolvable
36
+ // binary we delegate to it for the full version string (which
37
+ // includes the Go build ldflags); otherwise we print just the npm
38
+ // shim's VERSION and exit. Fixes the sandbox UX where asking for
39
+ // the version used to hang installing the binary.
40
+ function handleVersionShortCircuit() {
41
+ const first = process.argv[2];
42
+ if (first !== 'version' && first !== '--version' && first !== '-v') {
43
+ return false;
44
+ }
45
+ const binary = tryResolveBinary();
46
+ if (binary) {
47
+ const child = spawn(binary, [first], { stdio: 'inherit' });
48
+ child.on('error', () => printShimVersion());
49
+ child.on('exit', (code) => process.exit(code || 0));
50
+ return true;
51
+ }
52
+ printShimVersion();
53
+ return true;
54
+ }
55
+
56
+ function printShimVersion() {
57
+ console.log(`pushci ${VERSION} (npm shim — no binary installed)`);
58
+ console.log('Install the native binary with:');
59
+ console.log(` brew install finsavvyai/tap/pushci # macOS / Linux`);
60
+ console.log(` go install ${GO_MODULE}@latest # anywhere with Go`);
61
+ process.exit(0);
62
+ }
63
+
64
+ // tryResolveBinary is the non-download portion of getBinaryPath —
65
+ // used by the version short-circuit so we never kick off a network
66
+ // fetch just to print a version string.
67
+ function tryResolveBinary() {
68
+ if (process.env.PUSHCI_BINARY) {
69
+ const override = process.env.PUSHCI_BINARY;
70
+ if (fs.existsSync(override) && isValidBinary(override)) return override;
71
+ }
25
72
  const local = path.join(__dirname, '..', BINARY_NAME);
26
73
  if (fs.existsSync(local) && isValidBinary(local)) return local;
27
74
 
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
75
  const plat = PLATFORM_MAP[os.platform()];
34
76
  const arch = ARCH_MAP[os.arch()];
35
77
  if (plat && arch) {
@@ -38,16 +80,18 @@ function getBinaryPath() {
38
80
  if (fs.existsSync(bundled) && isValidBinary(bundled)) return bundled;
39
81
  }
40
82
 
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.
44
83
  try {
45
84
  const cmd = os.platform() === 'win32' ? 'where' : 'which';
46
85
  const found = execSync(`${cmd} pushci`, { encoding: 'utf8' }).trim();
47
86
  if (found && found !== __filename && isValidBinary(found)) return found;
48
87
  } catch (_) {}
49
88
 
50
- // 4-6. Download, build, or give up.
89
+ return null;
90
+ }
91
+
92
+ function getBinaryPath() {
93
+ const resolved = tryResolveBinary();
94
+ if (resolved) return resolved;
51
95
  return downloadOrBuild();
52
96
  }
53
97
 
@@ -58,62 +102,50 @@ function downloadOrBuild() {
58
102
  const target = path.join(os.tmpdir(), `pushci-${VERSION}${ext}`);
59
103
 
60
104
  if (fs.existsSync(target) && isValidBinary(target)) return target;
61
-
62
- if (plat && arch) {
63
- if (downloadFromReleases(plat, arch, target)) {
64
- return target;
65
- }
66
- }
67
-
68
- if (buildFromSource(target)) {
69
- return target;
70
- }
71
-
72
- // If invoked from a git hook, don't block the push.
105
+ if (plat && arch && downloadFromReleases(plat, arch, target)) return target;
106
+ if (buildFromSource(target)) return target;
107
+
108
+ // If invoked from a git hook, warn visibly then let the push
109
+ // through. Earlier versions exited 0 with a one-liner that was
110
+ // easy to miss — users got the false impression that the
111
+ // pre-push hook had actually run their checks. Now we print a
112
+ // loud multi-line warning so the miss is obvious.
73
113
  if (process.env.GIT_DIR) {
74
- console.error('pushci: binary unavailable, skipping checks.');
114
+ console.error('');
115
+ console.error(' ⚠ pushci: BINARY UNAVAILABLE — pre-push checks SKIPPED');
116
+ console.error(' ⚠ This push is going through without running any CI.');
117
+ console.error(' ⚠ Fix: re-run `npm i -g pushci` OR set PUSHCI_BINARY=<path>');
118
+ console.error(' ⚠ Silence: export PUSHCI_SKIP_HOOK=1');
119
+ console.error('');
75
120
  process.exit(0);
76
121
  }
77
122
  printInstallHelp();
78
123
  process.exit(1);
79
124
  }
80
125
 
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.
126
+ // downloadFromReleases pulls the goreleaser archive from GitHub
127
+ // Releases, extracts the binary, and moves it into place. Returns
128
+ // true on success, false on any failure so the caller can fall
129
+ // through to buildFromSource.
85
130
  function downloadFromReleases(plat, arch, target) {
86
131
  const isWin = os.platform() === 'win32';
87
132
  const archiveExt = isWin ? 'zip' : 'tar.gz';
88
133
  const archiveName = `pushci_${VERSION}_${plat}_${arch}.${archiveExt}`;
89
134
  const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
90
-
91
135
  const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pushci-install-'));
92
136
  const archivePath = path.join(scratchDir, archiveName);
93
137
 
94
138
  console.log(`Downloading pushci v${VERSION} from GitHub Releases...`);
95
139
  try {
96
- execSync(
97
- `curl -sfL --retry 2 --retry-delay 1 -o "${archivePath}" "${url}"`,
98
- { timeout: 60000 },
99
- );
140
+ execSync(`curl -sfL --retry 2 --retry-delay 1 -o "${archivePath}" "${url}"`,
141
+ { timeout: 60000 });
100
142
  } catch (_) {
101
143
  fs.rmSync(scratchDir, { recursive: true, force: true });
102
144
  return false;
103
145
  }
104
-
105
146
  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
- }
147
+ execSync(`tar -${isWin ? 'x' : 'xz'}f "${archivePath}" -C "${scratchDir}"`,
148
+ { timeout: 30000 });
117
149
  const extracted = path.join(scratchDir, BINARY_NAME);
118
150
  if (!fs.existsSync(extracted) || !isValidBinary(extracted)) {
119
151
  fs.rmSync(scratchDir, { recursive: true, force: true });
@@ -129,8 +161,9 @@ function downloadFromReleases(plat, arch, target) {
129
161
  }
130
162
  }
131
163
 
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.
164
+ // buildFromSource is the Go-install fallback. Only runs when
165
+ // download fails AND the user has Go on PATH. Useful for
166
+ // air-gapped dev setups.
134
167
  function buildFromSource(target) {
135
168
  try {
136
169
  execSync('go version', { stdio: 'ignore' });
@@ -151,8 +184,7 @@ function buildFromSource(target) {
151
184
  }
152
185
 
153
186
  // 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).
187
+ // archive doesn't pass for a real binary.
156
188
  function isValidBinary(filepath) {
157
189
  try {
158
190
  const stat = fs.statSync(filepath);
@@ -162,8 +194,8 @@ function isValidBinary(filepath) {
162
194
  fs.readSync(fd, buf, 0, 4, 0);
163
195
  fs.closeSync(fd);
164
196
  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)
197
+ if (buf[0] === 0xcf && buf[1] === 0xfa) return true; // Mach-O 64-bit LE
198
+ if (buf[0] === 0xfe && buf[1] === 0xed) return true; // Mach-O 32-bit BE
167
199
  if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE (MZ)
168
200
  return false;
169
201
  } catch (_) {
@@ -173,27 +205,36 @@ function isValidBinary(filepath) {
173
205
 
174
206
  function printInstallHelp() {
175
207
  console.error('');
176
- console.error('pushci: could not download or build the binary.');
208
+ console.error('pushci: could not locate a working binary.');
177
209
  console.error('');
178
- console.error('Install pushci directly:');
210
+ console.error('Online install paths:');
179
211
  console.error(' curl -sSL https://pushci.dev/install | bash');
180
212
  console.error(' brew install finsavvyai/tap/pushci');
181
- console.error(' go install github.com/finsavvyai/pushci/cmd/pushci@latest');
213
+ console.error(` go install ${GO_MODULE}@latest`);
214
+ console.error('');
215
+ console.error('Offline / sandbox / air-gapped path:');
216
+ console.error(' 1. Download the tarball for your platform from:');
217
+ console.error(` https://github.com/${REPO}/releases/tag/v${VERSION}`);
218
+ console.error(' 2. Extract it: tar -xzf pushci_<version>_<os>_<arch>.tar.gz');
219
+ console.error(' 3. Point the shim at the extracted binary:');
220
+ console.error(' export PUSHCI_BINARY=/path/to/pushci');
182
221
  console.error('');
183
222
  console.error("Don't have Node installed?");
184
223
  console.error(' macOS: brew install node');
185
- console.error(' Linux: see https://nodejs.org/en/download/package-manager');
224
+ console.error(' Linux: https://nodejs.org/en/download/package-manager');
186
225
  console.error(' Windows: https://nodejs.org');
187
- console.error('');
188
- console.error('Offline? Install Go and the shim will build from source:');
189
- console.error(' https://go.dev/dl');
190
226
  }
191
227
 
192
- const binary = getBinaryPath();
193
- const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' });
194
- child.on('error', (err) => {
195
- console.error(`pushci: failed to start — ${err.message}`);
196
- printInstallHelp();
197
- process.exit(1);
198
- });
199
- child.on('exit', (code) => process.exit(code || 0));
228
+ function main() {
229
+ if (handleVersionShortCircuit()) return;
230
+ const binary = getBinaryPath();
231
+ const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' });
232
+ child.on('error', (err) => {
233
+ console.error(`pushci: failed to start — ${err.message}`);
234
+ printInstallHelp();
235
+ process.exit(1);
236
+ });
237
+ child.on('exit', (code) => process.exit(code || 0));
238
+ }
239
+
240
+ main();
package/package.json CHANGED
@@ -1,16 +1,23 @@
1
1
  {
2
2
  "name": "pushci",
3
- "version": "1.4.2",
4
- "description": "AI-native CI/CD that runs on your machine. Zero config, zero cost. 33 languages, 69 skills, Tailscale mesh, blast radius analysis.",
3
+ "version": "1.4.3",
4
+ "description": "AI-native CI/CD that runs on your machine. Zero config, zero cost. Works inside AI agent sandboxes (Claude, Cursor, Windsurf). 33 languages, 69 skills, Tailscale mesh, blast radius analysis.",
5
5
  "bin": {
6
6
  "pushci": "bin/pushci.js"
7
7
  },
8
8
  "files": [
9
9
  "bin/pushci.js",
10
+ "bin/pushci-darwin-amd64",
11
+ "bin/pushci-darwin-arm64",
12
+ "bin/pushci-linux-amd64",
13
+ "bin/pushci-linux-arm64",
14
+ "bin/pushci-windows-amd64.exe",
15
+ "bin/pushci-windows-arm64.exe",
10
16
  "README.md",
11
17
  "LICENSE"
12
18
  ],
13
19
  "scripts": {
20
+ "prepack": "bash scripts/build-bundled-binaries.sh",
14
21
  "prepublishOnly": "echo 'Publishing pushci v'${npm_package_version}",
15
22
  "postpublish": "bash scripts/submit-registries.sh"
16
23
  },