@iseoane/agent-depot 0.4.1 → 0.5.0
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 +29 -0
- package/README.es.md +969 -0
- package/README.md +244 -23
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
|
@@ -2,8 +2,10 @@ import { writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadAppOwnedSkillFilter } from "./app-owned-skills.js";
|
|
4
4
|
import { sourceIdForUrl } from "./git-source.js";
|
|
5
|
+
import { bundleRoot, describeCoordinates } from "./package-model.js";
|
|
5
6
|
import { AGENT_DEPOT_PACKAGE_VERSION } from "./project-manifest.js";
|
|
6
7
|
import { parseProfile, PROFILE_FORMAT, serializeProfile } from "./profile.js";
|
|
8
|
+
import { formatVersionPolicy } from "./skill-install.js";
|
|
7
9
|
import { scanUserGlobalSkillInventory } from "./user-global-skill-inventory.js";
|
|
8
10
|
export function selectProfileItems(items, requested, disabled, names, block) {
|
|
9
11
|
if (disabled && requested?.length)
|
|
@@ -14,6 +16,24 @@ export function selectProfileItems(items, requested, disabled, names, block) {
|
|
|
14
16
|
}
|
|
15
17
|
return disabled ? [] : items.filter(item => !requested?.length || names(item).some(name => requested.includes(name)));
|
|
16
18
|
}
|
|
19
|
+
/** The Package identities a `--package` filter accepts, matching `describeCoordinates`. */
|
|
20
|
+
export function packageFilterNames(selection) {
|
|
21
|
+
const root = bundleRoot(selection);
|
|
22
|
+
const names = [describeCoordinates(selection)];
|
|
23
|
+
if (root !== "") {
|
|
24
|
+
names.push(root);
|
|
25
|
+
const basename = path.posix.basename(root);
|
|
26
|
+
if (basename !== root)
|
|
27
|
+
names.push(basename);
|
|
28
|
+
}
|
|
29
|
+
if (selection.host === "claude")
|
|
30
|
+
names.push(selection.pluginName);
|
|
31
|
+
return names;
|
|
32
|
+
}
|
|
33
|
+
/** One Package line, shared by the export preview and the import plan label. */
|
|
34
|
+
export function describeProfilePackage(selection) {
|
|
35
|
+
return `Package ${describeCoordinates(selection)} (${formatVersionPolicy(selection.version)})`;
|
|
36
|
+
}
|
|
17
37
|
function validateOne(block, item) {
|
|
18
38
|
const profile = parseProfile({ format: PROFILE_FORMAT, agentDepotVersion: AGENT_DEPOT_PACKAGE_VERSION,
|
|
19
39
|
sources: [], skills: [], apps: [], [block]: [item] });
|
|
@@ -34,6 +54,11 @@ export async function buildProfileExport(operations, apps, options = {}) {
|
|
|
34
54
|
const allInstallations = await operations.listUserGlobalInstallations();
|
|
35
55
|
const installations = selectProfileItems(allInstallations, options.skills, options.noSkills, skill => [skill.path, path.posix.basename(skill.path)], "skills");
|
|
36
56
|
const entries = selectProfileItems(await apps.load(), options.apps, options.noApps, entry => [entry.recipe?.name ?? path.basename(entry.file)], "apps");
|
|
57
|
+
const packageRecords = options.packageOperations === undefined ? [] : await options.packageOperations.list();
|
|
58
|
+
if (options.packageOperations === undefined && options.packages?.length) {
|
|
59
|
+
throw new Error("Profile export requires Package operations to select Packages");
|
|
60
|
+
}
|
|
61
|
+
const selectedPackages = selectProfileItems(packageRecords, options.packages, options.noPackages, record => packageFilterNames(record.selection), "packages");
|
|
37
62
|
const owned = await loadAppOwnedSkillFilter({ appEnvironment: { recipesDirectory: apps.directory },
|
|
38
63
|
reportWarning: warn });
|
|
39
64
|
const portableSources = [];
|
|
@@ -79,6 +104,15 @@ export async function buildProfileExport(operations, apps, options = {}) {
|
|
|
79
104
|
exclude("apps", `App recipe ${path.basename(entry.file)}`, error instanceof Error ? error.message : String(error));
|
|
80
105
|
}
|
|
81
106
|
}
|
|
107
|
+
const packages = [];
|
|
108
|
+
for (const record of selectedPackages) {
|
|
109
|
+
try {
|
|
110
|
+
packages.push(validateOne("packages", record.selection));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
exclude("packages", describeProfilePackage(record.selection), `not portable (${error instanceof Error ? error.message : String(error)})`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
if (!options.noSkills) {
|
|
83
117
|
const inventory = await scanUserGlobalSkillInventory({
|
|
84
118
|
homeDirectory: options.homeDirectory, managedInstallations: allInstallations,
|
|
@@ -89,7 +123,7 @@ export async function buildProfileExport(operations, apps, options = {}) {
|
|
|
89
123
|
}
|
|
90
124
|
}
|
|
91
125
|
const profile = parseProfile({ format: PROFILE_FORMAT, agentDepotVersion: AGENT_DEPOT_PACKAGE_VERSION,
|
|
92
|
-
sources: portableSources, skills, apps: recipes });
|
|
126
|
+
sources: portableSources, skills, apps: recipes, packages });
|
|
93
127
|
return {
|
|
94
128
|
profile,
|
|
95
129
|
exclusions: diagnostics.filter((item) => "block" in item),
|
|
@@ -98,13 +132,13 @@ export async function buildProfileExport(operations, apps, options = {}) {
|
|
|
98
132
|
};
|
|
99
133
|
}
|
|
100
134
|
function sourceWarnings(profile) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
const missing = (source, label) => source.kind === "external" && "url" in source && !profile.sources.some(candidate => candidate.url === source.url)
|
|
136
|
+
? [{ kind: "source", message: `WARNING: ${label} references Source ${source.url} not included in profile` }]
|
|
137
|
+
: [];
|
|
138
|
+
return [
|
|
139
|
+
...profile.skills.flatMap(skill => missing(skill.source, `Skill ${skill.path}`)),
|
|
140
|
+
...profile.packages.flatMap(selection => missing(selection.source, describeProfilePackage(selection))),
|
|
141
|
+
];
|
|
108
142
|
}
|
|
109
143
|
/** Shared preview for a frontend-selected subset; Source warnings are recomputed. */
|
|
110
144
|
export function profileExportPreview(profile, diagnostics = []) {
|
|
@@ -112,12 +146,13 @@ export function profileExportPreview(profile, diagnostics = []) {
|
|
|
112
146
|
...profile.sources.map(source => `Source ${sourceIdForUrl(source.url)} ${source.url} (included: ${source.included})`),
|
|
113
147
|
...profile.skills.map(skill => `Skill ${skill.path} [${skill.hosts.join(", ")}] (${skill.version.policy})`),
|
|
114
148
|
...profile.apps.map(recipe => `App recipe ${recipe.name} (${recipe.platform ?? "all platforms"}; no approval)`),
|
|
149
|
+
...profile.packages.map(selection => `${describeProfilePackage(selection)}; no receipt, no installed state`),
|
|
115
150
|
...diagnostics.flatMap(item => "block" in item ? [`Excluded ${item.label}: ${item.reason}`] : item.kind === "load" ? [item.message] : []),
|
|
116
151
|
...sourceWarnings(profile).map(warning => warning.message)];
|
|
117
152
|
}
|
|
118
153
|
/** Empty profiles are valid to parse, but are not useful export artifacts. */
|
|
119
154
|
export function requireProfileExportSelection(profile) {
|
|
120
|
-
if (!profile.sources.length && !profile.skills.length && !profile.apps.length) {
|
|
155
|
+
if (!profile.sources.length && !profile.skills.length && !profile.apps.length && !profile.packages.length) {
|
|
121
156
|
throw new Error("Nothing selected; nothing written.");
|
|
122
157
|
}
|
|
123
158
|
}
|
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { compareAppVersions } from "./app-version.js";
|
|
6
6
|
import { canonicalizeGitSourceUrl, sourceIdForUrl } from "./git-source.js";
|
|
7
|
-
import {
|
|
7
|
+
import { selectionKey } from "./package-model.js";
|
|
8
|
+
import { describeProfilePackage, packageFilterNames, selectProfileItems } from "./profile-export.js";
|
|
8
9
|
import { parseProfile, PROFILE_FORMAT } from "./profile.js";
|
|
9
10
|
import { AGENT_DEPOT_PACKAGE_VERSION } from "./project-manifest.js";
|
|
10
11
|
import { defaultProjectSkillTreeAccess, executeSingleInstall, formatVersionPolicy, outputSingleInstallPreview, planSingleInstall } from "./skill-install.js";
|
|
@@ -75,6 +76,12 @@ function classifySkill(value, skills) {
|
|
|
75
76
|
const existing = skills.find(skill => skillIdentity(skill) === skillIdentity(value));
|
|
76
77
|
return { block: "skills", value, label: `Skill ${value.path}`, ...comparison(existing && skillChoices(existing), skillChoices(value)) };
|
|
77
78
|
}
|
|
79
|
+
/** Identity is the Source and coordinates; only the version policy is a portable choice. */
|
|
80
|
+
function classifyPackage(value, recorded) {
|
|
81
|
+
const existing = recorded.find(record => selectionKey(record.selection) === selectionKey(value));
|
|
82
|
+
return { block: "packages", value, label: describeProfilePackage(value),
|
|
83
|
+
...comparison(existing === undefined ? undefined : { version: existing.selection.version }, { version: value.version }) };
|
|
84
|
+
}
|
|
78
85
|
function classifyRecipe(value, recipes) {
|
|
79
86
|
const existing = recipes.filter(entry => entry.name === value.name &&
|
|
80
87
|
(entry.platform === undefined || value.platform === undefined || entry.platform === value.platform));
|
|
@@ -87,8 +94,18 @@ function classifyRecipe(value, recipes) {
|
|
|
87
94
|
status: existing.length ? "conflict" : "add",
|
|
88
95
|
...(existing.length ? { difference: existing.map(entry => `${entry.file}: name ${JSON.stringify(value.name)} already present with overlapping platforms; ${fieldDifferences(entry.content ?? {}, value).join("; ") || "content identical"}`).join("; ") } : {}) };
|
|
89
96
|
}
|
|
97
|
+
/** An external Source that no plan item registers is fetched but never added to Sources. */
|
|
98
|
+
function fetchedSourceNote(source, sources, entries) {
|
|
99
|
+
if (source.kind !== "external" || !("url" in source))
|
|
100
|
+
return undefined;
|
|
101
|
+
const url = canonicalizeGitSourceUrl(source.url);
|
|
102
|
+
const registered = sources.some(registered => registered.kind === "git" && canonicalizeGitSourceUrl(registered.url) === url);
|
|
103
|
+
const planned = entries.some(candidate => candidate.block === "sources" && candidate.status === "add"
|
|
104
|
+
&& canonicalizeGitSourceUrl(candidate.value.url) === url);
|
|
105
|
+
return registered || planned ? undefined : ` Source ${source.url} will be fetched but not registered`;
|
|
106
|
+
}
|
|
90
107
|
/** Load only this block; retained across skips and invalidated after every attempted write. */
|
|
91
|
-
async function loadImportClassifier(block, operations, apps) {
|
|
108
|
+
async function loadImportClassifier(block, operations, apps, packages) {
|
|
92
109
|
if (block === "sources") {
|
|
93
110
|
const sources = await operations.listSources();
|
|
94
111
|
return (item) => item.block === "sources" ? classifySource(item.value, sources) : item;
|
|
@@ -99,17 +116,28 @@ async function loadImportClassifier(block, operations, apps) {
|
|
|
99
116
|
const skills = await operations.listUserGlobalInstallations();
|
|
100
117
|
return (item) => item.block === "skills" ? classifySkill(item.value, skills) : item;
|
|
101
118
|
}
|
|
119
|
+
if (block === "packages") {
|
|
120
|
+
if (packages === undefined)
|
|
121
|
+
throw new Error("Profile import requires Package operations to replay Package selections");
|
|
122
|
+
const recorded = await packages.list();
|
|
123
|
+
return (item) => item.block === "packages" ? classifyPackage(item.value, recorded) : item;
|
|
124
|
+
}
|
|
102
125
|
const recipes = await loadImportRecipes(apps);
|
|
103
126
|
return (item) => item.block === "apps" ? classifyRecipe(item.value, recipes) : item;
|
|
104
127
|
}
|
|
105
128
|
/** Read-only classification. Inclusion is a transient frontend choice, not persisted Source state. */
|
|
106
|
-
export async function buildProfileImport(input, operations, apps, filters = {}) {
|
|
129
|
+
export async function buildProfileImport(input, operations, apps, filters = {}, packageOperations) {
|
|
107
130
|
const profile = parseProfile(input);
|
|
108
131
|
if (!operations.listUserGlobalInstallations)
|
|
109
132
|
throw new Error("Profile import requires user-global installation listing");
|
|
110
133
|
const sources = await operations.listSources();
|
|
111
134
|
const skills = await operations.listUserGlobalInstallations();
|
|
112
135
|
const recipes = await loadImportRecipes(apps);
|
|
136
|
+
const packageValues = selectProfileItems(profile.packages, filters.packages, filters.noPackages, selection => packageFilterNames(selection), "packages");
|
|
137
|
+
if (packageValues.length > 0 && packageOperations === undefined) {
|
|
138
|
+
throw new Error("Profile import requires Package operations to replay Package selections");
|
|
139
|
+
}
|
|
140
|
+
const recordedPackages = packageOperations === undefined ? [] : await packageOperations.list();
|
|
113
141
|
const items = [];
|
|
114
142
|
for (const value of selectProfileItems(profile.sources, filters.sources, filters.noSources, source => [source.url, sourceIdForUrl(source.url)], "sources"))
|
|
115
143
|
items.push(classifySource(value, sources));
|
|
@@ -118,7 +146,9 @@ export async function buildProfileImport(input, operations, apps, filters = {})
|
|
|
118
146
|
for (const value of selectProfileItems(profile.apps, filters.apps, filters.noApps, recipe => [recipe.name], "apps")) {
|
|
119
147
|
items.push(classifyRecipe(value, recipes));
|
|
120
148
|
}
|
|
121
|
-
const
|
|
149
|
+
const packages = packageValues.map(value => classifyPackage(value, recordedPackages));
|
|
150
|
+
const entries = [...items, ...packages];
|
|
151
|
+
const preview = ["Preview: import portable user-global profile", ...entries.flatMap(item => {
|
|
122
152
|
const status = item.block === "sources" && item.status === "same" && !item.value.included
|
|
123
153
|
? "same (discovery inclusion is not imported)" : item.status;
|
|
124
154
|
const lines = [`${status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`];
|
|
@@ -128,12 +158,16 @@ export async function buildProfileImport(input, operations, apps, filters = {})
|
|
|
128
158
|
for (const [action, method] of Object.entries(skill.methods ?? {})) {
|
|
129
159
|
lines.push(` ${action}: argv=${JSON.stringify(method.argv)} (user-provided method from the profile, runs only after --yes)`);
|
|
130
160
|
}
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
const note = fetchedSourceNote(skill.source, sources, entries);
|
|
162
|
+
if (note !== undefined)
|
|
163
|
+
lines.push(note);
|
|
164
|
+
}
|
|
165
|
+
if (item.block === "packages" && item.status === "add") {
|
|
166
|
+
const selection = item.value;
|
|
167
|
+
lines.push(` Source ${canonical(selection.source)}`, ` host ${selection.host}: select records the choice; it runs no host command and installs nothing`);
|
|
168
|
+
const note = fetchedSourceNote(selection.source, sources, entries);
|
|
169
|
+
if (note !== undefined)
|
|
170
|
+
lines.push(note);
|
|
137
171
|
}
|
|
138
172
|
return lines;
|
|
139
173
|
})];
|
|
@@ -142,7 +176,7 @@ export async function buildProfileImport(input, operations, apps, filters = {})
|
|
|
142
176
|
preview.push(`WARNING: profile Agent Depot ${profile.agentDepotVersion} is newer than running ${AGENT_DEPOT_PACKAGE_VERSION}`);
|
|
143
177
|
}
|
|
144
178
|
preview.push("Existing choices are never replaced. Source inclusion is a transient discovery choice.", "Same-name App recipes conflict only when platforms overlap; disjoint platform recipes can both be added.");
|
|
145
|
-
return { items, preview };
|
|
179
|
+
return { items, packages, preview };
|
|
146
180
|
}
|
|
147
181
|
/** Publish complete JSON exclusively: rename would silently overwrite an existing recipe. */
|
|
148
182
|
async function writeRecipe(directory, recipe) {
|
|
@@ -163,17 +197,24 @@ async function writeRecipe(directory, recipe) {
|
|
|
163
197
|
/** Apply only confirmed additions, rechecking destination choices per item. Failures remain independent. */
|
|
164
198
|
export async function applyProfileImport(plan, operations, apps, confirmed, output, environment = {}) {
|
|
165
199
|
const results = [];
|
|
166
|
-
|
|
200
|
+
const packageOperations = environment.packageOperations;
|
|
201
|
+
const groups = [
|
|
202
|
+
{ block: "sources", entries: plan.items.filter(item => item.block === "sources") },
|
|
203
|
+
{ block: "skills", entries: plan.items.filter(item => item.block === "skills") },
|
|
204
|
+
{ block: "apps", entries: plan.items.filter(item => item.block === "apps") },
|
|
205
|
+
{ block: "packages", entries: plan.packages },
|
|
206
|
+
];
|
|
207
|
+
for (const group of groups) {
|
|
167
208
|
let classify;
|
|
168
|
-
for (const item of
|
|
209
|
+
for (const item of group.entries) {
|
|
169
210
|
if (!confirmed || item.status !== "add") {
|
|
170
211
|
results.push({ item, status: "skipped" });
|
|
171
212
|
continue;
|
|
172
213
|
}
|
|
173
214
|
try {
|
|
174
215
|
parseProfile({ format: PROFILE_FORMAT, agentDepotVersion: AGENT_DEPOT_PACKAGE_VERSION,
|
|
175
|
-
sources: [], skills: [], apps: [], [block]: [item.value] });
|
|
176
|
-
classify ??= await loadImportClassifier(block, operations, apps);
|
|
216
|
+
sources: [], skills: [], apps: [], [group.block]: [item.value] });
|
|
217
|
+
classify ??= await loadImportClassifier(group.block, operations, apps, packageOperations);
|
|
177
218
|
const recheck = classify(item);
|
|
178
219
|
if (recheck.status !== "add") {
|
|
179
220
|
results.push({ item, status: "skipped", detail: `${recheck.status}: ${recheck.label}${recheck.difference ? `; ${recheck.difference}` : ""}` });
|
|
@@ -185,9 +226,10 @@ export async function applyProfileImport(plan, operations, apps, confirmed, outp
|
|
|
185
226
|
await operations.addGitSource(item.value.url);
|
|
186
227
|
else if (item.block === "apps")
|
|
187
228
|
await writeRecipe(apps.directory, item.value);
|
|
188
|
-
else
|
|
229
|
+
else if (item.block === "packages")
|
|
230
|
+
await selectImportedPackage(item.value, packageOperations, output);
|
|
231
|
+
else
|
|
189
232
|
await installImportedSkill(item.value, operations, apps, output, environment);
|
|
190
|
-
}
|
|
191
233
|
results.push({ item, status: "added" });
|
|
192
234
|
}
|
|
193
235
|
catch (error) {
|
|
@@ -197,6 +239,27 @@ export async function applyProfileImport(plan, operations, apps, confirmed, outp
|
|
|
197
239
|
}
|
|
198
240
|
return results;
|
|
199
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Replays one selection through the flow's select action. Approval is written for
|
|
244
|
+
* that action alone, whose declaration carries no argv, so no install or update
|
|
245
|
+
* authorization is created and no host command can run.
|
|
246
|
+
*/
|
|
247
|
+
async function selectImportedPackage(selection, packages, output) {
|
|
248
|
+
if (packages === undefined)
|
|
249
|
+
throw new Error("Package operations are required to replay a Package selection");
|
|
250
|
+
const plan = await packages.planLifecycle(selection, "select");
|
|
251
|
+
if (plan.commands.length > 0) {
|
|
252
|
+
throw new Error(`${describeProfilePackage(selection)} plan carries ${plan.commands.length} host steps; import runs no host command`);
|
|
253
|
+
}
|
|
254
|
+
await packages.approve(selection, "select");
|
|
255
|
+
const result = await packages.executeLifecycle(plan, true);
|
|
256
|
+
if (result.status !== "selected") {
|
|
257
|
+
const reason = result.status === "failed" || result.status === "stale plan" || result.status === "manual required"
|
|
258
|
+
? `: ${result.reason}` : "";
|
|
259
|
+
throw new Error(`${describeProfilePackage(selection)} was not selected (${result.status}${reason})`);
|
|
260
|
+
}
|
|
261
|
+
output(` selected ${describeProfilePackage(selection)}: no host command ran`);
|
|
262
|
+
}
|
|
200
263
|
async function installImportedSkill(selection, operations, apps, output, environment) {
|
|
201
264
|
if (!operations.addUserGlobalInstallation || !operations.listUserGlobalInstallations)
|
|
202
265
|
throw new Error("Cannot persist user-global Skills");
|
package/dist/src/profile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { parseAppRecipe } from "./app-recipes.js";
|
|
3
3
|
import { canonicalizeGitSourceUrl } from "./git-source.js";
|
|
4
|
+
import { selectionKey } from "./package-model.js";
|
|
4
5
|
import { containsCredentialArgument, parseProjectManifest, ProjectManifestError } from "./project-manifest.js";
|
|
5
6
|
export const PROFILE_FORMAT = "agent-depot-profile/v1";
|
|
6
7
|
function object(value, keys, field) {
|
|
@@ -64,9 +65,69 @@ function rejectDuplicates(items, identity, field) {
|
|
|
64
65
|
seen.add(key);
|
|
65
66
|
});
|
|
66
67
|
}
|
|
68
|
+
/** A Source-relative bundle root in the one canonical form discovery produces. */
|
|
69
|
+
function bundleDirectory(value, field) {
|
|
70
|
+
if (typeof value !== "string")
|
|
71
|
+
throw new Error(`${field}: expected a string`);
|
|
72
|
+
if (value === "")
|
|
73
|
+
return value;
|
|
74
|
+
const noncanonical = value.startsWith("/") || value === "." || value.startsWith("./") || value.endsWith("/") ||
|
|
75
|
+
value.includes("//") || value.includes("\\") || value === ".." || value.startsWith("../") ||
|
|
76
|
+
path.posix.normalize(value) !== value;
|
|
77
|
+
if (noncanonical)
|
|
78
|
+
throw new Error(`${field}: expected a canonical Source-relative directory`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
/** The single segment a Claude selection names, matching the host's own plugin key. */
|
|
82
|
+
function pluginName(value, field) {
|
|
83
|
+
if (typeof value !== "string" || value.length === 0)
|
|
84
|
+
throw new Error(`${field}: expected a plugin name`);
|
|
85
|
+
const unsafe = [...value].some(character => (character.codePointAt(0) ?? 0) <= 0x1f || character.codePointAt(0) === 0x7f);
|
|
86
|
+
if (value === "." || value === ".." || value.includes("/") || value.includes("\\") || /\s/u.test(value) || unsafe) {
|
|
87
|
+
throw new Error(`${field}: expected one plugin name segment`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A Package declaration carries its Source identity, host coordinates and version
|
|
93
|
+
* policy. The host's own install identity, verified version and receipt stay out,
|
|
94
|
+
* so they cannot travel to another machine.
|
|
95
|
+
*/
|
|
96
|
+
function parseProfilePackage(value, field) {
|
|
97
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
98
|
+
throw new Error(`${field}: expected an object`);
|
|
99
|
+
const host = value.host;
|
|
100
|
+
if (host !== "pi" && host !== "claude")
|
|
101
|
+
throw new Error(`${field}.host: expected "pi" or "claude"`);
|
|
102
|
+
const record = object(value, host === "pi"
|
|
103
|
+
? ["source", "version", "host", "root"]
|
|
104
|
+
: ["source", "version", "host", "marketplaceRoot", "pluginName"], field);
|
|
105
|
+
const coordinates = host === "pi"
|
|
106
|
+
? { host, root: bundleDirectory(record.root, `${field}.root`) }
|
|
107
|
+
: { host, marketplaceRoot: bundleDirectory(record.marketplaceRoot, `${field}.marketplaceRoot`),
|
|
108
|
+
pluginName: pluginName(record.pluginName, `${field}.pluginName`) };
|
|
109
|
+
const at = (root, name) => root === "" ? name : `${root}/${name}`;
|
|
110
|
+
const manifestPath = coordinates.host === "pi"
|
|
111
|
+
? at(coordinates.root, "package.json")
|
|
112
|
+
: at(coordinates.marketplaceRoot, ".claude-plugin/marketplace.json");
|
|
113
|
+
try {
|
|
114
|
+
const [parsed] = parseProjectManifest({ version: 1, skills: [
|
|
115
|
+
{ source: record.source, path: manifestPath, version: record.version, hosts: [host] },
|
|
116
|
+
] }, "profile").skills;
|
|
117
|
+
return Object.freeze({ source: parsed.source, version: parsed.version, ...coordinates });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!(error instanceof ProjectManifestError))
|
|
121
|
+
throw error;
|
|
122
|
+
const rootField = coordinates.host === "pi" ? "root" : "marketplaceRoot";
|
|
123
|
+
const reason = error.reason.replace(/^skills\[0\]\.path/u, `${field}.${rootField}`).replace(/^skills\[0\]\./u, `${field}.`);
|
|
124
|
+
const [path0, ...rest] = reason.split(" ");
|
|
125
|
+
throw new Error(`${path0}: ${rest.join(" ")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
67
128
|
/** Portable profile parsing shares the manifest and recipe safety contracts. */
|
|
68
129
|
export function parseProfile(value) {
|
|
69
|
-
const root = object(value, ["format", "agentDepotVersion", "sources", "skills", "apps"], "profile");
|
|
130
|
+
const root = object(value, ["format", "agentDepotVersion", "sources", "skills", "apps", "packages"], "profile");
|
|
70
131
|
if (root.format !== PROFILE_FORMAT)
|
|
71
132
|
throw new Error("profile.format: unsupported profile version");
|
|
72
133
|
if (typeof root.agentDepotVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][\w.+-]+)?$/u.test(root.agentDepotVersion)) {
|
|
@@ -112,7 +173,14 @@ export function parseProfile(value) {
|
|
|
112
173
|
throw new Error(reason.startsWith(`apps[${index}]`) ? reason : `apps[${index}].${reason}`);
|
|
113
174
|
}
|
|
114
175
|
});
|
|
176
|
+
const packages = (root.packages === undefined ? [] : list(root.packages, "packages")).map((value, index) => {
|
|
177
|
+
const field = `packages[${index}]`;
|
|
178
|
+
const selection = parseProfilePackage(value, field);
|
|
179
|
+
assertPortableText(selection, field);
|
|
180
|
+
return selection;
|
|
181
|
+
});
|
|
115
182
|
rejectDuplicates(sources, source => source.url, "sources");
|
|
183
|
+
rejectDuplicates(packages, selectionKey, "packages");
|
|
116
184
|
apps.forEach((app, index) => {
|
|
117
185
|
if (apps.slice(0, index).some(previous => previous.name === app.name &&
|
|
118
186
|
(previous.platform === undefined || app.platform === undefined || previous.platform === app.platform))) {
|
|
@@ -123,8 +191,15 @@ export function parseProfile(value) {
|
|
|
123
191
|
sources.sort((a, b) => compare(a.url, b.url));
|
|
124
192
|
const sortedSkills = [...parsedSkills].sort((a, b) => compare(JSON.stringify([a.source, a.path]), JSON.stringify([b.source, b.path])));
|
|
125
193
|
apps.sort((a, b) => compare(a.name, b.name) || compare(a.platform ?? "", b.platform ?? ""));
|
|
126
|
-
|
|
194
|
+
packages.sort((a, b) => compare(selectionKey(a), selectionKey(b)));
|
|
195
|
+
return { format: PROFILE_FORMAT, agentDepotVersion: root.agentDepotVersion, sources, skills: sortedSkills, apps, packages };
|
|
127
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* An empty Package block is omitted, so an export with no Packages stays readable
|
|
199
|
+
* by an Agent Depot version that predates the block.
|
|
200
|
+
*/
|
|
128
201
|
export function serializeProfile(profile) {
|
|
129
|
-
|
|
202
|
+
const parsed = parseProfile(profile);
|
|
203
|
+
const { packages, ...rest } = parsed;
|
|
204
|
+
return `${JSON.stringify(packages.length === 0 ? rest : parsed, null, 2)}\n`;
|
|
130
205
|
}
|
|
@@ -4,7 +4,7 @@ import { lstat, readdir, readFile } from "node:fs/promises";
|
|
|
4
4
|
import { assertNoSymlinkPath } from "./path-safety.js";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { defaultGitSourceCachePath, GitSourceSnapshotAccess, SKILL_TREE_LIMITS, validateSkillDirectoryPath, } from "./git-source.js";
|
|
7
|
+
import { defaultGitSourceCachePath, GitSourceSnapshotAccess, isSourceSnapshotFile, SKILL_TREE_LIMITS, validateSkillDirectoryPath, } from "./git-source.js";
|
|
8
8
|
import { AGENT_DEPOT_PACKAGE_VERSION, } from "./project-manifest.js";
|
|
9
9
|
export const EXCLUDED_LIFECYCLE_SEGMENTS = Object.freeze([
|
|
10
10
|
"in-progress",
|
|
@@ -325,7 +325,7 @@ async function walkDirectory(root, relativeDirectory, files, readTextFile) {
|
|
|
325
325
|
await walkDirectory(root, relativePath, files, readTextFile);
|
|
326
326
|
continue;
|
|
327
327
|
}
|
|
328
|
-
if (!entry.isFile() ||
|
|
328
|
+
if (!entry.isFile() || !isSourceSnapshotFile(relativePath) || (await safePathKind(absolutePath)) !== "file") {
|
|
329
329
|
continue;
|
|
330
330
|
}
|
|
331
331
|
files.push({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { assertSkillNotAppOwned } from "./app-owned-skills.js";
|
|
3
|
+
import { assertSkillNotPackageOwned, loadPackageOwnedSkillFilter, } from "./package-owned-skills.js";
|
|
3
4
|
import { pathsOverlap } from "./path-safety.js";
|
|
4
5
|
import { inspectProjectSkillInstallation, installProjectSkillTransaction, validateProjectSkillInstallationPreview, } from "./project-installation.js";
|
|
5
6
|
import { AGENT_DEPOT_PACKAGE_VERSION, parseProjectManifest, } from "./project-manifest.js";
|
|
@@ -313,6 +314,7 @@ export async function planSingleInstall(request, context) {
|
|
|
313
314
|
}
|
|
314
315
|
const { resolvedSource, resolved, installationInspection } = await resolveInstallInspection(selection, source, projectSource, context.root, request, context.operations, context.environment, context.sourceAccess);
|
|
315
316
|
await assertInstallNotAppOwned(request, context, installationInspection);
|
|
317
|
+
await assertInstallNotPackageOwned(selection, resolvedSource, context);
|
|
316
318
|
return { request, context, selection, source, resolvedSource, resolved, inspection: installationInspection };
|
|
317
319
|
}
|
|
318
320
|
/** Prints the preview the user confirms before {@link executeSingleInstall}. */
|
|
@@ -327,6 +329,7 @@ export function outputSingleInstallPreview(plan, output) {
|
|
|
327
329
|
export async function executeSingleInstall(plan, persist, output) {
|
|
328
330
|
const { request, context, selection, resolvedSource, resolved, inspection } = plan;
|
|
329
331
|
await assertInstallNotAppOwned(request, context, inspection);
|
|
332
|
+
await assertInstallNotPackageOwned(selection, resolvedSource, context);
|
|
330
333
|
const protectedPaths = assertNoManagedInstallationOverlap(context.existing, selection, inspection, context.root, request.overwrite);
|
|
331
334
|
rejectInstallationCollision(inspection, request.overwrite);
|
|
332
335
|
const transaction = await installOne(selection, resolvedSource, context.sourceAccess, context.root, context.environment, resolved.method, request.portableV1, resolved.files, resolved.resolvedVersion, request.confirmAdditionalHostExposure, request.overwrite, inspection, protectedPaths);
|
|
@@ -372,3 +375,16 @@ async function assertInstallNotAppOwned(request, context, inspection) {
|
|
|
372
375
|
await assertSkillNotAppOwned(path.basename(location.path), { ...context.environment, homeDirectory: context.root });
|
|
373
376
|
}
|
|
374
377
|
}
|
|
378
|
+
/** Reloads bundle ownership from the selected Source and refuses its own Skill paths. */
|
|
379
|
+
async function assertInstallNotPackageOwned(selection, source, context) {
|
|
380
|
+
const owned = await loadPackageOwnedSkillFilter({
|
|
381
|
+
sources: [source],
|
|
382
|
+
homeDirectory: context.root,
|
|
383
|
+
...(context.environment.sourceContentAccess === undefined ? {} : { sourceContentAccess: context.environment.sourceContentAccess }),
|
|
384
|
+
...(context.environment.selections === undefined ? {} : { selections: context.environment.selections }),
|
|
385
|
+
...(context.environment.hostView === undefined ? {} : { hostView: context.environment.hostView }),
|
|
386
|
+
...(context.environment.stateDirectory === undefined ? {} : { stateDirectory: context.environment.stateDirectory }),
|
|
387
|
+
...(context.environment.reportWarning === undefined ? {} : { reportWarning: context.environment.reportWarning }),
|
|
388
|
+
});
|
|
389
|
+
assertSkillNotPackageOwned(selection.path, owned);
|
|
390
|
+
}
|
package/dist/src/sources.js
CHANGED
|
@@ -286,6 +286,17 @@ export function createSourceOperations(options = {}) {
|
|
|
286
286
|
},
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
|
+
/** Converts a registered Source back into the self-contained identity a selection carries. */
|
|
290
|
+
export function projectSourceOf(source) {
|
|
291
|
+
if (source.kind === "builtin") {
|
|
292
|
+
return Object.freeze({ kind: "builtin", id: source.id });
|
|
293
|
+
}
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
kind: "external",
|
|
296
|
+
url: source.url,
|
|
297
|
+
...(source.ref === undefined ? {} : { ref: source.ref }),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
289
300
|
/** Converts persisted project identity into a read-only Source identity. */
|
|
290
301
|
export function resolveProjectSource(projectSource) {
|
|
291
302
|
if (projectSource.kind === "builtin") {
|
|
@@ -13,7 +13,7 @@ export function isAppMode(mode) {
|
|
|
13
13
|
export const appName = (entry) => entry.recipe?.name ?? entry.file;
|
|
14
14
|
export function appStepLines(step) {
|
|
15
15
|
return [
|
|
16
|
-
...(step.argv ? [`argv: ${JSON.stringify(step.argv)}`, `Executable: ${step.executable ??
|
|
16
|
+
...(step.argv ? [`argv: ${JSON.stringify(step.argv)}`, `Executable: ${step.executable ?? `${JSON.stringify(step.argv[0])} not found in PATH`}`] : []),
|
|
17
17
|
...(step.manual ? [`Manual: ${step.manual}`] : []),
|
|
18
18
|
`Working directory: ${step.cwd}`,
|
|
19
19
|
...(step.warning ? [step.warning] : []),
|
package/dist/src/tui/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CatalogView } from "./catalog-view.js";
|
|
|
6
6
|
import { InstallationsView } from "./installations-view.js";
|
|
7
7
|
import { useKeys } from "./keys.js";
|
|
8
8
|
import { ProfileView } from "./profile-view.js";
|
|
9
|
-
import { SourcesView } from "./sources-view.js";
|
|
9
|
+
import { SourcesView, sourceRowHints } from "./sources-view.js";
|
|
10
10
|
import { separatorLine, theme } from "./theme.js";
|
|
11
11
|
import { UpdatesView } from "./updates-view.js";
|
|
12
12
|
const TABS = [
|
|
@@ -16,11 +16,9 @@ const TABS = [
|
|
|
16
16
|
["updates", "4 Updates"],
|
|
17
17
|
["profile", "5 Import/Export"],
|
|
18
18
|
];
|
|
19
|
-
/**
|
|
19
|
+
/** Sources derives its own hints from the selected row, so it is excluded here. */
|
|
20
20
|
function hintsOf(view, catalogFocusedOnSource) {
|
|
21
21
|
switch (view) {
|
|
22
|
-
case "sources":
|
|
23
|
-
return "Tab section · j/k move · space mark · a all · Enter catalog · n add · r refresh · d remove · q quit";
|
|
24
22
|
case "catalog":
|
|
25
23
|
return `j/k move · Enter expand · space mark · a all · i install · / filter${catalogFocusedOnSource ? " · s all sources" : ""} · q quit`;
|
|
26
24
|
case "installations":
|
|
@@ -43,6 +41,7 @@ export function App({ operations, environment, onExit }) {
|
|
|
43
41
|
}, []);
|
|
44
42
|
const [view, setView] = useState("sources");
|
|
45
43
|
const [catalogSourceId, setCatalogSourceId] = useState();
|
|
44
|
+
const [sourcesHints, setSourcesHints] = useState(sourceRowHints(undefined));
|
|
46
45
|
const showView = (next) => setView(next);
|
|
47
46
|
useKeys((input) => {
|
|
48
47
|
if (capturingRef.current)
|
|
@@ -62,8 +61,8 @@ export function App({ operations, environment, onExit }) {
|
|
|
62
61
|
else if (input === "5")
|
|
63
62
|
showView("profile");
|
|
64
63
|
});
|
|
65
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: theme.accent, children: ["Agent Depot", AGENT_DEPOT_PACKAGE_VERSION === undefined ? "" : ` v${AGENT_DEPOT_PACKAGE_VERSION}`] }), _jsx(Text, { children: TABS.map(([name, label], position) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, bold: view === name, children: [position > 0 ? " " : "", view === name ? `[${label}]` : ` ${label} `] }, name))) }), _jsx(Text, { color: theme.muted, children: separatorLine(stdout.columns) }), view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onOpenCatalog: (source) => {
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: theme.accent, children: ["Agent Depot", AGENT_DEPOT_PACKAGE_VERSION === undefined ? "" : ` v${AGENT_DEPOT_PACKAGE_VERSION}`] }), _jsx(Text, { children: TABS.map(([name, label], position) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, bold: view === name, children: [position > 0 ? " " : "", view === name ? `[${label}]` : ` ${label} `] }, name))) }), _jsx(Text, { color: theme.muted, children: separatorLine(stdout.columns) }), view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onHintsChange: setSourcesHints, onOpenCatalog: (source) => {
|
|
66
65
|
setCatalogSourceId(source.id);
|
|
67
66
|
setView("catalog");
|
|
68
|
-
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })), _jsx(Text, { color: theme.muted, children: capturing ? "Enter submit · Esc cancel · y/n confirm · space toggle" : hintsOf(view, catalogSourceId !== undefined) })] }));
|
|
67
|
+
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })), _jsx(Text, { color: theme.muted, children: capturing ? "Enter submit · Esc cancel · y/n confirm · space toggle" : view === "sources" ? sourcesHints : hintsOf(view, catalogSourceId !== undefined) })] }));
|
|
69
68
|
}
|
|
@@ -22,15 +22,22 @@ function markAllListed(context) {
|
|
|
22
22
|
context.setMessage(undefined);
|
|
23
23
|
setMarked(leaves.every((skill) => markedRef.current.has(skillKey(skill))) ? new Set() : new Set(leaves.map(skillKey)));
|
|
24
24
|
}
|
|
25
|
-
/** `i`: installs the marked Skills that are listed, or the highlighted one. */
|
|
25
|
+
/** `i`: installs the highlighted bundle, or the marked Skills that are listed, or the highlighted one. */
|
|
26
26
|
function chooseHosts(context, row) {
|
|
27
|
+
const data = row.node.data;
|
|
28
|
+
// A bundle row is not markable, so it acts on itself and never picks up a Skill mark.
|
|
29
|
+
if (data.kind === "bundle") {
|
|
30
|
+
context.setMessage(undefined);
|
|
31
|
+
context.startPackage({ selection: data.entry.selection, name: data.entry.descriptor.name });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
27
34
|
// Only what is listed installs: marks on Skills hidden by the filter are ignored.
|
|
28
35
|
const marked = leavesOf(context.navigation.latestRoots())
|
|
29
36
|
.filter((skill) => context.marks.markedRef.current.has(skillKey(skill)));
|
|
30
|
-
const highlighted =
|
|
37
|
+
const highlighted = data.kind === "skill" ? [data.skill] : [];
|
|
31
38
|
const chosen = marked.length > 0 ? marked : highlighted;
|
|
32
39
|
if (chosen.length === 0) {
|
|
33
|
-
context.setMessage({ kind: "error", text: "Install is not available for source rows; highlight a skill or mark some with space" });
|
|
40
|
+
context.setMessage({ kind: "error", text: "Install is not available for source rows; highlight a skill or a Package bundle, or mark some skills with space" });
|
|
34
41
|
return;
|
|
35
42
|
}
|
|
36
43
|
context.setMessage(undefined);
|
|
@@ -4,6 +4,7 @@ import { describeSkills, HOST_CHOICES, SCOPE_CHOICES } from "./catalog-actions.j
|
|
|
4
4
|
import { skillKey } from "./catalog-tree.js";
|
|
5
5
|
import { hasUnmanagedCopy } from "./catalog-installs.js";
|
|
6
6
|
import { describeRow, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
7
|
+
import { isPackageMode, PackagePanel } from "./package-actions.js";
|
|
7
8
|
import { HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
8
9
|
import { rowStyle } from "./theme.js";
|
|
9
10
|
import { theme } from "./theme.js";
|
|
@@ -11,16 +12,19 @@ import { theme } from "./theme.js";
|
|
|
11
12
|
export function CatalogHeader({ scope, listed, installable, total, filter }) {
|
|
12
13
|
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Scope: ", scope, " ", listed, " of ", installable] }), filter.editing || filter.query !== "" ? (_jsxs(Text, { children: ["Filter: ", filter.query, filter.editing ? "_" : ""] })) : null, total === 0 ? _jsx(Text, { children: "No skills" }) : null, total > 0 && installable === 0 ? _jsx(Text, { children: "All skills are installed" }) : null, installable > 0 && listed === 0 ? _jsx(Text, { children: "No matching skills" }) : null] }));
|
|
13
14
|
}
|
|
14
|
-
/** One line of the Catalog tree: cursor, indent, mark box, label and the
|
|
15
|
+
/** One line of the Catalog tree: cursor, indent, mark box, label and the ownership markers. */
|
|
15
16
|
export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
16
17
|
const { data } = row.node;
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const isGroup = data.kind === "source" || data.kind === "group";
|
|
19
|
+
const ownedBy = data.kind === "skill" ? data.ownedBy : undefined;
|
|
20
|
+
return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : ownedBy === undefined ? undefined : theme.inactive, children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", describeRow(row), ownedBy === undefined ? null : _jsxs(Text, { color: theme.inactive, children: [" ", "provided by the ", ownedBy.host, " Package ", ownedBy.name] }), data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
19
21
|
? _jsxs(Text, { color: theme.marker, children: [" ", UNMANAGED_MARKER] })
|
|
20
22
|
: null] }));
|
|
21
23
|
}
|
|
22
24
|
/** The prompt of the install flow; browsing shows nothing. */
|
|
23
25
|
export function ActionPanel({ mode }) {
|
|
26
|
+
if (isPackageMode(mode))
|
|
27
|
+
return _jsx(PackagePanel, { mode: mode });
|
|
24
28
|
switch (mode.kind) {
|
|
25
29
|
case "browse":
|
|
26
30
|
return null;
|