akm-cli 0.8.0-rc1 → 0.8.0

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 (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  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 +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2162 -1258
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  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 +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +233 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +17 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +662 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +114 -48
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -307
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -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,6 @@
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/.
12
4
  import { getSources } from "../core/config";
13
5
  import { createProviderRegistry } from "../registry/create-provider-registry";
14
6
  // ── Factory map ─────────────────────────────────────────────────────────────
@@ -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,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 { spawnSync } from "node:child_process";
2
5
  import { createHash, randomBytes } from "node:crypto";
3
6
  import fs from "node:fs";
@@ -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) {
@@ -383,6 +389,29 @@ function parseGitRepoUrl(rawUrl) {
383
389
  }
384
390
  return { cloneUrl: rawUrl, ref: null, canonicalUrl };
385
391
  }
392
+ // ── Save support ─────────────────────────────────────────────────────────────
393
+ /**
394
+ * Recognize a stash directory as git-backed by the presence of a `.git` entry.
395
+ *
396
+ * Recognition is deliberately by `.git` presence — NOT by a configured remote.
397
+ * `akm init` git-inits the primary stash (see init.ts `ensureGitRepo`), so a
398
+ * freshly-initialized local stash with no remote is still git-backed. This is
399
+ * the single source of truth used both by `saveGitStash` (below) and by the
400
+ * end-of-run improve auto-sync gate.
401
+ */
402
+ export function isGitBackedStash(stashDir) {
403
+ return fs.existsSync(path.join(stashDir, ".git"));
404
+ }
405
+ /**
406
+ * Resolve the writable-override flag for an end-of-run / `akm sync` commit on
407
+ * the primary stash. Returns `true` when the root config explicitly marks the
408
+ * primary stash writable, otherwise `undefined` (leave the per-stash default
409
+ * untouched). Extracted so `akm sync`, `akm improve`'s end-of-run sync, and the
410
+ * CLI body all derive this identically instead of re-copying the expression.
411
+ */
412
+ export function resolveWritableOverride(config) {
413
+ return config.writable === true ? true : undefined;
414
+ }
386
415
  /**
387
416
  * Commit (and optionally push) local changes in a git-backed stash.
388
417
  *
@@ -394,8 +423,18 @@ function parseGitRepoUrl(rawUrl) {
394
423
  *
395
424
  * When `name` is omitted the primary stash directory is used.
396
425
  * When `message` is omitted a timestamp is used.
426
+ *
427
+ * `options.repoDir` overrides the primary-stash directory the commit targets
428
+ * (only honoured when `name` is omitted). Callers that already resolved the
429
+ * primary stash dir (e.g. `akm improve`'s end-of-run sync, whose pre-commit
430
+ * gate validates that exact directory) pass it here so the gate and the commit
431
+ * operate on the SAME directory instead of independently calling
432
+ * `resolveStashDir({ readOnly: true })`. When absent, behaviour is unchanged.
397
433
  */
