akm-cli 0.7.4 → 0.8.0-rc.10

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 (300) hide show
  1. package/CHANGELOG.md +224 -1
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -1,18 +1,6 @@
1
- /**
2
- * Composable runner abstraction for `akm setup`.
3
- *
4
- * The interactive wizard in `setup.ts` historically ran a fixed series of
5
- * step functions (`stepStashDir`, `stepOllama`, `stepLlm`, ...) inline.
6
- * This module formalizes that pattern so steps can be:
7
- * - reused by `akm init` (non-interactive preset, see Finding 31),
8
- * - tested in isolation by passing a stub `SetupContext`, and
9
- * - extended by plugins without touching the wizard call site.
10
- *
11
- * Steps mutate state through `SetupContext.apply()`, which accumulates a
12
- * delta on top of the original config. `stepLlm` reading the embedding
13
- * endpoint that `stepSemanticSearch` produced is the canonical example of
14
- * why mutable accumulation is preferred over immutable returns.
15
- */
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/.
16
4
  /**
17
5
  * Build a fresh `SetupContext` over a starting config. The returned context
18
6
  * applies deltas in-place onto an internal accumulator and exposes the
@@ -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 { isWithin } from "../core/common";
@@ -1,14 +1,7 @@
1
- /**
2
- * Source provider factory map.
3
- *
4
- * Maps source kind identifiers (e.g. "filesystem", "git", "website", "npm")
5
- * to factory functions that build {@link SourceProvider} instances from a
6
- * {@link SourceConfigEntry}.
7
- *
8
- * Distinct from the registry-discovery factory (`registry/factory.ts`).
9
- * Both share `create-provider-registry.ts` for the underlying string→factory
10
- * map.
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/.
4
+ import { getSources } from "../core/config";
12
5
  import { createProviderRegistry } from "../registry/create-provider-registry";
13
6
  // ── Factory map ─────────────────────────────────────────────────────────────
14
7
  const registry = createProviderRegistry();
@@ -24,7 +17,7 @@ export function resolveSourceProviderFactory(type) {
24
17
  */
