akm-cli 0.9.6 → 0.9.8-beta.1
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 +408 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +9 -20
- package/dist/commands/improve/preparation.js +53 -37
- package/dist/commands/improve/reflect.js +14 -18
- package/dist/commands/lint/base-linter.js +182 -19
- package/dist/commands/lint/index.js +21 -9
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +51 -18
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +14 -5
- package/dist/core/asset/frontmatter.js +212 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +155 -251
- package/dist/scripts/akm-migrate.js +155 -251
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +7 -1
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -15,7 +15,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
15
15
|
import { loadConfig } from "../../core/config/config.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
18
|
-
import {
|
|
18
|
+
import { diffProposal, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, recoverProposalTransactionsForStash, rejectProposalDurably, resolveProposalId, revertProposal, } from "./repository.js";
|
|
19
19
|
import { validateProposal } from "./validators/proposals.js";
|
|
20
20
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
21
21
|
function resolveStash(stashDir) {
|
|
@@ -131,26 +131,6 @@ export function akmProposalDiff(options) {
|
|
|
131
131
|
...(diff.targetPath ? { targetPath: diff.targetPath } : {}),
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
export function akmProposalCreate(options) {
|
|
135
|
-
const queue = resolveProposalQueue(options.stashDir, options.queue, options.config);
|
|
136
|
-
const stash = queue.stashDir;
|
|
137
|
-
const target = options.target ?? (queue.target ? { source: queue.target.source.name, root: queue.target.source.path } : undefined);
|
|
138
|
-
// Manual proposal creation (via `akm proposal create`) always bypasses
|
|
139
|
-
// dedup/cooldown guards — the operator is explicitly requesting a proposal.
|
|
140
|
-
const result = createProposal(stash, {
|
|
141
|
-
ref: options.ref,
|
|
142
|
-
source: options.source,
|
|
143
|
-
...(target !== undefined ? { target } : {}),
|
|
144
|
-
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
145
|
-
payload: options.payload,
|
|
146
|
-
force: true,
|
|
147
|
-
}, options.ctx);
|
|
148
|
-
if (isProposalSkipped(result)) {
|
|
149
|
-
// Should never happen with force:true — defensive only.
|
|
150
|
-
throw new Error(`Unexpected proposal skip: ${result.message}`);
|
|
151
|
-
}
|
|
152
|
-
return { schemaVersion: 1, ok: true, proposal: result };
|
|
153
|
-
}
|
|
154
134
|
/**
|
|
155
135
|
* Restore an accepted proposal's prior content from the backup captured at
|
|
156
136
|
* promotion time (Advantage D6c / Phase 6C).
|
|
@@ -563,9 +563,6 @@ export function listProposalsReadOnly(stashDir, options = {}, ctx) {
|
|
|
563
563
|
}
|
|
564
564
|
});
|
|
565
565
|
}
|
|
566
|
-
catch {
|
|
567
|
-
return [];
|
|
568
|
-
}
|
|
569
566
|
finally {
|
|
570
567
|
db?.close();
|
|
571
568
|
}
|
|
@@ -1013,7 +1010,6 @@ async function finalizeProposalTransaction(txn, target, proposal, ctx) {
|
|
|
1013
1010
|
paths: [commitPath],
|
|
1014
1011
|
snapshots: p.gitSnapshots ?? {},
|
|
1015
1012
|
onCommitRecorded: (commit) => {
|
|
1016
|
-
// biome-ignore lint/style/noNonNullAssertion: publishWriteTargetTransaction throws when absent
|
|
1017
1013
|
const publication = p.gitPublication;
|
|
1018
1014
|
if (publication.commit !== commit) {
|
|
1019
1015
|
publication.commit = commit;
|
|
@@ -190,12 +190,6 @@ export function hasHotCaptureMode(frontmatter) {
|
|
|
190
190
|
return frontmatter?.captureMode === "hot";
|
|
191
191
|
}
|
|
192
192
|
// ── Consolidate merge size gate ──────────────────────────────────────────────
|
|
193
|
-
/**
|
|
194
|
-
* Ratio lower-bound for merged body vs. the larger source body.
|
|
195
|
-
* Lower than reflect (0.5) because deduplication is expected — two memories
|
|
196
|
-
* with 80-90% overlap legitimately compress to well under 50% of the larger.
|
|
197
|
-
*/
|
|
198
|
-
export const MERGE_SHRINK_RATIO_MIN = 0.3;
|
|
199
193
|
// ── Reflect size gate ────────────────────────────────────────────────────────
|
|
200
194
|
/** Ratio lower-bound: proposed body must be at least this fraction of source. */
|
|
201
195
|
export const REFLECT_SHRINK_RATIO_MIN = 0.5;
|
|
@@ -24,6 +24,7 @@ import { enqueueGraphExtraction, hasGraphData } from "../../indexer/db/graph-db.
|
|
|
24
24
|
import { copySearchHitAttribution, getSearchHitAttribution, usageEventAttributionMetadata, } from "../../indexer/search/search-attribution.js";
|
|
25
25
|
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
26
26
|
import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
27
|
+
import { estimateTokenCount } from "../../llm/embedders/remote.js";
|
|
27
28
|
import { truncateDescription } from "../../output/shapes/helpers.js";
|
|
28
29
|
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
29
30
|
import { findEntryIdByRef, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
@@ -51,7 +52,9 @@ export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
|
51
52
|
export const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
52
53
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
53
54
|
const CURATE_CLOSE_SCORE_BAND = 0.12;
|
|
54
|
-
|
|
55
|
+
// Used by `shouldRunCurateFallback` to judge whether the initial search
|
|
56
|
+
// already returned strong results (a different purpose from ranking/keeping
|
|
57
|
+
// hits — see that function for its own rationale).
|
|
55
58
|
const CURATE_RELATIVE_SCORE_FLOOR = 0.7;
|
|
56
59
|
const CURATE_FALLBACK_TOP_SCORE_THRESHOLD = 0.8;
|
|
57
60
|
const CURATE_FALLBACK_STRONG_SCORE_FLOOR = 0.35;
|
|
@@ -163,6 +166,52 @@ export async function curateSearchResults(query, result, limit, selectedType, ev
|
|
|
163
166
|
...(result.tip ? { tip: result.tip } : {}),
|
|
164
167
|
};
|
|
165
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Pack a curate result's stash hits into a single token-budgeted blob:
|
|
171
|
+
* resolve each hit's content via the SAME path `akm show` uses
|
|
172
|
+
* (`akmShowUnified` — this also means a `ref#fragment` hit packs just the
|
|
173
|
+
* matched section), then greedily accumulate hits, in the ranking order
|
|
174
|
+
* `curateSearchResults` already produced, until the next hit would exceed
|
|
175
|
+
* `budgetTokens`.
|
|
176
|
+
*
|
|
177
|
+
* Registry hits are never packed — only `CuratedStashItem`s (locked
|
|
178
|
+
* contract, AGENTS.md: registry results stay separate/opt-in).
|
|
179
|
+
*
|
|
180
|
+
* Truncation policy: drop whole hits from the tail of the ranked list first.
|
|
181
|
+
* The only exception is a single high-rank hit that alone exceeds the
|
|
182
|
+
* budget — that one hit is truncated to fit rather than dropping everything.
|
|
183
|
+
*/
|
|
184
|
+
export async function packCuratedHits(result, budgetTokens) {
|
|
185
|
+
const stashItems = result.items.filter((item) => item.source === "local");
|
|
186
|
+
const packed = [];
|
|
187
|
+
let used = 0;
|
|
188
|
+
for (const item of stashItems) {
|
|
189
|
+
let shown;
|
|
190
|
+
try {
|
|
191
|
+
shown = await akmShowUnified({ ref: item.ref, skipLogging: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const content = shown.content ?? shown.template ?? shown.prompt ?? "";
|
|
197
|
+
const tokens = estimateTokenCount(content);
|
|
198
|
+
if (used + tokens <= budgetTokens) {
|
|
199
|
+
packed.push({ ref: item.ref, tokens, content });
|
|
200
|
+
used += tokens;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (packed.length === 0) {
|
|
204
|
+
const remaining = budgetTokens - used;
|
|
205
|
+
if (remaining > 0) {
|
|
206
|
+
const truncated = content.slice(0, remaining * 4);
|
|
207
|
+
packed.push({ ref: item.ref, tokens: estimateTokenCount(truncated), content: truncated });
|
|
208
|
+
used += estimateTokenCount(truncated);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
return { query: result.query, budget: budgetTokens, tokens: used, items: packed };
|
|
214
|
+
}
|
|
166
215
|
async function enrichCuratedStashHit(query, hit, supportRefs, selectedRefs, eventSource) {
|
|
167
216
|
let shown;
|
|
168
217
|
try {
|
|
@@ -530,11 +579,6 @@ function compareCurateHits(a, b) {
|
|
|
530
579
|
return rawDiff;
|
|
531
580
|
return a.originalIndex - b.originalIndex;
|
|
532
581
|
}
|
|
533
|
-
function passesCurateScoreFloor(hit, leaderScore) {
|
|
534
|
-
if (leaderScore === undefined)
|
|
535
|
-
return true;
|
|
536
|
-
return hit.rawScore >= Math.max(CURATE_TAIL_SCORE_FLOOR, leaderScore * CURATE_RELATIVE_SCORE_FLOOR);
|
|
537
|
-
}
|
|
538
582
|
function isNarrowReferenceFamilyQuery(query, family) {
|
|
539
583
|
if (!family || family.role !== "reference")
|
|
540
584
|
return false;
|
|
@@ -555,19 +599,8 @@ function selectCuratedStashHits(query, hits, limit) {
|
|
|
555
599
|
const ranked = collapsed.hits
|
|
556
600
|
.map(({ hit, originalIndex }) => annotateCurateHit(query, hit, originalIndex, intent))
|
|
557
601
|
.sort(compareCurateHits);
|
|
558
|
-
const selected = [];
|
|
559
602
|
const supportRefsByRef = collapsed.supportRefsByRef;
|
|
560
|
-
|
|
561
|
-
for (const candidate of ranked) {
|
|
562
|
-
if (!passesCurateScoreFloor(candidate, leaderScore))
|
|
563
|
-
continue;
|
|
564
|
-
selected.push(candidate);
|
|
565
|
-
if (leaderScore === undefined)
|
|
566
|
-
leaderScore = candidate.rawScore;
|
|
567
|
-
if (selected.length >= limit)
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
return { selected: selected.map((entry) => entry.hit), supportRefsByRef };
|
|
603
|
+
return { selected: ranked.slice(0, limit).map((entry) => entry.hit), supportRefsByRef };
|
|
571
604
|
}
|
|
572
605
|
function collapseCurateFamilies(query, hits) {
|
|
573
606
|
const passthrough = [];
|
|
@@ -20,7 +20,8 @@ import { parseMetaRef } from "../../core/asset/stash-meta.js";
|
|
|
20
20
|
import { UsageError } from "../../core/errors.js";
|
|
21
21
|
import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
|
|
22
22
|
import { getOutputMode } from "../../output/context.js";
|
|
23
|
-
import {
|
|
23
|
+
import { deliverRendered } from "../../output/html-render.js";
|
|
24
|
+
import { akmCurate, packCuratedHits } from "./curate.js";
|
|
24
25
|
import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./search.js";
|
|
25
26
|
import { akmShowUnified } from "./show.js";
|
|
26
27
|
/**
|
|
@@ -153,6 +154,15 @@ export const curateCommand = defineJsonCommand({
|
|
|
153
154
|
},
|
|
154
155
|
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
155
156
|
from: { type: "string", description: "Search source (local|registry|all)", default: "local" },
|
|
157
|
+
pack: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Pack the ranked stash hits' full content into a single token-budgeted blob instead of returning refs " +
|
|
160
|
+
"to follow up on individually — value is the max token budget, e.g. --pack 4000 (~4 chars/token, same " +
|
|
161
|
+
"estimator as embedding). Content is resolved the same way `akm show` resolves it, so a ref#fragment " +
|
|
162
|
+
"hit packs just that section. Registry hits (--from registry|all) are never packed. Not to be confused " +
|
|
163
|
+
"with a workflow asset's own `budget` field (a run-cost cap) — this is a context-size target for this " +
|
|
164
|
+
"one curate call.",
|
|
165
|
+
},
|
|
156
166
|
// Declared as the POSITIVE name with `default: true` — see the
|
|
157
167
|
// `project-context` comment on `searchCommand` above for why a flag NAME
|
|
158
168
|
// must never start with `no-`.
|
|
@@ -175,6 +185,7 @@ export const curateCommand = defineJsonCommand({
|
|
|
175
185
|
const source = parseSearchSource(args.from ?? "local");
|
|
176
186
|
const skipLogging = args["track-usage"] === false;
|
|
177
187
|
const outputMode = getOutputMode();
|
|
188
|
+
const packBudget = parsePositiveIntFlag(args.pack ?? undefined, "--pack");
|
|
178
189
|
const curated = await akmCurate({
|
|
179
190
|
query: args.query,
|
|
180
191
|
type,
|
|
@@ -184,9 +195,21 @@ export const curateCommand = defineJsonCommand({
|
|
|
184
195
|
eventSource: resolveUsageEventSource(),
|
|
185
196
|
attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
|
|
186
197
|
});
|
|
198
|
+
if (packBudget !== undefined) {
|
|
199
|
+
const packed = await packCuratedHits(curated, packBudget);
|
|
200
|
+
deliverRendered(outputMode.format === "text" ? formatCuratePackText(packed) : JSON.stringify(packed.items, null, 2), outputMode.outputPath);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
187
203
|
output("curate", curated);
|
|
188
204
|
},
|
|
189
205
|
});
|
|
206
|
+
/** Human-readable rendering for `akm curate --pack`: concatenated content per hit under a `## <ref>` header. */
|
|
207
|
+
function formatCuratePackText(packed) {
|
|
208
|
+
if (packed.items.length === 0) {
|
|
209
|
+
return `No packed content for "${packed.query}" (budget ${packed.budget} tokens).`;
|
|
210
|
+
}
|
|
211
|
+
return packed.items.map((item) => `## ${item.ref}\n\n${item.content}`).join("\n\n");
|
|
212
|
+
}
|
|
190
213
|
/**
|
|
191
214
|
* Reject `--scope` (either spelling) on `akm show` (E-3). `--scope` was
|
|
192
215
|
* removed in favor of `--filter` (R-047, guardrail 6 — no alias, must keep
|
|
@@ -27,7 +27,7 @@ import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.
|
|
|
27
27
|
import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
|
|
28
28
|
import { asNonEmptyString, isWithin } from "../../core/common.js";
|
|
29
29
|
import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
|
|
30
|
-
import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
30
|
+
import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
31
31
|
import { appendEvent } from "../../core/events.js";
|
|
32
32
|
import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
|
|
33
33
|
import { presentationFor } from "../../core/type-presentation.js";
|
|
@@ -292,6 +292,7 @@ export async function showLocal(input) {
|
|
|
292
292
|
}
|
|
293
293
|
catch (err) {
|
|
294
294
|
rethrowIfTestIsolationError(err);
|
|
295
|
+
rethrowIfDataDirUnreadable(err);
|
|
295
296
|
return { total: 0, hits: [] };
|
|
296
297
|
}
|
|
297
298
|
})(),
|
|
@@ -47,7 +47,11 @@ function canonicalJson(value) {
|
|
|
47
47
|
if (value && typeof value === "object") {
|
|
48
48
|
const entries = Object.entries(value)
|
|
49
49
|
.filter(([, entry]) => entry !== undefined)
|
|
50
|
-
|
|
50
|
+
// Codepoint order, NOT localeCompare: this string feeds sha256Hex for the
|
|
51
|
+
// bundle-audit generation, and localeCompare is ICU/locale-dependent — the
|
|
52
|
+
// same object could hash differently on two machines. Matches the other
|
|
53
|
+
// canonicalJson implementations (workflows/ir/plan-hash.ts).
|
|
54
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
51
55
|
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
52
56
|
}
|
|
53
57
|
return JSON.stringify(value);
|
|
@@ -333,7 +333,7 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
|
|
|
333
333
|
ok: false,
|
|
334
334
|
skipped: opts.skip,
|
|
335
335
|
message: `Upgrade completed, but the state schema was not prepared (${detail}). ` +
|
|
336
|
-
"Preserve state.db and run `akm upgrade --
|
|
336
|
+
"Preserve state.db and run `akm upgrade --state-only` before other AKM commands (the binary is already current).",
|
|
337
337
|
};
|
|
338
338
|
}
|
|
339
339
|
const stateNote = stateUpgrade.safetyCopyPath ? ` Historical state safety copy: ${stateUpgrade.safetyCopyPath}.` : "";
|
|
@@ -520,3 +520,40 @@ export function getPackageManagerUpgradeCommand(installMethod, packageName = get
|
|
|
520
520
|
}
|
|
521
521
|
return undefined;
|
|
522
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Apply pending historical destructive state.db migrations WITHOUT installing a
|
|
525
|
+
* new akm — the body of `akm upgrade --state-only`.
|
|
526
|
+
*
|
|
527
|
+
* Migrations flagged `historical-destructive` are refused during an ordinary
|
|
528
|
+
* managed open: they need a verified sibling safety copy taken under the
|
|
529
|
+
* migration writer lock, and that is deliberate, so an unattended `akm index`
|
|
530
|
+
* can never quietly drop operator state.
|
|
531
|
+
*
|
|
532
|
+
* The bug this fixes is not the guard but its reachability (#895). The only
|
|
533
|
+
* code path that set `allowHistoricalDestructiveStateUpgrade` ran as a
|
|
534
|
+
* POST-INSTALL step of a real upgrade, so it sat behind an npm install. Where
|
|
535
|
+
* akm is installed globally by an image and the runtime user is unprivileged,
|
|
536
|
+
* that install fails EACCES and throws long before the migration is reached —
|
|
537
|
+
* leaving the documented remedy impossible to run and `akm index --full`
|
|
538
|
+
* permanently blocked. Nothing about the migration itself needs the network,
|
|
539
|
+
* root, or a new binary; it is local, offline, and already verified.
|
|
540
|
+
*
|
|
541
|
+
* The safety copy is NOT skipped here. This changes only who may ask for the
|
|
542
|
+
* migration, never what it does.
|
|
543
|
+
*/
|
|
544
|
+
export function upgradeStateOnly(currentVersion, dependencies) {
|
|
545
|
+
const upgradeState = dependencies?.upgradeHistoricalStateDatabase ?? upgradeHistoricalStateDatabase;
|
|
546
|
+
const result = upgradeState();
|
|
547
|
+
return {
|
|
548
|
+
currentVersion,
|
|
549
|
+
newVersion: currentVersion,
|
|
550
|
+
upgraded: false,
|
|
551
|
+
installMethod: detectInstallMethod(),
|
|
552
|
+
message: result.upgraded
|
|
553
|
+
? `Applied pending state.db migrations. Safety copy: ${result.safetyCopyPath}`
|
|
554
|
+
: "state.db is already current; no migration was needed",
|
|
555
|
+
stateUpgrade: result.safetyCopyPath
|
|
556
|
+
? { applied: result.upgraded, safetyCopyPath: result.safetyCopyPath }
|
|
557
|
+
: { applied: result.upgraded },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
@@ -33,7 +33,7 @@ import { UsageError } from "../../core/errors.js";
|
|
|
33
33
|
import { appendEvent } from "../../core/events.js";
|
|
34
34
|
import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
|
|
35
35
|
import { pkgVersion } from "../../version.js";
|
|
36
|
-
import { checkForUpdate, performUpgrade } from "./self-update.js";
|
|
36
|
+
import { checkForUpdate, performUpgrade, upgradeStateOnly } from "./self-update.js";
|
|
37
37
|
import { akmClone } from "./source-clone.js";
|
|
38
38
|
export const upgradeCommand = defineJsonCommand({
|
|
39
39
|
meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
|
|
@@ -45,8 +45,24 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
45
45
|
description: "Skip the post-upgrade index rebuild",
|
|
46
46
|
default: false,
|
|
47
47
|
},
|
|
48
|
+
"state-only": {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
description: "Apply pending state.db migrations without installing a new akm",
|
|
51
|
+
default: false,
|
|
52
|
+
},
|
|
48
53
|
},
|
|
49
54
|
async run({ args }) {
|
|
55
|
+
// Applying a historical destructive state migration used to be reachable
|
|
56
|
+
// ONLY as a post-install step of a real upgrade, so an install akm cannot
|
|
57
|
+
// rewrite -- a global npm install owned by root, an image that ships the
|
|
58
|
+
// CLI -- had no route to it at all: the npm step fails EACCES and throws
|
|
59
|
+
// long before the migration runs (#895). The migration is a local,
|
|
60
|
+
// offline, already-verified operation; it does not need the network or a
|
|
61
|
+
// new binary, and coupling it to one was the bug.
|
|
62
|
+
if (args["state-only"]) {
|
|
63
|
+
output("upgrade", upgradeStateOnly(pkgVersion));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
50
66
|
const check = await checkForUpdate(pkgVersion);
|
|
51
67
|
if (args.check) {
|
|
52
68
|
output("upgrade", check);
|
|
@@ -875,14 +875,6 @@ function warnIneligibleRebind(runtime, explicitRebind, warnings) {
|
|
|
875
875
|
return;
|
|
876
876
|
warnings.push(`--rebind bound scheduled tasks to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
|
|
877
877
|
}
|
|
878
|
-
export function prepareSchedulerRuntime(explicitRebind, operation, deps = {}) {
|
|
879
|
-
const invocation = (deps.resolveInvocation ?? resolveAkmInvocation)();
|
|
880
|
-
if (!invocation.eligible && !explicitRebind) {
|
|
881
|
-
throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
|
|
882
|
-
}
|
|
883
|
-
const contextPath = (deps.writeDescriptor ?? writeSchedulerContextDescriptor)(schedulerContextDescriptor());
|
|
884
|
-
return { binding: invocation.argv, contextPath, eligible: invocation.eligible, kind: invocation.kind };
|
|
885
|
-
}
|
|
886
878
|
function groupInstalledBindings(entries, invocation) {
|
|
887
879
|
const groups = new Map();
|
|
888
880
|
for (const entry of entries) {
|
|
@@ -2,40 +2,32 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Check every URL in `entries` and report the ones that are dead.
|
|
7
|
+
*
|
|
8
|
+
* No cap, no per-entry slice, no ceiling option. There used to be a
|
|
9
|
+
* `MAX_URLS = 20` plus an undocumented `slice(0, 3)` per entry, so this
|
|
10
|
+
* examined at most twenty links in a bundle holding thousands and reported
|
|
11
|
+
* success. The first attempt at fixing that kept the cap and layered a
|
|
12
|
+
* coverage report, a warning, and an override on top — a constraint plus more
|
|
13
|
+
* code to explain the constraint. Both are gone. It checks what you asked it
|
|
14
|
+
* to check.
|
|
15
|
+
*
|
|
16
|
+
* A request that fails, times out, or cannot resolve surfaces as a `DeadUrl`
|
|
17
|
+
* rather than being swallowed, so a network problem is visible instead of
|
|
18
|
+
* looking like a clean bill of health.
|
|
19
|
+
*/
|
|
7
20
|
export async function checkDeadUrls(_stashDir, entries) {
|
|
8
|
-
const urlsToCheck = [];
|
|
9
|
-
for (const entry of entries) {
|
|
10
|
-
if (urlsToCheck.length >= MAX_URLS)
|
|
11
|
-
break;
|
|
12
|
-
const matches = entry.body.match(URL_RE) ?? [];
|
|
13
|
-
for (const url of matches.slice(0, 3)) {
|
|
14
|
-
urlsToCheck.push({ ref: entry.ref, url });
|
|
15
|
-
if (urlsToCheck.length >= MAX_URLS)
|
|
16
|
-
break;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
21
|
+
const urlsToCheck = entries.flatMap((entry) => (entry.body.match(URL_RE) ?? []).map((url) => ({ ref: entry.ref, url })));
|
|
19
22
|
const results = [];
|
|
20
23
|
await Promise.allSettled(urlsToCheck.map(async ({ ref, url }) => {
|
|
21
24
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const res = await fetch(url, {
|
|
25
|
-
method: "HEAD",
|
|
26
|
-
signal: controller.signal,
|
|
27
|
-
redirect: "follow",
|
|
28
|
-
});
|
|
29
|
-
clearTimeout(timer);
|
|
30
|
-
if (res.status >= 400) {
|
|
25
|
+
const res = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
26
|
+
if (res.status >= 400)
|
|
31
27
|
results.push({ ref, url, status: res.status });
|
|
32
|
-
}
|
|
33
28
|
}
|
|
34
|
-
catch
|
|
35
|
-
|
|
36
|
-
results.push({ ref, url, status: "timeout" });
|
|
37
|
-
}
|
|
38
|
-
// network errors (ENOTFOUND etc.) — skip, don't report as dead
|
|
29
|
+
catch {
|
|
30
|
+
results.push({ ref, url, status: "error" });
|
|
39
31
|
}
|
|
40
32
|
}));
|
|
41
33
|
return results;
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import fs from "node:fs";
|
|
45
45
|
import path from "node:path";
|
|
46
46
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
47
|
+
import { toPosix } from "../../common.js";
|
|
47
48
|
import { hashContent, nonEmptyString, readTags } from "./shared.js";
|
|
48
49
|
/** A skills collection is a single-component bundle; its one component is `main`. */
|
|
49
50
|
const COMPONENT_ID = "main";
|
|
@@ -56,11 +57,6 @@ const DESCRIPTION_MAX = 1024;
|
|
|
56
57
|
const RESERVED_NAME_WORDS = ["anthropic", "claude"];
|
|
57
58
|
/** name charset/shape rule (spec §4.5). */
|
|
58
59
|
const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
59
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
60
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
61
|
-
function toPosix(p) {
|
|
62
|
-
return p.replace(/\\/g, "/");
|
|
63
|
-
}
|
|
64
60
|
/** conceptId + package-dir name for a `<name>/SKILL.md` file, or null when the file is not a skill manifest. */
|
|
65
61
|
function skillPackage(relPath) {
|
|
66
62
|
const posix = toPosix(relPath);
|
|
@@ -95,7 +91,7 @@ function recognize(c, file) {
|
|
|
95
91
|
adapterId: "agent-skills",
|
|
96
92
|
type: "skill",
|
|
97
93
|
name,
|
|
98
|
-
content: body
|
|
94
|
+
content: body,
|
|
99
95
|
};
|
|
100
96
|
if (description !== undefined)
|
|
101
97
|
doc.description = description;
|
|
@@ -88,7 +88,7 @@ import { executionDefaultsFromFrontmatter, renderMarkdownExecutionSource } from
|
|
|
88
88
|
import { recognizeMatch } from "../recognize-match.js";
|
|
89
89
|
import { perTypeValidateChecks, skillDirectoryDiagnostics, workflowYamlSourceDiagnostics } from "./akm-lint.js";
|
|
90
90
|
import { applyFoldedMetadata, foldRecognizedMetadata } from "./akm-metadata.js";
|
|
91
|
-
import { hashContent, runBaseValidateChecks } from "./shared.js";
|
|
91
|
+
import { hashContent, RESERVED_FILES, runBaseValidateChecks } from "./shared.js";
|
|
92
92
|
// `recognizeMatch` + the builtin matcher list moved to the cycle-free leaf
|
|
93
93
|
// `../recognize-match` (Chunk 5 M-b) so both this adapter AND the indexer
|
|
94
94
|
// metadata pass import it without closing a metadata ↔ adapter cycle. Re-exported
|
|
@@ -99,12 +99,11 @@ export { recognizeMatch } from "../recognize-match.js";
|
|
|
99
99
|
* OKF reserved structural files (ref-grammar decision D-R6, spec §5.1 /
|
|
100
100
|
* adapter-spec §5.1/§6): `index.md` (directory listing) and `log.md` (update
|
|
101
101
|
* history) are bundle structure at EVERY depth — never a concept document. The
|
|
102
|
-
* `okf` / `llm-wiki` adapters already exclude them (`
|
|
102
|
+
* `okf` / `llm-wiki` adapters already exclude them (`shared.ts`'s
|
|
103
103
|
* `RESERVED_FILES`); this brings the `akm` adapter into conformance so a
|
|
104
104
|
* `knowledge/index.md` never classifies as a `knowledge` item. Case-insensitive,
|
|
105
105
|
* matched on the bare filename so the exclusion holds at any depth.
|
|
106
106
|
*/
|
|
107
|
-
const RESERVED_FILES = new Set(["index.md", "log.md"]);
|
|
108
107
|
/** True when `name` (a bare file name) is an OKF reserved file, case-insensitively (D-R6). */
|
|
109
108
|
function isReservedFileName(name) {
|
|
110
109
|
return RESERVED_FILES.has(name.toLowerCase());
|
|
@@ -57,7 +57,7 @@ import { taskSourceErrorDetail } from "../../../tasks/source-v3.js";
|
|
|
57
57
|
import { compileWorkflowPlan } from "../../../workflows/ir/compile.js";
|
|
58
58
|
import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
|
|
59
59
|
import { conceptIdForStashFile } from "../../asset/resolve-ref.js";
|
|
60
|
-
import { isAkmRegistryCachePath } from "../../common.js";
|
|
60
|
+
import { isAkmRegistryCachePath, scanEnvKeyNames } from "../../common.js";
|
|
61
61
|
/** Recommended `category` values for facts — `commands/lint/fact-linter.ts:9`. */
|
|
62
62
|
const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
|
|
63
63
|
/** Placeholder markers a workflow stub carries — `commands/lint/workflow-linter.ts:10`. */
|
|
@@ -118,24 +118,6 @@ export function nameOrTypeDiagnostics(relPath, data, frontmatter, allowedTypes)
|
|
|
118
118
|
return [];
|
|
119
119
|
}
|
|
120
120
|
// ── env dangerous-key scan (content-based port of env-key-rules.ts) ──────────
|
|
121
|
-
/** Matches a KEY=value assignment line, capturing only the key — `commands/env/env.ts:62`. */
|
|
122
|
-
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
123
|
-
/** Port of `commands/env/env.ts#scanKeys` (`:65-77`) — content-based (overlay `raw`, not disk). */
|
|
124
|
-
function scanKeys(text) {
|
|
125
|
-
const keys = [];
|
|
126
|
-
const seen = new Set();
|
|
127
|
-
for (const line of text.split(/\r?\n/)) {
|
|
128
|
-
const m = line.match(ASSIGN_RE);
|
|
129
|
-
if (!m)
|
|
130
|
-
continue;
|
|
131
|
-
const key = m[1];
|
|
132
|
-
if (seen.has(key))
|
|
133
|
-
continue;
|
|
134
|
-
seen.add(key);
|
|
135
|
-
keys.push(key);
|
|
136
|
-
}
|
|
137
|
-
return keys;
|
|
138
|
-
}
|
|
139
121
|
/** Port of `env-key-rules.ts#collectSuppressedKeys` (`:144-164`) — content-based. */
|
|
140
122
|
function collectSuppressedKeys(raw) {
|
|
141
123
|
const suppressed = new Set();
|
|
@@ -171,7 +153,7 @@ export function dangerousEnvKeyDiagnostics(type, relPath, raw) {
|
|
|
171
153
|
return []; // NARROWNESS: collectEnvFiles only visits *.env
|
|
172
154
|
// `relPath` is already stash-root-relative, so "." IS the stash root here.
|
|
173
155
|
const ref = conceptIdForStashFile(type, ".", relPath);
|
|
174
|
-
const keys =
|
|
156
|
+
const keys = scanEnvKeyNames(raw);
|
|
175
157
|
const suppressed = collectSuppressedKeys(raw);
|
|
176
158
|
const diagnostics = [];
|
|
177
159
|
for (const key of keys) {
|
|
@@ -35,16 +35,12 @@ import fs from "node:fs";
|
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
|
|
37
37
|
import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail, taskSourceErrorDetail, } from "../../../tasks/source-v3.js";
|
|
38
|
+
import { toPosix } from "../../common.js";
|
|
38
39
|
import { hashContent } from "./shared.js";
|
|
39
40
|
/** A native task bundle is single-component; its one component is `main`. */
|
|
40
41
|
const COMPONENT_ID = "main";
|
|
41
42
|
/** The task YAML extension (spec §6 task row). */
|
|
42
43
|
const TASK_EXT = TASK_EXTENSION;
|
|
43
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
44
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
45
|
-
function toPosix(p) {
|
|
46
|
-
return p.replace(/\\/g, "/");
|
|
47
|
-
}
|
|
48
44
|
function recognize(c, file) {
|
|
49
45
|
if (file.ext !== TASK_EXT)
|
|
50
46
|
return null;
|
|
@@ -61,7 +57,7 @@ function recognize(c, file) {
|
|
|
61
57
|
adapterId: "akm-task",
|
|
62
58
|
type: "task",
|
|
63
59
|
name,
|
|
64
|
-
content: raw
|
|
60
|
+
content: raw,
|
|
65
61
|
};
|
|
66
62
|
}
|
|
67
63
|
async function validate(c, changes, ctx) {
|
|
@@ -23,17 +23,13 @@ import fs from "node:fs";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { looksLikeGithubWorkflowSource } from "../../../workflows/source-ir/compile.js";
|
|
25
25
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
26
|
+
import { toPosix } from "../../common.js";
|
|
26
27
|
import { perTypeValidateChecks, workflowYamlSourceDiagnostics } from "./akm-lint.js";
|
|
27
28
|
import { hashContent, nonEmptyString, readTags, runBaseValidateChecks } from "./shared.js";
|
|
28
29
|
/** A native workflow bundle is single-component; its one component is `main`. */
|
|
29
30
|
const COMPONENT_ID = "main";
|
|
30
31
|
/** The two authoritative workflow source formats. `.yaml` is deliberately not accepted. */
|
|
31
32
|
const WORKFLOW_EXTS = new Set([".md", ".yml"]);
|
|
32
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
33
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
34
|
-
function toPosix(p) {
|
|
35
|
-
return p.replace(/\\/g, "/");
|
|
36
|
-
}
|
|
37
33
|
/** Strip the recognized workflow extension from a component-root-relative path → conceptId. */
|
|
38
34
|
function conceptIdOf(relPath) {
|
|
39
35
|
return toPosix(relPath).replace(/\.(?:md|yml)$/i, "");
|
|
@@ -78,7 +74,7 @@ function recognize(c, file) {
|
|
|
78
74
|
adapterId: "akm-workflow",
|
|
79
75
|
type: "workflow",
|
|
80
76
|
name,
|
|
81
|
-
content: body
|
|
77
|
+
content: body,
|
|
82
78
|
};
|
|
83
79
|
if (description !== undefined)
|
|
84
80
|
doc.description = description;
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import fs from "node:fs";
|
|
31
31
|
import path from "node:path";
|
|
32
32
|
import { assetPathCandidatesForName, assetPathForName, typeForStashDir } from "../../asset/asset-placement.js";
|
|
33
|
+
import { scanEnvKeyNames, toPosix } from "../../common.js";
|
|
33
34
|
import { dangerousEnvKeyDiagnostics } from "./akm-lint.js";
|
|
34
35
|
import { hashContent } from "./shared.js";
|
|
35
36
|
/** A dotenv bundle is single-component; its one component is `main`. */
|
|
@@ -40,11 +41,6 @@ const ENV_DIR = "env";
|
|
|
40
41
|
const SECRETS_DIR = "secrets";
|
|
41
42
|
/** Non-secret marker suffixes under `secrets/` (spec §6 secret row). */
|
|
42
43
|
const SECRET_SKIP_SUFFIXES = [".lock", ".sensitive"];
|
|
43
|
-
/** Matches a KEY=value assignment line, capturing only the key (mirrors akm-lint scanKeys). */
|
|
44
|
-
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
45
|
-
function toPosix(p) {
|
|
46
|
-
return p.replace(/\\/g, "/");
|
|
47
|
-
}
|
|
48
44
|
/** Classify a component-root-relative file as env / secret, or null (abstain). */
|
|
49
45
|
function classify(relPath) {
|
|
50
46
|
const posix = toPosix(relPath);
|
|
@@ -80,22 +76,6 @@ function hasSensitiveMarker(absPath, type) {
|
|
|
80
76
|
const marker = type === "env" ? absPath.replace(/\.env$/i, ".sensitive") : `${absPath}.sensitive`;
|
|
81
77
|
return marker !== absPath && fs.existsSync(marker);
|
|
82
78
|
}
|
|
83
|
-
/** Extract KEY NAMES (never values) from an env file's raw content, first-appearance order, deduped. */
|
|
84
|
-
function scanKeyNames(raw) {
|
|
85
|
-
const keys = [];
|
|
86
|
-
const seen = new Set();
|
|
87
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
88
|
-
const m = line.match(ASSIGN_RE);
|
|
89
|
-
if (!m)
|
|
90
|
-
continue;
|
|
91
|
-
const key = m[1];
|
|
92
|
-
if (seen.has(key))
|
|
93
|
-
continue;
|
|
94
|
-
seen.add(key);
|
|
95
|
-
keys.push(key);
|
|
96
|
-
}
|
|
97
|
-
return keys;
|
|
98
|
-
}
|
|
99
79
|
function conceptIdForPath(type, relativePath) {
|
|
100
80
|
const posix = toPosix(relativePath);
|
|
101
81
|
if (type === "secret")
|
|
@@ -115,7 +95,7 @@ function recognize(c, file) {
|
|
|
115
95
|
// env: strip `.env`; surface KEY NAMES only (never values/comments/content).
|
|
116
96
|
const conceptId = conceptIdForPath(type, posix);
|
|
117
97
|
const name = (conceptId.split("/").pop() ?? conceptId) || "default";
|
|
118
|
-
const keys =
|
|
98
|
+
const keys = scanEnvKeyNames(raw);
|
|
119
99
|
const doc = {
|
|
120
100
|
ref: `${c.id}//${conceptId}`,
|
|
121
101
|
bundle: c.id,
|