@rse/ase 1.0.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +357 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +228 -111
- 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} +8 -1
- 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 +269 -81
- 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 +231 -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 +81 -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-config.js
CHANGED
|
@@ -4,584 +4,16 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import os from "node:os";
|
|
8
7
|
import fs from "node:fs";
|
|
9
8
|
import readline from "node:readline/promises";
|
|
10
|
-
import {
|
|
9
|
+
import { isMap, isScalar } from "yaml";
|
|
11
10
|
import { execaSync } from "execa";
|
|
12
|
-
import * as v from "valibot";
|
|
13
|
-
import writeFileAtomic from "write-file-atomic";
|
|
14
|
-
import lockfile from "proper-lockfile";
|
|
15
11
|
import { z } from "zod";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
boxing: ["white", "grey", "black"]
|
|
22
|
-
};
|
|
23
|
-
/* agent classification taxonomy */
|
|
24
|
-
export const agentClassification = {
|
|
25
|
-
persona: ["writer", "engineer", "journalist", "telegrapher", "caveman"],
|
|
26
|
-
guidance: ["none", "minimal", "normal", "verbose"]
|
|
27
|
-
};
|
|
28
|
-
/* classification presets */
|
|
29
|
-
export const projectClassificationPresets = {
|
|
30
|
-
vibe: {
|
|
31
|
-
"agent.persona": "writer",
|
|
32
|
-
"project.id": "example",
|
|
33
|
-
"project.name": "Example Project",
|
|
34
|
-
"project.boxing": "black"
|
|
35
|
-
},
|
|
36
|
-
pro: {
|
|
37
|
-
"agent.persona": "engineer",
|
|
38
|
-
"project.id": "example",
|
|
39
|
-
"project.name": "Example Project",
|
|
40
|
-
"project.boxing": "white"
|
|
41
|
-
},
|
|
42
|
-
default: {
|
|
43
|
-
"agent.task": "default",
|
|
44
|
-
"agent.persona": "engineer",
|
|
45
|
-
"agent.guidance": "normal",
|
|
46
|
-
"project.id": "example",
|
|
47
|
-
"project.name": "Example Project",
|
|
48
|
-
"project.boxing": "white",
|
|
49
|
-
"project.artifact.task.basedir": ".ase/task",
|
|
50
|
-
"project.artifact.task.files": "*.md",
|
|
51
|
-
"project.artifact.spec.basedir": "docs/specbook",
|
|
52
|
-
"project.artifact.spec.files": "*.{md,txt,svg,png,jpg}",
|
|
53
|
-
"project.artifact.spec.schema": "",
|
|
54
|
-
"project.artifact.code.basedir": "src",
|
|
55
|
-
"project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
|
|
56
|
-
"project.artifact.docs.basedir": "doc",
|
|
57
|
-
"project.artifact.docs.files": "** **/{README,LICENSE,CHANGELOG}.{md,txt} !{spec,arch}/**",
|
|
58
|
-
"project.artifact.infr.basedir": "",
|
|
59
|
-
"project.artifact.infr.files": "**/{.github,.claude*,etc}/** **/{AGENTS.md,{package,tsconfig*}.json,.{git,npm}ignore}"
|
|
60
|
-
},
|
|
61
|
-
industry: {
|
|
62
|
-
"agent.persona": "engineer",
|
|
63
|
-
"project.id": "example",
|
|
64
|
-
"project.name": "Example Project",
|
|
65
|
-
"project.boxing": "grey"
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
/* hard-coded map: which scope kinds each variable may be SET on
|
|
69
|
-
(reads always cascade through the full chain; this restricts writes only);
|
|
70
|
-
keys absent from this map default to all non-"default" scope kinds */
|
|
71
|
-
export const configWritableScopes = {
|
|
72
|
-
"agent.task": ["session"],
|
|
73
|
-
"agent.skill": ["session"],
|
|
74
|
-
"project.artifact.task.basedir": ["user", "project"],
|
|
75
|
-
"project.artifact.task.files": ["user", "project"],
|
|
76
|
-
"project.artifact.spec.basedir": ["user", "project"],
|
|
77
|
-
"project.artifact.spec.files": ["user", "project"],
|
|
78
|
-
"project.artifact.spec.schema": ["user", "project"],
|
|
79
|
-
"project.artifact.code.basedir": ["user", "project"],
|
|
80
|
-
"project.artifact.code.files": ["user", "project"],
|
|
81
|
-
"project.artifact.docs.basedir": ["user", "project"],
|
|
82
|
-
"project.artifact.docs.files": ["user", "project"],
|
|
83
|
-
"project.artifact.infr.basedir": ["user", "project"],
|
|
84
|
-
"project.artifact.infr.files": ["user", "project"]
|
|
85
|
-
};
|
|
86
|
-
/* default set of scope kinds writable for any unrestricted key */
|
|
87
|
-
const configWritableScopesDefault = ["user", "project", "task", "session"];
|
|
88
|
-
/* canonical ordering rank of a scope kind */
|
|
89
|
-
const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
|
|
90
|
-
/* parse a single scope term */
|
|
91
|
-
const parseScopeTerm = (value) => {
|
|
92
|
-
if (value === "user")
|
|
93
|
-
return { kind: "user" };
|
|
94
|
-
else if (value === "project")
|
|
95
|
-
return { kind: "project" };
|
|
96
|
-
const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
|
|
97
|
-
if (m !== null) {
|
|
98
|
-
if (m[2] === "." || m[2] === "..")
|
|
99
|
-
throw new Error(`invalid --scope term "${value}": id must not be "." or ".."`);
|
|
100
|
-
return { kind: m[1], id: m[2] };
|
|
101
|
-
}
|
|
102
|
-
throw new Error(`invalid --scope term "${value}" ` +
|
|
103
|
-
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
104
|
-
};
|
|
105
|
-
/* determine the Git top-level directory, if inside a Git repository;
|
|
106
|
-
cached per working directory ("" ≡ not inside a Git working tree), as
|
|
107
|
-
each determination spawns a Git subprocess and the Git context can
|
|
108
|
-
change over the lifetime of the long-running ASE service */
|
|
109
|
-
const gitToplevelCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
|
|
110
|
-
const gitToplevel = () => {
|
|
111
|
-
const cwd = process.cwd();
|
|
112
|
-
const cached = gitToplevelCache.get(cwd);
|
|
113
|
-
if (cached !== undefined)
|
|
114
|
-
return cached === "" ? null : cached;
|
|
115
|
-
let top = "";
|
|
116
|
-
try {
|
|
117
|
-
top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
/* not inside a Git working tree */
|
|
121
|
-
}
|
|
122
|
-
gitToplevelCache.set(cwd, top);
|
|
123
|
-
return top === "" ? null : top;
|
|
124
|
-
};
|
|
125
|
-
/* determine the project root directory, i.e. either the top-level
|
|
126
|
-
directory of the Git working tree or the nearest directory at or
|
|
127
|
-
above cwd which carries a ".ase" directory */
|
|
128
|
-
const projectRoot = () => {
|
|
129
|
-
const top = gitToplevel();
|
|
130
|
-
if (top !== null)
|
|
131
|
-
return top;
|
|
132
|
-
let dir = fs.realpathSync(process.cwd());
|
|
133
|
-
for (;;) {
|
|
134
|
-
if (fs.existsSync(path.join(dir, ".ase")))
|
|
135
|
-
return dir;
|
|
136
|
-
const parent = path.dirname(dir);
|
|
137
|
-
if (parent === dir)
|
|
138
|
-
return null;
|
|
139
|
-
dir = parent;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
/* detect whether a project context exists, i.e. either we are inside
|
|
143
|
-
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
144
|
-
const hasProjectContext = () => projectRoot() !== null;
|
|
145
|
-
/* parse a raw "--scope" option value into a canonical Scope chain;
|
|
146
|
-
accepts a comma-separated list of terms in any order. The "user"
|
|
147
|
-
term is always implicitly added at the bottom of the chain; the
|
|
148
|
-
"project" term is implicitly added only when a project context
|
|
149
|
-
exists (Git repository or ".ase" directory at or above cwd) and it
|
|
150
|
-
stays weaker than the strongest explicitly requested term, and an
|
|
151
|
-
explicit "project" term requires that same context */
|
|
152
|
-
export const parseScope = (value) => {
|
|
153
|
-
const projectActive = hasProjectContext();
|
|
154
|
-
const input = (value === undefined || value === "") ?
|
|
155
|
-
(projectActive ? "project" : "user") :
|
|
156
|
-
value.trim();
|
|
157
|
-
if (input === "")
|
|
158
|
-
throw new Error("invalid --scope: value must not be empty");
|
|
159
|
-
const terms = input.split(",").map((s) => parseScopeTerm(s.trim()));
|
|
160
|
-
const seen = new Set();
|
|
161
|
-
for (const t of terms) {
|
|
162
|
-
if (seen.has(t.kind))
|
|
163
|
-
throw new Error(`invalid --scope: duplicate term of kind "${t.kind}"`);
|
|
164
|
-
seen.add(t.kind);
|
|
165
|
-
}
|
|
166
|
-
if (seen.has("project") && !projectActive)
|
|
167
|
-
throw new Error("invalid --scope: \"project\" requires a project context " +
|
|
168
|
-
"(a Git repository or a \".ase\" directory at or above the current directory)");
|
|
169
|
-
/* the strongest term of the chain is the write target, so an implicitly
|
|
170
|
-
added "project" term must never outrank the strongest explicitly
|
|
171
|
-
requested term, as this would silently retarget the caller's request */
|
|
172
|
-
const rankMax = Math.max(...terms.map((t) => scopeRank(t.kind)));
|
|
173
|
-
if (!seen.has("project") && projectActive && rankMax > scopeRank("project"))
|
|
174
|
-
terms.unshift({ kind: "project" });
|
|
175
|
-
if (!seen.has("user"))
|
|
176
|
-
terms.unshift({ kind: "user" });
|
|
177
|
-
terms.sort((a, b) => scopeRank(a.kind) - scopeRank(b.kind));
|
|
178
|
-
terms.unshift({ kind: "default" });
|
|
179
|
-
return terms;
|
|
180
|
-
};
|
|
181
|
-
/* schema for a single artifact kind's "basedir"/"files" specification */
|
|
182
|
-
const artifactSchema = v.optional(v.strictObject({
|
|
183
|
-
basedir: v.optional(v.string()),
|
|
184
|
-
files: v.optional(v.string())
|
|
185
|
-
}));
|
|
186
|
-
/* schema for the "spec" artifact kind, additionally carrying the
|
|
187
|
-
whitespace-separated list of SpecBook YAML "schema" configuration
|
|
188
|
-
files, with "std" naming the bundled standard one (empty: "std") */
|
|
189
|
-
const artifactSpecSchema = v.optional(v.strictObject({
|
|
190
|
-
basedir: v.optional(v.string()),
|
|
191
|
-
files: v.optional(v.string()),
|
|
192
|
-
schema: v.optional(v.string())
|
|
193
|
-
}));
|
|
194
|
-
/* schema for ".ase/config.yaml" */
|
|
195
|
-
export const configSchema = v.nullish(v.strictObject({
|
|
196
|
-
project: v.optional(v.strictObject({
|
|
197
|
-
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
198
|
-
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
199
|
-
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
200
|
-
artifact: v.optional(v.strictObject({
|
|
201
|
-
spec: artifactSpecSchema,
|
|
202
|
-
code: artifactSchema,
|
|
203
|
-
docs: artifactSchema,
|
|
204
|
-
infr: artifactSchema,
|
|
205
|
-
task: artifactSchema
|
|
206
|
-
}))
|
|
207
|
-
})),
|
|
208
|
-
agent: v.optional(v.strictObject({
|
|
209
|
-
persona: v.optional(v.picklist(agentClassification.persona)),
|
|
210
|
-
guidance: v.optional(v.picklist(agentClassification.guidance)),
|
|
211
|
-
task: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
212
|
-
skill: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
213
|
-
}))
|
|
214
|
-
}));
|
|
215
|
-
/* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
|
|
216
|
-
each associated with a scope term; reads cascade along user < project < task < session,
|
|
217
|
-
writes are confined to the target (strongest) scope term */
|
|
218
|
-
export class Config {
|
|
219
|
-
/* public state */
|
|
220
|
-
filename;
|
|
221
|
-
/* private state */
|
|
222
|
-
name;
|
|
223
|
-
scope;
|
|
224
|
-
schema;
|
|
225
|
-
log;
|
|
226
|
-
docs;
|
|
227
|
-
target;
|
|
228
|
-
pruned;
|
|
229
|
-
/* creation */
|
|
230
|
-
constructor(name, schema, log, scope = [{ kind: "user" }, { kind: "project" }]) {
|
|
231
|
-
if (scope.length === 0)
|
|
232
|
-
throw new Error("invalid scope: chain must not be empty");
|
|
233
|
-
this.name = name;
|
|
234
|
-
this.scope = scope[0].kind === "default" ? scope : [{ kind: "default" }, ...scope];
|
|
235
|
-
this.schema = schema ?? null;
|
|
236
|
-
this.log = log;
|
|
237
|
-
const tgt = this.scope[this.scope.length - 1];
|
|
238
|
-
this.filename = this.resolveFilename(name, tgt);
|
|
239
|
-
this.docs = [{ scope: tgt, filename: this.filename, doc: new Document() }];
|
|
240
|
-
this.target = 0;
|
|
241
|
-
this.pruned = [];
|
|
242
|
-
}
|
|
243
|
-
/* render a scope term as a short textual label */
|
|
244
|
-
static scopeLabel(term) {
|
|
245
|
-
if (term.kind === "default" || term.kind === "user" || term.kind === "project")
|
|
246
|
-
return term.kind;
|
|
247
|
-
return `${term.kind}:${term.id}`;
|
|
248
|
-
}
|
|
249
|
-
/* resolve the per-OS user-scope base directory */
|
|
250
|
-
userConfigDir() {
|
|
251
|
-
if (process.platform === "darwin")
|
|
252
|
-
/* macOS */
|
|
253
|
-
return path.join(os.homedir(), "Library", "Application Support", "ase");
|
|
254
|
-
else if (process.platform === "win32")
|
|
255
|
-
/* Windows */
|
|
256
|
-
return path.join(process.env.APPDATA ?? os.homedir(), "ase");
|
|
257
|
-
else {
|
|
258
|
-
/* Linux */
|
|
259
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
260
|
-
const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".config");
|
|
261
|
-
return path.join(base, "ase");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
/* resolve the configuration filename based on the selected scope term */
|
|
265
|
-
resolveFilename(name, term) {
|
|
266
|
-
if (term.kind === "default")
|
|
267
|
-
throw new Error("internal error: \"default\" scope has no filename");
|
|
268
|
-
if (term.kind === "user")
|
|
269
|
-
return path.join(this.userConfigDir(), `${name}.yaml`);
|
|
270
|
-
else if (term.kind === "project") {
|
|
271
|
-
const rel = path.join(".ase", `${name}.yaml`);
|
|
272
|
-
const root = projectRoot() ?? process.cwd();
|
|
273
|
-
const found = this.findUpward(process.cwd(), root, rel);
|
|
274
|
-
return found ?? path.join(root, rel);
|
|
275
|
-
}
|
|
276
|
-
else if (term.kind === "task") {
|
|
277
|
-
const root = projectRoot() ?? process.cwd();
|
|
278
|
-
return path.join(root, ".ase", "task", term.id, `${name}.yaml`);
|
|
279
|
-
}
|
|
280
|
-
else
|
|
281
|
-
return path.join(os.homedir(), ".ase", "session", term.id, `${name}.yaml`);
|
|
282
|
-
}
|
|
283
|
-
/* upward-walk on filesystem for a file path relative to a start directory,
|
|
284
|
-
bounded above (inclusive) by a stop directory */
|
|
285
|
-
findUpward(start, stop, rel) {
|
|
286
|
-
let dir = fs.realpathSync(start);
|
|
287
|
-
const end = fs.realpathSync(stop);
|
|
288
|
-
const steps = dir.split(path.sep).length - end.split(path.sep).length;
|
|
289
|
-
if (steps < 0)
|
|
290
|
-
return null;
|
|
291
|
-
for (let i = 0; i <= steps; i++) {
|
|
292
|
-
const candidate = path.join(dir, rel);
|
|
293
|
-
if (fs.existsSync(candidate))
|
|
294
|
-
return candidate;
|
|
295
|
-
const parent = path.dirname(dir);
|
|
296
|
-
if (parent === dir)
|
|
297
|
-
return null;
|
|
298
|
-
dir = parent;
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
/* read the full scope chain into memory; the requested mode applies
|
|
303
|
-
to the target scope only, inherited scopes are always lenient */
|
|
304
|
-
read(mode = "lenient") {
|
|
305
|
-
const chain = this.scope;
|
|
306
|
-
const docs = [];
|
|
307
|
-
this.pruned = [];
|
|
308
|
-
for (let i = 0; i < chain.length; i++) {
|
|
309
|
-
const sc = chain[i];
|
|
310
|
-
if (sc.kind === "default") {
|
|
311
|
-
const doc = new Document();
|
|
312
|
-
doc.contents = doc.createNode({});
|
|
313
|
-
if (this.name === "config") {
|
|
314
|
-
const preset = projectClassificationPresets.default;
|
|
315
|
-
for (const [k, val] of Object.entries(preset)) {
|
|
316
|
-
const segments = k.split(".");
|
|
317
|
-
for (let j = 1; j < segments.length; j++) {
|
|
318
|
-
const prefix = segments.slice(0, j);
|
|
319
|
-
const node = doc.getIn(prefix, true);
|
|
320
|
-
if (node === undefined)
|
|
321
|
-
doc.setIn(prefix, doc.createNode({}));
|
|
322
|
-
}
|
|
323
|
-
doc.setIn(segments, doc.createNode(val));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
docs.push({ scope: sc, filename: "", doc });
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
const filename = this.resolveFilename(this.name, sc);
|
|
330
|
-
const isTarget = (i === chain.length - 1);
|
|
331
|
-
const perDocMode = isTarget ? mode : "lenient";
|
|
332
|
-
const text = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : "";
|
|
333
|
-
let doc = parseDocument(text);
|
|
334
|
-
if (doc.errors.length > 0) {
|
|
335
|
-
const msg = `invalid YAML in ${filename}: ${doc.errors[0].message}`;
|
|
336
|
-
if (perDocMode === "strict")
|
|
337
|
-
throw new Error(msg);
|
|
338
|
-
this.log.write("warning", msg);
|
|
339
|
-
if (isTarget)
|
|
340
|
-
this.pruned.push(`unparsable YAML (${doc.errors[0].message.split("\n")[0]})`);
|
|
341
|
-
doc = new Document();
|
|
342
|
-
}
|
|
343
|
-
docs.push({ scope: sc, filename, doc });
|
|
344
|
-
}
|
|
345
|
-
this.docs = docs;
|
|
346
|
-
this.target = docs.length - 1;
|
|
347
|
-
for (let i = 0; i < docs.length; i++) {
|
|
348
|
-
const isTarget = (i === this.target);
|
|
349
|
-
const perDocMode = isTarget ? mode : "lenient";
|
|
350
|
-
const removed = this.validateDoc(docs[i].doc, docs[i].filename, perDocMode);
|
|
351
|
-
if (isTarget)
|
|
352
|
-
this.pruned.push(...removed);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
/* acquire a cross-process advisory lock on the target scope's file,
|
|
356
|
-
execute the callback, then release the lock */
|
|
357
|
-
lock(cb) {
|
|
358
|
-
const td = this.docs[this.target];
|
|
359
|
-
if (td.scope.kind === "default")
|
|
360
|
-
throw new Error("internal error: \"default\" scope is not lockable");
|
|
361
|
-
fs.mkdirSync(path.dirname(td.filename), { recursive: true });
|
|
362
|
-
if (!fs.existsSync(td.filename))
|
|
363
|
-
fs.writeFileSync(td.filename, "", "utf8");
|
|
364
|
-
const release = lockfile.lockSync(td.filename);
|
|
365
|
-
try {
|
|
366
|
-
cb();
|
|
367
|
-
}
|
|
368
|
-
finally {
|
|
369
|
-
release();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
/* write in-memory configuration back to the target scope's file */
|
|
373
|
-
write() {
|
|
374
|
-
const td = this.docs[this.target];
|
|
375
|
-
if (td.scope.kind === "default")
|
|
376
|
-
throw new Error("internal error: \"default\" scope is not writable");
|
|
377
|
-
/* a lenient read physically removes the invalid content from the in-memory
|
|
378
|
-
target document, so writing that document back would silently erase the
|
|
379
|
-
very same content from the file on disk */
|
|
380
|
-
if (this.pruned.length > 0)
|
|
381
|
-
throw new Error(`refusing to overwrite ${td.filename}: it carries invalid content ` +
|
|
382
|
-
`(${this.pruned.join(", ")}) which was skipped on reading and hence would be ` +
|
|
383
|
-
"lost on writing -- repair the file first");
|
|
384
|
-
this.validateDoc(td.doc, td.filename, "strict");
|
|
385
|
-
fs.mkdirSync(path.dirname(td.filename), { recursive: true });
|
|
386
|
-
writeFileAtomic.sync(td.filename, td.doc.toString({ indent: 4 }), { encoding: "utf8" });
|
|
387
|
-
}
|
|
388
|
-
/* validate a single YAML document against the optional schema; in "strict"
|
|
389
|
-
mode all invalid entries are reported as a thrown error, in "lenient" mode
|
|
390
|
-
they are removed from the document and their dotted paths are returned */
|
|
391
|
-
validateDoc(doc, filename, mode = "strict") {
|
|
392
|
-
if (this.schema === null)
|
|
393
|
-
return [];
|
|
394
|
-
const removed = [];
|
|
395
|
-
for (;;) {
|
|
396
|
-
const result = v.safeParse(this.schema, doc.toJS());
|
|
397
|
-
if (result.success)
|
|
398
|
-
return removed;
|
|
399
|
-
if (mode === "strict") {
|
|
400
|
-
const issues = result.issues.map((i) => {
|
|
401
|
-
const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
|
|
402
|
-
return dotPath ? `${dotPath}: ${i.message}` : i.message;
|
|
403
|
-
}).join("; ");
|
|
404
|
-
throw new Error(`invalid configuration in ${filename}: ${issues}`);
|
|
405
|
-
}
|
|
406
|
-
const before = removed.length;
|
|
407
|
-
for (const i of result.issues) {
|
|
408
|
-
const segs = (i.path ?? []).map((p) => String(p.key));
|
|
409
|
-
const dotPath = segs.join(".");
|
|
410
|
-
this.log.write("warning", `invalid entry in ${filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
|
|
411
|
-
if (segs.length > 0 && doc.deleteIn(segs))
|
|
412
|
-
removed.push(dotPath);
|
|
413
|
-
/* issues at the document root and issues whose stringified path does not
|
|
414
|
-
address a deletable node (e.g. a non-string YAML key like "404:", which
|
|
415
|
-
"toJS" stringifies) cannot be removed; processing continues with the
|
|
416
|
-
remaining issues */
|
|
417
|
-
}
|
|
418
|
-
if (removed.length === before)
|
|
419
|
-
return removed;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/* enumerate all full dotted leaf paths from the attached valibot schema */
|
|
423
|
-
schemaLeafPaths() {
|
|
424
|
-
const unwrap = (s) => {
|
|
425
|
-
let cur = s;
|
|
426
|
-
while (cur !== undefined && cur !== null && (cur.type === "optional" || cur.type === "nullish"
|
|
427
|
-
|| cur.type === "nullable" || cur.type === "undefinedable"))
|
|
428
|
-
cur = cur.wrapped;
|
|
429
|
-
return cur ?? null;
|
|
430
|
-
};
|
|
431
|
-
const walk = (s, prefix) => {
|
|
432
|
-
const u = unwrap(s);
|
|
433
|
-
if (u !== null
|
|
434
|
-
&& (u.type === "object" || u.type === "strict_object" || u.type === "loose_object")
|
|
435
|
-
&& u.entries !== undefined) {
|
|
436
|
-
const paths = [];
|
|
437
|
-
for (const [k, sub] of Object.entries(u.entries))
|
|
438
|
-
paths.push(...walk(sub, [...prefix, k]));
|
|
439
|
-
return paths;
|
|
440
|
-
}
|
|
441
|
-
return [prefix];
|
|
442
|
-
};
|
|
443
|
-
return walk(this.schema, []);
|
|
444
|
-
}
|
|
445
|
-
/* resolve a (possibly trailing-segment) dotted key to its full schema path */
|
|
446
|
-
resolveKey(key) {
|
|
447
|
-
if (this.schema === null)
|
|
448
|
-
return key;
|
|
449
|
-
const segs = key.split(".");
|
|
450
|
-
const matches = this.schemaLeafPaths().filter((p) => {
|
|
451
|
-
if (p.length < segs.length)
|
|
452
|
-
return false;
|
|
453
|
-
for (let i = 0; i < segs.length; i++)
|
|
454
|
-
if (p[p.length - segs.length + i] !== segs[i])
|
|
455
|
-
return false;
|
|
456
|
-
return true;
|
|
457
|
-
});
|
|
458
|
-
if (matches.length === 0)
|
|
459
|
-
return key;
|
|
460
|
-
if (matches.length > 1)
|
|
461
|
-
throw new Error(`ambiguous key "${key}" matches: ${matches.map((m) => m.join(".")).join(", ")}`);
|
|
462
|
-
return matches[0].join(".");
|
|
463
|
-
}
|
|
464
|
-
/* retrieve the effective value at a dotted key (strongest scope wins),
|
|
465
|
-
or the target scope's root contents if no key is given */
|
|
466
|
-
get(key) {
|
|
467
|
-
if (key === undefined)
|
|
468
|
-
return this.docs[this.target].doc.contents;
|
|
469
|
-
const segs = this.resolveKey(key).split(".");
|
|
470
|
-
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
471
|
-
const node = this.docs[i].doc.getIn(segs);
|
|
472
|
-
if (node !== undefined)
|
|
473
|
-
return node;
|
|
474
|
-
}
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
/* retrieve the explicitly configured value at a dotted key, i.e. the same
|
|
478
|
-
cascade as "get", but skipping the built-in "default" scope layer, so
|
|
479
|
-
callers can distinguish a deliberately configured value from a merely
|
|
480
|
-
preset one */
|
|
481
|
-
getExplicit(key) {
|
|
482
|
-
const segs = this.resolveKey(key).split(".");
|
|
483
|
-
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
484
|
-
if (this.docs[i].scope.kind === "default")
|
|
485
|
-
continue;
|
|
486
|
-
const node = this.docs[i].doc.getIn(segs);
|
|
487
|
-
if (node !== undefined)
|
|
488
|
-
return node;
|
|
489
|
-
}
|
|
490
|
-
return undefined;
|
|
491
|
-
}
|
|
492
|
-
/* enumerate the effective leaf entries across the full scope chain;
|
|
493
|
-
each returned entry identifies the originating scope */
|
|
494
|
-
entries() {
|
|
495
|
-
const keys = new Set();
|
|
496
|
-
const walk = (node, prefix) => {
|
|
497
|
-
if (isMap(node))
|
|
498
|
-
for (const item of node.items) {
|
|
499
|
-
const k = [...prefix, String(item.key)];
|
|
500
|
-
if (isMap(item.value))
|
|
501
|
-
walk(item.value, k);
|
|
502
|
-
else if (isScalar(item.value))
|
|
503
|
-
keys.add(k.join("."));
|
|
504
|
-
else
|
|
505
|
-
throw new Error(`key "${k.join(".")}" has unsupported node type`);
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
for (const d of this.docs)
|
|
509
|
-
walk(d.doc.contents, []);
|
|
510
|
-
const result = [];
|
|
511
|
-
for (const k of keys) {
|
|
512
|
-
const segs = k.split(".");
|
|
513
|
-
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
514
|
-
const node = this.docs[i].doc.getIn(segs);
|
|
515
|
-
if (node !== undefined) {
|
|
516
|
-
result.push({ key: k, value: node, scope: this.docs[i].scope });
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
result.sort((a, b) => a.key.localeCompare(b.key));
|
|
522
|
-
return result;
|
|
523
|
-
}
|
|
524
|
-
/* determine whether a key is writable on a given scope kind */
|
|
525
|
-
isWritableOn(key, kind) {
|
|
526
|
-
if (kind === "default")
|
|
527
|
-
return false;
|
|
528
|
-
const resolved = this.resolveKey(key);
|
|
529
|
-
const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
|
|
530
|
-
return allowed.includes(kind);
|
|
531
|
-
}
|
|
532
|
-
/* enforce write-scope policy for the current target scope */
|
|
533
|
-
assertWritable(key) {
|
|
534
|
-
const td = this.docs[this.target];
|
|
535
|
-
const resolved = this.resolveKey(key);
|
|
536
|
-
const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
|
|
537
|
-
if (!allowed.includes(td.scope.kind))
|
|
538
|
-
throw new Error(`cannot set "${resolved}" on scope "${Config.scopeLabel(td.scope)}": ` +
|
|
539
|
-
`this key is only writable on scope(s): ${allowed.join(", ")}`);
|
|
540
|
-
}
|
|
541
|
-
/* set a value at a dotted key in the target scope, creating intermediate maps as needed */
|
|
542
|
-
set(key, value) {
|
|
543
|
-
this.assertWritable(key);
|
|
544
|
-
const segments = this.resolveKey(key).split(".");
|
|
545
|
-
const td = this.docs[this.target];
|
|
546
|
-
const next = td.doc.clone();
|
|
547
|
-
for (let i = 1; i < segments.length; i++) {
|
|
548
|
-
const prefix = segments.slice(0, i);
|
|
549
|
-
const node = next.getIn(prefix, true);
|
|
550
|
-
if (node !== undefined && !isMap(node))
|
|
551
|
-
throw new Error(`cannot set "${key}": intermediate path "${prefix.join(".")}" is not a map`);
|
|
552
|
-
if (node === undefined)
|
|
553
|
-
next.setIn(prefix, next.createNode({}));
|
|
554
|
-
}
|
|
555
|
-
next.setIn(segments, value);
|
|
556
|
-
const saved = td.doc;
|
|
557
|
-
td.doc = next;
|
|
558
|
-
try {
|
|
559
|
-
this.validateDoc(td.doc, td.filename, "strict");
|
|
560
|
-
}
|
|
561
|
-
catch (err) {
|
|
562
|
-
td.doc = saved;
|
|
563
|
-
throw err;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
/* delete a value at a dotted key from the target scope */
|
|
567
|
-
delete(key) {
|
|
568
|
-
this.assertWritable(key);
|
|
569
|
-
const td = this.docs[this.target];
|
|
570
|
-
if (td.doc.contents === null || td.doc.contents === undefined)
|
|
571
|
-
return;
|
|
572
|
-
const next = td.doc.clone();
|
|
573
|
-
next.deleteIn(this.resolveKey(key).split("."));
|
|
574
|
-
const saved = td.doc;
|
|
575
|
-
td.doc = next;
|
|
576
|
-
try {
|
|
577
|
-
this.validateDoc(td.doc, td.filename, "strict");
|
|
578
|
-
}
|
|
579
|
-
catch (err) {
|
|
580
|
-
td.doc = saved;
|
|
581
|
-
throw err;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
12
|
+
import { writeStdout } from "./ase-lib-stdio.js";
|
|
13
|
+
import { renderTable } from "./ase-lib-table.js";
|
|
14
|
+
import { parseScope } from "./ase-config-scope.js";
|
|
15
|
+
import { projectClassificationPresets, configSecretKeys, configSchema } from "./ase-config-schema.js";
|
|
16
|
+
import { Config } from "./ase-config-core.js";
|
|
585
17
|
/* CLI command "ase config" */
|
|
586
18
|
export default class ConfigCommand {
|
|
587
19
|
log;
|
|
@@ -628,17 +60,20 @@ export default class ConfigCommand {
|
|
|
628
60
|
/* register CLI sub-command "ase config list" */
|
|
629
61
|
configCmd
|
|
630
62
|
.command("list")
|
|
631
|
-
.description("list all
|
|
63
|
+
.description("list all available configuration keys and their effective values")
|
|
632
64
|
.action(async (_opts, cmd) => {
|
|
633
65
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
634
66
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
635
67
|
cfg.read();
|
|
636
|
-
const rows =
|
|
68
|
+
const rows = new Map();
|
|
69
|
+
for (const key of cfg.keys())
|
|
70
|
+
rows.set(key, [key, "", "-"]);
|
|
637
71
|
for (const e of cfg.entries()) {
|
|
638
72
|
const val = isScalar(e.value) ? e.value.value : e.value;
|
|
639
|
-
rows.
|
|
73
|
+
rows.set(e.key, [e.key, configSecretKeys.includes(e.key) ? "***" : String(val), Config.scopeLabel(e.scope)]);
|
|
640
74
|
}
|
|
641
|
-
|
|
75
|
+
const sorted = [...rows.values()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
76
|
+
await writeStdout(renderTable(["KEY", "VALUE", "SCOPE"], sorted));
|
|
642
77
|
});
|
|
643
78
|
/* register CLI sub-command "ase config edit" */
|
|
644
79
|
configCmd
|
|
@@ -646,15 +81,20 @@ export default class ConfigCommand {
|
|
|
646
81
|
.description("edit configuration file with $EDITOR")
|
|
647
82
|
.action(async (_opts, cmd) => {
|
|
648
83
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
649
|
-
const editor = process.env.
|
|
84
|
+
const editor = [process.env.VISUAL, process.env.EDITOR]
|
|
85
|
+
.find((e) => e !== undefined && e.trim() !== "") ?? "vi";
|
|
86
|
+
const [editorCmd, ...editorArgs] = editor.trim().split(/\s+/);
|
|
650
87
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
651
88
|
fs.mkdirSync(path.dirname(cfg.filename), { recursive: true });
|
|
652
89
|
if (!fs.existsSync(cfg.filename))
|
|
653
|
-
fs.writeFileSync(cfg.filename, "",
|
|
90
|
+
fs.writeFileSync(cfg.filename, "", {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
mode: scope[scope.length - 1].kind === "user" ? 0o600 : undefined
|
|
93
|
+
});
|
|
654
94
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
655
95
|
try {
|
|
656
96
|
for (;;) {
|
|
657
|
-
execaSync(
|
|
97
|
+
execaSync(editorCmd, [...editorArgs, cfg.filename], { stdio: "inherit" });
|
|
658
98
|
try {
|
|
659
99
|
cfg.read("strict");
|
|
660
100
|
break;
|
|
@@ -741,7 +181,7 @@ export class ConfigMCP {
|
|
|
741
181
|
"returns an empty string if no value is set.",
|
|
742
182
|
inputSchema: {
|
|
743
183
|
key: z.string()
|
|
744
|
-
.describe("dotted configuration key (e.g. \"agent.
|
|
184
|
+
.describe("dotted configuration key (e.g. \"agent.persona\")"),
|
|
745
185
|
scope: z.string()
|
|
746
186
|
.describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
|
|
747
187
|
}
|
|
@@ -766,7 +206,7 @@ export class ConfigMCP {
|
|
|
766
206
|
"the configuration schema before being persisted.",
|
|
767
207
|
inputSchema: {
|
|
768
208
|
key: z.string()
|
|
769
|
-
.describe("dotted configuration key (e.g. \"agent.
|
|
209
|
+
.describe("dotted configuration key (e.g. \"agent.persona\")"),
|
|
770
210
|
val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())])
|
|
771
211
|
.describe("value to store under `key`"),
|
|
772
212
|
scope: z.string()
|
|
@@ -794,7 +234,7 @@ export class ConfigMCP {
|
|
|
794
234
|
"(the strongest scope term in the chain). No-op if the key is not present.",
|
|
795
235
|
inputSchema: {
|
|
796
236
|
key: z.string()
|
|
797
|
-
.describe("dotted configuration key (e.g. \"agent.
|
|
237
|
+
.describe("dotted configuration key (e.g. \"agent.persona\")"),
|
|
798
238
|
scope: z.string()
|
|
799
239
|
.describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
|
|
800
240
|
}
|
|
@@ -841,7 +281,7 @@ export class ConfigMCP {
|
|
|
841
281
|
cfg.read();
|
|
842
282
|
const entries = cfg.entries().map((e) => ({
|
|
843
283
|
key: e.key,
|
|
844
|
-
value: String(isScalar(e.value) ? e.value.value : e.value),
|
|
284
|
+
value: configSecretKeys.includes(e.key) ? "***" : String(isScalar(e.value) ? e.value.value : e.value),
|
|
845
285
|
scope: Config.scopeLabel(e.scope)
|
|
846
286
|
}));
|
|
847
287
|
const result = { entries };
|