398
- export function saveGitStash(name, message, writableOverride) {
434
+ export function saveGitStash(name, message, writableOverride, options) {
435
+ // `push: false` (from `akm sync --no-push`) commits but never pushes, even
436
+ // when the stash is writable with a remote configured.
437
+ const allowPush = options?.push !== false;
399
438
  const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
400
439
  // Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
401
440
  // clamp length. An attacker can otherwise pass `--message "subject\n\n\
@@ -410,7 +449,7 @@ export function saveGitStash(name, message, writableOverride) {
410
449
  const stash = findGitStashByTarget(getSources(config), name);
411
450
  if (!stash)
412
451
  throw new UsageError(`No git stash found with name "${name}"`);
413
- if (!GIT_STASH_TYPES.has(stash.type)) {
452
+ if (stash.type !== "git") {
414
453
  throw new UsageError(`Stash "${name}" is not a git stash (type: ${stash.type})`);
415
454
  }
416
455
  if (!stash.url)
@@ -420,44 +459,72 @@ export function saveGitStash(name, message, writableOverride) {
420
459
  writable = stash.writable === true;
421
460
  }
422
461
  else {
423
- repoDir = resolveStashDir({ readOnly: true });
462
+ // Honour an explicit primary-stash dir override (keeps the improve gate and
463
+ // the commit on the same directory); otherwise resolve the default.
464
+ repoDir = options?.repoDir ?? resolveStashDir({ readOnly: true });
424
465
  // Allow caller to override writable for the primary stash (e.g. from root config.writable)
425
466
  if (writableOverride !== undefined) {
426
467
  writable = writableOverride;
427
468
  }
428
469
  }
429
470
  // No-op: not a git repo
430
- if (!fs.existsSync(path.join(repoDir, ".git"))) {
471
+ if (!isGitBackedStash(repoDir)) {
431
472
  return { committed: false, pushed: false, skipped: true, reason: "not a git repository", output: "" };
432
473
  }
433
474
  // Nothing to commit?
434
- const statusResult = spawnSync("git", ["-C", repoDir, "status", "--porcelain"], { encoding: "utf8" });
475
+ const statusResult = runGit(["-C", repoDir, "status", "--porcelain"]);
435
476
  if (statusResult.error || statusResult.status !== 0) {
436
477
  throw new Error(`git status failed: ${statusResult.error?.message || statusResult.stderr?.trim() || "unknown error"}`);
437
478
  }
438
479
  if (!statusResult.stdout.trim()) {
439
480
  return { committed: false, pushed: false, skipped: false, output: "nothing to commit, working tree clean" };
440
481
  }
482
+ // Safety check (#476): when the stash dir is shared with a non-akm project
483
+ // (stash root == project repo root), `git add -A` would stage every dirty
484
+ // file in the user's working tree and push their unrelated WIP to the
485
+ // stash's remote. Refuse if any dirty path is outside the known akm-
486
+ // managed subtrees (TYPE_DIRS + `.akm/` state).
487
+ const nonAkmDirty = collectNonAkmDirtyPaths(statusResult.stdout);
488
+ if (nonAkmDirty.length > 0) {
489
+ const sample = nonAkmDirty.slice(0, 10);
490
+ const more = nonAkmDirty.length > sample.length ? `\n ...and ${nonAkmDirty.length - sample.length} more` : "";
491
+ throw new Error(`refusing to push: stash repo at ${repoDir} has uncommitted non-akm changes:\n` +
492
+ sample.map((p) => ` ${p}`).join("\n") +
493
+ more +
494
+ `\nCommit or stash these manually before running an akm push. ` +
495
+ `Akm-managed paths are: ${Object.values(TYPE_DIRS).join(", ")}, .akm/`);
496
+ }
441
497
  // Stage and commit — supply fallback identity so fresh environments without
442
498
  // user.name/user.email configured can always commit to the default stash.
443
- const addResult = spawnSync("git", ["-C", repoDir, "add", "-A"], { encoding: "utf8" });
499
+ // `add -A` is safe here because nonAkmDirty was just verified empty.
500
+ const addResult = runGit(["-C", repoDir, "add", "-A"]);
444
501
  if (addResult.status !== 0) {
445
502
  throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
446
503
  }
447
- const commitResult = spawnSync("git", ["-C", repoDir, "-c", "user.name=akm", "-c", "user.email=akm@local", "commit", "-m", commitMessage], { encoding: "utf8" });
504
+ const commitResult = runGit([
505
+ "-C",
506
+ repoDir,
507
+ "-c",
508
+ "user.name=akm",
509
+ "-c",
510
+ "user.email=akm@local",
511
+ "commit",
512
+ "-m",
513
+ commitMessage,
514
+ ]);
448
515
  if (commitResult.status !== 0) {
449
516
  throw new Error(`git commit failed: ${commitResult.stderr?.trim() || "unknown error"}`);
450
517
  }
451
518
  // Push only when there is a remote AND the stash is marked writable
452
- const remoteResult = spawnSync("git", ["-C", repoDir, "remote"], { encoding: "utf8" });
519
+ const remoteResult = runGit(["-C", repoDir, "remote"]);
453
520
  if (remoteResult.status !== 0) {
454
521
  throw new Error(`git remote failed: ${remoteResult.stderr?.trim() || "unknown error"}`);
455
522
  }
456
523
  const hasRemote = remoteResult.stdout.trim().length > 0;
457
- if (!hasRemote || !writable) {
524
+ if (!hasRemote || !writable || !allowPush) {
458
525
  return { committed: true, pushed: false, skipped: false, output: commitResult.stdout.trim() };
459
526
  }
460
- const pushResult = spawnSync("git", ["-C", repoDir, "push"], { encoding: "utf8", timeout: 120_000 });
527
+ const pushResult = runGit(["-C", repoDir, "push"], { timeout: 120_000 });
461
528
  if (pushResult.status !== 0) {
462
529
  throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
463
530
  }
@@ -472,7 +539,7 @@ function findGitStashByTarget(stashes, target) {
472
539
  return stashes.find((stash) => matchesGitStashTarget(stash, target));
473
540
  }
474
541
  function matchesGitStashTarget(stash, target) {
475
- if (!GIT_STASH_TYPES.has(stash.type))
542
+ if (stash.type !== "git")
476
543
  return false;
477
544
  if (stash.name === target || stash.url === target)
478
545
  return true;
@@ -510,5 +577,88 @@ function buildGithubTargetAliases(canonicalUrl) {
510
577
  return new Set();
511
578
  }
512
579
  }
580
+ // ── Clone-failure classification (#487) ─────────────────────────────────────
581
+ /**
582
+ * Translate git's stderr into an actionable message. Without this, a user
583
+ * who passes a nonexistent or private repo to `akm add` sees:
584
+ *
585
+ * "could not read Username for 'https://github.com': No such device or
586
+ * address"
587
+ *
588
+ * That is git falling through to its auth-prompt path — the actual cause
589
+ * is "repo doesn't exist (or is private)". We classify the common patterns
590
+ * and emit a message that names the cause and the fix.
591
+ */
592
+ export function classifyCloneFailure(url, stderr, spawnError) {
593
+ const raw = (stderr ?? "").trim();
594
+ const spawnMsg = spawnError?.message ?? "";
595
+ // `git` binary not on PATH.
596
+ if (spawnError?.code === "ENOENT") {
597
+ return `Failed to clone ${url}: 'git' is not installed or not on PATH. Install git, then re-run.`;
598
+ }
599
+ // Auth-prompt fall-through (the headline #487 case).
600
+ if (/could not read Username|terminal prompts disabled|Authentication failed|fatal: Authentication/i.test(raw)) {
601
+ return (`Failed to clone ${url}: repository not found or private. ` +
602
+ `If the repository is public, double-check the URL and try again. ` +
603
+ `If it is private, set GH_TOKEN (or configure a git credential helper) before re-running.`);
604
+ }
605
+ // 404-style messages from git http.
606
+ if (/repository '.*' not found|HTTP 404|fatal: remote error|not found:|Not Found/i.test(raw)) {
607
+ return (`Failed to clone ${url}: repository not found. ` +
608
+ `Check the URL — for GitHub, the form is 'owner/repo' or 'github:owner/repo'.`);
609
+ }
610
+ // SSH connection issues.
611
+ if (/Permission denied \(publickey\)|kex_exchange_identification|Connection refused|Connection timed out/i.test(raw)) {
612
+ return (`Failed to clone ${url}: network or SSH failure. ` +
613
+ `Check connectivity, your SSH agent, and the remote host's availability.`);
614
+ }
615
+ // Branch / ref-specific failures.
616
+ if (/Remote branch .* not found in upstream origin|couldn't find remote ref/i.test(raw)) {
617
+ return (`Failed to clone ${url}: the requested branch/tag does not exist on the remote. ` +
618
+ `Verify the ref name and re-run.`);
619
+ }
620
+ const detail = raw || spawnMsg || "unknown error";
621
+ return `Failed to clone ${url}: ${detail}`;
622
+ }
623
+ // ── Stash-safety helpers (#476) ──────────────────────────────────────────────
624
+ /**
625
+ * Inspect `git status --porcelain` output and return every dirty path that is
626
+ * NOT inside an akm-managed subtree. Used by `runUpstreamPush` to refuse
627
+ * pushing unrelated WIP when a writable stash shares its root with a project
628
+ * repo.
629
+ *
630
+ * Porcelain v1 format: `XY <path>` or `XY <orig> -> <new>` for renames. We
631
+ * key off the post-rename path (or the only path) — that is the working-tree
632
+ * file at risk of being staged by `git add -A`.
633
+ */
634
+ function collectNonAkmDirtyPaths(porcelainOutput) {
635
+ const akmDirs = new Set(Object.values(TYPE_DIRS));
636
+ const result = [];
637
+ for (const rawLine of porcelainOutput.split("\n")) {
638
+ const line = rawLine.replace(/\r$/, "");
639
+ if (line.length === 0)
640
+ continue;
641
+ // Skip the 2-char status code + 1 space.
642
+ let p = line.length > 3 ? line.slice(3) : "";
643
+ // Renames / copies: `from -> to`. Stage decision applies to `to`.
644
+ const arrow = p.lastIndexOf(" -> ");
645
+ if (arrow !== -1) {
646
+ p = p.slice(arrow + 4);
647
+ }
648
+ // Strip surrounding quotes for paths with special chars.
649
+ if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
650
+ p = p.slice(1, -1);
651
+ }
652
+ if (!p)
653
+ continue;
654
+ const segments = p.split("/");
655
+ const top = segments[0];
656
+ if (top === ".akm" || akmDirs.has(top))
657
+ continue;
658
+ result.push(p);
659
+ }
660
+ return result;
661
+ }
513
662
  // ── Exports ─────────────────────────────────────────────────────────────────
514
- export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
663
+ export { collectNonAkmDirtyPaths, ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
664
+ // resolveWritableOverride is exported at its declaration above.
@@ -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";
@@ -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
  // crontab backend for `akm tasks` (Linux default).
2
5
  //
3
6
  // Each akm-owned entry is wrapped in markers so a hand-edited crontab keeps
@@ -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 { spawnSync } from "node:child_process";
2
5
  /**
3
6
  * Run a command synchronously, normalizing null results to safe defaults.
@@ -1,14 +1,6 @@
1
- /**
2
- * Backend selection for the OS-native scheduler.
3
- *
4
- * • Linux → crontab
5
- * • macOS → launchd (per-user LaunchAgent)
6
- * • Windows → schtasks.exe / Task Scheduler
7
- *
8
- * Each backend implements {@link TaskBackend}; selection is a one-line
9
- * platform check. Tests inject a fake `platform` to exercise non-host
10
- * code paths.
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 { CRON_BACKEND } from "./cron";
13
5
  import { LAUNCHD_BACKEND } from "./launchd";
14
6
  import { SCHTASKS_BACKEND } from "./schtasks";
@@ -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
  * launchd backend for `akm tasks` (macOS default).
3
6
  *
@@ -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
  * schtasks.exe backend for `akm tasks` (Windows default).
3
6
  *