@rse/ase 1.0.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +357 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +228 -111
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +8 -1
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +269 -81
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +231 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +81 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
package/dst/ase-service.js
CHANGED
|
@@ -13,24 +13,30 @@ import { ofetch } from "ofetch";
|
|
|
13
13
|
import { isMap } from "yaml";
|
|
14
14
|
import prettyMs from "pretty-ms";
|
|
15
15
|
import * as v from "valibot";
|
|
16
|
+
import { Tail } from "tail";
|
|
16
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
18
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
19
|
+
import { ConfigMCP } from "./ase-config.js";
|
|
20
|
+
import { Config } from "./ase-config-core.js";
|
|
21
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
22
|
+
import { ensureAseGitignore } from "./ase-config-scope.js";
|
|
23
|
+
import { isLogLevel } from "./ase-lib-log.js";
|
|
24
|
+
import { CompatMCP } from "./ase-util-compat.js";
|
|
25
|
+
import { DiagramMCP } from "./ase-util-diagram.js";
|
|
26
|
+
import { Task, TaskMCP } from "./ase-task.js";
|
|
27
|
+
import { MarkdownMCP } from "./ase-service-markdown.js";
|
|
24
28
|
import { ArtifactMCP } from "./ase-artifact.js";
|
|
25
29
|
import { SpecMCP } from "./ase-spec.js";
|
|
26
|
-
import { KVMCP } from "./ase-kv.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
30
|
+
import { KVMCP } from "./ase-service-kv.js";
|
|
31
|
+
import { StateMCP } from "./ase-service-state.js";
|
|
32
|
+
import { TimestampMCP } from "./ase-service-timestamp.js";
|
|
33
|
+
import { SleepMCP } from "./ase-service-sleep.js";
|
|
34
|
+
import { GetoptMCP } from "./ase-service-getopt.js";
|
|
35
|
+
import { SkillsMCP } from "./ase-service-skills.js";
|
|
36
|
+
import { WorktreeMCP } from "./ase-util-worktree.js";
|
|
37
|
+
import { MintMCP } from "./ase-util-mint.js";
|
|
38
|
+
import { MetricMCP } from "./ase-util-metric.js";
|
|
39
|
+
import { UsageTracker } from "./ase-service-otel.js";
|
|
34
40
|
import pkg from "../package.json" with { type: "json" };
|
|
35
41
|
/* shared service host */
|
|
36
42
|
export const SERVICE_HOST = "127.0.0.1";
|
|
@@ -80,7 +86,7 @@ const LOG_MAX_SIZE = 1024 * 1024;
|
|
|
80
86
|
const LOG_KEEP_LINES = 2000;
|
|
81
87
|
const LOG_ARGS_MAX = 200;
|
|
82
88
|
/* load the optional "config.yaml" and "service.yaml" files and derive
|
|
83
|
-
the service identity context (project id, port, service config) */
|
|
89
|
+
the service identity context (project id, port, preferred port, service config) */
|
|
84
90
|
export const loadServiceContext = (log) => {
|
|
85
91
|
/* load files */
|
|
86
92
|
const cfg = new Config("config", configSchema, log);
|
|
@@ -88,13 +94,16 @@ export const loadServiceContext = (log) => {
|
|
|
88
94
|
const svc = new Config("service", serviceSchema, log);
|
|
89
95
|
svc.read();
|
|
90
96
|
/* determine project id */
|
|
91
|
-
const projectId = cfg.get("project.id")
|
|
97
|
+
const projectId = cfg.get("project.id") || Task.projectIdOf(Task.projectRoot());
|
|
92
98
|
/* determine service port */
|
|
93
99
|
const port = svc.get("port") ?? null;
|
|
100
|
+
/* determine preferred service port */
|
|
101
|
+
const pref = cfg.get("project.service.port");
|
|
102
|
+
const portPref = pref !== null && pref !== undefined ? Number(pref) : null;
|
|
94
103
|
/* determine path to ".ase" directory */
|
|
95
104
|
const aseDir = path.dirname(svc.filename);
|
|
96
105
|
/* return context information */
|
|
97
|
-
return { projectId, port, svc, aseDir };
|
|
106
|
+
return { projectId, port, portPref, svc, aseDir };
|
|
98
107
|
};
|
|
99
108
|
/* reusable functionality: port allocation, persistence, and process spawning */
|
|
100
109
|
export class Service {
|
|
@@ -111,8 +120,11 @@ export class Service {
|
|
|
111
120
|
s.listen(port, HOST);
|
|
112
121
|
});
|
|
113
122
|
}
|
|
114
|
-
/* allocate
|
|
115
|
-
|
|
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;
|
|
116
128
|
for (let i = 0; i < PORT_TRIES; i++) {
|
|
117
129
|
const p = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1));
|
|
118
130
|
if (await Service.tryBind(p))
|
|
@@ -122,6 +134,7 @@ export class Service {
|
|
|
122
134
|
}
|
|
123
135
|
/* persist an allocated port into ".ase/service.yaml" */
|
|
124
136
|
static persistPort(svc, port) {
|
|
137
|
+
ensureAseGitignore(path.dirname(svc.filename));
|
|
125
138
|
svc.lock(() => {
|
|
126
139
|
svc.read();
|
|
127
140
|
svc.set("port", port);
|
|
@@ -159,6 +172,7 @@ export class Service {
|
|
|
159
172
|
/* spawn the current executable detached as a background service */
|
|
160
173
|
static spawnDetached(aseDir, port, logLevel) {
|
|
161
174
|
fs.mkdirSync(aseDir, { recursive: true });
|
|
175
|
+
ensureAseGitignore(aseDir);
|
|
162
176
|
const logFile = path.join(aseDir, "service.log");
|
|
163
177
|
/* trim the log before handing it to the service, as the detached
|
|
164
178
|
service inherits the file descriptor for its entire lifetime and
|
|
@@ -208,6 +222,34 @@ export class Service {
|
|
|
208
222
|
fs.closeSync(fd);
|
|
209
223
|
}
|
|
210
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
|
+
}
|
|
211
253
|
}
|
|
212
254
|
/* MCP registration entry point for service-identity tools */
|
|
213
255
|
export class ServiceMCP {
|
|
@@ -243,8 +285,56 @@ export default class ServiceCommand {
|
|
|
243
285
|
loadContext() {
|
|
244
286
|
return loadServiceContext(this.log);
|
|
245
287
|
}
|
|
246
|
-
/*
|
|
247
|
-
|
|
288
|
+
/* build a fresh MCP server instance with all registered tools
|
|
289
|
+
(also used by the "ase mcp" bridge for answering the MCP handshake
|
|
290
|
+
and tool listing in-process before the service is started) */
|
|
291
|
+
static buildMcpServer(log, ctx, startTime, usage) {
|
|
292
|
+
const mcp = new McpServer({ name: "ase", version: pkg.version });
|
|
293
|
+
new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
|
|
294
|
+
new CompatMCP().register(mcp);
|
|
295
|
+
new DiagramMCP().register(mcp);
|
|
296
|
+
new TaskMCP(log, usage).register(mcp);
|
|
297
|
+
new MarkdownMCP().register(mcp);
|
|
298
|
+
new ArtifactMCP(log).register(mcp);
|
|
299
|
+
new SpecMCP(log).register(mcp);
|
|
300
|
+
new KVMCP().register(mcp);
|
|
301
|
+
new TimestampMCP().register(mcp);
|
|
302
|
+
new SleepMCP().register(mcp);
|
|
303
|
+
new GetoptMCP().register(mcp);
|
|
304
|
+
new SkillsMCP().register(mcp);
|
|
305
|
+
new WorktreeMCP().register(mcp);
|
|
306
|
+
new MintMCP().register(mcp);
|
|
307
|
+
new MetricMCP().register(mcp);
|
|
308
|
+
new ConfigMCP(log).register(mcp);
|
|
309
|
+
new StateMCP(log).register(mcp);
|
|
310
|
+
return mcp;
|
|
311
|
+
}
|
|
312
|
+
/* summarize the method, tool name, and capped tool arguments of an MCP request body */
|
|
313
|
+
static mcpBodyInfo(body) {
|
|
314
|
+
const b = body;
|
|
315
|
+
const bParams = b?.params;
|
|
316
|
+
const bMethod = typeof b?.method === "string" ? b.method : null;
|
|
317
|
+
const bName = typeof bParams?.name === "string" ? bParams.name : null;
|
|
318
|
+
const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
|
|
319
|
+
let info = "";
|
|
320
|
+
if (bMethod !== null) {
|
|
321
|
+
info = ` [${bMethod}]`;
|
|
322
|
+
if (bName !== null) {
|
|
323
|
+
info += ` ${bName}`;
|
|
324
|
+
if (bArgs !== null) {
|
|
325
|
+
/* cap the arguments, as payload-carrying tool calls
|
|
326
|
+
(task plans, key/value batches, etc) would
|
|
327
|
+
otherwise dominate the entire log file */
|
|
328
|
+
const args = JSON.stringify(bArgs);
|
|
329
|
+
info += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { method: bMethod, info };
|
|
334
|
+
}
|
|
335
|
+
/* service-side: bind HAPI server until "/stop" command is received or idle timeout happens
|
|
336
|
+
(or, in the foreground, until "/stop" command is received or a termination signal arrives) */
|
|
337
|
+
async runService(ctx, foreground = false) {
|
|
248
338
|
/* establish HAPI HTTP/REST service */
|
|
249
339
|
const server = Hapi.server({ host: HOST, port: ctx.port });
|
|
250
340
|
/* track start time and last activity */
|
|
@@ -252,9 +342,14 @@ export default class ServiceCommand {
|
|
|
252
342
|
let lastActivity = Date.now();
|
|
253
343
|
let inFlight = 0;
|
|
254
344
|
let stopping = false;
|
|
255
|
-
|
|
345
|
+
/* accept only loopback "Host" headers, to defeat DNS rebinding attacks from browser pages */
|
|
346
|
+
const allowedHosts = new Set([`${HOST}:${ctx.port}`, `localhost:${ctx.port}`]);
|
|
347
|
+
server.ext("onRequest", (request, h) => {
|
|
256
348
|
inFlight++;
|
|
257
349
|
lastActivity = Date.now();
|
|
350
|
+
const host = (request.headers.host ?? "").toLowerCase();
|
|
351
|
+
if (!allowedHosts.has(host))
|
|
352
|
+
return h.response({ error: "invalid Host header" }).code(421).takeover();
|
|
258
353
|
return h.continue;
|
|
259
354
|
});
|
|
260
355
|
server.ext("onPreResponse", (_request, h) => {
|
|
@@ -262,27 +357,10 @@ export default class ServiceCommand {
|
|
|
262
357
|
lastActivity = Date.now();
|
|
263
358
|
return h.continue;
|
|
264
359
|
});
|
|
265
|
-
/*
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
new CompatMCP().register(mcp);
|
|
270
|
-
new DiagramMCP().register(mcp);
|
|
271
|
-
new TaskMCP(this.log).register(mcp);
|
|
272
|
-
new MarkdownMCP().register(mcp);
|
|
273
|
-
new ArtifactMCP(this.log).register(mcp);
|
|
274
|
-
new SpecMCP(this.log).register(mcp);
|
|
275
|
-
new KVMCP().register(mcp);
|
|
276
|
-
new TimestampMCP().register(mcp);
|
|
277
|
-
new SleepMCP().register(mcp);
|
|
278
|
-
new GetoptMCP().register(mcp);
|
|
279
|
-
new SkillsMCP().register(mcp);
|
|
280
|
-
new WorktreeMCP().register(mcp);
|
|
281
|
-
new MintMCP().register(mcp);
|
|
282
|
-
new MetricMCP().register(mcp);
|
|
283
|
-
new ConfigMCP(this.log).register(mcp);
|
|
284
|
-
return mcp;
|
|
285
|
-
};
|
|
360
|
+
/* establish the usage tracker of the AI usage accounting (and the OTLP ingress) */
|
|
361
|
+
const usage = new UsageTracker(this.log, Task.projectRoot(), ctx.projectId, ctx.aseDir, () => {
|
|
362
|
+
lastActivity = Date.now();
|
|
363
|
+
});
|
|
286
364
|
/* listen to HTTP/REST endpoints */
|
|
287
365
|
server.route({
|
|
288
366
|
method: "OPTIONS",
|
|
@@ -292,9 +370,14 @@ export default class ServiceCommand {
|
|
|
292
370
|
}
|
|
293
371
|
});
|
|
294
372
|
server.route({
|
|
295
|
-
method: "
|
|
373
|
+
method: "POST",
|
|
296
374
|
path: "/stop",
|
|
297
|
-
handler: (
|
|
375
|
+
handler: (request, h) => {
|
|
376
|
+
/* require a JSON content type, as browsers cannot send it cross-origin
|
|
377
|
+
without a CORS preflight (which this service never grants) */
|
|
378
|
+
const ct = (request.headers["content-type"] ?? "").toLowerCase();
|
|
379
|
+
if (!ct.startsWith("application/json"))
|
|
380
|
+
return h.response({ error: "unsupported media type" }).code(415);
|
|
298
381
|
this.log.write("info", "service: stop requested");
|
|
299
382
|
setImmediate(async () => {
|
|
300
383
|
try {
|
|
@@ -304,38 +387,21 @@ export default class ServiceCommand {
|
|
|
304
387
|
const e = err;
|
|
305
388
|
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
306
389
|
}
|
|
390
|
+
await usage.stop();
|
|
307
391
|
process.exit(0);
|
|
308
392
|
});
|
|
309
393
|
return h.response({ ok: true }).code(200);
|
|
310
394
|
}
|
|
311
395
|
});
|
|
312
396
|
const mcpHandler = async (request, h, body) => {
|
|
313
|
-
const
|
|
314
|
-
const bParams = b?.params;
|
|
315
|
-
const bMethod = typeof b?.method === "string" ? b.method : null;
|
|
316
|
-
const bName = typeof bParams?.name === "string" ? bParams.name : null;
|
|
317
|
-
const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
|
|
318
|
-
let bodyInfo = "";
|
|
319
|
-
if (bMethod !== null) {
|
|
320
|
-
bodyInfo = ` [${bMethod}]`;
|
|
321
|
-
if (bName !== null) {
|
|
322
|
-
bodyInfo += ` ${bName}`;
|
|
323
|
-
if (bArgs !== null) {
|
|
324
|
-
/* cap the arguments, as payload-carrying tool calls
|
|
325
|
-
(task plans, key/value batches, etc) would
|
|
326
|
-
otherwise dominate the entire log file */
|
|
327
|
-
const args = JSON.stringify(bArgs);
|
|
328
|
-
bodyInfo += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
397
|
+
const { method: bMethod, info: bodyInfo } = ServiceCommand.mcpBodyInfo(body);
|
|
332
398
|
/* log tool calls regularly, but all remaining MCP traffic
|
|
333
399
|
(session handshakes, notifications, SSE stream opens) at
|
|
334
400
|
debug level only, as it carries no diagnostic value */
|
|
335
401
|
const level = bMethod === "tools/call" ? "info" : "debug";
|
|
336
402
|
this.log.write(level, `mcp: ${request.method.toUpperCase()} ${request.path}${bodyInfo}`);
|
|
337
403
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
338
|
-
const mcp = buildMcpServer();
|
|
404
|
+
const mcp = ServiceCommand.buildMcpServer(this.log, ctx, startTime, usage);
|
|
339
405
|
request.raw.res.on("close", () => {
|
|
340
406
|
/* "h.abandon" (see below) bypasses "onPreResponse",
|
|
341
407
|
so undo the "onRequest" accounting here instead */
|
|
@@ -390,11 +456,77 @@ export default class ServiceCommand {
|
|
|
390
456
|
return h.response({ error: "unknown command", command: cmd }).code(400);
|
|
391
457
|
}
|
|
392
458
|
});
|
|
459
|
+
/* listen to the endpoints of the AI usage accounting: the usage records forwarded
|
|
460
|
+
by the OTLP ingress, the session end, the task assignment on behalf of a session,
|
|
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
|
+
});
|
|
519
|
+
/* serve the web board of the project (imported dynamically to keep it out of all
|
|
520
|
+
other CLI commands; its rendering dependencies marked, MRCS, and ELK load on
|
|
521
|
+
first request only, its board core dependencies at service startup) */
|
|
522
|
+
const { registerBoardRoutes } = await import("./ase-task-board-web-server.js");
|
|
523
|
+
registerBoardRoutes(server, this.log);
|
|
393
524
|
/* start service */
|
|
394
525
|
try {
|
|
395
526
|
await server.start();
|
|
396
527
|
Service.persistPort(ctx.svc, ctx.port);
|
|
397
528
|
this.log.write("info", `service: listening on port ${ctx.port}`);
|
|
529
|
+
usage.start();
|
|
398
530
|
}
|
|
399
531
|
catch (err) {
|
|
400
532
|
const e = err;
|
|
@@ -410,31 +542,67 @@ export default class ServiceCommand {
|
|
|
410
542
|
this.log.write("error", `service: ${e.message}`);
|
|
411
543
|
process.exit(1);
|
|
412
544
|
}
|
|
545
|
+
/* stop service in the foreground on termination signal */
|
|
546
|
+
if (foreground) {
|
|
547
|
+
const shutdown = async (signal) => {
|
|
548
|
+
if (stopping)
|
|
549
|
+
return;
|
|
550
|
+
stopping = true;
|
|
551
|
+
this.log.write("info", `service: received ${signal}, stopping`);
|
|
552
|
+
try {
|
|
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);
|
|
562
|
+
};
|
|
563
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
564
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
565
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
566
|
+
process.on("SIGQUIT", () => shutdown("SIGQUIT"));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
413
569
|
/* stop service after idle timeout */
|
|
414
570
|
setInterval(async () => {
|
|
415
571
|
if (stopping)
|
|
416
572
|
return;
|
|
417
573
|
if (inFlight > 0)
|
|
418
574
|
return;
|
|
575
|
+
/* never idle out while holding usage windows, as their sessions keep on reporting */
|
|
576
|
+
if (usage.active())
|
|
577
|
+
return;
|
|
419
578
|
if (Date.now() - lastActivity > IDLE_MS) {
|
|
420
579
|
stopping = true;
|
|
421
580
|
this.log.write("info", "service: idle timeout reached, stopping");
|
|
422
581
|
try {
|
|
423
582
|
await server.stop({ timeout: 1000 });
|
|
583
|
+
await usage.stop();
|
|
424
584
|
Service.clearPort(ctx.svc);
|
|
425
585
|
process.exit(0);
|
|
426
586
|
}
|
|
427
587
|
catch (err) {
|
|
428
588
|
const e = err;
|
|
429
589
|
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
590
|
+
await usage.stop();
|
|
430
591
|
Service.clearPort(ctx.svc);
|
|
431
592
|
process.exit(1);
|
|
432
593
|
}
|
|
433
594
|
}
|
|
434
595
|
}, TICK_MS).unref();
|
|
435
596
|
}
|
|
436
|
-
/*
|
|
437
|
-
async
|
|
597
|
+
/* allocate the service port, preferring the configured one, else falling back to a random one */
|
|
598
|
+
async allocatePort(ctx) {
|
|
599
|
+
const port = await Service.allocatePort(ctx.portPref);
|
|
600
|
+
if (ctx.portPref !== null && port !== ctx.portPref)
|
|
601
|
+
this.log.write("warning", `service: preferred port ${ctx.portPref} unavailable, falling back to port ${port}`);
|
|
602
|
+
return port;
|
|
603
|
+
}
|
|
604
|
+
/* start flow: ensure port, probe, optionally detach (or run in the foreground) */
|
|
605
|
+
async doStart(opts = {}) {
|
|
438
606
|
const ctx = this.loadContext();
|
|
439
607
|
let port = ctx.port;
|
|
440
608
|
if (process.env[SERVE_ENV] === "1") {
|
|
@@ -451,15 +619,23 @@ export default class ServiceCommand {
|
|
|
451
619
|
if (port !== null) {
|
|
452
620
|
const match = await probe(port, ctx.projectId).catch(() => null);
|
|
453
621
|
if (match === true) {
|
|
622
|
+
if (opts.foreground)
|
|
623
|
+
throw new Error(`service: already running on port ${port} (stop it first with "ase service stop")`);
|
|
454
624
|
this.log.write("info", `service: already running on port ${port}`);
|
|
455
625
|
return 0;
|
|
456
626
|
}
|
|
457
627
|
}
|
|
628
|
+
/* run the service in the foreground, logging to the terminal and without idle timeout */
|
|
629
|
+
if (opts.foreground) {
|
|
630
|
+
port = await this.allocatePort(ctx);
|
|
631
|
+
await this.runService({ ...ctx, port }, true);
|
|
632
|
+
return await new Promise(() => { });
|
|
633
|
+
}
|
|
458
634
|
/* bounded retry across the bind/start TOCTOU window: on each attempt
|
|
459
635
|
re-allocate, re-spawn; early-break on foreign listener */
|
|
460
636
|
let lastErr = new Error("service failed to start within timeout");
|
|
461
637
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
462
|
-
port = await
|
|
638
|
+
port = await this.allocatePort(ctx);
|
|
463
639
|
const { child, logFile } = Service.spawnDetached(ctx.aseDir, port, this.log.logLevel());
|
|
464
640
|
let exited = false;
|
|
465
641
|
let exitCode = null;
|
|
@@ -524,12 +700,12 @@ export default class ServiceCommand {
|
|
|
524
700
|
}
|
|
525
701
|
throw lastErr;
|
|
526
702
|
}
|
|
527
|
-
/* build the "POST
|
|
528
|
-
|
|
703
|
+
/* build the JSON "POST" request options for a service endpoint */
|
|
704
|
+
postRequest(body, timeoutMs) {
|
|
529
705
|
return {
|
|
530
706
|
method: "POST",
|
|
531
707
|
headers: { "Content-Type": "application/json" },
|
|
532
|
-
body
|
|
708
|
+
body,
|
|
533
709
|
signal: AbortSignal.timeout(timeoutMs),
|
|
534
710
|
ignoreResponseError: true
|
|
535
711
|
};
|
|
@@ -543,7 +719,7 @@ export default class ServiceCommand {
|
|
|
543
719
|
}
|
|
544
720
|
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
545
721
|
if (match === true) {
|
|
546
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.
|
|
722
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.postRequest({ command: "status" }, 2000));
|
|
547
723
|
const d = r._data;
|
|
548
724
|
const uptimeMs = typeof d?.uptimeMs === "number" ? d.uptimeMs : 0;
|
|
549
725
|
const uptime = prettyMs(uptimeMs, { verbose: true });
|
|
@@ -569,13 +745,18 @@ export default class ServiceCommand {
|
|
|
569
745
|
if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
|
|
570
746
|
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
571
747
|
}
|
|
572
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.
|
|
748
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.postRequest({ command: cmd }, 5000), responseType: "text" });
|
|
573
749
|
const body = typeof r._data === "string" ? r._data : JSON.stringify(r._data);
|
|
574
750
|
process.stdout.write(body);
|
|
575
751
|
if (!body.endsWith("\n"))
|
|
576
752
|
process.stdout.write("\n");
|
|
577
753
|
return r.status >= 200 && r.status < 300 ? 0 : 1;
|
|
578
754
|
}
|
|
755
|
+
/* log flow: show the (tail of the) log file and optionally follow it */
|
|
756
|
+
async doLog(opts) {
|
|
757
|
+
const ctx = this.loadContext();
|
|
758
|
+
return Service.showLog(path.join(ctx.aseDir, "service.log"), opts);
|
|
759
|
+
}
|
|
579
760
|
/* stop flow: no-op if no port configured or connection refused */
|
|
580
761
|
async doStop() {
|
|
581
762
|
const ctx = this.loadContext();
|
|
@@ -593,11 +774,7 @@ export default class ServiceCommand {
|
|
|
593
774
|
Service.clearPort(ctx.svc);
|
|
594
775
|
return 0;
|
|
595
776
|
}
|
|
596
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, {
|
|
597
|
-
method: "GET",
|
|
598
|
-
signal: AbortSignal.timeout(5000),
|
|
599
|
-
ignoreResponseError: true
|
|
600
|
-
});
|
|
777
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, this.postRequest({}, 5000));
|
|
601
778
|
const ok = r.status >= 200 && r.status < 300;
|
|
602
779
|
Service.clearPort(ctx.svc);
|
|
603
780
|
return ok ? 0 : 1;
|
|
@@ -615,9 +792,11 @@ export default class ServiceCommand {
|
|
|
615
792
|
/* register CLI sub-command "ase service start" */
|
|
616
793
|
service
|
|
617
794
|
.command("start")
|
|
618
|
-
.description("Start the background service")
|
|
619
|
-
.
|
|
620
|
-
|
|
795
|
+
.description("Start the background service (or run it in the foreground)")
|
|
796
|
+
.option("-f, --foreground", "run the service in the foreground, logging to the terminal, " +
|
|
797
|
+
"until it is interrupted or stopped (without idle timeout)")
|
|
798
|
+
.action(async (opts) => {
|
|
799
|
+
process.exit(await this.doStart(opts));
|
|
621
800
|
});
|
|
622
801
|
/* register CLI sub-command "ase service status" */
|
|
623
802
|
service
|
|
@@ -634,6 +813,15 @@ export default class ServiceCommand {
|
|
|
634
813
|
.action(async (cmd) => {
|
|
635
814
|
process.exit(await this.doSend(cmd));
|
|
636
815
|
});
|
|
816
|
+
/* register CLI sub-command "ase service log" */
|
|
817
|
+
service
|
|
818
|
+
.command("log")
|
|
819
|
+
.description("Show the log of the background service")
|
|
820
|
+
.option("-f, --follow", "keep following the log for newly appended lines")
|
|
821
|
+
.option("-n, --lines <n>", "show the last <n> lines only (default: all, or 10 with --follow)")
|
|
822
|
+
.action(async (opts) => {
|
|
823
|
+
process.exit(await this.doLog(opts));
|
|
824
|
+
});
|
|
637
825
|
/* register CLI sub-command "ase service stop" */
|
|
638
826
|
service
|
|
639
827
|
.command("stop")
|