impel-cli 0.18.15-beta.6 → 0.18.15-beta.7

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.
package/README.md CHANGED
@@ -223,6 +223,11 @@ impel update --no-recovery # disable recovery for this run
223
223
  schema verified by that CLI. It does not mean cloning an untested moving vendor
224
224
  release.
225
225
 
226
+ Stable installations follow npm's `latest` tag. Prerelease installations made
227
+ from `impel-cli@next` remain on the `next` channel during `impel update`. If npm
228
+ metadata is temporarily unavailable, update preserves the installed CLI build
229
+ and continues tenant reconciliation without attempting a downgrade.
230
+
226
231
  ## Status and diagnosis
227
232
 
228
233
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.15-beta.6",
3
+ "version": "0.18.15-beta.7",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,8 @@ import { promptText } from "../prompt.js";
13
13
  import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
14
14
  import { restoreNativeProfiles } from "./use.js";
15
15
 
16
+ const RETRYABLE_TENANT_DISCOVERY_ERROR = /could not reach|request timed out|fetch failed|ECONNRESET|ECONNABORTED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|network error|socket/iu;
17
+
16
18
  function confirmed(answer) {
17
19
  return /^(?:y|yes)$/iu.test(String(answer || "").trim());
18
20
  }
@@ -44,6 +46,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
44
46
  platform: process.platform,
45
47
  environment: process.env,
46
48
  preparePlatformClis,
49
+ discoveryLogger: console,
47
50
  ...overrides,
48
51
  };
