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
|
@@ -19,7 +19,7 @@ export function getConfigDir(env = process.env, platform = process.platform) {
|
|
|
19
19
|
return path.join(appData, "akm");
|
|
20
20
|
const userProfile = env.USERPROFILE?.trim();
|
|
21
21
|
if (!userProfile) {
|
|
22
|
-
throw new ConfigError("Unable to determine config directory. Set APPDATA or USERPROFILE.");
|
|
22
|
+
throw new ConfigError("Unable to determine config directory. Set APPDATA or USERPROFILE.", "CONFIG_DIR_UNRESOLVABLE");
|
|
23
23
|
}
|
|
24
24
|
return path.join(userProfile, "AppData", "Roaming", "akm");
|
|
25
25
|
}
|
|
@@ -28,7 +28,7 @@ export function getConfigDir(env = process.env, platform = process.platform) {
|
|
|
28
28
|
return path.join(xdgConfigHome, "akm");
|
|
29
29
|
const home = env.HOME?.trim();
|
|
30
30
|
if (!home) {
|
|
31
|
-
throw new ConfigError("Unable to determine config directory. Set XDG_CONFIG_HOME or HOME.");
|
|
31
|
+
throw new ConfigError("Unable to determine config directory. Set XDG_CONFIG_HOME or HOME.", "CONFIG_DIR_UNRESOLVABLE");
|
|
32
32
|
}
|
|
33
33
|
return path.join(home, ".config", "akm");
|
|
34
34
|
}
|
|
@@ -49,7 +49,7 @@ export function getCacheDir() {
|
|
|
49
49
|
return path.join(userProfile, "AppData", "Local", "akm");
|
|
50
50
|
const appData = process.env.APPDATA?.trim();
|
|
51
51
|
if (!appData) {
|
|
52
|
-
throw new ConfigError("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.");
|
|
52
|
+
throw new ConfigError("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
|
|
53
53
|
}
|
|
54
54
|
// Heuristic fallback: APPDATA points to %APPDATA% (Roaming), so navigate
|
|
55
55
|
// to the sibling "Local" directory. This is typically
|
|
@@ -96,7 +96,7 @@ export function getDefaultStashDir() {
|
|
|
96
96
|
}
|
|
97
97
|
const home = process.env.HOME?.trim();
|
|
98
98
|
if (!home) {
|
|
99
|
-
throw new ConfigError("Unable to determine default stash directory. Set HOME.");
|
|
99
|
+
throw new ConfigError("Unable to determine default stash directory. Set HOME.", "STASH_DIR_NOT_FOUND");
|
|
100
100
|
}
|
|
101
101
|
return path.join(home, "akm");
|
|
102
102
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-source — the only place in the codebase that branches on `source.kind`.
|
|
3
|
+
*
|
|
4
|
+
* v1 architecture spec §2.6 / §2.7 / §10 step 5: writing to a source is *not*
|
|
5
|
+
* a SourceProvider interface concern. It's a small command-layer helper that
|
|
6
|
+
* does a plain filesystem write, plus a git-specific commit (and optional
|
|
7
|
+
* push) when the source is backed by a git working tree.
|
|
8
|
+
*
|
|
9
|
+
* If a third kind ever needs special write handling, it gets added here. For
|
|
10
|
+
* v1 there are exactly two cases. Adding more parallel scoring systems for
|
|
11
|
+
* different provider kinds is explicitly disallowed by CLAUDE.md.
|
|
12
|
+
*
|
|
13
|
+
* This module is the **single dispatch point** for `kind`-branching write
|
|
14
|
+
* logic. Callers (remember, import, source-add, etc.) MUST go through
|
|
15
|
+
* `writeAssetToSource` / `deleteAssetFromSource` rather than re-inlining the
|
|
16
|
+
* filesystem-write + git-commit dance.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { getCachePaths, parseGitRepoUrl } from "../sources/providers/git";
|
|
22
|
+
import { makeAssetRef } from "./asset-ref";
|
|
23
|
+
import { resolveAssetPathFromName, TYPE_DIRS } from "./asset-spec";
|
|
24
|
+
import { isWithin, resolveStashDir } from "./common";
|
|
25
|
+
import { resolveConfiguredSources } from "./config";
|
|
26
|
+
import { ConfigError, UsageError } from "./errors";
|
|
27
|
+
/**
|
|
28
|
+
* Source kinds that the loader is allowed to mark `writable: true`. Anything
|
|
29
|
+
* else is rejected at config load (per locked decision 4) — see
|
|
30
|
+
* {@link assertWritableAllowedForKind}.
|
|
31
|
+
*/
|
|
32
|
+
const REJECTED_WRITABLE_KINDS = new Set(["website", "npm"]);
|
|
33
|
+
// ── Public helpers ──────────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the effective `writable` flag for a source config entry, applying
|
|
36
|
+
* the v1 default policy from spec §5.4:
|
|
37
|
+
*
|
|
38
|
+
* - `filesystem` → `true` by default
|
|
39
|
+
* - everything else → `false` by default
|
|
40
|
+
*
|
|
41
|
+
* Users can opt out for `filesystem` via `writable: false`. They cannot opt
|
|
42
|
+
* **in** for `website` / `npm` — that combination is rejected at config load
|
|
43
|
+
* (see {@link assertWritableAllowedForKind}).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveWritable(entry) {
|
|
46
|
+
if (typeof entry.writable === "boolean")
|
|
47
|
+
return entry.writable;
|
|
48
|
+
return entry.type === "filesystem";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Reject `writable: true` on `website` / `npm` sources at config-load time.
|
|
52
|
+
* Per locked decision 4 (§6 of the v1 implementation plan): `sync()` would
|
|
53
|
+
* clobber writes on the next refresh, so allowing writes here is a footgun.
|
|
54
|
+
*
|
|
55
|
+
* Throws {@link ConfigError} when the combination is rejected.
|
|
56
|
+
*/
|
|
57
|
+
export function assertWritableAllowedForKind(entry) {
|
|
58
|
+
if (entry.writable !== true)
|
|
59
|
+
return;
|
|
60
|
+
if (REJECTED_WRITABLE_KINDS.has(entry.type)) {
|
|
61
|
+
const label = entry.name ? ` "${entry.name}"` : "";
|
|
62
|
+
throw new ConfigError(`writable: true is only supported on filesystem and git sources (got "${entry.type}" on source${label}).`, "INVALID_CONFIG_FILE", "To author into a checked-out package, add the same path as a separate filesystem source.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write a textual asset (`content`) into `source` at the path implied by
|
|
67
|
+
* `ref`. Always:
|
|
68
|
+
*
|
|
69
|
+
* 1. Refuses if `config.writable` is not truthy (per §5.4).
|
|
70
|
+
* 2. Performs a plain filesystem write to `path.join(source.path, …)`.
|
|
71
|
+
*
|
|
72
|
+
* For sources of `kind === "git"`, additionally:
|
|
73
|
+
*
|
|
74
|
+
* 3. `git -C <path> add <file>`
|
|
75
|
+
* 4. `git -C <path> commit -m "Update <ref>"`
|
|
76
|
+
* 5. `git -C <path> push` when `config.options.pushOnCommit` is truthy.
|
|
77
|
+
*
|
|
78
|
+
* Any other `kind` reaching this helper is a configuration bug — the loader
|
|
79
|
+
* rejects unsupported writable kinds — so we throw {@link ConfigError}.
|
|
80
|
+
*/
|
|
81
|
+
export async function writeAssetToSource(source, config, ref, content) {
|
|
82
|
+
ensureWritable(source, config);
|
|
83
|
+
const filePath = resolveAssetFilePath(source, ref);
|
|
84
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
85
|
+
const normalized = content.endsWith("\n") ? content : `${content}\n`;
|
|
86
|
+
fs.writeFileSync(filePath, normalized, "utf8");
|
|
87
|
+
await runKindSpecificCommit(source, config, filePath, `Update ${formatRefForMessage(ref)}`);
|
|
88
|
+
return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Delete the asset at `ref` from `source`. Symmetric to
|
|
92
|
+
* {@link writeAssetToSource}: same writable check, same git-commit-and-push
|
|
93
|
+
* convenience for `kind === "git"`.
|
|
94
|
+
*/
|
|
95
|
+
export async function deleteAssetFromSource(source, config, ref) {
|
|
96
|
+
ensureWritable(source, config);
|
|
97
|
+
const filePath = resolveAssetFilePath(source, ref);
|
|
98
|
+
if (!fs.existsSync(filePath)) {
|
|
99
|
+
throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
|
|
100
|
+
}
|
|
101
|
+
fs.unlinkSync(filePath);
|
|
102
|
+
await runKindSpecificCommit(source, config, filePath, `Remove ${formatRefForMessage(ref)}`);
|
|
103
|
+
return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the destination for a write per locked decision 3:
|
|
107
|
+
*
|
|
108
|
+
* 1. Explicit `--target <name>` (when supplied)
|
|
109
|
+
* 2. `config.defaultWriteTarget`
|
|
110
|
+
* 3. `config.stashDir` (the working stash created by `akm init`)
|
|
111
|
+
* 4. `ConfigError("no writable source configured; run `akm init`")`
|
|
112
|
+
*
|
|
113
|
+
* The legacy `first-writable-in-source-array-order` fallback is *not* used —
|
|
114
|
+
* see plan §6 decision 3 for the rationale.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveWriteTarget(akmConfig, explicitTarget) {
|
|
117
|
+
const configuredSources = resolveConfiguredSources(akmConfig);
|
|
118
|
+
// 1. Explicit --target wins.
|
|
119
|
+
if (explicitTarget) {
|
|
120
|
+
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
121
|
+
if (!match) {
|
|
122
|
+
throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
123
|
+
}
|
|
124
|
+
// Up-front writable check so an explicit --target fails fast with a
|
|
125
|
+
// ConfigError (rather than the generic UsageError ensureWritable would
|
|
126
|
+
// raise after we've already started building paths). Resolve the
|
|
127
|
+
// effective writable flag (filesystem defaults to true; everything else
|
|
128
|
+
// defaults to false) so unset values are interpreted correctly.
|
|
129
|
+
const effectiveWritable = resolveWritable({ type: match.type, writable: match.writable });
|
|
130
|
+
if (!effectiveWritable) {
|
|
131
|
+
throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass --target to a different source.`);
|
|
132
|
+
}
|
|
133
|
+
return adaptConfiguredSource(match);
|
|
134
|
+
}
|
|
135
|
+
// 2. config.defaultWriteTarget.
|
|
136
|
+
if (akmConfig.defaultWriteTarget) {
|
|
137
|
+
const match = configuredSources.find((s) => s.name === akmConfig.defaultWriteTarget);
|
|
138
|
+
if (match)
|
|
139
|
+
return adaptConfiguredSource(match);
|
|
140
|
+
// Fall through if the named target no longer exists — surface a clear error.
|
|
141
|
+
throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" does not match any configured source.`, "INVALID_CONFIG_FILE", "Update `defaultWriteTarget` in your config (run `akm config get defaultWriteTarget`) or run `akm list` to see configured sources.");
|
|
142
|
+
}
|
|
143
|
+
// 3. Working stash (config.stashDir / resolveStashDir()).
|
|
144
|
+
try {
|
|
145
|
+
const stashDir = resolveStashDir({ readOnly: true });
|
|
146
|
+
return {
|
|
147
|
+
source: { kind: "filesystem", name: "stash", path: stashDir },
|
|
148
|
+
config: { type: "filesystem", path: stashDir, name: "stash", writable: true },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Fall through to the final ConfigError below.
|
|
153
|
+
}
|
|
154
|
+
// 4. Nothing usable.
|
|
155
|
+
throw new ConfigError("no writable source configured; run `akm init`", "STASH_DIR_NOT_FOUND", "Run `akm init` to create a working stash, or set `defaultWriteTarget` in your config.");
|
|
156
|
+
}
|
|
157
|
+
// ── Internals ───────────────────────────────────────────────────────────────
|
|
158
|
+
function ensureWritable(source, config) {
|
|
159
|
+
// Apply the same default-resolution rule as resolveWritable so callers can
|
|
160
|
+
// pass through a SourceConfigEntry with an absent `writable` field.
|
|
161
|
+
const writable = resolveWritable(config);
|
|
162
|
+
if (!writable) {
|
|
163
|
+
throw new UsageError(`Source "${source.name}" is not writable. Set \`writable: true\` on the source config entry to enable writes.`, "INVALID_FLAG_VALUE");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function resolveAssetFilePath(source, ref) {
|
|
167
|
+
const typeDir = TYPE_DIRS[ref.type];
|
|
168
|
+
if (!typeDir) {
|
|
169
|
+
throw new UsageError(`Unknown asset type "${ref.type}". Cannot resolve a write path.`, "INVALID_FLAG_VALUE");
|
|
170
|
+
}
|
|
171
|
+
const typeRoot = path.join(source.path, typeDir);
|
|
172
|
+
const assetPath = resolveAssetPathFromName(ref.type, typeRoot, ref.name);
|
|
173
|
+
if (!isWithin(assetPath, typeRoot)) {
|
|
174
|
+
throw new UsageError(`Resolved asset path escapes its source: "${ref.name}" in source "${source.name}".`, "PATH_ESCAPE_VIOLATION");
|
|
175
|
+
}
|
|
176
|
+
return assetPath;
|
|
177
|
+
}
|
|
178
|
+
async function runKindSpecificCommit(source, config, filePath, message) {
|
|
179
|
+
if (source.kind === "filesystem") {
|
|
180
|
+
return; // No commit step.
|
|
181
|
+
}
|
|
182
|
+
if (source.kind === "git") {
|
|
183
|
+
runGitCommit(source.path, filePath, message);
|
|
184
|
+
if (config.options?.pushOnCommit) {
|
|
185
|
+
runGitPush(source.path);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Reject any other kind reaching the helper. The config loader is the
|
|
190
|
+
// first line of defence (assertWritableAllowedForKind), but we throw here
|
|
191
|
+
// so external callers that bypass the loader still get a clear error.
|
|
192
|
+
throw new ConfigError(`write-source: unsupported kind "${source.kind}" for source "${source.name}". ` +
|
|
193
|
+
"Writes are only defined for `filesystem` and `git` sources.", "INVALID_CONFIG_FILE", 'Set `kind: "filesystem"` (or `kind: "git"`) on the source, or add a parallel filesystem entry.');
|
|
194
|
+
}
|
|
195
|
+
function runGitCommit(repoDir, filePath, message) {
|
|
196
|
+
// Stage the specific file rather than `add -A` so unrelated working-tree
|
|
197
|
+
// changes don't get folded into the asset commit.
|
|
198
|
+
const relPath = path.relative(repoDir, filePath) || filePath;
|
|
199
|
+
const addResult = spawnSync("git", ["-C", repoDir, "add", "--", relPath], { encoding: "utf8" });
|
|
200
|
+
if (addResult.status !== 0) {
|
|
201
|
+
throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
|
|
202
|
+
}
|
|
203
|
+
// Provide a fallback identity so fresh CI/test environments without
|
|
204
|
+
// user.name/user.email configured can always commit.
|
|
205
|
+
const commitResult = spawnSync("git", ["-C", repoDir, "-c", "user.name=akm", "-c", "user.email=akm@local", "commit", "-m", message], { encoding: "utf8" });
|
|
206
|
+
if (commitResult.status !== 0) {
|
|
207
|
+
// `nothing to commit` is a no-op success — the file may have matched the
|
|
208
|
+
// existing tree exactly. Surface other errors verbatim.
|
|
209
|
+
const stderr = commitResult.stderr ?? "";
|
|
210
|
+
if (/nothing to commit|no changes added/i.test(stderr) ||
|
|
211
|
+
/nothing to commit|no changes added/i.test(commitResult.stdout ?? "")) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`git commit failed: ${stderr.trim() || "unknown error"}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function runGitPush(repoDir) {
|
|
218
|
+
const pushResult = spawnSync("git", ["-C", repoDir, "push"], { encoding: "utf8", timeout: 120_000 });
|
|
219
|
+
if (pushResult.status !== 0) {
|
|
220
|
+
throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function formatRefForMessage(ref) {
|
|
224
|
+
return ref.origin ? `${ref.origin}//${ref.type}:${ref.name}` : `${ref.type}:${ref.name}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Derive a {@link WriteTargetSource} + persisted {@link SourceConfigEntry}
|
|
228
|
+
* from the runtime {@link ConfiguredSource} representation used elsewhere in
|
|
229
|
+
* the codebase. The mapping is:
|
|
230
|
+
*
|
|
231
|
+
* ConfiguredSource.type → WriteTargetSource.kind
|
|
232
|
+
* ConfiguredSource.name → WriteTargetSource.name
|
|
233
|
+
* ConfiguredSource.source.* → WriteTargetSource.path (via parseSourceSpec)
|
|
234
|
+
*
|
|
235
|
+
* Legacy aliases (`context-hub`, `github`) have already been normalised to
|
|
236
|
+
* `git` by the config loader, so this mapping is straightforward.
|
|
237
|
+
*/
|
|
238
|
+
function adaptConfiguredSource(runtime) {
|
|
239
|
+
const writePath = pathFromConfiguredSource(runtime);
|
|
240
|
+
if (!writePath) {
|
|
241
|
+
throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
|
|
242
|
+
}
|
|
243
|
+
// Map the runtime kind to the write helper's `kind` discriminator. Only
|
|
244
|
+
// filesystem and git produce writable sources at v1.
|
|
245
|
+
const kind = runtime.type === "filesystem" || runtime.type === "git" ? runtime.type : runtime.type;
|
|
246
|
+
const config = {
|
|
247
|
+
type: runtime.type,
|
|
248
|
+
name: runtime.name,
|
|
249
|
+
...(writePath !== undefined ? { path: writePath } : {}),
|
|
250
|
+
...(runtime.writable !== undefined ? { writable: runtime.writable } : {}),
|
|
251
|
+
...(runtime.options ? { options: runtime.options } : {}),
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
source: { kind, name: runtime.name, path: writePath },
|
|
255
|
+
config,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function pathFromConfiguredSource(runtime) {
|
|
259
|
+
// ConfiguredSource.source is the parsed SourceSpec (filesystem|git|website|npm).
|
|
260
|
+
// For writable kinds we only ever care about a local on-disk path: filesystem
|
|
261
|
+
// sources expose it directly; git sources resolve through the cache mirror
|
|
262
|
+
// (handled by the existing source provider). For v1 the helper trusts
|
|
263
|
+
// callers to materialise the cache path beforehand and does not re-clone.
|
|
264
|
+
const spec = runtime.source;
|
|
265
|
+
if (spec.type === "filesystem")
|
|
266
|
+
return spec.path;
|
|
267
|
+
// For git sources we fall back to the cached repo directory the provider
|
|
268
|
+
// already materialised. The lookup is intentionally lazy — we only import
|
|
269
|
+
// it when needed to keep the helper's import graph small.
|
|
270
|
+
if (spec.type === "git") {
|
|
271
|
+
try {
|
|
272
|
+
const repo = parseGitRepoUrl(spec.url);
|
|
273
|
+
return getCachePaths(repo.canonicalUrl).repoDir;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Database-backed (SQLite + FTS5/vector) source search implementation.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
5
|
-
*
|
|
4
|
+
* Extracted from source-search.ts to break the circular import:
|
|
5
|
+
* source-search.ts → sources/providers/filesystem.ts → db-search.ts (no cycle)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* source-search.ts imports this module for the `searchLocal` export.
|
|
8
|
+
* sources/providers/filesystem.ts also imports `searchLocal` from here.
|
|
9
|
+
*
|
|
10
|
+
* Renamed from `local-search.ts` to signal that this is the DB-layer search
|
|
11
|
+
* implementation, not a "local vs. remote" distinction.
|
|
9
12
|
*/
|
|
10
13
|
import fs from "node:fs";
|
|
11
14
|
import path from "node:path";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
15
|
+
import { makeAssetRef } from "../core/asset-ref";
|
|
16
|
+
import { defaultRendererRegistry } from "../core/asset-registry";
|
|
17
|
+
import { deriveCanonicalAssetNameFromStashRoot } from "../core/asset-spec";
|
|
18
|
+
import { getDbPath } from "../core/paths";
|
|
19
|
+
import { warn } from "../core/warn";
|
|
14
20
|
import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getUtilityScoresByIds, openDatabase, searchFts, searchVec, } from "./db";
|
|
15
21
|
import { getRenderer } from "./file-context";
|
|
16
|
-
import { buildSearchText } from "./indexer";
|
|
17
22
|
import { generateMetadataFlat, loadStashFile, shouldIndexStashFile } from "./metadata";
|
|
18
|
-
import {
|
|
23
|
+
import { buildSearchText } from "./search-fields";
|
|
19
24
|
import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
|
|
20
25
|
import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status";
|
|
21
|
-
import { makeAssetRef } from "./stash-ref";
|
|
22
26
|
import { walkStashFlat } from "./walker";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const name = TYPE_TO_RENDERER[type];
|
|
27
|
+
export async function rendererForType(type, registry = defaultRendererRegistry) {
|
|
28
|
+
const name = registry.rendererNameFor(type);
|
|
26
29
|
return name ? getRenderer(name) : undefined;
|
|
27
30
|
}
|
|
28
|
-
export function buildLocalAction(type, ref) {
|
|
29
|
-
const builder =
|
|
31
|
+
export function buildLocalAction(type, ref, registry = defaultRendererRegistry) {
|
|
32
|
+
const builder = registry.actionBuilderFor(type);
|
|
30
33
|
return builder ? builder(ref) : `akm show ${ref}`;
|
|
31
34
|
}
|
|
32
35
|
function resolveSearchHitRef(entry, refName, source) {
|
|
@@ -41,7 +44,8 @@ function resolveSearchHitOrigin(source) {
|
|
|
41
44
|
// ── Main search entrypoint ───────────────────────────────────────────────────
|
|
42
45
|
export async function searchLocal(input) {
|
|
43
46
|
const { query, searchType, limit, stashDir, sources, config } = input;
|
|
44
|
-
const
|
|
47
|
+
const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
|
|
48
|
+
const allSourceDirs = sources.map((s) => s.path);
|
|
45
49
|
const rawStatus = readSemanticStatus();
|
|
46
50
|
const semanticStatus = getEffectiveSemanticStatus(config, rawStatus);
|
|
47
51
|
const warnings = [];
|
|
@@ -81,7 +85,7 @@ export async function searchLocal(input) {
|
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
if (entryCount > 0 && stashDirMatch) {
|
|
84
|
-
const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir,
|
|
88
|
+
const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry);
|
|
85
89
|
return {
|
|
86
90
|
hits,
|
|
87
91
|
tip: hits.length === 0
|
|
@@ -101,7 +105,7 @@ export async function searchLocal(input) {
|
|
|
101
105
|
catch (error) {
|
|
102
106
|
warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
|
|
103
107
|
}
|
|
104
|
-
const hitArrays = await Promise.all(
|
|
108
|
+
const hitArrays = await Promise.all(allSourceDirs.map((dir) => substringSearch(query, searchType, limit, dir, sources, config, rendererRegistry)));
|
|
105
109
|
const hits = hitArrays.flat().slice(0, limit);
|
|
106
110
|
return {
|
|
107
111
|
hits,
|
|
@@ -110,7 +114,7 @@ export async function searchLocal(input) {
|
|
|
110
114
|
};
|
|
111
115
|
}
|
|
112
116
|
// ── Database search ─────────────────────────────────────────────────────────
|
|
113
|
-
async function searchDatabase(db, query, searchType, limit, stashDir,
|
|
117
|
+
async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry) {
|
|
114
118
|
// Empty query: return all entries
|
|
115
119
|
if (!query) {
|
|
116
120
|
const typeFilter = searchType === "any" ? undefined : searchType;
|
|
@@ -131,9 +135,10 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
|
|
|
131
135
|
query,
|
|
132
136
|
rankingMode: "fts",
|
|
133
137
|
defaultStashDir: stashDir,
|
|
134
|
-
|
|
138
|
+
allSourceDirs,
|
|
135
139
|
sources,
|
|
136
140
|
config,
|
|
141
|
+
rendererRegistry,
|
|
137
142
|
})));
|
|
138
143
|
return { hits };
|
|
139
144
|
}
|
|
@@ -350,13 +355,19 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
|
|
|
350
355
|
item.utilityBoosted = true;
|
|
351
356
|
}
|
|
352
357
|
}
|
|
358
|
+
// ── minScore floor ──────────────────────────────────────────────────────
|
|
359
|
+
// Drop semantic-only hits (cosine-only, no FTS match) whose score falls
|
|
360
|
+
// below the configured floor. FTS hits and hybrid hits are always kept.
|
|
361
|
+
// Default floor: 0.2. Set search.minScore = 0 in config to disable.
|
|
362
|
+
const minScore = config.search?.minScore ?? 0.2;
|
|
363
|
+
const preFilter = minScore > 0 ? scored.filter((item) => item.rankingMode !== "semantic" || item.score >= minScore) : scored;
|
|
353
364
|
// Deterministic tiebreaker on equal scores
|
|
354
|
-
|
|
365
|
+
preFilter.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name));
|
|
355
366
|
// Deduplicate by file path — keep only the highest-scored entry per file.
|
|
356
367
|
// Multiple .stash.json entries can map to the same file (e.g. entries without
|
|
357
368
|
// a filename field all collapse to files[0]). Showing the same path/ref
|
|
358
369
|
// multiple times clutters results.
|
|
359
|
-
const deduped = deduplicateByPath(
|
|
370
|
+
const deduped = deduplicateByPath(preFilter);
|
|
360
371
|
const rankMs = Date.now() - tRank0;
|
|
361
372
|
const selected = deduped.slice(0, limit);
|
|
362
373
|
const hits = await Promise.all(selected.map(({ entry, filePath, score, rankingMode, utilityBoosted }) => buildDbHit({
|
|
@@ -367,10 +378,11 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
|
|
|
367
378
|
query,
|
|
368
379
|
rankingMode,
|
|
369
380
|
defaultStashDir: stashDir,
|
|
370
|
-
|
|
381
|
+
allSourceDirs,
|
|
371
382
|
sources,
|
|
372
383
|
config,
|
|
373
384
|
utilityBoosted,
|
|
385
|
+
rendererRegistry,
|
|
374
386
|
})));
|
|
375
387
|
return { embedMs, rankMs, hits };
|
|
376
388
|
}
|
|
@@ -383,7 +395,7 @@ async function tryVecScores(db, query, k, config) {
|
|
|
383
395
|
if (hasEmbeddings !== "1")
|
|
384
396
|
return null;
|
|
385
397
|
try {
|
|
386
|
-
const { embed } = await import("
|
|
398
|
+
const { embed } = await import("../llm/embedder.js");
|
|
387
399
|
const queryEmbedding = await embed(query, config.embedding);
|
|
388
400
|
const vecResults = searchVec(db, queryEmbedding, k);
|
|
389
401
|
const scores = new Map();
|
|
@@ -401,20 +413,24 @@ async function tryVecScores(db, query, k, config) {
|
|
|
401
413
|
}
|
|
402
414
|
}
|
|
403
415
|
// ── Substring fallback (no index) ───────────────────────────────────────────
|
|
404
|
-
async function substringSearch(query, searchType, limit, stashDir, sources, config) {
|
|
416
|
+
async function substringSearch(query, searchType, limit, stashDir, sources, config, rendererRegistry = defaultRendererRegistry) {
|
|
405
417
|
const assets = await indexAssets(stashDir, searchType, sources);
|
|
406
418
|
const matched = assets.filter((asset) => !query || buildSearchText(asset.entry).includes(query));
|
|
407
419
|
if (!query) {
|
|
408
420
|
const sorted = matched.sort(compareAssets);
|
|
409
421
|
const unique = deduplicateAssetsByPath(sorted);
|
|
410
|
-
return Promise.all(unique
|
|
422
|
+
return Promise.all(unique
|
|
423
|
+
.slice(0, limit)
|
|
424
|
+
.map((asset) => assetToSearchHit(asset, stashDir, sources, config, undefined, rendererRegistry)));
|
|
411
425
|
}
|
|
412
426
|
// Score and sort by relevance
|
|
413
427
|
const scored = matched.map((asset) => ({ asset, score: scoreSubstringMatch(asset.entry, query) }));
|
|
414
428
|
scored.sort((a, b) => b.score - a.score || compareAssets(a.asset, b.asset));
|
|
415
429
|
// Deduplicate by path — keep highest-scored entry per file
|
|
416
430
|
const dedupedScored = deduplicateByPath(scored.map((s) => ({ ...s, filePath: s.asset.path })));
|
|
417
|
-
return Promise.all(dedupedScored
|
|
431
|
+
return Promise.all(dedupedScored
|
|
432
|
+
.slice(0, limit)
|
|
433
|
+
.map(({ asset, score }) => assetToSearchHit(asset, stashDir, sources, config, score, rendererRegistry)));
|
|
418
434
|
}
|
|
419
435
|
function scoreSubstringMatch(entry, query) {
|
|
420
436
|
const tokens = query.split(/\s+/).filter(Boolean);
|
|
@@ -444,6 +460,7 @@ function scoreSubstringMatch(entry, query) {
|
|
|
444
460
|
}
|
|
445
461
|
// ── Hit building ────────────────────────────────────────────────────────────
|
|
446
462
|
export async function buildDbHit(input) {
|
|
463
|
+
const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
|
|
447
464
|
const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
|
|
448
465
|
const canonical = deriveCanonicalAssetNameFromStashRoot(input.entry.type, entryStashDir, input.path);
|
|
449
466
|
const refName = canonical && !canonical.startsWith("../") && !canonical.startsWith("..\\") ? canonical : input.entry.name;
|
|
@@ -471,12 +488,12 @@ export async function buildDbHit(input) {
|
|
|
471
488
|
description: input.entry.description,
|
|
472
489
|
tags: input.entry.tags,
|
|
473
490
|
size: deriveSize(input.entry.fileSize),
|
|
474
|
-
action: buildLocalAction(input.entry.type, ref),
|
|
491
|
+
action: buildLocalAction(input.entry.type, ref, rendererRegistry),
|
|
475
492
|
score,
|
|
476
493
|
whyMatched,
|
|
477
494
|
...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
|
|
478
495
|
};
|
|
479
|
-
const renderer = await rendererForType(input.entry.type);
|
|
496
|
+
const renderer = await rendererForType(input.entry.type, rendererRegistry);
|
|
480
497
|
if (renderer?.enrichSearchHit) {
|
|
481
498
|
renderer.enrichSearchHit(hit, entryStashDir);
|
|
482
499
|
}
|
|
@@ -530,7 +547,7 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
|
|
|
530
547
|
reasons.push("usage history boost");
|
|
531
548
|
return reasons;
|
|
532
549
|
}
|
|
533
|
-
async function assetToSearchHit(asset, stashDir, sources, config, score) {
|
|
550
|
+
async function assetToSearchHit(asset, stashDir, sources, config, score, rendererRegistry = defaultRendererRegistry) {
|
|
534
551
|
const source = findSourceForPath(asset.path, sources);
|
|
535
552
|
const editable = isEditable(asset.path, config);
|
|
536
553
|
const ref = resolveSearchHitRef(asset.entry, asset.entry.name, source);
|
|
@@ -550,11 +567,11 @@ async function assetToSearchHit(asset, stashDir, sources, config, score) {
|
|
|
550
567
|
description: asset.entry.description,
|
|
551
568
|
tags: asset.entry.tags,
|
|
552
569
|
...(size ? { size } : {}),
|
|
553
|
-
action: buildLocalAction(asset.entry.type, ref),
|
|
570
|
+
action: buildLocalAction(asset.entry.type, ref, rendererRegistry),
|
|
554
571
|
...(score !== undefined ? { score } : {}),
|
|
555
572
|
...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
|
|
556
573
|
};
|
|
557
|
-
const renderer = await rendererForType(asset.entry.type);
|
|
574
|
+
const renderer = await rendererForType(asset.entry.type, rendererRegistry);
|
|
558
575
|
if (renderer?.enrichSearchHit) {
|
|
559
576
|
renderer.enrichSearchHit(hit, stashDir);
|
|
560
577
|
}
|