@rse/ase 2.0.2 → 2.0.4
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 +2 -0
- package/dst/ase-hook.js +13 -13
- package/dst/ase-lib-daemon-cli.js +117 -4
- package/dst/ase-lib-server-cli.js +4 -5
- package/dst/ase-lib-version.js +14 -5
- package/dst/ase-man-catalog.js +229 -0
- package/dst/ase-man-render.js +278 -0
- package/dst/ase-man-tui.js +636 -0
- package/dst/ase-man.js +33 -27
- 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 +4 -5
- package/dst/ase-spec.js +103 -20
- package/dst/ase-statusline.js +170 -170
- package/dst/ase-task-board-filter.js +5 -4
- 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 +41 -35
- package/dst/ase-task-board-tui-model.js +185 -167
- package/dst/ase-task-board-tui-popup.js +2 -3
- package/dst/ase-task-board-tui-view.js +74 -70
- package/dst/ase-task-board-web-client.js +27 -27
- package/dst/ase-task-board-web-server.js +2 -2
- 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 +16 -1
- package/dst/ase-task-store-plugin-ase.js +7 -3
- package/dst/ase-task-store-plugin-gitea.js +9 -32
- package/dst/ase-task-store-plugin-github.js +3 -12
- package/dst/ase-task-store-plugin-gitlab.js +2 -18
- package/dst/ase-task.js +51 -23
- package/dst/ase-util-compat.js +1 -1
- package/dst/ase-util-diagram.js +13 -37
- package/dst/ase.js +5 -5
- package/dst/man/ase-agent-adhoc.md +1165 -0
- package/dst/man/ase-agent-condenser.md +383 -0
- package/dst/man/ase-agent-editor.md +441 -0
- package/dst/man/ase-agent-estimator.md +477 -0
- package/dst/man/ase-agent-fleet.md +358 -0
- package/dst/man/ase-agent-implementer.md +521 -0
- package/dst/man/ase-agent-integrator.md +449 -0
- package/dst/man/ase-agent-qualifier.md +426 -0
- package/dst/man/ase-agent-rebooter.md +384 -0
- package/dst/man/ase-agent-relay.md +262 -0
- package/dst/man/ase-agent-researcher.md +478 -0
- package/dst/man/ase-agent.md +628 -0
- package/dst/man/ase-artifact-list.md +57 -0
- package/dst/man/ase-artifact-name.md +56 -0
- package/dst/man/ase-artifact.md +92 -0
- package/dst/man/ase-config-delete.md +74 -0
- package/dst/man/ase-config-edit.md +83 -0
- package/dst/man/ase-config-get.md +70 -0
- package/dst/man/ase-config-init.md +110 -0
- package/dst/man/ase-config-list.md +68 -0
- package/dst/man/ase-config-set.md +92 -0
- package/dst/man/ase-config.md +426 -0
- package/dst/man/ase-hook-permission-request.md +61 -0
- package/dst/man/ase-hook-pre-tool-use.md +96 -0
- package/dst/man/ase-hook-session-end.md +71 -0
- package/dst/man/ase-hook-session-persona.md +65 -0
- package/dst/man/ase-hook-session-start.md +184 -0
- package/dst/man/ase-hook-stop.md +52 -0
- package/dst/man/ase-hook-user-prompt-submit.md +54 -0
- package/dst/man/ase-hook.md +93 -0
- package/dst/man/ase-man.md +195 -0
- package/dst/man/ase-mcp.md +102 -0
- package/dst/man/ase-service-log.md +59 -0
- package/dst/man/ase-service-send.md +60 -0
- package/dst/man/ase-service-start.md +93 -0
- package/dst/man/ase-service-status.md +58 -0
- package/dst/man/ase-service-stop.md +52 -0
- package/dst/man/ase-service.md +124 -0
- package/dst/man/ase-setup-disable.md +67 -0
- package/dst/man/ase-setup-enable.md +66 -0
- package/dst/man/ase-setup-install.md +116 -0
- package/dst/man/ase-setup-mcp.md +213 -0
- package/dst/man/ase-setup-status.md +101 -0
- package/dst/man/ase-setup-statusline.md +162 -0
- package/dst/man/ase-setup-telemetry.md +166 -0
- package/dst/man/ase-setup-uninstall.md +85 -0
- package/dst/man/ase-setup-update.md +117 -0
- package/dst/man/ase-setup.md +156 -0
- package/dst/man/ase-spec-export.md +99 -0
- package/dst/man/ase-spec-init.md +57 -0
- package/dst/man/ase-spec-lint.md +74 -0
- package/dst/man/ase-spec-preview.md +57 -0
- package/dst/man/ase-spec.md +85 -0
- package/dst/man/ase-statusline.md +330 -0
- package/dst/man/ase-task-assign.md +138 -0
- package/dst/man/ase-task-board.md +651 -0
- package/dst/man/ase-task-delete.md +46 -0
- package/dst/man/ase-task-edit.md +73 -0
- package/dst/man/ase-task-idscheme.md +92 -0
- package/dst/man/ase-task-lifecycle.md +92 -0
- package/dst/man/ase-task-list.md +86 -0
- package/dst/man/ase-task-load.md +48 -0
- package/dst/man/ase-task-newid.md +100 -0
- package/dst/man/ase-task-purge.md +49 -0
- package/dst/man/ase-task-rename.md +69 -0
- package/dst/man/ase-task-request.md +108 -0
- package/dst/man/ase-task-save.md +78 -0
- package/dst/man/ase-task-status.md +79 -0
- package/dst/man/ase-task-store.md +377 -0
- package/dst/man/ase-task-view.md +50 -0
- package/dst/man/ase-task.md +335 -0
- package/dst/man/ase-util-compat.md +38 -0
- package/dst/man/ase-util-diagram.md +140 -0
- package/dst/man/ase-util-meta.md +68 -0
- package/dst/man/ase-util-metric.md +74 -0
- package/dst/man/ase-util-mint.md +85 -0
- package/dst/man/ase-util-worktree.md +109 -0
- package/dst/man/ase-util.md +81 -0
- package/dst/man/ase.md +228 -0
- package/package.json +12 -17
- 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-code-lint.md +3 -3
- package/plugin/agents/ase-docs-proofread.md +3 -3
- package/plugin/agents/ase-docs-refine.md +3 -3
- package/plugin/agents/ase-spec-lint.md +338 -0
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/mods/ase-mods.ts +10 -7
- package/plugin/mods/claude-code.d.ts +26 -3
- package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
- package/plugin/skills/ase-code-analyze/help.md +11 -6
- package/plugin/skills/ase-code-lint/SKILL.md +37 -23
- package/plugin/skills/ase-code-lint/help.md +10 -5
- package/plugin/skills/ase-help-intent/data.md +204 -13
- package/plugin/skills/ase-help-skill/catalog.md +1 -0
- package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
- package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
- package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
- 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 +3 -3
- package/plugin/skills/ase-spec-edit/help.md +1 -1
- package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
- package/plugin/skills/ase-spec-lint/help.md +171 -0
- package/plugin/skills/ase-task-implement/SKILL.md +24 -6
- package/plugin/skills/ase-task-implement/help.md +11 -1
- package/dst/man/ase-agent-adhoc.1 +0 -515
- package/dst/man/ase-agent-condenser.1 +0 -193
- package/dst/man/ase-agent-editor.1 +0 -212
- package/dst/man/ase-agent-estimator.1 +0 -214
- package/dst/man/ase-agent-fleet.1 +0 -185
- package/dst/man/ase-agent-implementer.1 +0 -233
- package/dst/man/ase-agent-integrator.1 +0 -213
- package/dst/man/ase-agent-qualifier.1 +0 -214
- package/dst/man/ase-agent-rebooter.1 +0 -193
- package/dst/man/ase-agent-relay.1 +0 -128
- package/dst/man/ase-agent-researcher.1 +0 -217
- package/dst/man/ase-agent.1 +0 -264
- package/dst/man/ase-artifact-list.1 +0 -38
- package/dst/man/ase-artifact-name.1 +0 -39
- package/dst/man/ase-artifact.1 +0 -56
- package/dst/man/ase-config-delete.1 +0 -45
- package/dst/man/ase-config-edit.1 +0 -48
- package/dst/man/ase-config-get.1 +0 -46
- package/dst/man/ase-config-init.1 +0 -60
- package/dst/man/ase-config-list.1 +0 -42
- package/dst/man/ase-config-set.1 +0 -52
- package/dst/man/ase-config.1 +0 -231
- package/dst/man/ase-hook-permission-request.1 +0 -36
- package/dst/man/ase-hook-pre-tool-use.1 +0 -59
- package/dst/man/ase-hook-session-end.1 +0 -47
- package/dst/man/ase-hook-session-persona.1 +0 -37
- package/dst/man/ase-hook-session-start.1 +0 -81
- package/dst/man/ase-hook-stop.1 +0 -34
- package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
- package/dst/man/ase-hook.1 +0 -51
- package/dst/man/ase-man.1 +0 -67
- package/dst/man/ase-mcp.1 +0 -61
- package/dst/man/ase-service-log.1 +0 -43
- package/dst/man/ase-service-send.1 +0 -47
- package/dst/man/ase-service-start.1 +0 -54
- package/dst/man/ase-service-status.1 +0 -46
- package/dst/man/ase-service-stop.1 +0 -37
- package/dst/man/ase-service.1 +0 -85
- package/dst/man/ase-setup-disable.1 +0 -44
- package/dst/man/ase-setup-enable.1 +0 -45
- package/dst/man/ase-setup-install.1 +0 -73
- package/dst/man/ase-setup-mcp.1 +0 -124
- package/dst/man/ase-setup-status.1 +0 -60
- package/dst/man/ase-setup-statusline.1 +0 -92
- package/dst/man/ase-setup-telemetry.1 +0 -92
- package/dst/man/ase-setup-uninstall.1 +0 -59
- package/dst/man/ase-setup-update.1 +0 -64
- package/dst/man/ase-setup.1 +0 -93
- package/dst/man/ase-spec-export.1 +0 -62
- package/dst/man/ase-spec-lint.1 +0 -40
- package/dst/man/ase-spec-preview.1 +0 -40
- package/dst/man/ase-spec.1 +0 -46
- package/dst/man/ase-statusline.1 +0 -189
- package/dst/man/ase-task-assign.1 +0 -71
- package/dst/man/ase-task-board.1 +0 -384
- package/dst/man/ase-task-delete.1 +0 -35
- package/dst/man/ase-task-edit.1 +0 -46
- package/dst/man/ase-task-idscheme.1 +0 -65
- package/dst/man/ase-task-lifecycle.1 +0 -57
- package/dst/man/ase-task-list.1 +0 -47
- package/dst/man/ase-task-load.1 +0 -36
- package/dst/man/ase-task-newid.1 +0 -61
- package/dst/man/ase-task-purge.1 +0 -37
- package/dst/man/ase-task-rename.1 +0 -44
- package/dst/man/ase-task-request.1 +0 -61
- package/dst/man/ase-task-save.1 +0 -48
- package/dst/man/ase-task-status.1 +0 -55
- package/dst/man/ase-task-store.1 +0 -194
- package/dst/man/ase-task-view.1 +0 -40
- package/dst/man/ase-task.1 +0 -160
- package/dst/man/ase-util-compat.1 +0 -25
- package/dst/man/ase-util-diagram.1 +0 -76
- package/dst/man/ase-util-meta.1 +0 -45
- package/dst/man/ase-util-metric.1 +0 -54
- package/dst/man/ase-util-mint.1 +0 -59
- package/dst/man/ase-util-worktree.1 +0 -72
- package/dst/man/ase-util.1 +0 -50
- package/dst/man/ase.1 +0 -124
package/dst/ase-man.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
10
9
|
import { writeStdout } from "./ase-lib-stdio.js";
|
|
10
|
+
/* the width of the manual pages printed as plain text */
|
|
11
|
+
const WIDTH = 80;
|
|
11
12
|
/* reusable functionality: resolve the internally shipped manual pages */
|
|
12
13
|
export class Man {
|
|
13
14
|
/* determine the directory of the manual pages; the build process
|
|
14
|
-
|
|
15
|
+
copies "man/*.md" into "dst/man/*.md" (see the "build-man" build
|
|
15
16
|
step), right next to the compiled tool */
|
|
16
17
|
static dir() {
|
|
17
18
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "man");
|
|
@@ -26,15 +27,15 @@ export class Man {
|
|
|
26
27
|
throw new Error(`man: no manual pages found in "${Man.dir()}" (tool not completely built?)`, { cause: err });
|
|
27
28
|
}
|
|
28
29
|
return files
|
|
29
|
-
.filter((file) => /^ase(?:-[a-z0-9-]+)
|
|
30
|
-
.map((file) => file.replace(
|
|
30
|
+
.filter((file) => /^ase(?:-[a-z0-9-]+)?\.md$/.test(file))
|
|
31
|
+
.map((file) => file.replace(/\.md$/, ""))
|
|
31
32
|
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
|
32
33
|
}
|
|
33
|
-
/* resolve the words of a requested manual page to
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
/* resolve the words of a requested manual page to its name: the words
|
|
35
|
+
are joined with dashes, the "ase" prefix is optional, and a command path
|
|
36
|
+
deeper than the shipped pages (e.g. "ase task store start") falls back to
|
|
37
|
+
the page of its nearest parent (e.g. "ase-task-store"), as the pages of a
|
|
38
|
+
group with third-level commands cover them */
|
|
38
39
|
static resolve(words) {
|
|
39
40
|
const name = words.join("-").toLowerCase();
|
|
40
41
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
|
|
@@ -46,7 +47,7 @@ export class Man {
|
|
|
46
47
|
while (parts.length > 0) {
|
|
47
48
|
const page = parts.join("-");
|
|
48
49
|
if (pages.has(page))
|
|
49
|
-
return
|
|
50
|
+
return page;
|
|
50
51
|
parts.pop();
|
|
51
52
|
if (parts.length === 1)
|
|
52
53
|
break;
|
|
@@ -64,33 +65,38 @@ export default class ManCommand {
|
|
|
64
65
|
register(program) {
|
|
65
66
|
program
|
|
66
67
|
.command("man")
|
|
67
|
-
.description("
|
|
68
|
+
.description("Browse all manual pages interactively, or print a manual page as plain text")
|
|
68
69
|
.argument("[page...]", "manual page name, either as \"ase-<command>[-<sub-command>]\" " +
|
|
69
|
-
"or as the words of the command path (\"ase\" prefix optional,
|
|
70
|
+
"or as the words of the command path (\"ase\" prefix optional), selected in the " +
|
|
71
|
+
"interactive browser on a terminal (or searched for, if no manual page fits exactly), " +
|
|
72
|
+
"else printed as plain text (default: \"ase\")")
|
|
70
73
|
.option("-l, --list", "list the names of all shipped manual pages instead", false)
|
|
71
|
-
.option("-p, --path", "print the path of the manual page file instead", false)
|
|
72
74
|
.action(async (words, opts) => {
|
|
73
75
|
/* list all manual pages */
|
|
74
76
|
if (opts.list) {
|
|
75
77
|
await writeStdout(Man.list().map((page) => `${page}\n`).join(""));
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
|
-
/*
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
await
|
|
80
|
+
/* browse all manual pages interactively (only on a terminal), with
|
|
81
|
+
the given manual page selected resp. searched for (if any) */
|
|
82
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
83
|
+
const { runManTUI } = await import("./ase-man-tui.js");
|
|
84
|
+
await runManTUI(this.log, words);
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
|
-
/*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
/* resolve manual page, where a skill or command fitting exactly
|
|
88
|
+
is preferred over the fallback onto the nearest parent command */
|
|
89
|
+
const { loadCatalog, findPage } = await import("./ase-man-catalog.js");
|
|
90
|
+
const { renderPage } = await import("./ase-man-render.js");
|
|
91
|
+
const catalog = loadCatalog();
|
|
92
|
+
const request = words.length > 0 ? words : ["ase"];
|
|
93
|
+
const page = findPage(catalog, request) ?? catalog.pages.get(`${Man.resolve(request)}.1`);
|
|
94
|
+
if (page === undefined)
|
|
95
|
+
throw new Error(`man: no manual page found for "${request.join(" ")}" (see "ase man --list")`);
|
|
96
|
+
/* print manual page as plain text */
|
|
97
|
+
this.log.write("debug", `man: printing manual page "${page.id}"`);
|
|
98
|
+
const { lines } = renderPage(page.md, WIDTH, page.id, (id) => catalog.pages.has(id));
|
|
99
|
+
await writeStdout(lines.map((line) => line.map((span) => span.text).join("").trimEnd() + "\n").join(""));
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
102
|
}
|
package/dst/ase-mcp.js
CHANGED
|
@@ -72,7 +72,7 @@ export default class MCPCommand {
|
|
|
72
72
|
const value = state.getExplicit("basedir");
|
|
73
73
|
return typeof value === "string" && value !== "" ? value : null;
|
|
74
74
|
}
|
|
75
|
-
catch
|
|
75
|
+
catch {
|
|
76
76
|
/* best-effort: an unreadable session config just defers the lookup */
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
@@ -139,7 +139,7 @@ export default class MCPCommand {
|
|
|
139
139
|
next.onerror = (err) => {
|
|
140
140
|
this.log.write("error", `mcp: http: ${err.message}`);
|
|
141
141
|
};
|
|
142
|
-
/* service closed the connection
|
|
142
|
+
/* service closed the connection -- try to recover */
|
|
143
143
|
next.onclose = () => {
|
|
144
144
|
if (client !== next || closedByUs.has(next) || bridgeDone || reconnecting)
|
|
145
145
|
return;
|
|
@@ -183,7 +183,7 @@ export default class MCPCommand {
|
|
|
183
183
|
if (reconnecting)
|
|
184
184
|
return;
|
|
185
185
|
reconnecting = true;
|
|
186
|
-
this.log.write("warning", `mcp: ${reason}
|
|
186
|
+
this.log.write("warning", `mcp: ${reason} -- reconnecting`);
|
|
187
187
|
reconnect(0).catch(() => { });
|
|
188
188
|
};
|
|
189
189
|
/* bounded buffer for messages arriving while no HTTP client is connected */
|
|
@@ -315,7 +315,7 @@ export default class MCPCommand {
|
|
|
315
315
|
await connectClient();
|
|
316
316
|
}
|
|
317
317
|
catch (err) {
|
|
318
|
-
/* service vanished between probe and connect
|
|
318
|
+
/* service vanished between probe and connect -- recover instead of crashing */
|
|
319
319
|
triggerReconnect(`initial connect failed: ${this.asError(err).message}`);
|
|
320
320
|
}
|
|
321
321
|
}
|
|
@@ -23,7 +23,7 @@ const tokenizeRanges = (input) => {
|
|
|
23
23
|
const quote = ch;
|
|
24
24
|
i++;
|
|
25
25
|
while (i < input.length && input[i] !== quote) {
|
|
26
|
-
if (input[i] === "\\" && i + 1 < input.length)
|
|
26
|
+
if (quote === "\"" && input[i] === "\\" && i + 1 < input.length)
|
|
27
27
|
i++;
|
|
28
28
|
i++;
|
|
29
29
|
}
|
|
@@ -50,7 +50,7 @@ const unquote = (s) => {
|
|
|
50
50
|
const quote = ch;
|
|
51
51
|
j++;
|
|
52
52
|
while (j < s.length && s[j] !== quote) {
|
|
53
|
-
if (s[j] === "\\" && j + 1 < s.length) {
|
|
53
|
+
if (quote === "\"" && s[j] === "\\" && j + 1 < s.length) {
|
|
54
54
|
out += s[j + 1];
|
|
55
55
|
j += 2;
|
|
56
56
|
}
|
package/dst/ase-service-otel.js
CHANGED
|
@@ -580,7 +580,7 @@ export class UsageTracker {
|
|
|
580
580
|
const self = userId();
|
|
581
581
|
if (assignee.trim() === "")
|
|
582
582
|
throw new Error("usage: empty assignee (use \"none\" to unassign)");
|
|
583
|
-
const to = assignee === "me" ? self : assignee.trim();
|
|
583
|
+
const to = assignee.trim() === "me" ? self : assignee.trim();
|
|
584
584
|
/* (keep the session out of the adoptions meanwhile, and let an assignment to oneself
|
|
585
585
|
settle the adoption of its active task, as the explicit assignment takes precedence) */
|
|
586
586
|
this.explicit.add(session);
|
|
@@ -591,6 +591,7 @@ export class UsageTracker {
|
|
|
591
591
|
this.explicit.delete(session);
|
|
592
592
|
}
|
|
593
593
|
}
|
|
594
|
+
/* perform the assignment of "assign" (with the session kept out of the adoptions) */
|
|
594
595
|
async assignSession(id, to, self, session) {
|
|
595
596
|
const warning = await Task.checkAssignee(this.log, to);
|
|
596
597
|
const from = await Task.assign(this.log, id, to === "none" ? null : to);
|
|
@@ -952,6 +953,7 @@ export class UsageTracker {
|
|
|
952
953
|
this.unwatch = null;
|
|
953
954
|
}
|
|
954
955
|
}
|
|
956
|
+
/* close resp. implicitly account the open windows of the watched tasks whose assignee changed elsewhere */
|
|
955
957
|
async check() {
|
|
956
958
|
for (const task of Array.from(this.watched)) {
|
|
957
959
|
const w = this.openWindow(task);
|
|
@@ -1212,9 +1214,7 @@ export class UsageTracker {
|
|
|
1212
1214
|
(dropping the records of unregistered sessions), with the session registry
|
|
1213
1215
|
re-read from the telemetry state file only once the file changed */
|
|
1214
1216
|
route(records) {
|
|
1215
|
-
const bySession =
|
|
1216
|
-
for (const r of records)
|
|
1217
|
-
bySession.set(r.session, [...(bySession.get(r.session) ?? []), r]);
|
|
1217
|
+
const bySession = Map.groupBy(records, (r) => r.session);
|
|
1218
1218
|
let registry;
|
|
1219
1219
|
try {
|
|
1220
1220
|
const file = UsageTracker.stateFile();
|
|
@@ -187,7 +187,8 @@ export class Skills {
|
|
|
187
187
|
if (m === null)
|
|
188
188
|
return "N.A.";
|
|
189
189
|
const base = parseFloat(m[1].replace(/,/g, ""));
|
|
190
|
-
const
|
|
190
|
+
const unit = m[2].toUpperCase();
|
|
191
|
+
const mult = unit === "K" ? 1_000 : unit === "M" ? 1_000_000 : unit === "B" ? 1_000_000_000 : 1;
|
|
191
192
|
const n = Math.round(base * mult);
|
|
192
193
|
return Number.isFinite(n) ? n : "N.A.";
|
|
193
194
|
}
|
|
@@ -246,20 +247,9 @@ export class Skills {
|
|
|
246
247
|
const now = Date.now();
|
|
247
248
|
const msPerDay = 1000 * 60 * 60 * 24;
|
|
248
249
|
const halfLife = 365 / 2;
|
|
249
|
-
/*
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
remaining metrics, instead of collapsing the product to zero.
|
|
253
|
-
For `lifespan` (an unbounded duration) the multiplicative-neutral
|
|
254
|
-
`1` is also the low floor, so a missing value cannot unfairly
|
|
255
|
-
inflate the rank. For `recentness`, however, the value is an
|
|
256
|
-
exp-decay factor bounded in `(0,1]` where `1` is the *ceiling*
|
|
257
|
-
(a just-now update), not a floor -- defaulting a missing update
|
|
258
|
-
date to `1` would hand packages with absent metadata the highest
|
|
259
|
-
possible recentness and let them outrank genuinely fresh ones.
|
|
260
|
-
We therefore default a missing `recentness` to the decay value at
|
|
261
|
-
one half-life (`0.5`), a conservative midpoint that keeps the
|
|
262
|
-
entry rankable without rewarding the missing date. */
|
|
250
|
+
/* an unavailable date-derived factor gets a neutral default: a missing lifespan
|
|
251
|
+
its floor "1", a missing recentness (whose ceiling is "1") the decay value at
|
|
252
|
+
one half-life ("0.5"), so a missing date is never rewarded */
|
|
263
253
|
const lifespan = (!Number.isNaN(cMs) && !Number.isNaN(uMs)) ? Math.max(1, uMs - cMs) : 1;
|
|
264
254
|
const recentness = !Number.isNaN(uMs) ? 0.5 ** (Math.max(0, (now - uMs) / msPerDay) / halfLife) : 0.5;
|
|
265
255
|
let rank = d * s * lifespan * recentness;
|
package/dst/ase-service.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
8
|
-
import net from "node:net";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
10
9
|
import { spawn } from "node:child_process";
|
|
11
10
|
import Hapi from "@hapi/hapi";
|
|
@@ -13,7 +12,6 @@ import { ofetch } from "ofetch";
|
|
|
13
12
|
import { isMap } from "yaml";
|
|
14
13
|
import prettyMs from "pretty-ms";
|
|
15
14
|
import * as v from "valibot";
|
|
16
|
-
import { Tail } from "tail";
|
|
17
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
16
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
19
17
|
import { ConfigMCP } from "./ase-config.js";
|
|
@@ -21,6 +19,7 @@ import { Config } from "./ase-config-core.js";
|
|
|
21
19
|
import { configSchema } from "./ase-config-schema.js";
|
|
22
20
|
import { ensureAseGitignore } from "./ase-config-scope.js";
|
|
23
21
|
import { isLogLevel } from "./ase-lib-log.js";
|
|
22
|
+
import { Daemon } from "./ase-lib-daemon-cli.js";
|
|
24
23
|
import { CompatMCP } from "./ase-util-compat.js";
|
|
25
24
|
import { DiagramMCP } from "./ase-util-diagram.js";
|
|
26
25
|
import { Task, TaskMCP } from "./ase-task.js";
|
|
@@ -76,14 +75,7 @@ const PORT_ENV = "ASE_SERVICE_PORT";
|
|
|
76
75
|
const LEVEL_ENV = "ASE_SERVICE_LOG_LEVEL";
|
|
77
76
|
const IDLE_MS = 30 * 60 * 1000;
|
|
78
77
|
const TICK_MS = 60 * 1000;
|
|
79
|
-
|
|
80
|
-
const PORT_MAX = 44000;
|
|
81
|
-
const PORT_TRIES = 20;
|
|
82
|
-
/* bounds for the append-only ".ase/service.log" file:
|
|
83
|
-
maximum tolerated size, number of tail lines surviving a trim,
|
|
84
|
-
and maximum length of the MCP tool call arguments logged per request */
|
|
85
|
-
const LOG_MAX_SIZE = 1024 * 1024;
|
|
86
|
-
const LOG_KEEP_LINES = 2000;
|
|
78
|
+
/* maximum length of the MCP tool call arguments logged per request */
|
|
87
79
|
const LOG_ARGS_MAX = 200;
|
|
88
80
|
/* load the optional "config.yaml" and "service.yaml" files and derive
|
|
89
81
|
the service identity context (project id, port, preferred port, service config) */
|
|
@@ -105,33 +97,8 @@ export const loadServiceContext = (log) => {
|
|
|
105
97
|
/* return context information */
|
|
106
98
|
return { projectId, port, portPref, svc, aseDir };
|
|
107
99
|
};
|
|
108
|
-
/* reusable functionality: port
|
|
100
|
+
/* reusable functionality: port persistence and process spawning */
|
|
109
101
|
export class Service {
|
|
110
|
-
/* try binding a single candidate port to verify availability */
|
|
111
|
-
static tryBind(port) {
|
|
112
|
-
return new Promise((resolve) => {
|
|
113
|
-
const s = net.createServer();
|
|
114
|
-
s.once("error", () => {
|
|
115
|
-
resolve(false);
|
|
116
|
-
});
|
|
117
|
-
s.once("listening", () => {
|
|
118
|
-
s.close(() => resolve(true));
|
|
119
|
-
});
|
|
120
|
-
s.listen(port, HOST);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
/* allocate the preferred port, if given and available,
|
|
124
|
-
else a fresh random port in PORT_MIN..PORT_MAX */
|
|
125
|
-
static async allocatePort(pref = null) {
|
|
126
|
-
if (pref !== null && await Service.tryBind(pref))
|
|
127
|
-
return pref;
|
|
128
|
-
for (let i = 0; i < PORT_TRIES; i++) {
|
|
129
|
-
const p = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1));
|
|
130
|
-
if (await Service.tryBind(p))
|
|
131
|
-
return p;
|
|
132
|
-
}
|
|
133
|
-
throw new Error(`failed to allocate a port in ${PORT_MIN}..${PORT_MAX} after ${PORT_TRIES} attempts`);
|
|
134
|
-
}
|
|
135
102
|
/* persist an allocated port into ".ase/service.yaml" */
|
|
136
103
|
static persistPort(svc, port) {
|
|
137
104
|
ensureAseGitignore(path.dirname(svc.filename));
|
|
@@ -156,19 +123,6 @@ export class Service {
|
|
|
156
123
|
svc.write();
|
|
157
124
|
});
|
|
158
125
|
}
|
|
159
|
-
/* trim the log file down to its tail if it grew beyond the size limit */
|
|
160
|
-
static trimLog(logFile) {
|
|
161
|
-
try {
|
|
162
|
-
if (!fs.existsSync(logFile) || fs.statSync(logFile).size <= LOG_MAX_SIZE)
|
|
163
|
-
return;
|
|
164
|
-
const tail = Service.readLogTail(logFile, LOG_KEEP_LINES);
|
|
165
|
-
fs.writeFileSync(logFile, tail.length > 0 ? `${tail}\n` : "");
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
/* intentionally ignore all trimming errors, as a
|
|
169
|
-
non-trimmable log file must never block the service */
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
126
|
/* spawn the current executable detached as a background service */
|
|
173
127
|
static spawnDetached(aseDir, port, logLevel) {
|
|
174
128
|
fs.mkdirSync(aseDir, { recursive: true });
|
|
@@ -177,7 +131,7 @@ export class Service {
|
|
|
177
131
|
/* trim the log before handing it to the service, as the detached
|
|
178
132
|
service inherits the file descriptor for its entire lifetime and
|
|
179
133
|
hence cannot rotate the file itself while it is running */
|
|
180
|
-
|
|
134
|
+
Daemon.trimLog(logFile);
|
|
181
135
|
const fd = fs.openSync(logFile, "a");
|
|
182
136
|
const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
|
|
183
137
|
const child = spawn(process.execPath, [entry, "service", "start"], {
|
|
@@ -188,68 +142,6 @@ export class Service {
|
|
|
188
142
|
fs.closeSync(fd);
|
|
189
143
|
return { child, logFile };
|
|
190
144
|
}
|
|
191
|
-
/* read the last N non-empty lines of a log file for diagnostics */
|
|
192
|
-
static readLogTail(logFile, lines) {
|
|
193
|
-
let fd = null;
|
|
194
|
-
try {
|
|
195
|
-
fd = fs.openSync(logFile, "r");
|
|
196
|
-
const size = fs.fstatSync(fd).size;
|
|
197
|
-
const CHUNK = 8192;
|
|
198
|
-
const chunks = [];
|
|
199
|
-
let pos = size;
|
|
200
|
-
let count = 0;
|
|
201
|
-
while (pos > 0 && count <= lines) {
|
|
202
|
-
const n = Math.min(CHUNK, pos);
|
|
203
|
-
const buf = Buffer.alloc(n);
|
|
204
|
-
pos -= n;
|
|
205
|
-
fs.readSync(fd, buf, 0, n, pos);
|
|
206
|
-
chunks.unshift(buf);
|
|
207
|
-
for (let i = 0; i < n; i++)
|
|
208
|
-
if (buf[i] === 10)
|
|
209
|
-
count++;
|
|
210
|
-
}
|
|
211
|
-
/* decode once after re-assembly to avoid splitting
|
|
212
|
-
multi-byte UTF-8 sequences at chunk boundaries */
|
|
213
|
-
const all = Buffer.concat(chunks).toString("utf8")
|
|
214
|
-
.split("\n").filter((l) => l.length > 0);
|
|
215
|
-
return all.slice(-lines).join("\n");
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
return "";
|
|
219
|
-
}
|
|
220
|
-
finally {
|
|
221
|
-
if (fd !== null)
|
|
222
|
-
fs.closeSync(fd);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/* show a log file on stdout, either completely or its last <lines> lines only, and
|
|
226
|
-
optionally follow it for newly appended lines (default: the last 10 lines first),
|
|
227
|
-
where each line is passed through <transform> (e.g. to colorize parts of it) */
|
|
228
|
-
static async showLog(logFile, opts, transform = (line) => line) {
|
|
229
|
-
const lines = opts.lines !== undefined ? Number(opts.lines) : null;
|
|
230
|
-
if (lines !== null && (!Number.isInteger(lines) || lines < 1))
|
|
231
|
-
throw new Error(`invalid number of lines: ${opts.lines}`);
|
|
232
|
-
if (!fs.existsSync(logFile))
|
|
233
|
-
return 0;
|
|
234
|
-
if (opts.follow) {
|
|
235
|
-
/* follow until interrupted (on log trimming,
|
|
236
|
-
"tail" continues at the new end of the file) */
|
|
237
|
-
const tail = new Tail(logFile, { nLines: lines ?? 10 });
|
|
238
|
-
tail.on("line", (line) => {
|
|
239
|
-
process.stdout.write(`${transform(line)}\n`);
|
|
240
|
-
});
|
|
241
|
-
return await new Promise((_resolve, reject) => {
|
|
242
|
-
tail.on("error", (err) => {
|
|
243
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
const text = lines !== null ?
|
|
248
|
-
Service.readLogTail(logFile, lines) :
|
|
249
|
-
fs.readFileSync(logFile, "utf8").replace(/\n$/, "");
|
|
250
|
-
process.stdout.write(text.length > 0 ? `${text.split("\n").map(transform).join("\n")}\n` : "");
|
|
251
|
-
return 0;
|
|
252
|
-
}
|
|
253
145
|
}
|
|
254
146
|
/* MCP registration entry point for service-identity tools */
|
|
255
147
|
export class ServiceMCP {
|
|
@@ -332,6 +224,68 @@ export default class ServiceCommand {
|
|
|
332
224
|
}
|
|
333
225
|
return { method: bMethod, info };
|
|
334
226
|
}
|
|
227
|
+
/* register the endpoints of the AI usage accounting: the usage records forwarded
|
|
228
|
+
by the OTLP ingress, the session end, the task assignment on behalf of a session,
|
|
229
|
+
and the task rename */
|
|
230
|
+
static registerUsageRoutes(server, usage) {
|
|
231
|
+
server.route({
|
|
232
|
+
method: "POST",
|
|
233
|
+
path: "/usage/records",
|
|
234
|
+
options: { payload: { parse: true, allow: "application/json", maxBytes: 32 * 1024 * 1024 } },
|
|
235
|
+
handler: (request, h) => {
|
|
236
|
+
const payload = request.payload;
|
|
237
|
+
if (!Array.isArray(payload?.records))
|
|
238
|
+
return h.response({ error: "missing or invalid 'records' field" }).code(400);
|
|
239
|
+
usage.ingest(payload.records);
|
|
240
|
+
return h.response({ ok: true }).code(200);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
server.route({
|
|
244
|
+
method: "POST",
|
|
245
|
+
path: "/usage/detach",
|
|
246
|
+
options: { payload: { parse: true, allow: "application/json" } },
|
|
247
|
+
handler: (request, h) => {
|
|
248
|
+
const payload = request.payload;
|
|
249
|
+
if (typeof payload?.session !== "string")
|
|
250
|
+
return h.response({ error: "missing or invalid 'session' field" }).code(400);
|
|
251
|
+
usage.detach(payload.session);
|
|
252
|
+
return h.response({ ok: true }).code(200);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
server.route({
|
|
256
|
+
method: "POST",
|
|
257
|
+
path: "/usage/rename",
|
|
258
|
+
options: { payload: { parse: true, allow: "application/json" } },
|
|
259
|
+
handler: (request, h) => {
|
|
260
|
+
const payload = request.payload;
|
|
261
|
+
if (typeof payload?.old !== "string" || typeof payload.new !== "string")
|
|
262
|
+
return h.response({ error: "missing or invalid 'old' or 'new' field" }).code(400);
|
|
263
|
+
try {
|
|
264
|
+
usage.rename(payload.old, payload.new);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
|
|
268
|
+
}
|
|
269
|
+
return h.response({ ok: true }).code(200);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
server.route({
|
|
273
|
+
method: "POST",
|
|
274
|
+
path: "/usage/assign",
|
|
275
|
+
options: { payload: { parse: true, allow: "application/json" } },
|
|
276
|
+
handler: async (request, h) => {
|
|
277
|
+
const payload = request.payload;
|
|
278
|
+
if (typeof payload?.id !== "string" || typeof payload.assignee !== "string" || typeof payload.session !== "string")
|
|
279
|
+
return h.response({ error: "missing or invalid 'id', 'assignee', or 'session' field" }).code(400);
|
|
280
|
+
try {
|
|
281
|
+
return h.response(await usage.assign(payload.id, payload.assignee, payload.session)).code(200);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
335
289
|
/* service-side: bind HAPI server until "/stop" command is received or idle timeout happens
|
|
336
290
|
(or, in the foreground, until "/stop" command is received or a termination signal arrives) */
|
|
337
291
|
async runService(ctx, foreground = false) {
|
|
@@ -361,6 +315,18 @@ export default class ServiceCommand {
|
|
|
361
315
|
const usage = new UsageTracker(this.log, Task.projectRoot(), ctx.projectId, ctx.aseDir, () => {
|
|
362
316
|
lastActivity = Date.now();
|
|
363
317
|
});
|
|
318
|
+
/* stop the HTTP server and the usage tracker, clear the persisted port, and exit */
|
|
319
|
+
const stopService = async () => {
|
|
320
|
+
try {
|
|
321
|
+
await server.stop({ timeout: 1000 });
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
this.log.write("error", `service: stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
325
|
+
}
|
|
326
|
+
await usage.stop();
|
|
327
|
+
Service.clearPort(ctx.svc);
|
|
328
|
+
process.exit(0);
|
|
329
|
+
};
|
|
364
330
|
/* listen to HTTP/REST endpoints */
|
|
365
331
|
server.route({
|
|
366
332
|
method: "OPTIONS",
|
|
@@ -379,17 +345,7 @@ export default class ServiceCommand {
|
|
|
379
345
|
if (!ct.startsWith("application/json"))
|
|
380
346
|
return h.response({ error: "unsupported media type" }).code(415);
|
|
381
347
|
this.log.write("info", "service: stop requested");
|
|
382
|
-
setImmediate(
|
|
383
|
-
try {
|
|
384
|
-
await server.stop({ timeout: 1000 });
|
|
385
|
-
}
|
|
386
|
-
catch (err) {
|
|
387
|
-
const e = err;
|
|
388
|
-
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
389
|
-
}
|
|
390
|
-
await usage.stop();
|
|
391
|
-
process.exit(0);
|
|
392
|
-
});
|
|
348
|
+
setImmediate(stopService);
|
|
393
349
|
return h.response({ ok: true }).code(200);
|
|
394
350
|
}
|
|
395
351
|
});
|
|
@@ -456,66 +412,8 @@ export default class ServiceCommand {
|
|
|
456
412
|
return h.response({ error: "unknown command", command: cmd }).code(400);
|
|
457
413
|
}
|
|
458
414
|
});
|
|
459
|
-
/* listen to the endpoints of the AI usage accounting
|
|
460
|
-
|
|
461
|
-
and the task rename */
|
|
462
|
-
server.route({
|
|
463
|
-
method: "POST",
|
|
464
|
-
path: "/usage/records",
|
|
465
|
-
options: { payload: { parse: true, allow: "application/json", maxBytes: 32 * 1024 * 1024 } },
|
|
466
|
-
handler: (request, h) => {
|
|
467
|
-
const payload = request.payload;
|
|
468
|
-
if (!Array.isArray(payload?.records))
|
|
469
|
-
return h.response({ error: "missing or invalid 'records' field" }).code(400);
|
|
470
|
-
usage.ingest(payload.records);
|
|
471
|
-
return h.response({ ok: true }).code(200);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
server.route({
|
|
475
|
-
method: "POST",
|
|
476
|
-
path: "/usage/detach",
|
|
477
|
-
options: { payload: { parse: true, allow: "application/json" } },
|
|
478
|
-
handler: (request, h) => {
|
|
479
|
-
const payload = request.payload;
|
|
480
|
-
if (typeof payload?.session !== "string")
|
|
481
|
-
return h.response({ error: "missing or invalid 'session' field" }).code(400);
|
|
482
|
-
usage.detach(payload.session);
|
|
483
|
-
return h.response({ ok: true }).code(200);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
server.route({
|
|
487
|
-
method: "POST",
|
|
488
|
-
path: "/usage/rename",
|
|
489
|
-
options: { payload: { parse: true, allow: "application/json" } },
|
|
490
|
-
handler: (request, h) => {
|
|
491
|
-
const payload = request.payload;
|
|
492
|
-
if (typeof payload?.old !== "string" || typeof payload.new !== "string")
|
|
493
|
-
return h.response({ error: "missing or invalid 'old' or 'new' field" }).code(400);
|
|
494
|
-
try {
|
|
495
|
-
usage.rename(payload.old, payload.new);
|
|
496
|
-
}
|
|
497
|
-
catch (err) {
|
|
498
|
-
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
|
|
499
|
-
}
|
|
500
|
-
return h.response({ ok: true }).code(200);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
server.route({
|
|
504
|
-
method: "POST",
|
|
505
|
-
path: "/usage/assign",
|
|
506
|
-
options: { payload: { parse: true, allow: "application/json" } },
|
|
507
|
-
handler: async (request, h) => {
|
|
508
|
-
const payload = request.payload;
|
|
509
|
-
if (typeof payload?.id !== "string" || typeof payload.assignee !== "string" || typeof payload.session !== "string")
|
|
510
|
-
return h.response({ error: "missing or invalid 'id', 'assignee', or 'session' field" }).code(400);
|
|
511
|
-
try {
|
|
512
|
-
return h.response(await usage.assign(payload.id, payload.assignee, payload.session)).code(200);
|
|
513
|
-
}
|
|
514
|
-
catch (err) {
|
|
515
|
-
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
});
|
|
415
|
+
/* listen to the endpoints of the AI usage accounting */
|
|
416
|
+
ServiceCommand.registerUsageRoutes(server, usage);
|
|
519
417
|
/* serve the web board of the project (imported dynamically to keep it out of all
|
|
520
418
|
other CLI commands; its rendering dependencies marked, MRCS, and ELK load on
|
|
521
419
|
first request only, its board core dependencies at service startup) */
|
|
@@ -549,16 +447,7 @@ export default class ServiceCommand {
|
|
|
549
447
|
return;
|
|
550
448
|
stopping = true;
|
|
551
449
|
this.log.write("info", `service: received ${signal}, stopping`);
|
|
552
|
-
|
|
553
|
-
await server.stop({ timeout: 1000 });
|
|
554
|
-
}
|
|
555
|
-
catch (err) {
|
|
556
|
-
const e = err;
|
|
557
|
-
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
558
|
-
}
|
|
559
|
-
await usage.stop();
|
|
560
|
-
Service.clearPort(ctx.svc);
|
|
561
|
-
process.exit(0);
|
|
450
|
+
await stopService();
|
|
562
451
|
};
|
|
563
452
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
564
453
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
@@ -578,25 +467,13 @@ export default class ServiceCommand {
|
|
|
578
467
|
if (Date.now() - lastActivity > IDLE_MS) {
|
|
579
468
|
stopping = true;
|
|
580
469
|
this.log.write("info", "service: idle timeout reached, stopping");
|
|
581
|
-
|
|
582
|
-
await server.stop({ timeout: 1000 });
|
|
583
|
-
await usage.stop();
|
|
584
|
-
Service.clearPort(ctx.svc);
|
|
585
|
-
process.exit(0);
|
|
586
|
-
}
|
|
587
|
-
catch (err) {
|
|
588
|
-
const e = err;
|
|
589
|
-
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
590
|
-
await usage.stop();
|
|
591
|
-
Service.clearPort(ctx.svc);
|
|
592
|
-
process.exit(1);
|
|
593
|
-
}
|
|
470
|
+
await stopService();
|
|
594
471
|
}
|
|
595
472
|
}, TICK_MS).unref();
|
|
596
473
|
}
|
|
597
474
|
/* allocate the service port, preferring the configured one, else falling back to a random one */
|
|
598
475
|
async allocatePort(ctx) {
|
|
599
|
-
const port = await
|
|
476
|
+
const port = await Daemon.allocatePort(ctx.portPref);
|
|
600
477
|
if (ctx.portPref !== null && port !== ctx.portPref)
|
|
601
478
|
this.log.write("warning", `service: preferred port ${ctx.portPref} unavailable, falling back to port ${port}`);
|
|
602
479
|
return port;
|
|
@@ -612,7 +489,7 @@ export default class ServiceCommand {
|
|
|
612
489
|
if (level !== undefined && isLogLevel(level))
|
|
613
490
|
this.log.logLevel(level);
|
|
614
491
|
const raw = process.env[PORT_ENV];
|
|
615
|
-
port = raw !== undefined ? Number(raw) : await
|
|
492
|
+
port = raw !== undefined ? Number(raw) : await Daemon.allocatePort();
|
|
616
493
|
await this.runService({ ...ctx, port });
|
|
617
494
|
return await new Promise(() => { });
|
|
618
495
|
}
|
|
@@ -668,7 +545,7 @@ export default class ServiceCommand {
|
|
|
668
545
|
break;
|
|
669
546
|
}
|
|
670
547
|
}
|
|
671
|
-
const tail =
|
|
548
|
+
const tail = Daemon.readLogTail(logFile, 20);
|
|
672
549
|
const reason = exited ?
|
|
673
550
|
`service exited during startup (code ${exitCode})` :
|
|
674
551
|
foreign ?
|
|
@@ -755,7 +632,7 @@ export default class ServiceCommand {
|
|
|
755
632
|
/* log flow: show the (tail of the) log file and optionally follow it */
|
|
756
633
|
async doLog(opts) {
|
|
757
634
|
const ctx = this.loadContext();
|
|
758
|
-
return
|
|
635
|
+
return Daemon.showLog(path.join(ctx.aseDir, "service.log"), opts);
|
|
759
636
|
}
|
|
760
637
|
/* stop flow: no-op if no port configured or connection refused */
|
|
761
638
|
async doStop() {
|
package/dst/ase-setup-common.js
CHANGED
|
@@ -51,8 +51,7 @@ export class SetupRunner {
|
|
|
51
51
|
if (process.platform === "win32")
|
|
52
52
|
return false;
|
|
53
53
|
/* already running as root */
|
|
54
|
-
|
|
55
|
-
if (typeof getuid === "function" && getuid.call(process) === 0)
|
|
54
|
+
if (process.getuid?.() === 0)
|
|
56
55
|
return false;
|
|
57
56
|
/* determine the npm global prefix and probe writability of the
|
|
58
57
|
directories that "npm -g" actually mutates */
|