@rse/ase 1.0.4 → 1.0.6
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-artifact.js +7 -56
- package/dst/ase-config.js +6 -9
- package/dst/ase-hook.js +16 -2
- package/dst/ase-ignore.js +129 -0
- package/dst/ase-setup.js +161 -71
- package/dst/ase-spec.js +26 -10
- package/dst/ase-table.js +80 -0
- package/package.json +7 -7
- 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-format-spec.md +19 -14
- package/plugin/meta/ase-format-specbook.yaml +23 -20
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/output-styles/ase-terse.md +58 -0
- package/plugin/skills/ase-help-intent/data.md +7 -6
- package/plugin/skills/ase-spec-activate/SKILL.md +14 -12
- package/plugin/skills/ase-spec-activate/help.md +7 -6
package/dst/ase-setup.js
CHANGED
|
@@ -10,12 +10,12 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { execa } from "execa";
|
|
11
11
|
import which from "which";
|
|
12
12
|
import * as dotenvx from "@dotenvx/dotenvx";
|
|
13
|
-
import Table from "cli-table3";
|
|
14
13
|
import chalk from "chalk";
|
|
15
14
|
import writeFileAtomic from "write-file-atomic";
|
|
16
15
|
import { mkdirp } from "mkdirp";
|
|
17
16
|
import JsonAsty from "json-asty";
|
|
18
17
|
import Version from "./ase-version.js";
|
|
18
|
+
import { renderTable } from "./ase-table.js";
|
|
19
19
|
const toolSpecs = {
|
|
20
20
|
"claude": { cli: "claude", label: "Anthropic Claude Code CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
|
|
21
21
|
"copilot": { cli: "copilot", label: "GitHub Copilot CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
|
|
@@ -143,6 +143,9 @@ export default class SetupCommand {
|
|
|
143
143
|
const scopeArgs = tool === "claude" && scope !== "user" ? ["--scope", scope] : [];
|
|
144
144
|
await this.run(spec.cli, ["plugin", "marketplace", "add", source, ...scopeArgs]);
|
|
145
145
|
await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
|
|
146
|
+
/* select the plugin-shipped ASE output style (Anthropic Claude Code CLI only) */
|
|
147
|
+
if (tool === "claude")
|
|
148
|
+
await this.outputStyleActivate(`install${dev ? "[dev]" : ""}`, scope);
|
|
146
149
|
return 0;
|
|
147
150
|
}
|
|
148
151
|
/* handler for "ase setup update" (both tools) */
|
|
@@ -197,6 +200,9 @@ export default class SetupCommand {
|
|
|
197
200
|
await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
|
|
198
201
|
}
|
|
199
202
|
}
|
|
203
|
+
/* select the plugin-shipped ASE output style (Anthropic Claude Code CLI only) */
|
|
204
|
+
if (tool === "claude")
|
|
205
|
+
await this.outputStyleActivate(`update${dev ? "[dev]" : ""}`, scope);
|
|
200
206
|
return 0;
|
|
201
207
|
}
|
|
202
208
|
/* handler for "ase setup enable|disable" (both tools) */
|
|
@@ -227,6 +233,9 @@ export default class SetupCommand {
|
|
|
227
233
|
this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
|
|
228
234
|
"stopping potentially running ASE service");
|
|
229
235
|
await this.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
|
|
236
|
+
/* deselect the plugin-shipped ASE output style (Anthropic Claude Code CLI only) */
|
|
237
|
+
if (tool === "claude")
|
|
238
|
+
await this.outputStyleDeactivate(`uninstall${dev ? "[dev]" : ""}`, scope);
|
|
230
239
|
/* uninstall ASE plugin */
|
|
231
240
|
this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
|
|
232
241
|
`uninstalling ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote/bundled"})`);
|
|
@@ -245,21 +254,15 @@ export default class SetupCommand {
|
|
|
245
254
|
const spec = toolSpecs[tool];
|
|
246
255
|
await this.ensureTool(spec.cli);
|
|
247
256
|
this.log.write("info", `setup: status: probing ASE registrations for ${spec.label}`);
|
|
248
|
-
const
|
|
249
|
-
head: ["KIND", "ID", "SCOPE", "STATUS"],
|
|
250
|
-
colWidths: [13, 34, 10, 16],
|
|
251
|
-
wordWrap: true,
|
|
252
|
-
chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
|
|
253
|
-
style: { head: ["blue"] }
|
|
254
|
-
});
|
|
257
|
+
const rows = [];
|
|
255
258
|
/* report the ASE plugin registrations, with an absent plugin
|
|
256
259
|
still rendered as an explicit row instead of being omitted */
|
|
257
260
|
const plugins = await this.pluginStatus(tool);
|
|
258
261
|
if (plugins.length === 0)
|
|
259
|
-
|
|
262
|
+
rows.push(["PLUGIN", chalk.bold("ase@ase"), "(n/a)", "not installed"]);
|
|
260
263
|
else
|
|
261
264
|
for (const plugin of plugins)
|
|
262
|
-
|
|
265
|
+
rows.push(["PLUGIN", chalk.bold("ase@ase"), plugin.scope, plugin.status]);
|
|
263
266
|
/* report the registered MCP servers, probed concurrently and
|
|
264
267
|
silently skipping every server which is not registered at all */
|
|
265
268
|
const scopes = await Promise.all(this.mcpServers.map((handle) => this.mcpScope(tool, handle.server)));
|
|
@@ -267,12 +270,15 @@ export default class SetupCommand {
|
|
|
267
270
|
const scope = scopes[i];
|
|
268
271
|
if (scope === undefined)
|
|
269
272
|
continue;
|
|
270
|
-
|
|
273
|
+
rows.push(["MCP", chalk.bold(this.mcpServers[i].server), scope, "registered"]);
|
|
271
274
|
}
|
|
275
|
+
/* report the ASE output style selections */
|
|
276
|
+
for (const outputStyle of await this.outputStyleStatus(tool))
|
|
277
|
+
rows.push(["OUTPUTSTYLE", chalk.bold(outputStyle.file), outputStyle.scope, outputStyle.status]);
|
|
272
278
|
/* report the ASE statusline registrations */
|
|
273
279
|
for (const statusline of await this.statuslineStatus(tool))
|
|
274
|
-
|
|
275
|
-
process.stdout.write(
|
|
280
|
+
rows.push(["STATUSLINE", chalk.bold(statusline.file), statusline.scope, statusline.status]);
|
|
281
|
+
process.stdout.write(renderTable(["KIND", "ID", "SCOPE", "STATUS"], rows));
|
|
276
282
|
return 0;
|
|
277
283
|
}
|
|
278
284
|
/* probe the ASE plugin registrations by scraping the human-readable
|
|
@@ -323,15 +329,9 @@ export default class SetupCommand {
|
|
|
323
329
|
}
|
|
324
330
|
/* handler for "ase setup mcp list" */
|
|
325
331
|
async doMcpList() {
|
|
326
|
-
const
|
|
327
|
-
head: ["ID", "NAME", "VERS", "MCP", "KEY", "SKILLS"],
|
|
328
|
-
colWidths: [16, 16, 8, 21, 17, 20],
|
|
329
|
-
wordWrap: true,
|
|
330
|
-
chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
|
|
331
|
-
style: { head: ["blue"] }
|
|
332
|
-
});
|
|
332
|
+
const rows = [];
|
|
333
333
|
for (const handle of this.mcpServers)
|
|
334
|
-
|
|
334
|
+
rows.push([
|
|
335
335
|
chalk.bold(handle.id),
|
|
336
336
|
handle.name,
|
|
337
337
|
handle.version ?? "(unknown)",
|
|
@@ -339,7 +339,7 @@ export default class SetupCommand {
|
|
|
339
339
|
handle.env.join(", "),
|
|
340
340
|
handle.skills.join(", ")
|
|
341
341
|
]);
|
|
342
|
-
process.stdout.write(
|
|
342
|
+
process.stdout.write(renderTable(["ID", "NAME", "VERS", "MCP", "KEY", "SKILLS"], rows));
|
|
343
343
|
return 0;
|
|
344
344
|
}
|
|
345
345
|
/* handler for "ase setup mcp activate|deactivate [<servers>]" */
|
|
@@ -672,7 +672,7 @@ export default class SetupCommand {
|
|
|
672
672
|
"%P %h %B %c"
|
|
673
673
|
];
|
|
674
674
|
/* resolve the tool settings file for a given installation scope */
|
|
675
|
-
|
|
675
|
+
settingsFile(tool, scope) {
|
|
676
676
|
if (tool === "copilot") {
|
|
677
677
|
const home = process.env.COPILOT_HOME ?? "";
|
|
678
678
|
const base = home !== "" ? home : path.join(os.homedir(), ".copilot");
|
|
@@ -728,11 +728,11 @@ export default class SetupCommand {
|
|
|
728
728
|
}
|
|
729
729
|
return false;
|
|
730
730
|
}
|
|
731
|
-
/* locate
|
|
732
|
-
|
|
731
|
+
/* locate a top-level member node by key within the root object */
|
|
732
|
+
settingsFindMember(root, name) {
|
|
733
733
|
for (const m of root.query("/ member")) {
|
|
734
734
|
const key = m.query("/ string [ pos() == 1 ]")[0];
|
|
735
|
-
if (key !== undefined && key.get("value") ===
|
|
735
|
+
if (key !== undefined && key.get("value") === name)
|
|
736
736
|
return m;
|
|
737
737
|
}
|
|
738
738
|
return undefined;
|
|
@@ -761,7 +761,7 @@ export default class SetupCommand {
|
|
|
761
761
|
return root.create("member").add(key, obj);
|
|
762
762
|
}
|
|
763
763
|
/* normalize the previous-last member so a following ",\n " comma reads cleanly */
|
|
764
|
-
|
|
764
|
+
settingsMakeNonLast(member) {
|
|
765
765
|
const val = member.query("/ * [ pos() == 2 ]")[0];
|
|
766
766
|
if (val !== undefined) {
|
|
767
767
|
const ep = val.get("epilog");
|
|
@@ -770,8 +770,42 @@ export default class SetupCommand {
|
|
|
770
770
|
}
|
|
771
771
|
member.set({ epilog: ",\n " });
|
|
772
772
|
}
|
|
773
|
+
/* append a member to the root object, repairing the whitespace at
|
|
774
|
+
the seam: a scalar value carries no pre-"}" newline of its own
|
|
775
|
+
(a container value carries it in its own epilog already), so the
|
|
776
|
+
root epilog has to supply it */
|
|
777
|
+
settingsAddMember(root, member, scalar) {
|
|
778
|
+
const members = root.query("/ member");
|
|
779
|
+
if (members.length === 0)
|
|
780
|
+
root.set({ prolog: "{\n " });
|
|
781
|
+
else
|
|
782
|
+
this.settingsMakeNonLast(members[members.length - 1]);
|
|
783
|
+
root.set({ epilog: scalar ? "\n}\n" : "}\n" });
|
|
784
|
+
root.add(member);
|
|
785
|
+
}
|
|
786
|
+
/* remove a member from the root object, repairing the whitespace at the seam */
|
|
787
|
+
settingsRemoveMember(root, target) {
|
|
788
|
+
const members = root.query("/ member");
|
|
789
|
+
const wasLast = members[members.length - 1] === target;
|
|
790
|
+
root.del(target);
|
|
791
|
+
if (wasLast) {
|
|
792
|
+
if (members.length > 1) {
|
|
793
|
+
/* promote the new last member: strip its comma epilog and
|
|
794
|
+
restore the pre-"}" newline into the root epilog when the
|
|
795
|
+
new-last value is a scalar (a container value carries the
|
|
796
|
+
newline in its own epilog already) */
|
|
797
|
+
const newLast = members[members.length - 2];
|
|
798
|
+
newLast.set({ epilog: undefined });
|
|
799
|
+
const val = newLast.query("/ * [ pos() == 2 ]")[0];
|
|
800
|
+
const ep = val !== undefined ? val.get("epilog") : undefined;
|
|
801
|
+
root.set({ epilog: (typeof ep === "string" && ep.endsWith("\n")) ? "}\n" : "\n}\n" });
|
|
802
|
+
}
|
|
803
|
+
else
|
|
804
|
+
root.set({ epilog: "}\n" });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
773
807
|
/* read a settings.json file into a JSON-ASTy AST */
|
|
774
|
-
async
|
|
808
|
+
async settingsReadAst(file) {
|
|
775
809
|
let text = "";
|
|
776
810
|
try {
|
|
777
811
|
text = await fs.readFile(file, "utf8");
|
|
@@ -784,7 +818,7 @@ export default class SetupCommand {
|
|
|
784
818
|
return JsonAsty.parse(text);
|
|
785
819
|
}
|
|
786
820
|
/* write a JSON-ASTy AST back to a settings.json file */
|
|
787
|
-
async
|
|
821
|
+
async settingsWriteAst(file, root) {
|
|
788
822
|
await mkdirp(path.dirname(file));
|
|
789
823
|
const text = JsonAsty.unparse(root);
|
|
790
824
|
await writeFileAtomic(file, text, { encoding: "utf8" });
|
|
@@ -793,10 +827,10 @@ export default class SetupCommand {
|
|
|
793
827
|
async doStatuslineActivate(tool, scope, opts, format) {
|
|
794
828
|
this.requireStatuslineTool(tool);
|
|
795
829
|
this.requireStatuslineScope(tool, scope);
|
|
796
|
-
const file = this.
|
|
830
|
+
const file = this.settingsFile(tool, scope);
|
|
797
831
|
const command = this.statuslineCommand(tool, opts, format);
|
|
798
|
-
const root = await this.
|
|
799
|
-
const existing = this.
|
|
832
|
+
const root = await this.settingsReadAst(file);
|
|
833
|
+
const existing = this.settingsFindMember(root, "statusLine");
|
|
800
834
|
if (existing !== undefined) {
|
|
801
835
|
/* preserve a foreign, hand-crafted statusLine: skip and warn */
|
|
802
836
|
if (!this.statuslineIsOwned(existing)) {
|
|
@@ -813,27 +847,17 @@ export default class SetupCommand {
|
|
|
813
847
|
}
|
|
814
848
|
else {
|
|
815
849
|
/* insert a fresh statusLine member */
|
|
816
|
-
|
|
817
|
-
const member = this.statuslineBuildMember(root, command);
|
|
818
|
-
if (members.length === 0) {
|
|
819
|
-
root.set({ prolog: "{\n ", epilog: "}\n" });
|
|
820
|
-
root.add(member);
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
root.set({ epilog: "}\n" });
|
|
824
|
-
this.statuslineMakeNonLast(members[members.length - 1]);
|
|
825
|
-
root.add(member);
|
|
826
|
-
}
|
|
850
|
+
this.settingsAddMember(root, this.statuslineBuildMember(root, command), false);
|
|
827
851
|
this.log.write("info", `setup: statusline: activate: adding ASE "statusLine" to ${file}`);
|
|
828
852
|
}
|
|
829
|
-
await this.
|
|
853
|
+
await this.settingsWriteAst(file, root);
|
|
830
854
|
return 0;
|
|
831
855
|
}
|
|
832
856
|
/* handler for "ase setup statusline deactivate" */
|
|
833
857
|
async doStatuslineDeactivate(tool, scope) {
|
|
834
858
|
this.requireStatuslineTool(tool);
|
|
835
859
|
this.requireStatuslineScope(tool, scope);
|
|
836
|
-
const file = this.
|
|
860
|
+
const file = this.settingsFile(tool, scope);
|
|
837
861
|
/* a missing settings file means nothing to remove */
|
|
838
862
|
try {
|
|
839
863
|
await fs.access(file);
|
|
@@ -843,8 +867,8 @@ export default class SetupCommand {
|
|
|
843
867
|
return 0;
|
|
844
868
|
}
|
|
845
869
|
/* read file */
|
|
846
|
-
const root = await this.
|
|
847
|
-
const target = this.
|
|
870
|
+
const root = await this.settingsReadAst(file);
|
|
871
|
+
const target = this.settingsFindMember(root, "statusLine");
|
|
848
872
|
if (target === undefined) {
|
|
849
873
|
this.log.write("info", `setup: statusline: deactivate: no "statusLine" in ${file} (skipped)`);
|
|
850
874
|
return 0;
|
|
@@ -856,26 +880,8 @@ export default class SetupCommand {
|
|
|
856
880
|
return 0;
|
|
857
881
|
}
|
|
858
882
|
/* remove the member and repair the whitespace at the seam */
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
root.del(target);
|
|
862
|
-
if (wasLast) {
|
|
863
|
-
if (members.length > 1) {
|
|
864
|
-
/* promote the new last member: strip its comma epilog and
|
|
865
|
-
restore the pre-"}" newline into the root epilog when the
|
|
866
|
-
new-last value is a scalar (a container value carries the
|
|
867
|
-
newline in its own epilog already) */
|
|
868
|
-
const newLast = members[members.length - 2];
|
|
869
|
-
newLast.set({ epilog: undefined });
|
|
870
|
-
const val = newLast.query("/ * [ pos() == 2 ]")[0];
|
|
871
|
-
const ep = val !== undefined ? val.get("epilog") : undefined;
|
|
872
|
-
if (!(typeof ep === "string" && ep.endsWith("\n")))
|
|
873
|
-
root.set({ epilog: "\n}\n" });
|
|
874
|
-
}
|
|
875
|
-
else
|
|
876
|
-
root.set({ epilog: "}\n" });
|
|
877
|
-
}
|
|
878
|
-
await this.statuslineWriteAst(file, root);
|
|
883
|
+
this.settingsRemoveMember(root, target);
|
|
884
|
+
await this.settingsWriteAst(file, root);
|
|
879
885
|
this.log.write("info", `setup: statusline: deactivate: removing ASE "statusLine" from ${file}`);
|
|
880
886
|
return 0;
|
|
881
887
|
}
|
|
@@ -889,16 +895,16 @@ export default class SetupCommand {
|
|
|
889
895
|
const scopes = tool === "claude" ? ["user", "project", "local"] : ["user"];
|
|
890
896
|
const entries = [];
|
|
891
897
|
for (const scope of scopes) {
|
|
892
|
-
const file = this.
|
|
898
|
+
const file = this.settingsFile(tool, scope);
|
|
893
899
|
let root;
|
|
894
900
|
try {
|
|
895
|
-
root = await this.
|
|
901
|
+
root = await this.settingsReadAst(file);
|
|
896
902
|
}
|
|
897
903
|
catch {
|
|
898
904
|
/* an unparsable settings file carries no usable state */
|
|
899
905
|
continue;
|
|
900
906
|
}
|
|
901
|
-
const member = this.
|
|
907
|
+
const member = this.settingsFindMember(root, "statusLine");
|
|
902
908
|
if (member === undefined)
|
|
903
909
|
continue;
|
|
904
910
|
entries.push({
|
|
@@ -909,6 +915,90 @@ export default class SetupCommand {
|
|
|
909
915
|
}
|
|
910
916
|
return entries;
|
|
911
917
|
}
|
|
918
|
+
/* determine whether an existing "outputStyle" value is owned by us */
|
|
919
|
+
outputStyleIsOwned(member) {
|
|
920
|
+
const val = member.query("/ string [ pos() == 2 ]")[0];
|
|
921
|
+
return val !== undefined && val.get("value") === "ase:ase-terse";
|
|
922
|
+
}
|
|
923
|
+
/* build the "outputStyle" member subtree natively inside the target AST */
|
|
924
|
+
outputStyleBuildMember(root) {
|
|
925
|
+
const key = root.create("string").set({ body: JSON.stringify("outputStyle"), value: "outputStyle", epilog: ": " });
|
|
926
|
+
const val = root.create("string").set({ body: JSON.stringify("ase:ase-terse"), value: "ase:ase-terse" });
|
|
927
|
+
return root.create("member").add(key, val);
|
|
928
|
+
}
|
|
929
|
+
/* activate the plugin-shipped ASE output style by selecting it in
|
|
930
|
+
the settings file of the scope (Anthropic Claude Code CLI only:
|
|
931
|
+
the other tools have no output style concept, so the session-start
|
|
932
|
+
hook injects the style into their session context instead) */
|
|
933
|
+
async outputStyleActivate(action, scope) {
|
|
934
|
+
const file = this.settingsFile("claude", scope);
|
|
935
|
+
const root = await this.settingsReadAst(file);
|
|
936
|
+
const existing = this.settingsFindMember(root, "outputStyle");
|
|
937
|
+
if (existing !== undefined) {
|
|
938
|
+
/* preserve a foreign, hand-selected outputStyle: skip and warn */
|
|
939
|
+
if (!this.outputStyleIsOwned(existing))
|
|
940
|
+
this.log.write("warning", `setup: ${action}: a non-ASE "outputStyle" ` +
|
|
941
|
+
`is already selected in ${file}: preserving it (skipped)`);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
this.settingsAddMember(root, this.outputStyleBuildMember(root), true);
|
|
945
|
+
await this.settingsWriteAst(file, root);
|
|
946
|
+
this.log.write("info", `setup: ${action}: selecting ASE "outputStyle" in ${file}`);
|
|
947
|
+
}
|
|
948
|
+
/* deactivate the plugin-shipped ASE output style by removing its
|
|
949
|
+
selection from the settings file of the scope (Anthropic Claude Code CLI only) */
|
|
950
|
+
async outputStyleDeactivate(action, scope) {
|
|
951
|
+
const file = this.settingsFile("claude", scope);
|
|
952
|
+
/* a missing settings file means nothing to remove */
|
|
953
|
+
try {
|
|
954
|
+
await fs.access(file);
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const root = await this.settingsReadAst(file);
|
|
960
|
+
const target = this.settingsFindMember(root, "outputStyle");
|
|
961
|
+
if (target === undefined)
|
|
962
|
+
return;
|
|
963
|
+
/* preserve a foreign, hand-selected outputStyle: skip and warn */
|
|
964
|
+
if (!this.outputStyleIsOwned(target)) {
|
|
965
|
+
this.log.write("warning", `setup: ${action}: a non-ASE "outputStyle" ` +
|
|
966
|
+
`is selected in ${file}: preserving it (skipped)`);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
this.settingsRemoveMember(root, target);
|
|
970
|
+
await this.settingsWriteAst(file, root);
|
|
971
|
+
this.log.write("info", `setup: ${action}: deselecting ASE "outputStyle" in ${file}`);
|
|
972
|
+
}
|
|
973
|
+
/* probe the output style selections by inspecting the tool
|
|
974
|
+
settings files of all installation scopes (Anthropic Claude Code CLI only) */
|
|
975
|
+
async outputStyleStatus(tool) {
|
|
976
|
+
if (tool !== "claude")
|
|
977
|
+
return [];
|
|
978
|
+
const home = os.homedir();
|
|
979
|
+
const scopes = ["user", "project", "local"];
|
|
980
|
+
const entries = [];
|
|
981
|
+
for (const scope of scopes) {
|
|
982
|
+
const file = this.settingsFile(tool, scope);
|
|
983
|
+
let root;
|
|
984
|
+
try {
|
|
985
|
+
root = await this.settingsReadAst(file);
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
/* an unparsable settings file carries no usable state */
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const member = this.settingsFindMember(root, "outputStyle");
|
|
992
|
+
if (member === undefined)
|
|
993
|
+
continue;
|
|
994
|
+
entries.push({
|
|
995
|
+
file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
|
|
996
|
+
scope,
|
|
997
|
+
status: this.outputStyleIsOwned(member) ? "selected" : "foreign"
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
return entries;
|
|
1001
|
+
}
|
|
912
1002
|
/* parse and validate the --tool option */
|
|
913
1003
|
parseTool(value) {
|
|
914
1004
|
if (value !== "claude" && value !== "copilot" && value !== "codex")
|
|
@@ -997,7 +1087,7 @@ export default class SetupCommand {
|
|
|
997
1087
|
/* register CLI sub-command "ase setup status" */
|
|
998
1088
|
setupCmd
|
|
999
1089
|
.command("status")
|
|
1000
|
-
.description("report the ASE plugin, MCP server, and statusline registrations for a tool")
|
|
1090
|
+
.description("report the ASE plugin, MCP server, output style, and statusline registrations for a tool")
|
|
1001
1091
|
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
1002
1092
|
.action(async (opts) => {
|
|
1003
1093
|
process.exit(await this.doStatus(this.parseTool(opts.tool)));
|
package/dst/ase-spec.js
CHANGED
|
@@ -18,18 +18,22 @@ import { writeStdout } from "./ase-stdio.js";
|
|
|
18
18
|
project specification, located via the "project.artifact.spec.basedir"
|
|
19
19
|
and "project.artifact.spec.schema" configuration */
|
|
20
20
|
export class Spec {
|
|
21
|
-
/* resolve the YAML schema configuration files: the
|
|
22
|
-
"project.artifact.spec.schema"
|
|
23
|
-
the
|
|
24
|
-
|
|
21
|
+
/* resolve the YAML schema configuration files: the whitespace-separated
|
|
22
|
+
entries of the configured "project.artifact.spec.schema", merged in
|
|
23
|
+
the given order, where each entry is either the literal "std" for the
|
|
24
|
+
bundled standard "ase-format-specbook.yaml" plugin meta file or a file
|
|
25
|
+
path relative to the project root, and an unset or empty value means "std" */
|
|
25
26
|
static configFiles(log) {
|
|
26
27
|
const cfg = new Config("config", configSchema, log);
|
|
27
28
|
cfg.read();
|
|
28
29
|
const val = cfg.get("project.artifact.spec.schema");
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const list = val === undefined ? "" : String(isScalar(val) ? val.value : val);
|
|
31
|
+
const entries = list.split(/\s+/).filter((entry) => entry !== "");
|
|
32
|
+
if (entries.length === 0)
|
|
33
|
+
entries.push("std");
|
|
34
|
+
return entries.map((entry) => entry === "std" ?
|
|
35
|
+
Meta.resolve("ase-format-specbook.yaml") :
|
|
36
|
+
path.resolve(Task.projectRoot(), entry));
|
|
33
37
|
}
|
|
34
38
|
/* the ASE log level each SpecBook verbosity level maps onto: the
|
|
35
39
|
"none" level carries the messages a bare SpecBook CLI run always
|
|
@@ -80,12 +84,19 @@ export class Spec {
|
|
|
80
84
|
return file;
|
|
81
85
|
return rel.replace(/\\/g, "/");
|
|
82
86
|
}
|
|
87
|
+
/* every specification command honors the Git exclude rules, so that
|
|
88
|
+
SpecBook resolves the very same artifact files as "ase artifact
|
|
89
|
+
list --kind spec": both consult ".gitignore", so a specification
|
|
90
|
+
the project does not track is invisible to both instead of only
|
|
91
|
+
to one of them */
|
|
92
|
+
static gitignore = true;
|
|
83
93
|
/* lint the specification Markdown files below the "spec" artifact
|
|
84
94
|
base directory against the schema configuration */
|
|
85
95
|
static async lint(log) {
|
|
86
96
|
const result = await Spec.unmarked(Spec.api(log).lint({
|
|
87
97
|
config: Spec.configFiles(log),
|
|
88
|
-
basedir: Artifact.basedir(log, "spec")
|
|
98
|
+
basedir: Artifact.basedir(log, "spec"),
|
|
99
|
+
gitignore: Spec.gitignore
|
|
89
100
|
}));
|
|
90
101
|
return result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
|
|
91
102
|
}
|
|
@@ -120,6 +131,7 @@ export class Spec {
|
|
|
120
131
|
return Spec.unmarked(Spec.api(log, notices).export({
|
|
121
132
|
config: Spec.configFiles(log),
|
|
122
133
|
basedir: Artifact.basedir(log, "spec"),
|
|
134
|
+
gitignore: Spec.gitignore,
|
|
123
135
|
formats
|
|
124
136
|
}));
|
|
125
137
|
}
|
|
@@ -131,6 +143,7 @@ export class Spec {
|
|
|
131
143
|
return Spec.unmarked(Spec.api(log).watch({
|
|
132
144
|
config: Spec.configFiles(log),
|
|
133
145
|
basedir: Artifact.basedir(log, "spec"),
|
|
146
|
+
gitignore: Spec.gitignore,
|
|
134
147
|
formats,
|
|
135
148
|
outputs,
|
|
136
149
|
onExport
|
|
@@ -143,6 +156,7 @@ export class Spec {
|
|
|
143
156
|
return Spec.unmarked(Spec.api(log).preview({
|
|
144
157
|
config: Spec.configFiles(log),
|
|
145
158
|
basedir: Artifact.basedir(log, "spec"),
|
|
159
|
+
gitignore: Spec.gitignore,
|
|
146
160
|
addr,
|
|
147
161
|
port
|
|
148
162
|
}));
|
|
@@ -246,7 +260,9 @@ export class SpecMCP {
|
|
|
246
260
|
title: "ASE SpecBook lint",
|
|
247
261
|
description: "Lint the SpecBook specification Markdown files of the project (located via the " +
|
|
248
262
|
"`project.artifact.spec.basedir` configuration) against the SpecBook YAML schema " +
|
|
249
|
-
"configuration (`project.artifact.spec.schema`,
|
|
263
|
+
"configuration (`project.artifact.spec.schema`, a whitespace-separated list of schema files " +
|
|
264
|
+
"merged in order, with `std` naming the bundled standard `ase-format-specbook.yaml`, " +
|
|
265
|
+
"which is also the default). " +
|
|
250
266
|
"Returns a `diagnostics` array of `{ file, line, column, severity, message }` objects (with " +
|
|
251
267
|
"project-relative `file` and a `severity` of `error` or `warning`), rendered as bullet " +
|
|
252
268
|
"points in `text`. With `verbose`, " +
|
package/dst/ase-table.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import Table from "cli-table3";
|
|
7
|
+
import { Diagram } from "./ase-diagram.js";
|
|
8
|
+
/* assumed terminal width in case the output is not a terminal at all */
|
|
9
|
+
const termWidthFallback = 80;
|
|
10
|
+
/* content width a column is at most shrunk down to, unless its own
|
|
11
|
+
content or its column head already is narrower anyway */
|
|
12
|
+
const colWidthMinimum = 8;
|
|
13
|
+
/* measure the visible width of a cell content, ignoring ANSI escape
|
|
14
|
+
sequences and honoring the longest line of a multi-line content */
|
|
15
|
+
const cellWidth = (text) => text.split("\n").reduce((width, line) =>
|
|
16
|
+
/* eslint-disable-next-line no-control-regex */
|
|
17
|
+
Math.max(width, line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").length), 0);
|
|
18
|
+
/* check whether a cell content carries a single token which does not
|
|
19
|
+
fit the column and hence can be wrapped on character boundaries only */
|
|
20
|
+
const hasOverlongToken = (text, width) => text.split(/\s+/).some((token) => cellWidth(token) > width);
|
|
21
|
+
/* hard-break all over-long tokens of a cell content, so the content as
|
|
22
|
+
a whole can still be wrapped on its regular word boundaries; tokens
|
|
23
|
+
carrying ANSI escape sequences are left alone, as breaking them
|
|
24
|
+
would tear apart their escape sequences */
|
|
25
|
+
const breakOverlongTokens = (text, width) => text.split(/(\s+)/).map((token) => {
|
|
26
|
+
if (cellWidth(token) <= width || /\s/.test(token) || token.includes("\x1b"))
|
|
27
|
+
return token;
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for (let i = 0; i < token.length; i += width)
|
|
30
|
+
chunks.push(token.slice(i, i + width));
|
|
31
|
+
return chunks.join("\n");
|
|
32
|
+
}).join("");
|
|
33
|
+
/* reduce the column content widths until their sum fits the available
|
|
34
|
+
budget, by binary-searching the largest common upper cap which still
|
|
35
|
+
fits and which never cuts a column below its individual minimum */
|
|
36
|
+
const capWidths = (natural, minimum, budget) => {
|
|
37
|
+
const capped = (cap) => natural.map((width, i) => Math.max(Math.min(width, cap), Math.min(width, minimum[i])));
|
|
38
|
+
const total = (widths) => widths.reduce((sum, width) => sum + width, 0);
|
|
39
|
+
let lo = 1;
|
|
40
|
+
let hi = Math.max(...natural, 1);
|
|
41
|
+
while (lo < hi) {
|
|
42
|
+
const mid = Math.floor((lo + hi + 1) / 2);
|
|
43
|
+
if (total(capped(mid)) <= budget)
|
|
44
|
+
lo = mid;
|
|
45
|
+
else
|
|
46
|
+
hi = mid - 1;
|
|
47
|
+
}
|
|
48
|
+
return capped(lo);
|
|
49
|
+
};
|
|
50
|
+
/* render a table which never exceeds the terminal width, by wrapping
|
|
51
|
+
over-wide cell contents over multiple lines instead */
|
|
52
|
+
export const renderTable = (head, rows) => {
|
|
53
|
+
const cols = head.length;
|
|
54
|
+
/* determine the natural, still unwrapped content width per column */
|
|
55
|
+
const natural = head.map((text, i) => rows.reduce((width, row) => Math.max(width, cellWidth(row[i] ?? "")), cellWidth(text)));
|
|
56
|
+
/* shrink the columns down to the width still remaining after the
|
|
57
|
+
border characters and the padding spaces of all columns */
|
|
58
|
+
const term = Diagram.detectTermWidth() || termWidthFallback;
|
|
59
|
+
const budget = Math.max(term - (3 * cols + 1), cols);
|
|
60
|
+
const minimum = head.map((text) => Math.max(cellWidth(text), colWidthMinimum));
|
|
61
|
+
const widths = capWidths(natural, minimum, budget);
|
|
62
|
+
/* render the table, falling back to wrapping on character
|
|
63
|
+
boundaries in just those cells where a token still overflows */
|
|
64
|
+
const table = new Table({
|
|
65
|
+
head,
|
|
66
|
+
colWidths: widths.map((width) => width + 2),
|
|
67
|
+
wordWrap: true,
|
|
68
|
+
chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
|
|
69
|
+
style: { head: ["blue"] }
|
|
70
|
+
});
|
|
71
|
+
for (const row of rows)
|
|
72
|
+
table.push(row.map((text, i) => {
|
|
73
|
+
const content = breakOverlongTokens(text, widths[i]);
|
|
74
|
+
return {
|
|
75
|
+
content,
|
|
76
|
+
wrapOnWordBoundary: !hasOverlongToken(content, widths[i])
|
|
77
|
+
};
|
|
78
|
+
}));
|
|
79
|
+
return `${table.toString()}\n`;
|
|
80
|
+
};
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "https://ase.tools",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "https://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.6",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"globals": "17.12.0",
|
|
27
27
|
"typescript": "6.0.3",
|
|
28
28
|
|
|
29
|
-
"@rse/stx": "1.1.
|
|
29
|
+
"@rse/stx": "1.1.7",
|
|
30
30
|
"nodemon": "3.1.14",
|
|
31
31
|
"shx": "0.4.0",
|
|
32
32
|
|
|
33
|
-
"@types/node": "26.5.
|
|
33
|
+
"@types/node": "26.5.1",
|
|
34
34
|
"@types/luxon": "3.7.5",
|
|
35
35
|
"@types/which": "3.0.4",
|
|
36
36
|
"@types/update-notifier": "6.0.8",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"commander": "15.0.0",
|
|
45
|
-
"@dotenvx/dotenvx": "2.
|
|
46
|
-
"yaml": "2.9.
|
|
45
|
+
"@dotenvx/dotenvx": "2.24.1",
|
|
46
|
+
"yaml": "2.9.1",
|
|
47
47
|
"valibot": "1.5.0",
|
|
48
48
|
"execa": "10.0.1",
|
|
49
49
|
"mkdirp": "3.0.1",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"luxon": "3.7.2",
|
|
56
56
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
57
57
|
"json-asty": "1.3.4",
|
|
58
|
-
"zod": "4.6.
|
|
58
|
+
"zod": "4.6.5",
|
|
59
59
|
"which": "7.0.0",
|
|
60
60
|
"update-notifier": "7.3.1",
|
|
61
61
|
"shell-quote": "1.10.0",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"ofetch": "1.5.1",
|
|
67
67
|
"picomatch": "4.0.7",
|
|
68
68
|
"lru-cache": "11.5.2",
|
|
69
|
-
"@rse/specbook": "1.2.
|
|
69
|
+
"@rse/specbook": "1.2.13",
|
|
70
70
|
"source-code-error": "1.2.0",
|
|
71
71
|
"pure-uuid": "2.0.0"
|
|
72
72
|
},
|