akm-cli 0.9.15 → 0.9.16-alpha.2

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 (86) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  4. package/dist/cli/retired-commands.js +0 -2
  5. package/dist/commands/env/env-binding.js +4 -4
  6. package/dist/commands/env/env-cli.js +3 -3
  7. package/dist/commands/improve/improve-cli.js +19 -14
  8. package/dist/commands/improve/reflect.js +23 -2
  9. package/dist/commands/lint/base-linter.js +9 -0
  10. package/dist/commands/lint/env-key-rules.js +2 -2
  11. package/dist/commands/proposal/propose.js +15 -1
  12. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  13. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  14. package/dist/commands/read/search.js +33 -4
  15. package/dist/commands/read/show.js +21 -2
  16. package/dist/commands/registry-cli.js +5 -5
  17. package/dist/commands/sources/add-cli.js +59 -16
  18. package/dist/commands/sources/bundle-cli.js +35 -11
  19. package/dist/commands/sources/bundle-config-ops.js +30 -0
  20. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  21. package/dist/commands/sources/installed-stashes.js +43 -28
  22. package/dist/commands/sources/source-add.js +33 -17
  23. package/dist/commands/sources/source-manage.js +34 -12
  24. package/dist/commands/sources/stash-skeleton.js +6 -3
  25. package/dist/commands/tasks/explain.js +4 -1
  26. package/dist/commands/tasks/tasks-cli.js +31 -9
  27. package/dist/commands/tasks/tasks.js +239 -194
  28. package/dist/commands/tasks/validate.js +20 -32
  29. package/dist/core/activation-policy.js +4 -4
  30. package/dist/core/adapter/adapters/akm-adapter.js +5 -0
  31. package/dist/core/adapter/execution-source.js +10 -29
  32. package/dist/core/config/config-schema.js +64 -8
  33. package/dist/core/config/config-sources.js +96 -2
  34. package/dist/core/config/config.js +190 -24
  35. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  36. package/dist/core/config/schema/execution.js +23 -0
  37. package/dist/core/config/schema/experimental.js +1 -1
  38. package/dist/core/config/schema/scheduler.js +20 -0
  39. package/dist/core/config/schema/search.js +1 -1
  40. package/dist/core/config/schema/sources-bundles.js +32 -1
  41. package/dist/core/content-safety.js +52 -0
  42. package/dist/core/maintenance-barrier.js +6 -6
  43. package/dist/core/type-presentation.js +1 -1
  44. package/dist/core/write-source.js +13 -8
  45. package/dist/indexer/bundle-identity-guard.js +45 -8
  46. package/dist/indexer/indexer.js +1 -1
  47. package/dist/indexer/materialize-embeddings.js +15 -1
  48. package/dist/indexer/search/search-source.js +29 -11
  49. package/dist/integrations/agent/execution-lowering.js +3 -2
  50. package/dist/integrations/agent/execution-preparation.js +32 -1
  51. package/dist/integrations/agent/prompts.js +1 -1
  52. package/dist/integrations/agent/request-lowering.js +3 -2
  53. package/dist/llm/client.js +2 -1
  54. package/dist/output/shapes/passthrough.js +2 -0
  55. package/dist/registry/resolve.js +37 -10
  56. package/dist/scripts/akm-migrate-node.js +13644 -9894
  57. package/dist/scripts/akm-migrate.js +12626 -8876
  58. package/dist/setup/setup.js +3 -3
  59. package/dist/setup/steps/tasks.js +29 -36
  60. package/dist/sources/providers/git-install.js +17 -11
  61. package/dist/sources/providers/git-provider.js +12 -5
  62. package/dist/sources/providers/git-stash.js +38 -16
  63. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  64. package/dist/storage/repositories/index-vec-repository.js +100 -0
  65. package/dist/tasks/activation-config.js +90 -0
  66. package/dist/tasks/backends/cron.js +9 -0
  67. package/dist/tasks/backends/launchd.js +1 -0
  68. package/dist/tasks/backends/schtasks.js +2 -0
  69. package/dist/tasks/embedded.js +4 -5
  70. package/dist/tasks/scheduler-binding.js +2 -2
  71. package/dist/tasks/scheduler-sync-preview.js +8 -1
  72. package/dist/tasks/scheduler-sync.js +19 -10
  73. package/dist/tasks/source/parse-task-source.js +10 -113
  74. package/dist/tasks/source/project-v4.js +2 -2
  75. package/dist/tasks/source/task-source-v4.js +4 -12
  76. package/dist/tasks/source/task-to-v3.js +4 -12
  77. package/dist/tasks/source/task-to-v4.js +40 -7
  78. package/docs/migration/README.md +1 -0
  79. package/docs/migration/release-notes/0.9.16.md +72 -0
  80. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  81. package/docs/reference/cli.md +37 -29
  82. package/docs/reference/configuration.md +48 -5
  83. package/docs/reference/tasks.md +34 -29
  84. package/package.json +1 -1
  85. package/schemas/akm-config.json +112 -4
  86. package/schemas/akm-task.json +1 -2
@@ -205,8 +205,8 @@ export function rebaseSetupChanges(original, desired, latest, pathParts = []) {
205
205
  }
