impel-cli 0.14.3 → 0.15.1

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
@@ -110,8 +110,20 @@ impel update # install the latest Impel CLI later
110
110
 
111
111
  `impel setup` keeps your normal Claude and Codex profiles separate. On Mac and
112
112
  Windows it also prepares the supported isolated desktop experiences. On Windows,
113
- it installs missing official vendor command-line tools when needed. On Linux,
114
- it prepares the isolated command-line workflow.
113
+ it installs missing official vendor command-line tools with the vendors'
114
+ checksum-verifying native installers, so Claude Code does not inherit its newer
115
+ Node.js requirement from the deprecated npm package. On Linux, it prepares the
116
+ isolated command-line workflow.
117
+
118
+ If setup or the CLI self-update fails, Impel first runs a deterministic local
119
+ check. In an interactive terminal it then offers assisted recovery; use
120
+ `impel setup --repair` (or `impel update --repair`) to opt in explicitly. Before
121
+ upload, the CLI prints the exact sanitized payload. The hosted agent returns a
122
+ reviewed action ID, and the CLI runs its allowlisted command locally; free-form
123
+ model commands are rejected and every mutation requires confirmation. Sessions
124
+ expire after 15 minutes, mutation capabilities are
125
+ single-use, and a private checkpoint lets `--repair` resume an interrupted run.
126
+ Use `--no-recovery` or `IMPEL_DISABLE_INSTALL_RECOVERY=1` to keep recovery off.
115
127
 
116
128
  ### If something does not work
117
129
 
@@ -133,11 +145,13 @@ access, scopes, and organization membership for every protected action.
133
145
 
