akm-cli 0.0.21 → 0.0.23

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.
Files changed (46) hide show
  1. package/README.md +8 -5
  2. package/dist/asset-spec.js +91 -10
  3. package/dist/cli.js +172 -57
  4. package/dist/common.js +15 -2
  5. package/dist/config-cli.js +55 -6
  6. package/dist/config.js +118 -22
  7. package/dist/create-provider-registry.js +18 -0
  8. package/dist/db.js +156 -53
  9. package/dist/embedder.js +36 -18
  10. package/dist/errors.js +6 -0
  11. package/dist/file-context.js +18 -19
  12. package/dist/frontmatter.js +19 -3
  13. package/dist/indexer.js +126 -89
  14. package/dist/{stash-registry.js → installed-kits.js} +16 -24
  15. package/dist/kit-include.js +108 -0
  16. package/dist/local-search.js +429 -0
  17. package/dist/lockfile.js +47 -5
  18. package/dist/matchers.js +6 -0
  19. package/dist/metadata.js +20 -10
  20. package/dist/paths.js +4 -0
  21. package/dist/providers/skills-sh.js +3 -2
  22. package/dist/providers/static-index.js +4 -9
  23. package/dist/registry-build-index.js +356 -0
  24. package/dist/registry-factory.js +19 -0
  25. package/dist/registry-install.js +114 -109
  26. package/dist/registry-resolve.js +44 -9
  27. package/dist/registry-search.js +14 -9
  28. package/dist/renderers.js +23 -7
  29. package/dist/ripgrep-install.js +9 -4
  30. package/dist/self-update.js +31 -4
  31. package/dist/stash-add.js +75 -6
  32. package/dist/stash-clone.js +1 -1
  33. package/dist/stash-provider-factory.js +37 -0
  34. package/dist/stash-provider.js +1 -0
  35. package/dist/stash-providers/filesystem.js +42 -0
  36. package/dist/stash-providers/index.js +9 -0
  37. package/dist/stash-providers/openviking.js +337 -0
  38. package/dist/stash-resolve.js +4 -4
  39. package/dist/stash-search.js +70 -401
  40. package/dist/stash-show.js +24 -5
  41. package/dist/stash-source-manage.js +82 -0
  42. package/dist/stash-source.js +19 -11
  43. package/dist/walker.js +15 -10
  44. package/dist/warn.js +7 -0
  45. package/package.json +1 -1
  46. package/dist/provider-registry.js +0 -8
@@ -0,0 +1,82 @@
1
+ import path from "node:path";
2
+ import { loadConfig, saveConfig } from "./config";
3
+ import { UsageError } from "./errors";
4
+ import { resolveStashSources } from "./stash-source";
5
+ // ── Operations ──────────────────────────────────────────────────────────────
6
+ /**
7
+ * Add a stash source (filesystem path or remote provider URL) to config.
8
+ *
9
+ * Filesystem paths are auto-detected when `target` does not start with
10
+ * `http://` or `https://`. URL sources require a `providerType` option
11
+ * (e.g. "openviking").
12
+ */
13
+ export function addStashSource(opts) {
14
+ const { target, name, providerType, options: providerOptions } = opts;
15
+ const config = loadConfig();
16
+ const stashes = [...(config.stashes ?? [])];
17
+ const isUrl = target.startsWith("http://") || target.startsWith("https://");
18
+ let entry;
19
+ if (isUrl) {
20
+ if (!providerType) {
21
+ throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
22
+ }
23
+ // Deduplicate by URL
24
+ if (stashes.some((s) => s.url === target)) {
25
+ return { stashes, added: false, message: "Source URL already configured" };
26
+ }
27
+ entry = { type: providerType, url: target };
28
+ if (name)
29
+ entry.name = name;
30
+ if (providerOptions)
31
+ entry.options = providerOptions;
32
+ }
33
+ else {
34
+ // Filesystem path
35
+ const resolvedPath = path.resolve(target);
36
+ if (stashes.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
37
+ return { stashes, added: false, message: "Source path already configured" };
38
+ }
39
+ entry = { type: "filesystem", path: resolvedPath };
40
+ if (name)
41
+ entry.name = name;
42
+ }
43
+ stashes.push(entry);
44
+ saveConfig({ ...config, stashes });
45
+ return { stashes, added: true, entry };
46
+ }
47
+ /**
48
+ * Remove a stash source by URL, path, or name.
49
+ * Match priority: URL > path > name (most specific first).
50
+ */
51
+ export function removeStashSource(target) {
52
+ const config = loadConfig();
53
+ const stashes = [...(config.stashes ?? [])];
54
+ const isUrl = target.startsWith("http://") || target.startsWith("https://");
55
+ const resolvedPath = !isUrl ? path.resolve(target) : undefined;
56
+ // Try URL match first, then path, then name (most specific → least specific)
57
+ let idx = -1;
58
+ if (isUrl) {
59
+ idx = stashes.findIndex((s) => s.url === target);
60
+ }
61
+ if (idx === -1 && resolvedPath) {
62
+ idx = stashes.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
63
+ }
64
+ if (idx === -1) {
65
+ idx = stashes.findIndex((s) => s.name === target);
66
+ }
67
+ if (idx === -1) {
68
+ return { stashes, removed: false, message: "No matching source found" };
69
+ }
70
+ const removed = stashes.splice(idx, 1)[0];
71
+ saveConfig({ ...config, stashes });
72
+ return { stashes, removed: true, entry: removed };
73
+ }
74
+ /**
75
+ * List all stash sources (local filesystem + configured stashes).
76
+ */
77
+ export function listStashSources() {
78
+ const config = loadConfig();
79
+ const localSources = resolveStashSources();
80
+ const stashes = config.stashes ?? [];
81
+ return { localSources, stashes };
82
+ }
@@ -17,25 +17,33 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
17
17
  const stashDir = overrideStashDir ?? resolveStashDir();
