impel-cli 0.18.15-beta.5 → 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.5",
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": {
package/src/apps.js CHANGED
@@ -257,7 +257,10 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
257
257
  // and the tenant manifest so unsupported hosts keep Chat and Code usable.
258
258
  // 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
259
259
  // and restore the reviewed always-enabled managed policy.
260
- export const CURRENT_CONFIG_VERSION = 25;
260
+ // 26: remember the managed provider defaults and move legacy GPT-5.5 profiles
261
+ // onto GPT-5.6 Sol so ChatGPT's compact Work picker does not fall back to its
262
+ // unsupported-selection "Reset to default" treatment.
263
+ export const CURRENT_CONFIG_VERSION = 26;
261
264
 
262
265
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
263
266
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
@@ -756,6 +759,7 @@ export function installManagedAppFiles({
756
759
  target,
757
760
  vendorPaths[target] || findVendorApp(target, homeDir),
758
761
  ]));
762
+ const previousManifest = readTenantManifest(homeDir, config.tenantId);
759
763
  const vendorCodexModels = targets.includes("chatgpt")
760
764
  ? readVendorCodexModels(resolvedVendorPaths.chatgpt)
761
765
  : new Map();
@@ -806,6 +810,7 @@ export function installManagedAppFiles({
806
810
  vendorCodexModels,
807
811
  chatgptInvocations,
808
812
  generatedAt,
813
+ previousManifest,
809
814
  );
810
815
  if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
811
816
  }
@@ -817,7 +822,7 @@ export function installManagedAppFiles({
817
822
  }
818
823
 
819
824
  writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
820
- schemaVersion: 6,
825
+ schemaVersion: 7,
821
826
  configVersion: CURRENT_CONFIG_VERSION,
822
827
  modelCatalogVersion: 3,
823
828
  gatewayUrl: config.gatewayUrl,
@@ -830,6 +835,10 @@ export function installManagedAppFiles({
830
835
  models,
831
836
  config,
832
837
  ),
838
+ modelDefaults: {
839
+ claude: models.find((model) => model.provider === "claude" && model.default)?.id ?? null,
840
+ chatgpt: models.find((model) => model.provider === "codex" && model.default)?.id ?? null,
841
+ },
833
842
  models: models.map((model) => model.id),
834
843
  updatedAt: generatedAt,
835
844
  }, null, 2) + "\n", 0o600);
@@ -1163,6 +1172,7 @@ function writeChatGPTConfig(
1163
1172
  vendorCodexModels,
1164
1173
  invocations = null,
1165
1174
  generatedAt = new Date().toISOString(),
1175
+ previousManifest = null,
1166
1176
  ) {
1167
1177
  const experimental = crossAppModelsEnabled(config);
1168
1178
  const orderedModels = experimental
@@ -1178,8 +1188,28 @@ function writeChatGPTConfig(
1178
1188
  const currentToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
1179
1189
  const configuredModel = readTopLevelTomlString(currentToml, "model");
1180
1190
  const defaultModelID = orderedModels.find((model) => model.provider === "codex" && model.default)?.id;
1181
- const selectedModel = codexModels.find((model) => model.slug === configuredModel)
1182
- || codexModels.find((model) => model.slug === defaultModelID)
1191
+ const defaultModel = codexModels.find((model) => model.slug === defaultModelID);
1192
+ const previousDefaultModelID = previousManifest?.modelDefaults?.chatgpt;
1193
+ const followsPreviousManagedDefault = (
1194
+ typeof previousDefaultModelID === "string"
1195
+ && configuredModel === previousDefaultModelID
1196
+ && configuredModel !== defaultModelID
1197
+ );
1198
+ // Profiles written before the default-model field existed cannot distinguish
1199
+ // an old managed default from an explicit user choice. GPT-5.5 is the one
1200
+ // observed legacy default that ChatGPT 26.727 renders as an unsupported
1201
+ // compact-picker selection; migrate it once, while preserving every other
1202
+ // explicit model choice.
1203
+ const hasLegacyCompactPickerDefault = (
1204
+ previousDefaultModelID == null
1205
+ && Number(previousManifest?.configVersion ?? 0) < CURRENT_CONFIG_VERSION
1206
+ && configuredModel === "gpt-5.5"
1207
+ && defaultModelID === "gpt-5.6-sol"
1208
+ );
1209
+ const selectedModel = (followsPreviousManagedDefault || hasLegacyCompactPickerDefault
1210
+ ? defaultModel
1211
+ : codexModels.find((model) => model.slug === configuredModel))
1212
+ || defaultModel
1183
1213
  || codexModels[0];
1184
1214
  if (!selectedModel) throw new Error("no Codex models are available for Impel ChatGPT");
1185
1215
  const configuredEffort = readTopLevelTomlString(currentToml, "model_reasoning_effort");
@@ -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. */