@detergent-software/atk 9.2.0-dev.4 → 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.
@@ -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()
@@ -108,7 +109,8 @@ export const options = z.object({
108
109
  description: 'Alias for --all: install every asset from an org scope without prompting',
109
110
  })),
110
111
  });
111
- export const args = z.tuple([
112
+ export const args = z
113
+ .tuple([
112
114
  z
113
115
  .string()
114
116
  .optional()
@@ -116,12 +118,19 @@ export const args = z.tuple([
116
118
  description: 'Asset, bundle, or org scope to install (supports name@version and @org)',
117
119
  name: 'target',
118
120
  })),
119
- ]);
120
- export default function Install({ args: [rawName], options: { adopt, all, dryRun, force, json, org, pin, projectRoot, refresh, tool, user, yes }, }) {
121
+ ])
122
+ .rest(z.string().describe(argument({
123
+ description: 'More assets, bundles or org scopes to install with the first, as one plan (name@version, @org/name, @org with --all)',
124
+ name: 'targets',
125
+ })));
126
+ export default function Install({ args: [rawName, ...more], options: { adopt, all, dryRun, force, json, org, pin, projectRoot, refresh, tool, user, yes }, }) {
121
127
  // No argument provided — delegate to sync behavior
122
128
  if (!rawName) {
123
129
  return _jsx(Sync, { options: { dryRun, force, json, projectRoot, refresh, tool, user } });
124
130
  }
131
+ if (more.length > 0) {
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 }));
133
+ }
125
134
  if (isBareOrgScope(rawName)) {
126
135
  const orgName = rawName.slice(1);
127
136
  return (_jsx(InstallOrg, { cliOrg: org, dryRun: dryRun, force: force, json: json, orgName: orgName, pin: pin, projectRoot: projectRoot, skipPicker: all || yes, tool: tool, user: user }));
@@ -265,16 +274,8 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
265
274
  // through to a misleading "asset not found".
266
275
  if (!indexBundle && (registry.bundles ?? []).some((b) => b.name === name)) {
267
276
  const assetExists = findAsset(registry, name, undefined, undefined, org);
268
- if (!assetExists) {
269
- if (inlineOrg) {
270
- throw new Error(`Bundle '${name}' not found in org '${inlineOrg}'.`);
271
- }
272
- const orgScopes = (registry.bundles ?? [])
273
- .filter((b) => b.name === name && b.org)
274
- .map((b) => `@${b.org}/${name}`);
275
- throw new Error(`Bundle '${name}' not found in the registry.` +
276
- (orgScopes.length > 0 ? ` Did you mean ${orgScopes.join(' or ')}?` : ''));
277
- }
277
+ if (!assetExists)
278
+ throw otherScopeBundleError(registry, name, inlineOrg);
278
279
  }
279
280
  // With no active org, `info` opens the only org asset of a bare name, but install doesn't
280
281
  // switch scopes on its own: it names the spec that installs that asset.
@@ -352,26 +353,7 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
352
353
  };
353
354
  // Bundle installation flow — check if already installed for ALL tools
354
355
  if (!force) {
355
- const bundleAssets = bundle.assets ?? [];
356
- // When no configured tool takes any member, fall through so the installer says why.
357
- const allInstalledForAllTools = tools.every(({ adapter: toolAdapter }) => members.length > 0 &&
358
- bundleAssets.every((ref) => {
359
- const found = findBundleMember(registry, bundle, ref);
360
- if (!found)
361
- return false;
362
- // A tool that doesn't take this member (its type, or a hook for other tools) has
363
- // nothing to check.
364
- if (!placesAsset(toolAdapter, found.asset.type, found.versionData.tools))
365
- return true;
366
- const installed = findInstalledAsset(before, ref.name, ref.type, found.asset.org);
367
- if (!installed)
368
- return false;
369
- if (requiredBundleVersion && !isFromBundleVersion(installed, bundle, requiredBundleVersion)) {
370
- return false;
371
- }
372
- return isInstalledForTool(installed, toolAdapter, projectDir);
373
- }));
374
- if (allInstalledForAllTools) {
356
+ if (isBundleInPlace(registry, bundle, before, { members, projectDir, requiredBundleVersion, tools })) {
375
357
  // Adopting a bundle that is in place only records its origin on the members with none:
376
358
  // their files, versions and pins stay as they are. Another bundle's member stays its.
377
359
  const outside = membersOutsideBundle(before, members, bundle);
@@ -502,16 +484,302 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
502
484
  return (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { color: 'green', children: ["\u2713 ", _jsx(Text, { bold: true, children: data.name }), " (v", data.version, ") is now a direct install", where, "."] }), _jsx(Text, { dimColor: true, children: "It will no longer be auto-removed when its parent is uninstalled." })] }));