206
206
  return result;
207
207
  }
208
- async function saveSetupConfig(original, desired, precommit) {
209
- const result = await mutateConfigWithPrecommit((latest) => rebaseSetupChanges(original, desired, latest), precommit);
208
+ async function saveSetupConfig(original, desired, precommit, options) {
209
+ const result = await mutateConfigWithPrecommit((latest) => rebaseSetupChanges(original, desired, latest), precommit, options);
210
210
  return { config: result.config, precommit: result.precommit };
211
211
  }
212
212
  /** Registry-managed bundles are not editable through setup's source picker. */
@@ -572,7 +572,7 @@ export async function runSetupWizard(opts) {
572
572
  const { config: savedConfig } = await saveSetupConfig(current, finalConfig, async () => {
573
573
  if (!opts?.noInit)
574
574
  await akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
575
- });
575
+ }, { persistTopLevelKeys: ["semanticSearchMode"] });
576
576
  // After config persistence, the task step reviews the plan and asks one
577
577
  // explicit confirmation before changing task files or scheduler state.
578
578
  // Non-interactive setup paths never reach this interactive-only step.
@@ -8,9 +8,11 @@ import path from "node:path";
8
8
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
9
9
  import * as p from "../../cli/clack.js";
10
10
  import { akmTasksSync } from "../../commands/tasks/tasks.js";
11
- import { loadConfig } from "../../core/config/config.js";
11
+ import { makeBundleRef } from "../../core/asset/asset-ref.js";
12
+ import { loadConfig, mutateConfig } from "../../core/config/config.js";
12
13
  import { UsageError } from "../../core/errors.js";
13
14
  import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
15
+ import { schedulerActivationSourceId, schedulerActivations } from "../../tasks/activation-config.js";
14
16
  import { backendNameForPlatform } from "../../tasks/backends/index.js";
15
17
  import { listEmbeddedTasks } from "../../tasks/embedded.js";
16
18
  import { parseSchedule } from "../../tasks/schedule.js";
@@ -71,34 +73,13 @@ export function detectServerDefault() {
71
73
  function normaliseTaskIdForMatch(raw) {
72
74
  return raw.trim().replace(/\.(yml|md)$/, "");
73
75
  }
74
- /**
75
- * Toggle a task source v4 file's enabled state via a full parse/render
76
- * round-trip (setup's own edits are infrequent and not comment-preservation-
77
- * sensitive, unlike `commands/tasks/tasks.ts`'s `setEnabledInYaml` line
78
- * splice). Broadcasts `enabled` across every `schedule[]` entry — the
79
- * closest v4 equivalent of v3's single document-level flag. `src` no longer
80
- * accepts a task v3 file at all (P4 §3.2) — `listSetupTaskDefinitions` below
81
- * already fails closed on one before this function is ever reached, so
82
- * there is no legacy `akm.enabled` shape left to handle here.
83
- */
84
- function setTaskEnabledInYaml(yaml, enabled) {
85
- const document = yamlParse(yaml);
86
- const schedule = document.schedule;
87
- if (typeof schedule === "string") {
88
- document.schedule = [{ cron: schedule, enabled }];
89
- }
90
- else if (Array.isArray(schedule) && schedule.length > 0) {
91
- document.schedule = schedule.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? { ...entry, enabled } : entry);
92
- }
93
- else {
94
- throw new UsageError("Task source v4 must declare a schedule before setup can change enabled state.");
95
- }
96
- return yamlStringify(document);
97
- }
98
76
  export function listSetupTaskDefinitions() {
99
77
  const config = loadConfig();
100
78
  const target = resolveWriteTarget(config, config.defaultBundle, { requireWritable: false });
101
79
  const taskDir = path.join(target.source.path, "tasks");
80
+ const enabledRefs = new Set(schedulerActivations(config)
81
+ .filter((activation) => activation.kind === "task")
82
+ .map((activation) => activation.ref));
102
83
  if (!fs.existsSync(taskDir))
103
84
  return [];
104
85
  const tasks = [];
@@ -121,10 +102,7 @@ export function listSetupTaskDefinitions() {
121
102
  id,
122
103
  schedule: schedules[0],
123
104
  schedules: Object.freeze(schedules),
124
- // task source v4 has no document-level enabled (P4-N6) — a task is
125
- // considered enabled for review purposes when at least one of its
126
- // schedule bindings will actually fire.
127
- enabled: document.schedule.some((entry) => entry.enabled),
105
+ enabled: enabledRefs.has(makeBundleRef(target.source.name, `tasks/${id}`)),
128
106
  ...(document.description !== undefined ? { description: document.description } : {}),
129
107
  });
130
108
  }
@@ -147,11 +125,11 @@ export async function prepareSetupTaskDefinitions(tasks, deps = {}) {
147
125
  const original = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : undefined;
148
126
  let yaml;
149
127
  if (original !== undefined) {
150
- yaml = setTaskEnabledInYaml(original, plan.enabled);
128
+ yaml = original;
151
129
  }
152
130
  else {
153
131
  const document = yamlParse(plan.task.yaml);
154
- document.schedule = plan.enabled ? plan.schedule : [{ cron: plan.schedule, enabled: false }];
132
+ document.schedule = plan.schedule;
155
133
  yaml = yamlStringify(document);
156
134
  }
157
135
  const parsed = parseTaskSource({ yaml, filePath, workspaceRoot: target.source.path });
@@ -161,8 +139,6 @@ export async function prepareSetupTaskDefinitions(tasks, deps = {}) {
161
139
  return { filePath, original, yaml, ref: { type: "task", name: plan.task.id } };
162
140
  });
