@pi-stef/catalog 0.2.2
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 +205 -0
- package/extensions/catalog.ts +7 -0
- package/package.json +42 -0
- package/src/catalog/crud.ts +143 -0
- package/src/catalog/install.ts +181 -0
- package/src/catalog/ratings.ts +32 -0
- package/src/catalog/reconcile.ts +339 -0
- package/src/catalog/source.ts +173 -0
- package/src/commands/add.ts +135 -0
- package/src/commands/definitions.ts +78 -0
- package/src/commands/diff.ts +158 -0
- package/src/commands/dispatch.ts +102 -0
- package/src/commands/init.ts +127 -0
- package/src/commands/login.ts +105 -0
- package/src/commands/profiles.ts +147 -0
- package/src/commands/remove.ts +90 -0
- package/src/commands/status.ts +142 -0
- package/src/commands/sync.ts +406 -0
- package/src/commands/toggle.ts +147 -0
- package/src/commands/types.ts +38 -0
- package/src/commands/verify.ts +107 -0
- package/src/config/io.ts +82 -0
- package/src/config/paths.ts +44 -0
- package/src/config/schema.ts +87 -0
- package/src/index.ts +94 -0
- package/src/profiles/manager.ts +159 -0
- package/src/register.ts +285 -0
- package/src/sync/auth.ts +109 -0
- package/src/sync/cache.ts +40 -0
- package/src/sync/gist.ts +253 -0
- package/src/sync/pull.ts +76 -0
- package/src/sync/push.ts +78 -0
- package/src/update/pi-update.ts +60 -0
- package/src/update/registry.ts +27 -0
- package/src/update/self-update.ts +60 -0
- package/src/update/semver.ts +38 -0
- package/src/update/types.ts +21 -0
- package/src/update/update-cache.ts +54 -0
- package/src/util/errors.ts +144 -0
- package/src/util/exec.ts +160 -0
package/src/config/io.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
|
|
4
|
+
import { catalogFile, lockFile, ensureCatalogDir } from "./paths.js";
|
|
5
|
+
import { CatalogYamlSchema, LockFileSchema } from "./schema.js";
|
|
6
|
+
import type { CatalogYaml, LockFile } from "./schema.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Empty defaults
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const EMPTY_CATALOG: CatalogYaml = {
|
|
13
|
+
meta: { pi_version: "0.0.0" },
|
|
14
|
+
packages: {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const EMPTY_LOCK: LockFile = { packages: {} };
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// cat.yaml I/O
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read and parse `cat.yaml`. Returns an empty catalog when the file is
|
|
25
|
+
* missing or empty. Throws on malformed YAML or schema-invalid content.
|
|
26
|
+
*/
|
|
27
|
+
export function readCatalog(home?: string): CatalogYaml {
|
|
28
|
+
const filePath = catalogFile(home);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(filePath)) {
|
|
31
|
+
return structuredClone(EMPTY_CATALOG);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
35
|
+
if (raw.trim() === "") {
|
|
36
|
+
return structuredClone(EMPTY_CATALOG);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parsed = yaml.load(raw);
|
|
40
|
+
return CatalogYamlSchema.parse(parsed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Serialize and write `cat.yaml`, creating the catalog directory if needed.
|
|
45
|
+
*/
|
|
46
|
+
export function writeCatalog(catalog: CatalogYaml, home?: string): void {
|
|
47
|
+
ensureCatalogDir(home);
|
|
48
|
+
const filePath = catalogFile(home);
|
|
49
|
+
const content = yaml.dump(catalog);
|
|
50
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// catalog.lock.json I/O
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read and parse `catalog.lock.json`. Returns an empty lock when the file
|
|
59
|
+
* is missing. Throws on malformed JSON or schema-invalid content.
|
|
60
|
+
*/
|
|
61
|
+
export function readLock(home?: string): LockFile {
|
|
62
|
+
const filePath = lockFile(home);
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(filePath)) {
|
|
65
|
+
return structuredClone(EMPTY_LOCK);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
return LockFileSchema.parse(parsed);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Serialize and write `catalog.lock.json`, creating the catalog directory
|
|
75
|
+
* if needed.
|
|
76
|
+
*/
|
|
77
|
+
export function writeLock(lock: LockFile, home?: string): void {
|
|
78
|
+
ensureCatalogDir(home);
|
|
79
|
+
const filePath = lockFile(home);
|
|
80
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
81
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
82
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { globalDir } from "@pi-stef/paths";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ~/.pi/sf/catalog/
|
|
8
|
+
*/
|
|
9
|
+
export function catalogDir(home?: string): string {
|
|
10
|
+
return globalDir("catalog", home);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ~/.pi/sf/catalog/cat.yaml
|
|
15
|
+
*/
|
|
16
|
+
export function catalogFile(home?: string): string {
|
|
17
|
+
return path.join(catalogDir(home), "cat.yaml");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ~/.pi/sf/catalog/catalog.lock.json
|
|
22
|
+
*/
|
|
23
|
+
export function lockFile(home?: string): string {
|
|
24
|
+
return path.join(catalogDir(home), "catalog.lock.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates the catalog directory if it does not already exist.
|
|
29
|
+
*/
|
|
30
|
+
export function ensureCatalogDir(home?: string): void {
|
|
31
|
+
const dir = catalogDir(home);
|
|
32
|
+
if (!fs.existsSync(dir)) {
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ~/.pi/agent/npm/node_modules
|
|
39
|
+
*
|
|
40
|
+
* Centralised so the npm node_modules path is not hardcoded in consumers.
|
|
41
|
+
*/
|
|
42
|
+
export function npmNodeModulesDir(home?: string): string {
|
|
43
|
+
return path.join(home ?? os.homedir(), ".pi", "agent", "npm", "node_modules");
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// cat.yaml schema
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/** Type discriminator for a catalog package entry. */
|
|
8
|
+
export const PackageType = z.enum(["skill", "pi-native"]);
|
|
9
|
+
|
|
10
|
+
/** Rating values for catalog packages. */
|
|
11
|
+
export const Rating = z.enum(["core", "useful", "debatable", "disabled"]);
|
|
12
|
+
|
|
13
|
+
/** A single package entry inside cat.yaml. */
|
|
14
|
+
export const CatalogPackageSchema = z.object({
|
|
15
|
+
/** Where to fetch the package from (URL, path, etc.). */
|
|
16
|
+
source: z.string().min(1),
|
|
17
|
+
/** User-assigned rating. */
|
|
18
|
+
rating: Rating,
|
|
19
|
+
/** Optional type discriminator. */
|
|
20
|
+
type: PackageType.optional(),
|
|
21
|
+
/** Optional profile name this package belongs to. */
|
|
22
|
+
profile: z.string().optional(),
|
|
23
|
+
/** Whether the package is active. Defaults to true when absent. */
|
|
24
|
+
enabled: z.boolean().optional(),
|
|
25
|
+
/** Previous rating before disable; used by enablePackage to restore. */
|
|
26
|
+
previousRating: Rating.optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** The meta section at the top of cat.yaml. */
|
|
30
|
+
export const CatalogMetaSchema = z.object({
|
|
31
|
+
/** Minimum pi version this catalog requires. */
|
|
32
|
+
pi_version: z.string().min(1),
|
|
33
|
+
/** Currently active profile name. */
|
|
34
|
+
activeProfile: z.string().optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** A single named profile with its own package overrides. */
|
|
38
|
+
export const ProfileSchema = z.object({
|
|
39
|
+
/** Packages specific to this profile (override base packages). */
|
|
40
|
+
packages: z.record(z.string(), CatalogPackageSchema),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Full cat.yaml document schema. */
|
|
44
|
+
export const CatalogYamlSchema = z.object({
|
|
45
|
+
meta: CatalogMetaSchema,
|
|
46
|
+
packages: z.record(z.string(), CatalogPackageSchema),
|
|
47
|
+
/** Named profiles with package overrides. */
|
|
48
|
+
profiles: z.record(z.string(), ProfileSchema).optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// catalog.lock.json schema
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/** Possible sync states for a locked package. */
|
|
56
|
+
export const SyncState = z.enum(["synced", "outdated", "conflict"]);
|
|
57
|
+
|
|
58
|
+
/** A single package entry inside catalog.lock.json. */
|
|
59
|
+
export const LockPackageSchema = z.object({
|
|
60
|
+
/** Installed version string. */
|
|
61
|
+
version: z.string().min(1),
|
|
62
|
+
/** Deterministic hash derived from the source specifier (not installed file contents). */
|
|
63
|
+
sourceHash: z.string().min(1),
|
|
64
|
+
/** ISO-8601 timestamp of when the package was installed. */
|
|
65
|
+
installedAt: z.string().min(1),
|
|
66
|
+
/** Current synchronization state relative to the source. */
|
|
67
|
+
syncState: SyncState,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Full catalog.lock.json document schema. */
|
|
71
|
+
export const LockFileSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
packages: z.record(z.string(), LockPackageSchema),
|
|
74
|
+
})
|
|
75
|
+
.passthrough();
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Inferred TypeScript types
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export type CatalogYaml = z.infer<typeof CatalogYamlSchema>;
|
|
82
|
+
export type CatalogMeta = z.infer<typeof CatalogMetaSchema>;
|
|
83
|
+
export type CatalogPackage = z.infer<typeof CatalogPackageSchema>;
|
|
84
|
+
export type Profile = z.infer<typeof ProfileSchema>;
|
|
85
|
+
export type LockFile = z.infer<typeof LockFileSchema>;
|
|
86
|
+
export type LockPackage = z.infer<typeof LockPackageSchema>;
|
|
87
|
+
export type RatingValue = z.infer<typeof Rating>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// catalog
|
|
2
|
+
export { scanInstalled, type InstalledPackage, type InstalledMap } from "./catalog/install.js";
|
|
3
|
+
export {
|
|
4
|
+
reconcile,
|
|
5
|
+
executeActions,
|
|
6
|
+
type CatalogEntry,
|
|
7
|
+
type InstallAction,
|
|
8
|
+
type UninstallAction,
|
|
9
|
+
type UpgradeAction,
|
|
10
|
+
type OrphanReport,
|
|
11
|
+
type ReconcilePlan,
|
|
12
|
+
type ReconcileOptions,
|
|
13
|
+
type ActionError,
|
|
14
|
+
type ExecuteResult,
|
|
15
|
+
type ExecuteOptions,
|
|
16
|
+
} from "./catalog/reconcile.js";
|
|
17
|
+
export {
|
|
18
|
+
extractNpmName,
|
|
19
|
+
extractNpmVersion,
|
|
20
|
+
cleanGitName,
|
|
21
|
+
parseSource,
|
|
22
|
+
sourceToKey,
|
|
23
|
+
extractVersionFromSource,
|
|
24
|
+
type ParsedSource,
|
|
25
|
+
} from "./catalog/source.js";
|
|
26
|
+
|
|
27
|
+
// config
|
|
28
|
+
export {
|
|
29
|
+
catalogDir,
|
|
30
|
+
catalogFile,
|
|
31
|
+
lockFile,
|
|
32
|
+
ensureCatalogDir,
|
|
33
|
+
npmNodeModulesDir,
|
|
34
|
+
} from "./config/paths.js";
|
|
35
|
+
export {
|
|
36
|
+
PackageType,
|
|
37
|
+
CatalogPackageSchema,
|
|
38
|
+
CatalogMetaSchema,
|
|
39
|
+
CatalogYamlSchema,
|
|
40
|
+
ProfileSchema,
|
|
41
|
+
SyncState,
|
|
42
|
+
LockPackageSchema,
|
|
43
|
+
LockFileSchema,
|
|
44
|
+
type CatalogYaml,
|
|
45
|
+
type CatalogMeta,
|
|
46
|
+
type CatalogPackage,
|
|
47
|
+
type LockFile,
|
|
48
|
+
type LockPackage,
|
|
49
|
+
} from "./config/schema.js";
|
|
50
|
+
|
|
51
|
+
// sync
|
|
52
|
+
export { checkAuth, getToken } from "./sync/auth.js";
|
|
53
|
+
export {
|
|
54
|
+
createGist,
|
|
55
|
+
readGist,
|
|
56
|
+
updateGist,
|
|
57
|
+
findGistByDescription,
|
|
58
|
+
_resetOctokit,
|
|
59
|
+
type GistFiles,
|
|
60
|
+
type GistResult,
|
|
61
|
+
type GistSummary,
|
|
62
|
+
} from "./sync/gist.js";
|
|
63
|
+
export {
|
|
64
|
+
gistCachePath,
|
|
65
|
+
readCachedGistId,
|
|
66
|
+
writeCachedGistId,
|
|
67
|
+
} from "./sync/cache.js";
|
|
68
|
+
export { pullCatalog, type PullResult } from "./sync/pull.js";
|
|
69
|
+
export { pushCatalog, type PushResult } from "./sync/push.js";
|
|
70
|
+
|
|
71
|
+
// util
|
|
72
|
+
export {
|
|
73
|
+
execCommand,
|
|
74
|
+
piInstall,
|
|
75
|
+
piUninstall,
|
|
76
|
+
ExecError,
|
|
77
|
+
type ExecOptions,
|
|
78
|
+
type ExecResult,
|
|
79
|
+
type PiExecOptions,
|
|
80
|
+
} from "./util/exec.js";
|
|
81
|
+
|
|
82
|
+
// update
|
|
83
|
+
export { checkSelfUpdate } from "./update/self-update.js";
|
|
84
|
+
export { checkPiUpdate } from "./update/pi-update.js";
|
|
85
|
+
export type { UpdateCheckResult, UpdateCacheEntry } from "./update/types.js";
|
|
86
|
+
|
|
87
|
+
// profiles
|
|
88
|
+
export {
|
|
89
|
+
DEFAULT_PROFILE,
|
|
90
|
+
createProfile,
|
|
91
|
+
switchProfile,
|
|
92
|
+
deleteProfile,
|
|
93
|
+
resolveEffectivePackages,
|
|
94
|
+
} from "./profiles/manager.js";
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile manager for multi-profile catalog support.
|
|
3
|
+
*
|
|
4
|
+
* Profiles allow different package sets for different machines or contexts
|
|
5
|
+
* (e.g., work vs personal). Each profile has its own package overrides that
|
|
6
|
+
* merge over the base packages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CatalogYaml, CatalogPackage } from "../config/schema.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/** Name of the default profile (always available). */
|
|
16
|
+
export const DEFAULT_PROFILE = "default";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Internal helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** Ensure the catalog has a profiles record, creating one if needed. */
|
|
23
|
+
function ensureProfiles(catalog: CatalogYaml): CatalogYaml {
|
|
24
|
+
if (catalog.profiles === undefined) {
|
|
25
|
+
return { ...catalog, profiles: {} };
|
|
26
|
+
}
|
|
27
|
+
return catalog;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// createProfile
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new empty profile.
|
|
36
|
+
*
|
|
37
|
+
* @throws if a profile with the given name already exists
|
|
38
|
+
*/
|
|
39
|
+
export function createProfile(
|
|
40
|
+
catalog: CatalogYaml,
|
|
41
|
+
name: string,
|
|
42
|
+
): CatalogYaml {
|
|
43
|
+
if (name === DEFAULT_PROFILE) {
|
|
44
|
+
throw new Error(`The "${DEFAULT_PROFILE}" profile always exists`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const withProfiles = ensureProfiles(catalog);
|
|
48
|
+
|
|
49
|
+
if (withProfiles.profiles![name]) {
|
|
50
|
+
throw new Error(`Profile "${name}" already exists`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...withProfiles,
|
|
55
|
+
profiles: {
|
|
56
|
+
...withProfiles.profiles!,
|
|
57
|
+
[name]: { packages: {} },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// switchProfile
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Switch the active profile.
|
|
68
|
+
*
|
|
69
|
+
* @throws if the target profile does not exist
|
|
70
|
+
*/
|
|
71
|
+
export function switchProfile(
|
|
72
|
+
catalog: CatalogYaml,
|
|
73
|
+
name: string,
|
|
74
|
+
): CatalogYaml {
|
|
75
|
+
if (name !== DEFAULT_PROFILE) {
|
|
76
|
+
if (!catalog.profiles?.[name]) {
|
|
77
|
+
throw new Error(`Profile "${name}" not found`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...catalog,
|
|
83
|
+
meta: {
|
|
84
|
+
...catalog.meta,
|
|
85
|
+
activeProfile: name,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// deleteProfile
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Delete a profile.
|
|
96
|
+
*
|
|
97
|
+
* @throws if the profile does not exist
|
|
98
|
+
* @throws if attempting to delete the default profile
|
|
99
|
+
*/
|
|
100
|
+
export function deleteProfile(
|
|
101
|
+
catalog: CatalogYaml,
|
|
102
|
+
name: string,
|
|
103
|
+
): CatalogYaml {
|
|
104
|
+
if (name === DEFAULT_PROFILE) {
|
|
105
|
+
throw new Error(`Cannot delete the "${DEFAULT_PROFILE}" profile`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!catalog.profiles?.[name]) {
|
|
109
|
+
throw new Error(`Profile "${name}" not found`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { [name]: _, ...remainingProfiles } = catalog.profiles;
|
|
113
|
+
|
|
114
|
+
// Clear activeProfile if it was the deleted profile
|
|
115
|
+
const meta = { ...catalog.meta };
|
|
116
|
+
if (meta.activeProfile === name) {
|
|
117
|
+
delete meta.activeProfile;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...catalog,
|
|
122
|
+
meta,
|
|
123
|
+
profiles: remainingProfiles,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// resolveEffectivePackages
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the effective package set by merging base packages with
|
|
133
|
+
* the active (or specified) profile's overrides.
|
|
134
|
+
*
|
|
135
|
+
* Profile packages override base packages with the same key.
|
|
136
|
+
* If no profile is specified, uses `meta.activeProfile` (falling back
|
|
137
|
+
* to `DEFAULT_PROFILE`).
|
|
138
|
+
*/
|
|
139
|
+
export function resolveEffectivePackages(
|
|
140
|
+
catalog: CatalogYaml,
|
|
141
|
+
profile?: string,
|
|
142
|
+
): Record<string, CatalogPackage> {
|
|
143
|
+
const profileName = profile ?? catalog.meta.activeProfile ?? DEFAULT_PROFILE;
|
|
144
|
+
|
|
145
|
+
// Start with a shallow clone of base packages
|
|
146
|
+
const result: Record<string, CatalogPackage> = { ...catalog.packages };
|
|
147
|
+
|
|
148
|
+
// Merge profile overrides on top
|
|
149
|
+
if (profileName !== DEFAULT_PROFILE) {
|
|
150
|
+
const profilePkgs = catalog.profiles?.[profileName]?.packages;
|
|
151
|
+
if (profilePkgs) {
|
|
152
|
+
for (const [key, value] of Object.entries(profilePkgs)) {
|
|
153
|
+
result[key] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|