akm-cli 0.6.0-rc1 → 0.6.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 +33 -0
- package/README.md +9 -9
- package/dist/cli.js +199 -114
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/{curate.js → commands/curate.js} +8 -3
- package/dist/{info.js → commands/info.js} +15 -9
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +4 -7
- package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
- package/dist/{migration-help.js → commands/migration-help.js} +2 -2
- package/dist/{registry-search.js → commands/registry-search.js} +8 -6
- package/dist/{remember.js → commands/remember.js} +55 -49
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +69 -3
- package/dist/{stash-show.js → commands/show.js} +104 -84
- package/dist/{stash-add.js → commands/source-add.js} +42 -32
- package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
- 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} +1 -1
- package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
- package/dist/{config.js → core/config.js} +133 -56
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
- package/dist/core/write-source.js +280 -0
- package/dist/{db-search.js → indexer/db-search.js} +25 -19
- package/dist/{db.js → indexer/db.js} +79 -47
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +132 -33
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +3 -6
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +52 -41
- 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} +1 -1
- package/dist/{llm-client.js → llm/client.js} +1 -1
- package/dist/{embedders → llm/embedders}/local.js +2 -2
- package/dist/{embedders → llm/embedders}/remote.js +1 -1
- package/dist/{embedders → llm/embedders}/types.js +1 -1
- package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
- package/dist/{cli-hints.js → output/cli-hints.js} +3 -0
- package/dist/{output-context.js → output/context.js} +21 -3
- package/dist/{renderers.js → output/renderers.js} +9 -65
- package/dist/{output-shapes.js → output/shapes.js} +18 -4
- package/dist/{output-text.js → output/text.js} +2 -2
- package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
- 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/{providers → registry/providers}/index.js +1 -1
- package/dist/{providers → registry/providers}/skills-sh.js +59 -3
- package/dist/{providers → registry/providers}/static-index.js +80 -12
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
- 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} +16 -56
- package/dist/{stash-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 +53 -64
- package/dist/{stash-providers → sources/providers}/index.js +3 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/{stash-providers → sources/providers}/npm.js +42 -41
- package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
- package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
- package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
- package/dist/{stash-providers → sources/providers}/website.js +29 -65
- package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
- package/dist/{wiki.js → wiki/wiki.js} +34 -18
- package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
- 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} +72 -28
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/migration/release-notes/0.6.0.md +91 -23
- package/package.json +1 -1
- package/dist/errors.js +0 -45
- package/dist/llm.js +0 -16
- package/dist/registry-factory.js +0 -19
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -3
- package/dist/stash-providers/filesystem.js +0 -71
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -260
- /package/dist/{common.js → core/common.js} +0 -0
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{paths.js → core/paths.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/{embedder.js → llm/embedder.js} +0 -0
- /package/dist/{embedders → llm/embedders}/cache.js +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{setup-steps.js → setup/steps.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -1,31 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `akm show` — entry point.
|
|
3
|
+
*
|
|
4
|
+
* Spec §6.2:
|
|
5
|
+
*
|
|
6
|
+
* show(ref) → indexer.lookup(ref) → readFile(entry.filePath)
|
|
7
|
+
*
|
|
8
|
+
* The richer presentation logic (matchers, renderers, wiki-root handling,
|
|
9
|
+
* edit-hints, summary-detail truncation) lives below in this file. The flow:
|
|
10
|
+
*
|
|
11
|
+
* 1. Special-case wiki-root refs (`wiki:<name>` with no page path).
|
|
12
|
+
* 2. Ask `indexer.lookup(ref)` for the row in the FTS index.
|
|
13
|
+
* 3. Fall back to the on-disk type-dir resolver only when the index has
|
|
14
|
+
* no matching row — covers the "indexed yet?" gap when the user has
|
|
15
|
+
* just added a file and not run `akm index`.
|
|
16
|
+
* 4. Render the file via the matcher/renderer pipeline.
|
|
17
|
+
*
|
|
18
|
+
* Step (2) is the v1 spec change: reading is the indexer's job. Step (3) is a
|
|
19
|
+
* pragmatic safety net (NOT remote provider fallback, which the spec
|
|
20
|
+
* forbids — "Show: Local FTS5 index only. No remote provider fallback.").
|
|
21
|
+
*/
|
|
1
22
|
import fs from "node:fs";
|
|
2
23
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { NotFoundError, UsageError } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import "
|
|
24
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
25
|
+
import { loadConfig } from "../core/config";
|
|
26
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
27
|
+
import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
|
|
28
|
+
import { closeDatabase, findEntryIdByRef, openDatabase } from "../indexer/db";
|
|
29
|
+
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
|
|
30
|
+
import { lookup } from "../indexer/indexer";
|
|
31
|
+
import { loadStashFile } from "../indexer/metadata";
|
|
32
|
+
import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../indexer/search-source";
|
|
33
|
+
import { insertUsageEvent } from "../indexer/usage-events";
|
|
34
|
+
import { resolveSourcesForOrigin } from "../registry/origin-resolve";
|
|
35
|
+
// Eagerly import source providers to trigger self-registration.
|
|
36
|
+
import "../sources/providers/index";
|
|
37
|
+
import { resolveAssetPath } from "../sources/resolve";
|
|
17
38
|
/**
|
|
18
39
|
* Show a wiki root (no page path) — returns the same payload as
|
|
19
40
|
* `akm wiki show <name>`.
|
|
20
|
-
*
|
|
21
|
-
* Called when `parseAssetRef` yields `type === "wiki"` and the name has no
|
|
22
|
-
* `/`, e.g. `wiki:research`.
|
|
23
41
|
*/
|
|
24
42
|
async function showWikiRoot(stashDir, wikiName) {
|
|
25
|
-
const { showWiki } = await import("
|
|
43
|
+
const { showWiki } = await import("../wiki/wiki.js");
|
|
26
44
|
const result = showWiki(stashDir, wikiName);
|
|
27
|
-
// Shape the WikiShowResult into a ShowResponse-compatible object.
|
|
28
|
-
// The payload mirrors what `akm wiki show <name>` returns.
|
|
29
45
|
return {
|
|
30
46
|
type: "wiki",
|
|
31
47
|
name: result.ref,
|
|
@@ -40,7 +56,7 @@ async function showWikiRoot(stashDir, wikiName) {
|
|
|
40
56
|
};
|
|
41
57
|
}
|
|
42
58
|
async function showWikiRootForSource(stashDir, source, wikiName) {
|
|
43
|
-
const { showWikiAtPath } = await import("
|
|
59
|
+
const { showWikiAtPath } = await import("../wiki/wiki.js");
|
|
44
60
|
if (source.wikiName === wikiName) {
|
|
45
61
|
const result = showWikiAtPath(wikiName, source.path);
|
|
46
62
|
return {
|
|
@@ -78,7 +94,9 @@ function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
|
|
|
78
94
|
return realTarget;
|
|
79
95
|
}
|
|
80
96
|
/**
|
|
81
|
-
* Unified show:
|
|
97
|
+
* Unified show: queries the local FTS5 index, then falls back to on-disk
|
|
98
|
+
* type-dir resolution if the index has no row. Spec §6.2; no remote provider
|
|
99
|
+
* fallback.
|
|
82
100
|
*
|
|
83
101
|
* When `detail` is `"summary"`, the response omits content/template/prompt and
|
|
84
102
|
* returns only compact metadata (name, type, description, tags, parameters).
|
|
@@ -92,7 +110,7 @@ export async function akmShowUnified(input) {
|
|
|
92
110
|
{
|
|
93
111
|
const parsed = parseAssetRef(ref);
|
|
94
112
|
if (parsed.type === "wiki" && !parsed.name.includes("/")) {
|
|
95
|
-
const allSources =
|
|
113
|
+
const allSources = resolveSourceEntries();
|
|
96
114
|
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
97
115
|
let lastError;
|
|
98
116
|
for (const source of searchSources) {
|
|
@@ -109,56 +127,19 @@ export async function akmShowUnified(input) {
|
|
|
109
127
|
new NotFoundError(`Wiki not found: ${parsed.name}. Run \`akm wiki create ${parsed.name}\` to create it.`));
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
logShowEvent(ref);
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
// Only fall through to remote providers on NotFoundError
|
|
121
|
-
if (!(err instanceof NotFoundError))
|
|
122
|
-
throw err;
|
|
123
|
-
localError = err;
|
|
124
|
-
}
|
|
125
|
-
// 2. Try remote providers (e.g. OpenViking)
|
|
126
|
-
const config = loadConfig();
|
|
127
|
-
const providers = resolveStashProviders(config).filter((p) => p.type !== "filesystem" && p.canShow(ref));
|
|
128
|
-
for (const provider of providers) {
|
|
129
|
-
try {
|
|
130
|
-
const response = await provider.show(ref, input.view);
|
|
131
|
-
logShowEvent(ref);
|
|
132
|
-
if (input.detail === "summary") {
|
|
133
|
-
return buildSummaryResponse(response);
|
|
134
|
-
}
|
|
135
|
-
return response;
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (!(err instanceof NotFoundError))
|
|
139
|
-
throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Nothing found anywhere — rethrow the original local error with its specific message
|
|
143
|
-
throw localError;
|
|
130
|
+
// Try local filesystem (FTS5 index lookup, then on-disk fallback)
|
|
131
|
+
const result = await showLocal(input);
|
|
132
|
+
logShowEvent(ref);
|
|
133
|
+
return result;
|
|
144
134
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Fire-and-forget: log a show event to the usage_events table.
|
|
147
|
-
* Never blocks the caller; errors are silently ignored.
|
|
148
|
-
*/
|
|
149
135
|
function logShowEvent(ref, existingDb) {
|
|
150
136
|
try {
|
|
151
137
|
const db = existingDb ?? openDatabase();
|
|
152
138
|
try {
|
|
153
|
-
const parsed = parseAssetRef(ref);
|
|
154
|
-
const safeName = parsed.name.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
155
|
-
const row = db
|
|
156
|
-
.prepare("SELECT id FROM entries WHERE entry_key LIKE ? ESCAPE '\\' AND entry_type = ? LIMIT 1")
|
|
157
|
-
.get(`%:${parsed.type}:${safeName}`, parsed.type);
|
|
158
139
|
insertUsageEvent(db, {
|
|
159
140
|
event_type: "show",
|
|
160
141
|
entry_ref: ref,
|
|
161
|
-
entry_id:
|
|
142
|
+
entry_id: findEntryIdByRef(db, ref),
|
|
162
143
|
});
|
|
163
144
|
}
|
|
164
145
|
finally {
|
|
@@ -170,14 +151,48 @@ function logShowEvent(ref, existingDb) {
|
|
|
170
151
|
/* fire-and-forget */
|
|
171
152
|
}
|
|
172
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve an asset path to a file via:
|
|
156
|
+
* 1. `indexer.lookup(ref)` — the spec's primary path (§6.2).
|
|
157
|
+
* 2. On-disk type-dir traversal — fallback for files not yet indexed.
|
|
158
|
+
*
|
|
159
|
+
* Returns `undefined` if neither path finds a match.
|
|
160
|
+
*/
|
|
161
|
+
async function resolvePathViaIndexThenDisk(parsed, searchSourceDirs) {
|
|
162
|
+
// Step 1: indexer
|
|
163
|
+
try {
|
|
164
|
+
const entry = await lookup(parsed);
|
|
165
|
+
if (entry) {
|
|
166
|
+
return { assetPath: entry.filePath };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
// Index unavailable (e.g. DB doesn't exist yet) — fall back to disk walk.
|
|
171
|
+
if (!(err instanceof NotFoundError)) {
|
|
172
|
+
// continue to disk fallback
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Step 2: on-disk type-dir traversal
|
|
176
|
+
let lastError;
|
|
177
|
+
for (const dir of searchSourceDirs) {
|
|
178
|
+
try {
|
|
179
|
+
const assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
|
|
180
|
+
return { assetPath, lastError };
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return lastError ? { assetPath: "", lastError } : undefined;
|
|
187
|
+
}
|
|
173
188
|
/** @internal Use akmShowUnified() for all external callers. */
|
|
174
189
|
export async function showLocal(input) {
|
|
175
190
|
const parsed = parseAssetRef(input.ref);
|
|
176
191
|
const displayType = parsed.type;
|
|
177
192
|
const config = loadConfig();
|
|
178
|
-
const allSources =
|
|
193
|
+
const allSources = resolveSourceEntries(input.stashDir);
|
|
179
194
|
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
180
|
-
const
|
|
195
|
+
const allSourceDirs = searchSources.map((s) => s.path);
|
|
181
196
|
let assetPath;
|
|
182
197
|
const matchedSource = parsed.type === "wiki" ? searchSources.find((source) => parsed.name.startsWith(`${source.wikiName}/`)) : undefined;
|
|
183
198
|
let lastError;
|
|
@@ -189,15 +204,13 @@ export async function showLocal(input) {
|
|
|
189
204
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
190
205
|
}
|
|
191
206
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
|
|
197
|
-
break;
|
|
207
|
+
if (!assetPath) {
|
|
208
|
+
const resolved = await resolvePathViaIndexThenDisk(parsed, allSourceDirs);
|
|
209
|
+
if (resolved?.assetPath) {
|
|
210
|
+
assetPath = resolved.assetPath;
|
|
198
211
|
}
|
|
199
|
-
|
|
200
|
-
lastError =
|
|
212
|
+
else if (resolved?.lastError) {
|
|
213
|
+
lastError = resolved.lastError;
|
|
201
214
|
}
|
|
202
215
|
}
|
|
203
216
|
if (!assetPath && parsed.origin && searchSources.length === 0) {
|
|
@@ -211,7 +224,7 @@ export async function showLocal(input) {
|
|
|
211
224
|
"Check the name with `akm search` or verify the asset exists in your stash."));
|
|
212
225
|
}
|
|
213
226
|
const source = matchedSource ?? findSourceForPath(assetPath, allSources);
|
|
214
|
-
const sourceStashDir = source?.path ??
|
|
227
|
+
const sourceStashDir = source?.path ?? allSourceDirs[0];
|
|
215
228
|
if (!sourceStashDir) {
|
|
216
229
|
throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}. ` +
|
|
217
230
|
"Run `akm init` to create the stash directory, or check `akm stash list` for configured paths.");
|
|
@@ -229,7 +242,7 @@ export async function showLocal(input) {
|
|
|
229
242
|
if (!renderer) {
|
|
230
243
|
throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${displayType}:${parsed.name}`);
|
|
231
244
|
}
|
|
232
|
-
const renderCtx = buildRenderContext(fileCtx, match,
|
|
245
|
+
const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs, source?.registryId);
|
|
233
246
|
const response = renderer.buildShowResponse(renderCtx);
|
|
234
247
|
const editable = isEditable(assetPath, config);
|
|
235
248
|
const fullResponse = {
|
|
@@ -243,6 +256,20 @@ export async function showLocal(input) {
|
|
|
243
256
|
}
|
|
244
257
|
return fullResponse;
|
|
245
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Minimal `show`: ref → indexer lookup → file contents. Used by callers that
|
|
261
|
+
* just need the raw file (e.g. clone, write-source) and don't want the full
|
|
262
|
+
* renderer graph. Spec §6.2's literal flow.
|
|
263
|
+
*/
|
|
264
|
+
export async function showByRef(ref) {
|
|
265
|
+
const parsed = parseAssetRef(ref);
|
|
266
|
+
const entry = await lookup(parsed);
|
|
267
|
+
if (!entry) {
|
|
268
|
+
throw new NotFoundError(`Asset not found for ref: ${parsed.type}:${parsed.name}`);
|
|
269
|
+
}
|
|
270
|
+
const body = await fs.promises.readFile(entry.filePath, "utf8");
|
|
271
|
+
return { filePath: entry.filePath, body };
|
|
272
|
+
}
|
|
246
273
|
/**
|
|
247
274
|
* Build a compact summary response from a full ShowResponse.
|
|
248
275
|
*
|
|
@@ -250,24 +277,17 @@ export async function showLocal(input) {
|
|
|
250
277
|
* type, name, path, description, tags, parameters, action.
|
|
251
278
|
* Enriches description and tags from frontmatter or .stash.json when available.
|
|
252
279
|
*
|
|
253
|
-
* Enrichment via frontmatter and .stash.json is only performed when `assetPath`
|
|
254
|
-
* is supplied (local assets). Remote provider responses (e.g. OpenViking) rely
|
|
255
|
-
* on the provider having already populated description and tags.
|
|
256
|
-
*
|
|
257
280
|
* The resulting JSON should be under 200 tokens.
|
|
258
281
|
*/
|
|
259
282
|
function buildSummaryResponse(full, assetPath) {
|
|
260
|
-
// Try to enrich metadata from .stash.json if description or tags are missing
|
|
261
283
|
let description = full.description;
|
|
262
284
|
let tags = full.tags;
|
|
263
285
|
if (assetPath) {
|
|
264
|
-
// Try frontmatter extraction from content fields
|
|
265
286
|
const textContent = full.content ?? full.template ?? full.prompt;
|
|
266
287
|
if (textContent && !description) {
|
|
267
288
|
const parsed = parseFrontmatter(textContent);
|
|
268
289
|
description = toStringOrUndefined(parsed.data.description);
|
|
269
290
|
}
|
|
270
|
-
// Try .stash.json for richer metadata (tags especially)
|
|
271
291
|
const dir = path.dirname(assetPath);
|
|
272
292
|
const stashFile = loadStashFile(dir);
|
|
273
293
|
if (stashFile) {
|
|
@@ -1,17 +1,17 @@
|
|
|
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 {
|
|
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";
|
|
7
14
|
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
|
|
8
|
-
import { upsertLockEntry } from "./lockfile";
|
|
9
|
-
import { parseRegistryRef } from "./registry-resolve";
|
|
10
|
-
import { detectStashRoot } from "./stash-providers/provider-utils";
|
|
11
|
-
import { syncFromRef } from "./stash-providers/sync-from-ref";
|
|
12
|
-
import { ensureWebsiteMirror, validateWebsiteInputUrl } from "./stash-providers/website";
|
|
13
|
-
import { warn } from "./warn";
|
|
14
|
-
import { ensureWikiNameAvailable, validateWikiName } from "./wiki";
|
|
15
15
|
const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
|
|
16
16
|
export async function akmAdd(input) {
|
|
17
17
|
const ref = input.ref.trim();
|
|
@@ -32,7 +32,7 @@ export async function akmAdd(input) {
|
|
|
32
32
|
}
|
|
33
33
|
const stashDir = resolveStashDir();
|
|
34
34
|
if (shouldAddAsWebsiteUrl(ref)) {
|
|
35
|
-
return
|
|
35
|
+
return addWebsiteSource(ref, stashDir, input.name ?? wikiName, input.options, wikiName);
|
|
36
36
|
}
|
|
37
37
|
// Detect local directory refs and route them to stashes[] instead of installed[]
|
|
38
38
|
try {
|
|
@@ -41,7 +41,7 @@ export async function akmAdd(input) {
|
|
|
41
41
|
if (input.trustThisInstall) {
|
|
42
42
|
warn("--trust has no effect on local directory sources; the install audit is not run for local paths.");
|
|
43
43
|
}
|
|
44
|
-
return
|
|
44
|
+
return addLocalSource(ref, parsed.sourcePath, stashDir, wikiName, input.name);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
@@ -67,29 +67,39 @@ export async function registerWikiSource(input) {
|
|
|
67
67
|
* Add a local directory as a filesystem stash source.
|
|
68
68
|
* Creates a stashes[] entry instead of an installed[] entry.
|
|
69
69
|
*/
|
|
70
|
-
async function
|
|
70
|
+
async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName) {
|
|
71
71
|
const stashRoot = detectStashRoot(sourcePath);
|
|
72
72
|
const resolvedPath = path.resolve(stashRoot);
|
|
73
73
|
const config = loadUserConfig();
|
|
74
|
-
//
|
|
75
|
-
const
|
|
76
|
-
|
|
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);
|
|
77
79
|
let persistedEntry;
|
|
78
80
|
if (!existing) {
|
|
79
81
|
persistedEntry = {
|
|
80
82
|
type: "filesystem",
|
|
81
83
|
path: resolvedPath,
|
|
82
|
-
name:
|
|
84
|
+
name: derivedName,
|
|
83
85
|
...(wikiName ? { wikiName } : {}),
|
|
84
86
|
};
|
|
85
|
-
|
|
86
|
-
saveConfig({ ...config, stashes });
|
|
87
|
+
sources.push(persistedEntry);
|
|
88
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
87
89
|
}
|
|
88
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
|
+
}
|
|
89
97
|
if (wikiName && existing.wikiName !== wikiName) {
|
|
90
98
|
existing.wikiName = wikiName;
|
|
91
|
-
|
|
99
|
+
changed = true;
|
|
92
100
|
}
|
|
101
|
+
if (changed)
|
|
102
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
93
103
|
persistedEntry = existing;
|
|
94
104
|
}
|
|
95
105
|
const index = await akmIndex({ stashDir });
|
|
@@ -98,7 +108,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
98
108
|
schemaVersion: 1,
|
|
99
109
|
stashDir,
|
|
100
110
|
ref: wikiName ?? ref,
|
|
101
|
-
|
|
111
|
+
sourceAdded: {
|
|
102
112
|
type: "filesystem",
|
|
103
113
|
path: resolvedPath,
|
|
104
114
|
name: persistedEntry.name ?? toReadableId(resolvedPath),
|
|
@@ -106,7 +116,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
106
116
|
...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
|
|
107
117
|
},
|
|
108
118
|
config: {
|
|
109
|
-
|
|
119
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
110
120
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
111
121
|
},
|
|
112
122
|
index: {
|
|
@@ -118,11 +128,11 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
118
128
|
},
|
|
119
129
|
};
|
|
120
130
|
}
|
|
121
|
-
async function
|
|
131
|
+
async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
122
132
|
const normalizedUrl = validateWebsiteInputUrl(ref);
|
|
123
133
|
const config = loadUserConfig();
|
|
124
|
-
const
|
|
125
|
-
let entry =
|
|
134
|
+
const sources = [...(config.sources ?? config.stashes ?? [])];
|
|
135
|
+
let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
|
|
126
136
|
if (!entry) {
|
|
127
137
|
entry = {
|
|
128
138
|
type: "website",
|
|
@@ -131,8 +141,8 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
131
141
|
...(options && Object.keys(options).length > 0 ? { options } : {}),
|
|
132
142
|
...(wikiName ? { wikiName } : {}),
|
|
133
143
|
};
|
|
134
|
-
|
|
135
|
-
saveConfig({ ...config, stashes });
|
|
144
|
+
sources.push(entry);
|
|
145
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
136
146
|
}
|
|
137
147
|
else {
|
|
138
148
|
let changed = false;
|
|
@@ -145,7 +155,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
145
155
|
changed = true;
|
|
146
156
|
}
|
|
147
157
|
if (changed)
|
|
148
|
-
saveConfig({ ...config, stashes });
|
|
158
|
+
saveConfig({ ...config, sources, stashes: undefined });
|
|
149
159
|
}
|
|
150
160
|
const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
|
|
151
161
|
const index = await akmIndex({ stashDir });
|
|
@@ -154,7 +164,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
154
164
|
schemaVersion: 1,
|
|
155
165
|
stashDir,
|
|
156
166
|
ref: wikiName ?? ref,
|
|
157
|
-
|
|
167
|
+
sourceAdded: {
|
|
158
168
|
type: "website",
|
|
159
169
|
url: normalizedUrl,
|
|
160
170
|
name: entry.name,
|
|
@@ -162,7 +172,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
162
172
|
...(entry.wikiName ? { wiki: entry.wikiName } : {}),
|
|
163
173
|
},
|
|
164
174
|
config: {
|
|
165
|
-
|
|
175
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
166
176
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
167
177
|
},
|
|
168
178
|
index: {
|
|
@@ -254,7 +264,7 @@ async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiN
|
|
|
254
264
|
audit,
|
|
255
265
|
},
|
|
256
266
|
config: {
|
|
257
|
-
|
|
267
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
258
268
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
259
269
|
},
|
|
260
270
|
index: {
|
|
@@ -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 { findSourceForPath, getPrimarySource,
|
|
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) {
|
|
@@ -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
|