503
485
  }
504
486
  if (data.outcome === 'dry-run-install') {
505
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), data.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)))), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
487
+ 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(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
506
488
  }
507
489
  if (data.outcome === 'dry-run-bundle') {
508
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsxs(Text, { bold: true, color: 'magenta', children: ["Bundle: ", data.bundleName] }), _jsxs(Text, { dimColor: true, children: [" v", data.version] })] }), _jsx(Text, { children: " " }), data.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)))), _jsx(BundleReportLines, { bundleName: data.bundleName, dryRun: true, report: data }), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
490
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsxs(Text, { bold: true, color: 'magenta', children: ["Bundle: ", data.bundleName] }), _jsxs(Text, { dimColor: true, children: [" v", data.version] })] }), _jsx(Text, { children: " " }), _jsx(PlanEntryLines, { plan: data.plan }), _jsx(BundleReportLines, { bundleName: data.bundleName, dryRun: true, report: data }), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
509
491
  }
510
492
  if (data.outcome === 'deprecated') {
511
493
  return (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { color: 'yellow', children: ["Warning: \"", data.name, "\" is deprecated \u2014 ", data.reason] }), data.replacement && (_jsxs(Text, { children: ["Consider using", ' ', _jsx(Text, { bold: true, color: 'cyan', children: data.replacement }), ' ', "instead."] })), _jsx(Text, { dimColor: true, children: "Use --force to install anyway." })] }));
512
494
  }
513
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 ?? [] })] }));
514
496
  }