25
18
  export function resolveSourceProviders(config) {
26
19
  const providers = [];
27
- for (const entry of config.sources ?? config.stashes ?? []) {
20
+ for (const entry of getSources(config)) {
28
21
  if (entry.enabled === false)
29
22
  continue;
30
23
  const factory = registry.resolve(entry.type);
@@ -1,21 +1,4 @@
1
- /**
2
- * SourceProvider minimal v1 interface (spec §2.1).
3
- *
4
- * A SourceProvider gets files into a directory. The indexer walks `path()`
5
- * and reads files from disk. Search and show go through the indexer, not
6
- * through provider methods.
7
- *
8
- * Three required members + one optional:
9
- * - name configured source name
10
- * - kind "filesystem" | "git" | "website" | "npm"
11
- * - init(ctx) called once after construction
12
- * - path() the directory the indexer walks (stable for instance lifetime)
13
- * - sync?() refresh the directory from upstream (no-op for filesystem)
14
- *
15
- * All other writing/reading concerns live outside this interface:
16
- * - Writes: src/core/write-source.ts (Phase 5)
17
- * - Reads: src/indexer.ts (Phase 4)
18
- * - Install: src/sources/providers/sync-from-ref.ts (install-time helpers,
19
- * separate from configured-source plumbing)
20
- */
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/.
21
4
  export {};
@@ -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 { resolveStashDir } from "../../core/common";
2
5
  import { ConfigError } from "../../core/errors";
3
6
  import { registerSourceProvider } from "../provider-factory";
@@ -8,28 +11,21 @@ import { registerSourceProvider } from "../provider-factory";
8
11
  * just `{ name, kind, init, path }`. No `sync()` — content is the user's
9
12
  * own directory, never refreshed by akm.
10
13
  */
11
- class FilesystemSourceProvider {
12
- kind = "filesystem";
13
- name;
14
- #stashDir;
15
- constructor(entry) {
16
- if (entry.type !== "filesystem") {
17
- throw new ConfigError(`FilesystemSourceProvider invoked with type="${entry.type}"`);
18
- }
19
- this.#stashDir = entry.path ?? resolveStashDir();
20
- if (!this.#stashDir) {
21
- throw new ConfigError("filesystem source requires a `path`");
22
- }
23
- this.name = entry.name ?? this.#stashDir;
14
+ registerSourceProvider("filesystem", (entry) => {
15
+ if (entry.type !== "filesystem") {
16
+ throw new ConfigError(`filesystem source invoked with type="${entry.type}"`);
24
17
  }
25
- async init(_ctx) {
26
- // Filesystem sources resolve their path eagerly in the constructor;
27
- // init has nothing to do beyond letting the registry know we're ready.
18
+ const stashDir = entry.path ?? resolveStashDir();
19
+ if (!stashDir) {
20
+ throw new ConfigError("filesystem source requires a `path`");
28
21
  }
29
- path() {
30
- return this.#stashDir;
31
- }
32
- }
33
- // ── Self-register ───────────────────────────────────────────────────────────
34
- registerSourceProvider("filesystem", (config) => new FilesystemSourceProvider(config));
35
- export { FilesystemSourceProvider };
22
+ const name = entry.name ?? stashDir;
23
+ return {
24
+ kind: "filesystem",
25
+ name,
26
+ async init(_ctx) { },
27
+ path() {
28
+ return stashDir;
29
+ },
30
+ };
31
+ });
@@ -1,10 +1,13 @@
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 { spawnSync } from "node:child_process";
2
5
  import { createHash, randomBytes } from "node:crypto";
3
6
  import fs from "node:fs";
4
7
  import path from "node:path";
5
8
  import { TYPE_DIRS } from "../../core/asset-spec";
6
9
  import { resolveStashDir } from "../../core/common";
7
- import { loadConfig } from "../../core/config";
10
+ import { getSources, loadConfig } from "../../core/config";
8
11
  import { ConfigError, UsageError } from "../../core/errors";
9
12
  import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
10
13
  import { sanitizeCommitMessage } from "../../core/write-source";
@@ -15,7 +18,13 @@ import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, det
15
18
  const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
16
19
  /** Maximum stale age allowed when refresh fails (7 days). */
17
20
  const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
18
- const GIT_STASH_TYPES = new Set(["git"]);
21
+ function runGit(args, options) {
22
+ return spawnSync("git", args, {
23
+ encoding: "utf8",
24
+ ...options,
25
+ env: { ...process.env, ...options?.env, GIT_TERMINAL_PROMPT: "0" },
26
+ });
27
+ }
19
28
  /**
20
29
  * Git source provider — clones (and re-pulls) a remote repo into a local
21
30
  * cache directory. Implements the v1 {@link SourceProvider} interface (spec
@@ -219,10 +228,9 @@ async function doSyncGit(parsed, options) {
219
228
  cloneArgs.push("--branch", parsed.requestedRef);
220
229
  }
221
230
  cloneArgs.push(parsed.url, cloneDir);
222
- const cloneResult = spawnSync("git", cloneArgs, { encoding: "utf8", timeout: 120_000 });
231
+ const cloneResult = runGit(cloneArgs, { timeout: 120_000 });
223
232
  if (cloneResult.status !== 0) {
224
- const err = cloneResult.stderr?.trim() || cloneResult.error?.message || "unknown error";
225
- throw new Error(`Failed to clone ${parsed.url}: ${err}`);
233
+ throw new Error(classifyCloneFailure(parsed.url, cloneResult.stderr, cloneResult.error));
226
234
  }
227
235
  // Copy contents to extracted dir without .git
228
236
  fs.mkdirSync(extractedDir, { recursive: true });
@@ -267,12 +275,11 @@ export function cloneRepo(cloneUrl, ref, destDir, writable = false) {
267
275
  if (ref)
268
276
  args.push("--branch", ref);
269
277
  args.push(cloneUrl, tmpDir);
270
- const result = spawnSync("git", args, { encoding: "utf8", timeout: 120_000 });
278
+ const result = runGit(args, { timeout: 120_000 });
271
279
  if (result.status !== 0) {
272
280
  // Clean up the (possibly partial) temp dir but leave destDir untouched.
273
281
  fs.rmSync(tmpDir, { recursive: true, force: true });
274
- const err = result.stderr?.trim() || result.error?.message || "unknown error";
275
- throw new Error(`Failed to clone ${cloneUrl}: ${err}`);
282
+ throw new Error(classifyCloneFailure(cloneUrl, result.stderr, result.error));
276
283
  }
277
284
  try {
278
285
  if (!writable) {
@@ -293,8 +300,7 @@ export function cloneRepo(cloneUrl, ref, destDir, writable = false) {
293
300
  }
294
301
  }
295
302
  function pullRepo(repoDir) {
296
- const result = spawnSync("git", ["-C", repoDir, "pull", "--ff-only"], {
297
- encoding: "utf8",
303
+ const result = runGit(["-C", repoDir, "pull", "--ff-only"], {
298
304
  timeout: 120_000,
299
305
  });
300
306
  if (result.status !== 0) {
@@ -395,7 +401,10 @@ function parseGitRepoUrl(rawUrl) {
395
401
  * When `name` is omitted the primary stash directory is used.
396
402
  * When `message` is omitted a timestamp is used.
397
403
  */
398
- export function saveGitStash(name, message, writableOverride) {
404
+ export function saveGitStash(name, message, writableOverride, options) {
405
+ // `push: false` (from `akm sync --no-push`) commits but never pushes, even
406
+ // when the stash is writable with a remote configured.
407
+ const allowPush = options?.push !== false;
399
408
  const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
400
409
  // Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
401
410
  // clamp length. An attacker can otherwise pass `--message "subject\n\n\
@@ -407,10 +416,10 @@ export function saveGitStash(name, message, writableOverride) {
407
416
  let writable = false;
408
417
  if (name) {
409
418
  const config = loadConfig();
410
- const stash = (config.sources ?? config.stashes ?? []).find((s) => s.name === name || s.url === name);
419
+ const stash = findGitStashByTarget(getSources(config), name);
411
420
  if (!stash)
412
421
  throw new UsageError(`No git stash found with name "${name}"`);
413
- if (!GIT_STASH_TYPES.has(stash.type)) {
422
+ if (stash.type !== "git") {
414
423
  throw new UsageError(`Stash "${name}" is not a git stash (type: ${stash.type})`);
415
424
  }
416
425
  if (!stash.url)
@@ -431,33 +440,59 @@ export function saveGitStash(name, message, writableOverride) {
431
440
  return { committed: false, pushed: false, skipped: true, reason: "not a git repository", output: "" };
432
441
  }
433
442
  // Nothing to commit?
434
- const statusResult = spawnSync("git", ["-C", repoDir, "status", "--porcelain"], { encoding: "utf8" });
443
+ const statusResult = runGit(["-C", repoDir, "status", "--porcelain"]);
435
444
  if (statusResult.error || statusResult.status !== 0) {
436
445
  throw new Error(`git status failed: ${statusResult.error?.message || statusResult.stderr?.trim() || "unknown error"}`);
437
446
  }
438
447
  if (!statusResult.stdout.trim()) {
439
448
  return { committed: false, pushed: false, skipped: false, output: "nothing to commit, working tree clean" };
440
449
  }
450
+ // Safety check (#476): when the stash dir is shared with a non-akm project
451
+ // (stash root == project repo root), `git add -A` would stage every dirty
452
+ // file in the user's working tree and push their unrelated WIP to the
453
+ // stash's remote. Refuse if any dirty path is outside the known akm-
454
+ // managed subtrees (TYPE_DIRS + `.akm/` state).
455
+ const nonAkmDirty = collectNonAkmDirtyPaths(statusResult.stdout);
456
+ if (nonAkmDirty.length > 0) {
457
+ const sample = nonAkmDirty.slice(0, 10);
458
+ const more = nonAkmDirty.length > sample.length ? `\n ...and ${nonAkmDirty.length - sample.length} more` : "";
459
+ throw new Error(`refusing to push: stash repo at ${repoDir} has uncommitted non-akm changes:\n` +
460
+ sample.map((p) => ` ${p}`).join("\n") +
461
+ more +
462
+ `\nCommit or stash these manually before running an akm push. ` +
463
+ `Akm-managed paths are: ${Object.values(TYPE_DIRS).join(", ")}, .akm/`);
464
+ }
441
465
  // Stage and commit — supply fallback identity so fresh environments without
442
466
  // user.name/user.email configured can always commit to the default stash.
443
- const addResult = spawnSync("git", ["-C", repoDir, "add", "-A"], { encoding: "utf8" });
467
+ // `add -A` is safe here because nonAkmDirty was just verified empty.
468
+ const addResult = runGit(["-C", repoDir, "add", "-A"]);
444
469
  if (addResult.status !== 0) {
445
470
  throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
446
471
  }
447
- const commitResult = spawnSync("git", ["-C", repoDir, "-c", "user.name=akm", "-c", "user.email=akm@local", "commit", "-m", commitMessage], { encoding: "utf8" });
472
+ const commitResult = runGit([
473
+ "-C",
474
+ repoDir,
475
+ "-c",
476
+ "user.name=akm",
477
+ "-c",
478
+ "user.email=akm@local",
479
+ "commit",
480
+ "-m",
481
+ commitMessage,
482
+ ]);
448
483
  if (commitResult.status !== 0) {
449
484
  throw new Error(`git commit failed: ${commitResult.stderr?.trim() || "unknown error"}`);
450
485
  }
451
486
  // Push only when there is a remote AND the stash is marked writable
452
- const remoteResult = spawnSync("git", ["-C", repoDir, "remote"], { encoding: "utf8" });
487
+ const remoteResult = runGit(["-C", repoDir, "remote"]);
453
488
  if (remoteResult.status !== 0) {
454
489
  throw new Error(`git remote failed: ${remoteResult.stderr?.trim() || "unknown error"}`);
455
490
  }
456
491
  const hasRemote = remoteResult.stdout.trim().length > 0;
457
- if (!hasRemote || !writable) {
492
+ if (!hasRemote || !writable || !allowPush) {
458
493
  return { committed: true, pushed: false, skipped: false, output: commitResult.stdout.trim() };
459
494
  }
460
- const pushResult = spawnSync("git", ["-C", repoDir, "push"], { encoding: "utf8", timeout: 120_000 });
495
+ const pushResult = runGit(["-C", repoDir, "push"], { timeout: 120_000 });
461
496
  if (pushResult.status !== 0) {
462
497
  throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
463
498
  }
@@ -468,5 +503,129 @@ export function saveGitStash(name, message, writableOverride) {
468
503
  output: (commitResult.stdout + pushResult.stdout).trim() || "changes committed and pushed",
469
504
  };
470
505
  }
506
+ function findGitStashByTarget(stashes, target) {
507
+ return stashes.find((stash) => matchesGitStashTarget(stash, target));
508
+ }
509
+ function matchesGitStashTarget(stash, target) {
510
+ if (stash.type !== "git")
511
+ return false;
512
+ if (stash.name === target || stash.url === target)
513
+ return true;
514
+ if (!stash.url)
515
+ return false;
516
+ try {
517
+ const repo = parseGitRepoUrl(stash.url);
518
+ if (repo.canonicalUrl === target)
519
+ return true;
520
+ return buildGithubTargetAliases(repo.canonicalUrl).has(target);
521
+ }
522
+ catch {
523
+ return false;
524
+ }
525
+ }
526
+ function buildGithubTargetAliases(canonicalUrl) {
527
+ try {
528
+ const parsed = new URL(canonicalUrl);
529
+ if (parsed.hostname !== "github.com")
530
+ return new Set();
531
+ const segments = parsed.pathname.split("/").filter(Boolean);
532
+ if (segments.length < 2)
533
+ return new Set();
534
+ const owner = segments[0];
535
+ const repo = segments[1];
536
+ const aliases = new Set([`${owner}/${repo}`, `github:${owner}/${repo}`]);
537
+ if (segments[2] === "tree" && segments.length >= 4) {
538
+ const ref = segments.slice(3).join("/");
539
+ aliases.add(`${owner}/${repo}#${ref}`);
540
+ aliases.add(`github:${owner}/${repo}#${ref}`);
541
+ }
542
+ return aliases;
543
+ }
544
+ catch {
545
+ return new Set();
546
+ }
547
+ }
548
+ // ── Clone-failure classification (#487) ─────────────────────────────────────
549
+ /**
550
+ * Translate git's stderr into an actionable message. Without this, a user
551
+ * who passes a nonexistent or private repo to `akm add` sees:
552
+ *
553
+ * "could not read Username for 'https://github.com': No such device or
554
+ * address"
555
+ *
556
+ * That is git falling through to its auth-prompt path — the actual cause
557
+ * is "repo doesn't exist (or is private)". We classify the common patterns
558
+ * and emit a message that names the cause and the fix.
559
+ */
560
+ export function classifyCloneFailure(url, stderr, spawnError) {
561
+ const raw = (stderr ?? "").trim();
562
+ const spawnMsg = spawnError?.message ?? "";
563
+ // `git` binary not on PATH.
564
+ if (spawnError?.code === "ENOENT") {
565
+ return `Failed to clone ${url}: 'git' is not installed or not on PATH. Install git, then re-run.`;
566
+ }
567
+ // Auth-prompt fall-through (the headline #487 case).
568
+ if (/could not read Username|terminal prompts disabled|Authentication failed|fatal: Authentication/i.test(raw)) {
569
+ return (`Failed to clone ${url}: repository not found or private. ` +
570
+ `If the repository is public, double-check the URL and try again. ` +
571
+ `If it is private, set GH_TOKEN (or configure a git credential helper) before re-running.`);
572
+ }
573
+ // 404-style messages from git http.
574
+ if (/repository '.*' not found|HTTP 404|fatal: remote error|not found:|Not Found/i.test(raw)) {
575
+ return (`Failed to clone ${url}: repository not found. ` +
576
+ `Check the URL — for GitHub, the form is 'owner/repo' or 'github:owner/repo'.`);
577
+ }
578
+ // SSH connection issues.
579
+ if (/Permission denied \(publickey\)|kex_exchange_identification|Connection refused|Connection timed out/i.test(raw)) {
580
+ return (`Failed to clone ${url}: network or SSH failure. ` +
581
+ `Check connectivity, your SSH agent, and the remote host's availability.`);
582
+ }
583
+ // Branch / ref-specific failures.
584
+ if (/Remote branch .* not found in upstream origin|couldn't find remote ref/i.test(raw)) {
585
+ return (`Failed to clone ${url}: the requested branch/tag does not exist on the remote. ` +
586
+ `Verify the ref name and re-run.`);
587
+ }
588
+ const detail = raw || spawnMsg || "unknown error";
589
+ return `Failed to clone ${url}: ${detail}`;
590
+ }
591
+ // ── Stash-safety helpers (#476) ──────────────────────────────────────────────
592
+ /**
593
+ * Inspect `git status --porcelain` output and return every dirty path that is
594
+ * NOT inside an akm-managed subtree. Used by `runUpstreamPush` to refuse
595
+ * pushing unrelated WIP when a writable stash shares its root with a project
596
+ * repo.
597
+ *
598
+ * Porcelain v1 format: `XY <path>` or `XY <orig> -> <new>` for renames. We
599
+ * key off the post-rename path (or the only path) — that is the working-tree
600
+ * file at risk of being staged by `git add -A`.
601
+ */
602
+ function collectNonAkmDirtyPaths(porcelainOutput) {
603
+ const akmDirs = new Set(Object.values(TYPE_DIRS));
604
+ const result = [];
605
+ for (const rawLine of porcelainOutput.split("\n")) {
606
+ const line = rawLine.replace(/\r$/, "");
607
+ if (line.length === 0)
608
+ continue;
609
+ // Skip the 2-char status code + 1 space.
610
+ let p = line.length > 3 ? line.slice(3) : "";
611
+ // Renames / copies: `from -> to`. Stage decision applies to `to`.
612
+ const arrow = p.lastIndexOf(" -> ");
613
+ if (arrow !== -1) {
614
+ p = p.slice(arrow + 4);
615
+ }
616
+ // Strip surrounding quotes for paths with special chars.
617
+ if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
618
+ p = p.slice(1, -1);
619
+ }
620
+ if (!p)
621
+ continue;
622
+ const segments = p.split("/");
623
+ const top = segments[0];
624
+ if (top === ".akm" || akmDirs.has(top))
625
+ continue;
626
+ result.push(p);
627
+ }
628
+ return result;
629
+ }
471
630
  // ── Exports ─────────────────────────────────────────────────────────────────
472
- export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
631
+ export { collectNonAkmDirtyPaths, ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
@@ -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
  * Centralized source provider registration.
3
6
  *
@@ -1,14 +1,4 @@
1
- /**
2
- * Install-time types used by `syncFromRef` and the legacy install pipeline.
3
- *
4
- * Distinct from the v1 {@link SourceProvider} interface (which only deals
5
- * with "configured sources" — entries already resolved into a directory).
6
- * These types describe the resolution+lockfile step that runs when
7
- * `akm add <install-ref>` materialises an upstream artifact into a local
8
- * cache directory.
9
- *
10
- * They live here, outside `provider.ts`, so the v1 SourceProvider
11
- * interface stays minimal (`{ name, kind, init, path, sync? }`) per the
12
- * architecture spec §2.1.
13
- */
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/.
14
4
  export {};
@@ -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
  * Npm-source stash provider.
3
6
  *
@@ -5,10 +8,6 @@
5
8
  * integrity, extracts it securely (via `extractTarGzSecure`), detects the
6
9
  * stash root inside the package, and applies any nested `.akm-include`
7
10
  * configuration. Cache hits short-circuit the fetch.
8
- *
9
- * Audit is intentionally NOT performed here — `akmAdd` calls
10
- * `auditInstallCandidate` after `sync()` so the policy decision lives at
11
- * the orchestrator layer where the `--trust` flag is known.
12
11
  */
13
12
  import fs from "node:fs";
14
13
  import path from "node:path";
@@ -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 { createHash } from "node:crypto";
2
5
  import fs from "node:fs";
3
6
  import path from "node:path";
@@ -1,14 +1,6 @@
1
- /**
2
- * Unified install-ref dispatcher.
3
- *
4
- * Replaces the historical `installRegistryRef()` entry point. Given an
5
- * unparsed install ref, this resolves the right syncable provider and
6
- * invokes its `sync()` method.
7
- *
8
- * Audit is intentionally NOT performed here; callers (`akmAdd`,
9
- * `akmUpdate`) decide whether to run `auditInstallCandidate` on the
10
- * synced `contentDir` because they own the `--trust` flag.
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/.
12
4
  import { UsageError } from "../../core/errors";
13
5
  import { parseRegistryRef } from "../../registry/resolve";
14
6
  import { detectStashRoot } from "./provider-utils";
@@ -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
  * Tar archive extraction and integrity verification utilities.
3
6
  *
@@ -1,27 +1,23 @@
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 { registerSourceProvider } from "../provider-factory";
2
5
  import { ensureWebsiteMirror, getWebsiteCachePaths, validateWebsiteUrl } from "../website-ingest";
3
6
  /**
4
7
  * Website source provider — thin adapter over the shared website ingest module.
5
8
  */
6
- class WebsiteSourceProvider {
7
- kind = "website";
8
- name;
9
- #config;
10
- #url;
11
- constructor(config) {
12
- this.#config = config;
13
- this.name = config.name ?? "website";
14
- this.#url = validateWebsiteUrl(config.url ?? "");
15
- }
16
- async init(_ctx) {
17
- // URL validation already happens in the constructor; nothing else to do.
18
- }
19
- path() {
20
- return getWebsiteCachePaths(this.#url).stashDir;
21
- }
22
- async sync() {
23
- await ensureWebsiteMirror(this.#config, { requireStashDir: true });
24
- }
25
- }
26
- registerSourceProvider("website", (config) => new WebsiteSourceProvider(config));
27
- export { WebsiteSourceProvider };
9
+ registerSourceProvider("website", (config) => {
10
+ const url = validateWebsiteUrl(config.url ?? "");
11
+ const name = config.name ?? "website";
12
+ return {
13
+ kind: "website",
14
+ name,
15
+ async init(_ctx) { },
16
+ path() {
17
+ return getWebsiteCachePaths(url).stashDir;
18
+ },
19
+ async sync() {
20
+ await ensureWebsiteMirror(config, { requireStashDir: true });
21
+ },
22
+ };
23
+ });
@@ -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 { deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, resolveAssetPathFromName, TYPE_DIRS, } from "../core/asset-spec";
@@ -1 +1,4 @@
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
  export {};
@@ -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 { createHash } from "node:crypto";
2
5
  import fs from "node:fs";
3
6
  import path from "node:path";
@@ -167,6 +170,10 @@ async function crawlWebsite(startUrl, options) {
167
170
  return pages;
168
171
  }
169
172
  async function fetchWebsitePage(pageUrl) {
173
+ const parsedUrl = new URL(pageUrl);
174
+ if (parsedUrl.hostname.endsWith(".invalid")) {
175
+ throw new Error(`Refusing to fetch reserved invalid hostname: ${parsedUrl.hostname}`);
176
+ }
170
177
  const response = await fetchWithRetry(pageUrl, {
171
178
  headers: {
172
179
  Accept: "text/html, text/markdown, text/plain;q=0.9, application/xhtml+xml;q=0.8",