akm-cli 0.10.261012-beta → 0.10.261013-beta
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.
- package/CHANGELOG.md +216 -0
- package/SECURITY.md +2 -2
- package/STABILITY.md +2 -2
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/hints/cli-hints-short.md +3 -4
- package/dist/commands/command/execution-source-loader.js +10 -14
- package/dist/commands/env/env-binding.js +47 -58
- package/dist/commands/env/env-cli.js +20 -22
- package/dist/commands/env/env.js +10 -58
- package/dist/commands/env/secret-cli.js +9 -27
- package/dist/commands/env/secret.js +3 -10
- package/dist/commands/health/scheduler-binary.js +1 -1
- package/dist/commands/improve/distill.js +0 -2
- package/dist/commands/improve/eligibility.js +0 -2
- package/dist/commands/lint/env-key-rules.js +7 -6
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/proposal/validators/proposal-validators.js +1 -4
- package/dist/commands/read/show.js +4 -25
- package/dist/commands/sources/add-cli.js +8 -63
- package/dist/commands/sources/bundle-cli.js +8 -9
- package/dist/commands/sources/installed-stashes.js +117 -61
- package/dist/commands/sources/schema-repair.js +0 -2
- package/dist/commands/sources/source-clone.js +31 -7
- package/dist/commands/tasks/explain.js +1 -1
- package/dist/commands/tasks/tasks.js +38 -56
- package/dist/commands/ui/redact.js +4 -4
- package/dist/commands/ui/workspace-actions.js +1 -1
- package/dist/commands/workflow/plan.js +47 -8
- package/dist/commands/workflow-cli.js +37 -41
- package/dist/core/adapter/adapters/akm-adapter.js +3 -3
- package/dist/core/adapter/adapters/akm-lint.js +3 -4
- package/dist/core/adapter/adapters/akm-metadata.js +2 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
- package/dist/core/asset/resolve-ref.js +5 -0
- package/dist/core/bundle-rename.js +12 -4
- package/dist/core/common.js +4 -52
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +55 -18
- package/dist/core/config/schema/primitives.js +12 -6
- package/dist/core/config/schema/sources-bundles.js +2 -2
- package/dist/core/env-secret-ref.js +84 -73
- package/dist/core/errors.js +1 -1
- package/dist/core/paths.js +1 -6
- package/dist/core/redaction.js +16 -3
- package/dist/core/ref-token.js +82 -0
- package/dist/core/secret-value.js +14 -0
- package/dist/core/write-source.js +20 -4
- package/dist/indexer/indexer.js +27 -36
- package/dist/indexer/installations.js +1 -8
- package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
- package/dist/indexer/materialize-embeddings.js +1 -1
- package/dist/indexer/scan/drain-dir.js +3 -3
- package/dist/indexer/search/search-source.js +4 -2
- package/dist/indexer/walk/path-resolver.js +13 -68
- package/dist/indexer/walk/walker.js +18 -22
- package/dist/integrations/agent/engine-resolution.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +4 -5
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +1 -1
- package/dist/llm/embedders/remote.js +4 -3
- package/dist/output/renderers.js +3 -4
- package/dist/output/shapes/helpers.js +0 -2
- package/dist/output/text/command-format.js +12 -2
- package/dist/output/text/show-directives.js +16 -28
- package/dist/output/text/workflow-format.js +37 -7
- package/dist/scripts/akm-migrate-node.js +45865 -44692
- package/dist/scripts/akm-migrate.js +45804 -44630
- package/dist/setup/steps/tasks.js +1 -2
- package/dist/sources/providers/provider-utils.js +12 -0
- package/dist/sources/resolve.js +12 -16
- package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
- package/dist/sources/snapshot-fetchers/x.js +1 -1
- package/dist/storage/repositories/proposals-repository.js +2 -2
- package/dist/storage/repositories/task-history-repository.js +2 -2
- package/dist/storage/repositories/workflow-runs-repository.js +38 -116
- package/dist/tasks/embedded.js +1 -1
- package/dist/tasks/prepare/prepare-support.js +3 -2
- package/dist/tasks/prepare/prepare.js +3 -4
- package/dist/tasks/prepare/script-capture.js +8 -22
- package/dist/tasks/run/load-task.js +46 -14
- package/dist/tasks/run/run-native-task.js +0 -8
- package/dist/tasks/run/run-workflow-task.js +2 -2
- package/dist/tasks/scheduler-sync.js +0 -1
- package/dist/tasks/source/bounded-document.js +0 -41
- package/dist/tasks/source/parse-task-source.js +0 -1
- package/dist/tasks/source/task-source-v4.js +1 -4
- package/dist/workflows/compile.js +1 -15
- package/dist/workflows/exec/child-workflow.js +1 -2
- package/dist/workflows/exec/dispatch-redaction.js +24 -6
- package/dist/workflows/exec/environment.js +11 -74
- package/dist/workflows/exec/exec-unit.js +31 -23
- package/dist/workflows/exec/frozen-judge.js +17 -3
- package/dist/workflows/exec/native-executor.js +70 -26
- package/dist/workflows/exec/run-workflow.js +31 -18
- package/dist/workflows/exec/step-work.js +4 -3
- package/dist/workflows/exec/unit-dispatch.js +1 -8
- package/dist/workflows/freeze/environment.js +14 -119
- package/dist/workflows/freeze/freeze.js +0 -1
- package/dist/workflows/freeze/step-values.js +1 -2
- package/dist/workflows/freeze/targets/child-workflow.js +3 -5
- package/dist/workflows/freeze/targets/command.js +6 -9
- package/dist/workflows/freeze/targets/shell.js +0 -3
- package/dist/workflows/parser.js +6 -25
- package/dist/workflows/plan.js +6 -2
- package/dist/workflows/renderer.js +77 -4
- package/dist/workflows/runtime/run-plan.js +37 -23
- package/dist/workflows/runtime/runs.js +41 -84
- package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
- package/dist/workflows/source-files.js +7 -19
- package/docs/architecture/0.10-process-rules.md +6 -6
- package/docs/architecture/architecture.md +6 -6
- package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
- package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
- package/docs/architecture/internals/storage-locations.md +4 -4
- package/docs/architecture/persisted-data-compat.md +3 -2
- package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
- package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
- package/docs/architecture/testing/manual-testing-checklist.md +46 -78
- package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
- package/docs/architecture/testing/testing-workflow.md +9 -11
- package/docs/architecture/workflow-engine.md +41 -27
- package/docs/guides/author-bundles.md +14 -17
- package/docs/guides/author-workflows.md +43 -9
- package/docs/guides/bundles.md +16 -11
- package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
- package/docs/guides/run-workflows.md +25 -13
- package/docs/guides/scheduling.md +1 -1
- package/docs/maintainers/README.md +2 -3
- package/docs/maintainers/release-checklist.md +73 -126
- package/docs/migration/v0.9-to-v0.10.md +5 -1
- package/docs/reference/cli.md +124 -112
- package/docs/reference/configuration.md +16 -9
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/env-and-secrets.md +66 -15
- package/docs/reference/registry.md +6 -11
- package/docs/reference/tasks.md +11 -3
- package/docs/reference/ui-api.md +3 -3
- package/docs/reference/workflow-schema.md +109 -36
- package/package.json +1 -1
- package/schemas/akm-task.json +2 -4
- package/dist/commands/sources/dangerous-env-audit.js +0 -134
- package/dist/core/activation-policy.js +0 -67
- package/dist/execution/directory-identity.js +0 -78
- package/dist/workflows/authoring/scope-key.js +0 -79
- package/dist/workflows/source-semantics.js +0 -110
|
@@ -21,9 +21,8 @@
|
|
|
21
21
|
* document fails closed with `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming
|
|
22
22
|
* `akm migrate apply`, which converts it). That parser owns the closed key sets,
|
|
23
23
|
* the executable-selector XOR, hostile YAML policy, the `akm/command`
|
|
24
|
-
* builtin, bounds
|
|
25
|
-
*
|
|
26
|
-
* diagnostic shape.
|
|
24
|
+
* builtin, and bounds. The adapter only translates a parser failure into
|
|
25
|
+
* the format-family diagnostic shape.
|
|
27
26
|
*
|
|
28
27
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
29
28
|
* `tests/fixtures/bundles/akm-task/` + goldens
|
|
@@ -59,7 +58,7 @@ function recognize(c, file) {
|
|
|
59
58
|
content: raw,
|
|
60
59
|
};
|
|
61
60
|
}
|
|
62
|
-
async function validate(
|
|
61
|
+
async function validate(_c, changes, ctx) {
|
|
63
62
|
const diagnostics = [];
|
|
64
63
|
for (const change of changes) {
|
|
65
64
|
if (change.op === "delete")
|
|
@@ -84,7 +83,7 @@ async function validate(c, changes, ctx) {
|
|
|
84
83
|
continue;
|
|
85
84
|
}
|
|
86
85
|
try {
|
|
87
|
-
parseTaskSource({ yaml: raw, filePath: relPath
|
|
86
|
+
parseTaskSource({ yaml: raw, filePath: relPath });
|
|
88
87
|
}
|
|
89
88
|
catch (cause) {
|
|
90
89
|
diagnostics.push({
|
|
@@ -145,7 +144,7 @@ export const akmTaskAdapter = {
|
|
|
145
144
|
continue;
|
|
146
145
|
}
|
|
147
146
|
try {
|
|
148
|
-
parseTaskSource({ yaml: raw, filePath: entry.name
|
|
147
|
+
parseTaskSource({ yaml: raw, filePath: entry.name });
|
|
149
148
|
return true;
|
|
150
149
|
}
|
|
151
150
|
catch {
|
|
@@ -31,6 +31,7 @@ import fs from "node:fs";
|
|
|
31
31
|
import path from "node:path";
|
|
32
32
|
import { assetPathCandidatesForName, typeForStashDir } from "../../asset/asset-placement.js";
|
|
33
33
|
import { scanEnvKeyNames, toPosix } from "../../common.js";
|
|
34
|
+
import { sensitiveMarkerPath } from "../../sensitive-marker-path.js";
|
|
34
35
|
import { dangerousEnvKeyDiagnostics } from "./akm-lint.js";
|
|
35
36
|
import { hashContent } from "./shared.js";
|
|
36
37
|
/** A dotenv bundle is single-component; its one component is `main`. */
|
|
@@ -73,7 +74,7 @@ function classify(relPath) {
|
|
|
73
74
|
* disagreed about a documented promise.
|
|
74
75
|
*/
|
|
75
76
|
function hasSensitiveMarker(absPath, type) {
|
|
76
|
-
const marker = type === "env" ?
|
|
77
|
+
const marker = sensitiveMarkerPath(absPath, type === "env" ? "env" : "secret");
|
|
77
78
|
return marker !== absPath && fs.existsSync(marker);
|
|
78
79
|
}
|
|
79
80
|
function conceptIdForPath(type, relativePath) {
|
|
@@ -167,8 +168,8 @@ export const dotenvAdapter = {
|
|
|
167
168
|
return [];
|
|
168
169
|
const primaries = assetPathCandidatesForName(type, path.join(c.root, head), rest);
|
|
169
170
|
const expanded = primaries.flatMap((primary) => type === "env"
|
|
170
|
-
? [primary, primary
|
|
171
|
-
: [primary,
|
|
171
|
+
? [primary, sensitiveMarkerPath(primary, "env")]
|
|
172
|
+
: [primary, sensitiveMarkerPath(primary, "secret"), `${primary}.lock`]);
|
|
172
173
|
return expanded.map((candidatePath) => ({ path: candidatePath, conceptId: posix }));
|
|
173
174
|
},
|
|
174
175
|
/** The dotenv bundle owns its `env/` + `secrets/` dirs. */
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* against an injected {@link RefContext}. This is the "the short
|
|
13
13
|
* form is input sugar" rule (§11.1) made structural: only a
|
|
14
14
|
* `ResolvedRef` ever crosses a storage boundary.
|
|
15
|
+
* It answers "which bundle" against an injected index snapshot;
|
|
16
|
+
* it does not find files. "Where is the file for this ref right
|
|
17
|
+
* now" has one answer, `resolveConceptOwner`
|
|
18
|
+
* (`src/indexer/lookup/adapter-concept-owner.ts`), which
|
|
19
|
+
* every resolver that needs a path calls.
|
|
15
20
|
* - serialize — `bundleRefToString` (`asset-ref.ts`); serializing a
|
|
16
21
|
* `ResolvedRef` always emits the fully-qualified form.
|
|
17
22
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* bundle's key. A bundle id is a mass identity prefix: every durable ref this
|
|
7
7
|
* tool minted (`entries.item_ref`/`bundle_id`, `proposals.ref`, a pending
|
|
8
8
|
* proposal's `proposedTarget.source`, a workflow task_history row's
|
|
9
|
-
* `target_ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
|
|
9
|
+
* `target_ref`, a `workflow_runs.workflow_ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
|
|
10
10
|
* `bundles` key strands all of it (see `src/indexer/bundle-identity-guard.ts`
|
|
11
11
|
* §11.5). This module is the rekey transaction that guard's docstring points
|
|
12
12
|
* to.
|
|
@@ -32,6 +32,7 @@ import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from
|
|
|
32
32
|
import { getFilePathsByBundle, renameEntriesBundleId } from "../storage/repositories/index-entries-repository.js";
|
|
33
33
|
import { countProposalsForBundleRename, renameProposalsBundleRef } from "../storage/repositories/proposals-repository.js";
|
|
34
34
|
import { countTaskHistoryTargetRefs, renameTaskHistoryTargetRefs, } from "../storage/repositories/task-history-repository.js";
|
|
35
|
+
import { countWorkflowRunRefsForRename, renameWorkflowRunRefs } from "../storage/repositories/workflow-runs-repository.js";
|
|
35
36
|
import { selectBackend } from "../tasks/backends/index.js";
|
|
36
37
|
import { bundleRefToString, parseBundleRef } from "./asset/asset-ref.js";
|
|
37
38
|
import { validateExplicitBundleName } from "./bundle-id.js";
|
|
@@ -152,12 +153,17 @@ async function buildPlan(config, oldId, newId, sched) {
|
|
|
152
153
|
let proposalRefs = 0;
|
|
153
154
|
let proposalTargets = 0;
|
|
154
155
|
let taskHistoryRefs = 0;
|
|
156
|
+
let workflowRunRefs = 0;
|
|
157
|
+
let activeWorkflowRuns = [];
|
|
155
158
|
if (fs.existsSync(getStateDbPath())) {
|
|
156
159
|
withStateDb((db) => {
|
|
157
160
|
const counts = countProposalsForBundleRename(db, oldId);
|
|
158
161
|
proposalRefs = counts.refs;
|
|
159
162
|
proposalTargets = counts.targets;
|
|
160
163
|
taskHistoryRefs = countTaskHistoryTargetRefs(db, oldId);
|
|
164
|
+
const runs = countWorkflowRunRefsForRename(db, oldId);
|
|
165
|
+
workflowRunRefs = runs.refs;
|
|
166
|
+
activeWorkflowRuns = runs.active;
|
|
161
167
|
});
|
|
162
168
|
}
|
|
163
169
|
return {
|
|
@@ -170,7 +176,8 @@ async function buildPlan(config, oldId, newId, sched) {
|
|
|
170
176
|
},
|
|
171
177
|
lock: { present: lockPresent },
|
|
172
178
|
index: { entries: indexEntries },
|
|
173
|
-
state: { proposalRefs, proposalTargets, taskHistoryRefs },
|
|
179
|
+
state: { proposalRefs, proposalTargets, taskHistoryRefs, workflowRunRefs },
|
|
180
|
+
activeWorkflowRuns,
|
|
174
181
|
contentRefs,
|
|
175
182
|
nativeSchedulerRows,
|
|
176
183
|
};
|
|
@@ -254,12 +261,13 @@ export async function renameBundle(oldId, newId, options = {}, deps) {
|
|
|
254
261
|
closeDatabase(writeIndexDb);
|
|
255
262
|
}
|
|
256
263
|
}
|
|
257
|
-
// State: proposals.ref, a pending proposal's proposedTarget.source,
|
|
258
|
-
// workflow task_history.target_ref.
|
|
264
|
+
// State: proposals.ref, a pending proposal's proposedTarget.source,
|
|
265
|
+
// workflow task_history.target_ref, and workflow_runs.workflow_ref.
|
|
259
266
|
if (fs.existsSync(getStateDbPath())) {
|
|
260
267
|
withStateDb((db) => {
|
|
261
268
|
renameProposalsBundleRef(db, oldId, newId);
|
|
262
269
|
renameTaskHistoryTargetRefs(db, oldId, newId);
|
|
270
|
+
renameWorkflowRunRefs(db, oldId, newId);
|
|
263
271
|
});
|
|
264
272
|
}
|
|
265
273
|
// Scheduler: remove native rows still naming `<old>//` (see
|
package/dist/core/common.js
CHANGED
|
@@ -376,25 +376,6 @@ export function hasErrnoCode(error, code) {
|
|
|
376
376
|
export function isChmodUnsupported(error) {
|
|
377
377
|
return hasErrnoCode(error, "EPERM") || hasErrnoCode(error, "ENOTSUP") || hasErrnoCode(error, "EINVAL");
|
|
378
378
|
}
|
|
379
|
-
/**
|
|
380
|
-
* True when `value` is a RELATIVE path that cannot leave its base directory:
|
|
381
|
-
* no absolute form (POSIX `/`, Windows `\` or a `C:` drive prefix), no `~`
|
|
382
|
-
* home expansion, and no `..` segment under either separator.
|
|
383
|
-
*
|
|
384
|
-
* This is the SYNTACTIC half of containment — cheap, string-only, usable at
|
|
385
|
-
* authoring time before any directory exists. It is deliberately paired with
|
|
386
|
-
* (never a substitute for) {@link isWithin}, which resolves symlinks against a
|
|
387
|
-
* real base at use time. Workflow `exec` units run both: the parser and the
|
|
388
|
-
* frozen-plan decoder reject uncontained spellings, and the executor re-checks
|
|
389
|
-
* the resolved path before spawning.
|
|
390
|
-
*/
|
|
391
|
-
export function isContainedRelativePath(value) {
|
|
392
|
-
if (value === "" || value.startsWith("/") || value.startsWith("\\") || value.startsWith("~"))
|
|
393
|
-
return false;
|
|
394
|
-
if (/^[A-Za-z]:/.test(value))
|
|
395
|
-
return false;
|
|
396
|
-
return !value.split(/[/\\]+/).includes("..");
|
|
397
|
-
}
|
|
398
379
|
export function isWithin(candidate, root) {
|
|
399
380
|
return isContainedResolvedPath(safeRealpath(candidate), safeRealpath(root));
|
|
400
381
|
}
|
|
@@ -419,16 +400,11 @@ export function isAkmRegistryCachePath(filePath) {
|
|
|
419
400
|
return isWithin(filePath, getRegistryCacheDir()) || isWithin(filePath, getRegistryIndexCacheDir());
|
|
420
401
|
}
|
|
421
402
|
/**
|
|
422
|
-
* {@link isWithin}
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* only the realpath syscalls are awaited.
|
|
403
|
+
* The containment comparison behind {@link isWithin}.
|
|
404
|
+
* Called directly with `path.resolve`d arguments it is a lexical check: it
|
|
405
|
+
* refuses `..` and absolute names without following symlinks.
|
|
426
406
|
*/
|
|
427
|
-
export
|
|
428
|
-
return isContainedResolvedPath(await safeRealpathAsync(candidate), await safeRealpathAsync(root));
|
|
429
|
-
}
|
|
430
|
-
/** The containment comparison shared by {@link isWithin} and {@link isWithinAsync}. */
|
|
431
|
-
function isContainedResolvedPath(resolvedCandidate, resolvedRoot) {
|
|
407
|
+
export function isContainedResolvedPath(resolvedCandidate, resolvedRoot) {
|
|
432
408
|
const rel = path.relative(normalizeFsPathForComparison(resolvedRoot), normalizeFsPathForComparison(resolvedCandidate));
|
|
433
409
|
if (rel === "")
|
|
434
410
|
return true;
|
|
@@ -475,30 +451,6 @@ export function safeRealpath(p) {
|
|
|
475
451
|
}
|
|
476
452
|
}
|
|
477
453
|
}
|
|
478
|
-
/** {@link safeRealpath}'s async twin — awaited syscalls, identical walk-up. */
|
|
479
|
-
export async function safeRealpathAsync(p) {
|
|
480
|
-
const resolved = path.resolve(p);
|
|
481
|
-
try {
|
|
482
|
-
return await fs.promises.realpath(resolved);
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
const suffix = [];
|
|
486
|
-
let current = resolved;
|
|
487
|
-
for (;;) {
|
|
488
|
-
const parent = path.dirname(current);
|
|
489
|
-
if (parent === current)
|
|
490
|
-
return resolved;
|
|
491
|
-
suffix.unshift(path.basename(current));
|
|
492
|
-
current = parent;
|
|
493
|
-
try {
|
|
494
|
-
return path.join(await fs.promises.realpath(current), ...suffix);
|
|
495
|
-
}
|
|
496
|
-
catch {
|
|
497
|
-
// parent also doesn't exist; keep walking up
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
454
|
function normalizeFsPathForComparison(value) {
|
|
503
455
|
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
504
456
|
}
|
|
@@ -219,7 +219,7 @@ export function configSet(config, dotted, raw) {
|
|
|
219
219
|
? { success: true, data: value }
|
|
220
220
|
: {
|
|
221
221
|
success: false,
|
|
222
|
-
error: { issues: [{ path: [], message: "apiKey must be $VAR, ${VAR}, or
|
|
222
|
+
error: { issues: [{ path: [], message: "apiKey must be $VAR, ${VAR}, or ${secrets/<name>}" }] },
|
|
223
223
|
}
|
|
224
224
|
: isUnknownKey
|
|
225
225
|
? { success: true, data: value }
|
|
@@ -9,13 +9,14 @@ import { isBundleSlug } from "../asset/asset-ref.js";
|
|
|
9
9
|
import { deriveBundleId } from "../bundle-id.js";
|
|
10
10
|
import { isRecord, resolveStashDir } from "../common.js";
|
|
11
11
|
import { ConfigError } from "../errors.js";
|
|
12
|
+
import { isRefToken, substituteRefTokens } from "../ref-token.js";
|
|
12
13
|
import { formatRegistryLabel, hasRegistryUrlCredentials } from "../registry-url.js";
|
|
13
14
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
14
15
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
15
16
|
import { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleSourceId, bundlesToSourceEntries, filesystemBundleSourceId, isBundleEnabled, } from "./config-sources.js";
|
|
16
17
|
import { resolveSchemaAt } from "./config-walker.js";
|
|
17
18
|
import { deepMergeConfig, isPlainObject } from "./deep-merge.js";
|
|
18
|
-
import { isApiKeyReference
|
|
19
|
+
import { isApiKeyReference } from "./schema/primitives.js";
|
|
19
20
|
export { stripJsonComments } from "./config-io.js";
|
|
20
21
|
import { getConfigPath } from "../paths.js";
|
|
21
22
|
import { warn, warnOnce } from "../warn.js";
|
|
@@ -102,7 +103,43 @@ export function loadUserConfig() {
|
|
|
102
103
|
* the file that extends it.
|
|
103
104
|
*/
|
|
104
105
|
function runConfigFilePipeline(text, sourcePath) {
|
|
105
|
-
|
|
106
|
+
const raw = parseConfigText(text, sourcePath);
|
|
107
|
+
return readConfigVersion(readRetiredSecretRefs(raw, [], sourcePath), sourcePath);
|
|
108
|
+
}
|
|
109
|
+
/** The retired `secret://<name>` credential form; it survives only in {@link readRetiredSecretRefs}. */
|
|
110
|
+
const SECRET_STORE_REFERENCE_PATTERN = /^secret:\/\/(.+)$/;
|
|
111
|
+
/**
|
|
112
|
+
* The `${ref}` a retired `secret://<x>` value reads as: `${<x>}` when `<x>` is
|
|
113
|
+
* already a full ref, else `${secrets/<x>}`. `undefined` for any other string.
|
|
114
|
+
* Shared by the config reader and the frozen workflow runner decoder.
|
|
115
|
+
*/
|
|
116
|
+
export function liftRetiredSecretRef(value) {
|
|
117
|
+
const name = SECRET_STORE_REFERENCE_PATTERN.exec(value)?.[1];
|
|
118
|
+
if (name === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
return isRefToken(`\${${name}}`) ? `\${${name}}` : `\${secrets/${name}}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* `secret://<x>` is retired in favour of `${ref}`. A value in that form is read
|
|
124
|
+
* as its {@link liftRetiredSecretRef} with one warning naming the key; every
|
|
125
|
+
* ordinary config write and `akm migrate apply`'s `configFile` step then
|
|
126
|
+
* persist the new form.
|
|
127
|
+
*/
|
|
128
|
+
function readRetiredSecretRefs(node, keyPath, sourcePath) {
|
|
129
|
+
if (typeof node === "string") {
|
|
130
|
+
const lifted = liftRetiredSecretRef(node);
|
|
131
|
+
if (lifted === undefined)
|
|
132
|
+
return node;
|
|
133
|
+
const dotted = keyPath.join(".");
|
|
134
|
+
warnOnce(`config:secret-scheme:${sourcePath ?? "inline"}:${dotted}`, `${dotted}${sourcePath ? ` at ${sourcePath}` : ""} uses the retired ${JSON.stringify(node)} form; reading it as ${JSON.stringify(lifted)}. \`akm migrate apply\` rewrites config.json.`);
|
|
135
|
+
return lifted;
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(node))
|
|
138
|
+
return node.map((child, i) => readRetiredSecretRefs(child, [...keyPath, String(i)], sourcePath));
|
|
139
|
+
if (isPlainConfigObject(node)) {
|
|
140
|
+
return Object.fromEntries(Object.entries(node).map(([key, child]) => [key, readRetiredSecretRefs(child, [...keyPath, key], sourcePath)]));
|
|
141
|
+
}
|
|
142
|
+
return node;
|
|
106
143
|
}
|
|
107
144
|
/**
|
|
108
145
|
* `configVersion` is read, never gated on. `"0.9.0"` is the only value akm
|
|
@@ -812,10 +849,10 @@ export function updateConfig(partial) {
|
|
|
812
849
|
}
|
|
813
850
|
/**
|
|
814
851
|
* Resolve a single secret value: expand `${VAR}` / `$VAR` against
|
|
815
|
-
* `process.env`,
|
|
816
|
-
* at apiKey / authorization-header consumption
|
|
817
|
-
* agent SDK runner) — NOT on the load path.
|
|
818
|
-
* unchanged.
|
|
852
|
+
* `process.env`, then substitute `${ref}` tokens (`${secrets/name}`) through
|
|
853
|
+
* `resolveFromStore`. Use this at apiKey / authorization-header consumption
|
|
854
|
+
* sites (LLM client, embedder, agent SDK runner) — NOT on the load path.
|
|
855
|
+
* Non-string inputs pass through unchanged.
|
|
819
856
|
*
|
|
820
857
|
* Returns the input unchanged when no substitution markers are present, so
|
|
821
858
|
* literal API key strings (already-resolved secrets) are zero-cost.
|
|
@@ -824,26 +861,18 @@ export function updateConfig(partial) {
|
|
|
824
861
|
* preserved verbatim on read — only fields explicitly routed through this
|
|
825
862
|
* helper are expanded.
|
|
826
863
|
*
|
|
827
|
-
* A `
|
|
828
|
-
*
|
|
829
|
-
* credential.
|
|
864
|
+
* A `${ref}` that fails to resolve throws `ConfigError` (naming the ref, never
|
|
865
|
+
* the secret) rather than silently sending an unusable credential.
|
|
830
866
|
*/
|
|
831
867
|
export function resolveSecret(value, resolveFromStore) {
|
|
832
868
|
if (value === undefined)
|
|
833
869
|
return undefined;
|
|
834
870
|
if (typeof value !== "string")
|
|
835
871
|
return value;
|
|
836
|
-
const storeRef = SECRET_STORE_REFERENCE_PATTERN.exec(value)?.[1];
|
|
837
|
-
if (storeRef !== undefined) {
|
|
838
|
-
const resolved = resolveFromStore?.(storeRef) ?? null;
|
|
839
|
-
if (resolved === null) {
|
|
840
|
-
throw new ConfigError(`Secret store reference "${value}" did not resolve to a stored value.`, "SECRET_REFERENCE_UNRESOLVED");
|
|
841
|
-
}
|
|
842
|
-
return resolved;
|
|
843
|
-
}
|
|
844
872
|
if (!value.includes("$"))
|
|
845
873
|
return value;
|
|
846
|
-
|
|
874
|
+
// Env expansion runs first so a secret value containing `$` is never re-expanded.
|
|
875
|
+
const expanded = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
|
|
847
876
|
const name = (braced ?? bare);
|
|
848
877
|
const resolved = process.env[name];
|
|
849
878
|
if (!resolved) {
|
|
@@ -854,6 +883,14 @@ export function resolveSecret(value, resolveFromStore) {
|
|
|
854
883
|
}
|
|
855
884
|
return resolved ?? "";
|
|
856
885
|
});
|
|
886
|
+
const { values, missing } = substituteRefTokens({ value: expanded }, (ref) => {
|
|
887
|
+
const stored = resolveFromStore?.(ref) ?? null;
|
|
888
|
+
return stored === null ? undefined : { kind: "secret", value: stored };
|
|
889
|
+
});
|
|
890
|
+
if (missing.length > 0) {
|
|
891
|
+
throw new ConfigError(`Secret store reference "\${${missing[0]}}" did not resolve to a stored value.`, "SECRET_REFERENCE_UNRESOLVED");
|
|
892
|
+
}
|
|
893
|
+
return values.value;
|
|
857
894
|
}
|
|
858
895
|
// Re-export source runtime helpers — implementation lives in config-sources.ts.
|
|
859
896
|
export { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleEntryToSourceEntry, bundleKeyForContentRoot, bundlePhysicalContentRoot, bundleSourceId, bundlesToSourceEntries, installedSourceDescriptor, isBundleEnabled, parseSourceSpec, primaryBundlePath, resolveActiveConfiguredSources, resolveConfiguredSources, } from "./config-sources.js";
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { validateExtraParams } from "../../extra-params.js";
|
|
11
|
+
import { isRefToken, isSecretRefToken } from "../../ref-token.js";
|
|
11
12
|
import { warnOnce } from "../../warn.js";
|
|
12
13
|
import { ENGINE_NAME_PATTERN_SOURCE } from "../engine-semantics.js";
|
|
13
14
|
/** Persisted config schema version. Package prerelease/patch versions do not change this value. */
|
|
@@ -28,21 +29,26 @@ export const httpUrl = z.string().refine((v) => v.startsWith("http://") || v.sta
|
|
|
28
29
|
});
|
|
29
30
|
const ENGINE_NAME_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
|
|
30
31
|
export const ENV_REFERENCE_PATTERN = /^\$[A-Za-z_][A-Za-z0-9_]*$|^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/;
|
|
31
|
-
/** `secret://<name>` — an apiKey reference into the akm secret store, resolved via `resolveSecretFromStore`. */
|
|
32
|
-
export const SECRET_STORE_REFERENCE_PATTERN = /^secret:\/\/(.+)$/;
|
|
33
32
|
export const engineName = z
|
|
34
33
|
.string()
|
|
35
34
|
.max(63)
|
|
36
35
|
.regex(ENGINE_NAME_PATTERN, "names must be lowercase kebab-case and must not begin with reserved akm-");
|
|
37
|
-
/** The two symbolic
|
|
36
|
+
/** The two symbolic credential forms akm accepts in config: an env-var reference (`$VAR`, `${VAR}`) or a `secrets/` ref (`${secrets/name}`). Never matches a literal key. */
|
|
38
37
|
export function isApiKeyReference(value) {
|
|
39
|
-
return ENV_REFERENCE_PATTERN.test(value) ||
|
|
38
|
+
return ENV_REFERENCE_PATTERN.test(value) || isSecretRefToken(value);
|
|
40
39
|
}
|
|
41
40
|
export function symbolicOrWarnApiKey(label) {
|
|
42
|
-
return z.string().superRefine((value) => {
|
|
41
|
+
return z.string().superRefine((value, ctx) => {
|
|
43
42
|
if (isApiKeyReference(value))
|
|
44
43
|
return;
|
|
45
|
-
|
|
44
|
+
if (isRefToken(value)) {
|
|
45
|
+
ctx.addIssue({
|
|
46
|
+
code: z.ZodIssueCode.custom,
|
|
47
|
+
message: `a ${label} accepts only a \${secrets/<name>} ref (or a $VAR reference); "${value}" names another kind of asset`,
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
warnOnce(`config:literal-api-key:${label}`, `A ${label} in config.json is a literal API key, not a $VAR/\${VAR} or \${secrets/<name>} reference; using it as configured. Prefer \`akm config set ...apiKey '$VAR'\` (with the corresponding env var set) or \`'\${secrets/<name>}'\` (with \`akm secret set secrets/<name> ...\`) — see docs/reference/data-and-telemetry.md.`);
|
|
46
52
|
});
|
|
47
53
|
}
|
|
48
54
|
export const chatCompletionsEndpoint = z.string().superRefine((value, ctx) => {
|
|
@@ -64,7 +64,7 @@ export const SourceConfigEntrySchema = z
|
|
|
64
64
|
ctx.addIssue({
|
|
65
65
|
code: z.ZodIssueCode.custom,
|
|
66
66
|
path: ["credential"],
|
|
67
|
-
message: "git credential must be a $VAR or
|
|
67
|
+
message: "git credential must be a $VAR, ${VAR} or ${secrets/<name>} reference",
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
});
|
|
@@ -178,7 +178,7 @@ export const BundleConfigEntrySchema = z
|
|
|
178
178
|
ctx.addIssue({
|
|
179
179
|
code: z.ZodIssueCode.custom,
|
|
180
180
|
path: ["credential"],
|
|
181
|
-
message: "git credential must be a $VAR or
|
|
181
|
+
message: "git credential must be a $VAR, ${VAR} or ${secrets/<name>} reference",
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
const componentEntries = entry.components ? Object.entries(entry.components) : [];
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import { resolveConceptOwner } from "../indexer/lookup/adapter-concept-owner.js";
|
|
16
17
|
import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
18
|
+
import { isFileOrFileSymlink } from "../indexer/walk/walker.js";
|
|
17
19
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
18
20
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "./asset/asset-create.js";
|
|
19
21
|
import { assetPathForName, deriveCanonicalAssetName } from "./asset/asset-placement.js";
|
|
20
|
-
import {
|
|
22
|
+
import { makeBundleRef, parseBundleRef } from "./asset/asset-ref.js";
|
|
23
|
+
import { conceptIdFromTypeName, displayRef, parseRefInput, typeNameFromConceptId } from "./asset/resolve-ref.js";
|
|
21
24
|
import { isWithin } from "./common.js";
|
|
22
25
|
import { loadConfig } from "./config/config.js";
|
|
23
26
|
import { NotFoundError, UsageError } from "./errors.js";
|
|
@@ -51,7 +54,7 @@ export function listEnvsRecursive(listKeysFn, config = loadConfig()) {
|
|
|
51
54
|
walk(full);
|
|
52
55
|
continue;
|
|
53
56
|
}
|
|
54
|
-
if (!entry.isFile())
|
|
57
|
+
if (!entry.isFile() && !(entry.isSymbolicLink() && isFileOrFileSymlink(full)))
|
|
55
58
|
continue;
|
|
56
59
|
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
57
60
|
continue;
|
|
@@ -71,33 +74,64 @@ export function listEnvsRecursive(listKeysFn, config = loadConfig()) {
|
|
|
71
74
|
return result;
|
|
72
75
|
}
|
|
73
76
|
export function parseEnvRef(ref) {
|
|
74
|
-
|
|
75
|
-
// `[bundle//]env/name` conceptId. A bare name's leading segment maps to no
|
|
76
|
-
// asset type, so it is qualified with the `env/` conceptId prefix; anything
|
|
77
|
-
// already a full new-grammar ref is parsed as-is.
|
|
78
|
-
return parseRefInput(isFullRefInput(ref) ? ref : `env/${ref}`);
|
|
77
|
+
return parseTypedRef(ref, "env", "env/<name>");
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Parse `ref` as a `[bundle//]conceptId` of the given asset type. There is no
|
|
81
|
+
* bare-name form: `prod` is a usage error that also names the ref it probably
|
|
82
|
+
* meant (`env/prod`), as text only — the suggestion is never resolved or accepted.
|
|
83
|
+
*/
|
|
84
|
+
function parseTypedRef(ref, type, form) {
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = parseRefInput(ref);
|
|
84
88
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (member)
|
|
89
|
-
return member;
|
|
90
|
-
if (!origin) {
|
|
91
|
-
const fallback = candidates[0];
|
|
92
|
-
if (fallback)
|
|
93
|
-
return fallback;
|
|
94
|
-
throw new UsageError("No bundles configured. Run `akm bundle create` to create your working bundle.");
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!(error instanceof NotFoundError))
|
|
91
|
+
throw error;
|
|
95
92
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
if (parsed?.type !== type) {
|
|
94
|
+
const bare = parseBundleRef(ref);
|
|
95
|
+
const hint = typeNameFromConceptId(bare.conceptId) === undefined
|
|
96
|
+
? ` Did you mean "${makeBundleRef(bare.bundle, conceptIdFromTypeName(type, bare.conceptId))}"?`
|
|
97
|
+
: "";
|
|
98
|
+
throw new UsageError(`Expected ${type === "env" ? "an" : "a"} ${type} ref (${form} or <bundle>//${form}); got "${ref}". Write the full ref.${hint}`, "INVALID_FLAG_VALUE");
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
/** The bundle and file that own an env or secret ref right now, or undefined. Sensitive files resolve too. */
|
|
103
|
+
function findEnvSecretOwner(origin, conceptId) {
|
|
104
|
+
return resolveConceptOwner({ bundle: origin, conceptId }, resolveSourceEntries(undefined, loadConfig()), {
|
|
105
|
+
includeSensitive: true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The file that owns any asset ref right now (the asset type its conceptId
|
|
110
|
+
* names, and the bundle-qualified spelling of the ref), or undefined when no
|
|
111
|
+
* bundle owns it. The lookup behind `${ref}` tokens.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveRefOwner(ref) {
|
|
114
|
+
const { bundle, conceptId } = parseBundleRef(ref);
|
|
115
|
+
const match = findEnvSecretOwner(bundle, conceptId);
|
|
116
|
+
return (match && {
|
|
117
|
+
absPath: match.owner.path,
|
|
118
|
+
type: typeNameFromConceptId(conceptId)?.type,
|
|
119
|
+
ref: makeBundleRef(match.bundleId, conceptId),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Is `bundle` the id of an installed bundle in the active source set? The predicate `${ref}` substitution uses to
|
|
124
|
+
* tell a qualified ref from shell text. A source set that cannot be read (for example `AKM_BUNDLE_DIR` naming a
|
|
125
|
+
* disabled bundle) answers "not installed", so shell text never raises a config error; a real ref then fails where
|
|
126
|
+
* it is resolved, with that error.
|
|
127
|
+
*/
|
|
128
|
+
export function isInstalledBundle(bundle) {
|
|
129
|
+
try {
|
|
130
|
+
return resolveSourcesForOrigin(bundle, resolveSourceEntries(undefined, loadConfig())).length > 0;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
99
134
|
}
|
|
100
|
-
return named;
|
|
101
135
|
}
|
|
102
136
|
/**
|
|
103
137
|
* `config` defaults to the real `loadConfig()`, same as {@link displayDefaultBundle}.
|
|
@@ -112,31 +146,28 @@ export function makeEnvRef(name, source, config = loadConfig()) {
|
|
|
112
146
|
return displayRef({ type: "env", name, bundleId: source?.registryId }, displayDefaultBundle(source, config));
|
|
113
147
|
}
|
|
114
148
|
/**
|
|
115
|
-
* Resolve an env ref to
|
|
116
|
-
*
|
|
117
|
-
*
|
|
149
|
+
* Resolve an env ref to the absolute `.env` path that owns it right now.
|
|
150
|
+
* Accepts a full `[bundle//]env/<name>` ref.
|
|
151
|
+
* Throws `NotFoundError` when no bundle owns it; creating one goes through
|
|
152
|
+
* {@link resolveEnvWriteTarget}.
|
|
118
153
|
*/
|
|
119
154
|
export function resolveEnvPath(ref) {
|
|
120
155
|
const parsed = parseEnvRef(ref);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
return { name: parsed.name, absPath: envPath, source, parsedRef: parsed, dir: "env" };
|
|
156
|
+
const conceptId = conceptIdFromTypeName("env", parsed.name);
|
|
157
|
+
const match = findEnvSecretOwner(parsed.origin, conceptId);
|
|
158
|
+
if (!match)
|
|
159
|
+
throw new NotFoundError(`Env not found: ${makeBundleRef(parsed.origin, conceptId)}`);
|
|
160
|
+
return {
|
|
161
|
+
name: parsed.name,
|
|
162
|
+
absPath: match.owner.path,
|
|
163
|
+
source: match.source,
|
|
164
|
+
bundleId: match.bundleId,
|
|
165
|
+
parsedRef: parsed,
|
|
166
|
+
dir: "env",
|
|
167
|
+
};
|
|
135
168
|
}
|
|
136
169
|
export function parseSecretRef(ref) {
|
|
137
|
-
|
|
138
|
-
// with the `secrets/` conceptId prefix (secret's stash subdir).
|
|
139
|
-
return parseRefInput(isFullRefInput(ref) ? ref : `secrets/${ref}`);
|
|
170
|
+
return parseTypedRef(ref, "secret", "secrets/<name>");
|
|
140
171
|
}
|
|
141
172
|
export function makeSecretRef(name, source) {
|
|
142
173
|
// F4b output-spelling flip: `secrets/name` in the primary stash,
|
|
@@ -149,39 +180,22 @@ function displayDefaultBundle(source, config = loadConfig()) {
|
|
|
149
180
|
const primary = resolveSourceEntries(undefined, config)[0];
|
|
150
181
|
return primary && path.resolve(primary.path) === path.resolve(source.path) ? source.registryId : undefined;
|
|
151
182
|
}
|
|
152
|
-
export function resolveSecretPath(ref
|
|
153
|
-
// Create-only (`secret set`): enforce a flat ref name and apply `--path` as
|
|
154
|
-
// the subdirectory. Lookup callers omit this so nested refs keep resolving.
|
|
155
|
-
create) {
|
|
183
|
+
export function resolveSecretPath(ref) {
|
|
156
184
|
const parsed = parseSecretRef(ref);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
parsed.name = combineCreatePath(normalizeCreateSubPath(create.subPath), parsed.name);
|
|
163
|
-
}
|
|
164
|
-
// Source resolution is identical for every asset type; reuse the env helper.
|
|
165
|
-
const source = findEnvSource(parsed.origin, "secret", parsed.name);
|
|
166
|
-
const typeRoot = path.join(source.path, "secrets");
|
|
167
|
-
const absPath = assetPathForName("secret", typeRoot, parsed.name);
|
|
168
|
-
// Defense-in-depth: ensure the resolved path stays inside the secrets dir.
|
|
169
|
-
if (!isWithin(absPath, typeRoot)) {
|
|
170
|
-
throw new UsageError(`Secret name "${parsed.name}" escapes the secrets directory.`);
|
|
171
|
-
}
|
|
172
|
-
return { name: parsed.name, absPath, source };
|
|
185
|
+
const conceptId = conceptIdFromTypeName("secret", parsed.name);
|
|
186
|
+
const match = findEnvSecretOwner(parsed.origin, conceptId);
|
|
187
|
+
if (!match)
|
|
188
|
+
throw new NotFoundError(`Secret not found: ${makeBundleRef(parsed.origin, conceptId)}`);
|
|
189
|
+
return { name: parsed.name, absPath: match.owner.path, source: match.source };
|
|
173
190
|
}
|
|
174
191
|
/**
|
|
175
192
|
* Resolve the destination for an env mutation. Mirrors {@link resolveEnvPath}
|
|
176
193
|
* but selects the source via {@link resolveWriteTarget} (writability-checked)
|
|
177
|
-
* instead of the read-side {@link
|
|
194
|
+
* instead of the read-side {@link resolveEnvPath}. `create` enforces a flat ref
|
|
178
195
|
* name and applies `--path` as the subdirectory (matching `env create`).
|
|
179
196
|
*/
|
|
180
197
|
export function resolveEnvWriteTarget(ref, writeTarget, create) {
|
|
181
198
|
const parsed = parseEnvRef(ref);
|
|
182
|
-
if (parsed.type !== "env") {
|
|
183
|
-
throw new UsageError(`Expected an env ref (env/<name>); got "${ref}".`);
|
|
184
|
-
}
|
|
185
199
|
if (create) {
|
|
186
200
|
assertFlatAssetName(parsed.name);
|
|
187
201
|
parsed.name = combineCreatePath(normalizeCreateSubPath(create.subPath), parsed.name);
|
|
@@ -199,13 +213,10 @@ export function resolveEnvWriteTarget(ref, writeTarget, create) {
|
|
|
199
213
|
* Resolve the destination for a secret mutation. Mirrors
|
|
200
214
|
* {@link resolveSecretPath} but selects the source via
|
|
201
215
|
* {@link resolveWriteTarget} (writability-checked) instead of the read-side
|
|
202
|
-
* {@link
|
|
216
|
+
* {@link resolveSecretPath}.
|
|
203
217
|
*/
|
|
204
218
|
export function resolveSecretWriteTarget(ref, writeTarget, create) {
|
|
205
219
|
const parsed = parseSecretRef(ref);
|
|
206
|
-
if (parsed.type !== "secret") {
|
|
207
|
-
throw new UsageError(`Expected a secret ref (secrets/<name>); got "${ref}".`);
|
|
208
|
-
}
|
|
209
220
|
if (create) {
|
|
210
221
|
assertFlatAssetName(parsed.name);
|
|
211
222
|
parsed.name = combineCreatePath(normalizeCreateSubPath(create.subPath), parsed.name);
|
package/dist/core/errors.js
CHANGED
|
@@ -17,7 +17,7 @@ const CONFIG_HINTS = {
|
|
|
17
17
|
UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The bundle directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
|
|
18
18
|
UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
|
|
19
19
|
EXECUTION_NOT_AUTHORIZED: "Change the selected tools or update the machine/user execution policy, then retry.",
|
|
20
|
-
SECRET_REFERENCE_UNRESOLVED: "Check the secret exists (`akm secret list`) and the
|
|
20
|
+
SECRET_REFERENCE_UNRESOLVED: "Check the secret exists (`akm secret list`) and the ref inside `${…}` matches it, or run `akm secret set secrets/<name>` to store it.",
|
|
21
21
|
REGISTRY_URL_INVALID: "Registry URLs must be credential-free http(s) URLs. Fix the entry with `akm registry list` / `akm registry add`, or the AKM_REGISTRY_URL / AKM_NPM_REGISTRY variable that supplied it.",
|
|
22
22
|
};
|
|
23
23
|
// Code-review finding: COMPOSITION_INVALID covers several unrelated causes
|