163
141
  const changed = prepared.filter((entry) => entry.original !== entry.yaml);
164
- if (changed.length === 0)
165
- return 0;
166
142
  const attempted = [];
167
143
  try {
168
144
  for (const entry of changed) {
@@ -199,6 +175,23 @@ export async function prepareSetupTaskDefinitions(tasks, deps = {}) {
199
175
  }
200
176
  throw error;
201
177
  }
178
+ const selected = new Set(tasks.filter((plan) => plan.enabled).map((plan) => makeBundleRef(target.source.name, `tasks/${plan.task.id}`)));
179
+ const managed = new Set(tasks.map((plan) => makeBundleRef(target.source.name, `tasks/${plan.task.id}`)));
180
+ mutateConfig((current) => {
181
+ const existing = schedulerActivations(current);
182
+ const next = existing.filter((activation) => activation.kind !== "task" || !managed.has(activation.ref));
183
+ const sourceId = schedulerActivationSourceId(current, target.source.name);
184
+ if (selected.size > 0 && !sourceId) {
185
+ throw new UsageError(`Cannot activate setup tasks from disabled bundle ${JSON.stringify(target.source.name)}.`);
186
+ }
187
+ for (const ref of selected) {
188
+ next.push({ kind: "task", ref, sourceId: sourceId });
189
+ }
190
+ next.sort((left, right) => left.ref.localeCompare(right.ref) || left.kind.localeCompare(right.kind));
191
+ if (JSON.stringify(existing) === JSON.stringify(next))
192
+ return current;
193
+ return { ...current, scheduler: { ...current.scheduler, enabled: next } };
194
+ });
202
195
  return changed.length;
203
196
  }
