akm-cli 0.5.0 → 0.6.0-rc2
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 +53 -5
- package/README.md +9 -9
- package/dist/cli.js +379 -1448
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/commands/curate.js +263 -0
- package/dist/{info.js → commands/info.js} +17 -11
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +14 -2
- package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
- package/dist/commands/migration-help.js +141 -0
- package/dist/{registry-search.js → commands/registry-search.js} +68 -9
- package/dist/commands/remember.js +178 -0
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +3 -3
- package/dist/{stash-show.js → commands/show.js} +106 -81
- package/dist/{stash-add.js → commands/source-add.js} +133 -67
- package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
- package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
- package/dist/{common.js → core/common.js} +147 -50
- package/dist/{config.js → core/config.js} +288 -29
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
- package/dist/{paths.js → core/paths.js} +4 -4
- package/dist/core/write-source.js +280 -0
- package/dist/{local-search.js → indexer/db-search.js} +49 -32
- package/dist/{db.js → indexer/db.js} +210 -81
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +153 -30
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +4 -7
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +97 -55
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
- package/dist/{llm.js → llm/client.js} +12 -48
- package/dist/llm/embedder.js +127 -0
- package/dist/llm/embedders/cache.js +47 -0
- package/dist/llm/embedders/local.js +152 -0
- package/dist/llm/embedders/remote.js +121 -0
- package/dist/llm/embedders/types.js +39 -0
- package/dist/llm/metadata-enhance.js +53 -0
- package/dist/output/cli-hints.js +301 -0
- package/dist/output/context.js +95 -0
- package/dist/{renderers.js → output/renderers.js} +57 -61
- package/dist/output/shapes.js +212 -0
- package/dist/output/text.js +520 -0
- package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/registry/providers/index.js +11 -0
- package/dist/{providers → registry/providers}/skills-sh.js +60 -4
- package/dist/{providers → registry/providers}/static-index.js +126 -56
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +162 -129
- package/dist/setup/steps.js +45 -0
- package/dist/{kit-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +218 -28
- package/dist/{stash-providers → sources/providers}/index.js +4 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/sources/providers/npm.js +160 -0
- package/dist/sources/providers/provider-utils.js +173 -0
- package/dist/sources/providers/sync-from-ref.js +45 -0
- package/dist/sources/providers/tar-utils.js +154 -0
- package/dist/{stash-providers → sources/providers}/website.js +60 -20
- package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
- package/dist/{wiki.js → wiki/wiki.js} +18 -17
- package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/README.md +30 -0
- package/docs/migration/release-notes/0.0.13.md +4 -0
- package/docs/migration/release-notes/0.1.0.md +6 -0
- package/docs/migration/release-notes/0.2.0.md +6 -0
- package/docs/migration/release-notes/0.3.0.md +5 -0
- package/docs/migration/release-notes/0.5.0.md +6 -0
- package/docs/migration/release-notes/0.6.0.md +75 -0
- package/docs/migration/release-notes/README.md +21 -0
- package/package.json +3 -2
- package/dist/embedder.js +0 -351
- package/dist/errors.js +0 -34
- package/dist/migration-help.js +0 -110
- package/dist/registry-factory.js +0 -19
- package/dist/registry-install.js +0 -532
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -1
- package/dist/stash-providers/filesystem.js +0 -41
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-providers/provider-utils.js +0 -11
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -251
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { isHttpUrl, resolveStashDir } from "
|
|
4
|
-
import { loadConfig, loadUserConfig, saveConfig } from "
|
|
5
|
-
import { UsageError } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { parseRegistryRef } from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
3
|
+
import { isHttpUrl, resolveStashDir } from "../core/common";
|
|
4
|
+
import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
5
|
+
import { UsageError } from "../core/errors";
|
|
6
|
+
import { warn } from "../core/warn";
|
|
7
|
+
import { akmIndex } from "../indexer/indexer";
|
|
8
|
+
import { upsertLockEntry } from "../integrations/lockfile";
|
|
9
|
+
import { parseRegistryRef } from "../registry/resolve";
|
|
10
|
+
import { detectStashRoot } from "../sources/providers/provider-utils";
|
|
11
|
+
import { syncFromRef } from "../sources/providers/sync-from-ref";
|
|
12
|
+
import { ensureWebsiteMirror, validateWebsiteInputUrl } from "../sources/providers/website";
|
|
13
|
+
import { ensureWikiNameAvailable, validateWikiName } from "../wiki/wiki";
|
|
14
|
+
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
|
|
13
15
|
const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
|
|
14
16
|
export async function akmAdd(input) {
|
|
15
17
|
const ref = input.ref.trim();
|
|
16
18
|
if (!ref)
|
|
17
19
|
throw new UsageError("Install ref or local directory is required. " +
|
|
18
|
-
"Examples: `akm add @scope/
|
|
20
|
+
"Examples: `akm add @scope/stash`, `akm add github:owner/repo`, `akm add ./local/path`");
|
|
19
21
|
// Validate and resolve wiki name when --type wiki is used
|
|
20
22
|
let wikiName;
|
|
21
23
|
if (input.overrideType) {
|
|
@@ -30,7 +32,7 @@ export async function akmAdd(input) {
|
|
|
30
32
|
}
|
|
31
33
|
const stashDir = resolveStashDir();
|
|
32
34
|
if (shouldAddAsWebsiteUrl(ref)) {
|
|
33
|
-
return
|
|
35
|
+
return addWebsiteSource(ref, stashDir, input.name ?? wikiName, input.options, wikiName);
|
|
34
36
|
}
|
|
35
37
|
// Detect local directory refs and route them to stashes[] instead of installed[]
|
|
36
38
|
try {
|
|
@@ -39,13 +41,13 @@ export async function akmAdd(input) {
|
|
|
39
41
|
if (input.trustThisInstall) {
|
|
40
42
|
warn("--trust has no effect on local directory sources; the install audit is not run for local paths.");
|
|
41
43
|
}
|
|
42
|
-
return
|
|
44
|
+
return addLocalSource(ref, parsed.sourcePath, stashDir, wikiName, input.name);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
catch {
|
|
46
48
|
// Not a local ref — fall through to registry install
|
|
47
49
|
}
|
|
48
|
-
return
|
|
50
|
+
return addRegistryStash(ref, stashDir, input.trustThisInstall, input.writable, wikiName);
|
|
49
51
|
}
|
|
50
52
|
export async function registerWikiSource(input) {
|
|
51
53
|
const stashDir = resolveStashDir();
|
|
@@ -65,29 +67,39 @@ export async function registerWikiSource(input) {
|
|
|
65
67
|
* Add a local directory as a filesystem stash source.
|
|
66
68
|
* Creates a stashes[] entry instead of an installed[] entry.
|
|
67
69
|
*/
|
|
68
|
-
async function
|
|
70
|
+
async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName) {
|
|
69
71
|
const stashRoot = detectStashRoot(sourcePath);
|
|
70
72
|
const resolvedPath = path.resolve(stashRoot);
|
|
71
73
|
const config = loadUserConfig();
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
74
|
+
// Derive the canonical name: explicit --name wins, then wiki name, then readable path.
|
|
75
|
+
const derivedName = explicitName ?? wikiName ?? toReadableId(resolvedPath);
|
|
76
|
+
// Check for duplicates in sources[]
|
|
77
|
+
const sources = [...(config.sources ?? config.stashes ?? [])];
|
|
78
|
+
const existing = sources.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
|
|
75
79
|
let persistedEntry;
|
|
76
80
|
if (!existing) {
|
|
77
81
|
persistedEntry = {
|
|
78
82
|
type: "filesystem",
|
|
79
83
|
path: resolvedPath,
|
|
80
|
-
name:
|
|
84
|
+
name: derivedName,
|
|
81
85
|
...(wikiName ? { wikiName } : {}),
|
|
82
86
|
};
|
|
83
|
-
|
|
84
|
-
saveConfig({ ...config, stashes });
|
|
87
|
+
sources.push(persistedEntry);
|
|
88
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
85
89
|
}
|
|
86
90
|
else {
|
|
91
|
+
let changed = false;
|
|
92
|
+
// If --name was explicitly supplied, update the persisted name.
|
|
93
|
+
if (explicitName && existing.name !== explicitName) {
|
|
94
|
+
existing.name = explicitName;
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
87
97
|
if (wikiName && existing.wikiName !== wikiName) {
|
|
88
98
|
existing.wikiName = wikiName;
|
|
89
|
-
|
|
99
|
+
changed = true;
|
|
90
100
|
}
|
|
101
|
+
if (changed)
|
|
102
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
91
103
|
persistedEntry = existing;
|
|
92
104
|
}
|
|
93
105
|
const index = await akmIndex({ stashDir });
|
|
@@ -96,7 +108,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
96
108
|
schemaVersion: 1,
|
|
97
109
|
stashDir,
|
|
98
110
|
ref: wikiName ?? ref,
|
|
99
|
-
|
|
111
|
+
sourceAdded: {
|
|
100
112
|
type: "filesystem",
|
|
101
113
|
path: resolvedPath,
|
|
102
114
|
name: persistedEntry.name ?? toReadableId(resolvedPath),
|
|
@@ -104,7 +116,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
104
116
|
...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
|
|
105
117
|
},
|
|
106
118
|
config: {
|
|
107
|
-
|
|
119
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
108
120
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
109
121
|
},
|
|
110
122
|
index: {
|
|
@@ -116,11 +128,11 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
116
128
|
},
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
|
-
async function
|
|
131
|
+
async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
120
132
|
const normalizedUrl = validateWebsiteInputUrl(ref);
|
|
121
133
|
const config = loadUserConfig();
|
|
122
|
-
const
|
|
123
|
-
let entry =
|
|
134
|
+
const sources = [...(config.sources ?? config.stashes ?? [])];
|
|
135
|
+
let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
|
|
124
136
|
if (!entry) {
|
|
125
137
|
entry = {
|
|
126
138
|
type: "website",
|
|
@@ -129,8 +141,8 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
129
141
|
...(options && Object.keys(options).length > 0 ? { options } : {}),
|
|
130
142
|
...(wikiName ? { wikiName } : {}),
|
|
131
143
|
};
|
|
132
|
-
|
|
133
|
-
saveConfig({ ...config, stashes });
|
|
144
|
+
sources.push(entry);
|
|
145
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
134
146
|
}
|
|
135
147
|
else {
|
|
136
148
|
let changed = false;
|
|
@@ -143,7 +155,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
143
155
|
changed = true;
|
|
144
156
|
}
|
|
145
157
|
if (changed)
|
|
146
|
-
saveConfig({ ...config, stashes });
|
|
158
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
147
159
|
}
|
|
148
160
|
const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
|
|
149
161
|
const index = await akmIndex({ stashDir });
|
|
@@ -152,7 +164,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
152
164
|
schemaVersion: 1,
|
|
153
165
|
stashDir,
|
|
154
166
|
ref: wikiName ?? ref,
|
|
155
|
-
|
|
167
|
+
sourceAdded: {
|
|
156
168
|
type: "website",
|
|
157
169
|
url: normalizedUrl,
|
|
158
170
|
name: entry.name,
|
|
@@ -160,7 +172,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
160
172
|
...(entry.wikiName ? { wiki: entry.wikiName } : {}),
|
|
161
173
|
},
|
|
162
174
|
config: {
|
|
163
|
-
|
|
175
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
164
176
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
165
177
|
},
|
|
166
178
|
index: {
|
|
@@ -173,34 +185,59 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
173
185
|
};
|
|
174
186
|
}
|
|
175
187
|
/**
|
|
176
|
-
* Install a
|
|
188
|
+
* Install a stash from a registry (npm, github, git) by dispatching to the
|
|
189
|
+
* matching syncable provider, then running the post-sync install audit and
|
|
190
|
+
* persisting the lock entry.
|
|
177
191
|
*/
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiName) {
|
|
193
|
+
// Pre-sync registry-policy enforcement uses just the parsed ref (no fetch needed),
|
|
194
|
+
// so we keep parity with the historical behavior where `enforceRegistryInstallPolicy`
|
|
195
|
+
// ran before `extractTarGzSecure` etc.
|
|
196
|
+
const config = loadConfig();
|
|
197
|
+
const synced = await syncFromRef(ref, { trustThisInstall, writable });
|
|
198
|
+
const registryLabels = deriveRegistryLabels({
|
|
199
|
+
source: synced.source,
|
|
200
|
+
ref: synced.ref,
|
|
201
|
+
artifactUrl: synced.artifactUrl,
|
|
202
|
+
});
|
|
203
|
+
enforceRegistryInstallPolicy(registryLabels, config, ref);
|
|
204
|
+
// Post-sync hook: install audit. Throws when blocked unless `--trust` is set
|
|
205
|
+
// (in which case the audit report still surfaces in the response).
|
|
206
|
+
const audit = auditInstallCandidate({
|
|
207
|
+
rootDir: synced.extractedDir,
|
|
208
|
+
source: synced.source,
|
|
209
|
+
ref: synced.ref,
|
|
210
|
+
registryLabels,
|
|
211
|
+
config,
|
|
212
|
+
trustThisInstall,
|
|
213
|
+
});
|
|
214
|
+
if (audit.blocked) {
|
|
215
|
+
throw new Error(formatInstallAuditFailure(synced.ref, audit));
|
|
216
|
+
}
|
|
217
|
+
const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === synced.id);
|
|
218
|
+
const updatedConfig = upsertInstalledRegistryEntry({
|
|
219
|
+
id: synced.id,
|
|
220
|
+
source: synced.source,
|
|
221
|
+
ref: synced.ref,
|
|
222
|
+
artifactUrl: synced.artifactUrl,
|
|
223
|
+
resolvedVersion: synced.resolvedVersion,
|
|
224
|
+
resolvedRevision: synced.resolvedRevision,
|
|
225
|
+
stashRoot: synced.contentDir,
|
|
226
|
+
cacheDir: synced.cacheDir,
|
|
227
|
+
installedAt: synced.syncedAt,
|
|
228
|
+
writable: synced.writable,
|
|
192
229
|
...(wikiName ? { wikiName } : {}),
|
|
193
230
|
});
|
|
194
231
|
await upsertLockEntry({
|
|
195
|
-
id:
|
|
196
|
-
source:
|
|
197
|
-
ref:
|
|
198
|
-
resolvedVersion:
|
|
199
|
-
resolvedRevision:
|
|
200
|
-
integrity:
|
|
232
|
+
id: synced.id,
|
|
233
|
+
source: synced.source,
|
|
234
|
+
ref: synced.ref,
|
|
235
|
+
resolvedVersion: synced.resolvedVersion,
|
|
236
|
+
resolvedRevision: synced.resolvedRevision,
|
|
237
|
+
integrity: synced.integrity,
|
|
201
238
|
});
|
|
202
239
|
// Clean up old cache directory on re-install
|
|
203
|
-
if (replaced && replaced.cacheDir !==
|
|
240
|
+
if (replaced && replaced.cacheDir !== synced.cacheDir) {
|
|
204
241
|
try {
|
|
205
242
|
fs.rmSync(replaced.cacheDir, { recursive: true, force: true });
|
|
206
243
|
}
|
|
@@ -214,21 +251,21 @@ async function addRegistryKit(ref, stashDir, trustThisInstall, writable, wikiNam
|
|
|
214
251
|
stashDir,
|
|
215
252
|
ref,
|
|
216
253
|
installed: {
|
|
217
|
-
id:
|
|
218
|
-
source:
|
|
219
|
-
ref:
|
|
220
|
-
artifactUrl:
|
|
221
|
-
resolvedVersion:
|
|
222
|
-
resolvedRevision:
|
|
223
|
-
stashRoot:
|
|
224
|
-
cacheDir:
|
|
225
|
-
extractedDir:
|
|
226
|
-
installedAt:
|
|
227
|
-
audit
|
|
254
|
+
id: synced.id,
|
|
255
|
+
source: synced.source,
|
|
256
|
+
ref: synced.ref,
|
|
257
|
+
artifactUrl: synced.artifactUrl,
|
|
258
|
+
resolvedVersion: synced.resolvedVersion,
|
|
259
|
+
resolvedRevision: synced.resolvedRevision,
|
|
260
|
+
stashRoot: synced.contentDir,
|
|
261
|
+
cacheDir: synced.cacheDir,
|
|
262
|
+
extractedDir: synced.extractedDir,
|
|
263
|
+
installedAt: synced.syncedAt,
|
|
264
|
+
audit,
|
|
228
265
|
},
|
|
229
266
|
config: {
|
|
230
|
-
|
|
231
|
-
installedKitCount:
|
|
267
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
268
|
+
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
232
269
|
},
|
|
233
270
|
index: {
|
|
234
271
|
mode: index.mode,
|
|
@@ -239,6 +276,35 @@ async function addRegistryKit(ref, stashDir, trustThisInstall, writable, wikiNam
|
|
|
239
276
|
},
|
|
240
277
|
};
|
|
241
278
|
}
|
|
279
|
+
/** Persist or replace an installed stash entry in the user config. */
|
|
280
|
+
export function upsertInstalledRegistryEntry(entry) {
|
|
281
|
+
const current = loadUserConfig();
|
|
282
|
+
const currentInstalled = current.installed ?? [];
|
|
283
|
+
const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
|
|
284
|
+
const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
|
|
285
|
+
const nextConfig = { ...current, installed: nextInstalled };
|
|
286
|
+
saveConfig(nextConfig);
|
|
287
|
+
return nextConfig;
|
|
288
|
+
}
|
|
289
|
+
/** Remove an installed stash entry from the user config. */
|
|
290
|
+
export function removeInstalledRegistryEntry(id) {
|
|
291
|
+
const current = loadUserConfig();
|
|
292
|
+
const currentInstalled = current.installed ?? [];
|
|
293
|
+
const nextInstalled = currentInstalled.filter((item) => item.id !== id);
|
|
294
|
+
const nextConfig = {
|
|
295
|
+
...current,
|
|
296
|
+
installed: nextInstalled.length > 0 ? nextInstalled : undefined,
|
|
297
|
+
};
|
|
298
|
+
saveConfig(nextConfig);
|
|
299
|
+
return nextConfig;
|
|
300
|
+
}
|
|
301
|
+
function normalizeInstalledEntry(entry) {
|
|
302
|
+
return {
|
|
303
|
+
...entry,
|
|
304
|
+
stashRoot: path.resolve(entry.stashRoot),
|
|
305
|
+
cacheDir: path.resolve(entry.cacheDir),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
242
308
|
function toReadableId(resolvedPath) {
|
|
243
309
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
244
310
|
if (home && resolvedPath.startsWith(home + path.sep)) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { resolveAssetPath } from "
|
|
3
|
+
import { makeAssetRef, parseAssetRef } from "../core/asset-ref";
|
|
4
|
+
import { TYPE_DIRS } from "../core/asset-spec";
|
|
5
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
6
|
+
import { findSourceForPath, getPrimarySource, resolveSourceEntries } from "../indexer/search-source";
|
|
7
|
+
import { isRemoteOrigin, resolveSourcesForOrigin } from "../registry/origin-resolve";
|
|
8
|
+
import { syncFromRef } from "../sources/providers/sync-from-ref";
|
|
9
|
+
import { resolveAssetPath } from "../sources/resolve";
|
|
10
10
|
export async function akmClone(options) {
|
|
11
11
|
const parsed = parseAssetRef(options.sourceRef);
|
|
12
12
|
// When --dest is provided, the working stash is optional
|
|
13
13
|
let allSources;
|
|
14
14
|
try {
|
|
15
|
-
allSources =
|
|
15
|
+
allSources = resolveSourceEntries();
|
|
16
16
|
}
|
|
17
17
|
catch (err) {
|
|
18
18
|
if (options.dest) {
|
|
@@ -31,16 +31,16 @@ export async function akmClone(options) {
|
|
|
31
31
|
// Remote fetch fallback: if no local source matched and origin looks remote, fetch it
|
|
32
32
|
let remoteFetched;
|
|
33
33
|
if (searchSources.length === 0 && parsed.origin && isRemoteOrigin(parsed.origin, allSources)) {
|
|
34
|
-
const installResult = await
|
|
34
|
+
const installResult = await syncFromRef(parsed.origin);
|
|
35
35
|
const syntheticSource = {
|
|
36
|
-
path: installResult.
|
|
36
|
+
path: installResult.contentDir,
|
|
37
37
|
registryId: installResult.id,
|
|
38
38
|
};
|
|
39
39
|
searchSources = [syntheticSource];
|
|
40
40
|
allSources = [...allSources, syntheticSource];
|
|
41
41
|
remoteFetched = {
|
|
42
42
|
origin: parsed.origin,
|
|
43
|
-
stashRoot: installResult.
|
|
43
|
+
stashRoot: installResult.contentDir,
|
|
44
44
|
cacheDir: installResult.cacheDir,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
@@ -56,8 +56,10 @@ export async function akmClone(options) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
if (!sourcePath) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if (remoteFetched) {
|
|
60
|
+
throw new NotFoundError(`Source asset not found for ref: ${options.sourceRef} (remote package fetched but asset not found inside it)`, "ASSET_NOT_FOUND", "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.");
|
|
61
|
+
}
|
|
62
|
+
throw lastError ?? new NotFoundError(`Source asset not found for ref: ${options.sourceRef}`, "ASSET_NOT_FOUND");
|
|
61
63
|
}
|
|
62
64
|
const sourceSource = findSourceForPath(sourcePath, allSources);
|
|
63
65
|
const destName = options.newName ?? parsed.name;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { loadConfig, loadUserConfig, saveConfig } from "
|
|
3
|
-
import { UsageError } from "
|
|
4
|
-
import {
|
|
2
|
+
import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
3
|
+
import { UsageError } from "../core/errors";
|
|
4
|
+
import { resolveSourceEntries } from "../indexer/search-source";
|
|
5
5
|
// ── Operations ──────────────────────────────────────────────────────────────
|
|
6
6
|
/**
|
|
7
7
|
* Add a stash source (filesystem path or remote provider URL) to config.
|
|
8
8
|
*
|
|
9
9
|
* Filesystem paths are auto-detected when `target` does not start with
|
|
10
10
|
* `http://` or `https://`. URL sources require a `providerType` option
|
|
11
|
-
* (e.g. "
|
|
11
|
+
* (e.g. "website", "git").
|
|
12
12
|
*/
|
|
13
13
|
export function addStash(opts) {
|
|
14
14
|
const { target, name, providerType, options: providerOptions, writable } = opts;
|
|
15
15
|
const config = loadUserConfig();
|
|
16
|
-
const
|
|
16
|
+
const sources = [...(config.sources ?? config.stashes ?? [])];
|
|
17
17
|
const isRemoteUrl = target.startsWith("http://") ||
|
|
18
18
|
target.startsWith("https://") ||
|
|
19
19
|
target.startsWith("git@") ||
|
|
@@ -22,11 +22,11 @@ export function addStash(opts) {
|
|
|
22
22
|
let entry;
|
|
23
23
|
if (isRemoteUrl) {
|
|
24
24
|
if (!providerType) {
|
|
25
|
-
throw new UsageError("--provider is required for URL sources (e.g. --provider
|
|
25
|
+
throw new UsageError("--provider is required for URL sources (e.g. --provider git --provider website)");
|
|
26
26
|
}
|
|
27
27
|
// Deduplicate by URL
|
|
28
|
-
if (
|
|
29
|
-
return {
|
|
28
|
+
if (sources.some((s) => s.url === target)) {
|
|
29
|
+
return { sources, added: false, message: "Source URL already configured" };
|
|
30
30
|
}
|
|
31
31
|
entry = { type: providerType, url: target };
|
|
32
32
|
if (name)
|
|
@@ -39,16 +39,16 @@ export function addStash(opts) {
|
|
|
39
39
|
else {
|
|
40
40
|
// Filesystem path
|
|
41
41
|
const resolvedPath = path.resolve(target);
|
|
42
|
-
if (
|
|
43
|
-
return {
|
|
42
|
+
if (sources.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
|
|
43
|
+
return { sources, added: false, message: "Source path already configured" };
|
|
44
44
|
}
|
|
45
45
|
entry = { type: "filesystem", path: resolvedPath };
|
|
46
46
|
if (name)
|
|
47
47
|
entry.name = name;
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
saveConfig({ ...config, stashes });
|
|
51
|
-
return {
|
|
49
|
+
sources.push(entry);
|
|
50
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
51
|
+
return { sources, added: true, entry };
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Remove a stash source by URL, path, or name.
|
|
@@ -56,7 +56,7 @@ export function addStash(opts) {
|
|
|
56
56
|
*/
|
|
57
57
|
export function removeStash(target) {
|
|
58
58
|
const config = loadUserConfig();
|
|
59
|
-
const
|
|
59
|
+
const sources = [...(config.sources ?? config.stashes ?? [])];
|
|
60
60
|
const isUrl = target.startsWith("http://") ||
|
|
61
61
|
target.startsWith("https://") ||
|
|
62
62
|
target.startsWith("git@") ||
|
|
@@ -66,27 +66,27 @@ export function removeStash(target) {
|
|
|
66
66
|
// Try URL match first, then path, then name (most specific → least specific)
|
|
67
67
|
let idx = -1;
|
|
68
68
|
if (isUrl) {
|
|
69
|
-
idx =
|
|
69
|
+
idx = sources.findIndex((s) => s.url === target);
|
|
70
70
|
}
|
|
71
71
|
if (idx === -1 && resolvedPath) {
|
|
72
|
-
idx =
|
|
72
|
+
idx = sources.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
|
|
73
73
|
}
|
|
74
74
|
if (idx === -1) {
|
|
75
|
-
idx =
|
|
75
|
+
idx = sources.findIndex((s) => s.name === target);
|
|
76
76
|
}
|
|
77
77
|
if (idx === -1) {
|
|
78
|
-
return {
|
|
78
|
+
return { sources, removed: false, message: "No matching source found" };
|
|
79
79
|
}
|
|
80
|
-
const removed =
|
|
81
|
-
saveConfig({ ...config, stashes });
|
|
82
|
-
return {
|
|
80
|
+
const removed = sources.splice(idx, 1)[0];
|
|
81
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
82
|
+
return { sources, removed: true, entry: removed };
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* List all stash sources (local filesystem + configured stashes).
|
|
86
86
|
*/
|
|
87
87
|
export function listStashes() {
|
|
88
88
|
const config = loadConfig();
|
|
89
|
-
const localSources =
|
|
90
|
-
const
|
|
91
|
-
return { localSources,
|
|
89
|
+
const localSources = resolveSourceEntries();
|
|
90
|
+
const sources = config.sources ?? config.stashes ?? [];
|
|
91
|
+
return { localSources, sources };
|
|
92
92
|
}
|
|
@@ -66,6 +66,49 @@ export function listKeys(vaultPath) {
|
|
|
66
66
|
const text = fs.readFileSync(vaultPath, "utf8");
|
|
67
67
|
return { keys: scanKeys(text), comments: scanComments(text) };
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Return structured `entries` pairing each key with the nearest preceding
|
|
71
|
+
* comment line (if any). This replaces the parallel `keys[]` + `comments[]`
|
|
72
|
+
* shape used internally by `listKeys` with a single merged array, which is
|
|
73
|
+
* easier for callers to consume (QA #35).
|
|
74
|
+
*
|
|
75
|
+
* Values are never included — the same privacy guarantee as `listKeys`.
|
|
76
|
+
*/
|
|
77
|
+
export function listEntries(vaultPath) {
|
|
78
|
+
if (!fs.existsSync(vaultPath))
|
|
79
|
+
return [];
|
|
80
|
+
const text = fs.readFileSync(vaultPath, "utf8");
|
|
81
|
+
const lines = text.split(/\r?\n/);
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const entries = [];
|
|
84
|
+
let pendingComment;
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const trimmed = line.trimStart();
|
|
87
|
+
if (trimmed.startsWith("#")) {
|
|
88
|
+
// Capture the most recent comment before a key
|
|
89
|
+
pendingComment = trimmed.slice(1).trimStart() || undefined;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const m = line.match(ASSIGN_RE);
|
|
93
|
+
if (m) {
|
|
94
|
+
const key = m[1];
|
|
95
|
+
if (!seen.has(key)) {
|
|
96
|
+
seen.add(key);
|
|
97
|
+
const entry = { key };
|
|
98
|
+
if (pendingComment)
|
|
99
|
+
entry.comment = pendingComment;
|
|
100
|
+
entries.push(entry);
|
|
101
|
+
}
|
|
102
|
+
pendingComment = undefined;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Any non-comment, non-assignment line (including blank lines)
|
|
106
|
+
// breaks "nearest preceding comment line" association.
|
|
107
|
+
pendingComment = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return entries;
|
|
111
|
+
}
|
|
69
112
|
/**
|
|
70
113
|
* Read all KEY=value pairs from a vault file. Intended for programmatic
|
|
71
114
|
* callers that need to inject values into a process environment. Callers
|
|
@@ -30,7 +30,7 @@ export function makeAssetRef(type, name, origin) {
|
|
|
30
30
|
export function parseAssetRef(ref) {
|
|
31
31
|
const trimmed = ref.trim();
|
|
32
32
|
if (!trimmed)
|
|
33
|
-
throw new UsageError("Empty ref.");
|
|
33
|
+
throw new UsageError("Empty ref.", "MISSING_REQUIRED_ARGUMENT");
|
|
34
34
|
let origin;
|
|
35
35
|
let body = trimmed;
|
|
36
36
|
const boundary = trimmed.indexOf("//");
|
|
@@ -38,16 +38,16 @@ export function parseAssetRef(ref) {
|
|
|
38
38
|
origin = trimmed.slice(0, boundary);
|
|
39
39
|
body = trimmed.slice(boundary + 2);
|
|
40
40
|
if (!origin)
|
|
41
|
-
throw new UsageError("Empty origin in ref.");
|
|
41
|
+
throw new UsageError("Empty origin in ref.", "MISSING_REQUIRED_ARGUMENT");
|
|
42
42
|
}
|
|
43
43
|
const colon = body.indexOf(":");
|
|
44
44
|
if (colon <= 0) {
|
|
45
|
-
throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name
|
|
45
|
+
throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name, e.g. skill:deploy or knowledge:guide.md`, "MISSING_REQUIRED_ARGUMENT");
|
|
46
46
|
}
|
|
47
47
|
const rawType = body.slice(0, colon);
|
|
48
48
|
const rawName = body.slice(colon + 1);
|
|
49
49
|
if (!isAssetType(rawType)) {
|
|
50
|
-
throw new UsageError(`Invalid asset type: "${rawType}"
|
|
50
|
+
throw new UsageError(`Invalid asset type: "${rawType}".`, "MISSING_REQUIRED_ARGUMENT");
|
|
51
51
|
}
|
|
52
52
|
validateName(rawName);
|
|
53
53
|
const name = normalizeName(rawName);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central registry for asset type renderer and action builder maps.
|
|
3
3
|
*
|
|
4
|
-
* Previously these maps lived in `local-search.ts` and
|
|
5
|
-
* `asset-spec.ts` via a fragile `_setAssetTypeHooks` deferred
|
|
6
|
-
* pattern. If
|
|
7
|
-
* calls, hooks would be silently dropped.
|
|
4
|
+
* Previously these maps lived in `db-search.ts` (then `local-search.ts`) and
|
|
5
|
+
* were wired into `asset-spec.ts` via a fragile `_setAssetTypeHooks` deferred
|
|
6
|
+
* callback pattern. If the search module was imported after
|
|
7
|
+
* `registerAssetType()` calls, hooks would be silently dropped.
|
|
8
8
|
*
|
|
9
9
|
* This module is a simple singleton that both `asset-spec.ts` and
|
|
10
|
-
* `
|
|
10
|
+
* `db-search.ts` import from, eliminating the import-order dependency
|
|
11
11
|
* entirely.
|
|
12
12
|
*/
|
|
13
|
-
import { buildWorkflowAction } from "
|
|
13
|
+
import { buildWorkflowAction } from "../output/renderers";
|
|
14
14
|
/** Map asset types to their primary renderer names. */
|
|
15
15
|
export const TYPE_TO_RENDERER = {
|
|
16
16
|
script: "script-source",
|
|
@@ -53,3 +53,27 @@ export function registerTypeRenderer(type, rendererName) {
|
|
|
53
53
|
export function registerActionBuilder(type, builder) {
|
|
54
54
|
ACTION_BUILDERS[type] = builder;
|
|
55
55
|
}
|
|
56
|
+
export const defaultRendererRegistry = {
|
|
57
|
+
rendererNameFor(type) {
|
|
58
|
+
return TYPE_TO_RENDERER[type];
|
|
59
|
+
},
|
|
60
|
+
actionBuilderFor(type) {
|
|
61
|
+
return ACTION_BUILDERS[type];
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Build a registry from explicit maps. Useful for tests that need to assert
|
|
66
|
+
* rendering behavior without touching the global singletons.
|
|
67
|
+
*/
|
|
68
|
+
export function createRendererRegistry(maps) {
|
|
69
|
+
const renderers = maps.renderers ?? {};
|
|
70
|
+
const actionBuilders = maps.actionBuilders ?? {};
|
|
71
|
+
return {
|
|
72
|
+
rendererNameFor(type) {
|
|
73
|
+
return renderers[type];
|
|
74
|
+
},
|
|
75
|
+
actionBuilderFor(type) {
|
|
76
|
+
return actionBuilders[type];
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|