primo-cli 0.1.12 → 0.1.14

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.
@@ -9,7 +9,7 @@ import ora from 'ora';
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const PRIMO_HOME = path.join(os.homedir(), '.primo');
11
11
  const BIN_DIR = path.join(PRIMO_HOME, 'bin');
12
- const VERSION = '3.2.0'; // matches primo releases
12
+ const VERSION = '3.2.2'; // matches primo releases
13
13
  // Path to locally built binary (for development)
14
14
  // The binary is at primo/primo (inside the primo repo directory)
15
15
  const LOCAL_BINARY = path.resolve(__dirname, '..', '..', '..', 'primo', 'primo');
@@ -50,6 +50,16 @@ function get_download_url(platform) {
50
50
  const filename = `primo_${platform.os}_${platform.arch}${platform.ext}`;
51
51
  return `${base}/v${VERSION}/${filename}`;
52
52
  }
53
+ // Extract a bare semver (e.g. "3.2.1") from a binary's --version output.
54
+ // The server prints a build banner first, then "<name> version vX.Y.Z", so we
55
+ // scan for the semver rather than trusting the whole string. Returns null when
56
+ // no semver is present (e.g. a "dev" build), which callers treat as a mismatch.
57
+ function parse_semver(output) {
58
+ if (!output)
59
+ return null;
60
+ const match = output.match(/\bv?(\d+\.\d+\.\d+)\b/);
61
+ return match ? match[1] : null;
62
+ }
53
63
  export async function get_binary_path() {
54
64
  // Explicit override wins — layout-independent, the way to point at a
55
65
  // local server build regardless of where this CLI lives on disk.
@@ -95,29 +105,50 @@ export async function ensure_binary() {
95
105
  if (process.env.PRIMO_BINARY) {
96
106
  return await get_binary_path();
97
107
  }
108
+ // A sibling dev build (running from source next to a `primo` checkout) is
109
+ // developer-chosen — never replace it with a download, even if its version
110
+ // differs from the pinned release.
111
+ try {
112
+ await fs.access(LOCAL_BINARY, fs.constants.X_OK);
113
+ return LOCAL_BINARY;
114
+ }
115
+ catch { }
116
+ // A managed binary already on disk is reused only when it matches the pinned
117
+ // version. A stale binary (older release, or a pre-rename "palacms" build
118
+ // reporting a different version) is re-downloaded so fixes actually reach
119
+ // users who already have a binary installed.
120
+ let updating_from = null;
98
121
  if (await is_binary_installed()) {
99
- return await get_binary_path();
122
+ if (await is_binary_current()) {
123
+ return await get_binary_path();
124
+ }
125
+ updating_from = await get_binary_version();
100
126
  }
101
127
  // Need to download - get the target path
102
128
  const platform = get_platform();
103
129
  const binary_path = path.join(BIN_DIR, `primo${platform.ext}`);
104
- const spinner = ora('Setting up Primo...').start();
130
+ const spinner = ora(updating_from
131
+ ? `Updating primo ${updating_from} → ${VERSION}...`
132
+ : 'Setting up Primo...').start();
105
133
  try {
106
134
  // Create directories
107
135
  await fs.mkdir(BIN_DIR, { recursive: true });
108
136
  const url = get_download_url(platform);
109
137
  spinner.text = `Downloading primo for ${platform.os}/${platform.arch}...`;
110
- // Download binary
138
+ // Download binary. Stream to a temp path and rename into place so an
139
+ // interrupted download can't leave a half-written binary that later
140
+ // looks "installed". rename() is atomic within the same directory.
111
141
  const response = await fetch(url);
112
142
  if (!response.ok) {
113
143
  throw new Error(`Download failed: ${response.status} ${response.statusText}`);
114
144
  }
115
- // Save to file
116
- const file_stream = createWriteStream(binary_path);
145
+ const tmp_path = `${binary_path}.download`;
146
+ const file_stream = createWriteStream(tmp_path);
117
147
  await pipeline(response.body, file_stream);
118
- // Make executable
119
- await fs.chmod(binary_path, 0o755);
120
- spinner.succeed('Primo setup complete');
148
+ // Make executable, then atomically replace any existing binary.
149
+ await fs.chmod(tmp_path, 0o755);
150
+ await fs.rename(tmp_path, binary_path);
151
+ spinner.succeed(updating_from ? `Primo updated to ${VERSION}` : 'Primo setup complete');
121
152
  return binary_path;
122
153
  }
123
154
  catch (error) {
@@ -132,14 +163,26 @@ export async function ensure_binary() {
132
163
  throw error;
133
164
  }
134
165
  }
166
+ // Return the installed binary's semver (e.g. "3.2.1"), or null if it can't be
167
+ // determined (missing binary, dev build, or unparseable output).
135
168
  export async function get_binary_version() {
136
169
  try {
137
170
  const binary_path = await get_binary_path();
138
- const { execSync } = await import('child_process');
139
- const output = execSync(`"${binary_path}" --version`, { encoding: 'utf-8' });
140
- return output.trim();
171
+ const { execFileSync } = await import('child_process');
172
+ // execFile (not execSync) avoids shell quoting issues with the path, and
173
+ // the timeout prevents a wedged binary from hanging CLI startup.
174
+ const output = execFileSync(binary_path, ['--version'], { encoding: 'utf-8', timeout: 5000 });
175
+ return parse_semver(output);
141
176
  }
142
177
  catch {
143
178
  return null;
144
179
  }
145
180
  }
181
+ // Whether the installed binary matches the version this CLI pins. A dev/unknown
182
+ // version (null) counts as not-current so we refresh to a known-good release.
183
+ // Skipped when PRIMO_BINARY or a sibling dev build is in use — those are
184
+ // developer-chosen and must not be clobbered by a download.
185
+ async function is_binary_current() {
186
+ const installed = await get_binary_version();
187
+ return installed === VERSION;
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primo-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Local development CLI for Primo",
5
5
  "type": "module",
6
6
  "bin": {