204
197
  const DEFAULT_SCHEDULED_TASKS_DEPS = {
@@ -212,9 +205,9 @@ export async function stepScheduledTasks(deps = DEFAULT_SCHEDULED_TASKS_DEPS, op
212
205
  return;
213
206
  }
214
207
  // ALL templates are offered, including ships-disabled ones (e.g. the
215
- // manual-recovery catchup task): an unselected template is still PREPARED
216
- // with `enabled: false`, so its YAML exists for `akm task run <id>` while
217
- // nothing lands in the scheduler uncommented. Filtering on `task.enabled`
208
+ // manual-recovery catchup task): an unselected template is still PREPARED,
209
+ // so its YAML exists for `akm task run <id>` while its ref remains absent
210
+ // from local scheduler activation. Filtering on `task.enabled`
218
211
  // here would make ships-disabled templates invisible and unpreparable.
219
212
  const embedded = listEmbeddedTasks();
220
213
  if (embedded.length === 0)
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  import { isWithin } from "../../core/common.js";
9
9
  import { UsageError } from "../../core/errors.js";
10
10
  import { getRegistryCacheDir } from "../../core/paths.js";
11
- import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve.js";
11
+ import { gitCredentialEnvironment, parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl, } from "../../registry/resolve.js";
12
12
  import { applyAkmIncludeConfig, buildInstallCacheDir, detectStashRoot, isDirectory } from "./provider-utils.js";
13
13
  /**
14
14
  * Shared subprocess wrapper for `git` invocations. Disables git's interactive
@@ -22,13 +22,16 @@ export function runGit(args, options) {
22
22
  });
23
23
  }
24
24
  /** Fetch and classify the current branch against its upstream without changing the worktree. */
25
- export function inspectGitUpstream(repoDir) {
25
+ export function inspectGitUpstream(repoDir, credential) {
26
26
  const remotes = runGit(["-C", repoDir, "remote"]);
27
27
  if (remotes.status !== 0)
28
28
  throw new UsageError(`Cannot inspect Git remotes at ${repoDir}: ${remotes.stderr.trim()}`);
29
29
  if (!remotes.stdout.trim())
30
30
  return { hasRemote: false, ahead: 0, behind: 0 };
31
- const fetch = runGit(["-C", repoDir, "fetch", "--prune"], { timeout: 120_000 });
31
+ const fetch = runGit(["-C", repoDir, "fetch", "--prune"], {
32
+ timeout: 120_000,
33
+ env: gitCredentialEnvironment(credential),
34
+ });
32
35
  if (fetch.status !== 0)
33
36
  throw new UsageError(`Cannot refresh Git target at ${repoDir}: ${fetch.stderr.trim()}`);
34
37
  const upstream = runGit(["-C", repoDir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
@@ -160,10 +163,10 @@ async function doSyncGit(parsed, options) {
160
163
  validateGitUrl(parsed.url);
161
164
  if (parsed.requestedRef)
162
165
  validateGitRef(parsed.requestedRef);
163
- const resolved = await resolveRegistryArtifact(parsed);
166
+ const resolved = await resolveRegistryArtifact(parsed, { gitCredential: options?.credential });
164
167
  const syncedAt = (options?.now ?? new Date()).toISOString();
165
168
  if (options?.writable && options.writableRoot) {
166
- return syncExistingWritableCheckout(parsed, resolved, options.writableRoot, syncedAt, options.writableRequiredRoots);
169
+ return syncExistingWritableCheckout(parsed, resolved, options.writableRoot, syncedAt, options.writableRequiredRoots, options.credential);
167
170
  }
168
171
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheDir();
169
172
  const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, options?.writable ? "writable" : resolved.resolvedRevision);
@@ -178,7 +181,7 @@ async function doSyncGit(parsed, options) {
178
181
  if (installRoot !== provisionalBundleRoot) {
179
182
  throw new UsageError("Writable Git installs do not support akm.include (package.json) filtered snapshots.");
180
183
  }
181
- return syncExistingWritableCheckout(parsed, resolved, detectStashRoot(installRoot), syncedAt, options.writableRequiredRoots);
184
+ return syncExistingWritableCheckout(parsed, resolved, detectStashRoot(installRoot), syncedAt, options.writableRequiredRoots, options.credential);
182
185
  }
183
186
  try {
184
187
  if (options?.force) {
@@ -228,7 +231,10 @@ async function doSyncGit(parsed, options) {
228
231
  cloneArgs.push("--branch", parsed.requestedRef);
229
232
  }
230
233
  cloneArgs.push(parsed.url, cloneDir);
231
- const cloneResult = runGit(cloneArgs, { timeout: 120_000 });
234
+ const cloneResult = runGit(cloneArgs, {
235
+ timeout: 120_000,
236
+ env: gitCredentialEnvironment(options?.credential),
237
+ });
232
238
  if (cloneResult.status !== 0) {
233
239
  throw new Error(classifyCloneFailure(parsed.url, cloneResult.stderr, cloneResult.error));
234
240
  }
@@ -292,7 +298,7 @@ async function doSyncGit(parsed, options) {
292
298
  syncedAt,
293
299
  };
294
300
  }
295
- export function syncExistingWritableCheckout(parsed, resolved, contentRoot, syncedAt, requiredRoots = []) {
301
+ export function syncExistingWritableCheckout(parsed, resolved, contentRoot, syncedAt, requiredRoots = [], credential) {
296
302
  const root = path.resolve(contentRoot);
297
303
  const repoResult = runGit(["-C", root, "rev-parse", "--show-toplevel"]);
298
304
  if (repoResult.status !== 0 || !repoResult.stdout.trim()) {
@@ -324,7 +330,7 @@ export function syncExistingWritableCheckout(parsed, resolved, contentRoot, sync
324
330
  fetchArgs.push("origin");
325
331
  if (parsed.requestedRef)
326
332
  fetchArgs.push(parsed.requestedRef);
327
- const fetch = runGit(fetchArgs, { timeout: 120_000 });
333
+ const fetch = runGit(fetchArgs, { timeout: 120_000, env: gitCredentialEnvironment(credential) });
328
334
  if (fetch.status !== 0) {
329
335
  throw new UsageError(`Writable Git install at ${root} could not fetch its expected origin; local work was preserved. ${fetch.stderr.trim()}`);
330
336
  }
@@ -403,7 +409,7 @@ export function syncExistingWritableCheckout(parsed, resolved, contentRoot, sync
403
409
  syncedAt,
404
410
  };
405
411
  }
406
- export function cloneRepo(cloneUrl, ref, destDir, writable = false) {
412
+ export function cloneRepo(cloneUrl, ref, destDir, writable = false, credential) {
407
413
  // Stage the clone into a sibling temp dir so that a failed clone never
408
414
  // destroys a previously-valid destDir (e.g. when the remote is temporarily
409
415
  // unreachable and we have a valid cached copy).
@@ -414,7 +420,7 @@ export function cloneRepo(cloneUrl, ref, destDir, writable = false) {
414
420
  if (ref)
415
421
  args.push("--branch", ref);
416
422
  args.push(cloneUrl, tmpDir);
417
- const result = runGit(args, { timeout: 120_000 });
423
+ const result = runGit(args, { timeout: 120_000, env: gitCredentialEnvironment(credential) });
418
424
  if (result.status !== 0) {
419
425
  // Clean up the (possibly partial) temp dir but leave destDir untouched.
420
426
  fs.rmSync(tmpDir, { recursive: true, force: true });
@@ -6,6 +6,7 @@ import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { akmAdapter } from "../../core/adapter/adapters/akm-adapter.js";
8
8
  import { stashDirNames } from "../../core/asset/asset-placement.js";
9
+ import { resolveSecret } from "../../core/config/config.js";
9
10
  import { ConfigError, UsageError } from "../../core/errors.js";
10
11
  import { getRegistryIndexCacheDir } from "../../core/paths.js";
11
12
  import { validateGitUrl } from "../../registry/resolve.js";
@@ -41,7 +42,12 @@ export class GitSourceProvider {
41
42
  return this.#path;
42
43
  }
43
44
  async sync(options) {
44
- await syncMirroredRepo(this.#config, { force: options?.force });
45
+ await syncMirroredRepo(this.#config, {
46
+ force: options?.force,
47
+ ...(this.#config.credential
48
+ ? { credential: resolveSecret(this.#config.credential, options?.secrets?.resolveSecret) }
49
+ : {}),
50
+ });
45
51
  }
46
52
  }
47
53
  /** Resolve the on-disk content directory for a configured git source. */
@@ -81,10 +87,10 @@ export async function ensureGitMirror(repo, cachePaths, options) {
81
87
  fs.mkdirSync(cachePaths.rootDir, { recursive: true });
82
88
  if (writable && fs.existsSync(path.join(cachePaths.repoDir, ".git"))) {
83
89
  // Writable repo already cloned — pull instead of re-clone to preserve local changes
84
- pullRepo(cachePaths.repoDir);
90
+ pullRepo(cachePaths.repoDir, options?.credential);
85
91
  }
86
92
  else {
87
- cloneRepo(repo.cloneUrl, repo.ref, cachePaths.repoDir, writable);
93
+ cloneRepo(repo.cloneUrl, repo.ref, cachePaths.repoDir, writable, options?.credential);
88
94
  }
89
95
  // Touch index file to track freshness
90
96
  fs.writeFileSync(cachePaths.indexPath, "[]", { encoding: "utf8", mode: 0o600 });
@@ -106,6 +112,7 @@ export async function syncMirroredRepo(config, options) {
106
112
  requireRepoDir: true,
107
113
  writable: options?.writable ?? config.writable === true,
108
114
  force: options?.force,
115
+ credential: options?.credential,
109
116
  });
110
117
  const syncedAt = (options?.now ?? new Date()).toISOString();
111
118
  const contentDir = cachePaths.repoDir;
@@ -121,12 +128,12 @@ export async function syncMirroredRepo(config, options) {
121
128
  syncedAt,
122
129
  };
123
130
  }
124
- function pullRepo(repoDir) {
131
+ function pullRepo(repoDir, credential) {
125
132
  const status = runGit(["-C", repoDir, "status", "--porcelain"]);
126
133
  if (status.status !== 0 || status.stdout.trim()) {
127
134
  throw new UsageError(`Writable Git source at ${repoDir} has uncommitted changes; refusing to update it.`);
128
135
  }
129
- const relation = inspectGitUpstream(repoDir);
136
+ const relation = inspectGitUpstream(repoDir, credential);
130
137
  if (relation.behind > 0 && relation.upstream) {
131
138
  if (relation.ahead > 0) {
132
139
  throw new UsageError(`Writable Git source at ${repoDir} has unpushed commits; refusing to update it.`);
@@ -120,30 +120,44 @@ export function saveGitStash(name, message, writableOverride, options) {
120
120
  let managedContentRoot;
121
121
  if (name) {
122
122
  const config = loadConfig();
123
- const stash = findGitStashByTarget(getSources(config), name);
123
+ const stash = findSyncStashByTarget(getSources(config), name);
124
124
  // NotFoundError (exit 1), not UsageError (exit 2): the argument is
125
125
  // well-formed, the bundle just isn't configured.
126
126
  if (!stash)
127
- throw new NotFoundError(`No git bundle found with name "${name}"`, "SOURCE_NOT_FOUND");
128
- if (stash.type !== "git") {
129
- throw new UsageError(`Stash "${name}" is not a git stash (type: ${stash.type})`);
127
+ throw new NotFoundError(`No git-backed bundle found with name "${name}"`, "SOURCE_NOT_FOUND");
128
+ if (stash.enabled === false) {
129
+ throw new UsageError(`Bundle "${name}" is disabled and cannot be synced.`, "INVALID_FLAG_VALUE");
130
130
  }
131
- const lockedRoot = lockContentRootFor(stash.name, stash.type);
132
- if (lockedRoot) {
133
- const topLevel = runGit(["-C", lockedRoot, "rev-parse", "--show-toplevel"]);
131
+ if (stash.type === "filesystem") {
132
+ if (!stash.path)
133
+ throw new UsageError(`Filesystem bundle "${name}" has no path configured.`);
134
+ const contentRoot = path.resolve(stash.path);
135
+ const topLevel = runGit(["-C", contentRoot, "rev-parse", "--show-toplevel"]);
134
136
  if (topLevel.status !== 0 || !topLevel.stdout.trim()) {
135
- throw new UsageError(`Managed Git stash "${name}" is not a checkout at ${lockedRoot}`);
137
+ throw new UsageError(`Filesystem bundle "${name}" is not a Git working tree at ${contentRoot}.`);
136
138
  }
137
139
  repoDir = path.resolve(topLevel.stdout.trim());
138
- managedContentRoot = path.resolve(lockedRoot);
140
+ managedContentRoot = contentRoot;
141
+ writable = stash.writable !== false;
139
142
  }
140
143
  else {
141
- if (!stash.url)
142
- throw new UsageError(`Stash "${name}" has no URL configured`);
143
- const repo = parseGitRepoUrl(stash.url);
144
- repoDir = getCachePaths(repo.canonicalUrl).repoDir;
144
+ const lockedRoot = lockContentRootFor(stash.name, stash.type);
145
+ if (lockedRoot) {
146
+ const topLevel = runGit(["-C", lockedRoot, "rev-parse", "--show-toplevel"]);
147
+ if (topLevel.status !== 0 || !topLevel.stdout.trim()) {
148
+ throw new UsageError(`Managed Git stash "${name}" is not a checkout at ${lockedRoot}`);
149
+ }
150
+ repoDir = path.resolve(topLevel.stdout.trim());
151
+ managedContentRoot = path.resolve(lockedRoot);
152
+ }
153
+ else {
154
+ if (!stash.url)
155
+ throw new UsageError(`Stash "${name}" has no URL configured`);
156
+ const repo = parseGitRepoUrl(stash.url);
157
+ repoDir = getCachePaths(repo.canonicalUrl).repoDir;
158
+ }
159
+ writable = stash.writable === true;
145
160
  }
146
- writable = stash.writable === true;
147
161
  }
148
162
  else {
149
163
  // Honour an explicit primary-stash dir override (keeps the improve gate and
@@ -554,8 +568,16 @@ function createExactPathCommit(repoDir, options) {
554
568
  fs.rmSync(`${temporaryIndex}.lock`, { force: true });
555
569
  }
556
570
  }
557
- function findGitStashByTarget(stashes, target) {
558
- return stashes.find((stash) => matchesGitStashTarget(stash, target));
571
+ function findSyncStashByTarget(stashes, target) {
572
+ return stashes.find((stash) => {
573
+ if (stash.type === "git")
574
+ return matchesGitStashTarget(stash, target);
575
+ if (stash.type !== "filesystem")
576
+ return false;
577
+ if (stash.name === target || stash.path === target)
578
+ return true;
579
+ return stash.path !== undefined && path.resolve(stash.path) === path.resolve(target);
580
+ });
559
581
  }
560
582
  function matchesGitStashTarget(stash, target) {
561
583
  if (stash.type !== "git")
@@ -324,7 +324,7 @@ async function scrapeWebsiteToStash(startUrl, stashDir, options) {
324
324
  export async function fetchWebsiteMarkdownSnapshot(rawUrl, options) {
325
325
  const normalizedUrl = validateWebsiteInputUrl(rawUrl, { allowPrivateHosts: options?.allowPrivateHosts });
326
326
  const parsedUrl = new URL(normalizedUrl);
327
- const allowPrivateHosts = await resolveAllowPrivateStartHost(parsedUrl, options?.allowPrivateHosts);
327
+ const allowPrivateHosts = await resolveAllowPrivateStartHost(parsedUrl, options?.allowPrivateHosts, options?.resolveHostname);
328
328
  const stashDir = resolveFetcherStashDir(options?.stashDir);
329
329
  const context = {
330
330
  stashDir: stashDir ?? "",
@@ -524,11 +524,11 @@ async function assertStartUrlAllowedByRobots(robots, start, rawStartUrl) {
524
524
  * before it ever reaches a guard, so this never extends trust to a host the
525
525
  * fetched content merely points at.
526
526
  */
527
- async function resolveAllowPrivateStartHost(start, requested) {
527
+ async function resolveAllowPrivateStartHost(start, requested, resolveHostname) {
528
528
  if (requested)
529
529
  return true;
530
530
  try {
531
- await assertResolvedHostAllowed(start.hostname);
531
+ await assertResolvedHostAllowed(start.hostname, { resolveHostname });
532
532
  return false;
533
533
  }
534
534
  catch {
@@ -106,6 +106,106 @@ export function isVecFastPathComplete(db) {
106
106
  return false;
107
107
  }
108
108
  }
109
+ /**
110
+ * Reconcile sqlite-vec's derived mirror from the durable BLOB embeddings.
111
+ *
112
+ * This never calls an embedding provider and never mutates the BLOB table.
113
+ * The readiness flag is lowered before the first mutation and is promoted only
114
+ * after a bidirectional aggregate check proves both ID sets match exactly.
115
+ */
116
+ export function repairVecFastPath(db, embeddingDim) {
117
+ let repaired = 0;
118
+ let removedOrphans = 0;
119
+ let rejected = 0;
120
+ setVecFastPathReady(db, false);
121
+ if (!isVecAvailable(db) || !hasVecTable(db)) {
122
+ return { available: false, repaired, removedOrphans, rejected, complete: false };
123
+ }
124
+ if (!Number.isInteger(embeddingDim) || embeddingDim <= 0) {
125
+ return {
126
+ available: true,
127
+ repaired,
128
+ removedOrphans,
129
+ rejected,
130
+ complete: false,
131
+ error: `Invalid embedding dimension ${embeddingDim}.`,
132
+ };
133
+ }
134
+ try {
135
+ while (true) {
136
+ const orphanIds = db
137
+ .prepare(`
138
+ SELECT id FROM entries_vec
139
+ EXCEPT
140
+ SELECT id FROM embeddings
141
+ ORDER BY id
142
+ LIMIT ?
143
+ `)
144
+ .all(SQLITE_CHUNK_SIZE);
145
+ if (orphanIds.length === 0)
146
+ break;
147
+ db.transaction(() => {
148
+ const remove = db.prepare("DELETE FROM entries_vec WHERE id = ?");
149
+ for (const { id } of orphanIds) {
150
+ remove.run(id);
151
+ removedOrphans++;
152
+ }
153
+ })();
154
+ }
155
+ let afterId = -1;
156
+ while (true) {
157
+ const missingIds = db
158
+ .prepare(`
159
+ SELECT id FROM (
160
+ SELECT id FROM embeddings
161
+ EXCEPT
162
+ SELECT id FROM entries_vec
163
+ ) AS missing
164
+ WHERE id > ?
165
+ ORDER BY id
166
+ LIMIT ?
167
+ `)
168
+ .all(afterId, SQLITE_CHUNK_SIZE);
169
+ if (missingIds.length === 0)
170
+ break;
171
+ afterId = missingIds[missingIds.length - 1].id;
172
+ const placeholders = missingIds.map(() => "?").join(",");
173
+ const rows = db
174
+ .prepare(`SELECT id, embedding FROM embeddings WHERE id IN (${placeholders}) ORDER BY id`)
175
+ .all(...missingIds.map(({ id }) => id));
176
+ db.transaction(() => {
177
+ const insert = db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)");
178
+ for (const row of rows) {
179
+ if (row.embedding.byteLength !== embeddingDim * 4) {
180
+ rejected++;
181
+ continue;
182
+ }
183
+ try {
184
+ insert.run(row.id, Buffer.from(row.embedding));
185
+ repaired++;
186
+ }
187
+ catch {
188
+ rejected++;
189
+ }
190
+ }
191
+ })();
192
+ }
193
+ const complete = rejected === 0 && isVecFastPathComplete(db);
194
+ setVecFastPathReady(db, complete);
195
+ return { available: true, repaired, removedOrphans, rejected, complete };
196
+ }
197
+ catch (error) {
198
+ setVecFastPathReady(db, false);
199
+ return {
200
+ available: true,
201
+ repaired,
202
+ removedOrphans,
203
+ rejected,
204
+ complete: false,
205
+ error: error instanceof Error ? error.message : String(error),
206
+ };
207
+ }
208
+ }
109
209
  const vecTablePresent = new WeakMap();
110
210
  /**
111
211
  * Whether `entries_vec` exists on this connection, memoized per handle.
@@ -0,0 +1,90 @@
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 { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
5
+ import { deriveBundleId } from "../core/bundle-id.js";
6
+ import { resolveStashDir } from "../core/common.js";
7
+ import { mutateConfig } from "../core/config/config.js";
8
+ import { bundleSourceId, filesystemBundleSourceId, isBundleEnabled } from "../core/config/config-sources.js";
9
+ import { UsageError } from "../core/errors.js";
10
+ export function canonicalSchedulerActivationRef(ref) {
11
+ const parsed = parseBundleRef(ref);
12
+ if (!parsed.bundle || parsed.fragment !== undefined || bundleRefToString(parsed) !== ref) {
13
+ throw new UsageError(`Scheduler activation requires a canonical fully-qualified ref, got ${JSON.stringify(ref)}.`, "INVALID_FLAG_VALUE");
14
+ }
15
+ return ref;
16
+ }
17
+ export function schedulerActivations(config) {
18
+ return Object.freeze((config.scheduler?.enabled ?? []).map((activation) => Object.freeze({
19
+ kind: activation.kind,
20
+ ref: canonicalSchedulerActivationRef(activation.ref),
21
+ sourceId: activation.sourceId,
22
+ })));
23
+ }
24
+ export function isSchedulerRefEnabled(config, kind, ref) {
25
+ const canonicalRef = canonicalSchedulerActivationRef(ref);
26
+ const bundle = parseBundleRef(canonicalRef).bundle;
27
+ const currentSourceId = schedulerActivationSourceId(config, bundle);
28
+ if (!currentSourceId)
29
+ return false;
30
+ return schedulerActivations(config).some((activation) => activation.kind === kind && activation.ref === canonicalRef && activation.sourceId === currentSourceId);
31
+ }
32
+ /** Grants that still name the same active source installation they approved. */
33
+ export function activeSchedulerActivations(config) {
34
+ return schedulerActivations(config).filter((activation) => {
35
+ const bundle = parseBundleRef(activation.ref).bundle;
36
+ return activation.sourceId === schedulerActivationSourceId(config, bundle);
37
+ });
38
+ }
39
+ /** Pure lifecycle helper used when a bundle is removed or replaced. */
40
+ export function revokeSchedulerActivationsForBundle(config, bundleId) {
41
+ const enabled = schedulerActivations(config).filter((activation) => parseBundleRef(activation.ref).bundle !== bundleId);
42
+ if (enabled.length === (config.scheduler?.enabled ?? []).length)
43
+ return config;
44
+ return { ...config, scheduler: { ...config.scheduler, enabled } };
45
+ }
46
+ export function setSchedulerRefEnabled(kind, ref, enabled) {
47
+ let canonicalRef = ref;
48
+ const result = mutateConfig((current) => {
49
+ canonicalRef = canonicalSchedulerActivationRef(ref);
50
+ const existing = schedulerActivations(current);
51
+ const bundle = parseBundleRef(canonicalRef).bundle;
52
+ const sourceId = enabled ? schedulerActivationSourceId(current, bundle) : undefined;
53
+ if (enabled && !sourceId) {
54
+ throw new UsageError(`Cannot enable scheduled execution from inactive or unconfigured bundle ${JSON.stringify(bundle)}.`, "INVALID_FLAG_VALUE");
55
+ }
56
+ const present = existing.some((activation) => activation.kind === kind && activation.ref === canonicalRef && (!enabled || activation.sourceId === sourceId));
57
+ if (present === enabled)
58
+ return current;
59
+ const next = enabled
60
+ ? [
61
+ ...existing.filter((activation) => activation.kind !== kind || activation.ref !== canonicalRef),
62
+ { kind, ref: canonicalRef, sourceId: sourceId },
63
+ ]
64
+ : existing.filter((activation) => activation.kind !== kind || activation.ref !== canonicalRef);
65
+ next.sort((left, right) => left.ref.localeCompare(right.ref) || left.kind.localeCompare(right.kind));
66
+ return {
67
+ ...current,
68
+ scheduler: {
69
+ ...current.scheduler,
70
+ enabled: next,
71
+ },
72
+ };
73
+ });
74
+ return { config: result.config, changed: result.written, ref: canonicalRef };
75
+ }
76
+ /** Current active source identity for a configured or environment-only bundle. */
77
+ export function schedulerActivationSourceId(config, bundleId) {
78
+ if (isBundleEnabled(config, bundleId))
79
+ return bundleSourceId(config, bundleId);
80
+ if (config.bundles?.[bundleId] !== undefined || !process.env.AKM_BUNDLE_DIR?.trim())
81
+ return undefined;
82
+ try {
83
+ const root = resolveStashDir();
84
+ const implicitId = deriveBundleId(undefined, root, new Set(Object.keys(config.bundles ?? {})));
85
+ return implicitId === bundleId ? filesystemBundleSourceId(root) : undefined;
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
@@ -41,6 +41,7 @@ import { nodeFs, throwIfNotOk } from "./exec-utils.js";
41
41
  const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
42
42
  const END = (id) => `# akm:task ${assertCronValue(id)} END`;
43
43
  const DISABLED_PREFIX = "# akm:disabled ";
44
+ const SOURCE_ENABLED_PREFIX = "# akm:source-enabled ";
44
45
  const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
45
46
  const BLOCK_END_RE = /^# akm:task ([\w.@:_-]+) END$/;
46
47
  export const PORTABLE_CRON_LINE_LIMIT = 1000;
@@ -216,6 +217,7 @@ function inspectCronState(crontab, fallbackContextPath) {
216
217
  continue;
217
218
  const ref = {
218
219
  id: schedulerLogicalBindingId(id, parsed.invocation),
220
+ enabled: !cronIsDisabled(body),
219
221
  signature: fingerprint,
220
222
  ...(parsed.target !== undefined ? { target: parsed.target } : {}),
221
223
  binding: parsed.binding,
@@ -438,9 +440,16 @@ function normalizeSignature(body) {
438
440
  return body
439
441
  .split(/\r?\n/)
440
442
  .map((l) => l.trim())
443
+ .filter((l) => !stripDisabledPrefix(l).startsWith(SOURCE_ENABLED_PREFIX))
441
444
  .filter((l) => l.length > 0)
442
445
  .join("\n");
443
446
  }
447
+ function stripDisabledPrefix(line) {
448
+ return line.startsWith(DISABLED_PREFIX) ? line.slice(DISABLED_PREFIX.length) : line;
449
+ }
450
+ function cronIsDisabled(body) {
451
+ return body.trimStart().startsWith(DISABLED_PREFIX);
452
+ }
444
453
  export function upsertBlock(existing, id, block) {
445
454
  const trimmed = existing.replace(/\s+$/g, "");
446
455
  const removed = removeBlock(trimmed, id);
@@ -557,6 +557,7 @@ function inspectStableLaunchdNamespace(seedIds, context) {
557
557
  continue;
558
558
  installed.push(withInstalledInvocation({
559
559
  id: schedulerLogicalBindingId(entry.nativeId, parsed.invocation),
560
+ enabled: entry.enabled && entry.loaded,
560
561
  ...(entry.loaded ? { signature: entry.artifact.fingerprint } : {}),
561
562
  ...(parsed.target !== undefined ? { target: parsed.target } : {}),
562
563
  binding: parsed.binding,
@@ -257,8 +257,10 @@ function inspectSchtasksState(exec, folder, taskName) {
257
257
  const parsed = extractSchtasksInvocation(query.stdout);
258
258
  if (!parsed)
259
259
  continue;
260
+ const enabled = taskXmlEnabled(query.stdout);
260
261
  const ref = {
261
262
  id: schedulerLogicalBindingId(nativeId, parsed.invocation),
263
+ ...(enabled !== undefined ? { enabled } : {}),
262
264
  ...(artifact.fingerprint !== undefined ? { signature: artifact.fingerprint } : {}),
263
265
  ...(parsed.target !== undefined ? { target: parsed.target } : {}),
264
266
  binding: parsed.binding,