@rse/ase 1.0.6 → 2.0.0
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/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
package/dst/ase-agent.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import { builtinKinds, discoverKinds } from "./ase-agent-definition-core.js";
|
|
7
|
+
import AgentAdhoc, { AgentBuiltin, AgentOfDefinition } from "./ase-agent-runtime-engine.js";
|
|
8
|
+
import AgentProcessCommand from "./ase-agent-process.js";
|
|
9
|
+
import AgentRelayCommand from "./ase-agent-relay-server-cli.js";
|
|
10
|
+
import AgentFleetCommand from "./ase-agent-fleet.js";
|
|
11
|
+
/* CLI command "ase agent" */
|
|
12
|
+
export default class AgentCommand {
|
|
13
|
+
log;
|
|
14
|
+
constructor(log) {
|
|
15
|
+
this.log = log;
|
|
16
|
+
}
|
|
17
|
+
/* register command */
|
|
18
|
+
register(program) {
|
|
19
|
+
/* the available agent kinds, each with the creation of a fresh agent: the built-in
|
|
20
|
+
kinds (each the agent of its declarative definitions), the ad-hoc kind, and the
|
|
21
|
+
definition kinds of the user and the project (see below) */
|
|
22
|
+
const kinds = new Map([
|
|
23
|
+
...builtinKinds.map((spec) => () => new AgentBuiltin(this.log, spec)),
|
|
24
|
+
() => new AgentAdhoc(this.log)
|
|
25
|
+
].map((create) => [create().kind, create]));
|
|
26
|
+
/* register CLI top-level command "ase agent" */
|
|
27
|
+
const cmd = program
|
|
28
|
+
.command("agent")
|
|
29
|
+
.description("Autonomous agents driving the task lifecycle phases (implementer, qualifier, integrator), " +
|
|
30
|
+
"rebooting and condensing the plans of tasks (rebooter, condenser), " +
|
|
31
|
+
"processing the edit requests of tasks (editor), estimating the effort of tasks (estimator), " +
|
|
32
|
+
"researching the plans and topics of tasks (researcher), " +
|
|
33
|
+
"or of the definitions of the user or the project " +
|
|
34
|
+
"(\".ase/agents/<kind>.{yaml,ts,js}\") or given ad-hoc (adhoc), " +
|
|
35
|
+
"each running in its own process, individually or as a fleet, optionally reporting to the agent relay")
|
|
36
|
+
.action(() => {
|
|
37
|
+
cmd.outputHelp();
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
/* register CLI sub-command groups "ase agent <kind>" (of the built-in and
|
|
41
|
+
the ad-hoc kinds), "ase agent relay", and "ase agent fleet" */
|
|
42
|
+
for (const create of kinds.values())
|
|
43
|
+
new AgentProcessCommand(this.log, create(), kinds).register(cmd);
|
|
44
|
+
new AgentRelayCommand(this.log).register(cmd);
|
|
45
|
+
new AgentFleetCommand(this.log, kinds).register(cmd);
|
|
46
|
+
/* register CLI sub-command groups "ase agent <kind>" of the definition kinds of
|
|
47
|
+
the user and the project on the dispatch of "ase agent" only, as their discovery
|
|
48
|
+
scans the file system, where a definition kind replaces a built-in kind */
|
|
49
|
+
program.hook("preSubcommand", (_program, sub) => {
|
|
50
|
+
if (sub !== cmd)
|
|
51
|
+
return;
|
|
52
|
+
const commands = cmd.commands;
|
|
53
|
+
for (const spec of discoverKinds(this.log, ["adhoc", "relay", "fleet"])) {
|
|
54
|
+
const shadowed = commands.findIndex((command) => command.name() === spec.kind);
|
|
55
|
+
const create = () => new AgentOfDefinition(this.log, spec, shadowed >= 0);
|
|
56
|
+
kinds.set(spec.kind, create);
|
|
57
|
+
if (shadowed >= 0)
|
|
58
|
+
commands.splice(shadowed, 1);
|
|
59
|
+
new AgentProcessCommand(this.log, create(), kinds).register(cmd);
|
|
60
|
+
if (shadowed >= 0)
|
|
61
|
+
commands.splice(shadowed, 0, commands.pop());
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dst/ase-artifact.js
CHANGED
|
@@ -8,10 +8,11 @@ import fs from "node:fs";
|
|
|
8
8
|
import picomatch from "picomatch";
|
|
9
9
|
import { isScalar } from "yaml";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { Config
|
|
11
|
+
import { Config } from "./ase-config-core.js";
|
|
12
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
12
13
|
import { Task } from "./ase-task.js";
|
|
13
|
-
import { Ignore } from "./ase-ignore.js";
|
|
14
|
-
import { writeStdout } from "./ase-stdio.js";
|
|
14
|
+
import { Ignore } from "./ase-lib-ignore.js";
|
|
15
|
+
import { writeStdout } from "./ase-lib-stdio.js";
|
|
15
16
|
/* the recognized artifact kinds, in descending precedence order;
|
|
16
17
|
"othr" is the implicit catch-all and is always resolved last */
|
|
17
18
|
export const artifactKinds = ["spec", "code", "docs", "infr", "othr"];
|
|
@@ -89,8 +90,8 @@ export class Artifact {
|
|
|
89
90
|
for a single kind; "basedir" is project-root-relative (POSIX,
|
|
90
91
|
"" ≡ project root) and "files" resolves relative to "basedir" */
|
|
91
92
|
static spec(cfg, kind) {
|
|
92
|
-
const basedir = Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
|
|
93
|
-
.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
|
|
93
|
+
const basedir = path.posix.normalize(Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
|
|
94
|
+
.replace(/\\/g, "/")).replace(/^\/+|\/+$/g, "")
|
|
94
95
|
.replace(/^\.$/, "");
|
|
95
96
|
if (basedir.split("/").includes(".."))
|
|
96
97
|
throw new Error(`artifact: configured "basedir" "${basedir}" must not escape the project root`);
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { Document, parseDocument, isMap, isScalar } from "yaml";
|
|
9
|
+
import * as v from "valibot";
|
|
10
|
+
import writeFileAtomic from "write-file-atomic";
|
|
11
|
+
import lockfile from "proper-lockfile";
|
|
12
|
+
import { projectRoot, userConfigDir, userStateDir } from "./ase-config-scope.js";
|
|
13
|
+
import { projectClassificationPresets, configWritableScopes, configWritableScopesDefault, configSecretKeys, sessionStateSchema } from "./ase-config-schema.js";
|
|
14
|
+
/* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
|
|
15
|
+
each associated with a scope term; reads cascade along user < project < task < session,
|
|
16
|
+
writes are confined to the target (strongest) scope term */
|
|
17
|
+
export class Config {
|
|
18
|
+
/* public state */
|
|
19
|
+
filename;
|
|
20
|
+
/* private state */
|
|
21
|
+
name;
|
|
22
|
+
scope;
|
|
23
|
+
schema;
|
|
24
|
+
log;
|
|
25
|
+
docs;
|
|
26
|
+
target;
|
|
27
|
+
pruned;
|
|
28
|
+
locked = null;
|
|
29
|
+
/* creation */
|
|
30
|
+
constructor(name, schema, log, scope = [{ kind: "user" }, { kind: "project" }]) {
|
|
31
|
+
if (scope.length === 0)
|
|
32
|
+
throw new Error("invalid scope: chain must not be empty");
|
|
33
|
+
this.name = name;
|
|
34
|
+
this.scope = scope[0].kind === "default" ? scope : [{ kind: "default" }, ...scope];
|
|
35
|
+
this.schema = schema ?? null;
|
|
36
|
+
this.log = log;
|
|
37
|
+
const tgt = this.scope[this.scope.length - 1];
|
|
38
|
+
this.filename = this.resolveFilename(name, tgt);
|
|
39
|
+
this.docs = [{ scope: tgt, filename: this.filename, doc: new Document() }];
|
|
40
|
+
this.target = 0;
|
|
41
|
+
this.pruned = [];
|
|
42
|
+
}
|
|
43
|
+
/* the configuration files of the scope chain (after reading) */
|
|
44
|
+
files() {
|
|
45
|
+
return this.docs.map((layer) => layer.filename).filter((filename) => filename !== "");
|
|
46
|
+
}
|
|
47
|
+
/* render a scope term as a short textual label */
|
|
48
|
+
static scopeLabel(term) {
|
|
49
|
+
if (term.kind === "default" || term.kind === "user" || term.kind === "project")
|
|
50
|
+
return term.kind;
|
|
51
|
+
return `${term.kind}:${term.id}`;
|
|
52
|
+
}
|
|
53
|
+
/* resolve the configuration filename based on the selected scope term */
|
|
54
|
+
resolveFilename(name, term) {
|
|
55
|
+
if (term.kind === "default")
|
|
56
|
+
throw new Error("internal error: \"default\" scope has no filename");
|
|
57
|
+
if (term.kind === "user")
|
|
58
|
+
return path.join(userConfigDir(), `${name}.yaml`);
|
|
59
|
+
else if (term.kind === "project") {
|
|
60
|
+
const rel = path.join(".ase", `${name}.yaml`);
|
|
61
|
+
const root = projectRoot() ?? process.cwd();
|
|
62
|
+
const found = this.findUpward(process.cwd(), root, rel);
|
|
63
|
+
return found ?? path.join(root, rel);
|
|
64
|
+
}
|
|
65
|
+
else if (term.kind === "task") {
|
|
66
|
+
const root = projectRoot() ?? process.cwd();
|
|
67
|
+
return path.join(root, ".ase", "task", term.id, `${name}.yaml`);
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
return path.join(userStateDir(), "session", term.id, `${name}.yaml`);
|
|
71
|
+
}
|
|
72
|
+
/* upward-walk on filesystem for a file path relative to a start directory,
|
|
73
|
+
bounded above (inclusive) by a stop directory */
|
|
74
|
+
findUpward(start, stop, rel) {
|
|
75
|
+
let dir = fs.realpathSync(start);
|
|
76
|
+
const end = fs.realpathSync(stop);
|
|
77
|
+
const steps = dir.split(path.sep).length - end.split(path.sep).length;
|
|
78
|
+
if (steps < 0)
|
|
79
|
+
return null;
|
|
80
|
+
for (let i = 0; i <= steps; i++) {
|
|
81
|
+
const candidate = path.join(dir, rel);
|
|
82
|
+
if (fs.existsSync(candidate))
|
|
83
|
+
return candidate;
|
|
84
|
+
const parent = path.dirname(dir);
|
|
85
|
+
if (parent === dir)
|
|
86
|
+
return null;
|
|
87
|
+
dir = parent;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/* read the full scope chain into memory; the requested mode applies
|
|
92
|
+
to the target scope only, inherited scopes are always lenient */
|
|
93
|
+
read(mode = "lenient") {
|
|
94
|
+
const chain = this.scope;
|
|
95
|
+
const docs = [];
|
|
96
|
+
this.pruned = [];
|
|
97
|
+
for (let i = 0; i < chain.length; i++) {
|
|
98
|
+
const sc = chain[i];
|
|
99
|
+
if (sc.kind === "default") {
|
|
100
|
+
const doc = new Document();
|
|
101
|
+
doc.contents = doc.createNode({});
|
|
102
|
+
if (this.name === "config") {
|
|
103
|
+
const preset = projectClassificationPresets.default;
|
|
104
|
+
for (const [k, val] of Object.entries(preset)) {
|
|
105
|
+
const segments = k.split(".");
|
|
106
|
+
for (let j = 1; j < segments.length; j++) {
|
|
107
|
+
const prefix = segments.slice(0, j);
|
|
108
|
+
const node = doc.getIn(prefix, true);
|
|
109
|
+
if (node === undefined)
|
|
110
|
+
doc.setIn(prefix, doc.createNode({}));
|
|
111
|
+
}
|
|
112
|
+
doc.setIn(segments, doc.createNode(val));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
docs.push({ scope: sc, filename: "", doc });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const filename = this.resolveFilename(this.name, sc);
|
|
119
|
+
const isTarget = (i === chain.length - 1);
|
|
120
|
+
const perDocMode = isTarget ? mode : "lenient";
|
|
121
|
+
const text = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : "";
|
|
122
|
+
let doc = parseDocument(text);
|
|
123
|
+
if (doc.errors.length > 0) {
|
|
124
|
+
const msg = `invalid YAML in ${filename}: ${doc.errors[0].message}`;
|
|
125
|
+
if (perDocMode === "strict")
|
|
126
|
+
throw new Error(msg);
|
|
127
|
+
this.log.write("warning", msg);
|
|
128
|
+
if (isTarget)
|
|
129
|
+
this.pruned.push(`unparsable YAML (${doc.errors[0].message.split("\n")[0]})`);
|
|
130
|
+
doc = new Document();
|
|
131
|
+
}
|
|
132
|
+
else if (this.name === "config" && this.migrate(doc)) {
|
|
133
|
+
/* persist the migration under the file lock (unless already held by us),
|
|
134
|
+
re-reading the file there to not clobber a concurrent write */
|
|
135
|
+
const persist = () => {
|
|
136
|
+
const disk = parseDocument(fs.readFileSync(filename, "utf8"));
|
|
137
|
+
if (disk.errors.length > 0 || !this.migrate(disk))
|
|
138
|
+
return;
|
|
139
|
+
const empty = isMap(disk.contents) && disk.contents.items.length === 0;
|
|
140
|
+
writeFileAtomic.sync(filename, empty ? "" : disk.toString({ indent: 4 }), { encoding: "utf8", mode: this.fileMode(sc, disk) });
|
|
141
|
+
this.log.write("info", `migrated obsolete configuration entries in ${filename}`);
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
if (this.locked === filename)
|
|
145
|
+
persist();
|
|
146
|
+
else {
|
|
147
|
+
const release = lockfile.lockSync(filename);
|
|
148
|
+
try {
|
|
149
|
+
persist();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
release();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.log.write("warning", `failed to migrate obsolete configuration entries in ${filename}: ` +
|
|
158
|
+
(err instanceof Error ? err.message : String(err)));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
docs.push({ scope: sc, filename, doc });
|
|
162
|
+
}
|
|
163
|
+
this.docs = docs;
|
|
164
|
+
this.target = docs.length - 1;
|
|
165
|
+
for (let i = 0; i < docs.length; i++) {
|
|
166
|
+
const isTarget = (i === this.target);
|
|
167
|
+
const perDocMode = isTarget ? mode : "lenient";
|
|
168
|
+
const removed = this.validateDoc(docs[i].doc, docs[i].filename, perDocMode);
|
|
169
|
+
if (isTarget)
|
|
170
|
+
this.pruned.push(...removed);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/* migrate a configuration document in place from obsolete variables: the
|
|
174
|
+
(pre-1.1.0) "project.artifact.task.{basedir,files}", where "basedir" becomes
|
|
175
|
+
"project.task.store.url" (as "ase:<basedir>") unless the latter is already
|
|
176
|
+
set and "files" is dropped, and the (pre-1.1.0) "agent.{task,skill}", which
|
|
177
|
+
are dropped, as they moved into the internal session state;
|
|
178
|
+
returns whether the document was changed */
|
|
179
|
+
migrate(doc) {
|
|
180
|
+
let changed = false;
|
|
181
|
+
const task = ["project", "artifact", "task"];
|
|
182
|
+
if (isMap(doc.getIn(task))) {
|
|
183
|
+
const basedir = doc.getIn([...task, "basedir"]);
|
|
184
|
+
const parent = doc.getIn(["project", "task"]);
|
|
185
|
+
const store = doc.getIn(["project", "task", "store"]);
|
|
186
|
+
if (typeof basedir === "string" && basedir !== ""
|
|
187
|
+
&& (parent === undefined || isMap(parent)) && (store === undefined || isMap(store))
|
|
188
|
+
&& !doc.hasIn(["project", "task", "store", "url"]))
|
|
189
|
+
doc.setIn(["project", "task", "store", "url"], `ase:${basedir}`);
|
|
190
|
+
doc.deleteIn(task);
|
|
191
|
+
changed = true;
|
|
192
|
+
}
|
|
193
|
+
if (isMap(doc.getIn(["agent"])))
|
|
194
|
+
for (const key of ["task", "skill"])
|
|
195
|
+
if (doc.deleteIn(["agent", key]))
|
|
196
|
+
changed = true;
|
|
197
|
+
if (!changed)
|
|
198
|
+
return false;
|
|
199
|
+
for (const p of [["project", "artifact"], ["project"], ["agent"]]) {
|
|
200
|
+
const node = doc.getIn(p);
|
|
201
|
+
if (isMap(node) && node.items.length === 0)
|
|
202
|
+
doc.deleteIn(p);
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/* acquire a cross-process advisory lock on the target scope's file,
|
|
207
|
+
execute the callback, then release the lock */
|
|
208
|
+
lock(cb) {
|
|
209
|
+
const td = this.docs[this.target];
|
|
210
|
+
if (td.scope.kind === "default")
|
|
211
|
+
throw new Error("internal error: \"default\" scope is not lockable");
|
|
212
|
+
fs.mkdirSync(path.dirname(td.filename), { recursive: true });
|
|
213
|
+
if (!fs.existsSync(td.filename))
|
|
214
|
+
fs.writeFileSync(td.filename, "", { encoding: "utf8", mode: this.fileMode(td.scope, null) });
|
|
215
|
+
const release = lockfile.lockSync(td.filename);
|
|
216
|
+
this.locked = td.filename;
|
|
217
|
+
try {
|
|
218
|
+
cb();
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
this.locked = null;
|
|
222
|
+
release();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/* write in-memory configuration back to the target scope's file */
|
|
226
|
+
write() {
|
|
227
|
+
const td = this.docs[this.target];
|
|
228
|
+
if (td.scope.kind === "default")
|
|
229
|
+
throw new Error("internal error: \"default\" scope is not writable");
|
|
230
|
+
/* a lenient read physically removes the invalid content from the in-memory
|
|
231
|
+
target document, so writing that document back would silently erase the
|
|
232
|
+
very same content from the file on disk */
|
|
233
|
+
if (this.pruned.length > 0)
|
|
234
|
+
throw new Error(`refusing to overwrite ${td.filename}: it carries invalid content ` +
|
|
235
|
+
`(${this.pruned.join(", ")}) which was skipped on reading and hence would be ` +
|
|
236
|
+
"lost on writing -- repair the file first");
|
|
237
|
+
this.validateDoc(td.doc, td.filename, "strict");
|
|
238
|
+
fs.mkdirSync(path.dirname(td.filename), { recursive: true });
|
|
239
|
+
writeFileAtomic.sync(td.filename, td.doc.toString({ indent: 4 }), { encoding: "utf8", mode: this.fileMode(td.scope, td.doc) });
|
|
240
|
+
}
|
|
241
|
+
/* determine the file mode of a scope's file: user-private (0600) for the
|
|
242
|
+
user scope, the "store" files, and any file carrying secrets, else
|
|
243
|
+
undefined (which lets an existing file's mode be preserved) */
|
|
244
|
+
fileMode(scope, doc) {
|
|
245
|
+
if (scope.kind === "user" || this.name === "store"
|
|
246
|
+
|| (doc !== null && configSecretKeys.some((key) => doc.hasIn(key.split(".")))))
|
|
247
|
+
return 0o600;
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
/* validate a single YAML document against the optional schema; in "strict"
|
|
251
|
+
mode all invalid entries are reported as a thrown error, in "lenient" mode
|
|
252
|
+
they are removed from the document and their dotted paths are returned */
|
|
253
|
+
validateDoc(doc, filename, mode = "strict") {
|
|
254
|
+
if (this.schema === null)
|
|
255
|
+
return [];
|
|
256
|
+
const removed = [];
|
|
257
|
+
for (;;) {
|
|
258
|
+
const result = v.safeParse(this.schema, doc.toJS());
|
|
259
|
+
if (result.success)
|
|
260
|
+
return removed;
|
|
261
|
+
if (mode === "strict") {
|
|
262
|
+
const issues = result.issues.map((i) => {
|
|
263
|
+
const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
|
|
264
|
+
return dotPath ? `${dotPath}: ${i.message}` : i.message;
|
|
265
|
+
}).join("; ");
|
|
266
|
+
throw new Error(`invalid configuration in ${filename}: ${issues}`);
|
|
267
|
+
}
|
|
268
|
+
const before = removed.length;
|
|
269
|
+
for (const i of result.issues) {
|
|
270
|
+
const segs = (i.path ?? []).map((p) => String(p.key));
|
|
271
|
+
const dotPath = segs.join(".");
|
|
272
|
+
this.log.write("warning", `invalid entry in ${filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
|
|
273
|
+
if (segs.length > 0 && doc.deleteIn(segs))
|
|
274
|
+
removed.push(dotPath);
|
|
275
|
+
/* issues at the document root and issues whose stringified path does not
|
|
276
|
+
address a deletable node (e.g. a non-string YAML key like "404:", which
|
|
277
|
+
"toJS" stringifies) cannot be removed; processing continues with the
|
|
278
|
+
remaining issues */
|
|
279
|
+
}
|
|
280
|
+
if (removed.length === before)
|
|
281
|
+
return removed;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/* enumerate all full dotted leaf paths from the attached valibot schema */
|
|
285
|
+
schemaLeafPaths() {
|
|
286
|
+
const unwrap = (s) => {
|
|
287
|
+
let cur = s;
|
|
288
|
+
while (cur !== undefined && cur !== null && (cur.type === "optional" || cur.type === "nullish"
|
|
289
|
+
|| cur.type === "nullable" || cur.type === "undefinedable"))
|
|
290
|
+
cur = cur.wrapped;
|
|
291
|
+
return cur ?? null;
|
|
292
|
+
};
|
|
293
|
+
const walk = (s, prefix) => {
|
|
294
|
+
const u = unwrap(s);
|
|
295
|
+
if (u !== null
|
|
296
|
+
&& (u.type === "object" || u.type === "strict_object" || u.type === "loose_object")
|
|
297
|
+
&& u.entries !== undefined) {
|
|
298
|
+
const paths = [];
|
|
299
|
+
for (const [k, sub] of Object.entries(u.entries))
|
|
300
|
+
paths.push(...walk(sub, [...prefix, k]));
|
|
301
|
+
return paths;
|
|
302
|
+
}
|
|
303
|
+
return [prefix];
|
|
304
|
+
};
|
|
305
|
+
return walk(this.schema, []);
|
|
306
|
+
}
|
|
307
|
+
/* enumerate all full dotted keys known from the attached valibot schema */
|
|
308
|
+
keys() {
|
|
309
|
+
if (this.schema === null)
|
|
310
|
+
return [];
|
|
311
|
+
return this.schemaLeafPaths().map((p) => p.join("."));
|
|
312
|
+
}
|
|
313
|
+
/* resolve a (possibly trailing-segment) dotted key to its full schema path */
|
|
314
|
+
resolveKey(key) {
|
|
315
|
+
if (this.schema === null)
|
|
316
|
+
return key;
|
|
317
|
+
const segs = key.split(".");
|
|
318
|
+
const matches = this.schemaLeafPaths().filter((p) => {
|
|
319
|
+
if (p.length < segs.length)
|
|
320
|
+
return false;
|
|
321
|
+
for (let i = 0; i < segs.length; i++)
|
|
322
|
+
if (p[p.length - segs.length + i] !== segs[i])
|
|
323
|
+
return false;
|
|
324
|
+
return true;
|
|
325
|
+
});
|
|
326
|
+
if (matches.length === 0)
|
|
327
|
+
return key;
|
|
328
|
+
if (matches.length > 1)
|
|
329
|
+
throw new Error(`ambiguous key "${key}" matches: ${matches.map((m) => m.join(".")).join(", ")}`);
|
|
330
|
+
return matches[0].join(".");
|
|
331
|
+
}
|
|
332
|
+
/* retrieve the effective value at a dotted key (strongest scope wins),
|
|
333
|
+
or the target scope's root contents if no key is given */
|
|
334
|
+
get(key) {
|
|
335
|
+
if (key === undefined)
|
|
336
|
+
return this.docs[this.target].doc.contents;
|
|
337
|
+
return this.getScoped(key)?.value;
|
|
338
|
+
}
|
|
339
|
+
/* retrieve the effective value at a dotted key (strongest scope wins)
|
|
340
|
+
together with the scope term and the configuration file (empty for
|
|
341
|
+
the built-in "default" scope) which supplied it */
|
|
342
|
+
getScoped(key) {
|
|
343
|
+
const segs = this.resolveKey(key).split(".");
|
|
344
|
+
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
345
|
+
const node = this.docs[i].doc.getIn(segs);
|
|
346
|
+
if (node !== undefined)
|
|
347
|
+
return { value: node, scope: this.docs[i].scope, filename: this.docs[i].filename };
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
/* retrieve the explicitly configured value at a dotted key, i.e. the same
|
|
352
|
+
cascade as "get", but skipping the built-in "default" scope layer, so
|
|
353
|
+
callers can distinguish a deliberately configured value from a merely
|
|
354
|
+
preset one */
|
|
355
|
+
getExplicit(key) {
|
|
356
|
+
const segs = this.resolveKey(key).split(".");
|
|
357
|
+
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
358
|
+
if (this.docs[i].scope.kind === "default")
|
|
359
|
+
continue;
|
|
360
|
+
const node = this.docs[i].doc.getIn(segs);
|
|
361
|
+
if (node !== undefined)
|
|
362
|
+
return node;
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
/* enumerate the effective leaf entries across the full scope chain;
|
|
367
|
+
each returned entry identifies the originating scope */
|
|
368
|
+
entries() {
|
|
369
|
+
const keys = new Set();
|
|
370
|
+
const walk = (node, prefix) => {
|
|
371
|
+
if (isMap(node))
|
|
372
|
+
for (const item of node.items) {
|
|
373
|
+
const k = [...prefix, String(item.key)];
|
|
374
|
+
if (isMap(item.value))
|
|
375
|
+
walk(item.value, k);
|
|
376
|
+
else if (isScalar(item.value))
|
|
377
|
+
keys.add(k.join("."));
|
|
378
|
+
else
|
|
379
|
+
throw new Error(`key "${k.join(".")}" has unsupported node type`);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
for (const d of this.docs)
|
|
383
|
+
walk(d.doc.contents, []);
|
|
384
|
+
const result = [];
|
|
385
|
+
for (const k of keys) {
|
|
386
|
+
const segs = k.split(".");
|
|
387
|
+
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
388
|
+
const node = this.docs[i].doc.getIn(segs);
|
|
389
|
+
if (node !== undefined) {
|
|
390
|
+
result.push({ key: k, value: node, scope: this.docs[i].scope });
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
result.sort((a, b) => a.key.localeCompare(b.key));
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
/* determine whether a key is writable on a given scope kind */
|
|
399
|
+
isWritableOn(key, kind) {
|
|
400
|
+
if (kind === "default")
|
|
401
|
+
return false;
|
|
402
|
+
const resolved = this.resolveKey(key);
|
|
403
|
+
const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
|
|
404
|
+
return allowed.includes(kind);
|
|
405
|
+
}
|
|
406
|
+
/* enforce write-scope policy for the current target scope */
|
|
407
|
+
assertWritable(key) {
|
|
408
|
+
const td = this.docs[this.target];
|
|
409
|
+
const resolved = this.resolveKey(key);
|
|
410
|
+
const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
|
|
411
|
+
if (!allowed.includes(td.scope.kind))
|
|
412
|
+
throw new Error(`cannot set "${resolved}" on scope "${Config.scopeLabel(td.scope)}": ` +
|
|
413
|
+
`this key is only writable on scope(s): ${allowed.join(", ")}`);
|
|
414
|
+
}
|
|
415
|
+
/* set a value at a dotted key in the target scope, creating intermediate maps as needed */
|
|
416
|
+
set(key, value) {
|
|
417
|
+
this.assertWritable(key);
|
|
418
|
+
const segments = this.resolveKey(key).split(".");
|
|
419
|
+
const td = this.docs[this.target];
|
|
420
|
+
const next = td.doc.clone();
|
|
421
|
+
for (let i = 1; i < segments.length; i++) {
|
|
422
|
+
const prefix = segments.slice(0, i);
|
|
423
|
+
const node = next.getIn(prefix, true);
|
|
424
|
+
if (node !== undefined && !isMap(node))
|
|
425
|
+
throw new Error(`cannot set "${key}": intermediate path "${prefix.join(".")}" is not a map`);
|
|
426
|
+
if (node === undefined)
|
|
427
|
+
next.setIn(prefix, next.createNode({}));
|
|
428
|
+
}
|
|
429
|
+
next.setIn(segments, value);
|
|
430
|
+
const saved = td.doc;
|
|
431
|
+
td.doc = next;
|
|
432
|
+
try {
|
|
433
|
+
this.validateDoc(td.doc, td.filename, "strict");
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
td.doc = saved;
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/* delete a value at a dotted key from the target scope */
|
|
441
|
+
delete(key) {
|
|
442
|
+
this.assertWritable(key);
|
|
443
|
+
const td = this.docs[this.target];
|
|
444
|
+
if (td.doc.contents === null || td.doc.contents === undefined)
|
|
445
|
+
return;
|
|
446
|
+
const next = td.doc.clone();
|
|
447
|
+
next.deleteIn(this.resolveKey(key).split("."));
|
|
448
|
+
const saved = td.doc;
|
|
449
|
+
td.doc = next;
|
|
450
|
+
try {
|
|
451
|
+
this.validateDoc(td.doc, td.filename, "strict");
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
td.doc = saved;
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/* access the internal "state.yaml" of a session, which, unlike the
|
|
460
|
+
configuration, does not cascade through any weaker scope */
|
|
461
|
+
export const sessionState = (log, sessionId) => {
|
|
462
|
+
if (!/^[A-Za-z0-9._-]+$/.test(sessionId))
|
|
463
|
+
throw new Error(`invalid session id "${sessionId}"`);
|
|
464
|
+
return new Config("state", sessionStateSchema, log, [{ kind: "session", id: sessionId }]);
|
|
465
|
+
};
|