akm-cli 0.9.2 → 0.9.4
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 +110 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/tasks/tasks-cli.js +28 -3
- package/dist/commands/tasks/tasks.js +29 -1
- package/dist/core/adapter/adapters/akm-adapter.js +21 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/core/config/config.js +19 -3
- package/dist/core/extra-params.js +115 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12698 -12530
- package/dist/scripts/akm-migrate.js +12698 -12530
- package/dist/storage/repositories/proposals-repository.js +33 -1
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/run-native-task.js +28 -1
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-sync-preview.js +43 -0
- package/dist/tasks/scheduler-sync.js +1 -0
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +500 -0
- package/dist/tasks/source/task-to-v4.js +467 -0
- package/docs/reference/cli.md +8 -3
- package/package.json +4 -4
|
@@ -23,8 +23,24 @@ function changesToStored(changes) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Reconstruct `Proposal.changes` from `metadata_json.changes` + the `content`
|
|
25
25
|
* column.
|
|
26
|
+
*
|
|
27
|
+
* Read-time compatibility shim (#858/#859): proposals created before this
|
|
28
|
+
* field existed have no `changes` key in `metadata_json` at all (~89% of the
|
|
29
|
+
* archived accepted/rejected rows on real installs) and that history cannot
|
|
30
|
+
* be reconstructed from `content` alone. Treat a completely absent `changes`
|
|
31
|
+
* key as a known legacy gap — return an empty change list rather than
|
|
32
|
+
* throwing — so these rows still round-trip as real proposals (and count
|
|
33
|
+
* toward accepted/rejected totals) instead of being dropped or crashing
|
|
34
|
+
* every reader. A `changes` value that *is* present but malformed (wrong
|
|
35
|
+
* type, invalid entries) is still corruption and throws, same as before; the
|
|
36
|
+
* write path (`proposalToRowValues`) still refuses to persist an empty or
|
|
37
|
+
* missing change list for new proposals, so this leniency only ever applies
|
|
38
|
+
* to pre-existing rows.
|
|
26
39
|
*/
|
|
27
40
|
function storedToChanges(stored, content) {
|
|
41
|
+
if (stored === undefined) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
28
44
|
if (!Array.isArray(stored) || stored.length === 0) {
|
|
29
45
|
throw new Error("Proposal metadata is missing changes.");
|
|
30
46
|
}
|
|
@@ -307,7 +323,23 @@ export function listStateProposals(db, options = {}) {
|
|
|
307
323
|
content, frontmatter_json, metadata_json
|
|
308
324
|
FROM proposals ${where} ORDER BY created_at ASC, rowid ASC`)
|
|
309
325
|
.all(...params);
|
|
310
|
-
|
|
326
|
+
// Per-row skip-and-warn (#858/#859): a single row that fails to parse
|
|
327
|
+
// (invalid ref/status/metadata shape — genuine corruption, distinct from
|
|
328
|
+
// the tolerated legacy-missing-`changes` case in `storedToChanges` above)
|
|
329
|
+
// must not abort the entire list. Every caller of this function reads a
|
|
330
|
+
// multi-row archive; one bad row hiding the rest behind a thrown error is
|
|
331
|
+
// strictly worse than surfacing the well-formed rows plus a warning.
|
|
332
|
+
const proposals = [];
|
|
333
|
+
for (const row of rows) {
|
|
334
|
+
try {
|
|
335
|
+
proposals.push(proposalRowToProposal(row));
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
339
|
+
console.warn(`[akm] Skipping unparseable proposal row (id=${row.id}, ref=${row.ref}): ${message}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return proposals;
|
|
311
343
|
}
|
|
312
344
|
/**
|
|
313
345
|
* Look up a single proposal by id, optionally scoped to one stash root.
|
|
@@ -24,7 +24,23 @@ function validateDetail(value) {
|
|
|
24
24
|
metadataError("detail.exitCode must be a number or null");
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Decode the task-history metadata shape.
|
|
29
|
+
*
|
|
30
|
+
* Read-time compatibility shim (mirrors proposals-repository's
|
|
31
|
+
* `storedToChanges`): `metadataVersion` was added in a later release. 58% of
|
|
32
|
+
* a real install's `task_history` rows (8,596/14,801) predate the field
|
|
33
|
+
* entirely, and 88 more carry additive fields (`profile`, `repairReason`)
|
|
34
|
+
* written by prior releases. An absent `metadataVersion` is a LEGACY row, not
|
|
35
|
+
* corruption — decode what's present (defaulting the absent `detail` key to
|
|
36
|
+
* `null`) instead of throwing. Unknown keys are ignored rather than
|
|
37
|
+
* hard-rejected, so the next additive field never regresses this again;
|
|
38
|
+
* neither `profile` nor `repairReason` is read anywhere downstream, so they
|
|
39
|
+
* are dropped harmlessly rather than round-tripped. A `metadataVersion` that
|
|
40
|
+
* IS present but not `2` is a genuinely unknown/future shape and still
|
|
41
|
+
* rejected, as is a non-number `durationMs` or a `detail` that fails
|
|
42
|
+
* {@link validateDetail} — real corruption, not version skew.
|
|
43
|
+
*/
|
|
28
44
|
export function decodeTaskHistoryMetadata(input) {
|
|
29
45
|
let parsed = input;
|
|
30
46
|
if (typeof input === "string") {
|
|
@@ -37,27 +53,23 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
37
53
|
}
|
|
38
54
|
if (!isRecord(parsed))
|
|
39
55
|
metadataError("root must be an object");
|
|
40
|
-
if (parsed.metadataVersion !== 2)
|
|
56
|
+
if (parsed.metadataVersion !== undefined && parsed.metadataVersion !== 2) {
|
|
41
57
|
metadataError(`unsupported metadataVersion: ${String(parsed.metadataVersion)}`);
|
|
42
|
-
|
|
43
|
-
const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
|
|
44
|
-
if (unknown.length > 0)
|
|
45
|
-
metadataError(`unknown fields: ${unknown.sort().join(", ")}`);
|
|
58
|
+
}
|
|
46
59
|
if (typeof parsed.durationMs !== "number")
|
|
47
60
|
metadataError("durationMs must be a number");
|
|
48
|
-
|
|
49
|
-
metadataError("detail is required");
|
|
61
|
+
const detail = "detail" in parsed ? parsed.detail : null;
|
|
50
62
|
if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
|
|
51
63
|
metadataError("engine must be a string or null");
|
|
52
64
|
}
|
|
53
65
|
if (parsed.targetVocab !== undefined && parsed.targetVocab !== 2) {
|
|
54
66
|
metadataError("targetVocab must be 2 when present");
|
|
55
67
|
}
|
|
56
|
-
validateDetail(
|
|
68
|
+
validateDetail(detail);
|
|
57
69
|
return {
|
|
58
70
|
metadataVersion: 2,
|
|
59
71
|
durationMs: parsed.durationMs,
|
|
60
|
-
detail:
|
|
72
|
+
detail: detail ?? null,
|
|
61
73
|
...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
|
|
62
74
|
...(parsed.targetVocab === 2 ? { targetVocab: 2 } : {}),
|
|
63
75
|
};
|
|
@@ -64,13 +64,40 @@ export function shellCommand(task, platform = process.platform, env = process.en
|
|
|
64
64
|
return [task.shell, "-c", command];
|
|
65
65
|
case "pwsh":
|
|
66
66
|
case "powershell":
|
|
67
|
-
return [
|
|
67
|
+
return [
|
|
68
|
+
shellExecutable(task.shell, platform, env),
|
|
69
|
+
"-NoProfile",
|
|
70
|
+
"-NonInteractive",
|
|
71
|
+
"-Command",
|
|
72
|
+
withExitCodePropagation(command),
|
|
73
|
+
];
|
|
68
74
|
case "cmd":
|
|
69
75
|
return [shellExecutable("cmd", platform, env), "/d", "/s", "/c", command];
|
|
70
76
|
default:
|
|
71
77
|
return assertNever(task.shell, "shellCommand");
|
|
72
78
|
}
|
|
73
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* `-Command` (documented identically for powershell.exe 5.1 and pwsh 7+, in
|
|
82
|
+
* about_PowerShell_exe / about_Pwsh) already derives its own process exit
|
|
83
|
+
* code from `$?`, so a genuinely failing last statement already yields a
|
|
84
|
+
* nonzero exit and status "failed" — but any native exit code outside {0, 1}
|
|
85
|
+
* is collapsed to 1, discarding the real value that task history reports.
|
|
86
|
+
*
|
|
87
|
+
* Appending a bare `exit $LASTEXITCODE` to recover it is unsafe on its own:
|
|
88
|
+
* `$LASTEXITCODE` stays `$null` for a command that never runs a native
|
|
89
|
+
* executable (a pure PowerShell/cmdlet command), and `exit $null` resolves
|
|
90
|
+
* to exit code 0 — turning a failed cmdlet into a false "completed".
|
|
91
|
+
*
|
|
92
|
+
* Reading `$?` first, before anything else can run, reproduces -Command's
|
|
93
|
+
* own completed/failed determination exactly (immune to that regression and
|
|
94
|
+
* to a `$LASTEXITCODE` left stale by an earlier native call in the same
|
|
95
|
+
* command), then upgrades to the precise native exit code only when the
|
|
96
|
+
* failing last statement actually set one.
|
|
97
|
+
*/
|
|
98
|
+
function withExitCodePropagation(command) {
|
|
99
|
+
return `${command}; if ($?) { exit 0 } elseif ($LASTEXITCODE -ne $null) { exit $LASTEXITCODE } else { exit 1 }`;
|
|
100
|
+
}
|
|
74
101
|
/**
|
|
75
102
|
* Bind an unambiguous leading bare `akm` (including the task-v2 migrator's
|
|
76
103
|
* quoted form) to this installation. Explicit paths and arbitrary shell
|
|
@@ -79,14 +79,34 @@ export function readTaskHistory(options = {}) {
|
|
|
79
79
|
// discarded and `akm task history --id X --limit 20` always returned one
|
|
80
80
|
// run. The CLI documents --limit as "Maximum rows to return"; honour it.
|
|
81
81
|
if (options.limit !== undefined && options.limit > 0) {
|
|
82
|
-
return getTaskHistoryRuns(db, options.id, options.limit)
|
|
82
|
+
return decodeTaskHistoryRows(getTaskHistoryRuns(db, options.id, options.limit));
|
|
83
83
|
}
|
|
84
84
|
const row = getTaskHistory(db, options.id);
|
|
85
|
-
return row ? [
|
|
85
|
+
return row ? decodeTaskHistoryRows([row]) : [];
|
|
86
86
|
}
|
|
87
|
-
return queryTaskHistory(db, options.limit !== undefined && options.limit > 0 ? { limit: options.limit } : {})
|
|
87
|
+
return decodeTaskHistoryRows(queryTaskHistory(db, options.limit !== undefined && options.limit > 0 ? { limit: options.limit } : {}));
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Per-row skip-and-warn (mirrors `listStateProposals` in
|
|
92
|
+
* proposals-repository.ts): a single genuinely-corrupt `metadata_json` row
|
|
93
|
+
* must not abort the entire history read. `decodeTaskHistoryMetadata` (via
|
|
94
|
+
* `taskHistoryRowToResult`) already tolerates legacy/additive shapes; only
|
|
95
|
+
* real corruption reaches this catch.
|
|
96
|
+
*/
|
|
97
|
+
function decodeTaskHistoryRows(rows) {
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
try {
|
|
101
|
+
results.push(taskHistoryRowToResult(row));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
console.warn(`[akm] Skipping unparseable task_history row (task_id=${row.task_id}, started_at=${row.started_at}): ${message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
90
110
|
/**
|
|
91
111
|
* Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
|
|
92
112
|
* that callers of `readTaskHistory()` expect.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Project a set of scheduler sync operations into a preview report. Pure:
|
|
6
|
+
* no I/O, no backend calls. Accepts a bare operation array (not the whole
|
|
7
|
+
* `SchedulerSyncPlan`) so a future `task prune` can build a preview from
|
|
8
|
+
* its own remove-only operation set without needing a full sync plan.
|
|
9
|
+
*/
|
|
10
|
+
export function renderSchedulerPlanPreview(backend, operations, unchanged = []) {
|
|
11
|
+
const adds = [];
|
|
12
|
+
const updates = [];
|
|
13
|
+
const removes = [];
|
|
14
|
+
for (const operation of operations) {
|
|
15
|
+
if (operation.kind === "remove") {
|
|
16
|
+
removes.push({
|
|
17
|
+
id: operation.id,
|
|
18
|
+
kind: "remove",
|
|
19
|
+
nativeId: operation.nativeId,
|
|
20
|
+
...(operation.ownerBundlePath !== undefined ? { ownerBundlePath: operation.ownerBundlePath } : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else if (operation.kind === "install") {
|
|
24
|
+
adds.push({ id: operation.binding.id, kind: "install" });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
updates.push({ id: operation.binding.id, kind: "update" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
backend,
|
|
32
|
+
dryRun: true,
|
|
33
|
+
adds: Object.freeze(adds),
|
|
34
|
+
updates: Object.freeze(updates),
|
|
35
|
+
removes: Object.freeze(removes),
|
|
36
|
+
unchanged: Object.freeze([...unchanged]),
|
|
37
|
+
hasRemovals: removes.length > 0,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Convenience wrapper for the common case: previewing a whole {@link SchedulerSyncPlan}. */
|
|
41
|
+
export function renderSchedulerSyncPlanPreview(backend, plan) {
|
|
42
|
+
return renderSchedulerPlanPreview(backend, plan.operations, plan.unchanged);
|
|
43
|
+
}
|
|
@@ -135,6 +135,7 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
|
|
|
135
135
|
invocation: current.invocation,
|
|
136
136
|
fingerprint: priorFingerprint,
|
|
137
137
|
}),
|
|
138
|
+
...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
|
|
138
139
|
}));
|
|
139
140
|
}
|
|
140
141
|
return Object.freeze({
|
|
@@ -392,7 +392,7 @@ export function assertBoundedTaskYamlDocument(document, options) {
|
|
|
392
392
|
* P4 (docs/plans/specs/p4-deletions-closeout.md §3.2) deleted task v3
|
|
393
393
|
* acceptance from `src` entirely — `parseTaskV3Yaml` and its `"task v3
|
|
394
394
|
* source"` label no longer exist here (the grammar survives only in the
|
|
395
|
-
* vendored, frozen `
|
|
395
|
+
* vendored, frozen `src/tasks/source/task-source-v3-frozen.ts`
|
|
396
396
|
* copy, which does not call this function). The two live `src` callers
|
|
397
397
|
* today: `parseTaskSourceV4`'s standalone YAML-string entry
|
|
398
398
|
* (`task-source-v4.ts:790`) passes `sourceLabel: "task source v4"`; the
|
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Runs the bounded YAML front end ONCE (`readBoundedTaskSourceYaml`), reads
|
|
9
9
|
* `root.version`, and either dispatches into `parseTaskSourceV4Document` or
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* routes through the in-memory read shim below — no second parse of the v4
|
|
11
|
+
* grammar itself, no re-serialization back to disk, no synthetic document
|
|
12
|
+
* (the P1b §4.3 invariant this phase carries forward: the shim adds a pure
|
|
13
|
+
* bytes-in/bytes-out detour, never a disk write).
|
|
12
14
|
*
|
|
13
15
|
* The terminal routing table:
|
|
14
16
|
*
|
|
15
17
|
* | root `version` | outcome |
|
|
16
18
|
* |------------------------|-----------------------------------------------------------------|
|
|
17
19
|
* | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
|
|
20
|
+
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) — the shim removes friction for the deterministic case, it never hides a real problem |
|
|
18
21
|
* | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
|
|
19
22
|
* | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
|
|
20
23
|
*
|
|
@@ -24,17 +27,28 @@
|
|
|
24
27
|
* message that would send the user to the migrator for a document that was
|
|
25
28
|
* never task v2 or v3 in the first place.
|
|
26
29
|
*
|
|
27
|
-
* task v2 and task v3 sources are no longer read
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* task v2 and task v3 sources are no longer read as their own standing
|
|
31
|
+
* grammar anywhere else in `src` — the only readers of that grammar are the
|
|
32
|
+
* pure, byte-producing planners (`./task-to-v3.ts`, `./task-to-v4.ts`, and
|
|
33
|
+
* the frozen v3 reader `./task-source-v3-frozen.ts`), reached either through
|
|
34
|
+
* this shim (bytes in, bytes out, never touches disk) or through
|
|
35
|
+
* `akm migrate apply` / `akm-migrate` (`scripts/akm-migrate`, which
|
|
36
|
+
* additionally rewrites the file on disk once the user asks for that).
|
|
37
|
+
* Policy: a deterministic byte transform is the tool's job, not the user's —
|
|
38
|
+
* upgrading past a schema bump must not silently break a scheduled task, so
|
|
39
|
+
* v2/v3 files keep reading successfully at the cost of a one-line
|
|
40
|
+
* deprecation warning, and `akm migrate apply` remains available to rewrite
|
|
41
|
+
* the file and silence it. The front end's own pre-version failures (source
|
|
42
|
+
* not a string, source too large, YAML parse/warning/expansion) render with
|
|
43
|
+
* the label `task source` (row B-17, closing the "task v3 source" label
|
|
44
|
+
* wart P2a's §3.4 recorded).
|
|
34
45
|
*/
|
|
35
46
|
import { UsageError } from "../../core/errors.js";
|
|
47
|
+
import { warn } from "../../core/warn.js";
|
|
36
48
|
import { readBoundedTaskSourceYaml } from "./bounded-document.js";
|
|
37
|
-
import { parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION } from "./task-source-v4.js";
|
|
49
|
+
import { parseTaskSourceV4, parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION, } from "./task-source-v4.js";
|
|
50
|
+
import { planTaskToV3File } from "./task-to-v3.js";
|
|
51
|
+
import { planTaskToV4File } from "./task-to-v4.js";
|
|
38
52
|
/** Read the root `version` field without over-accepting non-number values (e.g. the string `"4"`). */
|
|
39
53
|
export function peekTaskSourceVersion(root) {
|
|
40
54
|
if (root === null || typeof root !== "object" || Array.isArray(root))
|
|
@@ -43,12 +57,64 @@ export function peekTaskSourceVersion(root) {
|
|
|
43
57
|
return typeof value === "number" ? value : undefined;
|
|
44
58
|
}
|
|
45
59
|
const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
60
|
+
function unsupportedVersionError(filePath, version) {
|
|
61
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
|
|
65
|
+
* chained v2->v3->v4) migration planner(s) — never touches disk, never
|
|
66
|
+
* writes the file, never re-reads it from disk. Returns the produced v4
|
|
67
|
+
* YAML text, or `undefined` when the deterministic conversion cannot
|
|
68
|
+
* proceed (an unmigratable v2/v3 shape) — the caller falls back to the same
|
|
69
|
+
* hard error this gate threw before the shim existed.
|
|
70
|
+
*/
|
|
71
|
+
function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
72
|
+
const bytes = Buffer.from(yaml, "utf8");
|
|
73
|
+
// `writable`/`onDiskWritable` gate the DISK apply path's "don't touch a
|
|
74
|
+
// read-only file" check inside the planners; this shim never writes
|
|
75
|
+
// anything to disk, so that check does not apply here and must not block
|
|
76
|
+
// an otherwise-legal read of a task file that happens to be read-only.
|
|
77
|
+
const baseInput = {
|
|
78
|
+
filePath,
|
|
79
|
+
bytes,
|
|
80
|
+
mode: 0o644,
|
|
81
|
+
writable: true,
|
|
82
|
+
onDiskWritable: true,
|
|
83
|
+
...(workspaceRoot ? { containmentRoot: workspaceRoot } : {}),
|
|
84
|
+
};
|
|
85
|
+
let v3Bytes;
|
|
86
|
+
if (version === 3) {
|
|
87
|
+
v3Bytes = bytes;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const v3Outcome = planTaskToV3File(baseInput);
|
|
91
|
+
if (v3Outcome.status !== "changed")
|
|
92
|
+
return undefined;
|
|
93
|
+
v3Bytes = v3Outcome.after;
|
|
94
|
+
}
|
|
95
|
+
const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
|
|
96
|
+
if (v4Outcome.status !== "changed")
|
|
97
|
+
return undefined;
|
|
98
|
+
return v4Outcome.after.toString("utf8");
|
|
99
|
+
}
|
|
46
100
|
/** Parse task source YAML, routing per the terminal table above. */
|
|
47
101
|
export function parseTaskSource(input) {
|
|
48
102
|
const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: "task source" });
|
|
49
103
|
const version = peekTaskSourceVersion(root);
|
|
50
104
|
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
51
|
-
|
|
105
|
+
if (version === 2 || version === 3) {
|
|
106
|
+
const v4Yaml = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
|
|
107
|
+
if (v4Yaml !== undefined) {
|
|
108
|
+
const v4 = parseTaskSourceV4({
|
|
109
|
+
yaml: v4Yaml,
|
|
110
|
+
filePath: input.filePath,
|
|
111
|
+
...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
|
|
112
|
+
});
|
|
113
|
+
warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
|
|
114
|
+
return Object.freeze({ version: 4, v4 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw unsupportedVersionError(input.filePath, version);
|
|
52
118
|
}
|
|
53
119
|
const documentOptions = {
|
|
54
120
|
filePath: input.filePath,
|