@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
|
@@ -0,0 +1,495 @@
|
|
|
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 { ofetch } from "ofetch";
|
|
9
|
+
import { Agent } from "undici";
|
|
10
|
+
import { WebSocket } from "ws";
|
|
11
|
+
import { LRUCache } from "lru-cache";
|
|
12
|
+
import { Config } from "./ase-config-core.js";
|
|
13
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
14
|
+
import { parseScope, userStateDir } from "./ase-config-scope.js";
|
|
15
|
+
import * as Delegate from "./ase-task-store-plugin-delegate.js";
|
|
16
|
+
import * as Core from "./ase-task-store-core.js";
|
|
17
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
18
|
+
/* the local client: the REST API functionality operating in-process
|
|
19
|
+
on a built-in storage plugin (the "ase" one in "solo" mode), with the project
|
|
20
|
+
registered under its configured lifecycle model and task id scheme on every open */
|
|
21
|
+
export class LocalTaskStoreClient {
|
|
22
|
+
prjId;
|
|
23
|
+
lifecycle;
|
|
24
|
+
idscheme;
|
|
25
|
+
storage;
|
|
26
|
+
log;
|
|
27
|
+
/* the storage delegates, shared by all concurrently open clients of the
|
|
28
|
+
same store (keyed by the store only, as the lifecycle model and task
|
|
29
|
+
id scheme are re-registered on every open and hence survive a switch without a
|
|
30
|
+
second delegate), so its per-project queue serializes in-process, too */
|
|
31
|
+
static shared = new Map();
|
|
32
|
+
entry;
|
|
33
|
+
core;
|
|
34
|
+
constructor(prjId, lifecycle, idscheme, storage, log) {
|
|
35
|
+
this.prjId = prjId;
|
|
36
|
+
this.lifecycle = lifecycle;
|
|
37
|
+
this.idscheme = idscheme;
|
|
38
|
+
this.storage = storage;
|
|
39
|
+
this.log = log;
|
|
40
|
+
}
|
|
41
|
+
/* persist a configuration key on scope "project" (which the local store always follows) */
|
|
42
|
+
configure(key, value) {
|
|
43
|
+
const cfg = new Config("config", configSchema, this.log, parseScope("project"));
|
|
44
|
+
cfg.lock(() => {
|
|
45
|
+
cfg.read();
|
|
46
|
+
cfg.set(key, value);
|
|
47
|
+
cfg.write();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/* switch the lifecycle model by persisting "project.task.lifecycle"
|
|
51
|
+
and re-registering the project (mapping the plan states) */
|
|
52
|
+
async setLifecycle(name) {
|
|
53
|
+
this.configure("project.task.lifecycle", name);
|
|
54
|
+
this.lifecycle = (await this.core.projectSet(this.prjId, { lifecycle: name })).project.lifecycle;
|
|
55
|
+
}
|
|
56
|
+
/* switch the task id scheme by persisting "project.task.idscheme" and re-registering the project */
|
|
57
|
+
async setIdScheme(spec) {
|
|
58
|
+
this.configure("project.task.idscheme", spec);
|
|
59
|
+
this.idscheme = (await this.core.projectSet(this.prjId, { idscheme: spec })).project.idscheme;
|
|
60
|
+
}
|
|
61
|
+
async missing(op, fallback) {
|
|
62
|
+
try {
|
|
63
|
+
return await op();
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof Core.Problem && err.status === 404)
|
|
67
|
+
return fallback;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async open() {
|
|
72
|
+
let entry = LocalTaskStoreClient.shared.get(this.storage.key);
|
|
73
|
+
if (entry === undefined) {
|
|
74
|
+
const listeners = new Set();
|
|
75
|
+
const opened = (async () => {
|
|
76
|
+
const plugin = await Delegate.loadTaskStoragePlugin(this.storage.plugin, {
|
|
77
|
+
options: this.storage.options,
|
|
78
|
+
log: (level, message) => this.log.write(level, `task: store: ${message}`)
|
|
79
|
+
});
|
|
80
|
+
const store = new Delegate.TaskStore(plugin);
|
|
81
|
+
const core = new Core.TaskStoreCore(store, (prjId, frame) => {
|
|
82
|
+
for (const listener of listeners)
|
|
83
|
+
listener(prjId, frame);
|
|
84
|
+
});
|
|
85
|
+
await store.open();
|
|
86
|
+
return { store, core };
|
|
87
|
+
})();
|
|
88
|
+
entry = { refs: 0, listeners, opened };
|
|
89
|
+
LocalTaskStoreClient.shared.set(this.storage.key, entry);
|
|
90
|
+
}
|
|
91
|
+
entry.refs++;
|
|
92
|
+
this.entry = entry;
|
|
93
|
+
try {
|
|
94
|
+
this.core = (await entry.opened).core;
|
|
95
|
+
await this.core.projectSet(this.prjId, { lifecycle: this.lifecycle.name, idscheme: this.idscheme });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
await this.close();
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async close() {
|
|
103
|
+
const entry = this.entry;
|
|
104
|
+
if (entry === undefined)
|
|
105
|
+
return;
|
|
106
|
+
this.entry = undefined;
|
|
107
|
+
if (--entry.refs === 0) {
|
|
108
|
+
if (LocalTaskStoreClient.shared.get(this.storage.key) === entry)
|
|
109
|
+
LocalTaskStoreClient.shared.delete(this.storage.key);
|
|
110
|
+
await entry.opened.then((opened) => opened.store.close(), () => { });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
list(fields) {
|
|
114
|
+
return this.core.taskList(this.prjId, "none", "none", fields ?? "none");
|
|
115
|
+
}
|
|
116
|
+
load(id) {
|
|
117
|
+
return this.missing(() => this.core.taskLoad(this.prjId, id), null);
|
|
118
|
+
}
|
|
119
|
+
async save(id, plan, tag, create = false) {
|
|
120
|
+
return (await this.core.taskSave(this.prjId, id, plan, tag, create)).tag;
|
|
121
|
+
}
|
|
122
|
+
newId(title, proposal, taken) {
|
|
123
|
+
return this.core.taskNewId(this.prjId, { title, proposal, taken });
|
|
124
|
+
}
|
|
125
|
+
patch(id, change) {
|
|
126
|
+
return this.missing(() => this.core.taskPatch(this.prjId, id, change), null);
|
|
127
|
+
}
|
|
128
|
+
delete(id) {
|
|
129
|
+
return this.missing(async () => {
|
|
130
|
+
await this.core.taskDelete(this.prjId, id);
|
|
131
|
+
return true;
|
|
132
|
+
}, false);
|
|
133
|
+
}
|
|
134
|
+
purge(age) {
|
|
135
|
+
return this.core.taskPurge(this.prjId, age);
|
|
136
|
+
}
|
|
137
|
+
content(id, index) {
|
|
138
|
+
return this.missing(() => this.core.attachmentContent(this.prjId, id, String(index)), null);
|
|
139
|
+
}
|
|
140
|
+
attach(id, attachment, append = false) {
|
|
141
|
+
return this.missing(async () => {
|
|
142
|
+
await this.core.attachmentAdd(this.prjId, id, attachment, append);
|
|
143
|
+
return true;
|
|
144
|
+
}, false);
|
|
145
|
+
}
|
|
146
|
+
/* subscribe to the change events of the project, i.e. the changes made
|
|
147
|
+
in-process plus the ones detected by the storage plugin itself (e.g.
|
|
148
|
+
by polling a remote storage), keeping the store opened meanwhile and
|
|
149
|
+
reporting it as connected once opened; returns a function to unsubscribe */
|
|
150
|
+
subscribe(onChange, onState) {
|
|
151
|
+
const listener = (prjId) => {
|
|
152
|
+
if (prjId === this.prjId)
|
|
153
|
+
onChange();
|
|
154
|
+
};
|
|
155
|
+
let stopped = false;
|
|
156
|
+
const start = async () => {
|
|
157
|
+
await this.open();
|
|
158
|
+
if (stopped)
|
|
159
|
+
await this.close();
|
|
160
|
+
else {
|
|
161
|
+
this.entry?.listeners.add(listener);
|
|
162
|
+
onState?.(true);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
start().catch((err) => {
|
|
166
|
+
this.log.write("warning", `task: store: subscription failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
});
|
|
168
|
+
return () => {
|
|
169
|
+
stopped = true;
|
|
170
|
+
this.entry?.listeners.delete(listener);
|
|
171
|
+
this.close().catch(() => { });
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/* the interval of the keepalive pings of the event subscription (where a connection without
|
|
176
|
+
any traffic for this interval is pinged and without any traffic for twice this interval
|
|
177
|
+
is terminated) and the timeout of its WebSocket handshake */
|
|
178
|
+
const KEEPALIVE_MS = 15 * 1000;
|
|
179
|
+
const HANDSHAKE_MS = 10 * 1000;
|
|
180
|
+
/* the remote client: the REST API of a task store server, with the
|
|
181
|
+
project registered under its configured lifecycle model and task id
|
|
182
|
+
scheme on first use only (afterwards adopting the lifecycle model and
|
|
183
|
+
task id scheme of the registered project) and every problem details
|
|
184
|
+
response raised as an error; an "insecure" client skips the TLS
|
|
185
|
+
certificate verification */
|
|
186
|
+
export class RemoteTaskStoreClient {
|
|
187
|
+
prjId;
|
|
188
|
+
configured;
|
|
189
|
+
configuredIdScheme;
|
|
190
|
+
log;
|
|
191
|
+
base;
|
|
192
|
+
token;
|
|
193
|
+
insecure;
|
|
194
|
+
/* the effective lifecycle models and task id schemes of the registered projects
|
|
195
|
+
(TTL-bounded, to spare consecutive operations the registration round-trips) */
|
|
196
|
+
static registered = new LRUCache({ max: 16, ttl: 10 * 1000 });
|
|
197
|
+
dispatcher;
|
|
198
|
+
lifecycle;
|
|
199
|
+
idscheme;
|
|
200
|
+
constructor(prjId, configured, configuredIdScheme, log, base, token, insecure) {
|
|
201
|
+
this.prjId = prjId;
|
|
202
|
+
this.configured = configured;
|
|
203
|
+
this.configuredIdScheme = configuredIdScheme;
|
|
204
|
+
this.log = log;
|
|
205
|
+
this.base = base;
|
|
206
|
+
this.token = token;
|
|
207
|
+
this.insecure = insecure;
|
|
208
|
+
this.lifecycle = configured;
|
|
209
|
+
this.idscheme = configuredIdScheme;
|
|
210
|
+
this.dispatcher = insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined;
|
|
211
|
+
}
|
|
212
|
+
/* raise a failed connection as an unreachable store error,
|
|
213
|
+
reporting the innermost cause (like "connect ECONNREFUSED") */
|
|
214
|
+
unreachable(err) {
|
|
215
|
+
let cause = err;
|
|
216
|
+
while (cause instanceof Error && cause.cause instanceof Error)
|
|
217
|
+
cause = cause.cause;
|
|
218
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
219
|
+
throw new Error(`task: store "${this.base}" unreachable: ${reason}`, { cause: err });
|
|
220
|
+
}
|
|
221
|
+
/* perform a request: a 404 response or a tolerated error response yields
|
|
222
|
+
a null result, any other error response is raised as a problem carrying
|
|
223
|
+
its status, and a failed connection is raised as an unreachable store error */
|
|
224
|
+
async request(method, url, body, headers = {}, tolerated = []) {
|
|
225
|
+
const r = await ofetch.raw(`${this.base}${url}`, {
|
|
226
|
+
method,
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: `Bearer ${this.token}`,
|
|
229
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
230
|
+
...headers
|
|
231
|
+
},
|
|
232
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
233
|
+
dispatcher: this.dispatcher,
|
|
234
|
+
signal: AbortSignal.timeout(10000),
|
|
235
|
+
ignoreResponseError: true
|
|
236
|
+
}).catch((err) => this.unreachable(err));
|
|
237
|
+
if (r.status === 404 || tolerated.includes(r.status))
|
|
238
|
+
return null;
|
|
239
|
+
if (r.status < 200 || r.status >= 300) {
|
|
240
|
+
const d = r._data;
|
|
241
|
+
throw new Core.Problem(r.status, `store "${this.base}": ${d?.detail ?? d?.title ?? `HTTP ${r.status}`}`);
|
|
242
|
+
}
|
|
243
|
+
return (r._data ?? {});
|
|
244
|
+
}
|
|
245
|
+
get tasks() {
|
|
246
|
+
return `/projects/${this.prjId}/tasks`;
|
|
247
|
+
}
|
|
248
|
+
get key() {
|
|
249
|
+
return `${this.base}/${this.prjId}`;
|
|
250
|
+
}
|
|
251
|
+
/* adopt the lifecycle model and task id scheme of the registered project
|
|
252
|
+
(the configured task id scheme for a server predating task id schemes) */
|
|
253
|
+
adopt(project) {
|
|
254
|
+
if (project === null)
|
|
255
|
+
throw new Core.Problem(404, `store "${this.base}": project "${this.prjId}" not registered`);
|
|
256
|
+
const lifecycle = TaskFormat.taskLifecycles[project.lifecycle.name];
|
|
257
|
+
if (lifecycle === undefined)
|
|
258
|
+
throw new Error(`task: store "${this.base}" uses unknown lifecycle model "${project.lifecycle.name}"`);
|
|
259
|
+
const idscheme = project.idscheme ?? this.configuredIdScheme;
|
|
260
|
+
if (TaskFormat.checkIdScheme(idscheme) !== "")
|
|
261
|
+
throw new Error(`task: store "${this.base}" uses invalid task id scheme "${idscheme}"`);
|
|
262
|
+
this.lifecycle = lifecycle;
|
|
263
|
+
this.idscheme = idscheme;
|
|
264
|
+
RemoteTaskStoreClient.registered.set(this.key, { lifecycle, idscheme });
|
|
265
|
+
}
|
|
266
|
+
/* raise a not registered project (dropping its cached registration) */
|
|
267
|
+
unregistered() {
|
|
268
|
+
RemoteTaskStoreClient.registered.delete(this.key);
|
|
269
|
+
throw new Core.Problem(404, `store "${this.base}": project "${this.prjId}" not registered`);
|
|
270
|
+
}
|
|
271
|
+
/* pass through a task endpoint result, disambiguating a 404 response
|
|
272
|
+
(null) into a missing task or a not registered project */
|
|
273
|
+
async task(result) {
|
|
274
|
+
if (result === null && await this.request("GET", `/projects/${this.prjId}`) === null)
|
|
275
|
+
this.unregistered();
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
async open() {
|
|
279
|
+
const cached = RemoteTaskStoreClient.registered.get(this.key);
|
|
280
|
+
if (cached !== undefined) {
|
|
281
|
+
this.lifecycle = cached.lifecycle;
|
|
282
|
+
this.idscheme = cached.idscheme;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
/* register the project only if not yet registered (via "If-None-Match: *"), as the
|
|
286
|
+
lifecycle model and task id scheme of a registered project are shared by all its clients */
|
|
287
|
+
const created = await this.request("PUT", `/projects/${this.prjId}`, { lifecycle: this.configured.name, idscheme: this.configuredIdScheme }, { "If-None-Match": "*" }, [412]);
|
|
288
|
+
this.adopt(created ?? await this.request("GET", `/projects/${this.prjId}`));
|
|
289
|
+
this.mismatch();
|
|
290
|
+
}
|
|
291
|
+
async setLifecycle(name) {
|
|
292
|
+
this.adopt(await this.request("PUT", `/projects/${this.prjId}`, { lifecycle: name }));
|
|
293
|
+
this.mismatch();
|
|
294
|
+
}
|
|
295
|
+
async setIdScheme(spec) {
|
|
296
|
+
this.adopt(await this.request("PUT", `/projects/${this.prjId}`, { idscheme: spec }));
|
|
297
|
+
this.mismatch();
|
|
298
|
+
}
|
|
299
|
+
/* warn about a configured lifecycle model or task id scheme deviating from the
|
|
300
|
+
one of the registered project, once per deviation only (persisted across processes) */
|
|
301
|
+
mismatch() {
|
|
302
|
+
const file = path.join(userStateDir(), "task-lifecycle.json");
|
|
303
|
+
let seen = {};
|
|
304
|
+
try {
|
|
305
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
306
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data))
|
|
307
|
+
seen = data;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
/* no (valid) state yet */
|
|
311
|
+
}
|
|
312
|
+
const checks = [{
|
|
313
|
+
key: this.key,
|
|
314
|
+
config: "project.task.lifecycle",
|
|
315
|
+
cmd: "lifecycle",
|
|
316
|
+
configured: this.configured.name,
|
|
317
|
+
effective: this.lifecycle.name
|
|
318
|
+
}, {
|
|
319
|
+
key: `${this.key}#idscheme`,
|
|
320
|
+
config: "project.task.idscheme",
|
|
321
|
+
cmd: "idscheme",
|
|
322
|
+
configured: this.configuredIdScheme,
|
|
323
|
+
effective: this.idscheme
|
|
324
|
+
}];
|
|
325
|
+
let changed = false;
|
|
326
|
+
for (const check of checks) {
|
|
327
|
+
const pair = check.effective !== check.configured ?
|
|
328
|
+
`${check.configured}:${check.effective}` : undefined;
|
|
329
|
+
if (seen[check.key] === pair)
|
|
330
|
+
continue;
|
|
331
|
+
changed = true;
|
|
332
|
+
if (pair === undefined)
|
|
333
|
+
seen = Object.fromEntries(Object.entries(seen).filter(([key]) => key !== check.key));
|
|
334
|
+
else {
|
|
335
|
+
seen[check.key] = pair;
|
|
336
|
+
this.log.write("warning", `task: configured "${check.config}" "${check.configured}" ignored, ` +
|
|
337
|
+
`as store "${this.base}" uses "${check.effective}" for project "${this.prjId}" ` +
|
|
338
|
+
`(align the configuration via "ase config --scope project set ${check.config} ${check.effective}", ` +
|
|
339
|
+
`or switch the store via "ase task ${check.cmd} ${check.configured}"; reported once only)`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!changed)
|
|
343
|
+
return;
|
|
344
|
+
try {
|
|
345
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
346
|
+
fs.writeFileSync(file, JSON.stringify(seen, null, 4) + "\n", "utf8");
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
/* best-effort only (the warning then repeats) */
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async close() {
|
|
353
|
+
await this.dispatcher?.close();
|
|
354
|
+
}
|
|
355
|
+
async list(fields) {
|
|
356
|
+
const url = this.tasks + (fields !== undefined ? `?fields=${fields}` : "");
|
|
357
|
+
return (await this.request("GET", url) ?? this.unregistered()).tasks;
|
|
358
|
+
}
|
|
359
|
+
async load(id) {
|
|
360
|
+
return this.task(await this.request("GET", `${this.tasks}/${encodeURIComponent(id)}`));
|
|
361
|
+
}
|
|
362
|
+
async save(id, plan, tag, create = false) {
|
|
363
|
+
const result = await this.request("PUT", `${this.tasks}/${encodeURIComponent(id)}`, plan, {
|
|
364
|
+
...(tag !== undefined ? { "If-Match": `"${tag}"` } : {}),
|
|
365
|
+
...(create ? { "If-None-Match": "*" } : {})
|
|
366
|
+
});
|
|
367
|
+
if (result === null)
|
|
368
|
+
this.unregistered();
|
|
369
|
+
return result.tag;
|
|
370
|
+
}
|
|
371
|
+
/* allocate the next free task id on the server, falling back to determining
|
|
372
|
+
it locally (without any reservation) on a server predating the allocation */
|
|
373
|
+
async newId(title, proposal, taken) {
|
|
374
|
+
const result = await this.request("POST", `/projects/${this.prjId}/newid`, { title, proposal, taken });
|
|
375
|
+
if (result !== null)
|
|
376
|
+
return result;
|
|
377
|
+
const project = await this.request("GET", `/projects/${this.prjId}`) ?? this.unregistered();
|
|
378
|
+
const spec = project.idscheme ?? this.idscheme;
|
|
379
|
+
const ids = [...(await this.list()).map((item) => item.id), ...taken];
|
|
380
|
+
try {
|
|
381
|
+
return { scheme: spec, id: TaskFormat.nextTaskId(TaskFormat.parseIdScheme(spec), ids, title, proposal, project.seqmark ?? 0) };
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
throw new Core.Problem(422, err instanceof Error ? err.message : String(err));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async patch(id, change) {
|
|
388
|
+
return this.task(await this.request("PATCH", `${this.tasks}/${encodeURIComponent(id)}`, change));
|
|
389
|
+
}
|
|
390
|
+
async delete(id) {
|
|
391
|
+
return await this.task(await this.request("DELETE", `${this.tasks}/${encodeURIComponent(id)}`)) !== null;
|
|
392
|
+
}
|
|
393
|
+
async purge(age) {
|
|
394
|
+
const result = await this.request("DELETE", `${this.tasks}?age=${encodeURIComponent(age)}`);
|
|
395
|
+
return (result ?? this.unregistered()).purged;
|
|
396
|
+
}
|
|
397
|
+
async attach(id, attachment, append = false) {
|
|
398
|
+
return await this.task(await this.request("POST", `${this.tasks}/${encodeURIComponent(id)}/attachment${append ? "?append=true" : ""}`, attachment)) !== null;
|
|
399
|
+
}
|
|
400
|
+
async content(id, index) {
|
|
401
|
+
const r = await ofetch.raw(`${this.base}${this.tasks}/${encodeURIComponent(id)}/attachment/${index}/content`, {
|
|
402
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
403
|
+
responseType: "arrayBuffer",
|
|
404
|
+
dispatcher: this.dispatcher,
|
|
405
|
+
signal: AbortSignal.timeout(10000),
|
|
406
|
+
ignoreResponseError: true
|
|
407
|
+
}).catch((err) => this.unreachable(err));
|
|
408
|
+
if (r.status === 404)
|
|
409
|
+
return this.task(null);
|
|
410
|
+
if (r.status < 200 || r.status >= 300)
|
|
411
|
+
throw new Core.Problem(r.status, `store "${this.base}": HTTP ${r.status}`);
|
|
412
|
+
return { type: r.headers.get("content-type") ?? "application/octet-stream", content: Buffer.from(r._data ?? new ArrayBuffer(0)) };
|
|
413
|
+
}
|
|
414
|
+
/* subscribe to the change events of the project via the WebSocket event
|
|
415
|
+
endpoint, reconnecting after a connection loss, a failed or timed out
|
|
416
|
+
handshake, or a connection without any traffic despite keepalive pings
|
|
417
|
+
(e.g. a half-open one after a network partition), with a change notification
|
|
418
|
+
on reconnect (as events may have been missed, incl. lifecycle model changes),
|
|
419
|
+
reporting each change of the connection state (initially disconnected);
|
|
420
|
+
returns a function to unsubscribe */
|
|
421
|
+
subscribe(onChange, onState) {
|
|
422
|
+
const url = `${this.base.replace(/^http/, "ws")}/projects/${this.prjId}/events`;
|
|
423
|
+
let ws = null;
|
|
424
|
+
let timer = null;
|
|
425
|
+
let stopped = false;
|
|
426
|
+
let failed = false;
|
|
427
|
+
const connect = () => {
|
|
428
|
+
const sock = new WebSocket(url, {
|
|
429
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
430
|
+
rejectUnauthorized: !this.insecure,
|
|
431
|
+
handshakeTimeout: HANDSHAKE_MS
|
|
432
|
+
});
|
|
433
|
+
ws = sock;
|
|
434
|
+
/* keep the connection alive: record any traffic (the frames, and the pings
|
|
435
|
+
of the task store and the pongs of our own pings), and terminate the
|
|
436
|
+
connection if there was none since the previous (then unanswered) ping */
|
|
437
|
+
let traffic = true;
|
|
438
|
+
let watchdog = null;
|
|
439
|
+
const alive = () => { traffic = true; };
|
|
440
|
+
sock.on("ping", alive);
|
|
441
|
+
sock.on("pong", alive);
|
|
442
|
+
sock.on("open", () => {
|
|
443
|
+
watchdog = setInterval(() => {
|
|
444
|
+
if (!traffic) {
|
|
445
|
+
this.log.write("debug", `task: store "${this.base}": events: connection unresponsive -- reconnecting`);
|
|
446
|
+
sock.terminate();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
traffic = false;
|
|
450
|
+
sock.ping();
|
|
451
|
+
}, KEEPALIVE_MS);
|
|
452
|
+
if (failed) {
|
|
453
|
+
RemoteTaskStoreClient.registered.delete(this.key);
|
|
454
|
+
onChange();
|
|
455
|
+
}
|
|
456
|
+
failed = false;
|
|
457
|
+
onState?.(true);
|
|
458
|
+
});
|
|
459
|
+
sock.on("message", (data) => {
|
|
460
|
+
traffic = true;
|
|
461
|
+
/* drop the cached registration on a lifecycle model or task id scheme change */
|
|
462
|
+
try {
|
|
463
|
+
const frame = JSON.parse(String(data));
|
|
464
|
+
if (frame.lifecycle !== undefined || frame.idscheme !== undefined)
|
|
465
|
+
RemoteTaskStoreClient.registered.delete(this.key);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
/* ignore malformed frames (the change is notified anyway) */
|
|
469
|
+
}
|
|
470
|
+
onChange();
|
|
471
|
+
});
|
|
472
|
+
sock.on("error", (err) => {
|
|
473
|
+
this.log.write("debug", `task: store "${this.base}": events: ${err.message}`);
|
|
474
|
+
});
|
|
475
|
+
sock.on("close", () => {
|
|
476
|
+
if (watchdog !== null)
|
|
477
|
+
clearInterval(watchdog);
|
|
478
|
+
ws = null;
|
|
479
|
+
failed = true;
|
|
480
|
+
if (!stopped) {
|
|
481
|
+
onState?.(false);
|
|
482
|
+
timer = setTimeout(connect, 2000);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
connect();
|
|
487
|
+
return () => {
|
|
488
|
+
stopped = true;
|
|
489
|
+
if (timer !== null)
|
|
490
|
+
clearTimeout(timer);
|
|
491
|
+
ws?.close();
|
|
492
|
+
this.close().catch(() => { });
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|