18
18
  const config = existingConfig ?? loadConfig();
19
19
  const sources = [{ path: stashDir }];
20
- for (const dir of config.searchPaths) {
20
+ const seen = new Set([path.resolve(stashDir)]);
21
+ const addSource = (dir, registryId) => {
22
+ const resolved = path.resolve(dir);
23
+ if (seen.has(resolved))
24
+ return;
25
+ seen.add(resolved);
21
26
  if (isSuspiciousStashRoot(dir)) {
22
27
  warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
23
28
  }
24
29
  if (isValidDirectory(dir)) {
25
- sources.push({ path: dir });
30
+ sources.push({ path: resolved, ...(registryId ? { registryId } : {}) });
26
31
  }
32
+ };
33
+ // Legacy: searchPaths[]
34
+ for (const dir of config.searchPaths) {
35
+ addSource(dir);
27
36
  }
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
+ // Filesystem entries from stashes[]
38
+ for (const entry of config.stashes ?? []) {
39
+ if (entry.type === "filesystem" && entry.path && entry.enabled !== false) {
40
+ addSource(entry.path, entry.name);
37
41
  }
38
42
  }
43
+ // Installed kits (registry and local)
44
+ for (const entry of config.installed ?? []) {
45
+ addSource(entry.stashRoot, entry.id);
46
+ }
39
47
  return sources;
40
48
  }
41
49
  /**
package/dist/walker.js CHANGED
@@ -5,7 +5,6 @@
5
5
  * (stash.ts) and the indexer (indexer.ts) to walk type-specific asset
6
6
  * directories and group files by parent directory.
7
7
  */
8
- import { spawnSync } from "node:child_process";
9
8
  import fs from "node:fs";
10
9
  import path from "node:path";
11
10
  import { isRelevantAssetFile } from "./asset-spec";
@@ -77,17 +76,16 @@ function walkStashGit(stashRoot) {
77
76
  if (!isInsideGitRepo(stashRoot))
78
77
  return null;
79
78
  // Get tracked + untracked (non-ignored) files
80
- const result = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "."], {
79
+ const result = Bun.spawnSync(["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "."], {
81
80
  cwd: stashRoot,
82
- encoding: "utf8",
83
- timeout: 30_000,
84
- maxBuffer: 10 * 1024 * 1024, // 10MB
85
81
  });
86
- if (result.status !== 0)
82
+ // result.success is false if the process exited non-zero OR git was not found
83
+ if (!result.success)
87
84
  return null;
88
85
  const SKIP_DIRS = new Set([".git", "node_modules", "bin", ".cache"]);
89
86
  const SKIP_FILES = new Set([".stash.json", ".gitignore", ".gitattributes"]);
90
- const files = result.stdout
87
+ const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : String(result.stdout ?? "");
88
+ const files = stdout
91
89
  .split("\0")
92
90
  .filter((f) => f.length > 0)
93
91
  .filter((f) => !f.startsWith("..") && !path.isAbsolute(f))
@@ -98,8 +96,7 @@ function walkStashGit(stashRoot) {
98
96
  .filter(Boolean);
99
97
  return !dirParts.some((part) => SKIP_DIRS.has(part) || part.startsWith("."));
100
98
  })
101
- .filter((f) => !SKIP_FILES.has(path.basename(f)))
102
- .filter((f) => !f.includes("/.") && !f.startsWith(".")); // skip dot-dirs/files
99
+ .filter((f) => !SKIP_FILES.has(path.basename(f)));
103
100
  const results = [];
104
101
  for (const relFile of files) {
105
102
  const absPath = path.join(stashRoot, relFile);
@@ -114,7 +111,11 @@ function walkStashGit(stashRoot) {
114
111
  }
115
112
  return results;
116
113
  }
117
- /** Check if a directory is inside a git repository by walking up to find .git. */
114
+ /**
115
+ * Check if a directory is inside a git repository by walking up to find .git.
116
+ * Intentionally walks above stashRoot so that parent repo .gitignore rules
117
+ * apply when the stash is nested inside a larger git repository.
118
+ */
118
119
  function isInsideGitRepo(dir) {
119
120
  let current = path.resolve(dir);
120
121
  const root = path.parse(current).root;
@@ -149,6 +150,10 @@ function walkStashManual(stashRoot) {
149
150
  if (entry.name === ".stash.json")
150
151
  continue;
151
152
  const fullPath = path.join(current, entry.name);
153
+ if (entry.isSymbolicLink()) {
154
+ // Skip symlinks entirely to prevent potential path traversal outside stashRoot
155
+ continue;
156
+ }
152
157
  if (entry.isDirectory()) {
153
158
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
154
159
  continue;
package/dist/warn.js CHANGED
@@ -6,6 +6,13 @@ let quiet = false;
6
6
  export function setQuiet(value) {
7
7
  quiet = value;
8
8
  }
9
+ /**
10
+ * Reset the quiet flag to false.
11
+ * Intended for test teardown to prevent quiet state from leaking between tests.
12
+ */
13
+ export function resetQuiet() {
14
+ quiet = false;
15
+ }
9
16
  export function isQuiet() {
10
17
  return quiet;
11
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "description": "CLI tool to search, open, and run extension assets from an akm stash directory.",
6
6
  "keywords": [
@@ -1,8 +0,0 @@
1
- // ── Factory map ─────────────────────────────────────────────────────────────
2
- const providers = new Map();
3
- export function registerProvider(type, factory) {
4
- providers.set(type, factory);
5
- }
6
- export function resolveProviderFactory(type) {
7
- return providers.get(type) ?? null;
8
- }