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
@@ -8,9 +8,9 @@ import { UsageError } from "../../core/errors.js";
8
8
  import { appendEvent } from "../../core/events.js";
9
9
  import { warn } from "../../core/warn.js";
10
10
  import { auditStashForDangerousKeys } from "./dangerous-env-audit.js";
11
- import { akmRemove } from "./installed-stashes.js";
11
+ import { akmRemove, akmUpdate } from "./installed-stashes.js";
12
12
  import { akmAdd } from "./source-add.js";
13
- import { addStash } from "./source-manage.js";
13
+ import { addStash, removeStash } from "./source-manage.js";
14
14
  // ── Shared website-options helper ──────────
15
15
  export function buildWebsiteOptions(args) {
16
16
  // getStringArg maps absent/blank to undefined; parsePositiveIntFlag maps
@@ -86,28 +86,41 @@ export const addCommand = defineJsonCommand({
86
86
  description: "Mark a git bundle as writable so changes can be pushed back",
87
87
  default: false,
88
88
  },
89
+ before: { type: "string", description: "Insert the new bundle before this configured bundle" },
90
+ after: { type: "string", description: "Insert the new bundle after this configured bundle" },
91
+ credential: {
92
+ type: "string",
93
+ description: "Git HTTPS bearer credential reference ($VAR or secret://name); never a literal token",
94
+ },
89
95
  "max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
90
96
  "max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
91
- "allow-insecure": {
97
+ "allow-insecure-transport": {
98
+ type: "boolean",
99
+ description: "Allow a plain HTTP source URL after explicitly accepting transport substitution risk.",
100
+ default: false,
101
+ },
102
+ "allow-dangerous-env-keys": {
92
103
  type: "boolean",
93
- description: "Allow a plain HTTP source URL and skip confirmation for dangerous env keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the bundle.",
104
+ description: "Allow reviewed process-hijacking env keys (e.g. LD_PRELOAD, PATH) in the installed bundle.",
94
105
  default: false,
95
106
  },
96
107
  },
97
108
  async run({ args }) {
98
109
  const ref = args.ref.trim();
99
- const allowInsecure = args["allow-insecure"];
100
- const allowDangerousKeys = allowInsecure;
110
+ const allowInsecureTransport = args["allow-insecure-transport"];
111
+ const allowDangerousKeys = args["allow-dangerous-env-keys"];
112
+ if (args.before && args.after)
113
+ throw new UsageError("Only one of --before or --after may be used.");
101
114
  // --provider → declarative bundle source (URL for git/website; bare
102
- // package spec for npm — R-013). Config-only write; content is not
103
- // synced until a later `akm update`.
115
+ // package spec for npm — R-013). Git is materialized below before success;
116
+ // other providers remain declarative until bundle update.
104
117
  if (args.provider) {
105
118
  if (shouldWarnOnPlainHttp(ref)) {
106
- if (!allowInsecure) {
119
+ if (!allowInsecureTransport) {
107
120
  throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
108
- "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
121
+ "Use https:// or pass --allow-insecure-transport if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure-transport` only after confirming the URL is trusted.");
109
122
  }
110
- warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
123
+ warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure-transport was set; an on-path attacker could substitute a malicious payload.");
111
124
  }
112
125
  let parsedOptions;
113
126
  if (args.options) {
@@ -130,7 +143,34 @@ export const addCommand = defineJsonCommand({
130
143
  providerType: args.provider,
131
144
  options: parsedOptions,
132
145
  writable: args.writable,
146
+ before: args.before,
147
+ after: args.after,
148
+ credential: args.credential,
133
149
  });
150
+ // Declarative git sources must be materialized and indexed before add
151
+ // reports success. This also makes --provider git behave consistently
152
+ // with inferred/registry git installs (#968/#970).
153
+ if (args.provider === "git" && result.added && result.entry?.name) {
154
+ let updated;
155
+ try {
156
+ updated = await akmUpdate({ target: result.entry.name, allowDangerousEnvKeys: allowDangerousKeys });
157
+ }
158
+ catch (error) {
159
+ removeStash(result.entry.name);
160
+ throw error;
161
+ }
162
+ appendEvent({
163
+ eventType: "add",
164
+ metadata: {
165
+ target: ref,
166
+ provider: args.provider,
167
+ name: args.name ?? null,
168
+ writable: args.writable === true,
169
+ },
170
+ });
171
+ output("add", { ...result, index: updated.index });
172
+ return;
173
+ }
134
174
  appendEvent({
135
175
  eventType: "add",
136
176
  metadata: { target: ref, provider: args.provider, name: args.name ?? null, writable: args.writable === true },
@@ -139,11 +179,11 @@ export const addCommand = defineJsonCommand({
139
179
  return;
140
180
  }
141
181
  if (shouldWarnOnPlainHttp(ref)) {
142
- if (!allowInsecure) {
182
+ if (!allowInsecureTransport) {
143
183
  throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
144
- "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
184
+ "Use https:// or pass --allow-insecure-transport if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure-transport` only after confirming the URL is trusted.");
145
185
  }
146
- warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
186
+ warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure-transport was set; an on-path attacker could substitute a malicious payload.");
147
187
  }
148
188
  const websiteOptions = buildWebsiteOptions(args);
149
189
  const result = await akmAdd({
@@ -152,6 +192,9 @@ export const addCommand = defineJsonCommand({
152
192
  options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
153
193
  writable: args.writable,
154
194
  adapter: args.adapter,
195
+ before: args.before,
196
+ after: args.after,
197
+ credential: args.credential,
155
198
  });
156
199
  appendEvent({
157
200
  eventType: "add",
@@ -165,8 +208,8 @@ export const addCommand = defineJsonCommand({
165
208
  // Resolve the stash root from the install result and scan any env files
166
209
  // for dangerous env var keys. When findings are present the install is
167
210
  // gated: TTY → interactive confirmation prompt; non-TTY without
168
- // --allow-insecure → hard failure (exit 1). Pass
169
- // --allow-insecure to skip the prompt non-interactively.
211
+ // --allow-dangerous-env-keys → hard failure (exit 1). Pass the dedicated
212
+ // flag to skip the prompt non-interactively.
170
213
  const installedStashRoot = result.installed?.stashRoot ??
171
214
  (result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
172
215
  if (installedStashRoot) {
@@ -24,8 +24,9 @@
24
24
  * `proposal-new` shape). `show` is a new shape, `bundle-show`.
25
25
  */
26
26
  import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
27
- import { NotFoundError, UsageError } from "../../core/errors.js";
27
+ import { NotFoundError, TransientError, UsageError } from "../../core/errors.js";
28
28
  import { appendEvent } from "../../core/events.js";
29
+ import { warn } from "../../core/warn.js";
29
30
  import { addCommand } from "./add-cli.js";
30
31
  import { akmInit } from "./init.js";
31
32
  import { akmListSources, akmRemove, akmUpdate } from "./installed-stashes.js";
@@ -118,12 +119,20 @@ const removeCommand = defineJsonCommand({
118
119
  },
119
120
  });
120
121
  const updateCommand = defineJsonCommand({
121
- meta: { name: "update", description: "Refresh one or all configured bundles and reconcile their index" },
122
+ meta: {
123
+ name: "update",
124
+ description: "Refresh one or all configured bundles and reconcile their index. Git refresh is explicit; schedule this command for automatic updates.",
125
+ },
122
126
  args: {
123
127
  target: { type: "positional", description: "Bundle to update (id or ref)", required: false },
124
128
  all: { type: "boolean", description: "Update all configured bundles and report each outcome", default: false },
125
129
  force: { type: "boolean", description: "Force fresh download even if version is unchanged", default: false },
126
- "allow-insecure": {
130
+ "skip-if-locked": {
131
+ type: "boolean",
132
+ description: "Exit successfully when index/state database contention shows another akm operation is active",
133
+ default: false,
134
+ },
135
+ "allow-dangerous-env-keys": {
127
136
  type: "boolean",
128
137
  description: "Allow an update containing dangerous env keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the staged bundle.",
129
138
  default: false,
@@ -140,20 +149,31 @@ const updateCommand = defineJsonCommand({
140
149
  },
141
150
  },
142
151
  async run({ args }) {
143
- const result = await akmUpdate({
144
- target: args.target,
145
- all: args.all,
146
- force: args.force,
147
- yes: args.yes,
148
- allowInsecure: args["allow-insecure"],
149
- });
152
+ let result;
153
+ try {
154
+ result = await akmUpdate({
155
+ target: args.target,
156
+ all: args.all,
157
+ force: args.force,
158
+ yes: args.yes,
159
+ allowDangerousEnvKeys: args["allow-dangerous-env-keys"],
160
+ });
161
+ }
162
+ catch (error) {
163
+ const skippable = isSkippableBundleUpdateLock(error);
164
+ if (!args["skip-if-locked"] || !skippable)
165
+ throw error;
166
+ warn(`[bundle update] ${error.message}; skipping (--skip-if-locked)`);
167
+ output("update", { ok: true, skipped: { reason: "lock-held", code: error.code } });
168
+ return;
169
+ }
150
170
  appendEvent({
151
171
  eventType: "update",
152
172
  metadata: {
153
173
  target: args.target ?? null,
154
174
  all: args.all === true,
155
175
  force: args.force === true,
156
- allowInsecure: args["allow-insecure"] === true,
176
+ allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
157
177
  processed: Array.isArray(result.processed)
158
178
  ? result.processed.length
159
179
  : 0,
@@ -162,6 +182,10 @@ const updateCommand = defineJsonCommand({
162
182
  output("update", result);
163
183
  },
164
184
  });
185
+ export function isSkippableBundleUpdateLock(error) {
186
+ return (error instanceof TransientError &&
187
+ ["INDEX_DB_CONTENDED", "STATE_DB_CONTENDED", "MAINTENANCE_BARRIER_BUSY"].includes(error.code));
188
+ }
165
189
  export const bundleCommand = defineGroupCommand({
166
190
  meta: {
167
191
  name: "bundle",
@@ -14,8 +14,38 @@
14
14
  */
15
15
  import path from "node:path";
16
16
  import { bundleKeyForContentRoot, primaryBundlePath } from "../../core/config/config.js";
17
+ import { UsageError } from "../../core/errors.js";
17
18
  import { deriveBundleId } from "../../indexer/installations.js";
18
19
  export { primaryBundlePath };
20
+ /**
21
+ * Insert or move a bundle at an explicit config-map position. Object insertion
22
+ * order is bundle resolution priority after `defaultBundle`, which always
23
+ * remains first. Without a position an existing key keeps its place and a new
24
+ * key is appended.
25
+ */
26
+ export function placeBundle(bundles, key, entry, position = {}) {
27
+ if (position.before && position.after) {
28
+ throw new UsageError("Only one of --before or --after may be used.");
29
+ }
30
+ const target = position.before ?? position.after;
31
+ if (!target)
32
+ return { ...bundles, [key]: entry };
33
+ if (target === key)
34
+ throw new UsageError(`Bundle "${key}" cannot be positioned relative to itself.`);
35
+ if (!(target in bundles))
36
+ throw new UsageError(`Bundle position target "${target}" is not configured.`);
37
+ const ordered = {};
38
+ for (const [existingKey, existingEntry] of Object.entries(bundles)) {
39
+ if (existingKey === key)
40
+ continue;
41
+ if (position.before === existingKey)
42
+ ordered[key] = entry;
43
+ ordered[existingKey] = existingEntry;
44
+ if (position.after === existingKey)
45
+ ordered[key] = entry;
46
+ }
47
+ return ordered;
48
+ }
19
49
  /**
20
50
  * Upsert the primary filesystem bundle (`{ path, writable: true }`) and point
21
51
  * `defaultBundle` at it. Reuses the current default key when it already names a
@@ -85,13 +85,13 @@ export async function auditStashForDangerousKeys(opts) {
85
85
  }
86
86
  const stance = decideDangerousKeyInstall({
87
87
  findingsPresent: allFindings.length > 0,
88
- allowInsecure: opts.allowDangerousKeys,
88
+ allowDangerousEnvKeys: opts.allowDangerousKeys,
89
89
  });
90
90
  if (stance === "allow")
91
91
  return { blocked: false, findings: allFindings };
92
92
  if (stance === "warn-allow") {
93
93
  for (const finding of allFindings) {
94
- warn(`[dangerous-env-key] ${finding.relPath}: key \`${finding.keyName}\` in ${finding.envRef} can hijack process execution via \`akm env run\`. Proceeding because --allow-insecure was set.`);
94
+ warn(`[dangerous-env-key] ${finding.relPath}: key \`${finding.keyName}\` in ${finding.envRef} can hijack process execution via \`akm env run\`. Proceeding because --allow-dangerous-env-keys was set.`);
95
95
  }
96
96
  return { blocked: false, findings: allFindings };
97
97
  }
@@ -113,11 +113,11 @@ export async function auditStashForDangerousKeys(opts) {
113
113
  const confirmed = await p.confirm({ message: `${operationTitle} anyway?`, initialValue: false });
114
114
  if (!p.isCancel(confirmed) && confirmed === true)
115
115
  return { blocked: false, findings: allFindings };
116
- error = `${operationTitle} aborted: stash contains dangerous env keys. Remove the keys or re-run with --allow-insecure to bypass.`;
116
+ error = `${operationTitle} aborted: stash contains dangerous env keys. Remove the keys or re-run with --allow-dangerous-env-keys to bypass.`;
117
117
  }
118
118
  else {
119
119
  const keyList = allFindings.map((finding) => ` - ${finding.keyName} (${finding.envRef})`).join("\n");
120
- error = `${operationTitle} blocked: stash "${opts.ref}" contains dangerous env keys that can hijack process execution via \`akm env run\`:\n${keyList}\nRe-run with --allow-insecure to bypass this check after reviewing the env file.`;
120
+ error = `${operationTitle} blocked: stash "${opts.ref}" contains dangerous env keys that can hijack process execution via \`akm env run\`:\n${keyList}\nRe-run with --allow-dangerous-env-keys to bypass this check after reviewing the env file.`;
121
121
  }
122
122
  const rollbackWarning = await opts.rollback?.();
123
123
  if (opts.renderBlockedError !== false) {
@@ -18,7 +18,7 @@ import fs from "node:fs";
18
18
  import path from "node:path";
19
19
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
20
20
  import { isWithin, resolveStashDir } from "../../core/common.js";
21
- import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig } from "../../core/config/config.js";
21
+ import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig, resolveSecret, } from "../../core/config/config.js";
22
22
  import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
23
23
  import { isPathAbsent } from "../../core/path-access.js";
24
24
  import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
@@ -106,6 +106,7 @@ function listManagedInstalls(config) {
106
106
  componentRoot,
107
107
  requiredRoots: lock.localRoot ? [path.resolve(lock.localRoot, componentRoot)] : [],
108
108
  auditConfigGeneration: bundleAuditGeneration(bundle),
109
+ ...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
109
110
  });
110
111
  }
111
112
  return out;
@@ -641,12 +642,12 @@ async function verifyPublishedAudit(prepared, id) {
641
642
  throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
642
643
  }
643
644
  }
644
- async function auditPreparedUpdate(prepared, id, ref, allowInsecure) {
645
+ async function auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys) {
645
646
  updateTransactionHook("staged", id);
646
647
  const decision = await auditStashForDangerousKeys({
647
648
  stashRoot: prepared.auditRoot,
648
649
  ref,
649
- allowDangerousKeys: allowInsecure,
650
+ allowDangerousKeys: allowDangerousEnvKeys,
650
651
  isTTY: process.stdin.isTTY === true,
651
652
  operation: "update",
652
653
  renderBlockedError: false,
@@ -713,6 +714,7 @@ function prepareWritableManagedUpdate(managed, force) {
713
714
  writable: true,
714
715
  writableRoot: stagedContentRoot,
715
716
  ...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
717
+ ...(managed.credential ? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) } : {}),
716
718
  })
717
719
  .then((staged) => {
718
720
  const auditedTargetHead = gitHead(stagedRepo);
@@ -759,6 +761,9 @@ async function prepareManagedUpdate(managed, force) {
759
761
  force,
760
762
  writable: false,
761
763
  cacheRootDir: stagingParent,
764
+ ...(managed.credential
765
+ ? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) }
766
+ : {}),
762
767
  });
763
768
  if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
764
769
  // Test/provider seams may return an already-isolated candidate. It remains
@@ -795,9 +800,9 @@ function resolveComponentAuditRoot(contentRoot, componentRoot, id) {
795
800
  }
796
801
  return resolved;
797
802
  }
798
- async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, full, expectedConfigGeneration) {
803
+ async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, full, expectedConfigGeneration) {
799
804
  try {
800
- await auditPreparedUpdate(prepared, id, ref, allowInsecure);
805
+ await auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys);
801
806
  return await withAssetMutationLease("source-update", async () => {
802
807
  let transaction;
803
808
  let releaseConfigFence;
@@ -871,14 +876,18 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
871
876
  const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
872
877
  const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
873
878
  try {
874
- const expectedOldHead = writable ? gitHead(livePaths.repoDir) : undefined;
875
- if (writable && fs.existsSync(livePaths.rootDir)) {
879
+ const hasLiveCheckout = writable && fs.existsSync(livePaths.repoDir);
880
+ const expectedOldHead = hasLiveCheckout ? gitHead(livePaths.repoDir) : undefined;
881
+ if (hasLiveCheckout) {
876
882
  fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
877
883
  }
878
884
  const staged = await syncMirroredRepo(gitSource, {
879
885
  force: true,
880
886
  writable,
881
887
  cacheRootDir: stagingParent,
888
+ ...(gitSource.credential
889
+ ? { credential: resolveSecret(gitSource.credential, storeSecretResolver.resolveSecret) }
890
+ : {}),
882
891
  });
883
892
  const configuredStagedAuditRoot = resolveComponentAuditRoot(resolveGitContentRoot(staged.contentDir), componentRoot, gitSource.name ?? "git");
884
893
  if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
@@ -901,7 +910,13 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
901
910
  const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
902
911
  auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
903
912
  publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
904
- publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
913
+ if (hasLiveCheckout) {
914
+ publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
915
+ }
916
+ else {
917
+ const physicalCacheParent = canonicalWritablePath(path.dirname(livePaths.rootDir), "plain Git cache parent");
918
+ publishedAuditContainmentRoot = path.join(physicalCacheParent, path.basename(livePaths.rootDir), path.relative(livePaths.rootDir, livePaths.repoDir));
919
+ }
905
920
  publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
906
921
  }
907
922
  return {
@@ -909,7 +924,7 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
909
924
  ...(publishedAuditRoot ? { publishedAuditRoot } : {}),
910
925
  ...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
911
926
  ...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
912
- publication: writable
927
+ publication: writable && hasLiveCheckout
913
928
  ? writableGitPublication({
914
929
  stagedRepo: stagedPaths.repoDir,
915
930
  liveRepo: livePaths.repoDir,
@@ -924,14 +939,14 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
924
939
  throw error;
925
940
  }
926
941
  }
927
- async function syncGitPlainSource(gitSource, stashDir, allowInsecure) {
942
+ async function syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys) {
928
943
  const id = gitSource.name ?? gitSource.url ?? "";
929
944
  const currentConfig = loadConfig();
930
945
  const currentSource = currentPlainSource(currentConfig, id, "git");
931
946
  const ref = currentSource.url ?? "";
932
947
  const fence = bundleAuditFence(currentConfig, id);
933
948
  const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
934
- const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
949
+ const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
935
950
  return { item: { id, kind: "git", ref }, index };
936
951
  }
937
952
  async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
@@ -962,24 +977,24 @@ async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
962
977
  throw error;
963
978
  }
964
979
  }
965
- async function syncWebsitePlainSource(websiteSource, stashDir, allowInsecure) {
980
+ async function syncWebsitePlainSource(websiteSource, stashDir, allowDangerousEnvKeys) {
966
981
  const id = websiteSource.name ?? websiteSource.url ?? "";
967
982
  const currentConfig = loadConfig();
968
983
  const currentSource = currentPlainSource(currentConfig, id, "website");
969
984
  const ref = currentSource.url ?? "";
970
985
  const fence = bundleAuditFence(currentConfig, id);
971
986
  const prepared = await prepareWebsitePlainUpdate(currentSource, fence.componentRoot);
972
- const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
987
+ const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
973
988
  return { item: { id, kind: "website", ref }, index };
974
989
  }
975
990
  /** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
976
- async function updateGitSource(stashDir, target, all, gitSource, allowInsecure) {
977
- const synced = await syncGitPlainSource(gitSource, stashDir, allowInsecure);
991
+ async function updateGitSource(stashDir, target, all, gitSource, allowDangerousEnvKeys) {
992
+ const synced = await syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys);
978
993
  return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
979
994
  }
980
995
  /** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
981
- async function updateWebsiteSource(stashDir, target, all, websiteSource, allowInsecure) {
982
- const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowInsecure);
996
+ async function updateWebsiteSource(stashDir, target, all, websiteSource, allowDangerousEnvKeys) {
997
+ const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowDangerousEnvKeys);
983
998
  return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
984
999
  }
985
1000
  /** Reconcile a filesystem source's current bytes without provider hydration. */
@@ -1042,7 +1057,7 @@ function managedInstallViewOfPlainNpm(npmSource) {
1042
1057
  * scripts; only the branch that can `rm -rf` needs a gate, not the whole
1043
1058
  * command).
1044
1059
  */
1045
- async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure) {
1060
+ async function updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys) {
1046
1061
  // No pre-cleanup of the old root, even under --force: the providers already
1047
1062
  // re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
1048
1063
  // only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
@@ -1051,7 +1066,7 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure
1051
1066
  // config/lock publication and reindexing both succeed.
1052
1067
  const prepared = await prepareManagedUpdate(managed, force);
1053
1068
  try {
1054
- await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowInsecure);
1069
+ await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowDangerousEnvKeys);
1055
1070
  const synced = prepared.synced;
1056
1071
  const installedEntry = {
1057
1072
  id: managed.installId,
@@ -1259,14 +1274,14 @@ export async function akmUpdate(input) {
1259
1274
  const all = input?.all === true;
1260
1275
  const force = input?.force === true;
1261
1276
  const yes = input?.yes === true;
1262
- const allowInsecure = input?.allowInsecure === true;
1277
+ const allowDangerousEnvKeys = input?.allowDangerousEnvKeys === true;
1263
1278
  const config = loadConfig();
1264
1279
  const managedInstalls = listManagedInstalls(config);
1265
1280
  if (target && !all) {
1266
1281
  // Registry-managed install (lock-backed) — re-download from its locator.
1267
1282
  const managed = resolveManagedTarget(config, target);
1268
1283
  if (managed) {
1269
- const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
1284
+ const updated = await updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys);
1270
1285
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
1271
1286
  }
1272
1287
  // Plain git / website source (bundles without a lock) — provider re-sync.
@@ -1295,7 +1310,7 @@ export async function akmUpdate(input) {
1295
1310
  return false;
1296
1311
  });
1297
1312
  if (gitMatch)
1298
- return updateGitSource(stashDir, target, all, gitMatch, allowInsecure);
1313
+ return updateGitSource(stashDir, target, all, gitMatch, allowDangerousEnvKeys);
1299
1314
  const websiteMatch = stashes.find((s) => {
1300
1315
  if (s.type !== "website")
1301
1316
  return false;
@@ -1308,7 +1323,7 @@ export async function akmUpdate(input) {
1308
1323
  return false;
1309
1324
  });
1310
1325
  if (websiteMatch)
1311
- return updateWebsiteSource(stashDir, target, all, websiteMatch, allowInsecure);
1326
+ return updateWebsiteSource(stashDir, target, all, websiteMatch, allowDangerousEnvKeys);
1312
1327
  // Plain npm source (bundle without a lock) — sync via the registry
1313
1328
  // pipeline and promote to a managed install (see
1314
1329
  // managedInstallViewOfPlainNpm's doc comment for why npm can't stay
@@ -1323,7 +1338,7 @@ export async function akmUpdate(input) {
1323
1338
  return false;
1324
1339
  });
1325
1340
  if (npmMatch) {
1326
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowInsecure);
1341
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
1327
1342
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
1328
1343
  }
1329
1344
  const filesystemMatch = stashes.find((source) => {
@@ -1346,7 +1361,7 @@ export async function akmUpdate(input) {
1346
1361
  let latestIndex;
1347
1362
  for (const managed of selected) {
1348
1363
  try {
1349
- const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
1364
+ const updated = await updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys);
1350
1365
  processed.push(updated.item);
1351
1366
  latestIndex = updated.index;
1352
1367
  }
@@ -1371,17 +1386,17 @@ export async function akmUpdate(input) {
1371
1386
  const id = plain.name ?? plain.path ?? plain.url ?? "";
1372
1387
  try {
1373
1388
  if (plain.type === "git") {
1374
- const updated = await syncGitPlainSource(plain, stashDir, allowInsecure);
1389
+ const updated = await syncGitPlainSource(plain, stashDir, allowDangerousEnvKeys);
1375
1390
  plainSynced.push(updated.item);
1376
1391
  latestIndex = updated.index;
1377
1392
  }
1378
1393
  else if (plain.type === "npm") {
1379
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowInsecure);
1394
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowDangerousEnvKeys);
1380
1395
  processed.push(updated.item);
1381
1396
  latestIndex = updated.index;
1382
1397
  }
1383
1398
  else if (plain.type === "website") {
1384
- const updated = await syncWebsitePlainSource(plain, stashDir, allowInsecure);
1399
+ const updated = await syncWebsitePlainSource(plain, stashDir, allowDangerousEnvKeys);
1385
1400
  plainSynced.push(updated.item);
1386
1401
  latestIndex = updated.index;
1387
1402
  }