134
146
  ```
135
147
  impel setup [--pat <pat>] [--tenant <org>] [--skip-apps] [--skip-clis]
148
+ [--repair|--no-recovery]
136
149
  Guided setup: token, tenant, platform clients, verify
137
150
  (closes running Claude/ChatGPT apps, updates the vendor
138
151
  apps, then installs the vendored Impel apps on macOS;
139
- installs missing vendor CLIs on Windows)
140
- impel update [--check] [--skip-apps] Update EVERYTHING: reinstall the CLI from npm, then
152
+ runs official native vendor CLI installers on Windows)
153
+ impel update [--check] [--skip-apps] [--repair|--no-recovery]
154
+ Update EVERYTHING: reinstall the CLI from npm, then
141
155
  cascade to `app update all`, `skills sync all`, and
142
156
  `agents sync all`
143
157
  impel auth [--pat <pat>] [--gateway <url>] [--app <url>]
@@ -472,26 +486,36 @@ and pre-tenancy Impel CLI profiles, so switching organizations cannot strand an
472
486
  older snapshot. These safeguards apply only to Impel-owned `CODEX_HOME`
473
487
  directories; native `~/.codex` remains untouched.
474
488
 
475
- On Windows, `impel setup` ensures the official `@anthropic-ai/claude-code` and
476
- `@openai/codex` packages are available, installing only the missing package(s)
477
- with the same npm installation that supplies Impel. Use `--skip-clis` to prepare
478
- the profiles without installing missing vendor CLIs. The launchers and skill
479
- sync resolve native executables through `PATH`/`PATHEXT`, including global npm
480
- `.cmd` and `.bat` shims. Windows environment references such as `%NVM_SYMLINK%`
481
- are expanded during lookup, and the standard npm-global, Claude native-installer,
482
- and Codex standalone-installer directories are checked even when the parent
483
- PowerShell PATH is stale. Arguments are forwarded through an explicitly escaped
484
- `cmd.exe` invocation without Node shell mode; NULs and line breaks, which cannot
485
- be represented safely through a batch shim, are rejected before anything is
486
- launched. Managed Codex auth and MCP subprocesses bypass batch shims entirely by
487
- calling Node directly.
488
-
489
- If npm itself fails during setup, the wizard prints the native spawn error and
490
- the exact vendor-package command to retry in PowerShell. If npm succeeds but the
491
- new commands remain undiscoverable, close and reopen PowerShell; then use
492
- `npm prefix --global` to identify the directory that must be on the user PATH.
493
- Setup exits non-zero and says it is incomplete until both launchers are ready—it
494
- does not print a success banner after a failed installation.
489
+ On Windows, `impel setup` installs only missing command-line tools through the
490
+ vendors' current native PowerShell installers. Claude Code's npm installation is
491
+ deprecated and its current package requires Node.js 22, while Impel itself still
492
+ supports Node.js 18 or newer. The native path keeps that valid Impel setup
493
+ working. Codex's installer is run non-interactively. Both installers download
494
+ platform-specific native releases and verify their published SHA-256 checksums.
495
+ Use `--skip-clis` to prepare the profiles without installing missing vendor
496
+ CLIs. Each tool is installed independently, so a failure from one vendor does
497
+ not prevent the other tool from being installed and configured.
498
+
499
+ The launchers and skill sync still resolve executables through `PATH`/`PATHEXT`,
500
+ including existing global npm `.cmd` and `.bat` shims. Windows environment
501
+ references such as `%NVM_SYMLINK%` are expanded during lookup, and the standard
502
+ npm-global, Claude native-installer, Codex standalone-installer, and Windows
503
+ PowerShell directories are checked even when the parent PowerShell PATH is
504
+ stale. Candidates must also pass a bounded `--version` probe, so a stale or
505
+ half-removed npm shim cannot hide a healthy native install later in the search
506
+ path. Existing batch-shim arguments are forwarded through an explicitly
507
+ escaped `cmd.exe` invocation without Node shell mode; NULs and line breaks,
508
+ which cannot be represented safely through a batch shim, are rejected before
509
+ anything is launched. Managed Codex auth and MCP subprocesses bypass batch
510
+ shims entirely by calling Node directly.
511
+
512
+ If a native installer fails, the wizard identifies the affected tool, preserves
513
+ the other tool's successful install, and prints that vendor's exact official
514
+ PowerShell command to retry. If an installer exits successfully but its command
515
+ cannot be verified, close and reopen PowerShell, retry the printed command, and
516
+ run `impel setup` again. Setup exits non-zero and says it is incomplete until
517
+ both launchers are ready—it does not print a success banner after a failed
518
+ installation.
495
519
 
496
520
  Windows receives the complete CLI workflow: setup/auth, tenant selection,
497
521
  isolated Claude and Codex launches, native profile switching, tasks, skills,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.14.3",
3
+ "version": "0.15.1",
4
4
  "description": "Configure Claude Code and Codex CLI to talk to Impel's gateway, authenticated by an Impel Personal Access Token",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -114,6 +114,16 @@ export function normalizeAppTarget(value) {
114
114
  return null;
115
115
  }
116
116
 
117
+ /** Return only app targets backed by at least one live tenant model. */
118
+ export function appTargetsSupportedByModels(targets, models, config = {}) {
119
+ if (!Array.isArray(models) || models.length === 0) return [];
120
+ if (crossAppModelsEnabled(config)) return [...targets];
121
+ const providers = new Set(models.map((model) => model?.provider));
122
+ return targets.filter((target) => providers.has(
123
+ target === "claude" ? "claude" : "codex",
124
+ ));
125
+ }
126
+
117
127
  function tenantAppName(value, fallback) {
118
128
  const name = redactSecretText(String(value || fallback))
119
129
  .normalize("NFC")
@@ -151,7 +161,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
151
161
 
152
162
  // Bump when the written config/manifest schema changes; a mismatch forces the
153
163
  // slow open path (and thus a full config rewrite) after a CLI update.
154
- export const CURRENT_CONFIG_VERSION = 10;
164
+ export const CURRENT_CONFIG_VERSION = 11;
155
165
 
156
166
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
157
167
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
@@ -604,6 +614,11 @@ export function installManagedAppFiles({
604
614
  tenantName: config.tenantName || config.tenantId || null,
605
615
  experiments: { crossAppModels: crossAppModelsEnabled(config) },
606
616
  targets,
617
+ availableTargets: appTargetsSupportedByModels(
618
+ ["claude", "chatgpt"],
619
+ models,
620
+ config,
621
+ ),
607
622
  models: models.map((model) => model.id),
608
623
  updatedAt: new Date().toISOString(),
609
624
  }, null, 2) + "\n", 0o600);
package/src/cli.js CHANGED
@@ -23,7 +23,8 @@ const HELP = `impel — the Impel gateway CLI: isolated Claude Code, Codex, and
23
23
  Get started:
24
24
  impel setup Guided end-to-end onboarding: token, tenant,
