@detergent-software/atk 9.2.0-dev.5 → 9.2.0-dev.6

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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [9.2.0-dev.6](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTv9.2.0-dev.5&targetVersion=GTv9.2.0-dev.6) (2026-10-23)
2
+
3
+ ### Features
4
+
5
+ * **cli:** install bundles and [@org](https://emergentcode.visualstudio.com/org) among several names as one plan ([47425f3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/47425f35d4986306dc024fca8ad6d767d064188f))
6
+ * **cli:** uninstall a bundle among several names in one pass ([8c75075](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/8c75075465bd210989baba237955a68dcce2ad73))
7
+
8
+ ### Bug Fixes
9
+
10
+ * **cli:** review fixes for bundles and [@org](https://emergentcode.visualstudio.com/org) among install names ([fc66d79](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/fc66d7994d704cf1ceebb6f2b551737e3233f671))
11
+
12
+ ### Documentation
13
+
14
+ * **cli:** describe bundles and [@org](https://emergentcode.visualstudio.com/org) among several install names ([febfb7e](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/febfb7e6a81d6308c2df788fddfc2f1cb02e54f7))
15
+
1
16
  ## [9.2.0-dev.5](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTv9.2.0-dev.4&targetVersion=GTv9.2.0-dev.5) (2026-10-23)
2
17
 
3
18
  ### Features
@@ -515,7 +515,7 @@
515
515
  "name": "[target]"
516
516
  },
517
517
  {
518
- "description": "More assets to install with the first, as one plan (name@version and @org/name)",
518
+ "description": "More assets, bundles or org scopes to install with the first, as one plan (name@version, @org/name, @org with --all)",
519
519
  "name": "[targets...]"
520
520
  }
521
521
  ],
@@ -529,7 +529,7 @@
529
529
  },
530
530
  {
531
531
  "flags": "-a, --all",
532
- "description": "Install every asset from an org scope without prompting (for `atk install @org`; required with --json or no terminal)",
532
+ "description": "Install every asset from an org scope without prompting (for `atk install @org`; required with --json, no terminal, or other names)",
533
533
  "defaultValue": false
534
534
  },
535
535
  {
@@ -1331,7 +1331,7 @@
1331
1331
  "name": "<name>"
1332
1332
  },
1333
1333
  {
1334
- "description": "More assets to uninstall with the first",
1334
+ "description": "More assets or bundles to uninstall with the first, in one pass",
1335
1335
  "name": "[names...]"
1336
1336
  }
1337
1337
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAmExB,eAAO,MAAM,WAAW,6DAA6D,CAAC;AAEtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;iBA8GlB,CAAC;AAEH,eAAO,MAAM,IAAI,uDAmBd,CAAC;AAmGJ,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,EAC9B,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,EACxB,OAAO,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAC9F,EAAE,KAAK,+BAuDP"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqExB,eAAO,MAAM,WAAW,6DAA6D,CAAC;AAEtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;iBA8GlB,CAAC;AAEH,eAAO,MAAM,IAAI,uDAoBd,CAAC;AAqIJ,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;CAClC,CAAC;AAQF,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,EAC9B,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,EACxB,OAAO,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAC9F,EAAE,KAAK,+BAwDP"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text, useApp } from 'ink';
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { z } from 'zod';
5
5
  import { CrossScopeWarning, DryRunBanner, ErrorView, InstallSummary, MultiSelectList, Spinner, } from '../components/index.js';
6
6
  import { useCommand } from '../hooks/useCommand.js';
@@ -11,6 +11,7 @@ import { loadConfig } from '../lib/config.js';
11
11
  import { crossScopeNotice, findCrossScopeInstalls } from '../lib/cross-scope.js';
12
12
  import { errorGuidance, errorMessage } from '../lib/errors.js';
13
13
  import { isInstalledForTool } from '../lib/installed-for-tool.js';
14
+ import { InstallerError } from '../lib/installer.js';
14
15
  import { addAssetToLockfile, findInstalledAsset, identityKey, readLockfile, upgradeToDirectInstall, withLockfileErrorHandling, writeLockfile, } from '../lib/lockfile.js';
