@rse/ase 2.0.1 → 2.0.3
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-core.js +111 -87
- package/dst/ase-agent-fleet.js +1 -1
- package/dst/ase-agent-runtime-claim.js +5 -3
- package/dst/ase-agent-runtime-engine.js +1 -1
- package/dst/ase-agent-runtime-phase.js +1 -1
- package/dst/ase-agent-runtime-request.js +1 -1
- package/dst/ase-agent-runtime.js +3 -5
- package/dst/ase-artifact.js +3 -3
- package/dst/ase-config-core.js +2 -16
- package/dst/ase-config-schema.js +1 -3
- package/dst/ase-config.js +4 -1
- package/dst/ase-hook.js +14 -19
- package/dst/ase-lib-daemon-cli.js +117 -4
- package/dst/ase-lib-server-cli.js +10 -10
- package/dst/ase-mcp.js +4 -4
- package/dst/ase-service-getopt.js +2 -2
- package/dst/ase-service-otel.js +4 -4
- package/dst/ase-service-skills.js +5 -15
- package/dst/ase-service.js +87 -210
- package/dst/ase-setup-common.js +1 -2
- package/dst/ase-setup-settings.js +6 -9
- package/dst/ase-setup.js +2 -3
- package/dst/ase-statusline.js +170 -170
- package/dst/ase-task-board-core.js +1 -1
- package/dst/ase-task-board-flow.js +7 -6
- package/dst/ase-task-board-graph.js +5 -4
- package/dst/ase-task-board-tui-control.js +51 -45
- package/dst/ase-task-board-tui-model.js +195 -181
- package/dst/ase-task-board-tui-popup.js +2 -3
- package/dst/ase-task-board-tui-view.js +88 -79
- package/dst/ase-task-board-web-client.css +1 -1
- package/dst/ase-task-board-web-client.js +2 -2
- package/dst/ase-task-board-web-server.js +4 -27
- package/dst/ase-task-board.js +1 -1
- package/dst/ase-task-format.js +1 -1
- package/dst/ase-task-store-client.js +17 -5
- package/dst/ase-task-store-core.js +5 -7
- package/dst/ase-task-store-issues.js +91 -0
- package/dst/ase-task-store-plugin-ase.js +7 -3
- package/dst/ase-task-store-plugin-gitea.js +11 -97
- package/dst/ase-task-store-plugin-github.js +5 -77
- package/dst/ase-task-store-plugin-gitlab.js +4 -83
- package/dst/ase-task.js +51 -23
- package/dst/ase-util-compat.js +1 -1
- package/dst/ase-util-diagram.js +16 -42
- package/dst/ase.js +5 -5
- package/dst/man/ase-agent-adhoc.1 +1 -1
- package/dst/man/ase-agent-condenser.1 +1 -1
- package/dst/man/ase-agent-editor.1 +1 -1
- package/dst/man/ase-agent-estimator.1 +1 -1
- package/dst/man/ase-agent-fleet.1 +1 -1
- package/dst/man/ase-agent-implementer.1 +1 -1
- package/dst/man/ase-agent-integrator.1 +1 -1
- package/dst/man/ase-agent-qualifier.1 +1 -1
- package/dst/man/ase-agent-rebooter.1 +1 -1
- package/dst/man/ase-agent-relay.1 +1 -1
- package/dst/man/ase-agent-researcher.1 +1 -1
- package/dst/man/ase-agent.1 +1 -1
- package/dst/man/ase-artifact-list.1 +1 -1
- package/dst/man/ase-artifact-name.1 +1 -1
- package/dst/man/ase-artifact.1 +1 -1
- package/dst/man/ase-config-delete.1 +1 -1
- package/dst/man/ase-config-edit.1 +1 -1
- package/dst/man/ase-config-get.1 +1 -1
- package/dst/man/ase-config-init.1 +1 -1
- package/dst/man/ase-config-list.1 +1 -1
- package/dst/man/ase-config-set.1 +1 -1
- package/dst/man/ase-config.1 +1 -1
- package/dst/man/ase-hook-permission-request.1 +1 -1
- package/dst/man/ase-hook-pre-tool-use.1 +1 -1
- package/dst/man/ase-hook-session-end.1 +1 -1
- package/dst/man/ase-hook-session-persona.1 +1 -1
- package/dst/man/ase-hook-session-start.1 +1 -1
- package/dst/man/ase-hook-stop.1 +1 -1
- package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
- package/dst/man/ase-hook.1 +1 -1
- package/dst/man/ase-man.1 +1 -1
- package/dst/man/ase-mcp.1 +1 -1
- package/dst/man/ase-service-log.1 +1 -1
- package/dst/man/ase-service-send.1 +1 -1
- package/dst/man/ase-service-start.1 +1 -1
- package/dst/man/ase-service-status.1 +1 -1
- package/dst/man/ase-service-stop.1 +1 -1
- package/dst/man/ase-service.1 +1 -1
- package/dst/man/ase-setup-disable.1 +1 -1
- package/dst/man/ase-setup-enable.1 +1 -1
- package/dst/man/ase-setup-install.1 +1 -1
- package/dst/man/ase-setup-mcp.1 +1 -1
- package/dst/man/ase-setup-status.1 +1 -1
- package/dst/man/ase-setup-statusline.1 +1 -1
- package/dst/man/ase-setup-telemetry.1 +1 -1
- package/dst/man/ase-setup-uninstall.1 +1 -1
- package/dst/man/ase-setup-update.1 +1 -1
- package/dst/man/ase-setup.1 +1 -1
- package/dst/man/ase-spec-export.1 +1 -1
- package/dst/man/ase-spec-lint.1 +1 -1
- package/dst/man/ase-spec-preview.1 +1 -1
- package/dst/man/ase-spec.1 +1 -1
- package/dst/man/ase-statusline.1 +1 -1
- package/dst/man/ase-task-assign.1 +1 -1
- package/dst/man/ase-task-board.1 +3 -3
- package/dst/man/ase-task-delete.1 +1 -1
- package/dst/man/ase-task-edit.1 +1 -1
- package/dst/man/ase-task-idscheme.1 +1 -1
- package/dst/man/ase-task-lifecycle.1 +1 -1
- package/dst/man/ase-task-list.1 +1 -1
- package/dst/man/ase-task-load.1 +1 -1
- package/dst/man/ase-task-newid.1 +1 -1
- package/dst/man/ase-task-purge.1 +1 -1
- package/dst/man/ase-task-rename.1 +1 -1
- package/dst/man/ase-task-request.1 +1 -1
- package/dst/man/ase-task-save.1 +1 -1
- package/dst/man/ase-task-status.1 +1 -1
- package/dst/man/ase-task-store.1 +1 -1
- package/dst/man/ase-task-view.1 +1 -1
- package/dst/man/ase-task.1 +1 -1
- package/dst/man/ase-util-compat.1 +1 -1
- package/dst/man/ase-util-diagram.1 +1 -1
- package/dst/man/ase-util-meta.1 +1 -1
- package/dst/man/ase-util-metric.1 +1 -1
- package/dst/man/ase-util-mint.1 +1 -1
- package/dst/man/ase-util-worktree.1 +1 -1
- package/dst/man/ase-util.1 +1 -1
- package/dst/man/ase.1 +1 -1
- package/package.json +1 -1
- 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/meta/ase-common-resolve.md +2 -2
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/mods/ase-mods.ts +16 -12
- package/plugin/skills/ase-arch-analyze/SKILL.md +3 -1
- package/plugin/skills/ase-code-analyze/SKILL.md +26 -10
- package/plugin/skills/ase-code-analyze/help.md +11 -6
- package/plugin/skills/ase-code-edit/SKILL.md +5 -4
- package/plugin/skills/ase-code-edit/help.md +1 -1
- package/plugin/skills/ase-code-lint/SKILL.md +52 -27
- package/plugin/skills/ase-code-lint/help.md +10 -5
- package/plugin/skills/ase-help-intent/data.md +60 -30
- package/plugin/skills/ase-help-skill/help.md +3 -4
- package/plugin/skills/ase-meta-quorum/SKILL.md +9 -0
- package/plugin/skills/ase-meta-quorum/help.md +5 -3
- package/plugin/skills/ase-meta-workflow/sample.md +5 -3
- package/plugin/skills/ase-repo-changelog/SKILL.md +8 -9
- package/plugin/skills/ase-repo-changelog/help.md +2 -2
- package/plugin/skills/ase-repo-review/SKILL.md +5 -3
- package/plugin/skills/ase-repo-review/help.md +3 -1
- package/plugin/skills/ase-spec-edit/SKILL.md +2 -1
- package/plugin/skills/ase-spec-edit/help.md +2 -1
- package/plugin/skills/ase-sync-reconcile/help.md +4 -4
- package/plugin/skills/ase-task-dissect/SKILL.md +10 -6
- package/plugin/skills/ase-task-grill/help.md +1 -1
- package/plugin/skills/ase-task-implement/SKILL.md +58 -1
- package/plugin/skills/ase-task-implement/help.md +16 -0
- package/plugin/skills/ase-task-list/help.md +2 -2
|
@@ -18,7 +18,7 @@ import { flowOf, layoutFlow, drawFlowSVG } from "./ase-task-board-flow.js";
|
|
|
18
18
|
import { filterBoard, dropStandalone } from "./ase-task-board-filter.js";
|
|
19
19
|
import { statsOf, statsRanges, effortScale } from "./ase-task-board-stats.js";
|
|
20
20
|
import * as TaskFormat from "./ase-task-format.js";
|
|
21
|
-
import { editProbe, editRefused,
|
|
21
|
+
import { editProbe, editRefused, requestsOf, RequestError } from "./ase-task-request.js";
|
|
22
22
|
import pkg from "../package.json" with { type: "json" };
|
|
23
23
|
/* the build stamp of the loaded code (version and content hash of the board modules
|
|
24
24
|
and page assets), reported by the ping route to let the CLI detect a stale running service
|
|
@@ -26,7 +26,7 @@ import pkg from "../package.json" with { type: "json" };
|
|
|
26
26
|
export const BOARD_BUILD = (() => {
|
|
27
27
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const hash = crypto.createHash("sha1");
|
|
29
|
-
for (const f of ["core.js", "filter.js", "graph.js", "stats.js", "web-server.js", "web-client.html", "web-client.css", "web-client.js"])
|
|
29
|
+
for (const f of ["core.js", "filter.js", "flow.js", "graph.js", "stats.js", "web-server.js", "web-client.html", "web-client.css", "web-client.js"])
|
|
30
30
|
hash.update(fs.readFileSync(path.join(dir, `ase-task-board-${f}`)));
|
|
31
31
|
return `${pkg.version}:${hash.digest("hex")}`;
|
|
32
32
|
})();
|
|
@@ -192,7 +192,7 @@ const attachmentDocument = async (log, id, n) => {
|
|
|
192
192
|
try {
|
|
193
193
|
text = new TextDecoder("utf-8", { fatal: true }).decode(a.content);
|
|
194
194
|
}
|
|
195
|
-
catch
|
|
195
|
+
catch {
|
|
196
196
|
text = "\0";
|
|
197
197
|
}
|
|
198
198
|
if (text.includes("\0"))
|
|
@@ -364,7 +364,7 @@ const registerPageRoutes = (server, log) => {
|
|
|
364
364
|
};
|
|
365
365
|
/* the filter query of a request (its optional "filter" query parameter) */
|
|
366
366
|
const filterQuery = (request) => typeof request.query.filter === "string" ? request.query.filter : "";
|
|
367
|
-
/* register the view routes of the web board: the board, the
|
|
367
|
+
/* register the view routes of the web board: the board, the task and agent graphs, the statistics, the new task text, and the tasks with their attachments */
|
|
368
368
|
const registerViewRoutes = (server, log) => {
|
|
369
369
|
/* the board model, reduced onto the tasks matching the filter query */
|
|
370
370
|
server.route({
|
|
@@ -637,29 +637,6 @@ const registerUpdateRoutes = (server, log) => {
|
|
|
637
637
|
return h.response({ prompt: probe.prompt });
|
|
638
638
|
})
|
|
639
639
|
});
|
|
640
|
-
/* request the edit of a task by a prompt (processed by "ase agent editor"), answering
|
|
641
|
-
400 if the prompt is empty, 404 if the task is missing, and 409 if it is finished or
|
|
642
|
-
an edit of it is in progress (as checked atomically with the request) */
|
|
643
|
-
server.route({
|
|
644
|
-
method: "POST",
|
|
645
|
-
path: "/task-board/api/task/{id}/edit",
|
|
646
|
-
options: { payload: { parse: true, allow: "application/json" } },
|
|
647
|
-
handler: guarded(async (request, h) => {
|
|
648
|
-
const id = String(request.params.id);
|
|
649
|
-
const p = request.payload;
|
|
650
|
-
if (!TaskFormat.TASK_ID_RE.test(id) || p === null || typeof p.prompt !== "string")
|
|
651
|
-
return h.response({ error: "invalid edit request" }).code(400);
|
|
652
|
-
try {
|
|
653
|
-
await requestEdit(log, id, p.prompt);
|
|
654
|
-
}
|
|
655
|
-
catch (err) {
|
|
656
|
-
const code = !(err instanceof RequestError) || err.reason === "empty" ? 400 :
|
|
657
|
-
err.reason === "missing" ? 404 : 409;
|
|
658
|
-
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(code);
|
|
659
|
-
}
|
|
660
|
-
return h.response({ ok: true });
|
|
661
|
-
})
|
|
662
|
-
});
|
|
663
640
|
/* request a task by a prompt under a request name (processed by a request agent of
|
|
664
641
|
a definition, resp. by "ase agent editor" for "edit"), answering 400 if the
|
|
665
642
|
request name is invalid or the prompt of an "edit" request is empty (as the prompts
|
package/dst/ase-task-board.js
CHANGED
|
@@ -78,7 +78,7 @@ export default class TaskBoardCommand {
|
|
|
78
78
|
"(restart it with \"ase service stop\" once no agent session uses it)");
|
|
79
79
|
return port;
|
|
80
80
|
}
|
|
81
|
-
/* open
|
|
81
|
+
/* open a URL in the default browser of the platform, waiting only for
|
|
82
82
|
the opener to spawn, as e.g. "xdg-open" can stay attached to the browser */
|
|
83
83
|
async openBrowser(url) {
|
|
84
84
|
const [cmd, ...args] = process.platform === "darwin" ? ["open", url] :
|
package/dst/ase-task-format.js
CHANGED
|
@@ -745,7 +745,7 @@ export const bindEffort = (plan) => {
|
|
|
745
745
|
if (effort !== null && effort.hash === "")
|
|
746
746
|
plan.header.Effort = `${effort.size} ${hash}`;
|
|
747
747
|
else if (effort === null || effort.hash !== hash)
|
|
748
|
-
|
|
748
|
+
delete plan.header.Effort;
|
|
749
749
|
return plan;
|
|
750
750
|
};
|
|
751
751
|
/* derive the title of a task plan from its body as defined by the
|
|
@@ -58,6 +58,7 @@ export class LocalTaskStoreClient {
|
|
|
58
58
|
this.configure("project.task.idscheme", spec);
|
|
59
59
|
this.idscheme = (await this.core.projectSet(this.prjId, { idscheme: spec })).project.idscheme;
|
|
60
60
|
}
|
|
61
|
+
/* run an operation, yielding a fallback for a missing (404) task plan */
|
|
61
62
|
async missing(op, fallback) {
|
|
62
63
|
try {
|
|
63
64
|
return await op();
|
|
@@ -68,6 +69,7 @@ export class LocalTaskStoreClient {
|
|
|
68
69
|
throw err;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
/* open the storage delegate shared by all clients of the store and register the project */
|
|
71
73
|
async open() {
|
|
72
74
|
let entry = LocalTaskStoreClient.shared.get(this.storage.key);
|
|
73
75
|
if (entry === undefined) {
|
|
@@ -99,6 +101,7 @@ export class LocalTaskStoreClient {
|
|
|
99
101
|
throw err;
|
|
100
102
|
}
|
|
101
103
|
}
|
|
104
|
+
/* release the shared storage delegate, closing it with its last client */
|
|
102
105
|
async close() {
|
|
103
106
|
const entry = this.entry;
|
|
104
107
|
if (entry === undefined)
|
|
@@ -107,7 +110,9 @@ export class LocalTaskStoreClient {
|
|
|
107
110
|
if (--entry.refs === 0) {
|
|
108
111
|
if (LocalTaskStoreClient.shared.get(this.storage.key) === entry)
|
|
109
112
|
LocalTaskStoreClient.shared.delete(this.storage.key);
|
|
110
|
-
await entry.opened.then((opened) => opened.store.close(), () => {
|
|
113
|
+
await entry.opened.then((opened) => opened.store.close(), () => {
|
|
114
|
+
/* a failed opening was already raised to its opener */
|
|
115
|
+
});
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
list(fields) {
|
|
@@ -153,11 +158,13 @@ export class LocalTaskStoreClient {
|
|
|
153
158
|
onChange();
|
|
154
159
|
};
|
|
155
160
|
let stopped = false;
|
|
161
|
+
let started = false;
|
|
156
162
|
const start = async () => {
|
|
157
163
|
await this.open();
|
|
158
164
|
if (stopped)
|
|
159
165
|
await this.close();
|
|
160
166
|
else {
|
|
167
|
+
started = true;
|
|
161
168
|
this.entry?.listeners.add(listener);
|
|
162
169
|
onState?.(true);
|
|
163
170
|
}
|
|
@@ -167,6 +174,8 @@ export class LocalTaskStoreClient {
|
|
|
167
174
|
});
|
|
168
175
|
return () => {
|
|
169
176
|
stopped = true;
|
|
177
|
+
if (!started)
|
|
178
|
+
return;
|
|
170
179
|
this.entry?.listeners.delete(listener);
|
|
171
180
|
this.close().catch(() => { });
|
|
172
181
|
};
|
|
@@ -174,9 +183,10 @@ export class LocalTaskStoreClient {
|
|
|
174
183
|
}
|
|
175
184
|
/* the interval of the keepalive pings of the event subscription (where a connection without
|
|
176
185
|
any traffic for this interval is pinged and without any traffic for twice this interval
|
|
177
|
-
is terminated)
|
|
186
|
+
is terminated), the timeout of its WebSocket handshake, and the timeout of the REST API requests */
|
|
178
187
|
const KEEPALIVE_MS = 15 * 1000;
|
|
179
188
|
const HANDSHAKE_MS = 10 * 1000;
|
|
189
|
+
const REQUEST_MS = 10 * 1000;
|
|
180
190
|
/* the remote client: the REST API of a task store server, with the
|
|
181
191
|
project registered under its configured lifecycle model and task id
|
|
182
192
|
scheme on first use only (afterwards adopting the lifecycle model and
|
|
@@ -231,7 +241,7 @@ export class RemoteTaskStoreClient {
|
|
|
231
241
|
},
|
|
232
242
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
233
243
|
dispatcher: this.dispatcher,
|
|
234
|
-
signal: AbortSignal.timeout(
|
|
244
|
+
signal: AbortSignal.timeout(REQUEST_MS),
|
|
235
245
|
ignoreResponseError: true
|
|
236
246
|
}).catch((err) => this.unreachable(err));
|
|
237
247
|
if (r.status === 404 || tolerated.includes(r.status))
|
|
@@ -402,7 +412,7 @@ export class RemoteTaskStoreClient {
|
|
|
402
412
|
headers: { Authorization: `Bearer ${this.token}` },
|
|
403
413
|
responseType: "arrayBuffer",
|
|
404
414
|
dispatcher: this.dispatcher,
|
|
405
|
-
signal: AbortSignal.timeout(
|
|
415
|
+
signal: AbortSignal.timeout(REQUEST_MS),
|
|
406
416
|
ignoreResponseError: true
|
|
407
417
|
}).catch((err) => this.unreachable(err));
|
|
408
418
|
if (r.status === 404)
|
|
@@ -489,7 +499,9 @@ export class RemoteTaskStoreClient {
|
|
|
489
499
|
if (timer !== null)
|
|
490
500
|
clearTimeout(timer);
|
|
491
501
|
ws?.close();
|
|
492
|
-
this.close().catch(() => {
|
|
502
|
+
this.close().catch(() => {
|
|
503
|
+
/* best-effort release of the dispatcher on unsubscribe */
|
|
504
|
+
});
|
|
493
505
|
};
|
|
494
506
|
}
|
|
495
507
|
}
|
|
@@ -143,7 +143,7 @@ export class TaskStoreCore {
|
|
|
143
143
|
if (effort !== undefined)
|
|
144
144
|
header.Effort = effort;
|
|
145
145
|
else
|
|
146
|
-
|
|
146
|
+
delete header.Effort;
|
|
147
147
|
return header;
|
|
148
148
|
}
|
|
149
149
|
/* load an existing task plan of a registered project */
|
|
@@ -408,7 +408,7 @@ export class TaskStoreCore {
|
|
|
408
408
|
if (lifecycle.states.includes(status) && !states.includes(status))
|
|
409
409
|
continue;
|
|
410
410
|
const att = fields === "usage" ? (await this.store.taskLoad(prjId, entry.id))?.attachment
|
|
411
|
-
.find((
|
|
411
|
+
.find((a) => TaskFormat.isUsage(a) && a.Data !== undefined) : undefined;
|
|
412
412
|
const header = fields === "header" ? await this.boundHeader(prjId, entry) : entry.header;
|
|
413
413
|
result.push({
|
|
414
414
|
id: entry.id,
|
|
@@ -605,12 +605,10 @@ export class TaskStoreCore {
|
|
|
605
605
|
references: referrers.length
|
|
606
606
|
};
|
|
607
607
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
this.emitUpdated(prjId, taskId, plan, lifecycle, ["header"]);
|
|
611
|
-
}
|
|
608
|
+
await this.store.taskSave(prjId, taskId, plan);
|
|
609
|
+
this.emitUpdated(prjId, taskId, plan, lifecycle, ["header"]);
|
|
612
610
|
return {
|
|
613
|
-
id:
|
|
611
|
+
id: taskId,
|
|
614
612
|
status: TaskFormat.taskStatus(plan.header, lifecycle),
|
|
615
613
|
...(status !== null ? { from } : {})
|
|
616
614
|
};
|
|
@@ -235,3 +235,94 @@ export const attachmentSync = (current, wanted) => {
|
|
|
235
235
|
}
|
|
236
236
|
return ops;
|
|
237
237
|
};
|
|
238
|
+
/* reuse the cached promise of a load, else start the load and cache its promise
|
|
239
|
+
(dropped again on failure), so repeated and concurrent loads share one request */
|
|
240
|
+
export const memoize = (cache, key, load) => {
|
|
241
|
+
let promise = cache.get(key);
|
|
242
|
+
if (promise === undefined) {
|
|
243
|
+
promise = load();
|
|
244
|
+
cache.set(key, promise);
|
|
245
|
+
promise.catch(() => {
|
|
246
|
+
cache.delete(key);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return promise;
|
|
250
|
+
};
|
|
251
|
+
/* the common base of the issue tracker storage plugins: the projects mapped onto
|
|
252
|
+
their locations, the storage lifecycle polling for changes while opened and
|
|
253
|
+
observed (without keeping the process alive), and the project listing */
|
|
254
|
+
export class IssueTaskStoragePlugin {
|
|
255
|
+
ctx;
|
|
256
|
+
unit;
|
|
257
|
+
now;
|
|
258
|
+
url = "";
|
|
259
|
+
poll = 0;
|
|
260
|
+
repos = new Map();
|
|
261
|
+
listener = null;
|
|
262
|
+
timer = null;
|
|
263
|
+
opened;
|
|
264
|
+
polling = false;
|
|
265
|
+
polls = new Map();
|
|
266
|
+
constructor(ctx, unit, now) {
|
|
267
|
+
this.ctx = ctx;
|
|
268
|
+
this.unit = unit;
|
|
269
|
+
this.now = now;
|
|
270
|
+
this.opened = now();
|
|
271
|
+
}
|
|
272
|
+
/* the storage lifecycle */
|
|
273
|
+
async open() {
|
|
274
|
+
this.opened = this.now();
|
|
275
|
+
if (this.poll > 0 && this.listener !== null) {
|
|
276
|
+
this.timer = setInterval(() => {
|
|
277
|
+
this.pollAll().catch(() => {
|
|
278
|
+
/* failures are already logged per project */
|
|
279
|
+
});
|
|
280
|
+
}, this.poll * 1000);
|
|
281
|
+
this.timer.unref();
|
|
282
|
+
}
|
|
283
|
+
this.ctx.log("debug", `opened ${this.repos.size} ${this.unit} on ${this.url} (polling every ${this.poll}s)`);
|
|
284
|
+
}
|
|
285
|
+
async close() {
|
|
286
|
+
if (this.timer !== null)
|
|
287
|
+
clearInterval(this.timer);
|
|
288
|
+
this.timer = null;
|
|
289
|
+
}
|
|
290
|
+
/* observe the changes of the issues made outside of this plugin instance */
|
|
291
|
+
watch(listener) {
|
|
292
|
+
this.listener = listener;
|
|
293
|
+
}
|
|
294
|
+
/* list the registered ones of the mapped projects */
|
|
295
|
+
async projectList() {
|
|
296
|
+
const out = [];
|
|
297
|
+
for (const prjId of this.repos.keys()) {
|
|
298
|
+
const entry = await this.projectGet(prjId);
|
|
299
|
+
if (entry !== null)
|
|
300
|
+
out.push(entry);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
/* the change detection state of a project (starting at the opening time) */
|
|
305
|
+
pollState(prjId) {
|
|
306
|
+
let state = this.polls.get(prjId);
|
|
307
|
+
if (state === undefined) {
|
|
308
|
+
state = { since: this.opened, seen: new Map() };
|
|
309
|
+
this.polls.set(prjId, state);
|
|
310
|
+
}
|
|
311
|
+
return state;
|
|
312
|
+
}
|
|
313
|
+
/* poll all mapped projects, never overlapping itself */
|
|
314
|
+
async pollAll() {
|
|
315
|
+
if (this.polling)
|
|
316
|
+
return;
|
|
317
|
+
this.polling = true;
|
|
318
|
+
try {
|
|
319
|
+
for (const prjId of this.repos.keys())
|
|
320
|
+
await this.pollProject(prjId).catch((err) => {
|
|
321
|
+
this.ctx.log("warning", `polling project "${prjId}" failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
this.polling = false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -143,7 +143,7 @@ class FileTaskStoragePlugin {
|
|
|
143
143
|
doc = parseYAML(text);
|
|
144
144
|
}
|
|
145
145
|
catch (err) {
|
|
146
|
-
this.ctx.log("warning", `malformed project file "${file}" ignored: ${err.message}`);
|
|
146
|
+
this.ctx.log("warning", `malformed project file "${file}" ignored: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
147
|
return { lifecycle: this.lifecycle, idscheme: this.idscheme, seqmark: 0, broken: true };
|
|
148
148
|
}
|
|
149
149
|
return {
|
|
@@ -339,7 +339,9 @@ class FileTaskStoragePlugin {
|
|
|
339
339
|
if (this.watcher !== w)
|
|
340
340
|
return;
|
|
341
341
|
this.watcher = null;
|
|
342
|
-
w.close().catch(() => {
|
|
342
|
+
w.close().catch(() => {
|
|
343
|
+
/* re-arm even if the outdated watcher fails to close */
|
|
344
|
+
}).finally(() => {
|
|
343
345
|
this.arm();
|
|
344
346
|
this.schedule();
|
|
345
347
|
});
|
|
@@ -418,7 +420,9 @@ class FileTaskStoragePlugin {
|
|
|
418
420
|
clearTimeout(this.timer);
|
|
419
421
|
this.timer = setTimeout(() => {
|
|
420
422
|
this.timer = null;
|
|
421
|
-
this.rescan().catch(() => {
|
|
423
|
+
this.rescan().catch(() => {
|
|
424
|
+
/* failures are already logged per directory */
|
|
425
|
+
});
|
|
422
426
|
}, 100);
|
|
423
427
|
this.timer.unref();
|
|
424
428
|
}
|
|
@@ -50,19 +50,10 @@ const inRepo = (loc, issue) => issue.repository === undefined || issue.repositor
|
|
|
50
50
|
"After" onto the dependencies (the blocking issues), "Created"/"Modified" onto the
|
|
51
51
|
issue timestamps, any other key (including "Group", as Gitea has no parent issues)
|
|
52
52
|
onto "ase:<key>:<value>" labels, and the attachments onto the issue comments */
|
|
53
|
-
class GiteaTaskStoragePlugin {
|
|
54
|
-
ctx;
|
|
53
|
+
class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
|
|
55
54
|
name = "gitea";
|
|
56
|
-
url;
|
|
57
55
|
source;
|
|
58
56
|
gt;
|
|
59
|
-
repos = new Map();
|
|
60
|
-
poll;
|
|
61
|
-
listener = null;
|
|
62
|
-
timer = null;
|
|
63
|
-
polling = false;
|
|
64
|
-
opened = now();
|
|
65
|
-
polls = new Map();
|
|
66
57
|
registry = new LRUCache({ max: 64, ttl: 60 * 1000 });
|
|
67
58
|
latest = new Map();
|
|
68
59
|
labels = new Map();
|
|
@@ -71,7 +62,7 @@ class GiteaTaskStoragePlugin {
|
|
|
71
62
|
depends = new LRUCache({ max: 1024, ttl: 60 * 1000 });
|
|
72
63
|
loads = new LRUCache({ max: 256, ttl: 2 * 1000 });
|
|
73
64
|
constructor(ctx) {
|
|
74
|
-
|
|
65
|
+
super(ctx, "repositories", now);
|
|
75
66
|
const options = ctx.options;
|
|
76
67
|
const token = options.token ?? process.env.GITEA_TOKEN ?? "";
|
|
77
68
|
if (typeof token !== "string" || token === "")
|
|
@@ -147,27 +138,6 @@ class GiteaTaskStoragePlugin {
|
|
|
147
138
|
return out;
|
|
148
139
|
}
|
|
149
140
|
}
|
|
150
|
-
/* the storage lifecycle: polling for changes while opened and observed
|
|
151
|
-
(without keeping the process alive) */
|
|
152
|
-
async open() {
|
|
153
|
-
this.opened = now();
|
|
154
|
-
if (this.poll > 0 && this.listener !== null) {
|
|
155
|
-
this.timer = setInterval(() => {
|
|
156
|
-
this.pollAll().catch(() => { });
|
|
157
|
-
}, this.poll * 1000);
|
|
158
|
-
this.timer.unref();
|
|
159
|
-
}
|
|
160
|
-
this.ctx.log("debug", `opened ${this.repos.size} repositories on ${this.url} (polling every ${this.poll}s)`);
|
|
161
|
-
}
|
|
162
|
-
async close() {
|
|
163
|
-
if (this.timer !== null)
|
|
164
|
-
clearInterval(this.timer);
|
|
165
|
-
this.timer = null;
|
|
166
|
-
}
|
|
167
|
-
/* observe the changes of the issues made outside of this plugin instance */
|
|
168
|
-
watch(listener) {
|
|
169
|
-
this.listener = listener;
|
|
170
|
-
}
|
|
171
141
|
/* ==== project registry ==== */
|
|
172
142
|
/* the repository of a project */
|
|
173
143
|
repo(prjId) {
|
|
@@ -240,15 +210,6 @@ class GiteaTaskStoragePlugin {
|
|
|
240
210
|
async seqmark(loc, entry, fresh = false) {
|
|
241
211
|
return Math.max(await this.latestNumber(loc, fresh), entry?.seqmark ?? 0);
|
|
242
212
|
}
|
|
243
|
-
async projectList() {
|
|
244
|
-
const out = [];
|
|
245
|
-
for (const prjId of this.repos.keys()) {
|
|
246
|
-
const entry = await this.projectGet(prjId);
|
|
247
|
-
if (entry !== null)
|
|
248
|
-
out.push(entry);
|
|
249
|
-
}
|
|
250
|
-
return out;
|
|
251
|
-
}
|
|
252
213
|
/* get a project, with the sequence number high-water mark as the next
|
|
253
214
|
allocated task id is the one of the next created issue */
|
|
254
215
|
async projectGet(prjId) {
|
|
@@ -287,7 +248,7 @@ class GiteaTaskStoragePlugin {
|
|
|
287
248
|
await found(this.call(loc, this.gt.repos.issueDeleteLabel(loc.owner, loc.repo, label.id)).then(() => true)) : null;
|
|
288
249
|
this.labels.delete(`${loc.owner}/${loc.repo}`);
|
|
289
250
|
this.registry.delete(prjId);
|
|
290
|
-
return
|
|
251
|
+
return result !== null;
|
|
291
252
|
}
|
|
292
253
|
/* ==== issue mapping ==== */
|
|
293
254
|
/* fetch the live issue of an issue number (null if none) */
|
|
@@ -303,15 +264,8 @@ class GiteaTaskStoragePlugin {
|
|
|
303
264
|
const key = `${loc.owner}/${loc.repo}#${issue.number}@${issue.updated_at}`;
|
|
304
265
|
if (fresh)
|
|
305
266
|
this.depends.delete(key);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
depends = found(this.all(loc, (query) => this.gt.repos.issueListIssueDependencies(loc.owner, loc.repo, String(issue.number), query)))
|
|
309
|
-
.then((issues) => (issues ?? []));
|
|
310
|
-
this.depends.set(key, depends);
|
|
311
|
-
depends.catch(() => {
|
|
312
|
-
this.depends.delete(key);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
267
|
+
const depends = TaskIssues.memoize(this.depends, key, () => found(this.all(loc, (query) => this.gt.repos.issueListIssueDependencies(loc.owner, loc.repo, String(issue.number), query)))
|
|
268
|
+
.then((issues) => (issues ?? [])));
|
|
315
269
|
return (await depends).filter((blocker) => inRepo(loc, blocker));
|
|
316
270
|
}
|
|
317
271
|
/* the comments of an issue */
|
|
@@ -362,10 +316,10 @@ class GiteaTaskStoragePlugin {
|
|
|
362
316
|
if (label === undefined) {
|
|
363
317
|
label = await this.call(loc, this.gt.repos.issueCreateLabel(loc.owner, loc.repo, { name, color: name.startsWith("ase:") ? "#c5def5" : "#ededed" })).catch(async (err) => {
|
|
364
318
|
/* tolerate a label created concurrently */
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
319
|
+
const existing = statusOf(err) === 422 ? (await this.labelMap(loc, true)).get(name.toLowerCase()) : undefined;
|
|
320
|
+
if (existing === undefined)
|
|
367
321
|
throw err;
|
|
368
|
-
return
|
|
322
|
+
return existing;
|
|
369
323
|
});
|
|
370
324
|
const map = await this.labelMap(loc);
|
|
371
325
|
map.set(name.toLowerCase(), label);
|
|
@@ -422,15 +376,8 @@ class GiteaTaskStoragePlugin {
|
|
|
422
376
|
if (typeof assignee === "string" && assignee !== "") {
|
|
423
377
|
const logins = issue?.assignees?.map((user) => user.login) ?? [];
|
|
424
378
|
const key = `${loc.owner}/${loc.repo}`;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
users = this.call(loc, this.gt.repos.repoGetAssignees(loc.owner, loc.repo))
|
|
428
|
-
.then((list) => new Set(list.map((user) => (user.login ?? "").toLowerCase())));
|
|
429
|
-
this.assignable.set(key, users);
|
|
430
|
-
users.catch(() => {
|
|
431
|
-
this.assignable.delete(key);
|
|
432
|
-
});
|
|
433
|
-
}
|
|
379
|
+
const users = TaskIssues.memoize(this.assignable, key, () => this.call(loc, this.gt.repos.repoGetAssignees(loc.owner, loc.repo))
|
|
380
|
+
.then((list) => new Set(list.map((user) => (user.login ?? "").toLowerCase()))));
|
|
434
381
|
if (logins.includes(assignee))
|
|
435
382
|
assignees = logins;
|
|
436
383
|
else if ((await users).has(assignee.toLowerCase()))
|
|
@@ -486,11 +433,7 @@ class GiteaTaskStoragePlugin {
|
|
|
486
433
|
own write or detected change of the issue), as e.g. the boards load the plan
|
|
487
434
|
and its attachments in separate operations */
|
|
488
435
|
load(prjId, loc, n) {
|
|
489
|
-
|
|
490
|
-
const cached = this.loads.get(key);
|
|
491
|
-
if (cached !== undefined)
|
|
492
|
-
return cached;
|
|
493
|
-
const load = (async () => {
|
|
436
|
+
return TaskIssues.memoize(this.loads, `${prjId}#${n}`, async () => {
|
|
494
437
|
const [issue, comments] = await Promise.all([
|
|
495
438
|
this.fetch(loc, n),
|
|
496
439
|
n === 0 ? [] : found(this.comments(loc, n))
|
|
@@ -498,12 +441,7 @@ class GiteaTaskStoragePlugin {
|
|
|
498
441
|
if (issue === null)
|
|
499
442
|
return null;
|
|
500
443
|
return { issue, after: await this.blockers(loc, issue), comments: comments ?? [] };
|
|
501
|
-
})();
|
|
502
|
-
this.loads.set(key, load);
|
|
503
|
-
load.catch(() => {
|
|
504
|
-
this.loads.delete(key);
|
|
505
444
|
});
|
|
506
|
-
return load;
|
|
507
445
|
}
|
|
508
446
|
/* ==== task plans ==== */
|
|
509
447
|
async taskList(prjId) {
|
|
@@ -606,30 +544,6 @@ class GiteaTaskStoragePlugin {
|
|
|
606
544
|
throw new Error(`task "${oldId}" cannot be renamed to "${newId}", as task ids are Gitea issue numbers`);
|
|
607
545
|
}
|
|
608
546
|
/* ==== change detection ==== */
|
|
609
|
-
/* the change detection state of a project (starting at the opening time) */
|
|
610
|
-
pollState(prjId) {
|
|
611
|
-
let state = this.polls.get(prjId);
|
|
612
|
-
if (state === undefined) {
|
|
613
|
-
state = { since: this.opened, seen: new Map() };
|
|
614
|
-
this.polls.set(prjId, state);
|
|
615
|
-
}
|
|
616
|
-
return state;
|
|
617
|
-
}
|
|
618
|
-
/* poll all registered projects, never overlapping itself */
|
|
619
|
-
async pollAll() {
|
|
620
|
-
if (this.polling)
|
|
621
|
-
return;
|
|
622
|
-
this.polling = true;
|
|
623
|
-
try {
|
|
624
|
-
for (const prjId of this.repos.keys())
|
|
625
|
-
await this.pollProject(prjId).catch((err) => {
|
|
626
|
-
this.ctx.log("warning", `polling project "${prjId}" failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
finally {
|
|
630
|
-
this.polling = false;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
547
|
/* poll the issues of a project updated since the last poll (Gitea supports no
|
|
634
548
|
conditional requests), and report the issues not seen at their update time yet:
|
|
635
549
|
as added if created after the opening, else as updated (issues deleted on Gitea
|
|
@@ -60,18 +60,9 @@ const inRepo = (loc, issue) => {
|
|
|
60
60
|
the attachments onto the issue comments (where "Group" and "After" fall back
|
|
61
61
|
onto "ase:Group:<id>" resp. "ase:After:<ids>" labels on instances without
|
|
62
62
|
sub-issues resp. issue dependencies) */
|
|
63
|
-
class GitHubTaskStoragePlugin {
|
|
64
|
-
ctx;
|
|
63
|
+
class GitHubTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
|
|
65
64
|
name = "github";
|
|
66
|
-
url;
|
|
67
65
|
gh;
|
|
68
|
-
repos = new Map();
|
|
69
|
-
poll;
|
|
70
|
-
listener = null;
|
|
71
|
-
timer = null;
|
|
72
|
-
polling = false;
|
|
73
|
-
opened = now();
|
|
74
|
-
polls = new Map();
|
|
75
66
|
registry = new LRUCache({ max: 64, ttl: 60 * 1000 });
|
|
76
67
|
latest = new Map();
|
|
77
68
|
labels = new Map();
|
|
@@ -80,9 +71,9 @@ class GitHubTaskStoragePlugin {
|
|
|
80
71
|
recent = new Map();
|
|
81
72
|
loads = new LRUCache({ max: 256, ttl: 2 * 1000 });
|
|
82
73
|
constructor(ctx) {
|
|
83
|
-
|
|
74
|
+
super(ctx, "repositories", now);
|
|
84
75
|
const options = ctx.options;
|
|
85
|
-
const token = options.token ?? process.env.GITHUB_TOKEN
|
|
76
|
+
const token = options.token ?? (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) ?? "";
|
|
86
77
|
if (typeof token !== "string" || token === "")
|
|
87
78
|
throw new Error("task store: plugin \"github\" requires the \"token\" option (or $GITHUB_TOKEN resp. $GH_TOKEN)");
|
|
88
79
|
const url = options.url ?? "https://github.com";
|
|
@@ -131,7 +122,7 @@ class GitHubTaskStoragePlugin {
|
|
|
131
122
|
/* explain a denied access (but not an exceeded rate limit), as the
|
|
132
123
|
GitHub message names neither the token source nor the missing permission */
|
|
133
124
|
const source = options.token !== undefined ? "the \"token\" option (resp. \"project.task.store.token\")" :
|
|
134
|
-
process.env.GITHUB_TOKEN !== undefined ? "$GITHUB_TOKEN" : "$GH_TOKEN";
|
|
125
|
+
process.env.GITHUB_TOKEN !== undefined && process.env.GITHUB_TOKEN !== "" ? "$GITHUB_TOKEN" : "$GH_TOKEN";
|
|
135
126
|
this.gh.hook.error("request", (err, request) => {
|
|
136
127
|
const status = statusOf(err);
|
|
137
128
|
if ((status === 401 || status === 403) && !/rate limit/i.test(err.message)) {
|
|
@@ -144,27 +135,6 @@ class GitHubTaskStoragePlugin {
|
|
|
144
135
|
throw err;
|
|
145
136
|
});
|
|
146
137
|
}
|
|
147
|
-
/* the storage lifecycle: polling for changes while opened and observed
|
|
148
|
-
(without keeping the process alive) */
|
|
149
|
-
async open() {
|
|
150
|
-
this.opened = now();
|
|
151
|
-
if (this.poll > 0 && this.listener !== null) {
|
|
152
|
-
this.timer = setInterval(() => {
|
|
153
|
-
this.pollAll().catch(() => { });
|
|
154
|
-
}, this.poll * 1000);
|
|
155
|
-
this.timer.unref();
|
|
156
|
-
}
|
|
157
|
-
this.ctx.log("debug", `opened ${this.repos.size} repositories on ${this.url} (polling every ${this.poll}s)`);
|
|
158
|
-
}
|
|
159
|
-
async close() {
|
|
160
|
-
if (this.timer !== null)
|
|
161
|
-
clearInterval(this.timer);
|
|
162
|
-
this.timer = null;
|
|
163
|
-
}
|
|
164
|
-
/* observe the changes of the issues made outside of this plugin instance */
|
|
165
|
-
watch(listener) {
|
|
166
|
-
this.listener = listener;
|
|
167
|
-
}
|
|
168
138
|
/* ==== project registry ==== */
|
|
169
139
|
/* the repository of a project */
|
|
170
140
|
repo(prjId) {
|
|
@@ -222,15 +192,6 @@ class GitHubTaskStoragePlugin {
|
|
|
222
192
|
throw err;
|
|
223
193
|
}
|
|
224
194
|
}
|
|
225
|
-
async projectList() {
|
|
226
|
-
const out = [];
|
|
227
|
-
for (const prjId of this.repos.keys()) {
|
|
228
|
-
const entry = await this.projectGet(prjId);
|
|
229
|
-
if (entry !== null)
|
|
230
|
-
out.push(entry);
|
|
231
|
-
}
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
195
|
/* get a project, with the highest issue number as the sequence number high-water
|
|
235
196
|
mark, so the next allocated task id is the one of the next created issue */
|
|
236
197
|
async projectGet(prjId) {
|
|
@@ -501,11 +462,7 @@ class GitHubTaskStoragePlugin {
|
|
|
501
462
|
own write or detected change of the issue), as e.g. the boards load the plan
|
|
502
463
|
and its attachments in separate operations */
|
|
503
464
|
load(prjId, loc, n) {
|
|
504
|
-
|
|
505
|
-
const cached = this.loads.get(key);
|
|
506
|
-
if (cached !== undefined)
|
|
507
|
-
return cached;
|
|
508
|
-
const load = (async () => {
|
|
465
|
+
return TaskIssues.memoize(this.loads, `${prjId}#${n}`, async () => {
|
|
509
466
|
const [issue, comments] = await Promise.all([
|
|
510
467
|
this.fetch(loc, n),
|
|
511
468
|
n === 0 ? [] : found(this.gh.paginate(this.gh.rest.issues.listComments, { ...loc, issue_number: n, per_page: 100 }))
|
|
@@ -513,12 +470,7 @@ class GitHubTaskStoragePlugin {
|
|
|
513
470
|
if (issue === null)
|
|
514
471
|
return null;
|
|
515
472
|
return { issue, after: await this.blockers(loc, issue), comments: comments ?? [] };
|
|
516
|
-
})();
|
|
517
|
-
this.loads.set(key, load);
|
|
518
|
-
load.catch(() => {
|
|
519
|
-
this.loads.delete(key);
|
|
520
473
|
});
|
|
521
|
-
return load;
|
|
522
474
|
}
|
|
523
475
|
/* ==== task plans ==== */
|
|
524
476
|
async taskList(prjId) {
|
|
@@ -618,30 +570,6 @@ class GitHubTaskStoragePlugin {
|
|
|
618
570
|
throw new Error(`task "${oldId}" cannot be renamed to "${newId}", as task ids are GitHub issue numbers`);
|
|
619
571
|
}
|
|
620
572
|
/* ==== change detection ==== */
|
|
621
|
-
/* the change detection state of a project (starting at the opening time) */
|
|
622
|
-
pollState(prjId) {
|
|
623
|
-
let state = this.polls.get(prjId);
|
|
624
|
-
if (state === undefined) {
|
|
625
|
-
state = { since: this.opened, seen: new Map() };
|
|
626
|
-
this.polls.set(prjId, state);
|
|
627
|
-
}
|
|
628
|
-
return state;
|
|
629
|
-
}
|
|
630
|
-
/* poll all registered projects, never overlapping itself */
|
|
631
|
-
async pollAll() {
|
|
632
|
-
if (this.polling)
|
|
633
|
-
return;
|
|
634
|
-
this.polling = true;
|
|
635
|
-
try {
|
|
636
|
-
for (const prjId of this.repos.keys())
|
|
637
|
-
await this.pollProject(prjId).catch((err) => {
|
|
638
|
-
this.ctx.log("warning", `polling project "${prjId}" failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
finally {
|
|
642
|
-
this.polling = false;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
573
|
/* poll the issues of a project updated since the last poll, conditionally
|
|
646
574
|
via the entity tag of the last poll (a "304" answer is free of rate limit),
|
|
647
575
|
and report the issues not seen at their update time yet: as added if created
|