akm-cli 0.9.16-alpha.1 → 0.9.16
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 +56 -132
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -4
- package/dist/cli/unknown-flags.js +3 -36
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +4 -6
- package/dist/commands/improve/improve-cli.js +20 -15
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/repository.js +3 -12
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/curate.js +44 -34
- package/dist/commands/read/search.js +35 -54
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +55 -61
- package/dist/commands/sources/source-add.js +39 -38
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-cli.js +111 -119
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +8 -35
- package/dist/core/adapter/adapters/akm-metadata.js +1 -11
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/asset/asset-placement.js +0 -35
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/embedding.js +30 -7
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +10 -12
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/errors.js +2 -5
- package/dist/core/maintenance-barrier.js +11 -13
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +2 -5
- package/dist/core/state/migrations.js +1 -26
- package/dist/core/state-db.js +27 -63
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/ensure-index.js +0 -5
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-written-assets.js +171 -133
- package/dist/indexer/indexer.js +1621 -458
- package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
- package/dist/indexer/materialize-embeddings.js +785 -0
- package/dist/indexer/passes/dir-staleness.js +161 -0
- package/dist/indexer/passes/metadata.js +1 -18
- package/dist/indexer/scan/drain-dir.js +70 -27
- package/dist/indexer/search/db-search.js +89 -373
- package/dist/indexer/search/ranking-contributors.js +16 -21
- package/dist/indexer/search/ranking.js +57 -135
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +3 -11
- package/dist/llm/embedder.js +3 -10
- package/dist/llm/embedders/remote.js +104 -133
- package/dist/llm/feature-gate.js +2 -4
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/html-render.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/stdout.js +24 -0
- package/dist/output/text/command-format.js +13 -19
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +2 -5
- package/dist/output/text.js +4 -3
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +15197 -11351
- package/dist/scripts/akm-migrate.js +15514 -11668
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/connection.js +2 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-connection.js +3 -1
- package/dist/storage/repositories/index-entries-repository.js +68 -77
- package/dist/storage/repositories/index-entry-schema.js +25 -16
- package/dist/storage/repositories/index-fts-repository.js +263 -29
- package/dist/storage/repositories/index-meta-repository.js +29 -0
- package/dist/storage/repositories/index-schema.js +122 -115
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +435 -22
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.15.md +36 -34
- package/docs/migration/release-notes/0.9.16.md +60 -98
- package/docs/migration/release-notes/README.md +0 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +124 -122
- package/docs/reference/configuration.md +137 -133
- package/docs/reference/data-and-telemetry.md +1 -2
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +170 -6
- package/schemas/akm-task.json +1 -2
- package/dist/commands/sources/index-status.js +0 -99
- package/dist/core/hash.js +0 -18
- package/dist/indexer/drain.js +0 -306
- package/dist/indexer/embedding-identity.js +0 -20
- package/dist/indexer/enrich.js +0 -260
- package/dist/indexer/reconcile.js +0 -890
- package/dist/indexer/scan/parse-file.js +0 -66
- package/dist/indexer/units/unit.js +0 -159
- package/dist/llm/embedders/provider-limits.js +0 -288
- package/dist/storage/repositories/files-repository.js +0 -181
- package/dist/storage/repositories/units-repository.js +0 -510
|
@@ -18,7 +18,7 @@ import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
|
18
18
|
import { conceptIdFromTypeName, isFullRefInput } from "../../core/asset/resolve-ref.js";
|
|
19
19
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
20
20
|
import { loadConfig } from "../../core/config/config.js";
|
|
21
|
-
import { resolveConfiguredSources } from "../../core/config/config-sources.js";
|
|
21
|
+
import { bundleComponentConfig, bundleKeyForContentRoot, resolveActiveConfiguredSources, resolveConfiguredSources, } from "../../core/config/config-sources.js";
|
|
22
22
|
import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
|
|
23
23
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
24
24
|
import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
|
|
@@ -26,6 +26,7 @@ import { warn } from "../../core/warn.js";
|
|
|
26
26
|
import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
27
27
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
28
28
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
29
|
+
import { activeSchedulerActivations, isSchedulerRefEnabled, setSchedulerRefEnabled, } from "../../tasks/activation-config.js";
|
|
29
30
|
import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
|
|
30
31
|
import { prepareTaskV3Execution } from "../../tasks/prepare/prepare.js";
|
|
31
32
|
import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
|
|
@@ -36,7 +37,7 @@ import { exitCodeForStatus } from "../../tasks/run/task-result.js";
|
|
|
36
37
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
37
38
|
import { assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "../../tasks/scheduler-binding.js";
|
|
38
39
|
import { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
|
|
39
|
-
import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, buildSchedulerRemoveOperation, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
40
|
+
import { assertSchedulerBackendInspection, assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, buildSchedulerRemoveOperation, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
40
41
|
import { renderSchedulerPlanPreview, renderSchedulerSyncPlanPreview, } from "../../tasks/scheduler-sync-preview.js";
|
|
41
42
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
42
43
|
import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
|
|
@@ -81,7 +82,6 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
81
82
|
description: input.description,
|
|
82
83
|
when_to_use: input.when_to_use,
|
|
83
84
|
tags: input.tags,
|
|
84
|
-
enabled: input.disabled !== true,
|
|
85
85
|
});
|
|
86
86
|
const parsedTask = parseTaskSource({ yaml, filePath: assetPath, workspaceRoot: stashDir });
|
|
87
87
|
const task = projectTaskSourceV4(parsedTask.v4);
|
|
@@ -94,24 +94,15 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
94
94
|
config: bundle.config,
|
|
95
95
|
resolveAsset: taskProjectionAssetResolver(bundle.config, bundle.bundleName, stashDir),
|
|
96
96
|
});
|
|
97
|
-
// Bindings are compiled from the
|
|
98
|
-
//
|
|
99
|
-
// scheduler-sync.ts's compileTaskSources (spec §3.2.7's project-v4.ts
|
|
100
|
-
// header): the projection deliberately drops each schedule entry's own
|
|
101
|
-
// `enabled` (P4-N6), so building bindings from it would silently ignore
|
|
102
|
-
// `--disabled`. A task source v4 document has no document-level
|
|
103
|
-
// `akm.enabled`, so `enabled: true` is passed at the document level and
|
|
104
|
-
// every entry's own `enabled` (always present, defaulted at parse time)
|
|
105
|
-
// decides.
|
|
97
|
+
// Bindings are compiled from the original parsed document so each
|
|
98
|
+
// schedule entry's inputs reach the generated invocation.
|
|
106
99
|
const taskBindings = compileTaskSchedulerBindings({
|
|
107
100
|
id,
|
|
108
101
|
qualifiedRef,
|
|
109
102
|
...(bundle.installTarget ? { bundleTarget: bundle.installTarget } : {}),
|
|
110
|
-
enabled: true,
|
|
111
103
|
schedules: parsedTask.v4.schedule.map((schedule) => ({
|
|
112
104
|
cron: schedule.cron,
|
|
113
105
|
ordinal: schedule.ordinal,
|
|
114
|
-
enabled: schedule.enabled,
|
|
115
106
|
source: schedule.source,
|
|
116
107
|
inputs: schedule.inputs,
|
|
117
108
|
})),
|
|
@@ -124,6 +115,25 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
124
115
|
const writeAsset = deps.writeAsset ?? writeAssetToSource;
|
|
125
116
|
const deleteAsset = deps.deleteAsset ?? deleteAssetFromSource;
|
|
126
117
|
const commitBoundary = deps.commitBoundary ?? commitWriteTargetBoundary;
|
|
118
|
+
if (input.disabled) {
|
|
119
|
+
// Revoke before publishing replacement bytes. If a write/commit/sync
|
|
120
|
+
// fails, the safe partial state is an inert task, never an activated new
|
|
121
|
+
// source (or a stale native binding that can still dispatch it).
|
|
122
|
+
setSchedulerRefEnabled("task", qualifiedRef, false);
|
|
123
|
+
await writeAsset(writeTarget.source, writeTarget.config, ref, yaml);
|
|
124
|
+
commitBoundary(writeTarget, `Update tasks/${id}`);
|
|
125
|
+
await akmTasksSync(deps, bundle.bundleName);
|
|
126
|
+
return {
|
|
127
|
+
id,
|
|
128
|
+
ref: conceptIdFromTypeName("task", id),
|
|
129
|
+
path: assetPath,
|
|
130
|
+
bundleDir: stashDir,
|
|
131
|
+
schedule: taskBinding.cron,
|
|
132
|
+
enabled: false,
|
|
133
|
+
backend,
|
|
134
|
+
target: task.target,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
127
137
|
const transaction = await prepareTaskAddSchedulerTransaction({
|
|
128
138
|
id,
|
|
129
139
|
installTarget: bundle.installTarget,
|
|
@@ -213,13 +223,17 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
213
223
|
suppressNativeRollbackWhenExternalFails: true,
|
|
214
224
|
allowNativeRollbackAfterExternalFailure: (error) => error instanceof TaskSourceRestoredBoundaryError,
|
|
215
225
|
});
|
|
226
|
+
// Publish the host-local grant only after source and native state commit.
|
|
227
|
+
// A failure before this point therefore leaves any new binding inert at
|
|
228
|
+
// its scheduled-fire gate instead of authorizing partially committed code.
|
|
229
|
+
setSchedulerRefEnabled("task", qualifiedRef, true);
|
|
216
230
|
return {
|
|
217
231
|
id,
|
|
218
232
|
ref: conceptIdFromTypeName("task", id),
|
|
219
233
|
path: assetPath,
|
|
220
234
|
bundleDir: stashDir,
|
|
221
235
|
schedule: taskBinding.cron,
|
|
222
|
-
enabled:
|
|
236
|
+
enabled: true,
|
|
223
237
|
backend,
|
|
224
238
|
target: task.target,
|
|
225
239
|
};
|
|
@@ -250,6 +264,11 @@ export async function akmTasksRun(id, options = {}) {
|
|
|
250
264
|
const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
|
|
251
265
|
const resolvedId = taskIdForAdapter(parsed.id, adapterId);
|
|
252
266
|
const scheduled = options.scheduled === true;
|
|
267
|
+
const conceptId = adapterId === "akm" ? `tasks/${resolvedId}` : resolvedId;
|
|
268
|
+
const qualifiedRef = makeBundleRef(bundle.source.name, conceptId);
|
|
269
|
+
if (scheduled && !isSchedulerRefEnabled(loadConfig(), "task", qualifiedRef)) {
|
|
270
|
+
throw new UsageError(`Scheduled task ${JSON.stringify(qualifiedRef)} is not enabled in local scheduler config; run \`akm task enable ${qualifiedRef}\`.`, "INVALID_FLAG_VALUE");
|
|
271
|
+
}
|
|
253
272
|
// D5 "Construction" (spec docs/plans/specs/p1b-model-extraction.md §1.2/
|
|
254
273
|
// §5.2): built ONCE at this invocation boundary. eventSource is "task"
|
|
255
274
|
// whether or not --scheduled was passed (§1.6 D5-N1) — scheduled stays a
|
|
@@ -299,6 +318,42 @@ export async function akmTasksRun(id, options = {}) {
|
|
|
299
318
|
exitCode,
|
|
300
319
|
};
|
|
301
320
|
}
|
|
321
|
+
function resolveTaskActivation(ref, target, requireSchedulable) {
|
|
322
|
+
const parsed = parseTaskRef(ref);
|
|
323
|
+
const bundle = resolveTaskReadBundle(parsed.bundle, target);
|
|
324
|
+
const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
|
|
325
|
+
const id = taskIdForAdapter(parsed.id, adapterId);
|
|
326
|
+
const conceptId = adapterId === "akm" ? `tasks/${id}` : id;
|
|
327
|
+
const sourcePath = adapterId === "akm"
|
|
328
|
+
? path.join(bundle.source.path, "tasks", `${id}.yml`)
|
|
329
|
+
: path.join(bundle.source.path, `${id}.yml`);
|
|
330
|
+
if (requireSchedulable && !fs.existsSync(sourcePath)) {
|
|
331
|
+
throw new NotFoundError(`Task ${JSON.stringify(makeBundleRef(bundle.source.name, conceptId))} was not found.`);
|
|
332
|
+
}
|
|
333
|
+
if (requireSchedulable) {
|
|
334
|
+
const parsedSource = parseTaskSource({
|
|
335
|
+
yaml: fs.readFileSync(sourcePath, "utf8"),
|
|
336
|
+
filePath: sourcePath,
|
|
337
|
+
workspaceRoot: bundle.source.path,
|
|
338
|
+
});
|
|
339
|
+
if (parsedSource.v4.schedule.length === 0) {
|
|
340
|
+
throw new UsageError(`Task ${JSON.stringify(ref)} has no schedule to enable.`, "INVALID_FLAG_VALUE");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { qualifiedRef: makeBundleRef(bundle.source.name, conceptId), sourcePath, bundleName: bundle.source.name };
|
|
344
|
+
}
|
|
345
|
+
export async function akmTasksEnable(ref, options = {}, deps = {}) {
|
|
346
|
+
const resolved = resolveTaskActivation(ref, options.target, true);
|
|
347
|
+
const activation = setSchedulerRefEnabled("task", resolved.qualifiedRef, true);
|
|
348
|
+
const sync = await akmTasksSync(deps, resolved.bundleName);
|
|
349
|
+
return { ref: resolved.qualifiedRef, enabled: true, changed: activation.changed, sync };
|
|
350
|
+
}
|
|
351
|
+
export async function akmTasksDisable(ref, options = {}, deps = {}) {
|
|
352
|
+
const resolved = resolveTaskActivation(ref, options.target, false);
|
|
353
|
+
const activation = setSchedulerRefEnabled("task", resolved.qualifiedRef, false);
|
|
354
|
+
const sync = await akmTasksSync(deps, resolved.bundleName);
|
|
355
|
+
return { ref: resolved.qualifiedRef, enabled: false, changed: activation.changed, sync };
|
|
356
|
+
}
|
|
302
357
|
export async function akmTasksHistory(input) {
|
|
303
358
|
const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
|
|
304
359
|
const parsed = input.id ? parseTaskRef(input.id) : undefined;
|
|
@@ -309,23 +364,15 @@ export async function akmTasksHistory(input) {
|
|
|
309
364
|
return { rows: readTaskHistory({ id, limit }) };
|
|
310
365
|
}
|
|
311
366
|
/**
|
|
312
|
-
* Reconcile
|
|
313
|
-
* •
|
|
314
|
-
*
|
|
367
|
+
* Reconcile host-local scheduler activation with authored tasks/workflows.
|
|
368
|
+
* • without --bundle, scan every enabled configured bundle in one transaction
|
|
369
|
+
* • with --bundle, reconcile only that bundle
|
|
370
|
+
* • only refs present in scheduler.enabled enter the desired set
|
|
315
371
|
* • install missing bindings only after that whole-set preflight succeeds
|
|
316
|
-
* • reinstall
|
|
372
|
+
* • reinstall bindings whose authored schedule or runtime state changed
|
|
317
373
|
* (drift detected by comparing the backend's installed signature against
|
|
318
374
|
* the signature the current definition would produce)
|
|
319
375
|
* • remove orphan scheduler entries that no longer have a backing file
|
|
320
|
-
*
|
|
321
|
-
* `--bundle <bundle>` scopes the reconciliation to that bundle: the file set is
|
|
322
|
-
* the bundle's `tasks/*.yml` and — crucially — the scheduler entries considered
|
|
323
|
-
* are ONLY those attributed to the same bundle (parsed from the installed
|
|
324
|
-
* `--bundle` token; absent ⇒ primary). This is the security boundary that keeps
|
|
325
|
-
* "registering a bundle never activates code": a plain (primary) sync never
|
|
326
|
-
* installs from, updates, or removes another bundle's entries, and sync never
|
|
327
|
-
* scans all bundles. Activation happens only through explicit `add --bundle`
|
|
328
|
-
* (or `sync --bundle` on a bundle whose task files are already present).
|
|
329
376
|
*/
|
|
330
377
|
/**
|
|
331
378
|
* Compute (but never apply) a scheduler sync plan: everything through
|
|
@@ -337,10 +384,7 @@ export async function akmTasksHistory(input) {
|
|
|
337
384
|
* invoked here — only `applySchedulerSyncPlan` may call it.
|
|
338
385
|
*/
|
|
339
386
|
async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
340
|
-
const resolved = resolveTaskReadBundle(undefined, bundleTarget);
|
|
341
387
|
const config = loadConfig();
|
|
342
|
-
const stashDir = resolved.source.path;
|
|
343
|
-
const syncTarget = bundleTarget !== undefined && !isPrimaryStashPath(stashDir) ? bundleTarget : undefined;
|
|
344
388
|
const sched = deps.backend ?? selectBackend();
|
|
345
389
|
if (!sched.inspectBindings) {
|
|
346
390
|
throw new ConfigError(`Scheduler backend "${sched.name}" cannot provide one coherent inspection for transactional sync.`, "INVALID_CONFIG_FILE");
|
|
@@ -365,51 +409,176 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
365
409
|
};
|
|
366
410
|
});
|
|
367
411
|
const nativeArtifacts = inspection.artifacts;
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
412
|
+
const configuredSources = resolveConfiguredSources(config);
|
|
413
|
+
const activeSources = resolveActiveConfiguredSources(config);
|
|
414
|
+
const sourceNames = bundleTarget ? [bundleTarget] : activeSources.map((source) => source.name);
|
|
415
|
+
const inactiveOperations = bundleTarget
|
|
416
|
+
? []
|
|
417
|
+
: inactiveBundleRemovalOperations(config, configuredSources, allEntries, nativeArtifacts);
|
|
418
|
+
if (!bundleTarget && sourceNames.length === 0 && configuredSources.length > 0) {
|
|
419
|
+
assertSchedulerBackendInspection({ installed: allEntries, artifacts: nativeArtifacts });
|
|
420
|
+
const plan = emptySchedulerSyncPlan(inactiveOperations);
|
|
421
|
+
return { sched, plan, sourceSnapshots: Object.freeze([]), prepared: undefined, warnings: [] };
|
|
422
|
+
}
|
|
423
|
+
const selectedNames = sourceNames.length > 0 ? sourceNames : [undefined];
|
|
424
|
+
const enabled = activeSchedulerActivations(config);
|
|
425
|
+
const enabledActivations = new Set(enabled.map((activation) => `${activation.kind}\0${activation.ref}`));
|
|
426
|
+
const preparedSets = [];
|
|
427
|
+
for (const sourceName of selectedNames) {
|
|
428
|
+
const resolved = resolveTaskReadBundle(undefined, sourceName);
|
|
429
|
+
const stashDir = resolved.source.path;
|
|
430
|
+
const syncTarget = sourceName !== undefined && !isPrimaryStashPath(stashDir) ? sourceName : undefined;
|
|
431
|
+
const common = {
|
|
432
|
+
sourceRoot: stashDir,
|
|
433
|
+
adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
|
|
434
|
+
bundleName: resolved.source.name,
|
|
435
|
+
...(syncTarget === undefined ? { bundlePath: path.resolve(stashDir) } : {}),
|
|
436
|
+
...(syncTarget ? { bundleTarget: syncTarget } : {}),
|
|
437
|
+
backend: sched.name,
|
|
438
|
+
installed: allEntries,
|
|
439
|
+
nativeArtifacts,
|
|
440
|
+
inspection: Object.freeze({ installed: allEntries, artifacts: nativeArtifacts }),
|
|
441
|
+
enabledActivations,
|
|
442
|
+
rebind: options.rebind === true,
|
|
443
|
+
config,
|
|
444
|
+
resolveAsset: taskProjectionAssetResolver(config, resolved.source.name, stashDir),
|
|
445
|
+
};
|
|
446
|
+
preparedSets.push({
|
|
447
|
+
common,
|
|
448
|
+
preparedSources: await prepareSchedulerSyncSourceSet(common),
|
|
449
|
+
...(syncTarget ? { syncTarget } : {}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
// Pass one validates every selected bundle and computes whether any desired
|
|
453
|
+
// activation needs a new runtime descriptor before native mutation begins.
|
|
454
|
+
const preflights = preparedSets.map(({ common, preparedSources }) => finalizeSchedulerSyncPlan(common, preparedSources));
|
|
391
455
|
const warnings = [];
|
|
392
456
|
const expectedSignature = sched.expectedSignature?.bind(sched);
|
|
393
|
-
const needsRuntime = preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined);
|
|
457
|
+
const needsRuntime = preflights.some((preflight) => preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined));
|
|
394
458
|
const prepared = needsRuntime
|
|
395
|
-
? prepareSchedulerSyncRuntime(
|
|
459
|
+
? prepareSchedulerSyncRuntime(undefined, deps, warnings, allEntries.map((entry) => entry.binding))
|
|
396
460
|
: undefined;
|
|
397
|
-
const
|
|
461
|
+
const plans = preparedSets.map(({ common, preparedSources, syncTarget }) => finalizeSchedulerSyncPlan({
|
|
398
462
|
...common,
|
|
399
|
-
...(prepared?.options
|
|
463
|
+
...(prepared?.options
|
|
464
|
+
? { installOptions: { ...prepared.options, ...(syncTarget ? { target: syncTarget } : {}) } }
|
|
465
|
+
: syncTarget
|
|
466
|
+
? { installOptions: { target: syncTarget } }
|
|
467
|
+
: {}),
|
|
400
468
|
...(expectedSignature
|
|
401
469
|
? {
|
|
402
470
|
expectedSignature: (binding, install) => expectedSignature(binding, install),
|
|
403
471
|
}
|
|
404
472
|
: {}),
|
|
405
|
-
}, preparedSources);
|
|
406
|
-
|
|
473
|
+
}, preparedSources));
|
|
474
|
+
assertNoCrossBundleSchedulerCollisions(plans);
|
|
475
|
+
const representedRefs = new Set(plans.flatMap((candidate) => [
|
|
476
|
+
...candidate.desired.map((binding) => binding.logicalSource.ref),
|
|
477
|
+
...candidate.failures.flatMap((failure) => (failure.ref ? [failure.ref] : [])),
|
|
478
|
+
]));
|
|
479
|
+
const selectedBundleNames = new Set(preparedSets.map(({ common }) => common.bundleName));
|
|
480
|
+
const missingActivationFailures = enabled
|
|
481
|
+
.filter((activation) => {
|
|
482
|
+
const bundle = parseBundleRef(activation.ref).bundle;
|
|
483
|
+
return bundle !== undefined && selectedBundleNames.has(bundle) && !representedRefs.has(activation.ref);
|
|
484
|
+
})
|
|
485
|
+
.map((activation) => ({
|
|
486
|
+
path: activation.ref,
|
|
487
|
+
ref: activation.ref,
|
|
488
|
+
reason: `Enabled ${activation.kind} ${JSON.stringify(activation.ref)} was not found or has no schedule.`,
|
|
489
|
+
}));
|
|
490
|
+
const first = plans[0];
|
|
491
|
+
if (!first)
|
|
492
|
+
throw new ConfigError("No configured bundle is available for scheduler sync.", "INVALID_CONFIG_FILE");
|
|
493
|
+
const plan = Object.freeze({
|
|
494
|
+
desired: Object.freeze(plans.flatMap((candidate) => candidate.desired)),
|
|
495
|
+
installed: Object.freeze(plans.flatMap((candidate) => candidate.installed)),
|
|
496
|
+
updated: Object.freeze(plans.flatMap((candidate) => candidate.updated)),
|
|
497
|
+
removed: Object.freeze([
|
|
498
|
+
...plans.flatMap((candidate) => candidate.removed),
|
|
499
|
+
...inactiveOperations.map((operation) => operation.id),
|
|
500
|
+
]),
|
|
501
|
+
unchanged: Object.freeze(plans.flatMap((candidate) => candidate.unchanged)),
|
|
502
|
+
operations: Object.freeze([...plans.flatMap((candidate) => candidate.operations), ...inactiveOperations]),
|
|
503
|
+
sourceSnapshot: first.sourceSnapshot,
|
|
504
|
+
failures: Object.freeze([...plans.flatMap((candidate) => candidate.failures), ...missingActivationFailures]),
|
|
505
|
+
});
|
|
506
|
+
return { sched, plan, sourceSnapshots: plans.map((candidate) => candidate.sourceSnapshot), prepared, warnings };
|
|
507
|
+
}
|
|
508
|
+
function inactiveBundleRemovalOperations(config, configuredSources, installed, artifacts) {
|
|
509
|
+
const inactive = new Set(configuredSources.filter((source) => source.enabled === false).map((source) => source.name));
|
|
510
|
+
if (inactive.size === 0)
|
|
511
|
+
return [];
|
|
512
|
+
return installed
|
|
513
|
+
.map((entry) => ({ entry, bundleName: installedSchedulerBundle(config, entry) }))
|
|
514
|
+
.filter((candidate) => candidate.bundleName !== undefined && inactive.has(candidate.bundleName))
|
|
515
|
+
.sort((left, right) => left.entry.id.localeCompare(right.entry.id))
|
|
516
|
+
.map(({ entry, bundleName }) => {
|
|
517
|
+
const adapterId = bundleComponentConfig(config.bundles?.[bundleName])?.adapter ?? "akm";
|
|
518
|
+
return buildSchedulerRemoveOperation(entry.id, entry, artifacts, { adapterId, bundleName });
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
function installedSchedulerBundle(config, entry) {
|
|
522
|
+
const direct = entry.target ?? (entry.invocation ? scheduledInvocationBundle(entry.invocation) : undefined);
|
|
523
|
+
if (direct !== undefined)
|
|
524
|
+
return direct;
|
|
525
|
+
if (entry.ownerBundlePath === undefined)
|
|
526
|
+
return undefined;
|
|
527
|
+
return bundleKeyForContentRoot(config, entry.ownerBundlePath);
|
|
528
|
+
}
|
|
529
|
+
function emptySchedulerSyncPlan(operations) {
|
|
530
|
+
const sourceSnapshot = Object.freeze({
|
|
531
|
+
adapterId: "akm",
|
|
532
|
+
sourceRoot: "",
|
|
533
|
+
sourceRealPath: "",
|
|
534
|
+
sourcePhysicalIdentity: "inactive",
|
|
535
|
+
sourceDirectoryVersion: "inactive",
|
|
536
|
+
files: Object.freeze([]),
|
|
537
|
+
directoryManifests: Object.freeze([]),
|
|
538
|
+
});
|
|
539
|
+
return Object.freeze({
|
|
540
|
+
desired: Object.freeze([]),
|
|
541
|
+
installed: Object.freeze([]),
|
|
542
|
+
updated: Object.freeze([]),
|
|
543
|
+
removed: Object.freeze(operations.map((operation) => operation.id)),
|
|
544
|
+
unchanged: Object.freeze([]),
|
|
545
|
+
operations: Object.freeze([...operations]),
|
|
546
|
+
sourceSnapshot,
|
|
547
|
+
failures: Object.freeze([]),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function scheduledInvocationBundle(invocation) {
|
|
551
|
+
const bundleIndex = invocation.indexOf("--bundle");
|
|
552
|
+
if (bundleIndex >= 0)
|
|
553
|
+
return invocation[bundleIndex + 1];
|
|
554
|
+
if (invocation[0] === "workflow" && invocation[1] === "run" && invocation[2]) {
|
|
555
|
+
try {
|
|
556
|
+
return parseBundleRef(invocation[2]).bundle;
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
function assertNoCrossBundleSchedulerCollisions(plans) {
|
|
565
|
+
const owners = new Map();
|
|
566
|
+
for (const binding of plans.flatMap((plan) => plan.desired)) {
|
|
567
|
+
const key = schedulerNativeArtifactKey(schedulerBindingNativeId(binding));
|
|
568
|
+
const existing = owners.get(key);
|
|
569
|
+
if (existing && existing.logicalSource.ref !== binding.logicalSource.ref) {
|
|
570
|
+
throw new UsageError(`Scheduler native id ${JSON.stringify(schedulerBindingNativeId(binding))} is claimed by both ` +
|
|
571
|
+
`${JSON.stringify(existing.logicalSource.ref)} and ${JSON.stringify(binding.logicalSource.ref)}. ` +
|
|
572
|
+
"Rename one task before enabling both.", "RESOURCE_ALREADY_EXISTS");
|
|
573
|
+
}
|
|
574
|
+
owners.set(key, binding);
|
|
575
|
+
}
|
|
407
576
|
}
|
|
408
577
|
export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
409
|
-
const { sched, plan, prepared, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
578
|
+
const { sched, plan, sourceSnapshots, prepared, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
410
579
|
await applySchedulerSyncPlan(sched, plan, prepared?.publish && plan.operations.some((operation) => operation.kind !== "remove")
|
|
411
580
|
? prepared.publish
|
|
412
|
-
: undefined);
|
|
581
|
+
: undefined, sourceSnapshots);
|
|
413
582
|
return {
|
|
414
583
|
installed: [...plan.installed],
|
|
415
584
|
updated: [...plan.updated],
|
|
@@ -600,10 +769,13 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
600
769
|
};
|
|
601
770
|
}
|
|
602
771
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
603
|
-
async function applySchedulerSyncPlan(backend, plan, publish) {
|
|
772
|
+
async function applySchedulerSyncPlan(backend, plan, publish, sourceSnapshots = [plan.sourceSnapshot]) {
|
|
604
773
|
await applySchedulerTransaction(backend, plan.operations, {
|
|
605
774
|
initialExpectations: plan.operations.map((operation) => operation.expected),
|
|
606
|
-
assertReadSet: () =>
|
|
775
|
+
assertReadSet: () => {
|
|
776
|
+
for (const snapshot of sourceSnapshots)
|
|
777
|
+
assertSchedulerSourceSnapshot(snapshot);
|
|
778
|
+
},
|
|
607
779
|
beforeOperation: (_operation, index) => {
|
|
608
780
|
if (index === 0)
|
|
609
781
|
publish?.();
|
|
@@ -1114,7 +1286,7 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
|
|
|
1114
1286
|
resolved = resolveWorkingStashTarget(config, { requireWritable: false });
|
|
1115
1287
|
}
|
|
1116
1288
|
else {
|
|
1117
|
-
const configured =
|
|
1289
|
+
const configured = resolveActiveConfiguredSources(config).some((source) => source.name === selector);
|
|
1118
1290
|
const implicit = configured ? undefined : resolveImplicitScheduledBundleTarget(config, selector);
|
|
1119
1291
|
resolved = implicit ?? resolveWriteTarget(config, selector, { requireWritable: false });
|
|
1120
1292
|
}
|
|
@@ -1243,22 +1415,8 @@ function renderTaskYaml(input) {
|
|
|
1243
1415
|
obj.model = input.model;
|
|
1244
1416
|
if (input.timeoutMs !== undefined)
|
|
1245
1417
|
obj.timeout = input.timeoutMs;
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
// carrying `enabled: false` rather than the v3 `akm.enabled: false` flag.
|
|
1249
|
-
// `TasksAddInput.schedule`/the `add` CLI's `--schedule` are both still
|
|
1250
|
-
// required, so the "no schedule to disable" usage error B-21 also
|
|
1251
|
-
// describes is unreachable through this call site today; the check below
|
|
1252
|
-
// still guards `renderTaskYaml` itself against ever being called with an
|
|
1253
|
-
// empty schedule string.
|
|
1254
|
-
if (input.schedule.length === 0) {
|
|
1255
|
-
if (!input.enabled) {
|
|
1256
|
-
throw new UsageError("--disabled requires --schedule; a task with no schedule is already manual-only.");
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
else {
|
|
1260
|
-
obj.schedule = input.enabled ? input.schedule : [{ cron: input.schedule, enabled: false }];
|
|
1261
|
-
}
|
|
1418
|
+
if (input.schedule.length > 0)
|
|
1419
|
+
obj.schedule = input.schedule;
|
|
1262
1420
|
return yamlStringify(obj);
|
|
1263
1421
|
}
|
|
1264
1422
|
function assertInlineTaskPrompt(input) {
|
|
@@ -1282,119 +1440,6 @@ function parseJsonObjectArg(raw) {
|
|
|
1282
1440
|
}
|
|
1283
1441
|
return parsed;
|
|
1284
1442
|
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Toggle a task source v4 YAML file's `enabled` state without a full
|
|
1287
|
-
* parse/render round-trip (which would reformat the file). Task source v4
|
|
1288
|
-
* has no document-level `enabled` flag — it lives on each `schedule[]` entry
|
|
1289
|
-
* instead (D2-N5, P4-N6) — so this walks the top-level `schedule:` block,
|
|
1290
|
-
* finds every list entry in it (each line starting with `-` at the block's
|
|
1291
|
-
* item indent), and toggles that entry's own `enabled:` key, the closest v4
|
|
1292
|
-
* equivalent of v3's single document-level flag broadcasting to every
|
|
1293
|
-
* trigger. Each entry is handled independently — one entry already carrying
|
|
1294
|
-
* `enabled:` and a sibling entry with no such key (D2-N3's `schedule[i]`
|
|
1295
|
-
* shape: `{cron, enabled?, inputs?}`) toggles the first and inserts into the
|
|
1296
|
-
* second, rather than one entry's existing key short-circuiting the other's
|
|
1297
|
-
* insertion.
|
|
1298
|
-
*
|
|
1299
|
-
* A bare string-shorthand schedule (`schedule: "0 9 * * *"`) has nowhere for
|
|
1300
|
-
* `enabled:` to live and is rewritten to the one-entry list form. A list
|
|
1301
|
-
* entry with no explicit `enabled:` key (defaulting to `true` at parse) gets
|
|
1302
|
-
* one inserted rather than being silently left unaffected. A document with
|
|
1303
|
-
* no `schedule:` key at all throws — there is no trigger to enable or
|
|
1304
|
-
* disable (mirrors `renderTaskYaml`'s `--disabled`-with-no-`--schedule`
|
|
1305
|
-
* usage error, row B-21).
|
|
1306
|
-
*
|
|
1307
|
-
* Each entry's own key indent is taken from its `-` line (the indent before
|
|
1308
|
-
* `-`, plus two spaces for the conventional single space after it), so a
|
|
1309
|
-
* nested mapping inside an entry — e.g. `schedule[i].inputs` — sits deeper
|
|
1310
|
-
* and is never mistaken for the entry's own `enabled:` key.
|
|
1311
|
-
*
|
|
1312
|
-
* Preserves inline comments (e.g. `enabled: true # important`) and uses
|
|
1313
|
-
* case-sensitive matching (YAML keys are case-sensitive).
|
|
1314
|
-
*/
|
|
1315
|
-
export function setEnabledInYaml(yaml, enabled) {
|
|
1316
|
-
const lines = yaml.replace(/\r\n/g, "\n").split("\n");
|
|
1317
|
-
const scalarLine = lines.findIndex((line) => /^schedule:[ \t]+\S/.test(line));
|
|
1318
|
-
if (scalarLine >= 0) {
|
|
1319
|
-
const line = lines[scalarLine];
|
|
1320
|
-
const match = line?.match(/^schedule:[ \t]+([^\r\n]+?)[ \t]*(#[^\r\n]*)?$/);
|
|
1321
|
-
const cron = match?.[1] ?? "";
|
|
1322
|
-
const comment = match?.[2] ? ` ${match[2]}` : "";
|
|
1323
|
-
lines.splice(scalarLine, 1, "schedule:", ` - cron: ${cron}${comment}`, ` enabled: ${enabled}`);
|
|
1324
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
1325
|
-
}
|
|
1326
|
-
const blockLine = lines.findIndex((line) => /^schedule:\s*(?:#.*)?$/.test(line));
|
|
1327
|
-
if (blockLine < 0) {
|
|
1328
|
-
throw new UsageError("Task source v4 must declare a schedule before its enabled state can be toggled.");
|
|
1329
|
-
}
|
|
1330
|
-
// Find the block's extent and every top-level list item (`-`) within it.
|
|
1331
|
-
// Only items at the *first* item's own indent count as entries — anything
|
|
1332
|
-
// deeper belongs to a nested mapping/list inside an entry (e.g. an array
|
|
1333
|
-
// input under `inputs:`) and must not be treated as a sibling entry.
|
|
1334
|
-
let blockEnd = lines.length;
|
|
1335
|
-
const itemStarts = [];
|
|
1336
|
-
let topIndent = null;
|
|
1337
|
-
for (let index = blockLine + 1; index < lines.length; index += 1) {
|
|
1338
|
-
const line = lines[index];
|
|
1339
|
-
if (line === undefined) {
|
|
1340
|
-
blockEnd = index;
|
|
1341
|
-
break;
|
|
1342
|
-
}
|
|
1343
|
-
if (line !== "" && !/^[ \t]/.test(line)) {
|
|
1344
|
-
blockEnd = index;
|
|
1345
|
-
break;
|
|
1346
|
-
}
|
|
1347
|
-
const itemMatch = line.match(/^([ \t]*)-(?=[ \t]|$)/);
|
|
1348
|
-
if (itemMatch) {
|
|
1349
|
-
const itemIndent = itemMatch[1] ?? "";
|
|
1350
|
-
if (topIndent === null)
|
|
1351
|
-
topIndent = itemIndent;
|
|
1352
|
-
if (itemIndent === topIndent)
|
|
1353
|
-
itemStarts.push(index);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
if (itemStarts.length === 0) {
|
|
1357
|
-
throw new UsageError("Task source v4's schedule: block has no entries to toggle enabled on.");
|
|
1358
|
-
}
|
|
1359
|
-
// Walk entries back-to-front: inserting a missing `enabled:` line shifts
|
|
1360
|
-
// every later line index by one, but never touches `itemStarts[j]` for
|
|
1361
|
-
// j <= i (an insertion for entry i lands at `itemStarts[i] + 1`, which is
|
|
1362
|
-
// at or after entry i's own start), so already-computed start/end bounds
|
|
1363
|
-
// for entries processed later in this loop (earlier in the list) stay valid.
|
|
1364
|
-
for (let i = itemStarts.length - 1; i >= 0; i -= 1) {
|
|
1365
|
-
const start = itemStarts[i];
|
|
1366
|
-
const end = i + 1 < itemStarts.length ? itemStarts[i + 1] : blockEnd;
|
|
1367
|
-
const dashLead = lines[start]?.match(/^([ \t]*)-/)?.[1] ?? "";
|
|
1368
|
-
const keyIndent = `${dashLead} `;
|
|
1369
|
-
let found = false;
|
|
1370
|
-
for (let index = start; index < end; index += 1) {
|
|
1371
|
-
const line = lines[index];
|
|
1372
|
-
if (line === undefined)
|
|
1373
|
-
continue;
|
|
1374
|
-
const isStart = index === start;
|
|
1375
|
-
const prefixMatch = isStart ? line.match(/^([ \t]*-[ \t]*)(.*)$/) : line.match(/^([ \t]*)(.*)$/);
|
|
1376
|
-
const prefix = prefixMatch?.[1] ?? "";
|
|
1377
|
-
const content = prefixMatch?.[2] ?? "";
|
|
1378
|
-
if (prefix.length !== keyIndent.length)
|
|
1379
|
-
continue;
|
|
1380
|
-
const withValue = content.match(/^(enabled:[ \t]*)([^\s#\r\n][^\r\n]*?)([ \t]*(?:#[^\r\n]*))?$/);
|
|
1381
|
-
if (withValue) {
|
|
1382
|
-
lines[index] = `${prefix}${withValue[1]}${enabled}${withValue[3] ?? ""}`;
|
|
1383
|
-
found = true;
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
const bare = content.match(/^(enabled:)[ \t]*$/);
|
|
1387
|
-
if (bare) {
|
|
1388
|
-
lines[index] = `${prefix}${bare[1]} ${enabled}`;
|
|
1389
|
-
found = true;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
if (!found) {
|
|
1393
|
-
lines.splice(start + 1, 0, `${keyIndent}enabled: ${enabled}`);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
1397
|
-
}
|
|
1398
1443
|
// Re-exported so tests can verify the validator path directly.
|
|
1399
1444
|
// Re-export error classes consumed by callers that want to instanceof-check.
|
|
1400
1445
|
// Re-export this so the CLI can decide what process exit code to use after
|