akm-cli 0.0.0 → 0.0.17
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 +249 -6
- package/dist/asset-spec.js +70 -0
- package/dist/cli.js +934 -0
- package/dist/common.js +192 -0
- package/dist/config-cli.js +233 -0
- package/dist/config.js +338 -0
- package/dist/db.js +371 -0
- package/dist/embedder.js +150 -0
- package/dist/errors.js +28 -0
- package/dist/file-context.js +162 -0
- package/dist/frontmatter.js +86 -0
- package/dist/github.js +17 -0
- package/dist/indexer.js +311 -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 +159 -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 +365 -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 +73 -0
- package/dist/stash-registry.js +206 -0
- package/dist/stash-resolve.js +55 -0
- package/dist/stash-search.js +490 -0
- package/dist/stash-show.js +58 -0
- package/dist/stash-source.js +130 -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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveStashDir } from "./common";
|
|
4
|
+
import { loadConfig } from "./config";
|
|
5
|
+
import { warn } from "./warn";
|
|
6
|
+
// ── Resolution ──────────────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Build the ordered list of stash sources (search paths):
|
|
9
|
+
* 1. Primary stash dir (user's own, destination for clone)
|
|
10
|
+
* 2. Additional search paths (user-configured)
|
|
11
|
+
* 3. Installed kit paths (cache-managed, from registry)
|
|
12
|
+
*
|
|
13
|
+
* The first entry is always the primary stash. Additional entries come
|
|
14
|
+
* from `searchPaths` config and `installed` kit entries.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveStashSources(overrideStashDir, existingConfig) {
|
|
17
|
+
const stashDir = overrideStashDir ?? resolveStashDir();
|
|
18
|
+
const config = existingConfig ?? loadConfig();
|
|
19
|
+
const sources = [{ path: stashDir }];
|
|
20
|
+
for (const dir of config.searchPaths) {
|
|
21
|
+
if (isSuspiciousStashRoot(dir)) {
|
|
22
|
+
warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
|
|
23
|
+
}
|
|
24
|
+
if (isValidDirectory(dir)) {
|
|
25
|
+
sources.push({ path: dir });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const entry of config.installed ?? []) {
|
|
29
|
+
if (isSuspiciousStashRoot(entry.stashRoot)) {
|
|
30
|
+
warn(`Warning: stash root "${entry.stashRoot}" appears to be a system directory. This may be unintentional.`);
|
|
31
|
+
}
|
|
32
|
+
if (isValidDirectory(entry.stashRoot)) {
|
|
33
|
+
sources.push({
|
|
34
|
+
path: entry.stashRoot,
|
|
35
|
+
registryId: entry.id,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return sources;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convenience: returns just the directory paths, preserving priority order.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveAllStashDirs(overrideStashDir) {
|
|
45
|
+
return resolveStashSources(overrideStashDir).map((s) => s.path);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Find which source a file path belongs to.
|
|
49
|
+
*/
|
|
50
|
+
export function findSourceForPath(filePath, sources) {
|
|
51
|
+
const resolved = path.resolve(filePath);
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
if (resolved.startsWith(path.resolve(source.path) + path.sep))
|
|
54
|
+
return source;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Return the primary stash source (first entry in the list).
|
|
60
|
+
* This is the user's working stash and the default destination for clone.
|
|
61
|
+
*/
|
|
62
|
+
export function getPrimarySource(sources) {
|
|
63
|
+
return sources[0];
|
|
64
|
+
}
|
|
65
|
+
// ── Editability ─────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Determine whether a file is safe to edit in place.
|
|
68
|
+
*
|
|
69
|
+
* The only files that are NOT editable are those inside a cache directory
|
|
70
|
+
* managed by the package manager (`installed[].cacheDir`). These
|
|
71
|
+
* will be overwritten by `akm update` without warning.
|
|
72
|
+
*
|
|
73
|
+
* Everything else — working stash, search paths, local project dirs — is
|
|
74
|
+
* the user's domain to manage.
|
|
75
|
+
*/
|
|
76
|
+
export function isEditable(filePath, config) {
|
|
77
|
+
const cfg = config ?? loadConfig();
|
|
78
|
+
const resolved = path.resolve(filePath);
|
|
79
|
+
const cacheManaged = cfg.installed ?? [];
|
|
80
|
+
const isWin = process.platform === "win32";
|
|
81
|
+
for (const entry of cacheManaged) {
|
|
82
|
+
// Local sources reference original paths — always editable
|
|
83
|
+
if (entry.source === "local")
|
|
84
|
+
continue;
|
|
85
|
+
const cacheRoot = path.resolve(entry.cacheDir);
|
|
86
|
+
if (isWin) {
|
|
87
|
+
// Windows paths are case-insensitive — normalize both sides
|
|
88
|
+
if (resolved.toLowerCase().startsWith(cacheRoot.toLowerCase() + path.sep))
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
if (resolved.startsWith(cacheRoot + path.sep))
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build an actionable hint for the agent when a file is not editable.
|
|
100
|
+
* Callers must check `isEditable()` before calling — this function
|
|
101
|
+
* unconditionally returns the hint string.
|
|
102
|
+
*/
|
|
103
|
+
export function buildEditHint(_filePath, assetType, assetName, origin) {
|
|
104
|
+
const ref = origin ? `${origin}//${assetType}:${assetName}` : `${assetType}:${assetName}`;
|
|
105
|
+
return `This asset is managed by akm and may be overwritten on update. To edit, run: akm clone ${ref}`;
|
|
106
|
+
}
|
|
107
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
108
|
+
const SUSPICIOUS_ROOTS = new Set(["/", "/etc", "/bin", "/sbin", "/usr", "/var", "/tmp", "/dev", "/proc", "/sys"]);
|
|
109
|
+
function isSuspiciousStashRoot(dir) {
|
|
110
|
+
const resolved = path.resolve(dir);
|
|
111
|
+
const normalized = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
112
|
+
if (SUSPICIOUS_ROOTS.has(normalized))
|
|
113
|
+
return true;
|
|
114
|
+
if (process.platform === "win32") {
|
|
115
|
+
// Check for Windows system directories
|
|
116
|
+
const winDir = (process.env.SystemRoot || "C:\\Windows").toLowerCase();
|
|
117
|
+
if (normalized === winDir || normalized.startsWith(winDir + path.sep))
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
123
|
+
function isValidDirectory(dir) {
|
|
124
|
+
try {
|
|
125
|
+
return fs.statSync(dir).isDirectory();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/walker.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filesystem walker for akm stash directories.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single implementation used by both the search fallback
|
|
5
|
+
* (stash.ts) and the indexer (indexer.ts) to walk type-specific asset
|
|
6
|
+
* directories and group files by parent directory.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { isRelevantAssetFile } from "./asset-spec";
|
|
12
|
+
import { buildFileContext } from "./file-context";
|
|
13
|
+
/**
|
|
14
|
+
* Walk a type root directory and return files grouped by their parent directory.
|
|
15
|
+
*
|
|
16
|
+
* Only files relevant to the given `assetType` are included (e.g. `.md` for
|
|
17
|
+
* commands, script extensions for scripts, `SKILL.md` for skills).
|
|
18
|
+
*/
|
|
19
|
+
export function walkStash(typeRoot, assetType) {
|
|
20
|
+
if (!fs.existsSync(typeRoot))
|
|
21
|
+
return [];
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
const stack = [typeRoot];
|
|
24
|
+
while (stack.length > 0) {
|
|
25
|
+
const current = stack.pop();
|
|
26
|
+
if (!current)
|
|
27
|
+
continue;
|
|
28
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.name === ".stash.json")
|
|
31
|
+
continue;
|
|
32
|
+
const fullPath = path.join(current, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
stack.push(fullPath);
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile() && isRelevantAssetFile(assetType, entry.name)) {
|
|
37
|
+
const parentDir = path.dirname(fullPath);
|
|
38
|
+
const existing = groups.get(parentDir);
|
|
39
|
+
if (existing) {
|
|
40
|
+
existing.push(fullPath);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
groups.set(parentDir, [fullPath]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Array.from(groups, ([dirPath, files]) => ({ dirPath, files }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Walk an entire stash root directory and return FileContext objects for every
|
|
52
|
+
* regular file found.
|
|
53
|
+
*
|
|
54
|
+
* Unlike walkStash(), this does NOT filter by asset type or require files to
|
|
55
|
+
* live under type-specific directories. Matchers decide what each file is.
|
|
56
|
+
*
|
|
57
|
+
* If the directory is a git repo, uses `git ls-files` to respect .gitignore.
|
|
58
|
+
* Otherwise falls back to a manual walk that skips .git, node_modules, bin,
|
|
59
|
+
* .cache, dot-directories, and .stash.json files.
|
|
60
|
+
*/
|
|
61
|
+
export function walkStashFlat(stashRoot) {
|
|
62
|
+
if (!fs.existsSync(stashRoot))
|
|
63
|
+
return [];
|
|
64
|
+
// Try git-based walk first (respects .gitignore)
|
|
65
|
+
const gitResult = walkStashGit(stashRoot);
|
|
66
|
+
if (gitResult)
|
|
67
|
+
return gitResult;
|
|
68
|
+
// Fallback: manual walk
|
|
69
|
+
return walkStashManual(stashRoot);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Walk using `git ls-files` to respect .gitignore.
|
|
73
|
+
* Returns null if the directory is not a git repo or git fails.
|
|
74
|
+
*/
|
|
75
|
+
function walkStashGit(stashRoot) {
|
|
76
|
+
// Quick check: is this a git repo? Look for .git in this dir or parents.
|
|
77
|
+
if (!isInsideGitRepo(stashRoot))
|
|
78
|
+
return null;
|
|
79
|
+
// Get tracked + untracked (non-ignored) files
|
|
80
|
+
const result = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "."], {
|
|
81
|
+
cwd: stashRoot,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout: 30_000,
|
|
84
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
85
|
+
});
|
|
86
|
+
if (result.status !== 0)
|
|
87
|
+
return null;
|
|
88
|
+
const SKIP_DIRS = new Set([".git", "node_modules", "bin", ".cache"]);
|
|
89
|
+
const SKIP_FILES = new Set([".stash.json", ".gitignore", ".gitattributes"]);
|
|
90
|
+
const files = result.stdout
|
|
91
|
+
.split("\0")
|
|
92
|
+
.filter((f) => f.length > 0)
|
|
93
|
+
.filter((f) => !f.startsWith("..") && !path.isAbsolute(f))
|
|
94
|
+
.filter((f) => {
|
|
95
|
+
const dirParts = path
|
|
96
|
+
.dirname(f)
|
|
97
|
+
.split(/[\\/]+/)
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
return !dirParts.some((part) => SKIP_DIRS.has(part) || part.startsWith("."));
|
|
100
|
+
})
|
|
101
|
+
.filter((f) => !SKIP_FILES.has(path.basename(f)))
|
|
102
|
+
.filter((f) => !f.includes("/.") && !f.startsWith(".")); // skip dot-dirs/files
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const relFile of files) {
|
|
105
|
+
const absPath = path.join(stashRoot, relFile);
|
|
106
|
+
try {
|
|
107
|
+
if (fs.statSync(absPath).isFile()) {
|
|
108
|
+
results.push(buildFileContext(stashRoot, absPath));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// File may have been deleted since git ls-files ran
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
/** Check if a directory is inside a git repository by walking up to find .git. */
|
|
118
|
+
function isInsideGitRepo(dir) {
|
|
119
|
+
let current = path.resolve(dir);
|
|
120
|
+
const root = path.parse(current).root;
|
|
121
|
+
while (current !== root) {
|
|
122
|
+
try {
|
|
123
|
+
const gitDir = path.join(current, ".git");
|
|
124
|
+
const stat = fs.statSync(gitDir);
|
|
125
|
+
if (stat.isDirectory() || stat.isFile())
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// .git doesn't exist at this level, keep climbing
|
|
130
|
+
}
|
|
131
|
+
const parent = path.dirname(current);
|
|
132
|
+
if (parent === current)
|
|
133
|
+
break;
|
|
134
|
+
current = parent;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
/** Manual walk for non-git directories. */
|
|
139
|
+
function walkStashManual(stashRoot) {
|
|
140
|
+
const results = [];
|
|
141
|
+
const SKIP_DIRS = new Set([".git", "node_modules", "bin", ".cache"]);
|
|
142
|
+
const stack = [stashRoot];
|
|
143
|
+
while (stack.length > 0) {
|
|
144
|
+
const current = stack.pop();
|
|
145
|
+
if (!current)
|
|
146
|
+
continue;
|
|
147
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (entry.name === ".stash.json")
|
|
150
|
+
continue;
|
|
151
|
+
const fullPath = path.join(current, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
|
|
154
|
+
continue;
|
|
155
|
+
stack.push(fullPath);
|
|
156
|
+
}
|
|
157
|
+
else if (entry.isFile()) {
|
|
158
|
+
results.push(buildFileContext(stashRoot, fullPath));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
package/dist/warn.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level quiet flag for suppressing stderr warnings.
|
|
3
|
+
* Controlled by the CLI --quiet / -q flag.
|
|
4
|
+
*/
|
|
5
|
+
let quiet = false;
|
|
6
|
+
export function setQuiet(value) {
|
|
7
|
+
quiet = value;
|
|
8
|
+
}
|
|
9
|
+
export function isQuiet() {
|
|
10
|
+
return quiet;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Emit a warning to stderr unless --quiet is active.
|
|
14
|
+
* Drop-in replacement for console.warn() across the codebase.
|
|
15
|
+
*/
|
|
16
|
+
export function warn(...args) {
|
|
17
|
+
if (!quiet) {
|
|
18
|
+
console.warn(...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"keywords": [
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "0.0.17",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool to search, open, and run extension assets from an akm stash directory.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"akm",
|
|
8
|
+
"agent-i-kit",
|
|
9
|
+
"ai-agent",
|
|
10
|
+
"agent-framework",
|
|
11
|
+
"developer-tools",
|
|
12
|
+
"cli",
|
|
13
|
+
"tools",
|
|
14
|
+
"skills",
|
|
15
|
+
"commands"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/itlackey/agentikit#readme",
|
|
9
18
|
"repository": {
|
|
10
19
|
"type": "git",
|
|
11
20
|
"url": "git+https://github.com/itlackey/agentikit.git"
|
|
12
21
|
},
|
|
13
|
-
"homepage": "https://github.com/itlackey/agentikit#readme",
|
|
14
22
|
"bugs": {
|
|
15
23
|
"url": "https://github.com/itlackey/agentikit/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MPL-2.0",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"bin": {
|
|
32
|
+
"akm": "dist/cli.js"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "rm -rf dist && bun run tsc --project ./tsconfig.json --outDir dist",
|
|
36
|
+
"check": "bun run lint && bunx tsc --noEmit && bun test ./tests",
|
|
37
|
+
"check:changed": "bun test tests/output-baseline.test.ts tests/e2e.test.ts tests/stash-search.test.ts && bun run lint && bunx tsc --noEmit",
|
|
38
|
+
"test": "bun test ./tests",
|
|
39
|
+
"lint": "bunx biome check src/ tests/",
|
|
40
|
+
"lint:fix": "bunx biome check --write src/ tests/",
|
|
41
|
+
"format": "bunx biome format --write src/ tests/",
|
|
42
|
+
"prepublishOnly": "bun run build"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "^2.4.6",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"bun-types": "^1.3.10",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"@xenova/transformers": "^2.17.0",
|
|
55
|
+
"sqlite-vec": "0.1.7-alpha.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"bun": ">=1.0.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"citty": "^0.2.1"
|
|
16
62
|
}
|
|
17
63
|
}
|
package/index.js
DELETED