impel-cli 0.18.15-beta.2 → 0.18.15-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.15-beta.2",
3
+ "version": "0.18.15-beta.3",
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
@@ -679,7 +679,7 @@ export function migrateLegacyClaudeAppSessions(userData, homeDir = os.homedir())
679
679
  return copied;
680
680
  }
681
681
 
682
- export async function fetchGatewayModels(config, fetchImpl = fetch) {
682
+ export async function fetchGatewayModels(config, fetchImpl = fetch, { allowEmpty = false } = {}) {
683
683
  const controller = new AbortController();
684
684
  const timeout = setTimeout(() => controller.abort(), 20000);
685
685
  try {
@@ -697,7 +697,9 @@ export async function fetchGatewayModels(config, fetchImpl = fetch) {
697
697
  }
698
698
  if (!Array.isArray(payload?.data)) throw new Error("response has no model data array");
699
699
  const models = payload.data.filter(isGatewayModel);
700
- if (models.length === 0) throw new Error("gateway returned no supported models");
700
+ if (models.length === 0 && !(allowEmpty && isExplicitNoSeatCatalog(payload))) {
701
+ throw new Error("gateway returned no supported models");
702
+ }
701
703
  return { models, source: "gateway", version: payload.version ?? null };
702
704
  } finally {
703
705
  clearTimeout(timeout);
@@ -2547,6 +2549,17 @@ function isGatewayModel(model) {
2547
2549
  return model && typeof model.id === "string" && (model.provider === "claude" || model.provider === "codex");
2548
2550
  }
2549
2551
 
2552
+ function isExplicitNoSeatCatalog(payload) {
2553
+ const statuses = payload?.provider_status;
2554
+ return Boolean(statuses
2555
+ && typeof statuses === "object"
2556
+ && !Array.isArray(statuses)
2557
+ && ["claude", "codex"].every((provider) => (
2558
+ statuses[provider]?.state === "no_seat"
2559
+ && statuses[provider]?.routable === false
2560
+ )));
2561
+ }
2562
+
2550
2563
  function writeAtomic(target, contents, mode) {
2551
2564
  fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
2552
2565
  if (fs.existsSync(target) && fs.readFileSync(target, "utf8") === contents) {
@@ -76,7 +76,7 @@ export function selectCatalogAppTargets(
76
76
  requestedTargets,
77
77
  models,
78
78
  config,
79
- { log = console.log } = {},
79
+ { log = console.log, allowNone = false } = {},
80
80
  ) {
81
81
  const supported = appTargetsSupportedByModels(
82
82
  requestedTargets,
@@ -87,6 +87,14 @@ export function selectCatalogAppTargets(
87
87
  const unavailable = requestedTargets.filter((target) => !supportedSet.has(target));
88
88
  if (unavailable.length === 0) return supported;
89
89
 
90
+ if (allowNone && supported.length === 0) {
91
+ for (const target of unavailable) {
92
+ const capability = APP_CAPABILITIES[target];
93
+ log(`Skipping ${capability.label}: the selected tenant has no ${capability.provider} models.`);
94
+ }
95
+ return supported;
96
+ }
97
+
90
98
  if (requestedTargets.length === 1 || supported.length === 0) {
91
99
  const target = unavailable[0];
92
100
  const capability = APP_CAPABILITIES[target];
@@ -297,9 +305,9 @@ export function openManagedLauncher(launcher, {
297
305
  return false;
298
306
  }
299
307
 
300
- async function fetchWindowsCatalog(config, io) {
308
+ async function fetchWindowsCatalog(config, io, { allowEmpty = false } = {}) {
301
309
  try {
302
- return await io.fetchModels(config);
310
+ return await io.fetchModels(config, undefined, { allowEmpty });
303
311
  } catch (error) {
304
312
  throw new Error(`tenant model catalog is unavailable (${redactSecretText(error.message)}); the Impel app profiles were not changed`);
305
313
  }
@@ -374,12 +382,25 @@ export async function reconcileWindowsTenantApps({
374
382
  ...overrides,
375
383
  };
376
384
  const config = explicitTenantAppConfig(baseConfig, tenant, targets);
377
- const catalog = await fetchWindowsCatalog(config, io);
378
- const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
385
+ const catalog = await fetchWindowsCatalog(config, io, { allowEmpty: true });
386
+ const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
387
+ log: io.log,
388
+ allowNone: true,
389
+ });
379
390
  const supported = new Set(actionTargets);
380
391
  const unsupported = targets.filter((target) => !supported.has(target));
381
392
  const userData = io.claudeUserData(environment, config.tenantId);
382
393
  const paths = appPaths(homeDir, config.tenantId, { claudeUserData: userData, tenantName: config.tenantName });
394
+ if (actionTargets.length === 0) {
395
+ return {
396
+ tenantId: config.tenantId,
397
+ targets: [],
398
+ unsupported,
399
+ paths,
400
+ verification: {},
401
+ failed: [],
402
+ };
403
+ }
383
404
  const vendorPaths = {};
384
405
  const preparedTargets = new Set(skipVendor ? actionTargets : skipVendorTargets);
385
406
 
@@ -501,13 +522,28 @@ export async function reconcileMacTenantApps({
501
522
  const config = explicitTenantAppConfig(baseConfig, tenant, targets);
502
523
  let catalog;
503
524
  try {
504
- catalog = await io.fetchModels(config);
525
+ catalog = await io.fetchModels(config, undefined, { allowEmpty: true });
505
526
  } catch (error) {
506
527
  throw new Error(`tenant model catalog is unavailable (${redactSecretText(error?.message || error)}); no Impel app files were changed`);
507
528
  }
508
- const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
529
+ const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
530
+ log: io.log,
531
+ allowNone: true,
532
+ });
509
533
  const supported = new Set(actionTargets);
510
534
  const unsupported = targets.filter((target) => !supported.has(target));
535
+ const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
536
+ if (actionTargets.length === 0) {
537
+ return {
538
+ tenantId: config.tenantId,
539
+ targets: [],
540
+ unsupported,
541
+ paths,
542
+ verification: {},
543
+ installed: [],
544
+ failed: [],
545
+ };
546
+ }
511
547
  const vendorPaths = {};
512
548
  const statuses = io.status(actionTargets, homeDir, config.tenantId, config.tenantName);
513
549
  for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
@@ -549,7 +585,6 @@ export async function reconcileMacTenantApps({
549
585
  vendorPaths,
550
586
  writeBundles: staleTargets,
551
587
  }) : [];
552
- const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
553
588
  const agentItems = [];
554
589
  for (const item of installed) {
555
590
  const { client, env, label } = appSkillTarget(item.target, paths);