497
+ /**
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}).
505
+ */
506
+ function InstallAssets({ adopt, all, dryRun, force, json, org: cliOrg, pin, projectRoot, specs, tool, user, }) {
507
+ const { json: effectiveJson } = useNonInteractive(json);
508
+ const { fields, resolve, resolved } = useResolvedScope({ projectRoot, user });
509
+ const state = useCommand(async (progress) => {
510
+ const flags = { dryRun, force, pin, projectRoot, tool, user };
511
+ if (adopt) {
512
+ throw new Error('--adopt records a bundle as the owner of its members, so it works with one bundle only.');
513
+ }
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) });
521
+ }
522
+ const { root: projectDir, scope: installScope } = await resolve();
523
+ progress('Loading configuration...');
524
+ const config = await loadConfig();
525
+ const tools = await resolvePlacingTools(tool, projectDir);
526
+ const toolNames = tools.map((t) => t.tool);
527
+ progress('Fetching registry...');
528
+ const registry = await fetchRegistry({ force: true });
529
+ const currentLockfile = await readLockfile(projectDir);
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.
532
+ const deprecated = [];
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
+ }
560
+ const { name: parsedName, version } = parseNameVersion(spec);
561
+ const { name: nameFromOrg, org: inlineOrg } = parseBundleSpec(parsedName);
562
+ const name = inlineOrg ? nameFromOrg : parsedName;
563
+ const org = resolveActiveOrg(inlineOrg ?? cliOrg, currentLockfile.org, config.org);
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 } : {}) });
578
+ continue;
579
+ }
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);
583
+ }
584
+ deprecationOf(name, isDeprecated(registry, name, asset?.type, asset ? asset.org : inlineOrg));
585
+ targets.push({ kind: 'asset', name, ...(org ? { org } : {}), spec, ...(version ? { version } : {}) });
586
+ }
587
+ if (emptyOrgs.length > 0) {
588
+ throw new ResolverError(`No assets found for ${emptyOrgs.join(', ')}. Nothing was installed.`);
589
+ }
590
+ if (deprecated.length > 0) {
591
+ throw new ResolverError(`Deprecated: ${deprecated.join(', ')}. Nothing was installed.`, {
592
+ hint: 'Use --force to install them anyway.',
593
+ });
594
+ }
595
+ // A name every targeted tool already has in place is left out of the plan, as it is for one name.
596
+ const assets = [];
597
+ const bundles = [];
598
+ const upgrades = [];
599
+ const toInstall = [];
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);
631
+ const existing = match
632
+ ? findInstalledAsset(currentLockfile, request.name, match.asset.type, match.asset.org)
633
+ : undefined;
634
+ const supportingTools = existing && match ? tools.filter((t) => placesAsset(t.adapter, existing.type, match.versionData.tools)) : [];
635
+ const inPlace = !force &&
636
+ existing !== undefined &&
637
+ supportingTools.length > 0 &&
638
+ supportingTools.every((t) => isInstalledForTool(existing, t.adapter, projectDir)) &&
639
+ (!request.version || request.version === existing.version);
640
+ if (!inPlace) {
641
+ toInstall.push(request);
642
+ continue;
643
+ }
644
+ if (assets.some((a) => identityKey(a) === identityKey(existing)))
645
+ continue;
646
+ const dependency = (existing.installReason ?? 'direct') === 'dependency';
647
+ if (dependency)
648
+ upgrades.push(existing);
649
+ assets.push({
650
+ name: existing.name,
651
+ ...(existing.org ? { org: existing.org } : {}),
652
+ outcome: dependency ? 'upgraded-to-direct' : 'already-installed',
653
+ type: existing.type,
654
+ version: existing.version,
655
+ });
656
+ }
657
+ progress('Resolving dependencies...');
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
+ }));
675
+ const installed = plan.entries
676
+ .filter((entry) => entry.reason === 'direct' && requested.has(identityKey(entry.asset)))
677
+ .map((entry) => ({
678
+ name: entry.asset.name,
679
+ ...(entry.asset.org ? { org: entry.asset.org } : {}),
680
+ outcome: 'installed',
681
+ type: entry.asset.type,
682
+ version: entry.version,
683
+ }));
684
+ const report = [...installed, ...assets];
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
+ };
711
+ // A dry run says so even when every name is in place, and writes nothing (not even an upgrade).
712
+ if (dryRun) {
713
+ return {
714
+ assets: report,
715
+ ...targetsField(currentLockfile, true),
716
+ outcome: 'dry-run-install',
717
+ plan: withoutKeptDependencies(plan, currentLockfile, tools, {
718
+ onProgress: progress,
719
+ projectDir,
720
+ scope: installScope,
721
+ }),
722
+ tools: toolNames,
723
+ ...(await crossScope(plan)),
724
+ };
725
+ }
726
+ return await withLockfileErrorHandling(projectDir, async () => {
727
+ const toolResults = plan.entries.length > 0
728
+ ? await executeInstallPlanForTools(plan, tools, {
729
+ force,
730
+ onProgress: progress,
731
+ onTool: (toolName) => progress(`Installing for ${toolName}...`),
732
+ pin,
733
+ projectDir,
734
+ scope: installScope,
735
+ })
736
+ : [];
737
+ // After the install, so a failed one leaves even the install reasons as they were.
738
+ if (upgrades.length > 0) {
739
+ let lockfile = await readLockfile(projectDir);
740
+ for (const { name, org, type } of upgrades)
741
+ lockfile = upgradeToDirectInstall(lockfile, name, type, org);
742
+ await writeLockfile(projectDir, lockfile);
743
+ }
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
+ };
754
+ });
755
+ }, [adopt, all, cliOrg, dryRun, force, pin, specs.join(' '), tool]);
756
+ if (state.status === 'loading') {
757
+ return _jsx(Spinner, { message: state.message });
758
+ }
759
+ if (state.status === 'error') {
760
+ if (effectiveJson) {
761
+ return renderJson({
762
+ ...(state.code ? { code: state.code } : {}),
763
+ ...(state.command ? { command: state.command } : {}),
764
+ ...(state.hint ? { hint: state.hint } : {}),
765
+ message: state.message,
766
+ outcome: 'error',
767
+ ...fields,
768
+ });
769
+ }
770
+ return _jsx(ErrorView, { command: state.command, hint: state.hint, message: state.message });
771
+ }
772
+ const { data } = state;
773
+ const label = resolved && scopeLabel(resolved);
774
+ if (effectiveJson) {
775
+ return renderJson({ ...data, ...fields });
776
+ }
777
+ if (data.outcome === 'dry-run-install') {
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 ?? [] })] }));
779
+ }
780
+ return (_jsxs(Box, { flexDirection: 'column', children: [data.outcome === 'installed' &&
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 ?? [] })] }));
782
+ }
515
783
  /** `atk install <spec>` with the run's own flags, for a command an error suggests. */