25
25
  platform clients, gateway verification
26
- (--pat <pat> --tenant <org> --skip-apps/--skip-clis)
26
+ (--pat <pat> --tenant <org> --skip-apps/--skip-clis
27
+ --repair/--no-recovery)
27
28
  impel update Update everything: the CLI itself, then the
28
29
  Impel apps, skills, and agents (--check, --skip-apps)
29
30
 
@@ -69,6 +70,7 @@ Env:
69
70
  IMPEL_APP_URL Default app/control-plane URL for setup, auth, PATs, and tasks.
70
71
  IMPEL_SKIP_UPDATE_CHECK=1 Silence launch-time update notices.
71
72
  IMPEL_SKIP_AGENT_SYNC=1 Keep the last native-agent catalog without refreshing it.
73
+ IMPEL_DISABLE_INSTALL_RECOVERY=1 Disable local and hosted install recovery.
72
74
 
73
75
  Config file:
74
76
  ~/.config/impel/config.json (mode 0600)
@@ -25,6 +25,7 @@ import {
25
25
  import {
26
26
  CURRENT_CONFIG_VERSION,
27
27
  CLAUDE_CONFIG_ID,
28
+ appTargetsSupportedByModels,
28
29
  appPaths,
29
30
  appStatus,
30
31
  bundleIsCurrent,
@@ -58,6 +59,45 @@ import {
58
59
 
59
60
  const CLAUDE_KEYCHAIN_NOTICE = "Claude Keychain: enter your Mac login password and choose Always Allow on the first prompt for this tenant; Allow is temporary.";
60
61
 
62
+ const APP_CAPABILITIES = Object.freeze({
63
+ claude: Object.freeze({ label: "Impel Claude", provider: "Claude" }),
64
+ chatgpt: Object.freeze({ label: "Impel ChatGPT", provider: "Codex" }),
65
+ });
66
+
67
+ /**
68
+ * Select targets the live tenant catalog can actually run. An explicit target
69
+ * remains fail-closed; `all` degrades to the available app with a clear notice.
70
+ */
71
+ export function selectCatalogAppTargets(
72
+ requestedTargets,
73
+ models,
74
+ config,
75
+ { log = console.log } = {},
76
+ ) {
77
+ const supported = appTargetsSupportedByModels(
78
+ requestedTargets,
79
+ models,
80
+ config,
81
+ );
82
+ const supportedSet = new Set(supported);
83
+ const unavailable = requestedTargets.filter((target) => !supportedSet.has(target));
84
+ if (unavailable.length === 0) return supported;
85
+
86
+ if (requestedTargets.length === 1 || supported.length === 0) {
87
+ const target = unavailable[0];
88
+ const capability = APP_CAPABILITIES[target];
89
+ throw new Error(
90
+ `${capability.label} is unavailable because the selected tenant has no ${capability.provider} models; choose a tenant with ${capability.provider} access or install the other app`,
91
+ );
92
+ }
93
+
94
+ for (const target of unavailable) {
95
+ const capability = APP_CAPABILITIES[target];
96
+ log(`Skipping ${capability.label}: the selected tenant has no ${capability.provider} models.`);
97
+ }
98
+ return supported;
99
+ }
100
+
61
101
  // Each isolated desktop app maps to a client CLI + the env override that points
62
102
  // that CLI at the app's private profile, so skill syncing lands in the app's
63
103
  // installation rather than a global one.
@@ -328,6 +368,14 @@ export async function cmdWindowsApps(argv, overrides = {}) {
328
368
  try {
329
369
  if (action === "open") maybePrintUpdateNotice();
330
370
  const config = await io.selectedConfig(targets, flags.tenant || null);
371
+ const catalog = await withProgress("Fetching the tenant model catalog", () => (
372
+ fetchWindowsCatalog(config, io)
373
+ ));
374
+ const actionTargets = selectCatalogAppTargets(
375
+ targets,
376
+ catalog.models,
377
+ config,
378
+ );
331
379
  const actionUserData = io.claudeUserData(io.environment, config.tenantId);
332
380
  const actionPaths = appPaths(io.homeDir, config.tenantId, {
333
381
  claudeUserData: actionUserData,
@@ -342,7 +390,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
342
390
  chatgpt: fs.existsSync(path.join(actionPaths.chatgpt.codexHome, "config.toml")),
343
391
  };
344
392
  const vendorPaths = {};
345
- for (const target of targets) {
393
+ for (const target of actionTargets) {
346
394
  const isClaude = target === "claude";
347
395
  const find = isClaude ? io.findClaudeApp : io.findChatGPTApp;
348
396
  const ensure = isClaude ? io.ensureClaudeApp : io.ensureChatGPTApp;
@@ -367,16 +415,13 @@ export async function cmdWindowsApps(argv, overrides = {}) {
367
415
  if (binary) vendorPaths[target] = binary;
368
416
  }
369
417
 
370
- if (targets.includes("claude") && action !== "refresh" && !vendorPaths.claude) {
418
+ if (actionTargets.includes("claude") && action !== "refresh" && !vendorPaths.claude) {
371
419
  throw new Error("Claude vendor app is unavailable; run `impel app install claude` or install it from https://claude.com/download");
372
420
  }
373
- const catalog = await withProgress("Fetching the tenant model catalog", () => (
374
- fetchWindowsCatalog(config, io)
375
- ));
376
- let managedChatGPT = targets.includes("chatgpt")
421
+ let managedChatGPT = actionTargets.includes("chatgpt")
377
422
  ? io.findManagedChatGPTApp(actionPaths.root)
378
423
  : null;
379
- const mustStageChatGPT = targets.includes("chatgpt")
424
+ const mustStageChatGPT = actionTargets.includes("chatgpt")
380
425
  && (action === "install"
381
426
  || action === "update"
382
427
  || (action === "open" && (!managedChatGPT
@@ -388,14 +433,14 @@ export async function cmdWindowsApps(argv, overrides = {}) {
388
433
  managedChatGPT = io.stageChatGPTApp(vendorPaths.chatgpt, actionPaths.root);
389
434
  }
390
435
 
391
- const { userData } = configureWindowsApps(config, targets, vendorPaths, catalog, io);
436
+ const { userData } = configureWindowsApps(config, actionTargets, vendorPaths, catalog, io);
392
437
  const tenantPaths = appPaths(io.homeDir, config.tenantId, {
393
438
  claudeUserData: userData,
394
439
  tenantName: config.tenantName,
395
440
  });
396
441
  const configuredTargets = action === "open"
397
- ? targets.filter((target) => !profileWasInstalled[target])
398
- : targets;
442
+ ? actionTargets.filter((target) => !profileWasInstalled[target])
443
+ : actionTargets;
399
444
  if (action === "install" || action === "update" || action === "refresh" || configuredTargets.length > 0) {
400
445
  const verb = action === "update" ? "Updated" : action === "refresh" ? "Refreshed" : "Installed";
401
446
  for (const target of configuredTargets) {
@@ -417,7 +462,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
417
462
  }
418
463
  if (!flags["skip-agents"]) {
419
464
  await withProgress("Syncing app agent profiles", () => io.syncAgents({
420
- profiles: targets.map((target) => appAgentProfile(target, tenantPaths)),
465
+ profiles: actionTargets.map((target) => appAgentProfile(target, tenantPaths)),
421
466
  gatewayUrl: config.gatewayUrl,
422
467
  credential: config.pat,
423
468
  tenantId: config.tenantId,
@@ -426,11 +471,11 @@ export async function cmdWindowsApps(argv, overrides = {}) {
426
471
  console.log(`Models: ${catalog.models.length} from ${catalog.source}. The signed vendor apps and their normal profiles were not changed.`);
427
472
  }
428
473
  if (action === "open") {
429
- if (targets.includes("claude")) {
474
+ if (actionTargets.includes("claude")) {
430
475
  await io.launchClaudeApp(vendorPaths.claude, tenantPaths.claude.userData, { environment: io.environment });
431
476
  console.log("Opened Impel Claude with its tenant-isolated Windows profile.");
432
477
  }
433
- if (targets.includes("chatgpt")) {
478
+ if (actionTargets.includes("chatgpt")) {
434
479
  managedChatGPT ||= io.findManagedChatGPTApp(tenantPaths.root);
435
480
  if (!managedChatGPT) throw new Error("Impel ChatGPT is not staged; run `impel app install codex`");
436
481
  await io.launchChatGPTApp(managedChatGPT, {
@@ -558,9 +603,24 @@ export async function cmdApps(argv, overrides = {}) {
558
603
  const config = await io.selectedConfig(targets, flags.tenant || null);
559
604
  console.log(`Tenant: ${config.tenantId} (desktop history is isolated per tenant).`);
560
605
 
606
+ let catalog;
607
+ try {
608
+ catalog = await withProgress("Fetching the tenant model catalog", () => io.fetchModels(config));
609
+ } catch (error) {
610
+ throw new Error(
611
+ `tenant model catalog is unavailable (${redactSecretText(error.message)}); no Impel app files were changed`,
612
+ );
613
+ }
614
+ const actionTargets = selectCatalogAppTargets(
615
+ targets,
616
+ catalog.models,
617
+ config,
618
+ { log: io.log },
619
+ );
620
+
561
621
  const vendorPaths = {};
562
622
  if (!flags["skip-vendor"]) {
563
- for (const target of targets) {
623
+ for (const target of actionTargets) {
564
624
  // Reuse or install only the exact vendor build verified by this CLI.
565
625
  // Moving "latest" releases are never cloned into a managed bundle.
566
626
  const result = await withProgress(`Preparing the verified ${target} vendor app`, () => (
@@ -573,7 +633,7 @@ export async function cmdApps(argv, overrides = {}) {
573
633
  }
574
634
 
575
635
  const statuses = io.status(
576
- targets,
636
+ actionTargets,
577
637
  io.homeDir,
578
638
  config.tenantId,
579
639
  config.tenantName,
@@ -581,18 +641,9 @@ export async function cmdApps(argv, overrides = {}) {
581
641
  for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
582
642
  const force = Boolean(flags.force);
583
643
  const staleBundleTargets = force
584
- ? [...targets]
644
+ ? [...actionTargets]
585
645
  : statuses.filter((status) => !io.bundleCurrent(status)).map((status) => status.target);
586
646
 
587
- let catalog;
588
- try {
589
- catalog = await withProgress("Fetching the tenant model catalog", () => io.fetchModels(config));
590
- } catch (error) {
591
- throw new Error(
592
- `tenant model catalog is unavailable (${redactSecretText(error.message)}); no Impel app files were changed`,
593
- );
594
- }
595
-
596
647
  // A running vendor or Impel app only needs to close when its bundle will be
597
648
  // swapped. Remember which managed launchers we closed so the update can
598
649
  // reopen them afterward and feel in-place (the fast path never quits at all).
@@ -616,7 +667,7 @@ export async function cmdApps(argv, overrides = {}) {
616
667
  staleBundleTargets.length > 0 ? "Rebuilding managed app bundles" : "Updating managed app profiles",
617
668
  () => io.installFiles({
618
669
  config,
619
- targets,
670
+ targets: actionTargets,
620
671
  models: catalog.models,
621
672
  homeDir: io.homeDir,
622
673
  vendorPaths,
@@ -705,8 +756,17 @@ export async function provisionAndOpenManagedApps({
705
756
  );
706
757
  }
707
758
 
759
+ const actionTargets = selectCatalogAppTargets(
760
+ targets,
761
+ catalog.models,
762
+ config,
763
+ { log: io.log },
764
+ );
765
+ const actionTargetSet = new Set(actionTargets);
766
+ const actionStatuses = statuses.filter((status) => actionTargetSet.has(status.target));
767
+
708
768
  const vendorPaths = {};
709
- for (const status of statuses) {
769
+ for (const status of actionStatuses) {
710
770
  let vendorPath = status.vendorPath;
711
771
  if (!vendorPath) {
712
772
  const result = await withProgress(`Preparing the verified ${status.target} vendor app`, () => (
@@ -723,7 +783,7 @@ export async function provisionAndOpenManagedApps({
723
783
 
724
784
  // Only stale or absent bundles pay the clone + codesign rebuild. The model
725
785
  // catalog and vendor checks above finish before any running apps are closed.
726
- const staleBundleTargets = statuses
786
+ const staleBundleTargets = actionStatuses
727
787
  .filter((status) => !io.bundleCurrent(status))
728
788
  .map((status) => status.target);
729
789
  if (staleBundleTargets.length > 0) {
@@ -737,7 +797,7 @@ export async function provisionAndOpenManagedApps({
737
797
  staleBundleTargets.length > 0 ? "Rebuilding managed app bundles" : "Updating managed app profiles",
738
798
  () => io.installFiles({
739
799
  config,
740
- targets,
800
+ targets: actionTargets,
741
801
  models: catalog.models,
742
802
  homeDir,
743
803
  vendorPaths,
@@ -745,7 +805,7 @@ export async function provisionAndOpenManagedApps({
745
805
  }),
746
806
  );
747
807
  const newlyInstalled = new Set(
748
- statuses.filter((status) => !status.launcherInstalled).map((status) => status.target),
808
+ actionStatuses.filter((status) => !status.launcherInstalled).map((status) => status.target),
749
809
  );
750
810
  for (const item of installed) {
751
811
  if (newlyInstalled.has(item.target)) {
@@ -802,13 +862,30 @@ export function fastOpenLaunchers(targets, { homeDir = os.homedir(), config = lo
802
862
  ) {
803
863
  return null;
804
864
  }
865
+ const availableTargets = Array.isArray(manifest.availableTargets)
866
+ ? manifest.availableTargets
867
+ : [];
868
+ const candidateTargets = targets.length > 1
869
+ ? targets.filter((target) => availableTargets.includes(target))
870
+ : targets;
871
+ if (
872
+ candidateTargets.length === 0
873
+ || candidateTargets.some((target) => !availableTargets.includes(target))
874
+ || candidateTargets.some((target) => !manifest.targets?.includes(target))
875
+ ) {
876
+ return null;
877
+ }
878
+ const candidateTargetSet = new Set(candidateTargets);
879
+ const candidateStatuses = statuses
880
+ ? statuses.filter((status) => candidateTargetSet.has(status.target))
881
+ : appStatus(
882
+ candidateTargets,
883
+ homeDir,
884
+ config.tenantId,
885
+ config.tenantName || manifest.tenantName,
886
+ );
805
887
  const launchers = [];
806
- for (const status of statuses ?? appStatus(
807
- targets,
808
- homeDir,
809
- config.tenantId,
810
- config.tenantName || manifest.tenantName,
811
- )) {
888
+ for (const status of candidateStatuses) {
812
889
  if (!bundleIsCurrent(status)) return null;
813
890
  if (!fs.existsSync(status.configPath)) return null;
814
891
  if (status.target === "claude" && !claudeConfigIsCurrent(status.configPath, config, gatewayUrl)) {
@@ -870,24 +947,33 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {})
870
947
  } catch {
871
948
  return;
872
949
  }
950
+ const supportedTargetSet = new Set(appTargetsSupportedByModels(
951
+ installedTargets.map((status) => status.target),
952
+ catalog.models,
953
+ config,
954
+ ));
955
+ const supportedStatuses = installedTargets.filter((status) => (
956
+ supportedTargetSet.has(status.target)
957
+ ));
958
+ if (supportedStatuses.length === 0) return;
873
959
  installManagedAppFiles({
874
960
  config,
875
- targets: installedTargets.map((status) => status.target),
961
+ targets: supportedStatuses.map((status) => status.target),
876
962
  models: catalog.models,
877
963
  homeDir: os.homedir(),
878
- vendorPaths: Object.fromEntries(installedTargets.map((status) => [status.target, status.vendorPath])),
964
+ vendorPaths: Object.fromEntries(supportedStatuses.map((status) => [status.target, status.vendorPath])),
879
965
  writeBundles: false,
880
966
  });
881
967
 
882
968
  const tenantPaths = appPaths(os.homedir(), config.tenantId, { tenantName: config.tenantName });
883
969
  const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
884
- for (const status of installedTargets) {
970
+ for (const status of supportedStatuses) {
885
971
  const { client, env, label } = appSkillTarget(status.target, tenantPaths);
886
972
  await syncSkillsSafe({ client, gatewayUrl, env, label });
887
973
  if (status.target === "chatgpt") secureManagedCodexHome(tenantPaths.chatgpt.codexHome);
888
974
  }
889
975
  await syncAgentProfilesSafe({
890
- profiles: installedTargets.map((status) => appAgentProfile(status.target, tenantPaths)),
976
+ profiles: supportedStatuses.map((status) => appAgentProfile(status.target, tenantPaths)),
891
977
  gatewayUrl: config.gatewayUrl,
892
978
  credential: config.pat,
893
979
  tenantId: config.tenantId,