15
16
  import { executeInstallPlanForTools, withoutKeptDependencies } from '../lib/multi-tool-install.js';
16
17
  import { addNotice } from '../lib/notices.js';
@@ -18,7 +19,7 @@ import { formatOrgName, isBareOrgScope, parseBundleSpec, resolveActiveOrg } from
18
19
  import { renderJson, useNonInteractive } from '../lib/output.js';
19
20
  import { argument, option } from '../lib/pastel/index.js';
20
21
  import { fetchRegistry, findAsset, findAssetsByName, findBundle, findBundleMember, getAssetsByOrg, isDeprecated, isLatestAlias, } from '../lib/registry.js';
21
- import { resolveBundleInstallPlan, resolveInstallPlan, resolveMultipleInstallPlans, ResolverError, } from '../lib/resolver.js';
22
+ import { resolveBundleInstallPlan, resolveInstallPlan, resolveMultipleInstallPlans, ResolverError, resolveTargetsInstallPlan, } from '../lib/resolver.js';
22
23
  import { scopeLabel } from '../lib/scope.js';
23
24
  import { resolvePlacingTools } from '../lib/tool-resolver.js';
24
25
  import { parseNameVersion } from '../lib/version.js';
@@ -36,7 +37,7 @@ export const options = z.object({
36
37
  .default(false)
37
38
  .describe(option({
38
39
  alias: 'a',
39
- description: 'Install every asset from an org scope without prompting (for `atk install @org`; required with --json or no terminal)',
40
+ description: 'Install every asset from an org scope without prompting (for `atk install @org`; required with --json, no terminal, or other names)',
40
41
  })),
41
42
  dryRun: z
42
43
  .boolean()
@@ -119,7 +120,7 @@ export const args = z
119
120
  })),
120
121
  ])
121
122
  .rest(z.string().describe(argument({
122
- description: 'More assets to install with the first, as one plan (name@version and @org/name)',
123
+ description: 'More assets, bundles or org scopes to install with the first, as one plan (name@version, @org/name, @org with --all)',
123
124
  name: 'targets',
124
125
  })));
