akm-cli 0.9.0 → 0.9.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +724 -0
- package/README.md +28 -63
- package/STABILITY.md +4 -2
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/feedback-cli.js +7 -1
- package/dist/commands/health/llm-usage.js +2 -1
- package/dist/commands/health/surfaces.js +4 -77
- package/dist/commands/health.js +65 -11
- package/dist/commands/improve/distill/quality-gate.js +6 -1
- package/dist/commands/improve/eligibility.js +7 -1
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/improve.js +126 -10
- package/dist/commands/improve/locks.js +7 -0
- package/dist/commands/improve/memory/memory-improve.js +9 -0
- package/dist/commands/improve/run-context.js +5 -0
- package/dist/commands/improve/session-asset.js +4 -0
- package/dist/commands/lint/base-linter.js +31 -7
- package/dist/commands/lint/index.js +205 -51
- package/dist/commands/lint/types.js +22 -1
- package/dist/commands/proposal/repository.js +17 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/info.js +12 -2
- package/dist/commands/sources/installed-stashes.js +6 -1
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/commands/tasks/tasks.js +8 -2
- package/dist/commands/workflow-cli.js +17 -11
- package/dist/core/abort-deadline.js +28 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
- package/dist/core/adapter/adapters/akm-adapter.js +13 -10
- package/dist/core/adapter/adapters/akm-lint.js +78 -22
- package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/common.js +147 -9
- package/dist/core/concurrent.js +32 -0
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/config/schema/workflow.js +11 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/events.js +30 -24
- package/dist/core/extra-params.js +11 -0
- package/dist/core/file-lock.js +7 -1
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/improve-result.js +5 -0
- package/dist/core/json-schema.js +344 -9
- package/dist/core/loopback.js +89 -0
- package/dist/core/migration-operation.js +17 -2
- package/dist/core/path-access.js +107 -0
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +86 -18
- package/dist/core/spawn-env.js +234 -0
- package/dist/core/state-db-scope.js +134 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/subprocess.js +181 -37
- package/dist/core/write-provenance.js +85 -0
- package/dist/core/write-source.js +33 -2
- package/dist/indexer/db/graph-db.js +17 -6
- package/dist/indexer/ensure-index.js +10 -3
- package/dist/indexer/index-written-assets.js +17 -2
- package/dist/indexer/indexer.js +86 -21
- package/dist/indexer/passes/memory-inference.js +4 -0
- package/dist/indexer/search/db-search.js +25 -17
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/agent/engine-resolution.js +24 -11
- package/dist/integrations/agent/model-aliases.js +1 -1
- package/dist/integrations/agent/profiles.js +9 -1
- package/dist/integrations/agent/spawn.js +15 -87
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +55 -2
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/output/text/lint-format.js +17 -4
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +1714 -836
- package/dist/scripts/akm-migrate.js +1682 -804
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/providers/git-stash.js +19 -0
- package/dist/sources/providers/git.js +1 -1
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
- package/dist/storage/database.js +71 -7
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/managed-db.js +19 -0
- package/dist/storage/repositories/index-connection.js +39 -4
- package/dist/storage/repositories/index-entries-repository.js +6 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/repositories/workflow-runs-repository.js +66 -13
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/log-redaction.js +156 -0
- package/dist/tasks/parser.js +82 -5
- package/dist/tasks/runner.js +222 -17
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +86 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/concurrency-policy.js +95 -1
- package/dist/workflows/exec/dispatch-redaction.js +114 -0
- package/dist/workflows/exec/exec-unit.js +542 -0
- package/dist/workflows/exec/frozen-judge.js +114 -42
- package/dist/workflows/exec/native-executor.js +465 -238
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +424 -219
- package/dist/workflows/exec/step-work.js +506 -167
- package/dist/workflows/exec/unit-dispatch.js +31 -1
- package/dist/workflows/exec/unit-writer.js +53 -13
- package/dist/workflows/exec/worktree.js +454 -41
- package/dist/workflows/ir/compile.js +26 -2
- package/dist/workflows/ir/freeze.js +82 -15
- package/dist/workflows/ir/schema.js +105 -20
- package/dist/workflows/parser.js +242 -19
- package/dist/workflows/program/schema.js +24 -0
- package/dist/workflows/renderer.js +32 -4
- package/dist/workflows/resource-limits.js +182 -0
- package/dist/workflows/runtime/runs.js +146 -6
- package/dist/workflows/validate-summary.js +17 -2
- package/docs/README.md +74 -32
- package/docs/migration/release-notes/0.9.0.md +2 -1
- package/docs/migration/v0.7-to-v0.8.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +11 -4
- package/docs/reference/bundle-types.md +19 -0
- package/docs/reference/cli.md +105 -16
- package/docs/reference/configuration.md +15 -2
- package/docs/reference/data-and-telemetry.md +30 -10
- package/docs/reference/supported-formats.md +50 -0
- package/docs/reference/workflow-schema.md +1014 -0
- package/docs/reference/workflows.md +37 -633
- package/package.json +13 -6
- package/schemas/akm-config.json +18 -5
- package/schemas/akm-task.json +27 -5
- package/schemas/akm-workflow.json +92 -13
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import { factDiagnostics, matchWorkflowPlaceholder, memoryOrphanStubApplies, nameOrTypeDiagnostics, ORPHANED_STUB_DETAIL, taskDiagnostics, workflowStructureDiagnostics, } from "../../core/adapter/adapters/akm-lint.js";
|
|
6
|
+
import { factDiagnostics, matchWorkflowPlaceholder, memoryOrphanStubApplies, nameOrTypeDiagnostics, ORPHANED_STUB_DETAIL, taskDiagnostics, workflowFrontendDiagnostics, } from "../../core/adapter/adapters/akm-lint.js";
|
|
8
7
|
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
9
8
|
import { adapterForId } from "../../core/adapter/registry.js";
|
|
10
9
|
import { createValidateContext } from "../../core/adapter/validate-context.js";
|
|
@@ -15,9 +14,12 @@ import { deriveBundleIds } from "../../core/bundle-id.js";
|
|
|
15
14
|
import { resolveStashDir } from "../../core/common.js";
|
|
16
15
|
import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
|
|
17
16
|
import { UsageError } from "../../core/errors.js";
|
|
17
|
+
import { warn } from "../../core/warn.js";
|
|
18
18
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
19
|
+
import { parseTaskYaml, TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail, taskYamlParseDetail, } from "../../tasks/schema.js";
|
|
19
20
|
import { runBaseChecks } from "./base-linter.js";
|
|
20
21
|
import { checkEnvForDangerousKeys } from "./env-key-rules.js";
|
|
22
|
+
import { isAdvisoryLintIssue } from "./types.js";
|
|
21
23
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
22
24
|
const STASH_SUBDIRS = [
|
|
23
25
|
"agents",
|
|
@@ -31,21 +33,34 @@ const STASH_SUBDIRS = [
|
|
|
31
33
|
"facts",
|
|
32
34
|
];
|
|
33
35
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Every task-shaped file under `tasks/`: the recognized `.yml` spelling AND the
|
|
38
|
+
* `.yaml` near-miss. A `.yaml` file is not a runnable task — it is invisible to
|
|
39
|
+
* the indexer's `tasks` matcher — but collecting it here is what lets the sweep
|
|
40
|
+
* SAY so (`invalid-task-yaml`, see {@link taskExtensionDetail}) instead of
|
|
41
|
+
* walking past it and reporting a clean scan (issue #760).
|
|
42
|
+
*/
|
|
43
|
+
function collectTaskFiles(dir) {
|
|
35
44
|
if (!fs.existsSync(dir))
|
|
36
45
|
return [];
|
|
37
46
|
const results = [];
|
|
38
47
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
39
48
|
const full = path.join(dir, entry.name);
|
|
40
49
|
if (entry.isDirectory()) {
|
|
41
|
-
results.push(...
|
|
50
|
+
results.push(...collectTaskFiles(full));
|
|
42
51
|
}
|
|
43
|
-
else if (entry.isFile() && entry.name
|
|
52
|
+
else if (entry.isFile() && (isTaskFileName(entry.name) || isNearMissTaskFileName(entry.name))) {
|
|
44
53
|
results.push(full);
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
return results;
|
|
48
57
|
}
|
|
58
|
+
function isTaskFileName(fileName) {
|
|
59
|
+
return fileName.toLowerCase().endsWith(TASK_EXTENSION);
|
|
60
|
+
}
|
|
61
|
+
function isNearMissTaskFileName(fileName) {
|
|
62
|
+
return fileName.toLowerCase().endsWith(TASK_NEAR_MISS_EXTENSION);
|
|
63
|
+
}
|
|
49
64
|
function collectMarkdownFiles(dir, caseInsensitive = false) {
|
|
50
65
|
if (!fs.existsSync(dir))
|
|
51
66
|
return [];
|
|
@@ -141,18 +156,32 @@ const KNOWN_ADAPTER_ISSUE_TYPES = new Set([
|
|
|
141
156
|
"missing-type",
|
|
142
157
|
"missing-name-or-type",
|
|
143
158
|
"missing-skill-md",
|
|
159
|
+
// The `akm-task` adapter's own code. Now reachable from `akm lint` for a
|
|
160
|
+
// malformed or misnamed task file (issue #760); without it here, a genuine
|
|
161
|
+
// task finding would arrive folded onto `adapter-diagnostic`.
|
|
162
|
+
"invalid-task-yaml",
|
|
144
163
|
"dangerous-env-key",
|
|
145
164
|
"uncited-raw",
|
|
146
165
|
"missing-description",
|
|
147
166
|
"broken-xref",
|
|
148
167
|
"broken-source",
|
|
168
|
+
"workflow-warning",
|
|
149
169
|
]);
|
|
150
170
|
/** Map one adapter {@link Diagnostic} onto a {@link LintIssue} — see `types.ts`'s `"adapter-diagnostic"` doc comment for the open→closed reconciliation. */
|
|
151
171
|
export function diagnosticToLintIssue(diag) {
|
|
172
|
+
// `line` is optional on both shapes: carry it only when the adapter set one,
|
|
173
|
+
// so whole-file findings keep their exact existing serialization.
|
|
174
|
+
const location = typeof diag.line === "number" ? { line: diag.line } : {};
|
|
152
175
|
if (KNOWN_ADAPTER_ISSUE_TYPES.has(diag.issue)) {
|
|
153
|
-
return { file: diag.file, issue: diag.issue, detail: diag.detail, fixed: diag.fixed };
|
|
176
|
+
return { file: diag.file, issue: diag.issue, detail: diag.detail, fixed: diag.fixed, ...location };
|
|
154
177
|
}
|
|
155
|
-
return {
|
|
178
|
+
return {
|
|
179
|
+
file: diag.file,
|
|
180
|
+
issue: "adapter-diagnostic",
|
|
181
|
+
detail: `[${diag.issue}] ${diag.detail}`,
|
|
182
|
+
fixed: diag.fixed,
|
|
183
|
+
...location,
|
|
184
|
+
};
|
|
156
185
|
}
|
|
157
186
|
/**
|
|
158
187
|
* Lint a bundle through its OWN adapter's `validate()` (spec §12.1): the
|
|
@@ -169,6 +198,17 @@ async function lintViaAdapter(adapterId, stashRoot, extraStashRoots, sources, cf
|
|
|
169
198
|
// always applied to a bundle it can't otherwise place.
|
|
170
199
|
if (!adapter)
|
|
171
200
|
return lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options);
|
|
201
|
+
// `--type` names an AKM stash subdir; every other adapter has its own type
|
|
202
|
+
// vocabulary and `validate()` sees the whole bundle regardless. That is not a
|
|
203
|
+
// correctness problem (full-bundle validation is a superset of the requested
|
|
204
|
+
// scope), but a user narrowing a run deserves to hear the flag did nothing
|
|
205
|
+
// rather than infer it from identical output (issue #762). Warn, don't throw:
|
|
206
|
+
// a hard error would break scripts passing one `--type` across mixed-adapter
|
|
207
|
+
// bundle sets.
|
|
208
|
+
if (options.typeFilter) {
|
|
209
|
+
warn(`Warning: lint --type "${options.typeFilter}" is not supported for the "${adapterId}" adapter — ` +
|
|
210
|
+
"type scoping applies to akm bundles only; the whole bundle was validated.");
|
|
211
|
+
}
|
|
172
212
|
const files = collectAdapterFiles(stashRoot, adapter.extensions);
|
|
173
213
|
const changes = files.map((filePath) => ({
|
|
174
214
|
path: path.relative(stashRoot, filePath).replace(/\\/g, "/"),
|
|
@@ -179,7 +219,13 @@ async function lintViaAdapter(adapterId, stashRoot, extraStashRoots, sources, cf
|
|
|
179
219
|
const componentId = sourceIndex >= 0 ? ids[sourceIndex] : stashRoot;
|
|
180
220
|
const ctx = createValidateContext({ root: stashRoot, extraRoots: extraStashRoots });
|
|
181
221
|
const diagnostics = await adapter.validate({ id: componentId, adapter: adapterId, root: stashRoot, writable: true }, changes, ctx);
|
|
182
|
-
const
|
|
222
|
+
const mapped = diagnostics.map(diagnosticToLintIssue);
|
|
223
|
+
// Advisory diagnostics travel in their own channel — never `flagged`, so a
|
|
224
|
+
// `--fail-on-flagged` gate is not tripped by a non-fatal warning. Classified
|
|
225
|
+
// by the shared `ADVISORY_LINT_ISSUES` set rather than a code spelled out
|
|
226
|
+
// here, so this and the sweep below can never disagree about a code.
|
|
227
|
+
const warnings = mapped.filter(isAdvisoryLintIssue);
|
|
228
|
+
const flagged = mapped.filter((issue) => !isAdvisoryLintIssue(issue));
|
|
183
229
|
// The cross-bundle env dangerous-key sweep (see `runEnvDangerousKeyPass`'s
|
|
184
230
|
// doc comment) ran for every non-akm adapter via the STASH_SUBDIRS
|
|
185
231
|
// fallthrough this dispatch replaces — EXCEPT `okf`, which the old code
|
|
@@ -199,7 +245,13 @@ async function lintViaAdapter(adapterId, stashRoot, extraStashRoots, sources, cf
|
|
|
199
245
|
flagged.push(issue);
|
|
200
246
|
}
|
|
201
247
|
}
|
|
202
|
-
return {
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
fixed: [],
|
|
251
|
+
flagged,
|
|
252
|
+
warnings,
|
|
253
|
+
summary: { fixed: 0, flagged: flagged.length, warnings: warnings.length },
|
|
254
|
+
};
|
|
203
255
|
}
|
|
204
256
|
function lintIssueDedupeKey(issue) {
|
|
205
257
|
return `${issue.file} ${issue.issue} ${issue.detail}`;
|
|
@@ -259,6 +311,29 @@ function runEnvDangerousKeyPass(stashRoot, extraStashRoots, sources, cfg) {
|
|
|
259
311
|
}
|
|
260
312
|
return flagged;
|
|
261
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Refuse `--fix` against a bundle the config marks `writable: false`, BEFORE
|
|
316
|
+
* the sweep touches a single file (issue #761).
|
|
317
|
+
*
|
|
318
|
+
* Every other mutating command routes through `core/write-source.ts`'s
|
|
319
|
+
* `ensureWritable`/`resolveWritable` pair; `akm lint --fix` writes and deletes
|
|
320
|
+
* directly and never consulted the flag, so it happily rewrote frontmatter in a
|
|
321
|
+
* bundle explicitly configured read-only. `SearchSource.writable` is already the
|
|
322
|
+
* EFFECTIVE policy (`resolveWritable` applied — see `resolveSourceEntries`), so
|
|
323
|
+
* this reads the same answer the write path would, without a second resolver.
|
|
324
|
+
*
|
|
325
|
+
* A root that is not a configured source at all (an ad-hoc `--dir`) carries no
|
|
326
|
+
* policy and stays fixable, exactly as today.
|
|
327
|
+
*/
|
|
328
|
+
function assertFixTargetWritable(stashRoot, sources) {
|
|
329
|
+
const target = sources.find((source) => path.resolve(source.path) === path.resolve(stashRoot));
|
|
330
|
+
if (target?.writable !== false)
|
|
331
|
+
return;
|
|
332
|
+
// Same error kind and code `write-source.ts#ensureWritable` raises for the
|
|
333
|
+
// identical refusal, so a scripted caller classifies both the same way.
|
|
334
|
+
throw new UsageError(`lint --fix: bundle "${stashRoot}" is configured \`writable: false\`; refusing to modify it. ` +
|
|
335
|
+
"Run `akm lint` without --fix to report findings, or set `writable: true` on the bundle.", "INVALID_FLAG_VALUE");
|
|
336
|
+
}
|
|
262
337
|
/** True when the issue represents a file deletion that was successfully applied. */
|
|
263
338
|
function isFileDeletion(issue) {
|
|
264
339
|
return issue.fixed === true && (issue.issue === "orphaned-stub" || issue.issue === "placeholder-stub");
|
|
@@ -308,46 +383,53 @@ function appendMemoryStubIssue(ctx, issues) {
|
|
|
308
383
|
}
|
|
309
384
|
issues.push({ file: ctx.relPath, issue: "orphaned-stub", detail: ORPHANED_STUB_DETAIL, fixed: false });
|
|
310
385
|
}
|
|
311
|
-
/**
|
|
312
|
-
|
|
386
|
+
/**
|
|
387
|
+
* WorkflowLinter's `placeholder-stub` check WITH its `--fix` delete
|
|
388
|
+
* (workflow-linter.ts:22-79). Its sibling `invalid-workflow-structure` check is
|
|
389
|
+
* deliberately NOT here: parse+compile is a single pass shared with the
|
|
390
|
+
* advisory channel, so {@link lintAkmSweep} runs it once per file and routes
|
|
391
|
+
* both halves.
|
|
392
|
+
*/
|
|
393
|
+
function appendWorkflowStubIssue(ctx, issues) {
|
|
313
394
|
const placeholder = matchWorkflowPlaceholder(ctx.body);
|
|
314
|
-
if (placeholder)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
catch (e) {
|
|
326
|
-
issues.push({
|
|
327
|
-
file: ctx.relPath,
|
|
328
|
-
issue: "placeholder-stub",
|
|
329
|
-
detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
|
|
330
|
-
fixed: "failed",
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
return; // WorkflowLinter returns before the structure check once a stub is fixed.
|
|
395
|
+
if (!placeholder)
|
|
396
|
+
return;
|
|
397
|
+
if (ctx.fix) {
|
|
398
|
+
try {
|
|
399
|
+
fs.unlinkSync(ctx.filePath);
|
|
400
|
+
issues.push({
|
|
401
|
+
file: ctx.relPath,
|
|
402
|
+
issue: "placeholder-stub",
|
|
403
|
+
detail: `deleted: found "${placeholder}"`,
|
|
404
|
+
fixed: true,
|
|
405
|
+
});
|
|
334
406
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
407
|
+
catch (e) {
|
|
408
|
+
issues.push({
|
|
409
|
+
file: ctx.relPath,
|
|
410
|
+
issue: "placeholder-stub",
|
|
411
|
+
detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
|
|
412
|
+
fixed: "failed",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
341
416
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
417
|
+
issues.push({
|
|
418
|
+
file: ctx.relPath,
|
|
419
|
+
issue: "placeholder-stub",
|
|
420
|
+
detail: `placeholder text: "${placeholder}"`,
|
|
421
|
+
fixed: false,
|
|
422
|
+
});
|
|
345
423
|
}
|
|
346
424
|
/**
|
|
347
425
|
* Lint ONE asset file: the shared base checks, then the winning stash subdir's
|
|
348
426
|
* per-`type` extra rules. Replaces `getLinterForType(subdir).lint(ctx)`.
|
|
349
427
|
* `--fix` mutations (frontmatter rewrites inside `runBaseChecks`; stub deletes
|
|
350
428
|
* here) are applied when `ctx.fix` is set.
|
|
429
|
+
*
|
|
430
|
+
* The workflow parse/compile frontend is NOT one of these rules — it is one
|
|
431
|
+
* pass feeding two channels, so {@link lintAkmSweep} owns it (see
|
|
432
|
+
* {@link appendWorkflowStubIssue}).
|
|
351
433
|
*/
|
|
352
434
|
export function lintAssetFile(ctx, subdir) {
|
|
353
435
|
const issues = runBaseChecks(ctx);
|
|
@@ -368,7 +450,7 @@ export function lintAssetFile(ctx, subdir) {
|
|
|
368
450
|
appendMemoryStubIssue(ctx, issues);
|
|
369
451
|
break;
|
|
370
452
|
case "workflows":
|
|
371
|
-
|
|
453
|
+
appendWorkflowStubIssue(ctx, issues);
|
|
372
454
|
break;
|
|
373
455
|
// knowledge / lessons / skills: base checks only (skill directory-level
|
|
374
456
|
// `missing-skill-md` runs separately, per-subdir, in the sweep loop).
|
|
@@ -387,14 +469,17 @@ export function lintAssetFile(ctx, subdir) {
|
|
|
387
469
|
*/
|
|
388
470
|
function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
389
471
|
const fix = options.fix === true;
|
|
472
|
+
if (fix)
|
|
473
|
+
assertFixTargetWritable(stashRoot, sources);
|
|
390
474
|
const fixed = [];
|
|
391
475
|
const flagged = [];
|
|
476
|
+
const warnings = [];
|
|
392
477
|
const dirsToScan = options.typeFilter ? STASH_SUBDIRS.filter((d) => d === options.typeFilter) : STASH_SUBDIRS;
|
|
393
478
|
for (const subdir of dirsToScan) {
|
|
394
479
|
const dirPath = path.join(stashRoot, subdir);
|
|
395
480
|
// Tasks are .yml files; everything else (including workflows, one
|
|
396
481
|
// markdown format now) is .md
|
|
397
|
-
const files = subdir === "tasks" ?
|
|
482
|
+
const files = subdir === "tasks" ? collectTaskFiles(dirPath) : collectMarkdownFiles(dirPath, true);
|
|
398
483
|
const assetFiles = subdir === "workflows" ? files.filter((file) => path.basename(file).toLowerCase() !== "readme.md") : files;
|
|
399
484
|
// Directory-level check: skills require a SKILL.md entry point (was
|
|
400
485
|
// SkillLinter.lintDirectory). Run once per direct subdirectory before the
|
|
@@ -417,7 +502,11 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
417
502
|
}
|
|
418
503
|
for (const filePath of assetFiles) {
|
|
419
504
|
// Skip registry-cached read-only files — --fix must not mutate them.
|
|
420
|
-
|
|
505
|
+
// Compare on a separator-normalized copy: on Windows these paths carry
|
|
506
|
+
// backslashes, so the forward-slash substring never matched and --fix
|
|
507
|
+
// rewrote files inside the registry cache.
|
|
508
|
+
const posixPath = filePath.replace(/\\/g, "/");
|
|
509
|
+
if (posixPath.includes("/.cache/") || posixPath.includes("/registry/"))
|
|
421
510
|
continue;
|
|
422
511
|
const relPath = path.relative(stashRoot, filePath);
|
|
423
512
|
let raw;
|
|
@@ -430,15 +519,32 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
430
519
|
let data;
|
|
431
520
|
let body;
|
|
432
521
|
let frontmatter;
|
|
522
|
+
// File-identity findings the per-type rules cannot produce: they describe
|
|
523
|
+
// the FILE (its extension, whether it parsed at all), not its fields.
|
|
524
|
+
const fileIssues = [];
|
|
433
525
|
if (subdir === "tasks") {
|
|
434
526
|
// Task files are pure YAML — parseFrontmatter returns empty data for them.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
527
|
+
const parsed = parseTaskYaml(raw);
|
|
528
|
+
data = parsed.data;
|
|
529
|
+
if (!parsed.ok) {
|
|
530
|
+
// A parse failure used to fall through as `data = {}`, and every task
|
|
531
|
+
// rule short-circuits on an empty mapping — so an unparseable task
|
|
532
|
+
// file reported a CLEAN scan. Report the parse failure itself
|
|
533
|
+
// (issue #760).
|
|
534
|
+
fileIssues.push({
|
|
535
|
+
file: relPath,
|
|
536
|
+
issue: "invalid-task-yaml",
|
|
537
|
+
detail: taskYamlParseDetail(parsed.error),
|
|
538
|
+
fixed: false,
|
|
539
|
+
});
|
|
439
540
|
}
|
|
440
|
-
|
|
441
|
-
|
|
541
|
+
if (isNearMissTaskFileName(relPath)) {
|
|
542
|
+
fileIssues.push({
|
|
543
|
+
file: relPath,
|
|
544
|
+
issue: "invalid-task-yaml",
|
|
545
|
+
detail: taskExtensionDetail(relPath),
|
|
546
|
+
fixed: false,
|
|
547
|
+
});
|
|
442
548
|
}
|
|
443
549
|
body = raw;
|
|
444
550
|
frontmatter = null;
|
|
@@ -446,13 +552,40 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
446
552
|
else {
|
|
447
553
|
({ data, content: body, frontmatter } = parseFrontmatter(raw));
|
|
448
554
|
}
|
|
449
|
-
|
|
555
|
+
// One file's checks — including its `--fix` mutations — must never abort
|
|
556
|
+
// the sweep: an uncaught throw here left the caller with an exception and
|
|
557
|
+
// no record of which earlier files had ALREADY been rewritten on disk
|
|
558
|
+
// (issue #761). A failure is reported per-file, in-band, and the sweep
|
|
559
|
+
// continues so the rest of the bundle is still linted.
|
|
560
|
+
let issues;
|
|
561
|
+
try {
|
|
562
|
+
issues = [
|
|
563
|
+
...fileIssues,
|
|
564
|
+
...lintAssetFile({ filePath, relPath, raw, data, body, frontmatter, fix, stashRoot, extraStashRoots }, subdir),
|
|
565
|
+
];
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
flagged.push(...fileIssues, {
|
|
569
|
+
file: relPath,
|
|
570
|
+
issue: "lint-failed",
|
|
571
|
+
detail: `lint ${fix ? "--fix " : ""}failed for this file: ${e instanceof Error ? e.message : String(e)}`,
|
|
572
|
+
fixed: fix ? "failed" : false,
|
|
573
|
+
});
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
450
576
|
let fileDeleted = false;
|
|
451
577
|
for (const issue of issues) {
|
|
452
578
|
if (isFileDeletion(issue)) {
|
|
453
579
|
fileDeleted = true;
|
|
454
580
|
fixed.push(issue);
|
|
455
581
|
}
|
|
582
|
+
else if (isAdvisoryLintIssue(issue)) {
|
|
583
|
+
// `lintAssetFile` returns errors only today, so this branch is
|
|
584
|
+
// reached by no current producer — it is here so that an advisory
|
|
585
|
+
// added to a per-type check later cannot silently become a
|
|
586
|
+
// `--fail-on-flagged` failure, the way the unclassified default does.
|
|
587
|
+
warnings.push(issue);
|
|
588
|
+
}
|
|
456
589
|
else if (issue.fixed === true) {
|
|
457
590
|
fixed.push(issue);
|
|
458
591
|
}
|
|
@@ -463,6 +596,26 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
463
596
|
}
|
|
464
597
|
if (fileDeleted)
|
|
465
598
|
continue; // file is gone — skip any remaining checks
|
|
599
|
+
// The workflow frontend is ONE parse+compile whose output feeds BOTH
|
|
600
|
+
// channels, so it runs here — once per file — rather than inside
|
|
601
|
+
// `lintAssetFile`, which is an errors-only surface (pinned by the lint
|
|
602
|
+
// golden). Which channel a finding lands in is decided by
|
|
603
|
+
// `ADVISORY_LINT_ISSUES`, never by which half of the pass produced it, so
|
|
604
|
+
// a future compile-warning kind carrying a fatal code cannot slip past
|
|
605
|
+
// `--fail-on-flagged`.
|
|
606
|
+
// NB: the CLI passes the ABSOLUTE filePath to parseWorkflow (matching the
|
|
607
|
+
// old WorkflowLinter), whereas the adapter passes the change relPath.
|
|
608
|
+
if (subdir === "workflows") {
|
|
609
|
+
const frontend = workflowFrontendDiagnostics(relPath, raw, filePath);
|
|
610
|
+
for (const finding of [...frontend.errors, ...frontend.warnings]) {
|
|
611
|
+
if (isAdvisoryLintIssue(finding)) {
|
|
612
|
+
warnings.push(finding);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
flagged.push(finding);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
466
619
|
}
|
|
467
620
|
}
|
|
468
621
|
// ── Env dangerous-key pass ─────────────────────────────────────────────────
|
|
@@ -483,7 +636,8 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
483
636
|
ok: true,
|
|
484
637
|
fixed,
|
|
485
638
|
flagged,
|
|
486
|
-
|
|
639
|
+
warnings,
|
|
640
|
+
summary: { fixed: fixed.length, flagged: flagged.length, warnings: warnings.length },
|
|
487
641
|
};
|
|
488
642
|
}
|
|
489
643
|
/**
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
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
|
+
/**
|
|
5
|
+
* The issue codes that are ADVISORY: surfaced in lint output but never routed
|
|
6
|
+
* into `flagged`, so `--fail-on-flagged` cannot fail a run over one.
|
|
7
|
+
*
|
|
8
|
+
* ONE home for that decision. EVERY routing point consults it — the adapter
|
|
9
|
+
* path (`lint/index.ts#lintViaAdapter`), the sweep's per-file loop, and the
|
|
10
|
+
* sweep's workflow-frontend pass — so a new advisory code cannot be classified
|
|
11
|
+
* correctly in one place and land in `flagged` (exit 1) in another; a finding
|
|
12
|
+
* is never filed by which producer emitted it. A future advisory belongs in
|
|
13
|
+
* BOTH this set and {@link LintIssueType}: an unrecognized code is folded onto
|
|
14
|
+
* `adapter-diagnostic` at the adapter boundary, which is deliberately NOT
|
|
15
|
+
* advisory, so a code missing from the union cannot be routed by this set.
|
|
16
|
+
*
|
|
17
|
+
* Advisory-ness is deliberately NOT a field on {@link LintIssue}: issues are
|
|
18
|
+
* serialized verbatim by `--format json`, and a new key on every advisory
|
|
19
|
+
* would change every consumer's output to restate what `issue` already says.
|
|
20
|
+
*/
|
|
21
|
+
export const ADVISORY_LINT_ISSUES = new Set(["workflow-warning"]);
|
|
22
|
+
/** True when `issue` belongs to the advisory channel — see {@link ADVISORY_LINT_ISSUES}. */
|
|
23
|
+
export function isAdvisoryLintIssue(issue) {
|
|
24
|
+
return ADVISORY_LINT_ISSUES.has(issue.issue);
|
|
25
|
+
}
|
|
@@ -54,6 +54,7 @@ import { _setTxnMutationHookForTests, advanceTxn, beginTxn, canonicalTxnRoot, cl
|
|
|
54
54
|
import { canonicalBundleIdForTarget, resolveBundleWriteTarget } from "../../core/mutation-target.js";
|
|
55
55
|
import { withImmediateTransaction, withStateDb } from "../../core/state-db.js";
|
|
56
56
|
import { warn } from "../../core/warn.js";
|
|
57
|
+
import { recordWrittenPath } from "../../core/write-provenance.js";
|
|
57
58
|
import { assertAkmAssetWrite, assertWriteTargetPathsClean, captureGitPublication, captureWriteTargetPathSnapshot, prepareWriteTargetForMutation, publishWriteTargetTransaction, resolveWriteTarget, } from "../../core/write-source.js";
|
|
58
59
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
59
60
|
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
@@ -901,6 +902,9 @@ function rollbackPreparedProposalTransaction(txn) {
|
|
|
901
902
|
if (p.originalHash === null) {
|
|
902
903
|
if (currentHash === p.publishedHash && sameProposalFile(p.assetPath, p.publishPath)) {
|
|
903
904
|
fs.unlinkSync(p.assetPath);
|
|
905
|
+
// #652: un-publishing is a mutation of this run's own write — journal
|
|
906
|
+
// it so the sync stages the FINAL state of a written-then-reverted path.
|
|
907
|
+
recordWrittenPath(p.assetPath);
|
|
904
908
|
}
|
|
905
909
|
else if (currentHash !== null) {
|
|
906
910
|
throw new Error(`Cannot roll back proposal transaction: target was created externally.`);
|
|
@@ -912,8 +916,10 @@ function rollbackPreparedProposalTransaction(txn) {
|
|
|
912
916
|
cleanupProposalPublication(p);
|
|
913
917
|
return;
|
|
914
918
|
}
|
|
915
|
-
if (currentHash === p.publishedHash)
|
|
919
|
+
if (currentHash === p.publishedHash) {
|
|
916
920
|
fs.unlinkSync(p.assetPath);
|
|
921
|
+
recordWrittenPath(p.assetPath);
|
|
922
|
+
}
|
|
917
923
|
else if (currentHash !== null && currentHash !== p.originalHash) {
|
|
918
924
|
throw new Error(`Cannot roll back proposal transaction: ${p.assetPath} diverged.`);
|
|
919
925
|
}
|
|
@@ -922,6 +928,9 @@ function rollbackPreparedProposalTransaction(txn) {
|
|
|
922
928
|
throw new Error(`Cannot restore proposal backup: ${p.assetPath} is occupied.`);
|
|
923
929
|
}
|
|
924
930
|
fs.linkSync(p.displacedPath, p.assetPath);
|
|
931
|
+
// #652: restoring the displaced original still leaves the path in a state
|
|
932
|
+
// this run produced; journal it so the final on-disk bytes are staged.
|
|
933
|
+
recordWrittenPath(p.assetPath);
|
|
925
934
|
}
|
|
926
935
|
cleanupProposalPublication(p);
|
|
927
936
|
}
|
|
@@ -1023,6 +1032,10 @@ function persistProposalEvent(txn, proposal, ctx) {
|
|
|
1023
1032
|
async function finalizeProposalTransaction(txn, target, proposal, ctx) {
|
|
1024
1033
|
const p = txn.journal.payload;
|
|
1025
1034
|
validatePublishedProposal(p);
|
|
1035
|
+
// #652: finalizing an `asset-published` transaction that a CRASHED earlier
|
|
1036
|
+
// run left behind is this run adopting that write — journal the asset so the
|
|
1037
|
+
// adopting run's auto-sync commits it instead of leaving it stranded.
|
|
1038
|
+
recordWrittenPath(p.assetPath);
|
|
1026
1039
|
cleanupProposalPublication(p);
|
|
1027
1040
|
if (txn.journal.phase === "asset-published") {
|
|
1028
1041
|
const commitRoot = target.source.repoPath ?? target.source.path;
|
|
@@ -1343,6 +1356,9 @@ function publishProposalAsset(txn, target) {
|
|
|
1343
1356
|
}
|
|
1344
1357
|
}
|
|
1345
1358
|
fs.linkSync(p.publishPath, p.assetPath);
|
|
1359
|
+
// #652: the accepted-proposal (and revert) target is the run's headline
|
|
1360
|
+
// write — journal it the instant the asset lands, before the txn advances.
|
|
1361
|
+
recordWrittenPath(p.assetPath);
|
|
1346
1362
|
fsyncTxnDir(path.dirname(p.assetPath));
|
|
1347
1363
|
const snapshot = captureWriteTargetPathSnapshot(target, p.assetPath);
|
|
1348
1364
|
if (snapshot)
|
|
@@ -10,6 +10,7 @@ import { decideDangerousKeyInstall } from "../../core/activation-policy.js";
|
|
|
10
10
|
import { UsageError } from "../../core/errors.js";
|
|
11
11
|
import { appendEvent } from "../../core/events.js";
|
|
12
12
|
import { warn } from "../../core/warn.js";
|
|
13
|
+
import { sanitizeString } from "../../sources/providers/provider-utils.js";
|
|
13
14
|
import { akmRemove } from "./installed-stashes.js";
|
|
14
15
|
import { akmAdd } from "./source-add.js";
|
|
15
16
|
import { addStash } from "./source-manage.js";
|
|
@@ -180,9 +181,14 @@ export async function auditInstalledStashForDangerousKeys(opts) {
|
|
|
180
181
|
groupedByEnv.set(f.envRef, existing);
|
|
181
182
|
}
|
|
182
183
|
for (const [envRef, keys] of groupedByEnv) {
|
|
183
|
-
|
|
184
|
+
// envRef and keys come from filenames and KEY names inside a downloaded or
|
|
185
|
+
// cloned bundle, i.e. attacker-controllable. Tar validation rejects NUL but
|
|
186
|
+
// not ESC/CSI, and git checkout allows them in filenames on Linux/macOS —
|
|
187
|
+
// so printing them raw let a crafted bundle rewrite this security prompt
|
|
188
|
+
// with terminal escapes right before an "Install anyway?" confirmation.
|
|
189
|
+
warn(`[warn] Env "${sanitizeString(envRef)}" in stash "${sanitizeString(stashLabel)}" contains potentially dangerous keys:`);
|
|
184
190
|
for (const key of keys) {
|
|
185
|
-
warn(` - ${key}: can hijack process execution via \`akm env run\``);
|
|
191
|
+
warn(` - ${sanitizeString(key)}: can hijack process execution via \`akm env run\``);
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
const confirmed = await p.confirm({
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
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
|
-
import fs from "node:fs";
|
|
5
4
|
import { placementTypes } from "../../core/asset/asset-placement.js";
|
|
6
5
|
import { resolveStashDir } from "../../core/common.js";
|
|
7
6
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
7
|
+
import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-access.js";
|
|
8
8
|
import { getDbPath } from "../../core/paths.js";
|
|
9
9
|
import { error } from "../../core/warn.js";
|
|
10
10
|
import { getEffectiveSemanticStatus, readSemanticStatus } from "../../indexer/search/semantic-status.js";
|
|
@@ -83,8 +83,18 @@ function readIndexStats(resolvedPath) {
|
|
|
83
83
|
hasEmbeddings: false,
|
|
84
84
|
vecAvailable: false,
|
|
85
85
|
};
|
|
86
|
-
|
|
86
|
+
// "Absent" is the ordinary first-run state; "inaccessible" is a fault that
|
|
87
|
+
// must not present as an empty index (#791). `akm info` is the command an
|
|
88
|
+
// operator reaches for to DIAGNOSE this, so it reports rather than throws —
|
|
89
|
+
// but it says so explicitly instead of returning zeros that look healthy.
|
|
90
|
+
const { access, code } = classifyPathAccess(resolvedPath);
|
|
91
|
+
if (access === "absent")
|
|
87
92
|
return EMPTY;
|
|
93
|
+
if (access === "inaccessible") {
|
|
94
|
+
const detail = describeInaccessiblePath(resolvedPath, code);
|
|
95
|
+
error(`[akm info] index database is not readable: ${detail}`);
|
|
96
|
+
return { ...EMPTY, unreadable: detail };
|
|
97
|
+
}
|
|
88
98
|
let db;
|
|
89
99
|
try {
|
|
90
100
|
db = openExistingDatabase(resolvedPath);
|
|
@@ -18,6 +18,7 @@ import path from "node:path";
|
|
|
18
18
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
19
19
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
20
20
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
21
|
+
import { isPathAbsent } from "../../core/path-access.js";
|
|
21
22
|
import { getDbPath } from "../../core/paths.js";
|
|
22
23
|
import { warn } from "../../core/warn.js";
|
|
23
24
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
@@ -125,7 +126,11 @@ function describeLock(entry) {
|
|
|
125
126
|
function readBundleCounts() {
|
|
126
127
|
const counts = new Map();
|
|
127
128
|
const dbPath = getDbPath();
|
|
128
|
-
|
|
129
|
+
// An empty map renders as `itemCount: 0` for every bundle — indistinguishable
|
|
130
|
+
// from "these bundles really are empty". Only a never-built index gets to say
|
|
131
|
+
// that silently; an unreadable one falls through to the opener and is
|
|
132
|
+
// reported by the `warn` in the catch below (#791).
|
|
133
|
+
if (isPathAbsent(dbPath))
|
|
129
134
|
return counts;
|
|
130
135
|
let db;
|
|
131
136
|
try {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import embeddedChangelog from "../../../CHANGELOG.md" with { type: "text" };
|
|
6
7
|
import { getDirname } from "../../runtime.js";
|
|
7
8
|
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
|
|
8
9
|
const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
|
|
@@ -24,9 +25,13 @@ function loadChangelog() {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
|
-
// fall through to
|
|
28
|
+
// fall through to the embedded copy
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
// In the `bun build --compile` standalone binary, import.meta.url points into
|
|
31
|
+
// the virtual /$bunfs tree and every existsSync above misses, so `akm help
|
|
32
|
+
// migrate <version>` degraded to the generic "no dedicated note" message for
|
|
33
|
+
// EVERY version. Only assets imported `with { type: "text" }` are embedded.
|
|
34
|
+
return embeddedChangelog.length > 0 ? embeddedChangelog : undefined;
|
|
30
35
|
}
|
|
31
36
|
/**
|
|
32
37
|
* Load the bundled migration note for a specific version, if one exists.
|
|
@@ -87,7 +92,11 @@ function resolveLatestVersion(changelog) {
|
|
|
87
92
|
return undefined;
|
|
88
93
|
}
|
|
89
94
|
function extractChangelogSection(changelog, version) {
|
|
90
|
-
|
|
95
|
+
// `\Z` is not a JavaScript anchor — it matches a literal "Z", which truncated
|
|
96
|
+
// the section at the first capital Z in the body (and failed outright for the
|
|
97
|
+
// last entry). `$` with the `m` flag would stop at the first line end, so the
|
|
98
|
+
// end-of-input alternative has to be an explicit lookahead for the input end.
|
|
99
|
+
const pattern = new RegExp(`^## \\[${escapeRegexString(version)}\\][^\\n]*\\n([\\s\\S]*?)(?=^## \\[|$(?![\\s\\S]))`, "m");
|
|
91
100
|
const match = changelog.match(pattern);
|
|
92
101
|
if (!match)
|
|
93
102
|
return undefined;
|
|
@@ -10,6 +10,7 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
10
10
|
import { warn } from "../../core/warn.js";
|
|
11
11
|
import { githubHeaders } from "../../integrations/github.js";
|
|
12
12
|
import { getDirname, mainPath, semverOrder } from "../../runtime.js";
|
|
13
|
+
import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
|
|
13
14
|
const REPO = "itlackey/akm";
|
|
14
15
|
const DEFAULT_PACKAGE_NAME = "akm-cli";
|
|
15
16
|
const NODE_MODULES_SEGMENT = "/node_modules/";
|
|
@@ -474,7 +475,14 @@ function readInstalledCliVersion(akmBin) {
|
|
|
474
475
|
return match?.[0];
|
|
475
476
|
}
|
|
476
477
|
function runRequiredCommand(akmBin, args, label) {
|
|
477
|
-
|
|
478
|
+
// A bare "akm" is not spawnable on Windows: npm/pnpm/yarn install a global CLI
|
|
479
|
+
// as akm.cmd / akm.ps1 shims, and spawnSync without a shell does not apply
|
|
480
|
+
// PATHEXT — so the package-manager upgrade arm died with ENOENT before it ever
|
|
481
|
+
// ran. resolveAkmInvocation returns a concrete argv (launcher, runtime + main
|
|
482
|
+
// script, or a standalone binary) for however this install actually runs.
|
|
483
|
+
// An explicit path (the standalone arm passes one) is used as given.
|
|
484
|
+
const [command, ...prefixArgs] = path.isAbsolute(akmBin) ? [akmBin] : resolveAkmInvocation().argv;
|
|
485
|
+
const result = childProcess.spawnSync(command ?? akmBin, [...prefixArgs, ...args], {
|
|
478
486
|
encoding: "utf8",
|
|
479
487
|
env: process.env,
|
|
480
488
|
stdio: "pipe",
|