@iseoane/agent-depot 0.2.1 → 0.3.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 +22 -0
- package/README.md +109 -0
- package/dist/src/app-cli.js +172 -0
- package/dist/src/app-flow.js +457 -0
- package/dist/src/app-latest.js +63 -0
- package/dist/src/app-owned-skills.js +42 -0
- package/dist/src/app-recipes.js +116 -0
- package/dist/src/app-schema.js +45 -0
- package/dist/src/app-version.js +46 -0
- package/dist/src/atomic-file.js +26 -0
- package/dist/src/cli.js +112 -20
- package/dist/src/process-runner.js +10 -1
- package/dist/src/skill-install.js +12 -0
- package/dist/src/source-state.js +3 -28
- package/dist/src/tui/app-actions.js +157 -0
- package/dist/src/tui/catalog-installs.js +5 -1
- package/dist/src/tui/installations-browse.js +24 -2
- package/dist/src/tui/installations-tree.js +13 -1
- package/dist/src/tui/installations-view.js +46 -6
- package/dist/src/tui/installations.js +26 -3
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +6 -0
- package/dist/src/tui/unmanaged-actions.js +2 -2
- package/dist/src/tui/updates-flow.js +18 -3
- package/dist/src/tui/updates-keys.js +9 -0
- package/dist/src/tui/updates-model.js +3 -3
- package/dist/src/tui/updates-panel.js +4 -2
- package/dist/src/tui/updates-view.js +1 -1
- package/dist/src/tui/updates.js +76 -5
- package/dist/src/unmanaged-removal.js +3 -0
- package/dist/src/user-global-skill-inventory.js +7 -1
- package/package.json +4 -4
- package/skills/agent-depot-apprecipe/SKILL.md +44 -0
- package/skills/agent-depot-apprecipe/assets/example.md +25 -0
package/dist/src/tui/updates.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { checkAppUpdates, createAppOperations } from "../app-flow.js";
|
|
1
2
|
import { formatVersionPolicy } from "../skill-install.js";
|
|
2
3
|
import { applyConfirmedUpdates, describeUpdatePreview, loadUpdates, previewUpdateCandidates, readScopeInstallations, } from "../update-flow.js";
|
|
3
4
|
import { formatVersionEvidence } from "./installations.js";
|
|
4
5
|
/** Every scope the view can assess, in display order. */
|
|
5
6
|
export const UPDATE_SCOPES = ["project", "user-global"];
|
|
7
|
+
const APP_STATUS_LABEL = {
|
|
8
|
+
"update available": "update available",
|
|
9
|
+
current: "up to date",
|
|
10
|
+
unknown: "cannot assess",
|
|
11
|
+
};
|
|
6
12
|
const STATUS_LABEL = {
|
|
7
13
|
updateable: "update available",
|
|
8
14
|
current: "up to date",
|
|
@@ -61,6 +67,7 @@ async function refreshSources(operations, selections) {
|
|
|
61
67
|
* Changes nothing but the Source caches.
|
|
62
68
|
*/
|
|
63
69
|
export async function loadUpdateRows(operations, environment, options) {
|
|
70
|
+
const apps = createAppOperations({ homeDirectory: environment.homeDirectory, ...environment.appEnvironment });
|
|
64
71
|
let refresh = { attempted: false, refreshed: [], failed: [], unreadable: [] };
|
|
65
72
|
if (options.refresh) {
|
|
66
73
|
options.onPhase?.("refreshing");
|
|
@@ -81,6 +88,7 @@ export async function loadUpdateRows(operations, environment, options) {
|
|
|
81
88
|
}
|
|
82
89
|
}));
|
|
83
90
|
const rows = results.flatMap(({ scope, loaded }) => (loaded?.assessment.items ?? []).map((item) => ({
|
|
91
|
+
kind: "skill",
|
|
84
92
|
key: `${scope}:${item.id}`,
|
|
85
93
|
scope,
|
|
86
94
|
item,
|
|
@@ -91,7 +99,29 @@ export async function loadUpdateRows(operations, environment, options) {
|
|
|
91
99
|
status: STATUS_LABEL[item.status],
|
|
92
100
|
reason: item.reason,
|
|
93
101
|
})));
|
|
94
|
-
|
|
102
|
+
let appChecks = [];
|
|
103
|
+
let appError;
|
|
104
|
+
try {
|
|
105
|
+
appChecks = await checkAppUpdates(apps);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
appError = errorText(error);
|
|
109
|
+
}
|
|
110
|
+
for (const check of appChecks) {
|
|
111
|
+
rows.push({
|
|
112
|
+
kind: "app",
|
|
113
|
+
key: `app:${check.entry.file}`,
|
|
114
|
+
scope: "user-global",
|
|
115
|
+
check,
|
|
116
|
+
path: `App: ${check.entry.recipe?.name ?? check.entry.file}`,
|
|
117
|
+
policy: "latest",
|
|
118
|
+
installed: check.installedVersion ?? "?",
|
|
119
|
+
available: check.latestVersion ?? "?",
|
|
120
|
+
status: APP_STATUS_LABEL[check.status],
|
|
121
|
+
reason: check.reason ?? check.inspection.reason ?? check.inspection.status,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return { results, rows, refresh, apps, appError, checkedAt: (options.now ?? Date.now)() };
|
|
95
125
|
}
|
|
96
126
|
/** `just now`, `42s ago`, `5 min ago`, `2 h ago`. */
|
|
97
127
|
export function formatAgo(elapsedMs) {
|
|
@@ -108,10 +138,12 @@ export function formatAgo(elapsedMs) {
|
|
|
108
138
|
/** Plan step: previews the selection from the assessed snapshots, scope by scope. Changes nothing. */
|
|
109
139
|
export async function prepareUpdates(data, selected) {
|
|
110
140
|
const groups = [];
|
|
141
|
+
const appPlans = [];
|
|
142
|
+
const appFailures = [];
|
|
111
143
|
const lines = [];
|
|
112
144
|
const failures = [];
|
|
113
145
|
for (const { scope, loaded } of data.results) {
|
|
114
|
-
const items = selected.filter((row) => row.scope === scope).map((row) => row.item);
|
|
146
|
+
const items = selected.filter((row) => row.kind !== "app" && row.scope === scope).map((row) => row.item);
|
|
115
147
|
if (!loaded || items.length === 0)
|
|
116
148
|
continue;
|
|
117
149
|
const { context } = loaded;
|
|
@@ -126,10 +158,22 @@ export async function prepareUpdates(data, selected) {
|
|
|
126
158
|
overwriteShown: new Set(outcome.plans.filter((plan) => plan.overwriteRequired).map((plan) => plan.item.id)),
|
|
127
159
|
});
|
|
128
160
|
}
|
|
161
|
+
for (const row of selected) {
|
|
162
|
+
if (row.kind !== "app" || !data.apps)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
const plan = await data.apps.planLifecycle(row.check.entry, "update", undefined, row.check);
|
|
166
|
+
appPlans.push(plan);
|
|
167
|
+
lines.push(`Environment: ${plan.environment}`, row.path, `Version: ${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}`, ...(plan.argv ? [`argv: ${JSON.stringify(plan.argv)}`, `Executable: ${plan.executable ?? "unavailable"}`] : []), ...(plan.manual ? [`Manual: ${plan.manual}`] : []), `Working directory: ${plan.cwd}`, ...(plan.warning ? [plan.warning] : []));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
appFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
129
173
|
return {
|
|
130
|
-
groups,
|
|
174
|
+
groups, apps: data.apps, appPlans, appFailures,
|
|
131
175
|
lines,
|
|
132
|
-
applicable: groups.flatMap((group) => group.applicable),
|
|
176
|
+
applicable: [...groups.flatMap((group) => group.applicable), ...appPlans],
|
|
133
177
|
failures,
|
|
134
178
|
nonCanonicalPaths: groups.flatMap((group) => group.nonCanonicalPaths.map((location) => ({ scope: group.scope, path: location }))),
|
|
135
179
|
};
|
|
@@ -138,9 +182,11 @@ export async function prepareUpdates(data, selected) {
|
|
|
138
182
|
* Applies exactly the previewed items, scope by scope. Confirming the preview
|
|
139
183
|
* confirms overwriting and external commands; a non-canonical path is applied
|
|
140
184
|
* only when the caller passes it (with its scope) after the separate confirmation.
|
|
185
|
+
* confirmManual pauses for manual completion, showing the fallback reason. A false
|
|
186
|
+
* answer (or absent callback) cancels and clears pending update evidence.
|
|
141
187
|
* A Skill that became locally modified after the preview fails instead of being overwritten.
|
|
142
188
|
*/
|
|
143
|
-
export async function runUpdates(prepared, confirmedPaths) {
|
|
189
|
+
export async function runUpdates(prepared, confirmedPaths, confirmManual) {
|
|
144
190
|
const updated = [];
|
|
145
191
|
const failedLines = [];
|
|
146
192
|
for (const group of prepared.groups) {
|
|
@@ -150,6 +196,31 @@ export async function runUpdates(prepared, confirmedPaths) {
|
|
|
150
196
|
updated.push(...result.updated.map((item) => `Updated ${item.selection.path}`));
|
|
151
197
|
failedLines.push(...result.failed.map((item) => `Failed ${item.selection.path}: ${item.error ?? "unknown error"}`));
|
|
152
198
|
}
|
|
199
|
+
for (const plan of prepared.appPlans) {
|
|
200
|
+
const name = `App: ${plan.entry.recipe.name}`;
|
|
201
|
+
try {
|
|
202
|
+
const apps = prepared.apps;
|
|
203
|
+
if (!apps)
|
|
204
|
+
throw new Error("App operations unavailable; check again");
|
|
205
|
+
let result = await apps.executeLifecycle(plan, true);
|
|
206
|
+
if (result.status === "manual required") {
|
|
207
|
+
if (!await confirmManual?.(plan, result.reason)) {
|
|
208
|
+
await apps.cancelManual(plan);
|
|
209
|
+
failedLines.push(`Cancelled ${name}${result.reason ? `: ${result.reason}` : ""}`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
result = await apps.completeManual(plan, true);
|
|
213
|
+
}
|
|
214
|
+
if (result.status === "failed")
|
|
215
|
+
failedLines.push(`Failed ${name}: ${result.reason}`);
|
|
216
|
+
else
|
|
217
|
+
updated.push(`Updated ${name}: installed ${result.previousVersion} -> ${result.installedVersion} (latest ${result.latestVersion ?? "unknown"})`);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
failedLines.push(`Failed ${name}: ${errorText(error)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
failedLines.push(...prepared.appFailures);
|
|
153
224
|
const previewFailed = prepared.failures.map(({ item, message }) => `Failed ${item.selection.path}: preview failed: ${message}`);
|
|
154
225
|
const failed = failedLines.length + previewFailed.length;
|
|
155
226
|
return {
|
|
@@ -21,11 +21,13 @@ export async function removeInspectedUnmanagedSkill(context, inspection, expecte
|
|
|
21
21
|
const latestManagedInstallations = await operations.listUserGlobalInstallations();
|
|
22
22
|
assertManagedInstallationRecordsUnchanged(expectedManagedInstallations, latestManagedInstallations);
|
|
23
23
|
const latestInspection = await inspectUserGlobalSkillRemoval(inspection.path, {
|
|
24
|
+
...context,
|
|
24
25
|
homeDirectory,
|
|
25
26
|
managedInstallations: latestManagedInstallations,
|
|
26
27
|
});
|
|
27
28
|
assertUnmanagedInspectionUnchanged(inspection, latestInspection);
|
|
28
29
|
await removeUserGlobalSkill(inspection, {
|
|
30
|
+
...context,
|
|
29
31
|
homeDirectory,
|
|
30
32
|
managedInstallations: latestManagedInstallations,
|
|
31
33
|
readManagedInstallations: async () => operations.listUserGlobalInstallations(),
|
|
@@ -38,6 +40,7 @@ export async function removeInspectedUnmanagedSymlink(context, inspection, expec
|
|
|
38
40
|
const latestManagedInstallations = await operations.listUserGlobalInstallations();
|
|
39
41
|
assertManagedInstallationRecordsUnchanged(expectedManagedInstallations, latestManagedInstallations);
|
|
40
42
|
await removeUserGlobalSymlink(inspection, {
|
|
43
|
+
...context,
|
|
41
44
|
homeDirectory,
|
|
42
45
|
managedInstallations: latestManagedInstallations,
|
|
43
46
|
readManagedInstallations: async () => operations.listUserGlobalInstallations(),
|
|
@@ -2,6 +2,7 @@ import { lstat, readdir, readFile, readlink, rename, rm, unlink } from "node:fs/
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { assertSkillNotAppOwned, loadAppOwnedSkillFilter } from "./app-owned-skills.js";
|
|
5
6
|
import { pathsOverlap } from "./path-safety.js";
|
|
6
7
|
import { readExistingSkillTree } from "./skill-adoption.js";
|
|
7
8
|
import { parseSkillFrontmatter, skillTreeBaseline } from "./skill-discovery.js";
|
|
@@ -49,7 +50,8 @@ export async function scanUserGlobalSkillInventory(options = {}) {
|
|
|
49
50
|
const skipped = [];
|
|
50
51
|
const exposures = [];
|
|
51
52
|
const symlinks = [];
|
|
52
|
-
const
|
|
53
|
+
const isAppOwned = await loadAppOwnedSkillFilter({ ...options, homeDirectory });
|
|
54
|
+
const state = { isAppOwned, managedPaths, managedCanonicalPaths, entries, skipped, exposures, symlinks };
|
|
53
55
|
for (const root of GLOBAL_SKILL_ROOTS) {
|
|
54
56
|
const rootPath = path.join(homeDirectory, ...root.relativePath);
|
|
55
57
|
await scanRoot(root, rootPath, homeDirectory, state);
|
|
@@ -106,6 +108,8 @@ async function listScannableRoot(root, rootPath, homeDirectory, skipped) {
|
|
|
106
108
|
}
|
|
107
109
|
async function scanRootEntry(root, name, candidatePath, homeDirectory, state) {
|
|
108
110
|
const { managedPaths, entries, skipped } = state;
|
|
111
|
+
if (!managedPaths.has(pathKey(candidatePath)) && state.isAppOwned(name))
|
|
112
|
+
return;
|
|
109
113
|
if (isTransientName(name)) {
|
|
110
114
|
skipped.push({ name, path: candidatePath, root: root.kind, reason: "excluded-transient" });
|
|
111
115
|
return;
|
|
@@ -288,6 +292,7 @@ async function inspectCanonicalTarget(candidatePath) {
|
|
|
288
292
|
export async function inspectUserGlobalSkillRemoval(candidatePath, options) {
|
|
289
293
|
const homeDirectory = path.resolve(options.homeDirectory);
|
|
290
294
|
const targetPath = validateExplicitGlobalPath(candidatePath, homeDirectory);
|
|
295
|
+
await assertSkillNotAppOwned(path.basename(candidatePath), options);
|
|
291
296
|
const fileSystem = options.fileSystem ?? nodeFileSystem;
|
|
292
297
|
await assertRealGlobalAncestors(homeDirectory, path.dirname(targetPath), fileSystem);
|
|
293
298
|
const managedPaths = managedProtectionPaths(homeDirectory, options.managedInstallations);
|
|
@@ -382,6 +387,7 @@ export async function removeUserGlobalSkill(inspection, options) {
|
|
|
382
387
|
export async function inspectUserGlobalSymlinkRemoval(candidatePath, options) {
|
|
383
388
|
const homeDirectory = path.resolve(options.homeDirectory);
|
|
384
389
|
const linkPath = validateExplicitGlobalPath(candidatePath, homeDirectory);
|
|
390
|
+
await assertSkillNotAppOwned(path.basename(candidatePath), options);
|
|
385
391
|
const fileSystem = options.fileSystem ?? nodeFileSystem;
|
|
386
392
|
await assertRealGlobalAncestors(homeDirectory, path.dirname(linkPath), fileSystem);
|
|
387
393
|
const managedPath = managedProtectionPaths(homeDirectory, options.managedInstallations)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iseoane/agent-depot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A cross-platform catalog for reusable coding-agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "iseoane",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"lint": "eslint \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
|
|
47
|
-
"test": "pnpm build && node --test \"dist/tests/*.test.js\"",
|
|
48
|
-
"test:single": "pnpm build && node -e \"const { spawnSync } = require('node:child_process'); const input = process.argv[1] || 'tests/version.test.ts'; const file = input.replace(/^tests[\\\\/]/, 'dist/tests/').replace(/\\\\.ts$/i, '.js'); process.exit(spawnSync(process.execPath, ['--test', file], { stdio: 'inherit' }).status ?? 1);\"",
|
|
49
|
-
"test:coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && tsc -p tsconfig.coverage.json && node -e \"const { spawnSync } = require('node:child_process'); process.exit(spawnSync(process.execPath, ['--enable-source-maps','--test','--test-reporter=dot','dist/tests/*.test.js'], { stdio: 'inherit', env: { ...process.env, NODE_V8_COVERAGE: 'coverage/v8' } }).status ?? 1);\"",
|
|
47
|
+
"test": "pnpm build && node --test --import ./dist/tests/isolated-env.js \"dist/tests/*.test.js\"",
|
|
48
|
+
"test:single": "pnpm build && node -e \"const { spawnSync } = require('node:child_process'); const input = process.argv[1] || 'tests/version.test.ts'; const file = input.replace(/^tests[\\\\/]/, 'dist/tests/').replace(/\\\\.ts$/i, '.js'); process.exit(spawnSync(process.execPath, ['--import', './dist/tests/isolated-env.js', '--test', file], { stdio: 'inherit' }).status ?? 1);\"",
|
|
49
|
+
"test:coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && tsc -p tsconfig.coverage.json && node -e \"const { spawnSync } = require('node:child_process'); process.exit(spawnSync(process.execPath, ['--enable-source-maps','--import','./dist/tests/isolated-env.js','--test','--test-reporter=dot','dist/tests/*.test.js'], { stdio: 'inherit', env: { ...process.env, NODE_V8_COVERAGE: 'coverage/v8' } }).status ?? 1);\"",
|
|
50
50
|
"audit:dead-code": "npx --yes fallow@3.30.0 dead-code --fail-on-issues",
|
|
51
51
|
"audit:dupes": "npx --yes fallow@3.30.0 dupes",
|
|
52
52
|
"audit:health": "pnpm test:coverage && npx --yes fallow@3.30.0 health --coverage coverage/v8 --min-score 85",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "agent-depot-apprecipe"
|
|
3
|
+
description: "Trigger: App recipe creation or repair. Research an App's actual install channel and write platform-aware Agent Depot recipes."
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: "iseoane"
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Activation Contract
|
|
11
|
+
|
|
12
|
+
Use for creating or repairing user-global App recipes, not portable Skills.
|
|
13
|
+
|
|
14
|
+
## Hard Rules
|
|
15
|
+
|
|
16
|
+
- Read `agent-depot app schema` first: it is the format source of truth. Research upstream README, install docs and releases through the web or GitHub API; never clone the App repository.
|
|
17
|
+
- Write user-owned JSON in the environment-local `apps/` beside `sources.json`. Preserve existing files unless the user authorizes replacement. Keep Apps out of Sources and project manifests.
|
|
18
|
+
- Never approve a recipe on the user's behalf. Approval belongs to the user via `agent-depot app approve <name> --yes`; changed bytes need renewed approval. Validation is not approval.
|
|
19
|
+
- Use bare executable-plus-arguments arrays, without shells, shell syntax, credentials or environment assignments. Script-only routes (`curl | sh`, PowerShell scripts, `.cmd`) belong in `manual` text, never pipe-to-shell argv. Agent Depot displays manual text without executing it.
|
|
20
|
+
- Steps run from the user's home with stdin EOF and no timeout. Use non-interactive flags; terminal-dependent installers belong in manual steps.
|
|
21
|
+
|
|
22
|
+
## Decision Gates
|
|
23
|
+
|
|
24
|
+
| Evidence | Recipe decision |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Identical steps on all supported platforms | Omit `platform`. |
|
|
27
|
+
| Steps differ by OS or architecture | Use separate files with the same App `name` and explicit `linux`, `windows` or `darwin`; ask about architecture where needed. Never create duplicate applicable names. |
|
|
28
|
+
| Unsupported platform | Report it; invent no steps. WSL is Linux, not Windows. |
|
|
29
|
+
| Several installs or uncertain channel | List every path/channel and ask the user to choose before writing lifecycle steps. |
|
|
30
|
+
| brew / npm / pipx | Derive update and uninstall from that channel: upgrade/uninstall, global install/uninstall, or upgrade/uninstall respectively. |
|
|
31
|
+
| Go install / release binary | Use the documented update route and a manual binary-removal step. |
|
|
32
|
+
| App has its own uninstall | Prefer it after verifying scope and non-interactive flags; explain remaining channel cleanup. |
|
|
33
|
+
|
|
34
|
+
## Execution Steps
|
|
35
|
+
|
|
36
|
+
1. Record upstream evidence and platform support. Inspect all PATH matches (`which -a`/`whence -a`, or Windows `Get-Command -All`), resolved paths, and channel inventories such as `brew list`, `npm ls -g` or `pipx list`. List Windows shims seen from WSL separately: `/mnt/<drive>/` executables cannot serve a Linux recipe. Finish only when the actual channel is established or the user has chosen one.
|
|
37
|
+
2. Write required `name`, `install`, `update`, `uninstall`, and `version`. Each lifecycle step uses `argv`, `manual`, or both; manual fallback applies only to spawn failure, never non-zero exit. `version` needs read-only argv and a JavaScript regex with exactly one capture group. Choose optional `latest` as `github: owner/repo`, `npm: package`, or read-only `argv` plus `pattern`; omit it when unknown. Consult [the illustrative example](assets/example.md), not as upstream command evidence.
|
|
38
|
+
3. Put user-global Host wiring in optional `setup`/`teardown` maps keyed by `pi`, `claude`, `codex`, or `opencode`. Keep install separate; uninstall never implicitly runs teardown. Delegate wiring to App commands or manual user instructions, not direct agent edits of Host configuration. Exclude project initialization.
|
|
39
|
+
4. Declare optional `skills` only for documented App-owned directory/link names. Match whole names case-sensitively; only `*` is special, while `?` and brackets are literal. Reject empty/whitespace-only, path separators, NUL and star-only patterns. These declarations filter unmanaged adoption/removal even before approval/install; they verify no artifacts or provenance.
|
|
40
|
+
5. Run `agent-depot app validate <file>...` for current-platform recipes. Fix structural errors; report unapproved recipes as needs approval, not runtime-verified. Version commands run only for approved applicable content. Mark other platforms unverified. After user approval, the user can inspect `app list` and separately preview `app install|update|uninstall <name>` or `app setup|teardown <name> --host <host>`.
|
|
41
|
+
|
|
42
|
+
## Output Contract
|
|
43
|
+
|
|
44
|
+
Return file paths, platform/channel evidence, validation results and runtime-verification limits; unsupported platforms; omitted optional fields and why; license, telemetry/update-check behavior (or unknown); and conflicting duplicate install routes, including plugin-marketplace versus setup routes. Explain that version alone determines installed state, updates require a changed version, and unknown latest is not an available update. End with the user's approval command, without running it.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Illustrative recipe shape
|
|
2
|
+
|
|
3
|
+
This fictional App is a format example, not a recommended installation or shipped
|
|
4
|
+
App recipe. Replace commands with verified upstream evidence and the chosen
|
|
5
|
+
channel. Omitted platform means identical steps everywhere; optional fields may
|
|
6
|
+
be omitted rather than populated speculatively.
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"name": "example-tool",
|
|
11
|
+
"install": { "argv": ["npm", "install", "-g", "example-tool"] },
|
|
12
|
+
"update": { "argv": ["npm", "install", "-g", "example-tool"] },
|
|
13
|
+
"uninstall": { "argv": ["npm", "uninstall", "-g", "example-tool"] },
|
|
14
|
+
"version": { "argv": ["example-tool", "--version"], "pattern": "(\\d+\\.\\d+\\.\\d+)" },
|
|
15
|
+
"latest": { "npm": "example-tool" },
|
|
16
|
+
"setup": { "claude": { "argv": ["example-tool", "setup", "claude", "--yes"] } },
|
|
17
|
+
"teardown": { "claude": { "manual": "Follow the documented example-tool Claude teardown instructions." } },
|
|
18
|
+
"skills": ["example-tool-*"]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `latest: { "github": "owner/repo" }` for public GitHub Releases, or
|
|
23
|
+
`latest: { "argv": ["example-tool", "latest"], "pattern": "(\\d+\\.\\d+\\.\\d+)" }`
|
|
24
|
+
for a documented read-only command. Choose one latest signal, not a combination.
|
|
25
|
+
A step with both `argv` and `manual` falls back only when spawning fails.
|