125
126
  export default function Install({ args: [rawName, ...more], options: { adopt, all, dryRun, force, json, org, pin, projectRoot, refresh, tool, user, yes }, }) {
@@ -128,7 +129,7 @@ export default function Install({ args: [rawName, ...more], options: { adopt, al
128
129
  return _jsx(Sync, { options: { dryRun, force, json, projectRoot, refresh, tool, user } });
129
130
  }
130
131
  if (more.length > 0) {
131
- return (_jsx(InstallAssets, { adopt: adopt, dryRun: dryRun, force: force, json: json, org: org, pin: pin, projectRoot: projectRoot, specs: [rawName, ...more], tool: tool, user: user }));
132
+ return (_jsx(InstallAssets, { adopt: adopt, all: all || yes, dryRun: dryRun, force: force, json: json, org: org, pin: pin, projectRoot: projectRoot, specs: [rawName, ...more], tool: tool, user: user }));
132
133
  }
133
134
  if (isBareOrgScope(rawName)) {
134
135
  const orgName = rawName.slice(1);
@@ -273,16 +274,8 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
273
274
  // through to a misleading "asset not found".
274
275
  if (!indexBundle && (registry.bundles ?? []).some((b) => b.name === name)) {
275
276
  const assetExists = findAsset(registry, name, undefined, undefined, org);
276
- if (!assetExists) {
277
- if (inlineOrg) {
278
- throw new Error(`Bundle '${name}' not found in org '${inlineOrg}'.`);
279
- }
280
- const orgScopes = (registry.bundles ?? [])
281
- .filter((b) => b.name === name && b.org)
282
- .map((b) => `@${b.org}/${name}`);
283
- throw new Error(`Bundle '${name}' not found in the registry.` +
284
- (orgScopes.length > 0 ? ` Did you mean ${orgScopes.join(' or ')}?` : ''));
285
- }
277
+ if (!assetExists)
278
+ throw otherScopeBundleError(registry, name, inlineOrg);
286
279
  }
287
280
  // With no active org, `info` opens the only org asset of a bare name, but install doesn't
288
281
  // switch scopes on its own: it names the spec that installs that asset.
@@ -360,26 +353,7 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
360
353
  };
361
354
  // Bundle installation flow — check if already installed for ALL tools
362
355
  if (!force) {
363
- const bundleAssets = bundle.assets ?? [];
364
- // When no configured tool takes any member, fall through so the installer says why.
365
- const allInstalledForAllTools = tools.every(({ adapter: toolAdapter }) => members.length > 0 &&
366
- bundleAssets.every((ref) => {
367
- const found = findBundleMember(registry, bundle, ref);
368
- if (!found)
369
- return false;
370
- // A tool that doesn't take this member (its type, or a hook for other tools) has
371
- // nothing to check.
372
- if (!placesAsset(toolAdapter, found.asset.type, found.versionData.tools))
373
- return true;
374
- const installed = findInstalledAsset(before, ref.name, ref.type, found.asset.org);
375
- if (!installed)
376
- return false;
377
- if (requiredBundleVersion && !isFromBundleVersion(installed, bundle, requiredBundleVersion)) {
378
- return false;
379
- }
380
- return isInstalledForTool(installed, toolAdapter, projectDir);
381
- }));
382
- if (allInstalledForAllTools) {
356
+ if (isBundleInPlace(registry, bundle, before, { members, projectDir, requiredBundleVersion, tools })) {
383
357
  // Adopting a bundle that is in place only records its origin on the members with none:
384
358
  // their files, versions and pins stay as they are. Another bundle's member stays its.
385
359
  const outside = membersOutsideBundle(before, members, bundle);
@@ -521,13 +495,15 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
521
495
  return (_jsxs(Box, { flexDirection: 'column', children: [data.toolResults.map(({ results, skipped, tool: toolName, warnings }) => (_jsx(InstallSummary, { results: results, skipped: skipped, tools: [toolName], warnings: warnings, where: label }, toolName))), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
522
496
  }
523
497
  /**
524
- * `atk install a b@1.2.0 @org/c`: the named assets resolved together (shared dependencies once,
525
- * one version per asset) and installed as one plan, so a failure rolls the whole run back, as an
526
- * `@org` install does. Every name is checked before anything is written. A name already in place
527
- * for every tool is left out of the plan, and one installed only as a dependency becomes direct
528
- * once the install succeeds.
498
+ * `atk install a b@1.2.0 @org/c my-bundle @org --all`: the named assets, the bundles' members and
499
+ * the orgs' assets resolved together (shared dependencies once, one version per asset) and
500
+ * installed as one plan, so a failure rolls the whole run back, as an `@org` install does. Every
501
+ * name is checked before anything is written. An asset already in place for every tool is left out
502
+ * of the plan, and one installed only as a dependency becomes direct once the install succeeds. A
503
+ * bundle is left out when all of it is in place, as when it is installed alone; its members get
504
+ * its origin unless they are named themselves ({@link resolveTargetsInstallPlan}).
529
505
  */
530
- function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoot, specs, tool, user, }) {
506
+ function InstallAssets({ adopt, all, dryRun, force, json, org: cliOrg, pin, projectRoot, specs, tool, user, }) {
531
507
  const { json: effectiveJson } = useNonInteractive(json);
532
508
  const { fields, resolve, resolved } = useResolvedScope({ projectRoot, user });
533
509
  const state = useCommand(async (progress) => {
@@ -535,11 +511,13 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
535
511
  if (adopt) {
536
512
  throw new Error('--adopt records a bundle as the owner of its members, so it works with one bundle only.');
537
513
  }
538
- const orgScope = specs.find(isBareOrgScope);
539
- if (orgScope) {
540
- throw new ResolverError(`${orgScope} is an org scope, which installs on its own.`, {
541
- command: installCommandFor(orgScope, flags),
542
- });
514
+ // No picker opens halfway through a run, so an org among names takes all its assets, and
515
+ // only when asked to. A dry run shows them all, as `atk install @org --dry-run` does.
516
+ const orgScopes = [...new Set(specs.filter(isBareOrgScope))];
517
+ if (orgScopes.length > 0 && !all && !dryRun) {
518
+ throw new InstallerError(`${orgScopes.join(', ')} among other names installs every asset in ` +
519
+ `${orgScopes.length === 1 ? 'it' : 'them'}, so it needs --all (-a). ` +
520
+ `To choose ${orgScopes.length === 1 ? 'its' : 'their'} assets, install ${orgScopes.length === 1 ? 'it' : 'each'} alone.`, { code: 'selection_required', command: installCommandFor(`${specs.join(' ')} --all`, flags) });
543
521
  }
544
522
  const { root: projectDir, scope: installScope } = await resolve();
545
523
  progress('Loading configuration...');
@@ -549,33 +527,65 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
549
527
  progress('Fetching registry...');
550
528
  const registry = await fetchRegistry({ force: true });
551
529
  const currentLockfile = await readLockfile(projectDir);
552
- // Every name is checked before anything is resolved or written.
553
- // Each bundle as typed, and the spec that installs it (another scope's, when only that has it).
554
- const bundles = [];
530
+ // Every name is checked before anything is resolved or written. The targets keep the order
531
+ // they were named in, which decides the origin of a member two bundles share.
555
532
  const deprecated = [];
556
- const requests = [];
557
- for (const spec of specs) {
533
+ const emptyOrgs = [];
534
+ const orgs = [];
535
+ const targets = [];
536
+ const deprecationOf = (name, deprecation) => {
537
+ if (deprecation.deprecated && !force) {
538
+ deprecated.push(`"${name}" (${deprecation.reason}${deprecation.replacement ? `; use ${deprecation.replacement}` : ''})`);
539
+ }
540
+ };
541
+ for (const spec of new Set(specs)) {
542
+ if (isBareOrgScope(spec)) {
543
+ const orgName = spec.slice(1);
544
+ const orgAssets = getAssetsByOrg(registry, orgName);
545
+ if (orgAssets.length === 0)
546
+ emptyOrgs.push(spec);
547
+ orgs.push({ count: orgAssets.length, org: orgName });
548
+ for (const { name, type } of orgAssets) {
549
+ targets.push({
550
+ fromOrg: true,
551
+ kind: 'asset',
552
+ name,
553
+ org: orgName,
554
+ spec: formatOrgName(orgName, name),
555
+ type,
556
+ });
557
+ }
558
+ continue;
559
+ }
558
560
  const { name: parsedName, version } = parseNameVersion(spec);
559
561
  const { name: nameFromOrg, org: inlineOrg } = parseBundleSpec(parsedName);
560
562
  const name = inlineOrg ? nameFromOrg : parsedName;
561
563
  const org = resolveActiveOrg(inlineOrg ?? cliOrg, currentLockfile.org, config.org);
562
- const asset = findAsset(registry, name, undefined, undefined, org)?.asset;
563
- const bundle = findBundle(registry, name, inlineOrg) ??
564
- (asset ? undefined : (registry.bundles ?? []).find((b) => b.name === name));
565
- if (bundle) {
566
- bundles.push({ install: formatOrgName(bundle.org, bundle.name) + (version ? `@${version}` : ''), spec });
564
+ const indexBundle = findBundle(registry, name, inlineOrg);
565
+ if (indexBundle) {
566
+ const bundle = await bundleAtRequestedVersion(indexBundle, version, progress);
567
+ const earlier = targets.find((t) => t.kind === 'bundle' && t.bundle.name === name && t.bundle.org === indexBundle.org);
568
+ if (earlier) {
569
+ // A bundle installs at one version, as an asset does.
570
+ if (earlier.bundle.version !== bundle.version) {
571
+ throw new ResolverError(`Bundle "${formatOrgName(bundle.org, name)}" is named at two versions: ${earlier.bundle.version} ` +
572
+ `(${earlier.spec}) and ${bundle.version} (${spec}). A bundle installs at one version.`);
573
+ }
574
+ continue;
575
+ }
576
+ deprecationOf(name, isDeprecated(registry, name, undefined, indexBundle.org));
577
+ targets.push({ bundle, kind: 'bundle', spec, ...(version ? { version } : {}) });
567
578
  continue;
568
579
  }
569
- const deprecation = isDeprecated(registry, name, asset?.type, asset ? asset.org : inlineOrg);
570
- if (deprecation.deprecated && !force) {
571
- deprecated.push(`"${name}" (${deprecation.reason}${deprecation.replacement ? `; use ${deprecation.replacement}` : ''})`);
580
+ const asset = findAsset(registry, name, undefined, undefined, org)?.asset;
581
+ if (!asset && (registry.bundles ?? []).some((b) => b.name === name)) {
582
+ throw otherScopeBundleError(registry, name, inlineOrg);
572
583
  }
573
- requests.push({ name, ...(org ? { org } : {}), spec, ...(version ? { version } : {}) });
584
+ deprecationOf(name, isDeprecated(registry, name, asset?.type, asset ? asset.org : inlineOrg));
585
+ targets.push({ kind: 'asset', name, ...(org ? { org } : {}), spec, ...(version ? { version } : {}) });
574
586
  }
575
- if (bundles.length > 0) {
576
- throw new ResolverError(bundles.length === 1
577
- ? `${bundles[0].spec} is a bundle, which installs on its own.`
578
- : `${bundles.map((b) => b.spec).join(', ')} are bundles, which install on their own.`, { command: installCommandFor(bundles[0].install, flags) });
587
+ if (emptyOrgs.length > 0) {
588
+ throw new ResolverError(`No assets found for ${emptyOrgs.join(', ')}. Nothing was installed.`);
579
589
  }
580
590
  if (deprecated.length > 0) {
581
591
  throw new ResolverError(`Deprecated: ${deprecated.join(', ')}. Nothing was installed.`, {
@@ -584,10 +594,40 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
584
594
  }
585
595
  // A name every targeted tool already has in place is left out of the plan, as it is for one name.
586
596
  const assets = [];
597
+ const bundles = [];
587
598
  const upgrades = [];
588
599
  const toInstall = [];
589
- for (const request of requests) {
590
- const match = findAsset(registry, request.name, undefined, request.version, request.org);
600
+ const adoptCommandFor = ({ bundle, version }) => installCommandFor(formatOrgName(bundle.org, bundle.name) + (version ? `@${version}` : ''), {
601
+ adopt: true,
602
+ force: false,
603
+ pin: false,
604
+ projectRoot,
605
+ tool,
606
+ user,
607
+ });
608
+ for (const request of targets) {
609
+ if (request.kind === 'bundle') {
610
+ const { bundle, version } = request;
611
+ const members = bundleMembers(registry, bundle, tools);
612
+ const requiredBundleVersion = version && !isLatestAlias(version) ? bundle.version : undefined;
613
+ if (force ||
614
+ !isBundleInPlace(registry, bundle, currentLockfile, { members, projectDir, requiredBundleVersion, tools })) {
615
+ toInstall.push(request);
616
+ continue;
617
+ }
618
+ bundles.push({
619
+ name: bundle.name,
620
+ ...(bundle.org ? { org: bundle.org } : {}),
621
+ outcome: 'already-installed',
622
+ version: bundle.version,
623
+ ...bundleReport(membersOutsideBundle(currentLockfile, members, bundle), [], {
624
+ adoptCommand: adoptCommandFor(request),
625
+ bundle,
626
+ }),
627
+ });
628
+ continue;
629
+ }
630
+ const match = findAsset(registry, request.name, request.type, request.version, request.org);
591
631
  const existing = match
592
632
  ? findInstalledAsset(currentLockfile, request.name, match.asset.type, match.asset.org)
593
633
  : undefined;
@@ -615,9 +655,25 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
615
655
  });
616
656
  }
617
657
  progress('Resolving dependencies...');
618
- const plan = resolveMultipleInstallPlans(registry, toInstall.map(({ name, org, version }) => ({ name, ...(org ? { org } : {}), ...(version ? { version } : {}) })));
658
+ const plan = resolveTargetsInstallPlan(registry, toInstall.map((request) => request.kind === 'bundle'
659
+ ? { bundle: request.bundle, kind: 'bundle' }
660
+ : {
661
+ ...(request.fromOrg ? { fromOrg: true } : {}),
662
+ kind: 'asset',
663
+ name: request.name,
664
+ ...(request.org ? { org: request.org } : {}),
665
+ ...(request.type ? { type: request.type } : {}),
666
+ ...(request.version ? { version: request.version } : {}),
667
+ }));
668
+ // The named and the org's assets the plan installs, in plan order.
669
+ const requested = new Set(toInstall.flatMap((request) => {
670
+ if (request.kind === 'bundle')
671
+ return [];
672
+ const match = findAsset(registry, request.name, request.type, request.version, request.org);
673
+ return match ? [identityKey(match.asset)] : [];
674
+ }));
619
675
  const installed = plan.entries
620
- .filter((entry) => entry.reason === 'direct')
676
+ .filter((entry) => entry.reason === 'direct' && requested.has(identityKey(entry.asset)))
621
677
  .map((entry) => ({
622
678
  name: entry.asset.name,
623
679
  ...(entry.asset.org ? { org: entry.asset.org } : {}),
@@ -627,10 +683,36 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
627
683
  }));
628
684
  const report = [...installed, ...assets];
629
685
  const crossScope = (resolvedPlan, installedAt) => crossScopeOf(resolvedPlan, { installedAt, json: effectiveJson, scope: installScope, tools: toolNames });
686
+ // Each bundle in the plan, in the order named, then the ones left out; and the orgs.
687
+ const targetsField = (lockfile, planned) => {
688
+ const installedBundles = toInstall.flatMap((request) => {
689
+ if (request.kind !== 'bundle')
690
+ return [];
691
+ const { bundle } = request;
692
+ const members = bundleMembers(registry, bundle, tools);
693
+ // A dry run writes nothing, so the members it leaves outside the bundle are read from the plan.
694
+ const kept = [
695
+ ...membersOutsideBundle(lockfile, members, bundle),
696
+ ...(planned ? plannedOutsideBundle(plan, lockfile, members, bundle) : []),
697
+ ];
698
+ return [
699
+ {
700
+ name: bundle.name,
701
+ ...(bundle.org ? { org: bundle.org } : {}),
702
+ outcome: 'installed',
703
+ version: bundle.version,
704
+ ...bundleReport(kept, [], { adoptCommand: adoptCommandFor(request), bundle }),
705
+ },
706
+ ];
707
+ });
708
+ const named = [...installedBundles, ...bundles];
709
+ return { ...(named.length > 0 ? { bundles: named } : {}), ...(orgs.length > 0 ? { orgs } : {}) };
710
+ };
630
711
  // A dry run says so even when every name is in place, and writes nothing (not even an upgrade).
631
712
  if (dryRun) {
632
713
  return {
633
714
  assets: report,
715
+ ...targetsField(currentLockfile, true),
634
716
  outcome: 'dry-run-install',
635
717
  plan: withoutKeptDependencies(plan, currentLockfile, tools, {
636
718
  onProgress: progress,
@@ -659,11 +741,18 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
659
741
  lockfile = upgradeToDirectInstall(lockfile, name, type, org);
660
742
  await writeLockfile(projectDir, lockfile);
661
743
  }
662
- if (plan.entries.length === 0)
663
- return { assets: report, outcome: 'already-installed' };
664
- return { assets: report, outcome: 'installed', toolResults, ...(await crossScope(plan, projectDir)) };
744
+ if (plan.entries.length === 0) {
745
+ return { assets: report, ...targetsField(currentLockfile, false), outcome: 'already-installed' };
746
+ }
747
+ return {
748
+ assets: report,
749
+ ...targetsField(await readLockfile(projectDir), false),
750
+ outcome: 'installed',
751
+ toolResults,
752
+ ...(await crossScope(plan, projectDir)),
753
+ };
665
754
  });
666
- }, [adopt, cliOrg, dryRun, force, pin, specs.join(' '), tool]);
755
+ }, [adopt, all, cliOrg, dryRun, force, pin, specs.join(' '), tool]);
667
756
  if (state.status === 'loading') {
668
757
  return _jsx(Spinner, { message: state.message });
669
758
  }
@@ -686,10 +775,10 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
686
775
  return renderJson({ ...data, ...fields });
687
776
  }
688
777
  if (data.outcome === 'dry-run-install') {
689
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), _jsx(PlanEntryLines, { plan: data.plan }), _jsx(NamedAssetLines, { assets: data.assets, dryRun: true, where: label }), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
778
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), _jsx(PlanEntryLines, { plan: data.plan }), _jsx(NamedAssetLines, { assets: data.assets, dryRun: true, where: label }), _jsx(TargetLines, { bundles: data.bundles, dryRun: true, orgs: data.orgs, where: label }), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
690
779
  }
691
780
  return (_jsxs(Box, { flexDirection: 'column', children: [data.outcome === 'installed' &&
692
- data.toolResults.map(({ results, skipped, tool: toolName, warnings }) => (_jsx(InstallSummary, { results: results, skipped: skipped, tools: [toolName], warnings: warnings, where: label }, toolName))), _jsx(NamedAssetLines, { assets: data.assets, where: label }), data.outcome === 'installed' && _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
781
+ data.toolResults.map(({ results, skipped, tool: toolName, warnings }) => (_jsx(InstallSummary, { results: results, skipped: skipped, tools: [toolName], warnings: warnings, where: label }, toolName))), _jsx(NamedAssetLines, { assets: data.assets, where: label }), _jsx(TargetLines, { bundles: data.bundles, orgs: data.orgs, where: label }), data.outcome === 'installed' && _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
693
782
  }
694
783
  /** `atk install <spec>` with the run's own flags, for a command an error suggests. */
695
784
  function installCommandFor(spec, { adopt, dryRun, force, pin, projectRoot, tool, user }) {
@@ -953,6 +1042,31 @@ function InstallOrg({ cliOrg, dryRun, force, json, orgName, pin, projectRoot, sk
953
1042
  }
954
1043
  return null;
955
1044
  }
1045
+ /**
1046
+ * Whether every member of the bundle is in place for every tool that takes it (with
1047
+ * `requiredBundleVersion`, recorded as this bundle at that version). False when no tool takes any
1048
+ * member, so the installer says why.
1049
+ */
1050
+ function isBundleInPlace(registry, bundle, lockfile, { members, projectDir, requiredBundleVersion, tools, }) {
1051
+ const bundleAssets = bundle.assets ?? [];
1052
+ return tools.every(({ adapter: toolAdapter }) => members.length > 0 &&
1053
+ bundleAssets.every((ref) => {
1054
+ const found = findBundleMember(registry, bundle, ref);
1055
+ if (!found)
1056
+ return false;
1057
+ // A tool that doesn't take this member (its type, or a hook for other tools) has
1058
+ // nothing to check.
1059
+ if (!placesAsset(toolAdapter, found.asset.type, found.versionData.tools))
1060
+ return true;
1061
+ const installed = findInstalledAsset(lockfile, ref.name, ref.type, found.asset.org);
1062
+ if (!installed)
1063
+ return false;
1064
+ if (requiredBundleVersion && !isFromBundleVersion(installed, bundle, requiredBundleVersion)) {
1065
+ return false;
1066
+ }
1067
+ return isInstalledForTool(installed, toolAdapter, projectDir);
1068
+ }));
1069
+ }
956
1070
  /** Whether a lockfile asset was installed as part of this bundle (name, org) at this version. */
957
1071
  function isFromBundleVersion(installed, bundle, version) {
958
1072
  const origin = installed.bundleOrigin;
@@ -986,8 +1100,46 @@ function NamedAssetLines({ assets, dryRun = false, where, }) {
986
1100
  ? `${dryRun ? 'would become' : 'is now'} a direct install${inScope}.`
987
1101
  : `is already installed${inScope}.`] }, identityKey(a)))) }));
988
1102
  }
1103
+ /**
1104
+ * The error for a bundle name that only another scope has, when no asset can claim the name
1105
+ * either, instead of a misleading "asset not found".
1106
+ */
1107
+ function otherScopeBundleError(registry, name, inlineOrg) {
1108
+ if (inlineOrg) {
1109
+ return new Error(`Bundle '${name}' not found in org '${inlineOrg}'.`);
1110
+ }
1111
+ const orgScopes = (registry.bundles ?? []).filter((b) => b.name === name && b.org).map((b) => `@${b.org}/${name}`);
1112
+ return new Error(`Bundle '${name}' not found in the registry.` +
1113
+ (orgScopes.length > 0 ? ` Did you mean ${orgScopes.join(' or ')}?` : ''));
1114
+ }
989
1115
  /** A plan's entries, each with its type and version, its dependencies marked. */
990
1116
  function PlanEntryLines({ plan }) {
991
1117
  return (_jsx(_Fragment, { children: plan.entries.map((entry) => (_jsxs(Text, { children: [' ', entry.reason === 'dependency' ? (_jsxs(Text, { color: 'cyan', children: [formatOrgName(entry.asset.org, entry.asset.name), ' ', _jsxs(Text, { dimColor: true, children: ["(", entry.asset.type, ") v", entry.version, " \u2014 dependency"] })] })) : (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: formatOrgName(entry.asset.org, entry.asset.name) }), _jsxs(Text, { dimColor: true, children: [' ', "(", entry.asset.type, ") v", entry.version] })] }))] }, identityKey(entry.asset)))) }));
992
1118
  }
1119
+ /**
1120
+ * The members not installed yet that the plan installs without the bundle's origin: named on their
1121
+ * own, or with another bundle named before it. A dry run reports them as an install would.
1122
+ */
1123
+ function plannedOutsideBundle(plan, lockfile, members, bundle) {
1124
+ return members.flatMap((member) => {
1125
+ if (findInstalledAsset(lockfile, member.name, member.type, member.org))
1126
+ return [];
1127
+ const entry = plan.entries.find((e) => identityKey(e.asset) === identityKey(member));
1128
+ const origin = entry?.bundleOrigin;
1129
+ if (!entry || (origin?.name === bundle.name && origin.org === bundle.org))
1130
+ return [];
1131
+ return [{ ...(origin ? { bundle: origin } : {}), ...member }];
1132
+ });
1133
+ }
1134
+ /**
1135
+ * The bundles and orgs a run of several names had among them: what it did with each bundle, the
1136
+ * bundle's members outside it, and how many assets each org brought.
1137
+ */
1138
+ function TargetLines({ bundles = [], dryRun = false, orgs = [], where, }) {
1139
+ const inScope = where ? ` in ${where}` : '';
1140
+ return (_jsxs(_Fragment, { children: [bundles.map((bundle) => {
1141
+ const spec = formatOrgName(bundle.org, bundle.name);
1142
+ return (_jsxs(Fragment, { children: [bundle.outcome === 'already-installed' ? (_jsxs(Text, { children: [_jsx(Text, { color: 'yellow', children: "!" }), " Bundle ", _jsx(Text, { bold: true, children: spec }), " is already fully installed (v", bundle.version, ")", inScope, "."] })) : (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: "\u2713" }), " Bundle ", _jsx(Text, { bold: true, children: spec }), " v", bundle.version, ' ', dryRun ? 'would be installed' : 'installed', inScope, "."] })), _jsx(BundleReportLines, { bundleName: spec, dryRun: dryRun, report: bundle })] }, spec));
1143
+ }), orgs.map(({ count, org }) => (_jsxs(Text, { children: [_jsxs(Text, { bold: true, color: 'magenta', children: ["Org: @", org] }), _jsxs(Text, { dimColor: true, children: [' ', "\u2014 all ", count, " asset", count !== 1 ? 's' : ''] })] }, org)))] }));
1144
+ }
993
1145
  //# sourceMappingURL=install.js.map