akm-cli 0.7.5 → 0.8.0-rc.6

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 (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  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.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. 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,6 @@ 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"]);
19
21
  /**
20
22
  * Git source provider — clones (and re-pulls) a remote repo into a local
21
23
  * cache directory. Implements the v1 {@link SourceProvider} interface (spec
@@ -407,10 +409,10 @@ export function saveGitStash(name, message, writableOverride) {
407
409
  let writable = false;
408
410
  if (name) {
409
411
  const config = loadConfig();
410
- const stash = findGitStashByTarget(config.sources ?? config.stashes ?? [], name);
412
+ const stash = findGitStashByTarget(getSources(config), name);
411
413
  if (!stash)
412
414
  throw new UsageError(`No git stash found with name "${name}"`);
413
- if (!GIT_STASH_TYPES.has(stash.type)) {
415
+ if (stash.type !== "git") {
414
416
  throw new UsageError(`Stash "${name}" is not a git stash (type: ${stash.type})`);
415
417
  }
416
418
  if (!stash.url)
@@ -472,7 +474,7 @@ function findGitStashByTarget(stashes, target) {
472
474
  return stashes.find((stash) => matchesGitStashTarget(stash, target));
473
475
  }
474
476
  function matchesGitStashTarget(stash, target) {
475
- if (!GIT_STASH_TYPES.has(stash.type))
477
+ if (stash.type !== "git")
476
478
  return false;
477
479
  if (stash.name === target || stash.url === target)
478
480
  return true;
@@ -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",
@@ -0,0 +1,203 @@
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
+ // crontab backend for `akm tasks` (Linux default).
5
+ //
6
+ // Each akm-owned entry is wrapped in markers so a hand-edited crontab keeps
7
+ // its other lines untouched:
8
+ //
9
+ // # akm:task <id> BEGIN
10
+ // [SCHED] /abs/akm tasks run <id> >> /home/.../tasks/logs/<id>.log 2>&1
11
+ // # akm:task <id> END
12
+ //
13
+ // The backend reads/writes the user's crontab via `crontab -l` and
14
+ // `crontab -`. Disabling a task comments the entry with `# akm:disabled `
15
+ // rather than removing it, so re-enabling preserves the original schedule.
16
+ //
17
+ // Platform notes:
18
+ // • Operates on the *per-user* crontab — system-wide /etc/cron.d entries
19
+ // are out of scope.
20
+ // • Cron runs jobs with a stripped environment (`SHELL`, `PATH`, `HOME`,
21
+ // `LOGNAME`/`USER` only). The cron line uses an absolute akm path
22
+ // resolved at install time so it doesn't rely on the inherited PATH.
23
+ // • BSD `crontab -l` returns exit 1 with "no crontab for <user>" on a
24
+ // fresh user; we treat that as an empty crontab rather than an error.
25
+ //
26
+ // Tests inject a fake exec so unit tests don't touch the real crontab.
27
+ import { spawnSync } from "node:child_process";
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { ConfigError } from "../../core/errors";
31
+ import { getTaskLogDir } from "../../core/paths";
32
+ import { resolveAkmInvocation } from "../resolveAkmBin";
33
+ import { parseSchedule, translateToCron } from "../schedule";
34
+ const BEGIN = (id) => `# akm:task ${id} BEGIN`;
35
+ const END = (id) => `# akm:task ${id} END`;
36
+ const DISABLED_PREFIX = "# akm:disabled ";
37
+ const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
38
+ export function CRON_BACKEND(options = {}) {
39
+ const exec = options.exec ?? defaultCronExec();
40
+ const logDir = options.logDir ?? getTaskLogDir();
41
+ const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
42
+ return {
43
+ name: "cron",
44
+ install(task) {
45
+ // Create the log directory before writing the crontab line — cron
46
+ // appends with `>>` and the surrounding shell will fail the entire
47
+ // entry if the parent directory doesn't exist.
48
+ ensureDir(logDir);
49
+ const cronLine = buildCronLine(task, akmArgv, logDir);
50
+ const existing = readCrontab(exec);
51
+ const block = renderBlock(task.id, cronLine, task.enabled);
52
+ const next = upsertBlock(existing, task.id, block);
53
+ writeCrontab(exec, next);
54
+ },
55
+ uninstall(id) {
56
+ const existing = readCrontab(exec);
57
+ const next = removeBlock(existing, id);
58
+ writeCrontab(exec, next);
59
+ },
60
+ setEnabled(id, enabled) {
61
+ const existing = readCrontab(exec);
62
+ const next = toggleBlock(existing, id, enabled);
63
+ writeCrontab(exec, next);
64
+ },
65
+ list() {
66
+ const existing = readCrontab(exec);
67
+ const ids = [];
68
+ for (const line of existing.split(/\r?\n/)) {
69
+ const m = line.match(BLOCK_RE);
70
+ if (m)
71
+ ids.push(m[1]);
72
+ }
73
+ return ids.map((id) => ({ id }));
74
+ },
75
+ };
76
+ }
77
+ // ── helpers (exported for tests) ────────────────────────────────────────────
78
+ export function buildCronLine(task, akmArgv, logDir) {
79
+ const spec = parseSchedule(task.schedule, "cron");
80
+ const cronExpr = translateToCron(spec);
81
+ const logPath = path.join(logDir, `${task.id}.log`);
82
+ const cmd = [...akmArgv, "tasks", "run", task.id].map((part) => quoteForCron(part)).join(" ");
83
+ return `${cronExpr} ${cmd} >> ${quoteForCron(logPath)} 2>&1`;
84
+ }
85
+ export function renderBlock(id, cronLine, enabled) {
86
+ const body = enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
87
+ return [BEGIN(id), body, END(id)].join("\n");
88
+ }
89
+ export function upsertBlock(existing, id, block) {
90
+ const trimmed = existing.replace(/\s+$/g, "");
91
+ const removed = removeBlock(trimmed, id);
92
+ const sep = removed.length === 0 ? "" : "\n";
93
+ return `${removed}${sep}${block}\n`;
94
+ }
95
+ export function removeBlock(existing, id) {
96
+ const lines = existing.split(/\r?\n/);
97
+ const out = [];
98
+ let inBlock = false;
99
+ for (const line of lines) {
100
+ if (!inBlock && line === BEGIN(id)) {
101
+ inBlock = true;
102
+ continue;
103
+ }
104
+ if (inBlock && line === END(id)) {
105
+ inBlock = false;
106
+ continue;
107
+ }
108
+ if (inBlock)
109
+ continue;
110
+ out.push(line);
111
+ }
112
+ // Collapse trailing blank lines.
113
+ while (out.length > 0 && out[out.length - 1] === "")
114
+ out.pop();
115
+ return out.join("\n");
116
+ }
117
+ export function toggleBlock(existing, id, enabled) {
118
+ const lines = existing.split(/\r?\n/);
119
+ const out = [];
120
+ let inBlock = false;
121
+ for (const line of lines) {
122
+ if (!inBlock && line === BEGIN(id)) {
123
+ inBlock = true;
124
+ out.push(line);
125
+ continue;
126
+ }
127
+ if (inBlock && line === END(id)) {
128
+ inBlock = false;
129
+ out.push(line);
130
+ continue;
131
+ }
132
+ if (inBlock) {
133
+ const isComment = line.startsWith(DISABLED_PREFIX);
134
+ if (enabled && isComment) {
135
+ out.push(line.slice(DISABLED_PREFIX.length));
136
+ }
137
+ else if (!enabled && !isComment) {
138
+ out.push(`${DISABLED_PREFIX}${line}`);
139
+ }
140
+ else {
141
+ out.push(line);
142
+ }
143
+ continue;
144
+ }
145
+ out.push(line);
146
+ }
147
+ return out.join("\n");
148
+ }
149
+ function quoteForCron(part) {
150
+ // crontab passes the rest of the line to /bin/sh -c, so quote anything that
151
+ // isn't a plain shell-safe token. Single-quote and escape embedded single
152
+ // quotes via the standard shell idiom: `'foo'\''bar'`.
153
+ if (/^[A-Za-z0-9_\-./@:%=+,]+$/.test(part))
154
+ return part;
155
+ return `'${part.replace(/'/g, `'\\''`)}'`;
156
+ }
157
+ function readCrontab(exec) {
158
+ const result = exec.read();
159
+ if (result.status === 0)
160
+ return result.stdout ?? "";
161
+ // BSD crontab returns 1 with "no crontab for <user>" on stderr — treat as empty.
162
+ if (/no crontab for/i.test(result.stderr ?? ""))
163
+ return "";
164
+ if (/no crontab/i.test(result.stdout ?? ""))
165
+ return "";
166
+ throw new ConfigError(`crontab -l failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`, "INVALID_CONFIG_FILE", "Ensure the `crontab` binary is on PATH and your shell can read the user crontab.");
167
+ }
168
+ function writeCrontab(exec, content) {
169
+ const normalised = content.endsWith("\n") || content.length === 0 ? content : `${content}\n`;
170
+ const result = exec.write(normalised);
171
+ if (result.status !== 0) {
172
+ throw new ConfigError(`crontab - failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`, "INVALID_CONFIG_FILE", "Ensure the `crontab` binary is on PATH and your shell can write the user crontab.");
173
+ }
174
+ }
175
+ function ensureDir(dir) {
176
+ try {
177
+ fs.mkdirSync(dir, { recursive: true });
178
+ }
179
+ catch {
180
+ // Best-effort: the install will surface a clearer error if the cron
181
+ // line later fails at runtime due to a missing redirection target.
182
+ }
183
+ }
184
+ function defaultCronExec() {
185
+ return {
186
+ read() {
187
+ const r = spawnSync("crontab", ["-l"], { encoding: "utf8" });
188
+ return {
189
+ status: r.status ?? 1,
190
+ stdout: r.stdout ?? "",
191
+ stderr: r.stderr ?? "",
192
+ };
193
+ },
194
+ write(content) {
195
+ const r = spawnSync("crontab", ["-"], { encoding: "utf8", input: content });
196
+ return {
197
+ status: r.status ?? 1,
198
+ stdout: r.stdout ?? "",
199
+ stderr: r.stderr ?? "",
200
+ };
201
+ },
202
+ };
203
+ }
@@ -0,0 +1,28 @@
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 { spawnSync } from "node:child_process";
5
+ /**
6
+ * Run a command synchronously, normalizing null results to safe defaults.
7
+ * args[0] is the binary; args[1..] are its arguments.
8
+ */
9
+ export function spawnCommand(args) {
10
+ const [bin, ...rest] = args;
11
+ const r = spawnSync(bin, rest, { encoding: "utf8" });
12
+ return {
13
+ status: r.status ?? 1,
14
+ stdout: r.stdout ?? "",
15
+ stderr: r.stderr ?? "",
16
+ };
17
+ }
18
+ /**
19
+ * Escape a string for safe embedding in an XML attribute or text node.
20
+ */
21
+ export function escapeXml(s) {
22
+ return s
23
+ .replace(/&/g, "&amp;")
24
+ .replace(/</g, "&lt;")
25
+ .replace(/>/g, "&gt;")
26
+ .replace(/"/g, "&quot;")
27
+ .replace(/'/g, "&apos;");
28
+ }
@@ -0,0 +1,24 @@
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 { CRON_BACKEND } from "./cron";
5
+ import { LAUNCHD_BACKEND } from "./launchd";
6
+ import { SCHTASKS_BACKEND } from "./schtasks";
7
+ export function selectBackend(options = {}) {
8
+ const platform = options.platform ?? process.platform;
9
+ switch (platform) {
10
+ case "win32":
11
+ return SCHTASKS_BACKEND(options.schtasks);
12
+ case "darwin":
13
+ return LAUNCHD_BACKEND(options.launchd);
14
+ default:
15
+ return CRON_BACKEND(options.cron);
16
+ }
17
+ }
18
+ export function backendNameForPlatform(platform = process.platform) {
19
+ if (platform === "win32")
20
+ return "schtasks";
21
+ if (platform === "darwin")
22
+ return "launchd";
23
+ return "cron";
24
+ }
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>{{LABEL}}</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ {{PROGRAM_ARGS}}
10
+ </array>
11
+ <key>StandardOutPath</key>
12
+ <string>{{LOG_PATH}}</string>
13
+ <key>StandardErrorPath</key>
14
+ <string>{{LOG_PATH}}</string>
15
+ <key>RunAtLoad</key>
16
+ <false/>
17
+ {{ENV_VARS}}{{TRIGGER_XML}}
18
+ </dict>
19
+ </plist>