516
784
  function installCommandFor(spec, { adopt, dryRun, force, pin, projectRoot, tool, user }) {
517
785
  return [
@@ -758,7 +1026,7 @@ function InstallOrg({ cliOrg, dryRun, force, json, orgName, pin, projectRoot, sk
758
1026
  ...fields,
759
1027
  });
760
1028
  }
761
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: state.plan.totalCount, tools: state.tools, where: label }), _jsx(Text, { children: " " }), _jsx(Text, { children: _jsxs(Text, { bold: true, color: 'magenta', children: ["Org: @", orgName] }) }), _jsx(Text, { children: " " }), state.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)))), _jsx(CrossScopeWarning, { installs: state.crossScope ?? [] })] }));
1029
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: state.plan.totalCount, tools: state.tools, where: label }), _jsx(Text, { children: " " }), _jsx(Text, { children: _jsxs(Text, { bold: true, color: 'magenta', children: ["Org: @", orgName] }) }), _jsx(Text, { children: " " }), _jsx(PlanEntryLines, { plan: state.plan }), _jsx(CrossScopeWarning, { installs: state.crossScope ?? [] })] }));
762
1030
  }
763
1031
  if (state.phase === 'done') {
764
1032
  if (effectiveJson) {
@@ -774,6 +1042,31 @@ function InstallOrg({ cliOrg, dryRun, force, json, orgName, pin, projectRoot, sk
774
1042
  }
775
1043
  return null;
776
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
+ }
777
1070
  /** Whether a lockfile asset was installed as part of this bundle (name, org) at this version. */
778
1071
  function isFromBundleVersion(installed, bundle, version) {
779
1072
  const origin = installed.bundleOrigin;
@@ -798,4 +1091,55 @@ function membersOutsideBundle(lockfile, members, bundle) {
798
1091
  return [{ ...(origin ? { bundle: origin } : {}), ...member }];
799
1092
  });
800
1093
  }
1094
+ /** The names a run of several left out of its plan: already installed, or made a direct install. */
1095
+ function NamedAssetLines({ assets, dryRun = false, where, }) {
1096
+ const inScope = where ? ` in ${where}` : '';
1097
+ return (_jsx(_Fragment, { children: assets
1098
+ .filter((a) => a.outcome !== 'installed')
1099
+ .map((a) => (_jsxs(Text, { children: [a.outcome === 'upgraded-to-direct' ? _jsx(Text, { color: 'green', children: "\u2713" }) : _jsx(Text, { color: 'yellow', children: "!" }), ' ', _jsx(Text, { bold: true, children: formatOrgName(a.org, a.name) }), " (v", a.version, ")", ' ', a.outcome === 'upgraded-to-direct'
1100
+ ? `${dryRun ? 'would become' : 'is now'} a direct install${inScope}.`
1101
+ : `is already installed${inScope}.`] }, identityKey(a)))) }));
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
+ }
1115
+ /** A plan's entries, each with its type and version, its dependencies marked. */
1116
+ function PlanEntryLines({ plan }) {
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)))) }));
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
+ }
801
1145
  //# sourceMappingURL=install.js.map