49
52
  if (overrides.prepareWindowsClis && !overrides.preparePlatformClis) {
@@ -58,10 +61,22 @@ export async function cmdConverge(argv = [], overrides = {}) {
58
61
  let listing;
59
62
  try {
60
63
  listing = await io.fetchTenants(config);
64
+ // Tenant discovery is the entry gate for the whole convergence. A single
65
+ // transient timeout should not force a human to rerun every update step.
61
66
  } catch (error) {
62
- console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
63
- process.exitCode = 1;
64
- return false;
67
+ if (!RETRYABLE_TENANT_DISCOVERY_ERROR.test(String(error?.message || error))) {
68
+ console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
69
+ process.exitCode = 1;
70
+ return false;
71
+ }
72
+ io.discoveryLogger.log("Tenants: discovery request failed transiently; retrying once…");
73
+ try {
74
+ listing = await io.fetchTenants(config);
75
+ } catch (retryError) {
76
+ console.error(`impel update: tenant discovery failed (${redactSecretText(retryError?.message || retryError)})`);
77
+ process.exitCode = 1;
78
+ return false;
79
+ }
65
80
  }
66
81
  const selected = selectDefaultTenant(listing, { currentTenantId: config.tenantId });
67
82
  config.tenantId = selected.id;
@@ -18,6 +18,7 @@ import {
18
18
  isNewerVersion,
19
19
  refreshUpdateCache,
20
20
  updateInstallSpec,
21
+ updateTagForVersion,
21
22
  writeUpdateCache,
22
23
  } from "../updates.js";
23
24
 
@@ -200,30 +201,41 @@ export async function cmdUpdate(argv, overrides = {}) {
200
201
  }
201
202
 
202
203
  const current = io.installedVersion();
203
- const remote = await io.progress("Checking npm for impel-cli updates", () => io.fetchRemoteVersion());
204
+ const updateTag = updateTagForVersion(current);
205
+ const installSpec = updateInstallSpec(updateTag);
206
+ const remote = await io.progress(
207
+ "Checking npm for impel-cli updates",
208
+ () => io.fetchRemoteVersion({ tag: updateTag }),
209
+ );
204
210
  if (remote) io.writeCache({ remoteVersion: remote, checkedAt: Date.now() });
205
211
 
206
212
  console.log(`impel-cli v${current ?? "?"}`);
207
- console.log(`npm latest: ${remote ? `v${remote}` : "unknown — registry check failed"}`);
213
+ console.log(`npm ${updateTag}: ${remote ? `v${remote}` : "unknown — registry check failed"}`);
208
214
 
209
215
  const updateAvailable = Boolean(current && remote && isNewerVersion(remote, current));
210
216
  const upToDate = Boolean(current && remote && !updateAvailable);
211
217
  if (flags.check) {
212
- console.log(upToDate ? "Up to date." : "Update available: run `impel update`.");
213
- if (!remote) process.exitCode = 1;
218
+ if (!remote) {
219
+ console.log("Update status unavailable; the installed CLI was not changed.");
220
+ process.exitCode = 1;
221
+ } else {
222
+ console.log(upToDate ? "Up to date." : "Update available: run `impel update`.");
223
+ }
214
224
  return;
215
225
  }
216
226
 
217
227
  // ── CLI ────────────────────────────────────────────────────────────────
218
- if (upToDate) {
228
+ if (!remote) {
229
+ console.warn("CLI: npm update check unavailable; keeping the installed build.");
230
+ } else if (upToDate) {
219
231
  console.log("CLI: already up to date.");
220
232
  } else {
221
- console.log("CLI: installing the latest build…");
222
- if (!await io.progress("Installing the latest impel-cli build", () => io.selfUpdate(updateInstallSpec()))) {
233
+ console.log(`CLI: installing the npm ${updateTag} build…`);
234
+ if (!await io.progress(`Installing the npm ${updateTag} impel-cli build`, () => io.selfUpdate(installSpec))) {
223
235
  console.error("impel update: `npm install -g` failed; the CLI was not updated.");
224
236
  if (io.platform === "win32") {
225
237
  console.error(" Verify `npm --version` in PowerShell, then retry `impel update`.");
226
- console.error(" Manual recovery: `npm install --global impel-cli@latest`.");
238
+ console.error(` Manual recovery: \`npm install --global ${installSpec}\`.`);
227
239
  }
228
240
  const config = io.loadConfig();
229
241
  let recovered = false;
@@ -239,7 +251,7 @@ export async function cmdUpdate(argv, overrides = {}) {
239
251
  platform: io.platform,
240
252
  architecture: process.arch,
241
253
  step: "install.impel_cli",
242
- command: `npm install --global ${RUNTIME_BRAND.cli.packageName}@latest`,
254
+ command: `npm install --global ${installSpec}`,
243
255
  message: `The npm-verified global ${RUNTIME_BRAND.cli.packageName} update failed.`,
244
256
  },
245
257
  config,
@@ -265,7 +277,7 @@ export async function cmdUpdate(argv, overrides = {}) {
265
277
  }
266
278
  },
267
279
  retryStep: async () => {
268
- updateState.ok = io.selfUpdate(updateInstallSpec()) === true;
280
+ updateState.ok = io.selfUpdate(installSpec) === true;
269
281
  return updateState.ok;
270
282
  },
271
283
  },
@@ -294,7 +306,7 @@ export async function cmdUpdate(argv, overrides = {}) {
294
306
  console.error(`impel update: npm reported success but the running CLI still resolves v${postInstall ?? "?"} (expected v${remote}).`);
295
307
  console.error(` Running CLI: ${CLI_BIN}`);
296
308
  console.error(" This usually means the `impel` on PATH belongs to a different npm prefix than `npm prefix -g`.");
297
- console.error(" Fix: run `npm prefix -g`, confirm it owns the `impel` shim on PATH, then `npm install --global impel-cli@latest` there.");
309
+ console.error(` Fix: run \`npm prefix -g\`, confirm it owns the \`impel\` shim on PATH, then \`npm install --global ${installSpec}\` there.`);
298
310
  process.exitCode = 1;
299
311
  return;
300
312
  }
package/src/updates.js CHANGED
@@ -36,8 +36,19 @@ export function updateRegistry() {
36
36
  ).replace(/\/+$/u, "");
37
37
  }
38
38
 
39
- export function updateInstallSpec() {
40
- return `${updatePackage()}@latest`;
39
+ function normalizeUpdateTag(tag) {
40
+ if (tag === "latest" || tag === "next") return tag;
41
+ throw new Error(`unsupported npm update tag "${tag}"`);
42
+ }
43
+
44
+ /** Keep prerelease installs on npm's prerelease channel. */
45
+ export function updateTagForVersion(version) {
46
+ const parsed = validVersion(version) ? version.match(VERSION_RE) : null;
47
+ return parsed?.[4] ? "next" : "latest";
48
+ }
49
+
50
+ export function updateInstallSpec(tag = "latest") {
51
+ return `${updatePackage()}@${normalizeUpdateTag(tag)}`;
41
52
  }
42
53
 
43
54
  export function installedVersion() {
@@ -88,15 +99,17 @@ export function isNewerVersion(candidate, current) {
88
99
  function fetchRemoteVersionWithNpm({
89
100
  packageName,
90
101
  registry,
102
+ tag,
91
103
  timeoutMs,
92
104
  spawnSyncImpl = spawnSync,
93
105
  environment = process.env,
94
106
  platform = process.platform,
95
107
  }) {
96
108
  try {
109
+ const target = tag === "latest" ? packageName : `${packageName}@${tag}`;
97
110
  const invocation = nativeCommandInvocation(
98
111
  "npm",
99
- ["view", packageName, "version", "--json", "--registry", registry],
112
+ ["view", target, "version", "--json", "--registry", registry],
100
113
  environment,
101
114
  platform,
102
115
  );
@@ -122,39 +135,71 @@ function fetchRemoteVersionWithNpm({
122
135
  export async function fetchRemoteVersion({
123
136
  packageName = updatePackage(),
124
137
  registry = updateRegistry(),
138
+ tag = "latest",
125
139
  timeoutMs = 10_000,
126
140
  fetchImpl = null,
141
+ fallbackToNpm = fetchImpl === null,
127
142
  spawnSyncImpl = spawnSync,
128
143
  environment = process.env,
129
144
  platform = process.platform,
130
145
  } = {}) {
146
+ const normalizedTag = normalizeUpdateTag(tag);
131
147
  const normalizedRegistry = String(registry).replace(/\/+$/u, "");
132
148
  if (!fetchImpl && normalizedRegistry !== DEFAULT_UPDATE_REGISTRY) {
133
149
  return fetchRemoteVersionWithNpm({
134
150
  packageName,
135
151
  registry: normalizedRegistry,
152
+ tag: normalizedTag,
153
+ timeoutMs,
154
+ spawnSyncImpl,
155
+ environment,
156
+ platform,
157
+ });
158
+ }
159
+ // Windows npm commonly has proxy, certificate, or registry settings that
160
+ // Node's bare fetch does not inherit. Prefer the already-working npm client
161
+ // there, then retain the direct registry request as a fallback.
162
+ if (!fetchImpl && platform === "win32") {
163
+ const npmVersion = fetchRemoteVersionWithNpm({
164
+ packageName,
165
+ registry: normalizedRegistry,
166
+ tag: normalizedTag,
136
167
  timeoutMs,
137
168
  spawnSyncImpl,
138
169
  environment,
139
170
  platform,
140
171
  });
172
+ if (npmVersion) return npmVersion;
141
173
  }
142
174
  const controller = new AbortController();
143
175
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
176
+ let remoteVersion = null;
144
177
  try {
145
178
  const encodedName = encodeURIComponent(packageName);
146
- const response = await (fetchImpl || globalThis.fetch)(`${normalizedRegistry}/${encodedName}/latest`, {
179
+ const response = await (fetchImpl || globalThis.fetch)(`${normalizedRegistry}/${encodedName}/${normalizedTag}`, {
147
180
  headers: { accept: "application/json" },
148
181
  signal: controller.signal,
149
182
  });
150
- if (!response.ok) return null;
151
- const metadata = await response.json();
152
- return validVersion(metadata?.version) ? metadata.version : null;
183
+ if (response.ok) {
184
+ const metadata = await response.json();
185
+ remoteVersion = validVersion(metadata?.version) ? metadata.version : null;
186
+ }
153
187
  } catch {
154
- return null;
188
+ // npm is the authoritative fallback below. It also honors npm's proxy and
189
+ // registry configuration, which is especially important on Windows.
155
190
  } finally {
156
191
  clearTimeout(timeout);
157
192
  }
193
+ if (remoteVersion || !fallbackToNpm) return remoteVersion;
194
+ return fetchRemoteVersionWithNpm({
195
+ packageName,
196
+ registry: normalizedRegistry,
197
+ tag: normalizedTag,
198
+ timeoutMs,
199
+ spawnSyncImpl,
200
+ environment,
201
+ platform,
202
+ });
158
203
  }
159
204
 
160
205
  export function readUpdateCache() {
@@ -198,7 +243,9 @@ export function cacheIsFresh(cache, now = Date.now()) {
198
243
  export async function refreshUpdateCache(dependencies = {}) {
199
244
  const fetchLatest = dependencies.fetchRemoteVersion || fetchRemoteVersion;
200
245
  const writeCache = dependencies.writeCache || writeUpdateCache;
201
- const remoteVersion = await fetchLatest();
246
+ const currentVersion = (dependencies.installedVersion || installedVersion)();
247
+ const tag = updateTagForVersion(currentVersion);
248
+ const remoteVersion = await fetchLatest({ tag });
202
249
  if (!remoteVersion) {
203
250
  try {
204
251
  writeCache({ lastFailedAt: Date.now() });
@@ -207,7 +254,7 @@ export async function refreshUpdateCache(dependencies = {}) {
207
254
  }
208
255
  return null;
209
256
  }
210
- return writeCache({ remoteVersion, checkedAt: Date.now() });
257
+ return writeCache({ remoteVersion, updateTag: tag, checkedAt: Date.now() });
211
258
  }
212
259
 
213
260
  /** One-line human notice when npm has a newer stable package. */