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
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
* group, hard break, no alias kept at the top level:
|
|
9
9
|
* - `create` (= old top-level `init`, src/commands/sources/init.ts)
|
|
10
10
|
* - `add` (= old top-level `add`, src/commands/sources/add-cli.ts — the
|
|
11
|
-
* command definition moved here; `add-cli.ts` keeps its exported helpers
|
|
12
|
-
* including `auditInstalledStashForDangerousKeys`, which a test imports
|
|
13
|
-
* directly)
|
|
11
|
+
* command definition moved here; `add-cli.ts` keeps its exported helpers)
|
|
14
12
|
* - `list` (= old top-level `list`)
|
|
15
13
|
* - `remove` (= old top-level `remove`)
|
|
16
14
|
* - `update` (= old top-level `update`)
|
|
@@ -134,18 +132,21 @@ const updateCommand = defineJsonCommand({
|
|
|
134
132
|
description: "Exit successfully when index/state database contention shows another akm operation is active",
|
|
135
133
|
default: false,
|
|
136
134
|
},
|
|
135
|
+
// Deprecated no-op, accepted through 0.10 so scripts that pass it do not
|
|
136
|
+
// fail with UNKNOWN_FLAG. Installing a bundle is the trust decision: an
|
|
137
|
+
// update no longer scans env files for process-hijacking keys.
|
|
137
138
|
"allow-dangerous-env-keys": {
|
|
138
139
|
type: "boolean",
|
|
139
|
-
description: "
|
|
140
|
+
description: "Deprecated, no effect: update no longer scans env files for process-hijacking keys. Accepted through 0.10 for existing scripts.",
|
|
140
141
|
default: false,
|
|
141
142
|
},
|
|
142
143
|
// Deprecated no-op, accepted through 0.10 so scripts that pass it do not
|
|
143
|
-
// fail with UNKNOWN_FLAG.
|
|
144
|
-
//
|
|
144
|
+
// fail with UNKNOWN_FLAG. Pruning old install directories of a read-only
|
|
145
|
+
// bundle no longer asks: they are a re-fetchable cache.
|
|
145
146
|
yes: {
|
|
146
147
|
type: "boolean",
|
|
147
148
|
alias: "y",
|
|
148
|
-
description: "Deprecated, no effect: update no longer asks before
|
|
149
|
+
description: "Deprecated, no effect: update no longer asks before pruning old install directories of a read-only bundle. Accepted through 0.10 for existing scripts.",
|
|
149
150
|
default: false,
|
|
150
151
|
},
|
|
151
152
|
},
|
|
@@ -156,7 +157,6 @@ const updateCommand = defineJsonCommand({
|
|
|
156
157
|
target: args.target,
|
|
157
158
|
all: args.all,
|
|
158
159
|
force: args.force,
|
|
159
|
-
allowDangerousEnvKeys: args["allow-dangerous-env-keys"],
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
catch (error) {
|
|
@@ -173,7 +173,6 @@ const updateCommand = defineJsonCommand({
|
|
|
173
173
|
target: args.target ?? null,
|
|
174
174
|
all: args.all === true,
|
|
175
175
|
force: args.force === true,
|
|
176
|
-
allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
|
|
177
176
|
processed: Array.isArray(result.processed)
|
|
178
177
|
? result.processed.length
|
|
179
178
|
: 0,
|
|
@@ -22,20 +22,19 @@ import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/err
|
|
|
22
22
|
import { isPathAbsent } from "../../core/path-access.js";
|
|
23
23
|
import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
|
|
24
24
|
import { warn } from "../../core/warn.js";
|
|
25
|
-
import { resolveGitContentRoot } from "../../core/write-source.js";
|
|
26
25
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
27
26
|
import { akmIndex } from "../../indexer/indexer.js";
|
|
28
27
|
import { readLockfile, upsertLockEntry } from "../../integrations/lockfile.js";
|
|
29
|
-
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
28
|
+
import { parseRegistryRef, resolveRegistryArtifact } from "../../registry/resolve.js";
|
|
30
29
|
import { getCachePaths, parseGitRepoUrl, runGit, syncMirroredRepo } from "../../sources/providers/git.js";
|
|
31
30
|
import { replaceDirectory } from "../../sources/providers/git-install.js";
|
|
31
|
+
import { installRevisionDir, isDirectory } from "../../sources/providers/provider-utils.js";
|
|
32
32
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
33
33
|
import { createWebsiteProvider } from "../../sources/providers/website.js";
|
|
34
34
|
import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
35
35
|
import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
|
|
36
36
|
import { closeDatabase, openReadonlyExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
37
37
|
import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
|
|
38
|
-
import { auditStashForDangerousKeys } from "./dangerous-env-audit.js";
|
|
39
38
|
import { removeInstalledRegistryEntry } from "./source-add.js";
|
|
40
39
|
import { removeStash } from "./source-manage.js";
|
|
41
40
|
/** Enumerate the registry-managed installs (lock-backed bundles) in a config. */
|
|
@@ -55,6 +54,7 @@ function listManagedInstalls(config) {
|
|
|
55
54
|
source: lock.source,
|
|
56
55
|
ref: lock.ref,
|
|
57
56
|
localRoot: lock.localRoot ?? "",
|
|
57
|
+
installedAt: lock.installedAt ?? "",
|
|
58
58
|
resolvedVersion: lock.resolvedVersion,
|
|
59
59
|
resolvedRevision: lock.resolvedRevision,
|
|
60
60
|
writable: componentWritable ?? bundle.writable === true,
|
|
@@ -379,18 +379,6 @@ function bundleComponent(config, id) {
|
|
|
379
379
|
const component = bundleComponentConfig(bundle);
|
|
380
380
|
return { root: component?.root ?? ".", writable: component?.writable ?? bundle?.writable === true };
|
|
381
381
|
}
|
|
382
|
-
async function auditStagedUpdate(auditRoot, ref, allowDangerousEnvKeys) {
|
|
383
|
-
const decision = await auditStashForDangerousKeys({
|
|
384
|
-
stashRoot: auditRoot,
|
|
385
|
-
ref,
|
|
386
|
-
allowDangerousKeys: allowDangerousEnvKeys,
|
|
387
|
-
isTTY: process.stdin.isTTY === true,
|
|
388
|
-
operation: "update",
|
|
389
|
-
renderBlockedError: false,
|
|
390
|
-
});
|
|
391
|
-
if (decision.blocked)
|
|
392
|
-
throw new NotFoundError(decision.error, decision.code);
|
|
393
|
-
}
|
|
394
382
|
function gitOrThrow(repoDir, args) {
|
|
395
383
|
const result = runGit(["-C", repoDir, ...args], { timeout: 120_000 });
|
|
396
384
|
if (result.status !== 0) {
|
|
@@ -399,7 +387,7 @@ function gitOrThrow(repoDir, args) {
|
|
|
399
387
|
return result.stdout.trim();
|
|
400
388
|
}
|
|
401
389
|
/**
|
|
402
|
-
* Publish
|
|
390
|
+
* Publish a staged writable checkout: fast-forward the live checkout to
|
|
403
391
|
* exactly the staged commit. Git refuses a non-fast-forward or a merge that
|
|
404
392
|
* would overwrite local work, and changes nothing in that case.
|
|
405
393
|
*/
|
|
@@ -423,15 +411,61 @@ function publishAndIndex(stashDir, publish) {
|
|
|
423
411
|
return reindexAfterUpdate(stashDir);
|
|
424
412
|
});
|
|
425
413
|
}
|
|
414
|
+
function managedCredential(managed) {
|
|
415
|
+
return managed.credential ? resolveSecret(managed.credential, storeSecretResolver.resolveSecret) : undefined;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The unchanged result for a read-only git or npm install whose upstream still
|
|
419
|
+
* resolves to the locked revision while its `<rev>` directory is in place, so
|
|
420
|
+
* there is nothing to fetch, publish or replace. Undefined when the install
|
|
421
|
+
* must be updated: `--force`, a writable or lockless install, a missing
|
|
422
|
+
* directory, or a new upstream revision.
|
|
423
|
+
*/
|
|
424
|
+
async function unchangedManagedItem(managed, force) {
|
|
425
|
+
if (force || managed.writable || !managed.resolvedRevision || !managed.localRoot)
|
|
426
|
+
return undefined;
|
|
427
|
+
if (managed.source !== "git" && managed.source !== "github" && managed.source !== "npm")
|
|
428
|
+
return undefined;
|
|
429
|
+
const revisionDir = installRevisionDir(getRegistryCacheDir(), managed.localRoot);
|
|
430
|
+
if (!revisionDir || !isDirectory(managed.localRoot))
|
|
431
|
+
return undefined;
|
|
432
|
+
const resolved = await resolveRegistryArtifact(parseRegistryRef(managed.ref), {
|
|
433
|
+
gitCredential: managedCredential(managed),
|
|
434
|
+
});
|
|
435
|
+
if (resolved.resolvedRevision !== managed.resolvedRevision)
|
|
436
|
+
return undefined;
|
|
437
|
+
return {
|
|
438
|
+
id: managed.installId,
|
|
439
|
+
source: managed.source,
|
|
440
|
+
ref: managed.ref,
|
|
441
|
+
previous: {
|
|
442
|
+
resolvedVersion: managed.resolvedVersion,
|
|
443
|
+
resolvedRevision: managed.resolvedRevision,
|
|
444
|
+
cacheDir: managed.localRoot,
|
|
445
|
+
},
|
|
446
|
+
installed: {
|
|
447
|
+
id: managed.installId,
|
|
448
|
+
source: managed.source,
|
|
449
|
+
ref: managed.ref,
|
|
450
|
+
artifactUrl: resolved.artifactUrl,
|
|
451
|
+
resolvedVersion: managed.resolvedVersion,
|
|
452
|
+
resolvedRevision: managed.resolvedRevision,
|
|
453
|
+
stashRoot: managed.localRoot,
|
|
454
|
+
cacheDir: revisionDir,
|
|
455
|
+
installedAt: managed.installedAt,
|
|
456
|
+
writable: false,
|
|
457
|
+
extractedDir: path.join(revisionDir, "extracted"),
|
|
458
|
+
},
|
|
459
|
+
changed: { version: false, revision: false, any: false },
|
|
460
|
+
};
|
|
461
|
+
}
|
|
426
462
|
/** Fetch a registry-managed install into staging without touching its live content. */
|
|
427
463
|
async function stageManagedUpdate(managed, force) {
|
|
428
|
-
const credential = managed
|
|
429
|
-
? resolveSecret(managed.credential, storeSecretResolver.resolveSecret)
|
|
430
|
-
: undefined;
|
|
464
|
+
const credential = managedCredential(managed);
|
|
431
465
|
if (managed.writable && managed.localRoot) {
|
|
432
466
|
// A writable install is a checkout that may carry local commits: the
|
|
433
|
-
// provider fast-forwards a copy, the
|
|
434
|
-
//
|
|
467
|
+
// provider fast-forwards a copy, and the live checkout is then
|
|
468
|
+
// fast-forwarded to exactly that fetched commit.
|
|
435
469
|
const topLevel = runGit(["-C", managed.localRoot, "rev-parse", "--show-toplevel"]);
|
|
436
470
|
if (topLevel.status !== 0 || !topLevel.stdout.trim()) {
|
|
437
471
|
throw new UsageError(`Writable Git install at ${managed.localRoot} is not a checkout; refusing to update it.`);
|
|
@@ -452,7 +486,6 @@ async function stageManagedUpdate(managed, force) {
|
|
|
452
486
|
return {
|
|
453
487
|
synced: { ...fetched, contentDir: managed.localRoot, cacheDir: liveRepo, extractedDir: liveRepo },
|
|
454
488
|
staged: {
|
|
455
|
-
auditRoot: path.resolve(fetched.contentDir, managed.componentRoot),
|
|
456
489
|
publish: () => fastForwardCheckout(liveRepo, stagedRepo),
|
|
457
490
|
cleanup: () => cleanupDirectoryBestEffort(stagingParent, "bundle update"),
|
|
458
491
|
},
|
|
@@ -468,7 +501,7 @@ async function stageManagedUpdate(managed, force) {
|
|
|
468
501
|
const synced = await syncFromRef(managed.ref, { force, writable: managed.writable });
|
|
469
502
|
return {
|
|
470
503
|
synced,
|
|
471
|
-
staged: {
|
|
504
|
+
staged: { publish() { }, cleanup() { } },
|
|
472
505
|
};
|
|
473
506
|
}
|
|
474
507
|
const cacheRoot = getRegistryCacheDir();
|
|
@@ -490,7 +523,6 @@ async function stageManagedUpdate(managed, force) {
|
|
|
490
523
|
extractedDir: toLive(fetched.extractedDir),
|
|
491
524
|
},
|
|
492
525
|
staged: {
|
|
493
|
-
auditRoot: path.resolve(fetched.contentDir, managed.componentRoot),
|
|
494
526
|
publish: () => replaceDirectory(fetched.cacheDir, liveCacheDir),
|
|
495
527
|
cleanup: () => cleanupDirectoryBestEffort(stagingParent, "bundle update"),
|
|
496
528
|
},
|
|
@@ -501,8 +533,8 @@ async function stageManagedUpdate(managed, force) {
|
|
|
501
533
|
throw error;
|
|
502
534
|
}
|
|
503
535
|
}
|
|
504
|
-
/** Fetch a plain git source into staging,
|
|
505
|
-
async function syncGitPlainSource(config, gitSource, stashDir
|
|
536
|
+
/** Fetch a plain git source into staging, publish it, and reindex. */
|
|
537
|
+
async function syncGitPlainSource(config, gitSource, stashDir) {
|
|
506
538
|
const id = gitSource.name ?? gitSource.url ?? "";
|
|
507
539
|
const ref = gitSource.url ?? "";
|
|
508
540
|
if (!gitSource.url)
|
|
@@ -514,7 +546,7 @@ async function syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnv
|
|
|
514
546
|
try {
|
|
515
547
|
const stagedPaths = getCachePaths(canonicalUrl, stagingParent);
|
|
516
548
|
// A writable mirror may carry local commits: pull into a copy, then
|
|
517
|
-
// fast-forward the live checkout
|
|
549
|
+
// fast-forward the live checkout to it.
|
|
518
550
|
const liveCheckout = component.writable && fs.existsSync(livePaths.repoDir);
|
|
519
551
|
if (liveCheckout)
|
|
520
552
|
fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
|
|
@@ -526,7 +558,6 @@ async function syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnv
|
|
|
526
558
|
? { credential: resolveSecret(gitSource.credential, storeSecretResolver.resolveSecret) }
|
|
527
559
|
: {}),
|
|
528
560
|
});
|
|
529
|
-
await auditStagedUpdate(path.resolve(resolveGitContentRoot(stagedPaths.repoDir), component.root), ref, allowDangerousEnvKeys);
|
|
530
561
|
const index = await publishAndIndex(stashDir, () => {
|
|
531
562
|
if (liveCheckout)
|
|
532
563
|
fastForwardCheckout(livePaths.repoDir, stagedPaths.repoDir);
|
|
@@ -543,7 +574,7 @@ async function syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnv
|
|
|
543
574
|
* Re-crawl a website source and reindex. The website provider already builds
|
|
544
575
|
* its snapshot in a staging directory and swaps it in, so a killed refresh
|
|
545
576
|
* keeps the previous mirror; a snapshot holds only knowledge pages, so there
|
|
546
|
-
* is no env file
|
|
577
|
+
* is no env file in it.
|
|
547
578
|
*/
|
|
548
579
|
async function syncWebsitePlainSource(websiteSource, stashDir) {
|
|
549
580
|
const id = websiteSource.name ?? websiteSource.url ?? "";
|
|
@@ -556,8 +587,8 @@ async function syncWebsitePlainSource(websiteSource, stashDir) {
|
|
|
556
587
|
return { item: { id, kind: "website", ref: websiteSource.url ?? "" }, index };
|
|
557
588
|
}
|
|
558
589
|
/** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
|
|
559
|
-
async function updateGitSource(config, stashDir, target, all, gitSource
|
|
560
|
-
const synced = await syncGitPlainSource(config, gitSource, stashDir
|
|
590
|
+
async function updateGitSource(config, stashDir, target, all, gitSource) {
|
|
591
|
+
const synced = await syncGitPlainSource(config, gitSource, stashDir);
|
|
561
592
|
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
562
593
|
}
|
|
563
594
|
/** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
|
|
@@ -602,6 +633,7 @@ function managedInstallViewOfPlainNpm(config, npmSource) {
|
|
|
602
633
|
source: "npm",
|
|
603
634
|
ref: spec.startsWith("npm:") ? spec : `npm:${spec}`,
|
|
604
635
|
localRoot: "",
|
|
636
|
+
installedAt: "",
|
|
605
637
|
writable: false,
|
|
606
638
|
componentRoot: bundleComponent(config, id).root,
|
|
607
639
|
requiredRoots: [],
|
|
@@ -609,18 +641,21 @@ function managedInstallViewOfPlainNpm(config, npmSource) {
|
|
|
609
641
|
}
|
|
610
642
|
/**
|
|
611
643
|
* Sync a single registry-managed install and return the processed record:
|
|
612
|
-
* fetch into staging,
|
|
644
|
+
* fetch into staging, publish, write its lock entry, reindex.
|
|
613
645
|
*
|
|
614
|
-
* Read-only installs are keyed by resolved revision
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
646
|
+
* Read-only installs are keyed by resolved revision. An unchanged revision
|
|
647
|
+
* returns early: nothing is fetched or published. After a real change the new
|
|
648
|
+
* <rev> directory is published, the previous <rev> directory is kept, and older
|
|
649
|
+
* ones are pruned, unless another bundle or lock entry still references them.
|
|
650
|
+
* Those directories are a re-fetchable cache (local and writable installs never
|
|
651
|
+
* take this path), so none of this prompts.
|
|
619
652
|
*/
|
|
620
|
-
async function updateManagedInstall(managed, force, stashDir
|
|
653
|
+
async function updateManagedInstall(managed, force, stashDir) {
|
|
654
|
+
const unchanged = await unchangedManagedItem(managed, force);
|
|
655
|
+
if (unchanged)
|
|
656
|
+
return { item: unchanged };
|
|
621
657
|
const { synced, staged } = await stageManagedUpdate(managed, force);
|
|
622
658
|
try {
|
|
623
|
-
await auditStagedUpdate(staged.auditRoot, managed.ref, allowDangerousEnvKeys);
|
|
624
659
|
const installedEntry = {
|
|
625
660
|
id: managed.installId,
|
|
626
661
|
// Preserve the original source classification. syncFromRef() re-derives the
|
|
@@ -655,14 +690,8 @@ async function updateManagedInstall(managed, force, stashDir, allowDangerousEnvK
|
|
|
655
690
|
installedAt: synced.syncedAt,
|
|
656
691
|
});
|
|
657
692
|
});
|
|
658
|
-
if (movedRoot)
|
|
659
|
-
|
|
660
|
-
warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
664
|
-
}
|
|
665
|
-
}
|
|
693
|
+
if (movedRoot)
|
|
694
|
+
pruneOlderRevisions(managed.localRoot, synced.cacheDir);
|
|
666
695
|
const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
667
696
|
const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
668
697
|
return {
|
|
@@ -689,6 +718,31 @@ async function updateManagedInstall(managed, force, stashDir, allowDangerousEnvK
|
|
|
689
718
|
staged.cleanup();
|
|
690
719
|
}
|
|
691
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* After a real revision change, keep the previous `<rev>` directory (a process
|
|
723
|
+
* may still be running from it) and remove every older sibling in the install's
|
|
724
|
+
* directory, unless another configured or locked bundle still uses it.
|
|
725
|
+
*/
|
|
726
|
+
function pruneOlderRevisions(previousRoot, currentCacheDir) {
|
|
727
|
+
const previousDir = installRevisionDir(getRegistryCacheDir(), previousRoot);
|
|
728
|
+
const installDir = path.dirname(path.resolve(currentCacheDir));
|
|
729
|
+
if (!previousDir || path.dirname(previousDir) !== installDir)
|
|
730
|
+
return;
|
|
731
|
+
const keep = new Set([previousDir, path.resolve(currentCacheDir)]);
|
|
732
|
+
const config = loadConfig();
|
|
733
|
+
const locks = readLockfile();
|
|
734
|
+
for (const entry of fs.readdirSync(installDir, { withFileTypes: true })) {
|
|
735
|
+
const dir = path.join(installDir, entry.name);
|
|
736
|
+
if (!entry.isDirectory() || keep.has(dir))
|
|
737
|
+
continue;
|
|
738
|
+
if (referencesRoot(dir, config, locks)) {
|
|
739
|
+
warn(`[akm bundle update] kept older install directory at ${dir} because another configured or locked bundle still references it.`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
cleanupDirectoryBestEffort(dir, "update");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
692
746
|
function referencesRoot(target, config, locks) {
|
|
693
747
|
const resolvedTarget = path.resolve(target);
|
|
694
748
|
const locksById = new Map(locks.map((entry) => [entry.id, entry]));
|
|
@@ -716,15 +770,16 @@ export async function akmUpdate(input) {
|
|
|
716
770
|
const target = input?.target?.trim();
|
|
717
771
|
const all = input?.all === true;
|
|
718
772
|
const force = input?.force === true;
|
|
719
|
-
const allowDangerousEnvKeys = input?.allowDangerousEnvKeys === true;
|
|
720
773
|
const config = loadConfig();
|
|
721
774
|
const managedInstalls = listManagedInstalls(config);
|
|
722
775
|
if (target && !all) {
|
|
723
776
|
// Registry-managed install (lock-backed) — re-download from its locator.
|
|
724
777
|
const managed = resolveManagedTarget(config, target);
|
|
725
778
|
if (managed) {
|
|
726
|
-
const updated = await updateManagedInstall(managed, force, stashDir
|
|
727
|
-
return buildUpdateResponse(stashDir, target, all, [updated.item], {
|
|
779
|
+
const updated = await updateManagedInstall(managed, force, stashDir);
|
|
780
|
+
return buildUpdateResponse(stashDir, target, all, [updated.item], {
|
|
781
|
+
index: updated.index ?? readCurrentIndexSummary(),
|
|
782
|
+
});
|
|
728
783
|
}
|
|
729
784
|
// Plain git / website source (bundles without a lock) — provider re-sync.
|
|
730
785
|
const stashes = getSources(config);
|
|
@@ -752,7 +807,7 @@ export async function akmUpdate(input) {
|
|
|
752
807
|
return false;
|
|
753
808
|
});
|
|
754
809
|
if (gitMatch)
|
|
755
|
-
return updateGitSource(config, stashDir, target, all, gitMatch
|
|
810
|
+
return updateGitSource(config, stashDir, target, all, gitMatch);
|
|
756
811
|
const websiteMatch = stashes.find((s) => {
|
|
757
812
|
if (s.type !== "website")
|
|
758
813
|
return false;
|
|
@@ -780,8 +835,10 @@ export async function akmUpdate(input) {
|
|
|
780
835
|
return false;
|
|
781
836
|
});
|
|
782
837
|
if (npmMatch) {
|
|
783
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, npmMatch), force, stashDir
|
|
784
|
-
return buildUpdateResponse(stashDir, target, all, [updated.item], {
|
|
838
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, npmMatch), force, stashDir);
|
|
839
|
+
return buildUpdateResponse(stashDir, target, all, [updated.item], {
|
|
840
|
+
index: updated.index ?? readCurrentIndexSummary(),
|
|
841
|
+
});
|
|
785
842
|
}
|
|
786
843
|
const filesystemMatch = stashes.find((source) => {
|
|
787
844
|
if (source.type !== "filesystem")
|
|
@@ -803,9 +860,9 @@ export async function akmUpdate(input) {
|
|
|
803
860
|
let latestIndex;
|
|
804
861
|
for (const managed of selected) {
|
|
805
862
|
try {
|
|
806
|
-
const updated = await updateManagedInstall(managed, force, stashDir
|
|
863
|
+
const updated = await updateManagedInstall(managed, force, stashDir);
|
|
807
864
|
processed.push(updated.item);
|
|
808
|
-
latestIndex = updated.index;
|
|
865
|
+
latestIndex = updated.index ?? latestIndex;
|
|
809
866
|
}
|
|
810
867
|
catch (error) {
|
|
811
868
|
if (!all)
|
|
@@ -828,14 +885,14 @@ export async function akmUpdate(input) {
|
|
|
828
885
|
const id = plain.name ?? plain.path ?? plain.url ?? "";
|
|
829
886
|
try {
|
|
830
887
|
if (plain.type === "git") {
|
|
831
|
-
const updated = await syncGitPlainSource(config, plain, stashDir
|
|
888
|
+
const updated = await syncGitPlainSource(config, plain, stashDir);
|
|
832
889
|
plainSynced.push(updated.item);
|
|
833
890
|
latestIndex = updated.index;
|
|
834
891
|
}
|
|
835
892
|
else if (plain.type === "npm") {
|
|
836
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, stashDir
|
|
893
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, stashDir);
|
|
837
894
|
processed.push(updated.item);
|
|
838
|
-
latestIndex = updated.index;
|
|
895
|
+
latestIndex = updated.index ?? latestIndex;
|
|
839
896
|
}
|
|
840
897
|
else if (plain.type === "website") {
|
|
841
898
|
const updated = await syncWebsitePlainSource(plain, stashDir);
|
|
@@ -896,14 +953,13 @@ function sourceKindForInstall(source) {
|
|
|
896
953
|
}
|
|
897
954
|
function updateFailureOutcome(id, kind, error) {
|
|
898
955
|
const code = error instanceof AkmError ? error.code : undefined;
|
|
899
|
-
const status = code === "DANGEROUS_ENV_KEY" || code === "DANGEROUS_ENV_AUDIT_FAILED" ? "blocked" : "failed";
|
|
900
956
|
const message = error instanceof Error ? error.message : String(error);
|
|
901
957
|
return {
|
|
902
958
|
id,
|
|
903
959
|
kind,
|
|
904
|
-
status,
|
|
960
|
+
status: "failed",
|
|
905
961
|
...(code ? { code } : {}),
|
|
906
|
-
reason:
|
|
962
|
+
reason: `failed: ${message}`,
|
|
907
963
|
};
|
|
908
964
|
}
|
|
909
965
|
function selectManagedTargets(config, installs, target, all) {
|
|
@@ -12,7 +12,7 @@ import { isBundleSlug } from "../../core/asset/asset-ref.js";
|
|
|
12
12
|
import { serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
13
13
|
import { parseFrontmatter, replaceFrontmatterBlocks } from "../../core/asset/frontmatter.js";
|
|
14
14
|
import { conceptIdFromTypeName, displayRef, parseQualifiedRefInput } from "../../core/asset/resolve-ref.js";
|
|
15
|
-
import { isWithin } from "../../core/common.js";
|
|
15
|
+
import { isWithin, safeRealpath } from "../../core/common.js";
|
|
16
16
|
import { loadConfig } from "../../core/config/config.js";
|
|
17
17
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
18
18
|
import { defaultBundleForTarget } from "../../core/mutation-target.js";
|
|
@@ -23,6 +23,7 @@ import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
|
23
23
|
import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
|
|
24
24
|
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
25
25
|
import { buildFileContext } from "../../indexer/walk/file-context.js";
|
|
26
|
+
import { isFileOrFileSymlink } from "../../indexer/walk/walker.js";
|
|
26
27
|
import { isRemoteOrigin, resolveSourcesForLocator, resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
|
|
27
28
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
28
29
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
@@ -161,18 +162,20 @@ export async function akmClone(options) {
|
|
|
161
162
|
: writeTarget?.selector
|
|
162
163
|
? `in target "${writeTarget.selector}"`
|
|
163
164
|
: "in working stash";
|
|
164
|
-
// Guard against self-clone
|
|
165
|
+
// Guard against self-clone. Compare physical paths: sourcePath is the
|
|
166
|
+
// authored path, and a source that is a symlink to the destination is the
|
|
167
|
+
// same file (or skill) even though the authored paths differ.
|
|
165
168
|
if (isSkill) {
|
|
166
|
-
const sourceSkillDir =
|
|
167
|
-
const destSkillDir =
|
|
169
|
+
const sourceSkillDir = safeRealpath(path.dirname(sourcePath));
|
|
170
|
+
const destSkillDir = safeRealpath(path.join(destRoot, typeDir, destName));
|
|
168
171
|
if (sourceSkillDir === destSkillDir) {
|
|
169
172
|
throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
else {
|
|
173
|
-
const resolvedSource =
|
|
176
|
+
const resolvedSource = safeRealpath(sourcePath);
|
|
174
177
|
const effectiveDestName = !path.extname(destFileName) && sourceExt ? destFileName + sourceExt : destFileName;
|
|
175
|
-
const resolvedDest =
|
|
178
|
+
const resolvedDest = safeRealpath(path.join(destRoot, typeDir, effectiveDestName));
|
|
176
179
|
if (resolvedSource === resolvedDest) {
|
|
177
180
|
throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
|
|
178
181
|
}
|
|
@@ -182,7 +185,9 @@ export async function akmClone(options) {
|
|
|
182
185
|
let overwritten;
|
|
183
186
|
let operationPaths;
|
|
184
187
|
if (isSkill) {
|
|
185
|
-
|
|
188
|
+
// sourcePath is the authored path; a symlinked skill directory must be
|
|
189
|
+
// copied by content, never as the link.
|
|
190
|
+
const sourceSkillDir = fs.realpathSync(path.dirname(sourcePath));
|
|
186
191
|
const destSkillDir = path.join(destRoot, typeDir, destName);
|
|
187
192
|
assertNoDestinationSymlinkParent(destRoot, destSkillDir);
|
|
188
193
|
const existing = lstatIfExists(destSkillDir);
|
|
@@ -195,7 +200,11 @@ export async function akmClone(options) {
|
|
|
195
200
|
// final symlink is removed as an entry, never traversed.
|
|
196
201
|
const stagingDir = `${destSkillDir}.tmp-${randomUUID()}`;
|
|
197
202
|
try {
|
|
203
|
+
// Directory links are copied as links and never descended (as the walker
|
|
204
|
+
// does); each file link is then replaced by its content, so the stamp
|
|
205
|
+
// below never writes through a link.
|
|
198
206
|
fs.cpSync(sourceSkillDir, stagingDir, { recursive: true, errorOnExist: true, force: false });
|
|
207
|
+
materializeFileLinks(stagingDir);
|
|
199
208
|
stampCloneOrigin(path.join(stagingDir, "SKILL.md"), originRef);
|
|
200
209
|
if (overwritten)
|
|
201
210
|
fs.rmSync(destSkillDir, { recursive: true, force: true });
|
|
@@ -348,6 +357,21 @@ function assertNoDestinationSymlinkParent(root, destination) {
|
|
|
348
357
|
throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${parentDir}.`, "PATH_ESCAPE_VIOLATION");
|
|
349
358
|
}
|
|
350
359
|
}
|
|
360
|
+
/** Replace each file symlink under `root` with a regular file holding its target's bytes. Directory links are left as links. */
|
|
361
|
+
function materializeFileLinks(root) {
|
|
362
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
363
|
+
const entryPath = path.join(root, entry.name);
|
|
364
|
+
if (entry.isDirectory()) {
|
|
365
|
+
materializeFileLinks(entryPath);
|
|
366
|
+
}
|
|
367
|
+
else if (entry.isSymbolicLink() && isFileOrFileSymlink(entryPath)) {
|
|
368
|
+
const content = fs.readFileSync(entryPath);
|
|
369
|
+
const { mode } = fs.statSync(entryPath);
|
|
370
|
+
fs.rmSync(entryPath);
|
|
371
|
+
fs.writeFileSync(entryPath, content, { mode });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
351
375
|
function listTreeFiles(root) {
|
|
352
376
|
const files = [];
|
|
353
377
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
@@ -247,7 +247,7 @@ export async function akmTaskExplain(ref, options = {}) {
|
|
|
247
247
|
}
|
|
248
248
|
const sourcePath = owner.path;
|
|
249
249
|
const yaml = fs.readFileSync(sourcePath, "utf8");
|
|
250
|
-
const parsed = parseTaskSource({ yaml, filePath: sourcePath
|
|
250
|
+
const parsed = parseTaskSource({ yaml, filePath: sourcePath });
|
|
251
251
|
const inputContract = parsed.v4.inputs ?? {};
|
|
252
252
|
// Code-review finding (explain.ts:299, B-N4): `akm task run`'s
|
|
253
253
|
// load-task.ts copies this same materialize -> applyInputDefaults ->
|