akm-cli 0.0.0 → 0.0.16
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/LICENSE +385 -0
- package/README.md +298 -6
- package/dist/asset-spec.js +70 -0
- package/dist/cli.js +912 -0
- package/dist/common.js +199 -0
- package/dist/config-cli.js +195 -0
- package/dist/config.js +324 -0
- package/dist/db.js +371 -0
- package/dist/embedder.js +150 -0
- package/dist/errors.js +28 -0
- package/dist/file-context.js +187 -0
- package/dist/frontmatter.js +86 -0
- package/dist/github.js +17 -0
- package/dist/indexer.js +341 -0
- package/dist/init.js +43 -0
- package/dist/llm.js +87 -0
- package/dist/lockfile.js +60 -0
- package/dist/markdown.js +77 -0
- package/dist/matchers.js +170 -0
- package/dist/metadata.js +408 -0
- package/dist/origin-resolve.js +54 -0
- package/dist/paths.js +92 -0
- package/dist/registry-install.js +459 -0
- package/dist/registry-resolve.js +486 -0
- package/dist/registry-search.js +255 -0
- package/dist/registry-types.js +1 -0
- package/dist/renderers.js +386 -0
- package/dist/ripgrep-install.js +155 -0
- package/dist/ripgrep-resolve.js +78 -0
- package/dist/ripgrep.js +2 -0
- package/dist/self-update.js +226 -0
- package/dist/stash-add.js +71 -0
- package/dist/stash-clone.js +115 -0
- package/dist/stash-ref.js +75 -0
- package/dist/stash-registry.js +206 -0
- package/dist/stash-resolve.js +92 -0
- package/dist/stash-search.js +494 -0
- package/dist/stash-show.js +59 -0
- package/dist/stash-source.js +131 -0
- package/dist/stash-types.js +1 -0
- package/dist/walker.js +163 -0
- package/dist/warn.js +20 -0
- package/package.json +53 -7
- package/index.js +0 -4
package/dist/common.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TYPE_DIRS } from "./asset-spec";
|
|
4
|
+
import { ConfigError } from "./errors";
|
|
5
|
+
import { getConfigPath, getDefaultStashDir } from "./paths";
|
|
6
|
+
/**
|
|
7
|
+
* Normalize an asset type for output purposes.
|
|
8
|
+
* "tool" is a transparent alias for "script" -- all output should use "script".
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeAssetType(type) {
|
|
11
|
+
return type === "tool" ? "script" : type;
|
|
12
|
+
}
|
|
13
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
14
|
+
export const IS_WINDOWS = process.platform === "win32";
|
|
15
|
+
// ── Validators ──────────────────────────────────────────────────────────────
|
|
16
|
+
export function isAssetType(type) {
|
|
17
|
+
return type in TYPE_DIRS;
|
|
18
|
+
}
|
|
19
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the stash directory using a three-level fallback chain:
|
|
22
|
+
* 1. AKM_STASH_DIR environment variable (override for CI/scripts)
|
|
23
|
+
* 2. stashDir field in config.json
|
|
24
|
+
* 3. Platform default (~/akm or ~/Documents/akm on Windows)
|
|
25
|
+
*
|
|
26
|
+
* Throws if no valid stash directory is found.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveStashDir(options) {
|
|
29
|
+
// 1. Env var override (for CI, scripts, testing)
|
|
30
|
+
const envDir = process.env.AKM_STASH_DIR?.trim();
|
|
31
|
+
if (envDir) {
|
|
32
|
+
const resolved = validateStashDir(envDir);
|
|
33
|
+
if (!options?.readOnly)
|
|
34
|
+
persistStashDirToConfig(resolved);
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
// 2. Config file stashDir field
|
|
38
|
+
const configStashDir = readStashDirFromConfig();
|
|
39
|
+
if (configStashDir)
|
|
40
|
+
return validateStashDir(configStashDir);
|
|
41
|
+
// 3. Platform default — use it if it exists
|
|
42
|
+
const defaultDir = getDefaultStashDir();
|
|
43
|
+
if (isValidDirectory(defaultDir)) {
|
|
44
|
+
return defaultDir;
|
|
45
|
+
}
|
|
46
|
+
throw new ConfigError(`No stash directory found. Run "akm init" to create one at ${defaultDir}, ` +
|
|
47
|
+
`or set stashDir in ${getConfigPath()}.`);
|
|
48
|
+
}
|
|
49
|
+
function validateStashDir(raw) {
|
|
50
|
+
const stashDir = path.resolve(raw);
|
|
51
|
+
let stat;
|
|
52
|
+
try {
|
|
53
|
+
stat = fs.statSync(stashDir);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new ConfigError(`Unable to read stash directory at "${stashDir}".`);
|
|
57
|
+
}
|
|
58
|
+
if (!stat.isDirectory()) {
|
|
59
|
+
throw new ConfigError(`Stash path must point to a directory: "${stashDir}".`);
|
|
60
|
+
}
|
|
61
|
+
return stashDir;
|
|
62
|
+
}
|
|
63
|
+
function isValidDirectory(dir) {
|
|
64
|
+
try {
|
|
65
|
+
return fs.statSync(dir).isDirectory();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read stashDir directly from config.json without pulling in the full config
|
|
73
|
+
* module, to avoid circular dependencies.
|
|
74
|
+
*/
|
|
75
|
+
function readStashDirFromConfig() {
|
|
76
|
+
try {
|
|
77
|
+
const configPath = getConfigPath();
|
|
78
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
79
|
+
const raw = JSON.parse(text);
|
|
80
|
+
if (typeof raw === "object" && raw !== null && typeof raw.stashDir === "string" && raw.stashDir.trim()) {
|
|
81
|
+
return raw.stashDir.trim();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Config doesn't exist or is invalid — fall through
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Persist stashDir to config.json if not already set, so users can
|
|
91
|
+
* transition away from relying on the AKM_STASH_DIR env var.
|
|
92
|
+
*/
|
|
93
|
+
function persistStashDirToConfig(stashDir) {
|
|
94
|
+
try {
|
|
95
|
+
const configPath = getConfigPath();
|
|
96
|
+
let raw = {};
|
|
97
|
+
try {
|
|
98
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
99
|
+
const parsed = JSON.parse(text);
|
|
100
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
101
|
+
raw = parsed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// No existing config or invalid — start fresh
|
|
106
|
+
}
|
|
107
|
+
if (!raw.stashDir) {
|
|
108
|
+
raw.stashDir = stashDir;
|
|
109
|
+
const dir = path.dirname(configPath);
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
const tmpPath = `${configPath}.tmp.${process.pid}`;
|
|
112
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
113
|
+
fs.renameSync(tmpPath, configPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Non-fatal: best-effort persistence
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function toPosix(input) {
|
|
121
|
+
return input.replace(/\\/g, "/");
|
|
122
|
+
}
|
|
123
|
+
export function hasErrnoCode(error, code) {
|
|
124
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
125
|
+
return false;
|
|
126
|
+
return error.code === code;
|
|
127
|
+
}
|
|
128
|
+
export function isWithin(candidate, root) {
|
|
129
|
+
const resolvedRoot = safeRealpath(root);
|
|
130
|
+
const resolvedCandidate = safeRealpath(candidate);
|
|
131
|
+
const normalizedRoot = normalizeFsPathForComparison(resolvedRoot);
|
|
132
|
+
const normalizedCandidate = normalizeFsPathForComparison(resolvedCandidate);
|
|
133
|
+
const rel = path.relative(normalizedRoot, normalizedCandidate);
|
|
134
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
135
|
+
}
|
|
136
|
+
function safeRealpath(p) {
|
|
137
|
+
try {
|
|
138
|
+
return fs.realpathSync(path.resolve(p));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return path.resolve(p);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function normalizeFsPathForComparison(value) {
|
|
145
|
+
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch with an AbortController timeout.
|
|
149
|
+
* Defaults to 30 seconds if no timeout is specified.
|
|
150
|
+
*/
|
|
151
|
+
export async function fetchWithTimeout(url, opts, timeoutMs = 30_000) {
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
154
|
+
try {
|
|
155
|
+
return await fetch(url, { ...opts, signal: controller.signal });
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Fetch with retry and exponential backoff.
|
|
163
|
+
* Retries on network errors, 429, and 5xx responses.
|
|
164
|
+
* Honors Retry-After header for 429 responses.
|
|
165
|
+
*/
|
|
166
|
+
export async function fetchWithRetry(url, init, options) {
|
|
167
|
+
const maxRetries = options?.retries ?? 3;
|
|
168
|
+
const baseDelay = options?.baseDelay ?? 500;
|
|
169
|
+
const timeout = options?.timeout ?? 30_000;
|
|
170
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetchWithTimeout(url, init, timeout);
|
|
173
|
+
if (attempt < maxRetries && shouldRetry(response.status)) {
|
|
174
|
+
const retryAfter = parseRetryAfter(response);
|
|
175
|
+
const delay = retryAfter ?? baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5);
|
|
176
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
return response;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (attempt >= maxRetries)
|
|
183
|
+
throw err;
|
|
184
|
+
const delay = baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5);
|
|
185
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw new Error("fetchWithRetry: unreachable");
|
|
189
|
+
}
|
|
190
|
+
function shouldRetry(status) {
|
|
191
|
+
return status === 429 || status >= 500;
|
|
192
|
+
}
|
|
193
|
+
function parseRetryAfter(response) {
|
|
194
|
+
const header = response.headers.get("retry-after");
|
|
195
|
+
if (!header)
|
|
196
|
+
return undefined;
|
|
197
|
+
const seconds = parseInt(header, 10);
|
|
198
|
+
return Number.isNaN(seconds) ? undefined : seconds * 1000;
|
|
199
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG, } from "./config";
|
|
2
|
+
import { UsageError } from "./errors";
|
|
3
|
+
export function parseConfigValue(key, value) {
|
|
4
|
+
switch (key) {
|
|
5
|
+
case "stashDir":
|
|
6
|
+
return { stashDir: requireNonEmptyString(value, key) };
|
|
7
|
+
case "semanticSearch":
|
|
8
|
+
if (value !== "true" && value !== "false") {
|
|
9
|
+
throw new UsageError(`Invalid value for semanticSearch: expected "true" or "false"`);
|
|
10
|
+
}
|
|
11
|
+
return { semanticSearch: value === "true" };
|
|
12
|
+
case "searchPaths":
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(value);
|
|
15
|
+
if (!Array.isArray(parsed))
|
|
16
|
+
throw new UsageError("expected JSON array");
|
|
17
|
+
return { searchPaths: parsed.filter((d) => typeof d === "string") };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new UsageError(`Invalid value for searchPaths: expected JSON array (e.g. '["/path/a","/path/b"]')`);
|
|
21
|
+
}
|
|
22
|
+
case "embedding":
|
|
23
|
+
return { embedding: parseEmbeddingConnectionValue(value) };
|
|
24
|
+
case "llm":
|
|
25
|
+
return { llm: parseLlmConnectionValue(value) };
|
|
26
|
+
case "output.format":
|
|
27
|
+
return { output: { format: parseOutputFormat(value) } };
|
|
28
|
+
case "output.detail":
|
|
29
|
+
return { output: { detail: parseOutputDetail(value) } };
|
|
30
|
+
default:
|
|
31
|
+
throw new UsageError(`Unknown config key: ${key}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function getConfigValue(config, key) {
|
|
35
|
+
switch (key) {
|
|
36
|
+
case "stashDir":
|
|
37
|
+
return config.stashDir ?? null;
|
|
38
|
+
case "semanticSearch":
|
|
39
|
+
return config.semanticSearch;
|
|
40
|
+
case "searchPaths":
|
|
41
|
+
return [...config.searchPaths];
|
|
42
|
+
case "embedding":
|
|
43
|
+
return config.embedding ?? null;
|
|
44
|
+
case "llm":
|
|
45
|
+
return config.llm ?? null;
|
|
46
|
+
case "output.format":
|
|
47
|
+
return config.output?.format ?? null;
|
|
48
|
+
case "output.detail":
|
|
49
|
+
return config.output?.detail ?? null;
|
|
50
|
+
default:
|
|
51
|
+
throw new UsageError(`Unknown config key: ${key}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function setConfigValue(config, key, rawValue) {
|
|
55
|
+
switch (key) {
|
|
56
|
+
case "stashDir":
|
|
57
|
+
case "semanticSearch":
|
|
58
|
+
case "searchPaths":
|
|
59
|
+
case "embedding":
|
|
60
|
+
case "llm":
|
|
61
|
+
case "output.format":
|
|
62
|
+
case "output.detail":
|
|
63
|
+
return mergeConfigValue(config, parseConfigValue(key, rawValue));
|
|
64
|
+
default:
|
|
65
|
+
throw new UsageError(`Unknown config key: ${key}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function unsetConfigValue(config, key) {
|
|
69
|
+
switch (key) {
|
|
70
|
+
case "stashDir":
|
|
71
|
+
return { ...config, stashDir: undefined };
|
|
72
|
+
case "embedding":
|
|
73
|
+
return { ...config, embedding: undefined };
|
|
74
|
+
case "llm":
|
|
75
|
+
return { ...config, llm: undefined };
|
|
76
|
+
case "output.format":
|
|
77
|
+
return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
|
|
78
|
+
case "output.detail":
|
|
79
|
+
return { ...config, output: mergeOutputConfig(config.output, { detail: undefined }) };
|
|
80
|
+
default:
|
|
81
|
+
throw new UsageError(`Unknown or unsupported unset key: ${key}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function listConfig(config) {
|
|
85
|
+
return {
|
|
86
|
+
...DEFAULT_CONFIG,
|
|
87
|
+
...config,
|
|
88
|
+
output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
|
|
89
|
+
stashDir: config.stashDir ?? null,
|
|
90
|
+
embedding: config.embedding ?? null,
|
|
91
|
+
llm: config.llm ?? null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function mergeConfigValue(config, partial) {
|
|
95
|
+
return {
|
|
96
|
+
...config,
|
|
97
|
+
...partial,
|
|
98
|
+
output: mergeOutputConfig(config.output, partial.output),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function mergeOutputConfig(base, override) {
|
|
102
|
+
const merged = {
|
|
103
|
+
...(base ?? {}),
|
|
104
|
+
...(override ?? {}),
|
|
105
|
+
};
|
|
106
|
+
return merged.format || merged.detail ? merged : undefined;
|
|
107
|
+
}
|
|
108
|
+
function parseOutputFormat(value) {
|
|
109
|
+
if (value === "json" || value === "yaml" || value === "text")
|
|
110
|
+
return value;
|
|
111
|
+
throw new UsageError(`Invalid value for output.format: expected one of json|yaml|text`);
|
|
112
|
+
}
|
|
113
|
+
function parseOutputDetail(value) {
|
|
114
|
+
if (value === "brief" || value === "normal" || value === "full")
|
|
115
|
+
return value;
|
|
116
|
+
throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
|
|
117
|
+
}
|
|
118
|
+
function parseEmbeddingConnectionValue(value) {
|
|
119
|
+
if (value === "null" || value === "")
|
|
120
|
+
return undefined;
|
|
121
|
+
const parsed = parseJsonObject(value, "embedding", {
|
|
122
|
+
endpoint: "http://localhost:11434/v1/embeddings",
|
|
123
|
+
model: "nomic-embed-text",
|
|
124
|
+
});
|
|
125
|
+
const result = {
|
|
126
|
+
endpoint: asRequiredString(parsed.endpoint, "embedding", "endpoint"),
|
|
127
|
+
model: asRequiredString(parsed.model, "embedding", "model"),
|
|
128
|
+
};
|
|
129
|
+
if (typeof parsed.provider === "string" && parsed.provider)
|
|
130
|
+
result.provider = parsed.provider;
|
|
131
|
+
if (parsed.dimension !== undefined)
|
|
132
|
+
result.dimension = parseUnknownPositiveInteger(parsed.dimension, "embedding.dimension");
|
|
133
|
+
if (typeof parsed.apiKey === "string" && parsed.apiKey)
|
|
134
|
+
result.apiKey = parsed.apiKey;
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function parseLlmConnectionValue(value) {
|
|
138
|
+
if (value === "null" || value === "")
|
|
139
|
+
return undefined;
|
|
140
|
+
const parsed = parseJsonObject(value, "llm", {
|
|
141
|
+
endpoint: "http://localhost:11434/v1/chat/completions",
|
|
142
|
+
model: "llama3.2",
|
|
143
|
+
});
|
|
144
|
+
const result = {
|
|
145
|
+
endpoint: asRequiredString(parsed.endpoint, "llm", "endpoint"),
|
|
146
|
+
model: asRequiredString(parsed.model, "llm", "model"),
|
|
147
|
+
};
|
|
148
|
+
if (typeof parsed.provider === "string" && parsed.provider)
|
|
149
|
+
result.provider = parsed.provider;
|
|
150
|
+
if (parsed.temperature !== undefined)
|
|
151
|
+
result.temperature = parseUnknownNumber(parsed.temperature, "llm.temperature");
|
|
152
|
+
if (parsed.maxTokens !== undefined)
|
|
153
|
+
result.maxTokens = parseUnknownPositiveInteger(parsed.maxTokens, "llm.maxTokens");
|
|
154
|
+
if (typeof parsed.apiKey === "string" && parsed.apiKey)
|
|
155
|
+
result.apiKey = parsed.apiKey;
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
function parseJsonObject(value, key, example) {
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(value);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
|
|
165
|
+
` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`);
|
|
166
|
+
}
|
|
167
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
168
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
|
|
169
|
+
}
|
|
170
|
+
return parsed;
|
|
171
|
+
}
|
|
172
|
+
function asRequiredString(value, key, field) {
|
|
173
|
+
if (typeof value !== "string" || !value) {
|
|
174
|
+
throw new UsageError(`Invalid value for ${key}: "${field}" is a required string field`);
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function requireNonEmptyString(value, key) {
|
|
179
|
+
if (!value) {
|
|
180
|
+
throw new UsageError(`Invalid value for ${key}: expected a non-empty string`);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
function parseUnknownNumber(value, key) {
|
|
185
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
186
|
+
throw new UsageError(`Invalid value for ${key}: expected a number`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function parseUnknownPositiveInteger(value, key) {
|
|
191
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
192
|
+
throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|