akm-cli 0.8.0-rc1 → 0.8.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/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2162 -1258
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +233 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +662 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +114 -48
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -307
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
package/dist/commands/search.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm search` — entry point.
|
|
3
6
|
*
|
|
@@ -9,11 +12,13 @@
|
|
|
9
12
|
* Provider `search()` methods do not exist.
|
|
10
13
|
*/
|
|
11
14
|
import { loadConfig } from "../core/config";
|
|
12
|
-
import { UsageError } from "../core/errors";
|
|
15
|
+
import { rethrowIfTestIsolationError, UsageError } from "../core/errors";
|
|
13
16
|
import { appendEvent } from "../core/events";
|
|
17
|
+
import { isTransientStashPath } from "../core/paths";
|
|
14
18
|
import { bumpUtilityScoresBatch, closeDatabase, openExistingDatabase } from "../indexer/db";
|
|
15
19
|
import { searchLocal } from "../indexer/db-search";
|
|
16
20
|
import { resolveSourceEntries } from "../indexer/search-source";
|
|
21
|
+
import { getCurrentWorkflowScopeKey } from "../workflows/scope-key";
|
|
17
22
|
// Eagerly import source providers to trigger self-registration before the
|
|
18
23
|
// indexer or path-resolution code runs.
|
|
19
24
|
import "../sources/providers/index";
|
|
@@ -26,10 +31,42 @@ export async function akmSearch(input) {
|
|
|
26
31
|
const normalizedQuery = query.toLowerCase();
|
|
27
32
|
const searchType = input.type ?? "any";
|
|
28
33
|
const limit = normalizeLimit(input.limit);
|
|
29
|
-
const
|
|
34
|
+
const parsedSource = parseSearchSource(input.source ?? "stash");
|
|
30
35
|
const config = loadConfig();
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
// Named-source filter: when --source is not a standard enum value, treat it
|
|
37
|
+
// as a named source from config.sources[].name. Validate early (before
|
|
38
|
+
// resolveSourceEntries, which can throw STASH_DIR_NOT_FOUND) so that a bad
|
|
39
|
+
// --source name always produces INVALID_SOURCE_VALUE regardless of stash state.
|
|
40
|
+
let namedSourceName;
|
|
41
|
+
let source;
|
|
42
|
+
if (parsedSource !== "stash" && parsedSource !== "registry" && parsedSource !== "both") {
|
|
43
|
+
namedSourceName = parsedSource;
|
|
44
|
+
// Check that the named source exists in the config before touching the stash.
|
|
45
|
+
const configSources = config.sources ?? [];
|
|
46
|
+
const foundInConfig = configSources.some((s) => s.name === namedSourceName) || configSources.some((s) => s.path === namedSourceName);
|
|
47
|
+
if (!foundInConfig) {
|
|
48
|
+
const validNames = configSources.map((s) => s.name).filter((n) => Boolean(n));
|
|
49
|
+
const hint = validNames.length > 0
|
|
50
|
+
? `Known source names: ${validNames.join(", ")}`
|
|
51
|
+
: "No named sources are configured. Run `akm list` to see installed stashes.";
|
|
52
|
+
throw new UsageError(`Unknown source name: "${namedSourceName}". ${hint}`, "INVALID_SOURCE_VALUE");
|
|
53
|
+
}
|
|
54
|
+
source = "stash";
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
source = parsedSource;
|
|
58
|
+
}
|
|
59
|
+
let allSources = resolveSourceEntries(undefined, config);
|
|
60
|
+
// When a named source was requested, narrow the sources list to just that entry.
|
|
61
|
+
// `resolveSourceEntries` sets `registryId` to `entry.name` for each config source.
|
|
62
|
+
if (namedSourceName !== undefined) {
|
|
63
|
+
const ns = namedSourceName;
|
|
64
|
+
allSources = allSources.filter((s) => s.registryId === ns || s.path === ns);
|
|
65
|
+
// allSources may still be empty if the configured source dir doesn't exist on
|
|
66
|
+
// disk (resolveSourceEntries skips non-existent dirs). Fall through to the
|
|
67
|
+
// zero-sources guard below which emits a friendly warning.
|
|
68
|
+
}
|
|
69
|
+
if (allSources.length === 0) {
|
|
33
70
|
// stashDir: "" is a safe sentinel here — the response carries zero hits
|
|
34
71
|
// and a warning, so no downstream code will try to use the empty path.
|
|
35
72
|
const response = {
|
|
@@ -40,12 +77,15 @@ export async function akmSearch(input) {
|
|
|
40
77
|
warnings: ["No stashes configured. Run `akm init` to create your working stash."],
|
|
41
78
|
timing: { totalMs: Date.now() - t0 },
|
|
42
79
|
};
|
|
43
|
-
|
|
80
|
+
if (!input.skipLogging)
|
|
81
|
+
logSearchEvent(query, response, undefined, undefined, input.eventSource);
|
|
44
82
|
return response;
|
|
45
83
|
}
|
|
46
84
|
// Primary stash directory — used for DB path lookups and as the default
|
|
47
85
|
// stash root. Safe because the empty-sources case is handled above.
|
|
48
|
-
const stashDir =
|
|
86
|
+
const stashDir = allSources[0].path;
|
|
87
|
+
// Expose the filtered source list to downstream search calls.
|
|
88
|
+
const sources = allSources;
|
|
49
89
|
const filters = normalizeScopeFilters(input.filters);
|
|
50
90
|
const includeProposed = input.includeProposed === true;
|
|
51
91
|
const belief = input.belief ?? "all";
|
|
@@ -61,6 +101,12 @@ export async function akmSearch(input) {
|
|
|
61
101
|
filters,
|
|
62
102
|
includeProposed,
|
|
63
103
|
beliefFilter: belief,
|
|
104
|
+
// When `--source <name>` narrowed the source list above, propagate
|
|
105
|
+
// that intent down to the database layer so FTS/vector hits from
|
|
106
|
+
// sources outside the narrowed set are filtered out post-ranking.
|
|
107
|
+
// Without this, the index (which spans every configured source)
|
|
108
|
+
// would leak hits from sources the caller did not request.
|
|
109
|
+
restrictToSources: namedSourceName !== undefined,
|
|
64
110
|
});
|
|
65
111
|
const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
|
|
66
112
|
if (source === "stash") {
|
|
@@ -75,7 +121,8 @@ export async function akmSearch(input) {
|
|
|
75
121
|
warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
|
|
76
122
|
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
77
123
|
};
|
|
78
|
-
|
|
124
|
+
if (!input.skipLogging)
|
|
125
|
+
logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword", input.eventSource);
|
|
79
126
|
return response;
|
|
80
127
|
}
|
|
81
128
|
const registryHits = (registryResult?.hits ?? []).map((hit) => {
|
|
@@ -109,7 +156,8 @@ export async function akmSearch(input) {
|
|
|
109
156
|
warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
|
|
110
157
|
timing: { totalMs: Date.now() - t0 },
|
|
111
158
|
};
|
|
112
|
-
|
|
159
|
+
if (!input.skipLogging)
|
|
160
|
+
logSearchEvent(query, response, undefined, undefined, input.eventSource);
|
|
113
161
|
return response;
|
|
114
162
|
}
|
|
115
163
|
// source === "both"
|
|
@@ -126,7 +174,8 @@ export async function akmSearch(input) {
|
|
|
126
174
|
warnings: warnings.length ? warnings : undefined,
|
|
127
175
|
timing: { totalMs: Date.now() - t0 },
|
|
128
176
|
};
|
|
129
|
-
|
|
177
|
+
if (!input.skipLogging)
|
|
178
|
+
logSearchEvent(query, response, undefined, undefined, input.eventSource);
|
|
130
179
|
return response;
|
|
131
180
|
}
|
|
132
181
|
/**
|
|
@@ -162,13 +211,16 @@ function resolveEntryIds(db, hits) {
|
|
|
162
211
|
* Per-entry events are recorded only for stash hits because registry hits
|
|
163
212
|
* have no local entry_id to reference.
|
|
164
213
|
*/
|
|
165
|
-
function logSearchEvent(query, response, existingDb, mode = "keyword") {
|
|
214
|
+
function logSearchEvent(query, response, existingDb, mode = "keyword", eventSource = "user") {
|
|
166
215
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
167
216
|
// detect akm search invocations without relying on stdout scraping.
|
|
168
217
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
218
|
+
// D8: include registry hit refs so a show following a registry-only search generates a select event
|
|
219
|
+
const registryHitRefs = (response.registryHits ?? []).map((h) => `registry:${h.id}`);
|
|
220
|
+
const allResultRefs = [...stashHits.map((h) => h.ref), ...registryHitRefs];
|
|
169
221
|
appendEvent({
|
|
170
222
|
eventType: "search",
|
|
171
|
-
metadata: { query, hitCount: stashHits.length, resultRefs:
|
|
223
|
+
metadata: { query, hitCount: stashHits.length, resultRefs: allResultRefs, mode },
|
|
172
224
|
});
|
|
173
225
|
try {
|
|
174
226
|
const db = existingDb ?? openExistingDatabase();
|
|
@@ -180,13 +232,23 @@ function logSearchEvent(query, response, existingDb, mode = "keyword") {
|
|
|
180
232
|
query,
|
|
181
233
|
entry_id: entryId,
|
|
182
234
|
entry_ref: ref,
|
|
235
|
+
source: eventSource,
|
|
183
236
|
});
|
|
184
237
|
}
|
|
185
238
|
// Bump utility scores for all resolved entries (MemRL retrieval signal).
|
|
186
239
|
// The indexer overwrites these at next reindex; bumps are temporary hints.
|
|
187
240
|
const resolvedIds = resolved.map((r) => r.entryId).filter((id) => id !== undefined);
|
|
188
241
|
if (resolvedIds.length > 0) {
|
|
189
|
-
|
|
242
|
+
let scopeKey;
|
|
243
|
+
try {
|
|
244
|
+
const stashPath = response.stashDir;
|
|
245
|
+
const disabled = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" || (stashPath && isTransientStashPath(stashPath));
|
|
246
|
+
scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Non-fatal — fall back to global-only bumps on any error.
|
|
250
|
+
}
|
|
251
|
+
bumpUtilityScoresBatch(db, resolvedIds, 1.0, 0.1, scopeKey);
|
|
190
252
|
}
|
|
191
253
|
// Count registry hits separately so registry-only searches record a
|
|
192
254
|
// non-zero resultCount. response.hits is always [] when source="registry".
|
|
@@ -202,6 +264,7 @@ function logSearchEvent(query, response, existingDb, mode = "keyword") {
|
|
|
202
264
|
resolvedCount: resolved.length,
|
|
203
265
|
mode,
|
|
204
266
|
}),
|
|
267
|
+
source: eventSource,
|
|
205
268
|
});
|
|
206
269
|
}
|
|
207
270
|
finally {
|
|
@@ -209,7 +272,8 @@ function logSearchEvent(query, response, existingDb, mode = "keyword") {
|
|
|
209
272
|
closeDatabase(db);
|
|
210
273
|
}
|
|
211
274
|
}
|
|
212
|
-
catch {
|
|
275
|
+
catch (err) {
|
|
276
|
+
rethrowIfTestIsolationError(err);
|
|
213
277
|
/* fire-and-forget */
|
|
214
278
|
}
|
|
215
279
|
}
|
|
@@ -220,6 +284,24 @@ function normalizeLimit(limit) {
|
|
|
220
284
|
}
|
|
221
285
|
return Math.min(Math.floor(limit), 200);
|
|
222
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Parse the `--source` flag value.
|
|
289
|
+
*
|
|
290
|
+
* Accepts:
|
|
291
|
+
* - `stash` (default) — search the local stash index only
|
|
292
|
+
* - `registry` — search remote registries only
|
|
293
|
+
* - `both` — search stash and registries
|
|
294
|
+
* - `local` — alias for `stash`
|
|
295
|
+
* - Any named source from `config.sources[].name` — filters stash results to
|
|
296
|
+
* that single source only. The named-source path is detected and resolved
|
|
297
|
+
* inside `akmSearch`; this function returns the raw name so the caller can
|
|
298
|
+
* pass it through to `akmSearch` which accepts `SearchSource | string`.
|
|
299
|
+
*
|
|
300
|
+
* Unknown values that are not a known enum AND not a named source will still
|
|
301
|
+
* produce an error inside `akmSearch` when the config lookup finds nothing.
|
|
302
|
+
* This allows the CLI to accept named sources without requiring config access
|
|
303
|
+
* at parse time.
|
|
304
|
+
*/
|
|
223
305
|
export function parseSearchSource(source) {
|
|
224
306
|
if (source === "stash" || source === "registry" || source === "both")
|
|
225
307
|
return source;
|
|
@@ -228,7 +310,10 @@ export function parseSearchSource(source) {
|
|
|
228
310
|
return "stash";
|
|
229
311
|
if (typeof source === "undefined")
|
|
230
312
|
return "stash";
|
|
231
|
-
|
|
313
|
+
// Pass through unknown strings — they may be valid named sources.
|
|
314
|
+
// `akmSearch` will validate against config.sources and throw a UsageError
|
|
315
|
+
// with a helpful message if the name isn't found.
|
|
316
|
+
return source;
|
|
232
317
|
}
|
|
233
318
|
export function parseBeliefFilterMode(value) {
|
|
234
319
|
if (value === undefined || value === "all")
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Secret asset type — whole-file secret storage.
|
|
6
|
+
*
|
|
7
|
+
* A `secret` holds a single SENSITIVE value used on its own for authentication
|
|
8
|
+
* (a PEM private key, an API token, a TLS cert, a service-account JSON): the
|
|
9
|
+
* ENTIRE file is the secret. There is no safe region to parse, so only the
|
|
10
|
+
* filename is ever surfaced. Where an `env` file holds a GROUP of related
|
|
11
|
+
* configuration and exposes key NAMES as metadata, a secret is ONE value and
|
|
12
|
+
* exposes nothing but its name — reach for `secret` when one value *is* the
|
|
13
|
+
* credential, and for `env` when loading a service's related configuration.
|
|
14
|
+
*
|
|
15
|
+
* Invariant: a secret's bytes must never be written to stdout, returned
|
|
16
|
+
* through the indexer / `akm show` renderer, or any structured output channel.
|
|
17
|
+
* The supported value-use paths are:
|
|
18
|
+
*
|
|
19
|
+
* - `akm secret run <ref> <VAR> -- <cmd>` — value injected into the child
|
|
20
|
+
* process env as `VAR=<value>` (see `readValue`).
|
|
21
|
+
* - `akm secret path <ref>` — print the file path so a command can read it
|
|
22
|
+
* itself (Docker `/run/secrets` + `_FILE` convention).
|
|
23
|
+
*
|
|
24
|
+
* Values are stored as raw bytes (no quoting, multi-line allowed) so they
|
|
25
|
+
* round-trip byte-exact, unlike env values which forbid literal newlines.
|
|
26
|
+
*/
|
|
27
|
+
import crypto from "node:crypto";
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
|
|
31
|
+
// ── Write-lock helper ─────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Acquire an exclusive lock for the given secret path, run `fn`, then release.
|
|
34
|
+
* Mirrors the env write-lock: O_EXCL creation, 5s deadline, PID-based stale
|
|
35
|
+
* detection. A timeout is always a stale lock or a programming error, so we
|
|
36
|
+
* throw rather than silently proceeding.
|
|
37
|
+
*/
|
|
38
|
+
export function withSecretLock(secretPath, fn) {
|
|
39
|
+
const lockPath = `${secretPath}.lock`;
|
|
40
|
+
const deadline = Date.now() + 5000;
|
|
41
|
+
while (!tryAcquireLockSync(lockPath, String(process.pid))) {
|
|
42
|
+
const probe = probeLock(lockPath);
|
|
43
|
+
if (probe.state === "stale") {
|
|
44
|
+
releaseLock(lockPath);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (Date.now() > deadline) {
|
|
48
|
+
const holderHint = probe.state === "held"
|
|
49
|
+
? ` Lock file ${lockPath} is held by live PID ${probe.holderPid}.`
|
|
50
|
+
: ` Lock file ${lockPath} could not be inspected.`;
|
|
51
|
+
throw new Error(`Could not acquire secret lock for ${secretPath} after 5s.${holderHint} Retry once any other akm secret operation finishes, or remove the stale lock file.`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof globalThis.Bun?.sleepSync ===
|
|
54
|
+
"function") {
|
|
55
|
+
globalThis.Bun.sleepSync(10);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
let spin = 0;
|
|
59
|
+
while (spin++ < 100_000) {
|
|
60
|
+
/* yield */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return fn();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
releaseLock(lockPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ── Atomic byte write ──────────────────────────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Atomically write `data` to `target` at mode 0600. Unlike `writeFileAtomic`
|
|
74
|
+
* in core/common (string content), this accepts a Buffer so secret bytes
|
|
75
|
+
* round-trip exactly — binary certs and CRLF/LF line endings are preserved.
|
|
76
|
+
*/
|
|
77
|
+
function writeSecretAtomic(target, data) {
|
|
78
|
+
const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
|
|
79
|
+
const fd = fs.openSync(tmp, "w", 0o600);
|
|
80
|
+
try {
|
|
81
|
+
fs.writeSync(fd, data);
|
|
82
|
+
try {
|
|
83
|
+
fs.fdatasyncSync(fd);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Best-effort durability; some pseudo-filesystems lack fdatasync.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
fs.closeSync(fd);
|
|
91
|
+
}
|
|
92
|
+
fs.renameSync(tmp, target);
|
|
93
|
+
try {
|
|
94
|
+
const dirFd = fs.openSync(path.dirname(target), "r");
|
|
95
|
+
try {
|
|
96
|
+
fs.fsyncSync(dirFd);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
fs.closeSync(dirFd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Directory fsync is unsupported on FAT / some FUSE mounts / Windows.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function ensureParentDir(filePath) {
|
|
107
|
+
const dir = path.dirname(filePath);
|
|
108
|
+
if (!fs.existsSync(dir))
|
|
109
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
110
|
+
}
|
|
111
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* Walk a `secrets/` directory and return the POSIX-relative names of every
|
|
114
|
+
* secret file. Lock files (`*.lock`), sensitive markers (`*.sensitive`), and
|
|
115
|
+
* secrets with a sibling `<name>.sensitive` marker are excluded. The file
|
|
116
|
+
* bodies are NEVER read.
|
|
117
|
+
*/
|
|
118
|
+
export function listNames(secretsRoot) {
|
|
119
|
+
if (!fs.existsSync(secretsRoot))
|
|
120
|
+
return [];
|
|
121
|
+
const names = [];
|
|
122
|
+
const walk = (dir) => {
|
|
123
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
+
const full = path.join(dir, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
walk(full);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!entry.isFile())
|
|
130
|
+
continue;
|
|
131
|
+
if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
|
|
132
|
+
continue;
|
|
133
|
+
// A sibling `<name>.sensitive` marker suppresses listing.
|
|
134
|
+
if (fs.existsSync(`${full}.sensitive`))
|
|
135
|
+
continue;
|
|
136
|
+
names.push(path.relative(secretsRoot, full).split(path.sep).join("/"));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(secretsRoot);
|
|
140
|
+
return names.sort();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read a secret's raw bytes. Internal use only (for `secret run` / `secret
|
|
144
|
+
* path`). Callers MUST NOT write the returned value to stdout or any log.
|
|
145
|
+
*/
|
|
146
|
+
export function readValue(secretPath) {
|
|
147
|
+
return fs.readFileSync(secretPath);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Write (create or overwrite) a secret with the given raw bytes, atomically at
|
|
151
|
+
* mode 0600 under a write-lock. No quoting; multi-line / binary allowed.
|
|
152
|
+
*/
|
|
153
|
+
export function setSecret(secretPath, value) {
|
|
154
|
+
ensureParentDir(secretPath);
|
|
155
|
+
withSecretLock(secretPath, () => {
|
|
156
|
+
writeSecretAtomic(secretPath, value);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Remove a secret file (and its `.sensitive` marker, if present). Returns true
|
|
161
|
+
* if the secret existed.
|
|
162
|
+
*/
|
|
163
|
+
export function removeSecret(secretPath) {
|
|
164
|
+
return withSecretLock(secretPath, () => {
|
|
165
|
+
if (!fs.existsSync(secretPath))
|
|
166
|
+
return false;
|
|
167
|
+
fs.rmSync(secretPath);
|
|
168
|
+
const marker = `${secretPath}.sensitive`;
|
|
169
|
+
if (fs.existsSync(marker))
|
|
170
|
+
fs.rmSync(marker);
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import * as childProcess from "node:child_process";
|
|
2
5
|
import { createHash } from "node:crypto";
|
|
3
6
|
import fs from "node:fs";
|
package/dist/commands/show.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm show` — entry point.
|
|
3
6
|
*
|
|
@@ -16,10 +19,11 @@
|
|
|
16
19
|
import fs from "node:fs";
|
|
17
20
|
import path from "node:path";
|
|
18
21
|
import { parseAssetRef } from "../core/asset-ref";
|
|
22
|
+
import { asNonEmptyString } from "../core/common";
|
|
19
23
|
import { loadConfig } from "../core/config";
|
|
20
|
-
import { NotFoundError, UsageError } from "../core/errors";
|
|
24
|
+
import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../core/errors";
|
|
21
25
|
import { appendEvent, readEvents } from "../core/events";
|
|
22
|
-
import { parseFrontmatter
|
|
26
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
23
27
|
import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
|
|
24
28
|
import { ensureIndex } from "../indexer/ensure-index";
|
|
25
29
|
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
|
|
@@ -141,7 +145,7 @@ export async function akmShowUnified(input) {
|
|
|
141
145
|
}
|
|
142
146
|
// Count prior shows of this ref before logging the current one.
|
|
143
147
|
const priorShowCount = recentShowCount(ref);
|
|
144
|
-
logShowEvent(ref);
|
|
148
|
+
logShowEvent(ref, undefined, input.eventSource);
|
|
145
149
|
if (priorShowCount >= 2) {
|
|
146
150
|
// Agent has shown this same asset 3+ times — inject a loop-break hint.
|
|
147
151
|
result.showLoopWarning = priorShowCount + 1;
|
|
@@ -179,7 +183,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
|
|
|
179
183
|
for (const [key, expectedValue] of expected) {
|
|
180
184
|
if (expectedValue === undefined)
|
|
181
185
|
continue;
|
|
182
|
-
const actual =
|
|
186
|
+
const actual = asNonEmptyString(fm[`scope_${key}`]);
|
|
183
187
|
if (actual !== expectedValue) {
|
|
184
188
|
throw new NotFoundError(`Asset "${ref}" exists but is out of scope (expected scope_${key}="${expectedValue}").`);
|
|
185
189
|
}
|
|
@@ -191,21 +195,29 @@ function enforceScopeOrThrow(filePath, ref, scope) {
|
|
|
191
195
|
*/
|
|
192
196
|
function recentShowCount(ref) {
|
|
193
197
|
try {
|
|
194
|
-
const { events } = readEvents({
|
|
198
|
+
const { events } = readEvents({
|
|
199
|
+
type: "show",
|
|
200
|
+
ref,
|
|
201
|
+
since: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
|
|
202
|
+
});
|
|
195
203
|
return events.length;
|
|
196
204
|
}
|
|
197
205
|
catch {
|
|
198
206
|
return 0;
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
|
-
function logShowEvent(ref, existingDb) {
|
|
209
|
+
function logShowEvent(ref, existingDb, eventSource = "user") {
|
|
202
210
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
203
211
|
// detect akm show invocations without relying on stdout scraping.
|
|
204
212
|
const parsed = parseAssetRef(ref);
|
|
205
213
|
appendEvent({ eventType: "show", ref, metadata: { type: parsed.type, name: parsed.name } });
|
|
206
214
|
// Detect if this show is a selection from a recent search result.
|
|
207
215
|
try {
|
|
208
|
-
|
|
216
|
+
// D7: bound the query to the last 60 s so we never scan unbounded history
|
|
217
|
+
const { events: recentSearches } = readEvents({
|
|
218
|
+
type: "search",
|
|
219
|
+
since: new Date(Date.now() - 60_000).toISOString(),
|
|
220
|
+
});
|
|
209
221
|
const cutoffMs = Date.now() - 60_000;
|
|
210
222
|
const matchingSearch = [...recentSearches].reverse().find((e) => {
|
|
211
223
|
if (!e.ts || new Date(e.ts).getTime() < cutoffMs)
|
|
@@ -235,6 +247,7 @@ function logShowEvent(ref, existingDb) {
|
|
|
235
247
|
event_type: "show",
|
|
236
248
|
entry_ref: ref,
|
|
237
249
|
entry_id: findEntryIdByRef(db, ref),
|
|
250
|
+
source: eventSource,
|
|
238
251
|
});
|
|
239
252
|
}
|
|
240
253
|
finally {
|
|
@@ -242,7 +255,8 @@ function logShowEvent(ref, existingDb) {
|
|
|
242
255
|
closeDatabase(db);
|
|
243
256
|
}
|
|
244
257
|
}
|
|
245
|
-
catch {
|
|
258
|
+
catch (err) {
|
|
259
|
+
rethrowIfTestIsolationError(err);
|
|
246
260
|
/* fire-and-forget */
|
|
247
261
|
}
|
|
248
262
|
}
|
|
@@ -316,7 +330,8 @@ export async function showLocal(input) {
|
|
|
316
330
|
const related = listRelatedPathsForFile(sourceStashDir, assetPath, 5, db);
|
|
317
331
|
return { total: related.length, hits: related };
|
|
318
332
|
}
|
|
319
|
-
catch {
|
|
333
|
+
catch (err) {
|
|
334
|
+
rethrowIfTestIsolationError(err);
|
|
320
335
|
return { total: 0, hits: [] };
|
|
321
336
|
}
|
|
322
337
|
finally {
|
|
@@ -385,7 +400,7 @@ function buildSummaryResponse(full, assetPath) {
|
|
|
385
400
|
const textContent = full.content ?? full.template ?? full.prompt;
|
|
386
401
|
if (textContent && !description) {
|
|
387
402
|
const parsed = parseFrontmatter(textContent);
|
|
388
|
-
description =
|
|
403
|
+
description = asNonEmptyString(parsed.data.description);
|
|
389
404
|
}
|
|
390
405
|
}
|
|
391
406
|
const summary = {
|
|
@@ -432,15 +447,19 @@ export function normalizeShowArgv(argv) {
|
|
|
432
447
|
const showArgs = [];
|
|
433
448
|
for (let i = 0; i < rest.length; i++) {
|
|
434
449
|
const arg = rest[i];
|
|
435
|
-
if (arg === "--quiet" ||
|
|
450
|
+
if (arg === "--quiet" ||
|
|
451
|
+
arg === "-q" ||
|
|
452
|
+
arg === "--verbose" ||
|
|
453
|
+
arg === "--for-agent" ||
|
|
454
|
+
arg === "--for-agent=true") {
|
|
436
455
|
globalFlags.push(arg);
|
|
437
456
|
continue;
|
|
438
457
|
}
|
|
439
|
-
if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
|
|
458
|
+
if (arg.startsWith("--format=") || arg.startsWith("--detail=") || arg.startsWith("--shape=")) {
|
|
440
459
|
globalFlags.push(arg);
|
|
441
460
|
continue;
|
|
442
461
|
}
|
|
443
|
-
if (arg === "--format" || arg === "--detail") {
|
|
462
|
+
if (arg === "--format" || arg === "--detail" || arg === "--shape") {
|
|
444
463
|
globalFlags.push(arg);
|
|
445
464
|
if (rest[i + 1] !== undefined) {
|
|
446
465
|
globalFlags.push(rest[i + 1]);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { isHttpUrl, resolveStashDir } from "../core/common";
|
|
4
7
|
import { getSources, loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
5
8
|
import { ConfigError, UsageError } from "../core/errors";
|
|
6
|
-
import { warn } from "../core/warn";
|
|
7
9
|
import { akmIndex } from "../indexer/indexer";
|
|
8
10
|
import { upsertLockEntry } from "../integrations/lockfile";
|
|
9
11
|
import { parseRegistryRef } from "../registry/resolve";
|
|
@@ -11,7 +13,6 @@ import { detectStashRoot } from "../sources/providers/provider-utils";
|
|
|
11
13
|
import { syncFromRef } from "../sources/providers/sync-from-ref";
|
|
12
14
|
import { ensureWebsiteMirror, validateWebsiteInputUrl } from "../sources/website-ingest";
|
|
13
15
|
import { ensureWikiNameAvailable, validateWikiName } from "../wiki/wiki";
|
|
14
|
-
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
|
|
15
16
|
const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
|
|
16
17
|
export async function akmAdd(input) {
|
|
17
18
|
const ref = input.ref.trim();
|
|
@@ -38,16 +39,13 @@ export async function akmAdd(input) {
|
|
|
38
39
|
try {
|
|
39
40
|
const parsed = parseRegistryRef(ref);
|
|
40
41
|
if (parsed.source === "local") {
|
|
41
|
-
if (input.trustThisInstall) {
|
|
42
|
-
warn("--trust has no effect on local directory sources; the install audit is not run for local paths.");
|
|
43
|
-
}
|
|
44
42
|
return addLocalSource(ref, parsed.sourcePath, stashDir, wikiName, input.name);
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
catch {
|
|
48
46
|
// Not a local ref — fall through to registry install
|
|
49
47
|
}
|
|
50
|
-
return addRegistryStash(ref, stashDir, input.
|
|
48
|
+
return addRegistryStash(ref, stashDir, input.writable, wikiName);
|
|
51
49
|
}
|
|
52
50
|
export async function registerWikiSource(input) {
|
|
53
51
|
const stashDir = resolveStashDir();
|
|
@@ -59,7 +57,6 @@ export async function registerWikiSource(input) {
|
|
|
59
57
|
name,
|
|
60
58
|
overrideType: "wiki",
|
|
61
59
|
options: input.options,
|
|
62
|
-
trustThisInstall: input.trustThisInstall,
|
|
63
60
|
writable: input.writable,
|
|
64
61
|
});
|
|
65
62
|
}
|
|
@@ -186,38 +183,14 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
|
186
183
|
}
|
|
187
184
|
/**
|
|
188
185
|
* Install a stash from a registry (npm, github, git) by dispatching to the
|
|
189
|
-
* matching syncable provider
|
|
190
|
-
* persisting the lock entry.
|
|
186
|
+
* matching syncable provider and persisting the lock entry.
|
|
191
187
|
*/
|
|
192
|
-
async function addRegistryStash(ref, stashDir,
|
|
188
|
+
async function addRegistryStash(ref, stashDir, writable, wikiName) {
|
|
193
189
|
const parsedRef = parseRegistryRef(ref);
|
|
194
190
|
if (writable === true && parsedRef.source !== "git") {
|
|
195
191
|
throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
|
|
196
192
|
}
|
|
197
|
-
|
|
198
|
-
// so we keep parity with the historical behavior where `enforceRegistryInstallPolicy`
|
|
199
|
-
// ran before `extractTarGzSecure` etc.
|
|
200
|
-
const config = loadConfig();
|
|
201
|
-
const synced = await syncFromRef(ref, { trustThisInstall, writable });
|
|
202
|
-
const registryLabels = deriveRegistryLabels({
|
|
203
|
-
source: synced.source,
|
|
204
|
-
ref: synced.ref,
|
|
205
|
-
artifactUrl: synced.artifactUrl,
|
|
206
|
-
});
|
|
207
|
-
enforceRegistryInstallPolicy(registryLabels, config, ref);
|
|
208
|
-
// Post-sync hook: install audit. Throws when blocked unless `--trust` is set
|
|
209
|
-
// (in which case the audit report still surfaces in the response).
|
|
210
|
-
const audit = auditInstallCandidate({
|
|
211
|
-
rootDir: synced.extractedDir,
|
|
212
|
-
source: synced.source,
|
|
213
|
-
ref: synced.ref,
|
|
214
|
-
registryLabels,
|
|
215
|
-
config,
|
|
216
|
-
trustThisInstall,
|
|
217
|
-
});
|
|
218
|
-
if (audit.blocked) {
|
|
219
|
-
throw new Error(formatInstallAuditFailure(synced.ref, audit));
|
|
220
|
-
}
|
|
193
|
+
const synced = await syncFromRef(ref, { writable });
|
|
221
194
|
const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === synced.id);
|
|
222
195
|
const updatedConfig = upsertInstalledRegistryEntry({
|
|
223
196
|
id: synced.id,
|
|
@@ -265,7 +238,6 @@ async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiN
|
|
|
265
238
|
cacheDir: synced.cacheDir,
|
|
266
239
|
extractedDir: synced.extractedDir,
|
|
267
240
|
installedAt: synced.syncedAt,
|
|
268
|
-
audit,
|
|
269
241
|
},
|
|
270
242
|
config: {
|
|
271
243
|
sourceCount: getSources(updatedConfig).length,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { makeAssetRef, parseAssetRef } from "../core/asset-ref";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import path from "node:path";
|
|
2
5
|
import { isRemoteUrl } from "../core/common";
|
|
3
6
|
import { getSources, loadConfig, loadUserConfig, saveConfig } from "../core/config";
|