fwinit 1.6.2 → 1.7.0
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/dist/index.js +3414 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { Command } from "commander";
|
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
8
|
-
import { mkdir as mkdir3, cp as cp3, readFile as
|
|
9
|
-
import { join as
|
|
8
|
+
import { mkdir as mkdir3, cp as cp3, readFile as readFile5 } from "fs/promises";
|
|
9
|
+
import { join as join6, basename as basename2 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/templates.ts
|
|
12
12
|
var TEMPLATES = [
|
|
@@ -679,33 +679,33 @@ async function exists(path) {
|
|
|
679
679
|
}
|
|
680
680
|
async function listMarkdown(dir) {
|
|
681
681
|
try {
|
|
682
|
-
const { readdir:
|
|
683
|
-
return (await
|
|
682
|
+
const { readdir: readdir5 } = await import("fs/promises");
|
|
683
|
+
return (await readdir5(dir)).filter((f) => f.endsWith(".md"));
|
|
684
684
|
} catch {
|
|
685
685
|
return [];
|
|
686
686
|
}
|
|
687
687
|
}
|
|
688
688
|
async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
689
|
-
const { join:
|
|
689
|
+
const { join: join7 } = await import("path");
|
|
690
690
|
const installs = [];
|
|
691
691
|
const skillDirs = [];
|
|
692
692
|
const missingRequired = [];
|
|
693
693
|
for (const entry of bundle) {
|
|
694
|
-
const src =
|
|
694
|
+
const src = join7(repoRoot, "skills", entry.dir);
|
|
695
695
|
if (entry.kind === "library") {
|
|
696
696
|
if (!await exists(src)) {
|
|
697
697
|
if (entry.required) missingRequired.push(entry.dir);
|
|
698
698
|
continue;
|
|
699
699
|
}
|
|
700
700
|
installs.push({ from: src, to: `skills/${entry.dir}`, recursive: true });
|
|
701
|
-
const cmds =
|
|
701
|
+
const cmds = join7(src, "commands");
|
|
702
702
|
for (const file of await listMarkdown(cmds)) {
|
|
703
|
-
installs.push({ from:
|
|
703
|
+
installs.push({ from: join7(cmds, file), to: `commands/${file}` });
|
|
704
704
|
}
|
|
705
705
|
continue;
|
|
706
706
|
}
|
|
707
|
-
const skill =
|
|
708
|
-
const command =
|
|
707
|
+
const skill = join7(src, "SKILL.md");
|
|
708
|
+
const command = join7(src, "command.md");
|
|
709
709
|
if (!await exists(skill)) {
|
|
710
710
|
if (entry.required) missingRequired.push(entry.dir);
|
|
711
711
|
continue;
|
|
@@ -718,7 +718,7 @@ async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
|
718
718
|
missingRequired.push(`${entry.dir}/command.md`);
|
|
719
719
|
}
|
|
720
720
|
for (const extra of entry.extras ?? []) {
|
|
721
|
-
const from =
|
|
721
|
+
const from = join7(src, extra);
|
|
722
722
|
if (!await exists(from)) continue;
|
|
723
723
|
installs.push({
|
|
724
724
|
from,
|
|
@@ -730,6 +730,3306 @@ async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
|
730
730
|
return { installs, skillDirs, commandDirs: ["commands"], missingRequired };
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
+
// src/studio/server.ts
|
|
734
|
+
import { createServer } from "http";
|
|
735
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
736
|
+
|
|
737
|
+
// src/studio/specs.ts
|
|
738
|
+
import { readdir as readdir2, readFile as readFile2, realpath, stat as stat2 } from "fs/promises";
|
|
739
|
+
import { extname as extname2, join as join2, relative as relative2, resolve, sep as sep2 } from "path";
|
|
740
|
+
|
|
741
|
+
// src/studio/markdown.ts
|
|
742
|
+
var ESCAPES = {
|
|
743
|
+
"&": "&",
|
|
744
|
+
"<": "<",
|
|
745
|
+
">": ">",
|
|
746
|
+
'"': """,
|
|
747
|
+
"'": "'"
|
|
748
|
+
};
|
|
749
|
+
function esc(text) {
|
|
750
|
+
return text.replace(/[&<>"']/g, (char) => ESCAPES[char]);
|
|
751
|
+
}
|
|
752
|
+
function slugify(text) {
|
|
753
|
+
const slug = text.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/[\s-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
754
|
+
return slug === "" ? "section" : slug;
|
|
755
|
+
}
|
|
756
|
+
function plainText(source) {
|
|
757
|
+
return source.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/\*\*\*|\*\*|\*|___|__|_|~~/g, "").trim();
|
|
758
|
+
}
|
|
759
|
+
function matchLink(source, start) {
|
|
760
|
+
const close = source.indexOf("]", start + 1);
|
|
761
|
+
if (close === -1) return null;
|
|
762
|
+
if (source[close + 1] !== "(") return null;
|
|
763
|
+
const destination = matchDestination(source, close + 1);
|
|
764
|
+
if (destination === null) return null;
|
|
765
|
+
return {
|
|
766
|
+
text: source.slice(start + 1, close),
|
|
767
|
+
target: destination.target,
|
|
768
|
+
end: destination.end
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function matchDestination(source, openParen) {
|
|
772
|
+
let depth = 1;
|
|
773
|
+
let index = openParen + 1;
|
|
774
|
+
while (index < source.length) {
|
|
775
|
+
const char = source[index];
|
|
776
|
+
if (char === "(") depth += 1;
|
|
777
|
+
else if (char === ")") {
|
|
778
|
+
depth -= 1;
|
|
779
|
+
if (depth === 0) {
|
|
780
|
+
return { target: source.slice(openParen + 1, index), end: index + 1 };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
index += 1;
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
var SAFE_PROTOCOL = /^(?:https?|mailto):/i;
|
|
788
|
+
var ANY_PROTOCOL = /^[a-z][a-z0-9+.-]*:/i;
|
|
789
|
+
var SPEC_PATH = /\.(?:md|markdown)$/i;
|
|
790
|
+
function normalizeSpecPath(target) {
|
|
791
|
+
const path = target.split("#")[0].split("?")[0].replace(/\\/g, "/");
|
|
792
|
+
const segments = [];
|
|
793
|
+
for (const segment of path.split("/")) {
|
|
794
|
+
if (segment === "" || segment === ".") continue;
|
|
795
|
+
segments.push(segment);
|
|
796
|
+
}
|
|
797
|
+
return segments.join("/");
|
|
798
|
+
}
|
|
799
|
+
function renderLink(text, rawTarget) {
|
|
800
|
+
const target = rawTarget.trim();
|
|
801
|
+
const inner = renderInline(text);
|
|
802
|
+
if (SAFE_PROTOCOL.test(target)) {
|
|
803
|
+
return `<a href="${esc(target)}" rel="noreferrer noopener">${inner}</a>`;
|
|
804
|
+
}
|
|
805
|
+
if (ANY_PROTOCOL.test(target)) return inner;
|
|
806
|
+
if (target.startsWith("/") || target.startsWith("\\")) return inner;
|
|
807
|
+
const specPath = normalizeSpecPath(target);
|
|
808
|
+
if (SPEC_PATH.test(specPath)) {
|
|
809
|
+
return `<a href="#" data-spec-link="${esc(specPath)}">${inner}</a>`;
|
|
810
|
+
}
|
|
811
|
+
return inner;
|
|
812
|
+
}
|
|
813
|
+
var WORD_CHAR = /[\p{L}\p{N}]/u;
|
|
814
|
+
function findEmphasisClose(source, char, size, from) {
|
|
815
|
+
let index = from;
|
|
816
|
+
while (index < source.length) {
|
|
817
|
+
if (source[index] !== char) {
|
|
818
|
+
index += 1;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
let length = 0;
|
|
822
|
+
while (source[index + length] === char) length += 1;
|
|
823
|
+
if (length >= size) {
|
|
824
|
+
if (char === "_" && WORD_CHAR.test(source[index + length] ?? "")) {
|
|
825
|
+
index += length;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
return { start: index, length };
|
|
829
|
+
}
|
|
830
|
+
index += length;
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
function matchEmphasis(source, start) {
|
|
835
|
+
const char = source[start];
|
|
836
|
+
if (char === "~") {
|
|
837
|
+
if (source[start + 1] !== "~") return null;
|
|
838
|
+
const close = source.indexOf("~~", start + 2);
|
|
839
|
+
if (close <= start + 2) return null;
|
|
840
|
+
return {
|
|
841
|
+
html: `<del>${renderInline(source.slice(start + 2, close))}</del>`,
|
|
842
|
+
end: close + 2
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
let run = 0;
|
|
846
|
+
while (source[start + run] === char && run < 3) run += 1;
|
|
847
|
+
if (char === "_" && start > 0 && WORD_CHAR.test(source[start - 1])) return null;
|
|
848
|
+
for (let size = run; size >= 1; size -= 1) {
|
|
849
|
+
const close = findEmphasisClose(source, char, size, start + size);
|
|
850
|
+
if (close === null) continue;
|
|
851
|
+
if (close.start === start + size) continue;
|
|
852
|
+
const extra = close.length - size;
|
|
853
|
+
const content = source.slice(start + size, close.start + extra);
|
|
854
|
+
const inner = renderInline(content);
|
|
855
|
+
if (size === 3) {
|
|
856
|
+
return { html: `<strong><em>${inner}</em></strong>`, end: close.start + close.length };
|
|
857
|
+
}
|
|
858
|
+
if (size === 2) {
|
|
859
|
+
return { html: `<strong>${inner}</strong>`, end: close.start + close.length };
|
|
860
|
+
}
|
|
861
|
+
return { html: `<em>${inner}</em>`, end: close.start + close.length };
|
|
862
|
+
}
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
function renderInline(source) {
|
|
866
|
+
let html = "";
|
|
867
|
+
let index = 0;
|
|
868
|
+
while (index < source.length) {
|
|
869
|
+
const char = source[index];
|
|
870
|
+
if (char === "`") {
|
|
871
|
+
const close = source.indexOf("`", index + 1);
|
|
872
|
+
if (close > index + 1) {
|
|
873
|
+
html += `<code>${esc(source.slice(index + 1, close))}</code>`;
|
|
874
|
+
index = close + 1;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (char === "!" && source[index + 1] === "[") {
|
|
879
|
+
const image = matchLink(source, index + 1);
|
|
880
|
+
if (image !== null) {
|
|
881
|
+
html += `<span class="md-img">${renderInline(image.text)}</span>`;
|
|
882
|
+
index = image.end;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (char === "[") {
|
|
887
|
+
const link = matchLink(source, index);
|
|
888
|
+
if (link !== null) {
|
|
889
|
+
html += renderLink(link.text, link.target);
|
|
890
|
+
index = link.end;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (char === "*" || char === "_" || char === "~") {
|
|
895
|
+
const emphasis = matchEmphasis(source, index);
|
|
896
|
+
if (emphasis !== null) {
|
|
897
|
+
html += emphasis.html;
|
|
898
|
+
index = emphasis.end;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
html += esc(char);
|
|
903
|
+
index += 1;
|
|
904
|
+
}
|
|
905
|
+
return html;
|
|
906
|
+
}
|
|
907
|
+
var LIST_ITEM = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
|
|
908
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*([^`]*)$/;
|
|
909
|
+
var HEADING = /^(#{1,6})\s+(.*)$/;
|
|
910
|
+
var RULE = /^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
911
|
+
var QUOTE = /^\s{0,3}>/;
|
|
912
|
+
function tableCells(line) {
|
|
913
|
+
let trimmed = line.trim();
|
|
914
|
+
if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
|
|
915
|
+
if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1);
|
|
916
|
+
return trimmed.split("|").map((cell) => cell.trim());
|
|
917
|
+
}
|
|
918
|
+
function isTableSeparator(line) {
|
|
919
|
+
if (!line.includes("|")) return false;
|
|
920
|
+
const cells = tableCells(line);
|
|
921
|
+
return cells.length > 0 && cells.every((cell) => /^:?-+:?$/.test(cell));
|
|
922
|
+
}
|
|
923
|
+
function isBlockStart(lines, index) {
|
|
924
|
+
const line = lines[index];
|
|
925
|
+
if (line.trim() === "") return true;
|
|
926
|
+
if (FENCE.test(line)) return true;
|
|
927
|
+
if (HEADING.test(line)) return true;
|
|
928
|
+
if (RULE.test(line)) return true;
|
|
929
|
+
if (QUOTE.test(line)) return true;
|
|
930
|
+
if (LIST_ITEM.test(line)) return true;
|
|
931
|
+
if (line.includes("|") && index + 1 < lines.length && isTableSeparator(lines[index + 1])) {
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
function renderMarkdown(markdown) {
|
|
937
|
+
const headings = [];
|
|
938
|
+
const usedIds = /* @__PURE__ */ new Map();
|
|
939
|
+
const uniqueId = (base) => {
|
|
940
|
+
const seen = usedIds.get(base) ?? 0;
|
|
941
|
+
usedIds.set(base, seen + 1);
|
|
942
|
+
return seen === 0 ? base : `${base}-${seen + 1}`;
|
|
943
|
+
};
|
|
944
|
+
const renderItem = (item) => {
|
|
945
|
+
const task = /^\[([ xX])\]\s+(.*)$/.exec(item.text);
|
|
946
|
+
if (task !== null) {
|
|
947
|
+
const checked = task[1].toLowerCase() === "x";
|
|
948
|
+
const box = `<input type="checkbox" disabled${checked ? " checked" : ""}>`;
|
|
949
|
+
return { html: `${box} ${renderInline(task[2])}`, task: true };
|
|
950
|
+
}
|
|
951
|
+
return { html: renderInline(item.text), task: false };
|
|
952
|
+
};
|
|
953
|
+
const renderItems = (items, ordered) => {
|
|
954
|
+
const tag = ordered ? "ol" : "ul";
|
|
955
|
+
const parts = items.map((item) => {
|
|
956
|
+
const { html, task } = renderItem(item);
|
|
957
|
+
const cls = task ? ' class="task"' : "";
|
|
958
|
+
return `<li${cls}>${html}</li>`;
|
|
959
|
+
});
|
|
960
|
+
return `<${tag}>${parts.join("")}</${tag}>`;
|
|
961
|
+
};
|
|
962
|
+
const renderBlocks = (lines) => {
|
|
963
|
+
const out = [];
|
|
964
|
+
let index = 0;
|
|
965
|
+
while (index < lines.length) {
|
|
966
|
+
const line = lines[index];
|
|
967
|
+
if (line.trim() === "") {
|
|
968
|
+
index += 1;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const fence = FENCE.exec(line);
|
|
972
|
+
if (fence !== null) {
|
|
973
|
+
const marker = fence[1];
|
|
974
|
+
const fenceChar = marker[0];
|
|
975
|
+
const lang = fence[2].trim().split(/\s+/)[0] ?? "";
|
|
976
|
+
const body = [];
|
|
977
|
+
index += 1;
|
|
978
|
+
while (index < lines.length) {
|
|
979
|
+
const candidate = lines[index];
|
|
980
|
+
const trimmed = candidate.trim();
|
|
981
|
+
const closes = trimmed.length >= marker.length && trimmed.split("").every((char) => char === fenceChar);
|
|
982
|
+
if (closes) {
|
|
983
|
+
index += 1;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
body.push(candidate);
|
|
987
|
+
index += 1;
|
|
988
|
+
}
|
|
989
|
+
const cls = lang === "" ? "" : ` class="lang-${esc(lang)}"`;
|
|
990
|
+
out.push(`<pre><code${cls}>${esc(body.join("\n"))}</code></pre>`);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const heading = HEADING.exec(line);
|
|
994
|
+
if (heading !== null) {
|
|
995
|
+
const level = heading[1].length;
|
|
996
|
+
const raw = heading[2].replace(/\s+#+\s*$/, "").trim();
|
|
997
|
+
const text = plainText(raw);
|
|
998
|
+
const id = uniqueId(slugify(text));
|
|
999
|
+
headings.push({ level, text, id });
|
|
1000
|
+
out.push(`<h${level} id="${esc(id)}">${renderInline(raw)}</h${level}>`);
|
|
1001
|
+
index += 1;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (RULE.test(line)) {
|
|
1005
|
+
out.push("<hr>");
|
|
1006
|
+
index += 1;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (line.includes("|") && index + 1 < lines.length && isTableSeparator(lines[index + 1])) {
|
|
1010
|
+
const header = tableCells(line);
|
|
1011
|
+
const rows = [];
|
|
1012
|
+
index += 2;
|
|
1013
|
+
while (index < lines.length && lines[index].trim() !== "" && lines[index].includes("|")) {
|
|
1014
|
+
rows.push(tableCells(lines[index]));
|
|
1015
|
+
index += 1;
|
|
1016
|
+
}
|
|
1017
|
+
const head = header.map((cell) => `<th>${renderInline(cell)}</th>`).join("");
|
|
1018
|
+
const body = rows.map(
|
|
1019
|
+
(row) => `<tr>${row.map((cell) => `<td>${renderInline(cell)}</td>`).join("")}</tr>`
|
|
1020
|
+
).join("");
|
|
1021
|
+
out.push(
|
|
1022
|
+
`<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`
|
|
1023
|
+
);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (QUOTE.test(line)) {
|
|
1027
|
+
const quoted = [];
|
|
1028
|
+
while (index < lines.length && QUOTE.test(lines[index])) {
|
|
1029
|
+
quoted.push(lines[index].replace(/^\s{0,3}>\s?/, ""));
|
|
1030
|
+
index += 1;
|
|
1031
|
+
}
|
|
1032
|
+
out.push(`<blockquote>${renderBlocks(quoted)}</blockquote>`);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (LIST_ITEM.test(line)) {
|
|
1036
|
+
const items = [];
|
|
1037
|
+
let baseIndent = -1;
|
|
1038
|
+
while (index < lines.length) {
|
|
1039
|
+
const match = LIST_ITEM.exec(lines[index]);
|
|
1040
|
+
if (match === null) break;
|
|
1041
|
+
const indent = match[1].length;
|
|
1042
|
+
if (baseIndent === -1) baseIndent = indent;
|
|
1043
|
+
items.push({
|
|
1044
|
+
indent,
|
|
1045
|
+
ordered: /\d/.test(match[2]),
|
|
1046
|
+
text: match[3]
|
|
1047
|
+
});
|
|
1048
|
+
index += 1;
|
|
1049
|
+
}
|
|
1050
|
+
const top = [];
|
|
1051
|
+
for (const item of items) {
|
|
1052
|
+
if (item.indent > baseIndent && top.length > 0) {
|
|
1053
|
+
top[top.length - 1].children.push(item);
|
|
1054
|
+
} else {
|
|
1055
|
+
top.push({ item, children: [] });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const ordered = top[0].item.ordered;
|
|
1059
|
+
const lis = top.map(({ item, children }) => {
|
|
1060
|
+
const { html, task } = renderItem(item);
|
|
1061
|
+
const cls = task ? ' class="task"' : "";
|
|
1062
|
+
const nested = children.length > 0 ? renderItems(children, children[0].ordered) : "";
|
|
1063
|
+
return `<li${cls}>${html}${nested}</li>`;
|
|
1064
|
+
});
|
|
1065
|
+
out.push(`<${ordered ? "ol" : "ul"}>${lis.join("")}</${ordered ? "ol" : "ul"}>`);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
const paragraph = [];
|
|
1069
|
+
const start = index;
|
|
1070
|
+
while (index < lines.length) {
|
|
1071
|
+
const current = lines[index];
|
|
1072
|
+
if (current.trim() === "") break;
|
|
1073
|
+
if (index > start && isBlockStart(lines, index)) break;
|
|
1074
|
+
paragraph.push(current.trim());
|
|
1075
|
+
index += 1;
|
|
1076
|
+
}
|
|
1077
|
+
out.push(`<p>${renderInline(paragraph.join("\n"))}</p>`);
|
|
1078
|
+
}
|
|
1079
|
+
return out.join("");
|
|
1080
|
+
};
|
|
1081
|
+
return { html: renderBlocks(markdown.replace(/\r\n?/g, "\n").split("\n")), headings };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/studio/specs.ts
|
|
1085
|
+
var SKIPPED_NAMES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
1086
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]);
|
|
1087
|
+
var SpecPathError = class extends Error {
|
|
1088
|
+
code;
|
|
1089
|
+
path;
|
|
1090
|
+
constructor(code, path, message) {
|
|
1091
|
+
super(message);
|
|
1092
|
+
this.name = "SpecPathError";
|
|
1093
|
+
this.code = code;
|
|
1094
|
+
this.path = path;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
function isInside(root, candidate) {
|
|
1098
|
+
if (candidate === root) return true;
|
|
1099
|
+
const prefix = root.endsWith(sep2) ? root : `${root}${sep2}`;
|
|
1100
|
+
return candidate.startsWith(prefix);
|
|
1101
|
+
}
|
|
1102
|
+
async function realpathOrNull(path) {
|
|
1103
|
+
try {
|
|
1104
|
+
return await realpath(path);
|
|
1105
|
+
} catch {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function compareNodes(a, b) {
|
|
1110
|
+
const left = a.name.toLowerCase();
|
|
1111
|
+
const right = b.name.toLowerCase();
|
|
1112
|
+
const byName = left.localeCompare(right);
|
|
1113
|
+
return byName !== 0 ? byName : a.name.localeCompare(b.name);
|
|
1114
|
+
}
|
|
1115
|
+
async function scanDirectory(absoluteDir, relativeDir, totals) {
|
|
1116
|
+
let entries;
|
|
1117
|
+
try {
|
|
1118
|
+
entries = await readdir2(absoluteDir, { withFileTypes: true });
|
|
1119
|
+
} catch {
|
|
1120
|
+
return [];
|
|
1121
|
+
}
|
|
1122
|
+
const dirs = [];
|
|
1123
|
+
const files = [];
|
|
1124
|
+
for (const entry of entries) {
|
|
1125
|
+
if (entry.isSymbolicLink()) continue;
|
|
1126
|
+
if (entry.name.startsWith(".") || SKIPPED_NAMES.has(entry.name)) continue;
|
|
1127
|
+
const childPath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
|
|
1128
|
+
if (entry.isDirectory()) {
|
|
1129
|
+
const children = await scanDirectory(join2(absoluteDir, entry.name), childPath, totals);
|
|
1130
|
+
totals.dirs += 1;
|
|
1131
|
+
dirs.push({ name: entry.name, path: childPath, type: "dir", children });
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (!entry.isFile()) continue;
|
|
1135
|
+
let info;
|
|
1136
|
+
try {
|
|
1137
|
+
info = await stat2(join2(absoluteDir, entry.name));
|
|
1138
|
+
} catch {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
totals.files += 1;
|
|
1142
|
+
totals.bytes += info.size;
|
|
1143
|
+
files.push({
|
|
1144
|
+
name: entry.name,
|
|
1145
|
+
path: childPath,
|
|
1146
|
+
type: "file",
|
|
1147
|
+
bytes: info.size,
|
|
1148
|
+
modified: info.mtime.toISOString()
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
dirs.sort(compareNodes);
|
|
1152
|
+
files.sort(compareNodes);
|
|
1153
|
+
return [...dirs, ...files];
|
|
1154
|
+
}
|
|
1155
|
+
async function buildSpecsTree(projectRoot) {
|
|
1156
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1157
|
+
const specsRoot = join2(projectRoot, "specs");
|
|
1158
|
+
const empty = {
|
|
1159
|
+
found: false,
|
|
1160
|
+
root: null,
|
|
1161
|
+
projectRoot,
|
|
1162
|
+
sections: [],
|
|
1163
|
+
tree: [],
|
|
1164
|
+
totals: { files: 0, dirs: 0, bytes: 0 },
|
|
1165
|
+
generatedAt
|
|
1166
|
+
};
|
|
1167
|
+
let rootInfo;
|
|
1168
|
+
try {
|
|
1169
|
+
rootInfo = await stat2(specsRoot);
|
|
1170
|
+
} catch {
|
|
1171
|
+
return empty;
|
|
1172
|
+
}
|
|
1173
|
+
if (!rootInfo.isDirectory()) return empty;
|
|
1174
|
+
const totals = { files: 0, dirs: 0, bytes: 0 };
|
|
1175
|
+
const tree = await scanDirectory(specsRoot, "", totals);
|
|
1176
|
+
const sections = tree.filter((node) => node.type === "dir").map((node) => node.name);
|
|
1177
|
+
return {
|
|
1178
|
+
found: true,
|
|
1179
|
+
root: specsRoot,
|
|
1180
|
+
projectRoot,
|
|
1181
|
+
sections,
|
|
1182
|
+
tree,
|
|
1183
|
+
totals,
|
|
1184
|
+
generatedAt
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function frontmatterEndIndex(lines) {
|
|
1188
|
+
if (lines[0]?.trim() !== "---") return -1;
|
|
1189
|
+
return lines.findIndex(
|
|
1190
|
+
(line, index) => index > 0 && (line.trim() === "---" || line.trim() === "...")
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
function stripFrontmatter(markdown) {
|
|
1194
|
+
const lines = markdown.split(/\r?\n/);
|
|
1195
|
+
const end = frontmatterEndIndex(lines);
|
|
1196
|
+
if (end === -1) return markdown;
|
|
1197
|
+
return lines.slice(end + 1).join("\n");
|
|
1198
|
+
}
|
|
1199
|
+
function collectWarnings(markdown) {
|
|
1200
|
+
const warnings = [];
|
|
1201
|
+
const lines = markdown.split(/\r?\n/);
|
|
1202
|
+
if (lines[0]?.trim() === "---" && frontmatterEndIndex(lines) === -1) {
|
|
1203
|
+
warnings.push("El bloque de frontmatter no se cierra (falta `---`).");
|
|
1204
|
+
}
|
|
1205
|
+
return warnings;
|
|
1206
|
+
}
|
|
1207
|
+
function assertRelativePath(requested) {
|
|
1208
|
+
if (requested.trim() === "") {
|
|
1209
|
+
throw new SpecPathError("invalid", requested, "La ruta de la spec est\xE1 vac\xEDa.");
|
|
1210
|
+
}
|
|
1211
|
+
if (requested.includes("\0")) {
|
|
1212
|
+
throw new SpecPathError("invalid", requested, "La ruta de la spec contiene un NUL.");
|
|
1213
|
+
}
|
|
1214
|
+
if (requested.startsWith("/") || requested.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(requested)) {
|
|
1215
|
+
throw new SpecPathError(
|
|
1216
|
+
"invalid",
|
|
1217
|
+
requested,
|
|
1218
|
+
"La ruta de la spec debe ser relativa a specs/."
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
return requested.split(/[\\/]+/).filter((segment) => segment !== "" && segment !== ".");
|
|
1222
|
+
}
|
|
1223
|
+
function resolveSpecLink(baseDir, target) {
|
|
1224
|
+
const stack = baseDir.filter((segment) => segment !== "");
|
|
1225
|
+
for (const segment of target.split(/[\\/]+/)) {
|
|
1226
|
+
if (segment === "" || segment === ".") continue;
|
|
1227
|
+
if (segment === "..") {
|
|
1228
|
+
stack.pop();
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
stack.push(segment);
|
|
1232
|
+
}
|
|
1233
|
+
return stack.join("/");
|
|
1234
|
+
}
|
|
1235
|
+
function normalizeSpecLinks(html, basePath) {
|
|
1236
|
+
const baseDir = basePath.split("/").slice(0, -1);
|
|
1237
|
+
return html.replace(/data-spec-link="([^"]*)"/g, (_match, target) => {
|
|
1238
|
+
return `data-spec-link="${resolveSpecLink(baseDir, target)}"`;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
async function readSpecFile(roots, relativePath) {
|
|
1242
|
+
const segments = assertRelativePath(relativePath);
|
|
1243
|
+
const logicalRoot = resolve(roots.specsRoot);
|
|
1244
|
+
const logical = resolve(logicalRoot, relativePath);
|
|
1245
|
+
if (!isInside(logicalRoot, logical)) {
|
|
1246
|
+
throw new SpecPathError("outside", relativePath, "La spec pedida est\xE1 fuera de specs/.");
|
|
1247
|
+
}
|
|
1248
|
+
if (segments.includes("..")) {
|
|
1249
|
+
throw new SpecPathError(
|
|
1250
|
+
"invalid",
|
|
1251
|
+
relativePath,
|
|
1252
|
+
"La ruta de la spec no puede contener segmentos '..'."
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const realRoot = await realpathOrNull(logicalRoot);
|
|
1256
|
+
if (realRoot === null) {
|
|
1257
|
+
throw new SpecPathError("missing", relativePath, "El proyecto no tiene carpeta specs/.");
|
|
1258
|
+
}
|
|
1259
|
+
const real = await realpathOrNull(logical);
|
|
1260
|
+
if (real === null) {
|
|
1261
|
+
throw new SpecPathError("missing", relativePath, `No existe la spec ${relativePath}.`);
|
|
1262
|
+
}
|
|
1263
|
+
if (!isInside(realRoot, real)) {
|
|
1264
|
+
throw new SpecPathError("outside", relativePath, "La spec resuelve fuera de specs/.");
|
|
1265
|
+
}
|
|
1266
|
+
const info = await stat2(real).catch(() => null);
|
|
1267
|
+
if (info === null) {
|
|
1268
|
+
throw new SpecPathError("missing", relativePath, `No existe la spec ${relativePath}.`);
|
|
1269
|
+
}
|
|
1270
|
+
if (!info.isFile()) {
|
|
1271
|
+
throw new SpecPathError("unsupported", relativePath, "La spec no es un archivo regular.");
|
|
1272
|
+
}
|
|
1273
|
+
const extension = extname2(real).toLowerCase();
|
|
1274
|
+
if (!SUPPORTED_EXTENSIONS.has(extension)) {
|
|
1275
|
+
throw new SpecPathError(
|
|
1276
|
+
"unsupported",
|
|
1277
|
+
relativePath,
|
|
1278
|
+
`Extensi\xF3n no soportada (${extension || "sin extensi\xF3n"}); us\xE1 .md, .markdown o .txt.`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const markdown = await readFile2(real, "utf8").catch(() => null);
|
|
1282
|
+
if (markdown === null) {
|
|
1283
|
+
throw new SpecPathError("missing", relativePath, `No se pudo leer ${relativePath}.`);
|
|
1284
|
+
}
|
|
1285
|
+
const { html: rawHtml, headings } = renderMarkdown(stripFrontmatter(markdown));
|
|
1286
|
+
const level1 = headings.find((heading) => heading.level === 1);
|
|
1287
|
+
const posixPath = relative2(logicalRoot, logical).split(sep2).join("/");
|
|
1288
|
+
const html = normalizeSpecLinks(rawHtml, posixPath);
|
|
1289
|
+
return {
|
|
1290
|
+
path: posixPath,
|
|
1291
|
+
absolutePath: logical,
|
|
1292
|
+
title: level1?.text || posixPath.split("/").pop() || posixPath,
|
|
1293
|
+
markdown,
|
|
1294
|
+
html,
|
|
1295
|
+
headings,
|
|
1296
|
+
bytes: info.size,
|
|
1297
|
+
modified: info.mtime.toISOString(),
|
|
1298
|
+
warnings: collectWarnings(markdown)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/studio/skills.ts
|
|
1303
|
+
import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
1304
|
+
import { homedir } from "os";
|
|
1305
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1306
|
+
|
|
1307
|
+
// src/studio/frontmatter.ts
|
|
1308
|
+
var DELIMITER = "---";
|
|
1309
|
+
var KEY_VALUE = /^([A-Za-z0-9_.-]+)[ \t]*:[ \t]*(.*)$/;
|
|
1310
|
+
var MAX_LINE_IN_WARNING = 80;
|
|
1311
|
+
function splitLines(source) {
|
|
1312
|
+
return source.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
1313
|
+
}
|
|
1314
|
+
function stripQuotes(value) {
|
|
1315
|
+
if (value.length < 2) return value;
|
|
1316
|
+
const first = value[0];
|
|
1317
|
+
const last = value[value.length - 1];
|
|
1318
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
1319
|
+
return value.slice(1, -1);
|
|
1320
|
+
}
|
|
1321
|
+
return value;
|
|
1322
|
+
}
|
|
1323
|
+
function malformed(reason, body) {
|
|
1324
|
+
return { data: {}, warnings: [`malformed frontmatter: ${reason}`], body };
|
|
1325
|
+
}
|
|
1326
|
+
function parseFrontmatter(source) {
|
|
1327
|
+
const lines = splitLines(source);
|
|
1328
|
+
if ((lines[0] ?? "").trimEnd() !== DELIMITER) {
|
|
1329
|
+
return { data: {}, warnings: [], body: source };
|
|
1330
|
+
}
|
|
1331
|
+
let closing = -1;
|
|
1332
|
+
for (let index = 1; index < lines.length; index++) {
|
|
1333
|
+
if (lines[index].trimEnd() === DELIMITER) {
|
|
1334
|
+
closing = index;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (closing === -1) {
|
|
1339
|
+
return {
|
|
1340
|
+
data: {},
|
|
1341
|
+
warnings: ["frontmatter: block is unterminated"],
|
|
1342
|
+
body: ""
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
const data = {};
|
|
1346
|
+
const body = lines.slice(closing + 1).join("\n");
|
|
1347
|
+
let parent = null;
|
|
1348
|
+
for (const raw of lines.slice(1, closing)) {
|
|
1349
|
+
if (!raw.trim()) continue;
|
|
1350
|
+
if (raw.trimStart().startsWith("#")) continue;
|
|
1351
|
+
const indented = /^[ \t]/.test(raw);
|
|
1352
|
+
const match = KEY_VALUE.exec(raw.trim());
|
|
1353
|
+
if (!match) {
|
|
1354
|
+
return malformed(
|
|
1355
|
+
`line is not "key: value": ${raw.trim().slice(0, MAX_LINE_IN_WARNING)}`,
|
|
1356
|
+
body
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
const key = match[1];
|
|
1360
|
+
const rawValue = match[2].trim();
|
|
1361
|
+
const value = stripQuotes(rawValue);
|
|
1362
|
+
if (indented) {
|
|
1363
|
+
if (parent === null) {
|
|
1364
|
+
return malformed(`indented key without a parent block: ${key}`, body);
|
|
1365
|
+
}
|
|
1366
|
+
if (rawValue === "") {
|
|
1367
|
+
return malformed(`nested key has no value: ${parent}.${key}`, body);
|
|
1368
|
+
}
|
|
1369
|
+
data[`${parent}.${key}`] = value;
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (rawValue === "") {
|
|
1373
|
+
parent = key;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
parent = null;
|
|
1377
|
+
data[key] = value;
|
|
1378
|
+
}
|
|
1379
|
+
return { data, warnings: [], body };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/studio/skills.ts
|
|
1383
|
+
var ENTRY_FILE = "skill.md";
|
|
1384
|
+
var COMMAND_FILE = "command.md";
|
|
1385
|
+
function isMissing(error) {
|
|
1386
|
+
const code = error?.code;
|
|
1387
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
1388
|
+
}
|
|
1389
|
+
function resolveSkillTargets(options) {
|
|
1390
|
+
const home = resolve2(options.home ?? homedir());
|
|
1391
|
+
const projectRoot = resolve2(options.projectRoot);
|
|
1392
|
+
const gentleShellRoot = process.env.GENTLE_SHELL_HOME?.trim() || join3(home, ".gentle-shell", "agent");
|
|
1393
|
+
return [
|
|
1394
|
+
{
|
|
1395
|
+
id: "opencode",
|
|
1396
|
+
label: "opencode",
|
|
1397
|
+
globalDir: join3(home, ".config", "opencode", "skills"),
|
|
1398
|
+
projectDir: join3(projectRoot, ".opencode", "skills")
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
id: "pi",
|
|
1402
|
+
label: "pi",
|
|
1403
|
+
globalDir: join3(home, ".pi", "agent", "skills"),
|
|
1404
|
+
projectDir: join3(projectRoot, ".pi", "skills")
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
id: "gentle-shell",
|
|
1408
|
+
label: "Gentle Shell",
|
|
1409
|
+
globalDir: join3(gentleShellRoot, "skills"),
|
|
1410
|
+
projectDir: join3(projectRoot, ".gentle-shell", "skills")
|
|
1411
|
+
}
|
|
1412
|
+
];
|
|
1413
|
+
}
|
|
1414
|
+
function compareNames(a, b) {
|
|
1415
|
+
const left = a.toLowerCase();
|
|
1416
|
+
const right = b.toLowerCase();
|
|
1417
|
+
if (left < right) return -1;
|
|
1418
|
+
if (left > right) return 1;
|
|
1419
|
+
if (a < b) return -1;
|
|
1420
|
+
if (a > b) return 1;
|
|
1421
|
+
return 0;
|
|
1422
|
+
}
|
|
1423
|
+
function firstProseLine(body) {
|
|
1424
|
+
let inFence = false;
|
|
1425
|
+
for (const raw of body.split(/\r?\n/)) {
|
|
1426
|
+
const line = raw.trim();
|
|
1427
|
+
if (!line) continue;
|
|
1428
|
+
if (/^(?:```|~~~)/.test(line)) {
|
|
1429
|
+
inFence = !inFence;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (inFence) continue;
|
|
1433
|
+
if (line.startsWith("#")) continue;
|
|
1434
|
+
if (line === "---") continue;
|
|
1435
|
+
return line;
|
|
1436
|
+
}
|
|
1437
|
+
return "";
|
|
1438
|
+
}
|
|
1439
|
+
async function measureDirectory(dir) {
|
|
1440
|
+
let files = 0;
|
|
1441
|
+
let bytes = 0;
|
|
1442
|
+
let entries;
|
|
1443
|
+
try {
|
|
1444
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1445
|
+
} catch {
|
|
1446
|
+
return { files, bytes };
|
|
1447
|
+
}
|
|
1448
|
+
for (const entry of entries) {
|
|
1449
|
+
if (entry.isSymbolicLink()) continue;
|
|
1450
|
+
const path = join3(dir, entry.name);
|
|
1451
|
+
if (entry.isDirectory()) {
|
|
1452
|
+
const nested = await measureDirectory(path);
|
|
1453
|
+
files += nested.files;
|
|
1454
|
+
bytes += nested.bytes;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (!entry.isFile()) continue;
|
|
1458
|
+
try {
|
|
1459
|
+
const info = await stat3(path);
|
|
1460
|
+
files += 1;
|
|
1461
|
+
bytes += info.size;
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return { files, bytes };
|
|
1466
|
+
}
|
|
1467
|
+
async function readSkill(dir, name, target, scope, warnings) {
|
|
1468
|
+
let entries;
|
|
1469
|
+
try {
|
|
1470
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1471
|
+
} catch {
|
|
1472
|
+
warnings.push(`cannot read skill directory: ${dir}`);
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
const entryFile = entries.find(
|
|
1476
|
+
(entry) => entry.isFile() && entry.name.toLowerCase() === ENTRY_FILE
|
|
1477
|
+
);
|
|
1478
|
+
if (!entryFile) return null;
|
|
1479
|
+
const entryPath = join3(dir, entryFile.name);
|
|
1480
|
+
let source;
|
|
1481
|
+
let entryInfo;
|
|
1482
|
+
try {
|
|
1483
|
+
source = await readFile3(entryPath, "utf-8");
|
|
1484
|
+
entryInfo = await stat3(entryPath);
|
|
1485
|
+
} catch {
|
|
1486
|
+
warnings.push(`cannot read skill entry file: ${entryPath}`);
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
const parsed = parseFrontmatter(source);
|
|
1490
|
+
const { files, bytes } = await measureDirectory(dir);
|
|
1491
|
+
const version = parsed.data["metadata.version"]?.trim();
|
|
1492
|
+
const author = parsed.data["metadata.author"]?.trim();
|
|
1493
|
+
const license = parsed.data["license"]?.trim();
|
|
1494
|
+
const description = parsed.data["description"]?.trim() || firstProseLine(parsed.body);
|
|
1495
|
+
return {
|
|
1496
|
+
name,
|
|
1497
|
+
description,
|
|
1498
|
+
version: version || void 0,
|
|
1499
|
+
license: license || void 0,
|
|
1500
|
+
author: author || void 0,
|
|
1501
|
+
dir,
|
|
1502
|
+
path: entryPath,
|
|
1503
|
+
target,
|
|
1504
|
+
scope,
|
|
1505
|
+
files,
|
|
1506
|
+
bytes,
|
|
1507
|
+
modified: entryInfo.mtime.toISOString(),
|
|
1508
|
+
hasCommand: entries.some(
|
|
1509
|
+
(entry) => entry.isFile() && entry.name.toLowerCase() === COMMAND_FILE
|
|
1510
|
+
),
|
|
1511
|
+
warnings: parsed.warnings
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
async function scanSkillsDir(dir, target, scope, warnings) {
|
|
1515
|
+
let info;
|
|
1516
|
+
try {
|
|
1517
|
+
info = await stat3(dir);
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
if (isMissing(error)) return { exists: false, skills: [] };
|
|
1520
|
+
warnings.push(`cannot read skills directory: ${dir}`);
|
|
1521
|
+
return { exists: false, skills: [] };
|
|
1522
|
+
}
|
|
1523
|
+
if (!info.isDirectory()) return { exists: false, skills: [] };
|
|
1524
|
+
let entries;
|
|
1525
|
+
try {
|
|
1526
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1527
|
+
} catch {
|
|
1528
|
+
warnings.push(`cannot list skills directory: ${dir}`);
|
|
1529
|
+
return { exists: true, skills: [] };
|
|
1530
|
+
}
|
|
1531
|
+
const skills = [];
|
|
1532
|
+
for (const entry of entries) {
|
|
1533
|
+
if (entry.isSymbolicLink()) {
|
|
1534
|
+
warnings.push(`skipped symlinked skill directory: ${join3(dir, entry.name)}`);
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (!entry.isDirectory()) continue;
|
|
1538
|
+
const skill = await readSkill(
|
|
1539
|
+
join3(dir, entry.name),
|
|
1540
|
+
entry.name,
|
|
1541
|
+
target,
|
|
1542
|
+
scope,
|
|
1543
|
+
warnings
|
|
1544
|
+
);
|
|
1545
|
+
if (skill) skills.push(skill);
|
|
1546
|
+
}
|
|
1547
|
+
skills.sort((a, b) => compareNames(a.name, b.name));
|
|
1548
|
+
return { exists: true, skills };
|
|
1549
|
+
}
|
|
1550
|
+
async function discoverSkills(options) {
|
|
1551
|
+
const projectRoot = resolve2(options.projectRoot);
|
|
1552
|
+
const warnings = [];
|
|
1553
|
+
const targets = [];
|
|
1554
|
+
const totals = { global: 0, project: 0 };
|
|
1555
|
+
for (const spec of resolveSkillTargets(options)) {
|
|
1556
|
+
const globalScan = await scanSkillsDir(
|
|
1557
|
+
spec.globalDir,
|
|
1558
|
+
spec.id,
|
|
1559
|
+
"global",
|
|
1560
|
+
warnings
|
|
1561
|
+
);
|
|
1562
|
+
const projectScan = await scanSkillsDir(
|
|
1563
|
+
spec.projectDir,
|
|
1564
|
+
spec.id,
|
|
1565
|
+
"project",
|
|
1566
|
+
warnings
|
|
1567
|
+
);
|
|
1568
|
+
totals.global += globalScan.skills.length;
|
|
1569
|
+
totals.project += projectScan.skills.length;
|
|
1570
|
+
const skills = [...globalScan.skills, ...projectScan.skills].sort(
|
|
1571
|
+
(a, b) => compareNames(a.name, b.name)
|
|
1572
|
+
);
|
|
1573
|
+
targets.push({
|
|
1574
|
+
id: spec.id,
|
|
1575
|
+
label: spec.label,
|
|
1576
|
+
globalDir: spec.globalDir,
|
|
1577
|
+
projectDir: spec.projectDir,
|
|
1578
|
+
globalExists: globalScan.exists,
|
|
1579
|
+
projectExists: projectScan.exists,
|
|
1580
|
+
skills
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return {
|
|
1584
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1585
|
+
projectRoot,
|
|
1586
|
+
targets,
|
|
1587
|
+
totals,
|
|
1588
|
+
warnings
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/studio/ui/client.ts
|
|
1593
|
+
var CLIENT_JS = `
|
|
1594
|
+
(function () {
|
|
1595
|
+
'use strict';
|
|
1596
|
+
|
|
1597
|
+
var THEME_KEY = 'fwinit:theme';
|
|
1598
|
+
var metaEl = document.getElementById('studio-meta');
|
|
1599
|
+
var meta = metaEl ? JSON.parse(metaEl.textContent || '{}') : {};
|
|
1600
|
+
|
|
1601
|
+
var state = {
|
|
1602
|
+
view: 'skills',
|
|
1603
|
+
scope: 'global',
|
|
1604
|
+
skillKey: null,
|
|
1605
|
+
specPath: null,
|
|
1606
|
+
filter: { skills: '', specs: '' },
|
|
1607
|
+
skills: null,
|
|
1608
|
+
specs: null,
|
|
1609
|
+
doc: null,
|
|
1610
|
+
collapsed: {},
|
|
1611
|
+
expanded: {},
|
|
1612
|
+
tocLinks: []
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
function q(sel, root) {
|
|
1616
|
+
return (root || document).querySelector(sel);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function qa(sel, root) {
|
|
1620
|
+
return Array.prototype.slice.call((root || document).querySelectorAll(sel));
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function el(tag, props, children) {
|
|
1624
|
+
var node = document.createElement(tag);
|
|
1625
|
+
if (props) {
|
|
1626
|
+
Object.keys(props).forEach(function (key) {
|
|
1627
|
+
var value = props[key];
|
|
1628
|
+
if (value === undefined || value === null || value === false) return;
|
|
1629
|
+
if (key === 'class') node.className = value;
|
|
1630
|
+
else if (key === 'text') node.textContent = String(value);
|
|
1631
|
+
else if (key === 'dataset') {
|
|
1632
|
+
Object.keys(value).forEach(function (dataKey) {
|
|
1633
|
+
node.dataset[dataKey] = String(value[dataKey]);
|
|
1634
|
+
});
|
|
1635
|
+
} else if (key.indexOf('on') === 0 && typeof value === 'function') {
|
|
1636
|
+
node.addEventListener(key.slice(2).toLowerCase(), value);
|
|
1637
|
+
} else if (key === 'html') {
|
|
1638
|
+
node.innerHTML = value;
|
|
1639
|
+
} else {
|
|
1640
|
+
node.setAttribute(key, String(value));
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
(children || []).forEach(function (child) {
|
|
1645
|
+
if (child === null || child === undefined || child === false) return;
|
|
1646
|
+
node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
|
1647
|
+
});
|
|
1648
|
+
return node;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function fmtBytes(bytes) {
|
|
1652
|
+
if (typeof bytes !== 'number' || !isFinite(bytes)) return '\u2014';
|
|
1653
|
+
if (bytes < 1024) return bytes + ' B';
|
|
1654
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
1655
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function fmtDate(iso) {
|
|
1659
|
+
if (!iso) return '\u2014';
|
|
1660
|
+
var date = new Date(iso);
|
|
1661
|
+
if (isNaN(date.getTime())) return '\u2014';
|
|
1662
|
+
var pad = function (value) { return String(value).padStart(2, '0'); };
|
|
1663
|
+
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
|
|
1664
|
+
' ' + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function shortPath(path) {
|
|
1668
|
+
var home = meta.home || '';
|
|
1669
|
+
if (home && path.indexOf(home) === 0) return '~' + path.slice(home.length);
|
|
1670
|
+
return path;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function copyValue(value, button) {
|
|
1674
|
+
var run = function () {
|
|
1675
|
+
var done = function () {
|
|
1676
|
+
if (!button) return;
|
|
1677
|
+
var previous = button.getAttribute('data-label') || button.textContent;
|
|
1678
|
+
button.setAttribute('data-label', previous);
|
|
1679
|
+
button.setAttribute('data-copied', 'true');
|
|
1680
|
+
button.textContent = 'Copiado';
|
|
1681
|
+
setTimeout(function () {
|
|
1682
|
+
button.removeAttribute('data-copied');
|
|
1683
|
+
button.textContent = previous;
|
|
1684
|
+
}, 1400);
|
|
1685
|
+
};
|
|
1686
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1687
|
+
navigator.clipboard.writeText(value).then(done, function () {});
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
var area = el('textarea', { style: 'position:fixed;opacity:0' });
|
|
1691
|
+
area.value = value;
|
|
1692
|
+
document.body.appendChild(area);
|
|
1693
|
+
area.select();
|
|
1694
|
+
try { document.execCommand('copy'); done(); } catch (error) { /* ignore */ }
|
|
1695
|
+
document.body.removeChild(area);
|
|
1696
|
+
};
|
|
1697
|
+
run();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function copyButton(label, value) {
|
|
1701
|
+
return el('button', {
|
|
1702
|
+
type: 'button',
|
|
1703
|
+
class: 'btn',
|
|
1704
|
+
'aria-label': 'Copiar ' + label,
|
|
1705
|
+
text: label,
|
|
1706
|
+
onclick: function (event) { copyValue(value, event.currentTarget); }
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/* \u2500\u2500 Theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1711
|
+
|
|
1712
|
+
function systemTheme() {
|
|
1713
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function currentTheme() {
|
|
1717
|
+
var stored = document.documentElement.dataset.theme;
|
|
1718
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
1719
|
+
return systemTheme();
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function applyTheme(theme) {
|
|
1723
|
+
document.documentElement.dataset.theme = theme;
|
|
1724
|
+
try { localStorage.setItem(THEME_KEY, theme); } catch (error) { /* private mode */ }
|
|
1725
|
+
var button = q('[data-theme-toggle]');
|
|
1726
|
+
if (!button) return;
|
|
1727
|
+
var isLight = theme === 'light';
|
|
1728
|
+
button.setAttribute('aria-pressed', isLight ? 'true' : 'false');
|
|
1729
|
+
button.setAttribute('aria-label', isLight ? 'Cambiar al tema oscuro' : 'Cambiar al tema claro');
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function toggleTheme() {
|
|
1733
|
+
applyTheme(currentTheme() === 'light' ? 'dark' : 'light');
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/* \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1737
|
+
|
|
1738
|
+
function fetchJson(url) {
|
|
1739
|
+
return fetch(url, { headers: { accept: 'application/json' } }).then(function (response) {
|
|
1740
|
+
if (response.ok) return response.json();
|
|
1741
|
+
return response.json().catch(function () { return {}; }).then(function (body) {
|
|
1742
|
+
var error = new Error(body.error || 'La solicitud fall\xF3 (' + response.status + ')');
|
|
1743
|
+
error.status = response.status;
|
|
1744
|
+
throw error;
|
|
1745
|
+
});
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function loadSkills() {
|
|
1750
|
+
if (state.skills) return Promise.resolve(state.skills);
|
|
1751
|
+
return fetchJson('/api/skills').then(function (data) {
|
|
1752
|
+
state.skills = data;
|
|
1753
|
+
return data;
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function loadSpecs() {
|
|
1758
|
+
if (state.specs) return Promise.resolve(state.specs);
|
|
1759
|
+
return fetchJson('/api/specs').then(function (data) {
|
|
1760
|
+
state.specs = data;
|
|
1761
|
+
return data;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/* \u2500\u2500 Routing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1766
|
+
|
|
1767
|
+
function route() {
|
|
1768
|
+
var raw = (location.hash || '#/skills').replace(/^#\\/?/, '');
|
|
1769
|
+
var parts = raw.split('/').map(function (part) {
|
|
1770
|
+
try { return decodeURIComponent(part); } catch (error) { return part; }
|
|
1771
|
+
});
|
|
1772
|
+
state.view = parts[0] === 'specs' ? 'specs' : 'skills';
|
|
1773
|
+
if (state.view === 'skills') {
|
|
1774
|
+
if (parts.length >= 4) {
|
|
1775
|
+
state.scope = parts[1] === 'project' ? 'project' : 'global';
|
|
1776
|
+
state.skillKey = parts.slice(2).join('/');
|
|
1777
|
+
}
|
|
1778
|
+
} else {
|
|
1779
|
+
var nextPath = parts.slice(1).join('/') || null;
|
|
1780
|
+
if (state.doc && state.doc.path !== nextPath) state.doc = null;
|
|
1781
|
+
state.specPath = nextPath;
|
|
1782
|
+
}
|
|
1783
|
+
render();
|
|
1784
|
+
// Cambiar de ruta tiene que pedir el documento: render() s\xF3lo dibuja el
|
|
1785
|
+
// estado que ya tiene en memoria.
|
|
1786
|
+
if (state.view === 'specs') loadSpecDoc();
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function navigate(hash) {
|
|
1790
|
+
if (location.hash === hash) render();
|
|
1791
|
+
else location.hash = hash;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function skillHash(target, name) {
|
|
1795
|
+
return '#/skills/' + state.scope + '/' + encodeURIComponent(target) + '/' + encodeURIComponent(name);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function specHash(path) {
|
|
1799
|
+
return '#/specs/' + path.split('/').map(encodeURIComponent).join('/');
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/* \u2500\u2500 Shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1803
|
+
|
|
1804
|
+
function syncShell() {
|
|
1805
|
+
qa('[data-view]').forEach(function (tab) {
|
|
1806
|
+
tab.setAttribute('aria-selected', tab.dataset.view === state.view ? 'true' : 'false');
|
|
1807
|
+
});
|
|
1808
|
+
qa('[data-view-panel]').forEach(function (panel) {
|
|
1809
|
+
panel.dataset.active = panel.dataset.viewPanel === state.view ? 'true' : 'false';
|
|
1810
|
+
});
|
|
1811
|
+
var skillsCount = q('[data-count="skills"]');
|
|
1812
|
+
var specsCount = q('[data-count="specs"]');
|
|
1813
|
+
if (skillsCount && state.skills) {
|
|
1814
|
+
skillsCount.textContent = String(state.skills.totals.global + state.skills.totals.project);
|
|
1815
|
+
}
|
|
1816
|
+
if (specsCount && state.specs) {
|
|
1817
|
+
specsCount.textContent = String(state.specs.totals.files);
|
|
1818
|
+
}
|
|
1819
|
+
var counts = q('[data-status-counts]');
|
|
1820
|
+
if (counts) {
|
|
1821
|
+
var bits = [];
|
|
1822
|
+
if (state.skills) bits.push(state.skills.totals.global + ' skills globales');
|
|
1823
|
+
if (state.skills) bits.push(state.skills.totals.project + ' skills del proyecto');
|
|
1824
|
+
if (state.specs) bits.push(state.specs.totals.files + ' archivos de specs');
|
|
1825
|
+
counts.textContent = bits.join(' \xB7 ');
|
|
1826
|
+
}
|
|
1827
|
+
var root = q('[data-status-root]');
|
|
1828
|
+
if (root) {
|
|
1829
|
+
root.textContent = shortPath(meta.projectRoot || '');
|
|
1830
|
+
root.title = meta.projectRoot || '';
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function emptyState(options) {
|
|
1835
|
+
return el('div', { class: 'empty' }, [
|
|
1836
|
+
el('p', { class: 'empty__title', text: options.title }),
|
|
1837
|
+
options.text ? el('p', { class: 'empty__text', text: options.text }) : null,
|
|
1838
|
+
options.path ? el('p', { class: 'empty__path', text: options.path }) : null,
|
|
1839
|
+
options.hint ? el('p', { class: 'empty__hint', text: options.hint }) : null
|
|
1840
|
+
]);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function renderError(container, error) {
|
|
1844
|
+
container.textContent = '';
|
|
1845
|
+
container.appendChild(emptyState({
|
|
1846
|
+
title: 'El studio no pudo leer esto',
|
|
1847
|
+
text: error && error.message ? error.message : 'Error desconocido.',
|
|
1848
|
+
hint: 'Volv\xE9 a ejecutar fwinit studio desde la ra\xEDz del proyecto.'
|
|
1849
|
+
}));
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/* \u2500\u2500 Skills view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1853
|
+
|
|
1854
|
+
function visibleSkills(report, scope) {
|
|
1855
|
+
var filter = state.filter.skills.trim().toLowerCase();
|
|
1856
|
+
var out = [];
|
|
1857
|
+
(report.targets || []).forEach(function (target) {
|
|
1858
|
+
(target.skills || []).forEach(function (skill) {
|
|
1859
|
+
if (skill.scope !== scope) return;
|
|
1860
|
+
if (filter) {
|
|
1861
|
+
var haystack = (skill.name + ' ' + (skill.description || '') + ' ' + (skill.version || '')).toLowerCase();
|
|
1862
|
+
if (haystack.indexOf(filter) === -1) return;
|
|
1863
|
+
}
|
|
1864
|
+
out.push(skill);
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
return out;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function skillMetaLine(skill) {
|
|
1871
|
+
var bits = [];
|
|
1872
|
+
bits.push(skill.files + (skill.files === 1 ? ' archivo' : ' archivos'));
|
|
1873
|
+
if (skill.version) bits.push('v' + skill.version);
|
|
1874
|
+
if (skill.hasCommand) bits.push('command');
|
|
1875
|
+
return bits.join(' \xB7 ');
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function renderSkillList() {
|
|
1879
|
+
var container = q('[data-list="skills"]');
|
|
1880
|
+
if (!container) return;
|
|
1881
|
+
container.textContent = '';
|
|
1882
|
+
var report = state.skills;
|
|
1883
|
+
if (!report) {
|
|
1884
|
+
container.appendChild(el('p', { class: 'loading', text: 'Leyendo las skills instaladas\u2026' }));
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
var shown = visibleSkills(report, state.scope);
|
|
1888
|
+
if (!shown.length) {
|
|
1889
|
+
container.appendChild(emptyState({
|
|
1890
|
+
title: state.filter.skills ? 'Ninguna skill coincide con el filtro' : 'No hay skills en este alcance',
|
|
1891
|
+
text: state.filter.skills
|
|
1892
|
+
? 'Limpi\xE1 el filtro para ver todo lo instalado.'
|
|
1893
|
+
: 'Instalalas con: fwinit skills --global ' + state.scope,
|
|
1894
|
+
hint: String(shown.length) + ' de ' + String(report.totals[state.scope]) + ' en este alcance'
|
|
1895
|
+
}));
|
|
1896
|
+
}
|
|
1897
|
+
(report.targets || []).forEach(function (target) {
|
|
1898
|
+
var skills = (target.skills || []).filter(function (skill) {
|
|
1899
|
+
return skill.scope === state.scope && shown.indexOf(skill) !== -1;
|
|
1900
|
+
});
|
|
1901
|
+
var dir = state.scope === 'global' ? target.globalDir : target.projectDir;
|
|
1902
|
+
var exists = state.scope === 'global' ? target.globalExists : target.projectExists;
|
|
1903
|
+
var collapsed = state.collapsed[state.scope + ':' + target.id] === true;
|
|
1904
|
+
var group = el('div', { class: 'group' });
|
|
1905
|
+
|
|
1906
|
+
group.appendChild(el('button', {
|
|
1907
|
+
type: 'button',
|
|
1908
|
+
class: 'group__head',
|
|
1909
|
+
'aria-expanded': collapsed ? 'false' : 'true',
|
|
1910
|
+
onclick: function () {
|
|
1911
|
+
state.collapsed[state.scope + ':' + target.id] = !collapsed;
|
|
1912
|
+
renderSkillList();
|
|
1913
|
+
}
|
|
1914
|
+
}, [
|
|
1915
|
+
target.label,
|
|
1916
|
+
el('span', { class: 'group__count', text: String(skills.length) })
|
|
1917
|
+
]));
|
|
1918
|
+
|
|
1919
|
+
if (!collapsed) {
|
|
1920
|
+
group.appendChild(el('p', { class: 'group__path', title: dir, text: shortPath(dir) }));
|
|
1921
|
+
if (!exists || !skills.length) {
|
|
1922
|
+
group.appendChild(el('p', {
|
|
1923
|
+
class: 'row--empty',
|
|
1924
|
+
text: exists ? 'No hay skills ac\xE1' : 'No instalado'
|
|
1925
|
+
}));
|
|
1926
|
+
}
|
|
1927
|
+
skills.forEach(function (skill) {
|
|
1928
|
+
var key = skill.target + '/' + skill.name;
|
|
1929
|
+
group.appendChild(el('button', {
|
|
1930
|
+
type: 'button',
|
|
1931
|
+
class: 'row',
|
|
1932
|
+
'aria-current': state.skillKey === key ? 'true' : 'false',
|
|
1933
|
+
title: skill.description || skill.name,
|
|
1934
|
+
onclick: function () { navigate(skillHash(skill.target, skill.name)); }
|
|
1935
|
+
}, [
|
|
1936
|
+
el('span', { class: 'row__name', text: skill.name }),
|
|
1937
|
+
el('span', { class: 'row__meta', text: skillMetaLine(skill) })
|
|
1938
|
+
]));
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
container.appendChild(group);
|
|
1942
|
+
});
|
|
1943
|
+
// Avisos del reporte (directorios ilegibles o symlinks salteados): sin esto
|
|
1944
|
+
// una skill salteada desaparece de la lista sin explicaci\xF3n.
|
|
1945
|
+
(report.warnings || []).forEach(function (warning) {
|
|
1946
|
+
container.appendChild(el('p', { class: 'row--empty', text: 'Aviso: ' + warning }));
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function selectedSkill() {
|
|
1951
|
+
if (!state.skills || !state.skillKey) return null;
|
|
1952
|
+
var parts = state.skillKey.split('/');
|
|
1953
|
+
var target = parts[0];
|
|
1954
|
+
var name = parts.slice(1).join('/');
|
|
1955
|
+
var found = null;
|
|
1956
|
+
(state.skills.targets || []).forEach(function (entry) {
|
|
1957
|
+
if (entry.id !== target) return;
|
|
1958
|
+
(entry.skills || []).forEach(function (skill) {
|
|
1959
|
+
if (skill.scope !== state.scope) return;
|
|
1960
|
+
if (skill.name !== name) return;
|
|
1961
|
+
found = skill;
|
|
1962
|
+
});
|
|
1963
|
+
});
|
|
1964
|
+
return found;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function metaRow(key, value, strong) {
|
|
1968
|
+
return el('div', { class: 'meta__row' }, [
|
|
1969
|
+
el('span', { class: 'meta__key', text: key }),
|
|
1970
|
+
el('span', { class: 'meta__value' + (strong ? ' meta__value--strong' : ''), text: value === undefined || value === '' ? '\u2014' : String(value) })
|
|
1971
|
+
]);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function renderSkillDetail() {
|
|
1975
|
+
var container = q('[data-detail="skills"]');
|
|
1976
|
+
if (!container) return;
|
|
1977
|
+
container.textContent = '';
|
|
1978
|
+
if (!state.skills) {
|
|
1979
|
+
container.appendChild(el('p', { class: 'loading', text: 'Leyendo las skills instaladas\u2026' }));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
var skill = selectedSkill();
|
|
1983
|
+
if (!skill) {
|
|
1984
|
+
var target = (state.skills.targets || [])[0] || null;
|
|
1985
|
+
var dir = target ? (state.scope === 'global' ? target.globalDir : target.projectDir) : '';
|
|
1986
|
+
container.appendChild(emptyState({
|
|
1987
|
+
title: 'Eleg\xED una skill',
|
|
1988
|
+
text: 'Las skills de este alcance est\xE1n a la izquierda, agrupadas por la herramienta que las lee.',
|
|
1989
|
+
path: dir ? shortPath(dir) : undefined,
|
|
1990
|
+
hint: 'fwinit skills --global opencode|pi|gentle-shell'
|
|
1991
|
+
}));
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
var report = null;
|
|
1995
|
+
(state.skills.targets || []).forEach(function (entry) {
|
|
1996
|
+
if (entry.id === skill.target) report = entry;
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
container.appendChild(el('header', null, [
|
|
2000
|
+
el('p', { class: 'eyebrow', text: (report ? report.label : skill.target) + ' \xB7 ' + skill.scope }),
|
|
2001
|
+
el('h1', { class: 'detail__title', text: skill.name }),
|
|
2002
|
+
el('p', { class: 'detail__lede', text: skill.description || 'Esta skill no tiene descripci\xF3n en su frontmatter.' }),
|
|
2003
|
+
el('div', { class: 'detail__actions' }, [
|
|
2004
|
+
copyButton('Copiar ruta', skill.path),
|
|
2005
|
+
copyButton('Copiar nombre', skill.name),
|
|
2006
|
+
skill.version ? el('span', { class: 'badge badge--accent', text: 'v' + skill.version }) : null,
|
|
2007
|
+
el('span', { class: 'badge badge--muted', text: skill.files + (skill.files === 1 ? ' archivo' : ' archivos') }),
|
|
2008
|
+
skill.hasCommand ? el('span', { class: 'badge badge--muted', text: 'command.md' }) : null
|
|
2009
|
+
])
|
|
2010
|
+
]));
|
|
2011
|
+
|
|
2012
|
+
container.appendChild(el('div', { class: 'meta' }, [
|
|
2013
|
+
metaRow('Directorio', shortPath(skill.dir), true),
|
|
2014
|
+
metaRow('Archivo', skill.path.replace(meta.projectRoot || '', '')),
|
|
2015
|
+
metaRow('Destino', report ? report.label : skill.target),
|
|
2016
|
+
metaRow('Alcance', skill.scope === 'global' ? 'instalaci\xF3n global del usuario' : 'este proyecto'),
|
|
2017
|
+
metaRow('Versi\xF3n', skill.version),
|
|
2018
|
+
metaRow('Licencia', skill.license),
|
|
2019
|
+
metaRow('Autor', skill.author),
|
|
2020
|
+
metaRow('Archivos', String(skill.files)),
|
|
2021
|
+
metaRow('Tama\xF1o', fmtBytes(skill.bytes)),
|
|
2022
|
+
metaRow('Modificado', fmtDate(skill.modified))
|
|
2023
|
+
]));
|
|
2024
|
+
|
|
2025
|
+
if (skill.warnings && skill.warnings.length) {
|
|
2026
|
+
container.appendChild(el('div', { class: 'meta' }, skill.warnings.map(function (warning) {
|
|
2027
|
+
return el('div', { class: 'meta__row' }, [
|
|
2028
|
+
el('span', { class: 'meta__key', text: 'Aviso' }),
|
|
2029
|
+
el('span', { class: 'meta__value', text: warning })
|
|
2030
|
+
]);
|
|
2031
|
+
})));
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/* \u2500\u2500 Specs view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2036
|
+
|
|
2037
|
+
function matchesFilter(node, filter) {
|
|
2038
|
+
if (!filter) return true;
|
|
2039
|
+
if (node.path.toLowerCase().indexOf(filter) !== -1) return true;
|
|
2040
|
+
return (node.children || []).some(function (child) { return matchesFilter(child, filter); });
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function renderTreeNodes(nodes, depth, container) {
|
|
2044
|
+
var filter = state.filter.specs.trim().toLowerCase();
|
|
2045
|
+
(nodes || []).forEach(function (node) {
|
|
2046
|
+
if (!matchesFilter(node, filter)) return;
|
|
2047
|
+
if (node.type === 'dir') {
|
|
2048
|
+
var key = node.path;
|
|
2049
|
+
var open = filter ? true : (state.expanded[key] === true || (state.expanded[key] !== false && depth < 2));
|
|
2050
|
+
container.appendChild(el('button', {
|
|
2051
|
+
type: 'button',
|
|
2052
|
+
class: 'row row--dir',
|
|
2053
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
2054
|
+
onclick: function () {
|
|
2055
|
+
state.expanded[key] = !open;
|
|
2056
|
+
renderSpecTree();
|
|
2057
|
+
}
|
|
2058
|
+
}, [
|
|
2059
|
+
el('span', { class: 'tree__toggle' }, [
|
|
2060
|
+
el('span', { class: 'tree__chevron', dataset: { open: open ? 'true' : 'false' }, text: '\u203A' }),
|
|
2061
|
+
node.name
|
|
2062
|
+
]),
|
|
2063
|
+
el('span', { class: 'row__meta', text: String(countFiles(node)) + ' arch.' })
|
|
2064
|
+
]));
|
|
2065
|
+
if (open) {
|
|
2066
|
+
var kids = el('div', { class: 'tree__children' });
|
|
2067
|
+
renderTreeNodes(node.children, depth + 1, kids);
|
|
2068
|
+
container.appendChild(kids);
|
|
2069
|
+
}
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
container.appendChild(el('button', {
|
|
2073
|
+
type: 'button',
|
|
2074
|
+
class: 'row row--file',
|
|
2075
|
+
'aria-current': state.specPath === node.path ? 'true' : 'false',
|
|
2076
|
+
title: node.path,
|
|
2077
|
+
onclick: function () { navigate(specHash(node.path)); }
|
|
2078
|
+
}, [
|
|
2079
|
+
el('span', { class: 'row__name', text: node.name }),
|
|
2080
|
+
el('span', { class: 'row__meta', text: fmtBytes(node.bytes) })
|
|
2081
|
+
]));
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function countFiles(node) {
|
|
2086
|
+
if (node.type === 'file') return 1;
|
|
2087
|
+
var total = 0;
|
|
2088
|
+
(node.children || []).forEach(function (child) { total += countFiles(child); });
|
|
2089
|
+
return total;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function renderSpecTree() {
|
|
2093
|
+
var container = q('[data-list="specs"]');
|
|
2094
|
+
if (!container) return;
|
|
2095
|
+
container.textContent = '';
|
|
2096
|
+
var specs = state.specs;
|
|
2097
|
+
if (!specs) {
|
|
2098
|
+
container.appendChild(el('p', { class: 'loading', text: 'Buscando specs/\u2026' }));
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (!specs.found) {
|
|
2102
|
+
container.appendChild(el('p', { class: 'row--empty', text: 'Este proyecto no tiene carpeta specs/' }));
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (!specs.tree.length) {
|
|
2106
|
+
container.appendChild(el('p', { class: 'row--empty', text: 'specs/ est\xE1 vac\xEDa' }));
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
var tree = el('div', { class: 'tree' });
|
|
2110
|
+
renderTreeNodes(specs.tree, 0, tree);
|
|
2111
|
+
container.appendChild(tree);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
var SANITIZE_TAGS = ['script', 'style', 'iframe', 'object', 'embed', 'link', 'meta', 'form', 'base', 'noscript'];
|
|
2115
|
+
|
|
2116
|
+
function sanitize(html) {
|
|
2117
|
+
var doc = new DOMParser().parseFromString('<div id="studio-root">' + html + '</div>', 'text/html');
|
|
2118
|
+
var root = doc.getElementById('studio-root');
|
|
2119
|
+
if (!root) return '';
|
|
2120
|
+
qa(SANITIZE_TAGS.join(','), root).forEach(function (node) { node.remove(); });
|
|
2121
|
+
qa('*', root).forEach(function (node) {
|
|
2122
|
+
Array.prototype.slice.call(node.attributes).forEach(function (attribute) {
|
|
2123
|
+
var name = attribute.name.toLowerCase();
|
|
2124
|
+
if (name.indexOf('on') === 0 || name === 'style' || name === 'srcdoc') {
|
|
2125
|
+
node.removeAttribute(attribute.name);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (name === 'href' || name === 'src' || name === 'xlink:href') {
|
|
2129
|
+
var value = String(attribute.value).trim().toLowerCase();
|
|
2130
|
+
var safe = value.indexOf('http://') === 0 || value.indexOf('https://') === 0 ||
|
|
2131
|
+
value.indexOf('mailto:') === 0 || value.indexOf('#') === 0;
|
|
2132
|
+
if (!safe) node.removeAttribute(attribute.name);
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
2136
|
+
return root.innerHTML;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function renderToc(headings) {
|
|
2140
|
+
var list = q('[data-toc-list]');
|
|
2141
|
+
if (!list) return;
|
|
2142
|
+
list.textContent = '';
|
|
2143
|
+
state.tocLinks = [];
|
|
2144
|
+
var items = (headings || []).filter(function (heading) { return heading.level >= 2 && heading.level <= 3; });
|
|
2145
|
+
if (!items.length) {
|
|
2146
|
+
list.appendChild(el('p', { class: 'empty__hint', text: 'Sin secciones' }));
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
items.forEach(function (heading) {
|
|
2150
|
+
var link = el('a', {
|
|
2151
|
+
class: 'toc__link',
|
|
2152
|
+
href: '#' + heading.id,
|
|
2153
|
+
dataset: { level: String(heading.level), target: heading.id },
|
|
2154
|
+
text: heading.text,
|
|
2155
|
+
onclick: function (event) {
|
|
2156
|
+
event.preventDefault();
|
|
2157
|
+
var target = document.getElementById(heading.id);
|
|
2158
|
+
if (target) target.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
list.appendChild(link);
|
|
2162
|
+
state.tocLinks.push(link);
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function renderSpecDoc() {
|
|
2167
|
+
var container = q('[data-detail="specs"]');
|
|
2168
|
+
if (!container) return;
|
|
2169
|
+
var specs = state.specs;
|
|
2170
|
+
if (!specs) {
|
|
2171
|
+
container.textContent = '';
|
|
2172
|
+
container.appendChild(el('p', { class: 'loading', text: 'Buscando specs/\u2026' }));
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (!specs.found) {
|
|
2176
|
+
container.textContent = '';
|
|
2177
|
+
container.appendChild(emptyState({
|
|
2178
|
+
title: 'Este proyecto todav\xEDa no tiene specs/',
|
|
2179
|
+
text: 'El studio busca specs/ en la ra\xEDz del proyecto. Creala con la skill create-specs y recarg\xE1 la p\xE1gina.',
|
|
2180
|
+
path: specs.projectRoot + '/specs',
|
|
2181
|
+
hint: 'Skill: create-specs \xB7 /create-specs'
|
|
2182
|
+
}));
|
|
2183
|
+
renderToc([]);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
var doc = state.doc;
|
|
2187
|
+
if (!doc) {
|
|
2188
|
+
container.textContent = '';
|
|
2189
|
+
container.appendChild(
|
|
2190
|
+
state.specPath ? el('p', { class: 'loading', text: 'Leyendo la spec\u2026' }) : emptyState({
|
|
2191
|
+
title: 'Eleg\xED un documento',
|
|
2192
|
+
text: 'El \xE1rbol de la izquierda es el specs/ de este proyecto, ordenado como lo escribe la skill create-specs.',
|
|
2193
|
+
path: specs.root ? shortPath(specs.root) : undefined,
|
|
2194
|
+
hint: specs.totals.files + ' archivos \xB7 ' + fmtBytes(specs.totals.bytes)
|
|
2195
|
+
})
|
|
2196
|
+
);
|
|
2197
|
+
renderToc([]);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
container.textContent = '';
|
|
2201
|
+
container.appendChild(el('header', { class: 'doc-head' }, [
|
|
2202
|
+
el('p', { class: 'doc-head__path', text: 'specs/' + doc.path }),
|
|
2203
|
+
el('h1', { class: 'doc-head__title', text: doc.title }),
|
|
2204
|
+
el('div', { class: 'doc-head__stats' }, [
|
|
2205
|
+
el('span', { text: fmtBytes(doc.bytes) }),
|
|
2206
|
+
el('span', { text: 'modificado ' + fmtDate(doc.modified) }),
|
|
2207
|
+
el('span', { text: (doc.headings ? doc.headings.length : 0) + ' t\xEDtulos' }),
|
|
2208
|
+
copyButton('Copiar ruta', doc.absolutePath)
|
|
2209
|
+
])
|
|
2210
|
+
]));
|
|
2211
|
+
var article = el('article', { class: 'doc' });
|
|
2212
|
+
article.innerHTML = sanitize(doc.html || '');
|
|
2213
|
+
// El t\xEDtulo ya vive en la cabecera del documento: repetirlo en el cuerpo
|
|
2214
|
+
// deja dos h1 iguales en la misma p\xE1gina.
|
|
2215
|
+
var duplicateTitle = q('h1', article);
|
|
2216
|
+
if (duplicateTitle && duplicateTitle.textContent.trim() === doc.title.trim()) {
|
|
2217
|
+
duplicateTitle.remove();
|
|
2218
|
+
}
|
|
2219
|
+
qa('[data-spec-link]', article).forEach(function (link) {
|
|
2220
|
+
link.addEventListener('click', function (event) {
|
|
2221
|
+
event.preventDefault();
|
|
2222
|
+
var target = link.getAttribute('data-spec-link');
|
|
2223
|
+
if (!target) return;
|
|
2224
|
+
// El server ya reescribi\xF3 el target como path desde specs/: resolverlo
|
|
2225
|
+
// otra vez contra doc.path duplicar\xEDa el prefijo (modules/x \u2192 docs/modules/x).
|
|
2226
|
+
navigate(specHash(target));
|
|
2227
|
+
});
|
|
2228
|
+
});
|
|
2229
|
+
container.appendChild(article);
|
|
2230
|
+
if (doc.warnings && doc.warnings.length) {
|
|
2231
|
+
container.appendChild(el('div', { class: 'meta' }, doc.warnings.map(function (warning) {
|
|
2232
|
+
return el('div', { class: 'meta__row' }, [
|
|
2233
|
+
el('span', { class: 'meta__key', text: 'Aviso' }),
|
|
2234
|
+
el('span', { class: 'meta__value', text: warning })
|
|
2235
|
+
]);
|
|
2236
|
+
})));
|
|
2237
|
+
}
|
|
2238
|
+
renderToc(doc.headings);
|
|
2239
|
+
container.scrollTop = 0;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function loadSpecDoc() {
|
|
2243
|
+
if (!state.specPath) {
|
|
2244
|
+
state.doc = null;
|
|
2245
|
+
renderSpecDoc();
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
var path = state.specPath;
|
|
2249
|
+
q('[data-list="specs"]');
|
|
2250
|
+
fetchJson('/api/specs/file?path=' + encodeURIComponent(path)).then(function (doc) {
|
|
2251
|
+
if (state.specPath !== path) return;
|
|
2252
|
+
state.doc = doc;
|
|
2253
|
+
renderSpecDoc();
|
|
2254
|
+
renderSpecTree();
|
|
2255
|
+
}, function (error) {
|
|
2256
|
+
if (state.specPath !== path) return;
|
|
2257
|
+
state.doc = null;
|
|
2258
|
+
renderSpecDoc();
|
|
2259
|
+
var container = q('[data-detail="specs"]');
|
|
2260
|
+
if (container) {
|
|
2261
|
+
container.textContent = '';
|
|
2262
|
+
renderError(container, error);
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/* \u2500\u2500 Render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2268
|
+
|
|
2269
|
+
function render() {
|
|
2270
|
+
syncShell();
|
|
2271
|
+
if (state.view === 'skills') {
|
|
2272
|
+
renderSkillList();
|
|
2273
|
+
renderSkillDetail();
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
renderSpecTree();
|
|
2277
|
+
renderSpecDoc();
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function bootSkills() {
|
|
2281
|
+
loadSkills().then(function () {
|
|
2282
|
+
// Elegir la primera skill no debe reescribir la URL: si el usuario lleg\xF3
|
|
2283
|
+
// por un deep link a /specs, el hash ya es la ruta que eligi\xF3.
|
|
2284
|
+
if (!state.skillKey) {
|
|
2285
|
+
var first = visibleSkills(state.skills, state.scope)[0];
|
|
2286
|
+
if (first) state.skillKey = first.target + '/' + first.name;
|
|
2287
|
+
}
|
|
2288
|
+
renderSkillList();
|
|
2289
|
+
renderSkillDetail();
|
|
2290
|
+
syncShell();
|
|
2291
|
+
}, function (error) {
|
|
2292
|
+
var list = q('[data-list="skills"]');
|
|
2293
|
+
var detail = q('[data-detail="skills"]');
|
|
2294
|
+
if (list) { list.textContent = ''; renderError(list, error); }
|
|
2295
|
+
if (detail) { detail.textContent = ''; renderError(detail, error); }
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function bootSpecs() {
|
|
2300
|
+
loadSpecs().then(function (specs) {
|
|
2301
|
+
renderSpecTree();
|
|
2302
|
+
syncShell();
|
|
2303
|
+
if (!state.specPath && specs.found) {
|
|
2304
|
+
var firstFile = null;
|
|
2305
|
+
var walk = function (nodes) {
|
|
2306
|
+
(nodes || []).forEach(function (node) {
|
|
2307
|
+
if (firstFile) return;
|
|
2308
|
+
if (node.type === 'file') { firstFile = node; return; }
|
|
2309
|
+
walk(node.children);
|
|
2310
|
+
});
|
|
2311
|
+
};
|
|
2312
|
+
walk(specs.tree);
|
|
2313
|
+
if (firstFile) state.specPath = firstFile.path;
|
|
2314
|
+
}
|
|
2315
|
+
loadSpecDoc();
|
|
2316
|
+
}, function (error) {
|
|
2317
|
+
var list = q('[data-list="specs"]');
|
|
2318
|
+
var detail = q('[data-detail="specs"]');
|
|
2319
|
+
if (list) { list.textContent = ''; renderError(list, error); }
|
|
2320
|
+
if (detail) { detail.textContent = ''; renderError(detail, error); }
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function boot() {
|
|
2325
|
+
applyTheme(currentTheme());
|
|
2326
|
+
|
|
2327
|
+
var toggle = q('[data-theme-toggle]');
|
|
2328
|
+
if (toggle) toggle.addEventListener('click', toggleTheme);
|
|
2329
|
+
|
|
2330
|
+
qa('[data-view]').forEach(function (tab) {
|
|
2331
|
+
tab.addEventListener('click', function () { navigate('#/' + tab.dataset.view); });
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
qa('[data-scope]').forEach(function (button) {
|
|
2335
|
+
button.addEventListener('click', function () {
|
|
2336
|
+
state.scope = button.dataset.scope;
|
|
2337
|
+
state.skillKey = null;
|
|
2338
|
+
qa('[data-scope]').forEach(function (other) {
|
|
2339
|
+
other.setAttribute('aria-pressed', other.dataset.scope === state.scope ? 'true' : 'false');
|
|
2340
|
+
});
|
|
2341
|
+
var first = state.skills ? visibleSkills(state.skills, state.scope)[0] : null;
|
|
2342
|
+
if (first) {
|
|
2343
|
+
state.skillKey = first.target + '/' + first.name;
|
|
2344
|
+
history.replaceState(null, '', skillHash(first.target, first.name));
|
|
2345
|
+
}
|
|
2346
|
+
renderSkillList();
|
|
2347
|
+
renderSkillDetail();
|
|
2348
|
+
syncShell();
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
qa('[data-search]').forEach(function (input) {
|
|
2353
|
+
input.addEventListener('input', function () {
|
|
2354
|
+
state.filter[input.dataset.search] = input.value;
|
|
2355
|
+
if (input.dataset.search === 'skills') {
|
|
2356
|
+
renderSkillList();
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
renderSpecTree();
|
|
2360
|
+
});
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
document.addEventListener('keydown', function (event) {
|
|
2364
|
+
var target = event.target;
|
|
2365
|
+
var typing = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
|
|
2366
|
+
if (event.key === '/' && !typing) {
|
|
2367
|
+
event.preventDefault();
|
|
2368
|
+
var input = q('[data-search="' + state.view + '"]');
|
|
2369
|
+
if (input) input.focus();
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (event.key === 'Escape' && typing) {
|
|
2373
|
+
target.value = '';
|
|
2374
|
+
state.filter[state.view] = '';
|
|
2375
|
+
render();
|
|
2376
|
+
target.blur();
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (typing || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
2380
|
+
if (event.key === 't') { toggleTheme(); return; }
|
|
2381
|
+
if (event.key === '1') { navigate('#/skills'); return; }
|
|
2382
|
+
if (event.key === '2') { navigate('#/specs'); return; }
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
var pane = q('[data-detail="specs"]');
|
|
2386
|
+
if (pane) {
|
|
2387
|
+
pane.addEventListener('scroll', function () {
|
|
2388
|
+
if (!state.tocLinks.length) return;
|
|
2389
|
+
var active = null;
|
|
2390
|
+
for (var i = 0; i < state.tocLinks.length; i++) {
|
|
2391
|
+
var heading = document.getElementById(state.tocLinks[i].dataset.target);
|
|
2392
|
+
if (!heading) continue;
|
|
2393
|
+
if (heading.getBoundingClientRect().top - pane.getBoundingClientRect().top <= 24) active = state.tocLinks[i];
|
|
2394
|
+
}
|
|
2395
|
+
state.tocLinks.forEach(function (link) {
|
|
2396
|
+
link.setAttribute('aria-current', link === active ? 'true' : 'false');
|
|
2397
|
+
});
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
window.addEventListener('hashchange', route);
|
|
2402
|
+
|
|
2403
|
+
route();
|
|
2404
|
+
bootSkills();
|
|
2405
|
+
bootSpecs();
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
|
2409
|
+
else boot();
|
|
2410
|
+
})();
|
|
2411
|
+
`;
|
|
2412
|
+
|
|
2413
|
+
// src/studio/ui/styles.ts
|
|
2414
|
+
var STUDIO_CSS = `
|
|
2415
|
+
*,
|
|
2416
|
+
*::before,
|
|
2417
|
+
*::after {
|
|
2418
|
+
box-sizing: border-box;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
* {
|
|
2422
|
+
margin: 0;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
html {
|
|
2426
|
+
-webkit-text-size-adjust: 100%;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
body {
|
|
2430
|
+
min-height: 100vh;
|
|
2431
|
+
background-color: var(--base);
|
|
2432
|
+
color: var(--text);
|
|
2433
|
+
font-family: var(--font-sans);
|
|
2434
|
+
font-size: var(--text-small);
|
|
2435
|
+
line-height: var(--leading-body);
|
|
2436
|
+
font-synthesis-weight: none;
|
|
2437
|
+
-webkit-font-smoothing: antialiased;
|
|
2438
|
+
-moz-osx-font-smoothing: grayscale;
|
|
2439
|
+
transition:
|
|
2440
|
+
background-color var(--duration-base) ease,
|
|
2441
|
+
color var(--duration-base) ease;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
svg {
|
|
2445
|
+
display: block;
|
|
2446
|
+
flex: none;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
a {
|
|
2450
|
+
color: var(--accent);
|
|
2451
|
+
text-decoration: underline;
|
|
2452
|
+
text-decoration-thickness: 1px;
|
|
2453
|
+
text-underline-offset: 0.2em;
|
|
2454
|
+
transition: color var(--duration-fast) ease;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
a:hover {
|
|
2458
|
+
color: var(--accent-hover);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
a:not(:hover) {
|
|
2462
|
+
text-decoration-color: var(--accent-border);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
button,
|
|
2466
|
+
input {
|
|
2467
|
+
font: inherit;
|
|
2468
|
+
color: inherit;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
button {
|
|
2472
|
+
background: none;
|
|
2473
|
+
border: 0;
|
|
2474
|
+
cursor: pointer;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
:focus-visible {
|
|
2478
|
+
outline: 2px solid var(--focus-ring);
|
|
2479
|
+
outline-offset: 2px;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
::selection {
|
|
2483
|
+
background: var(--accent-soft);
|
|
2484
|
+
color: var(--text);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
.skip {
|
|
2488
|
+
position: absolute;
|
|
2489
|
+
left: -9999px;
|
|
2490
|
+
top: 0;
|
|
2491
|
+
z-index: 60;
|
|
2492
|
+
background: var(--surface-elevated);
|
|
2493
|
+
border: 1px solid var(--accent-border);
|
|
2494
|
+
padding: var(--space-2xs) var(--space-xs);
|
|
2495
|
+
font-family: var(--font-mono);
|
|
2496
|
+
font-size: var(--text-caption);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
.skip:focus {
|
|
2500
|
+
left: var(--space-sm);
|
|
2501
|
+
top: var(--space-2xs);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
/* \u2500\u2500 Top bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2505
|
+
.topbar {
|
|
2506
|
+
position: fixed;
|
|
2507
|
+
inset: 0 0 auto 0;
|
|
2508
|
+
z-index: 50;
|
|
2509
|
+
height: var(--header-height);
|
|
2510
|
+
background: var(--base);
|
|
2511
|
+
border-bottom: var(--hairline-strong);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
.topbar__inner {
|
|
2515
|
+
display: grid;
|
|
2516
|
+
grid-template-columns: auto 1fr auto;
|
|
2517
|
+
align-items: center;
|
|
2518
|
+
gap: var(--space-md);
|
|
2519
|
+
height: 100%;
|
|
2520
|
+
max-width: var(--container);
|
|
2521
|
+
margin-inline: auto;
|
|
2522
|
+
padding-inline: var(--container-pad);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
.brand {
|
|
2526
|
+
display: inline-flex;
|
|
2527
|
+
align-items: center;
|
|
2528
|
+
gap: var(--space-2xs);
|
|
2529
|
+
color: var(--text);
|
|
2530
|
+
text-decoration: none;
|
|
2531
|
+
font-family: var(--font-mono);
|
|
2532
|
+
font-size: var(--text-small);
|
|
2533
|
+
font-weight: 600;
|
|
2534
|
+
letter-spacing: -0.01em;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
.brand:hover {
|
|
2538
|
+
color: var(--text);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
.brand__glyph {
|
|
2542
|
+
display: grid;
|
|
2543
|
+
place-items: center;
|
|
2544
|
+
width: 28px;
|
|
2545
|
+
height: 28px;
|
|
2546
|
+
background: var(--accent-soft);
|
|
2547
|
+
border: 1px solid var(--accent-border);
|
|
2548
|
+
color: var(--accent);
|
|
2549
|
+
font-size: var(--text-caption);
|
|
2550
|
+
font-weight: 700;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
.brand__div {
|
|
2554
|
+
color: var(--text-muted);
|
|
2555
|
+
font-weight: 400;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
.brand__page {
|
|
2559
|
+
color: var(--accent);
|
|
2560
|
+
font-weight: 500;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
.tabs {
|
|
2564
|
+
display: flex;
|
|
2565
|
+
align-items: center;
|
|
2566
|
+
justify-content: center;
|
|
2567
|
+
gap: var(--space-md);
|
|
2568
|
+
height: 100%;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
.tab {
|
|
2572
|
+
position: relative;
|
|
2573
|
+
display: inline-flex;
|
|
2574
|
+
align-items: center;
|
|
2575
|
+
gap: var(--space-2xs);
|
|
2576
|
+
height: 100%;
|
|
2577
|
+
color: var(--text-secondary);
|
|
2578
|
+
font-size: var(--text-small);
|
|
2579
|
+
font-weight: 500;
|
|
2580
|
+
transition: color var(--duration-fast) ease;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
.tab::after {
|
|
2584
|
+
content: '';
|
|
2585
|
+
position: absolute;
|
|
2586
|
+
inset: auto 0 0 0;
|
|
2587
|
+
height: 2px;
|
|
2588
|
+
background: var(--accent);
|
|
2589
|
+
transform: scaleX(0);
|
|
2590
|
+
transform-origin: left;
|
|
2591
|
+
transition: transform var(--duration-base) var(--ease-out);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
.tab:hover {
|
|
2595
|
+
color: var(--text);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
.tab[aria-selected='true'] {
|
|
2599
|
+
color: var(--text);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
.tab[aria-selected='true']::after {
|
|
2603
|
+
transform: scaleX(1);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
.tab__count {
|
|
2607
|
+
font-family: var(--font-mono);
|
|
2608
|
+
font-size: var(--text-caption);
|
|
2609
|
+
color: var(--text-muted);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
.tab[aria-selected='true'] .tab__count {
|
|
2613
|
+
color: var(--accent);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.topbar__actions {
|
|
2617
|
+
display: flex;
|
|
2618
|
+
align-items: center;
|
|
2619
|
+
gap: var(--space-2xs);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
.badge {
|
|
2623
|
+
display: inline-flex;
|
|
2624
|
+
align-items: center;
|
|
2625
|
+
gap: var(--space-3xs);
|
|
2626
|
+
padding: 0.125rem var(--space-2xs);
|
|
2627
|
+
font-family: var(--font-mono);
|
|
2628
|
+
font-size: var(--text-caption);
|
|
2629
|
+
line-height: 1.5;
|
|
2630
|
+
white-space: nowrap;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
.badge--version {
|
|
2634
|
+
color: var(--text-secondary);
|
|
2635
|
+
background: var(--surface);
|
|
2636
|
+
border: 1px solid var(--border);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.badge--accent {
|
|
2640
|
+
color: var(--accent);
|
|
2641
|
+
background: var(--accent-soft);
|
|
2642
|
+
border: 1px solid var(--accent-border);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
.badge--muted {
|
|
2646
|
+
color: var(--text-muted);
|
|
2647
|
+
background: var(--surface);
|
|
2648
|
+
border: 1px solid var(--border);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
.btn {
|
|
2652
|
+
display: inline-flex;
|
|
2653
|
+
align-items: center;
|
|
2654
|
+
gap: var(--space-2xs);
|
|
2655
|
+
padding: var(--space-3xs) var(--space-2xs);
|
|
2656
|
+
color: var(--text-secondary);
|
|
2657
|
+
border: 1px solid var(--border);
|
|
2658
|
+
background: var(--surface);
|
|
2659
|
+
font-size: var(--text-caption);
|
|
2660
|
+
transition:
|
|
2661
|
+
color var(--duration-fast) ease,
|
|
2662
|
+
border-color var(--duration-fast) ease,
|
|
2663
|
+
background-color var(--duration-fast) ease;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
.btn:hover {
|
|
2667
|
+
color: var(--text);
|
|
2668
|
+
border-color: var(--accent-border);
|
|
2669
|
+
background: var(--surface-elevated);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
.btn--icon {
|
|
2673
|
+
width: 34px;
|
|
2674
|
+
height: 34px;
|
|
2675
|
+
padding: 0;
|
|
2676
|
+
justify-content: center;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
.btn[data-copied='true'] {
|
|
2680
|
+
color: var(--success);
|
|
2681
|
+
border-color: var(--success);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
.theme-toggle .icon-sun {
|
|
2685
|
+
display: none;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
[data-theme='light'] .theme-toggle .icon-sun {
|
|
2689
|
+
display: block;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
[data-theme='light'] .theme-toggle .icon-moon {
|
|
2693
|
+
display: none;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/* \u2500\u2500 Shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2697
|
+
.shell {
|
|
2698
|
+
padding-top: var(--header-height);
|
|
2699
|
+
padding-bottom: 36px;
|
|
2700
|
+
padding-inline: var(--container-pad);
|
|
2701
|
+
height: 100vh;
|
|
2702
|
+
max-width: var(--container);
|
|
2703
|
+
margin-inline: auto;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
.view {
|
|
2707
|
+
display: none;
|
|
2708
|
+
height: 100%;
|
|
2709
|
+
border-inline: var(--hairline);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
.view[data-active='true'] {
|
|
2713
|
+
display: grid;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
.view--skills {
|
|
2717
|
+
grid-template-columns: 340px minmax(0, 1fr);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
.view--specs {
|
|
2721
|
+
grid-template-columns: 320px minmax(0, 1fr) 232px;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
.rail {
|
|
2725
|
+
display: flex;
|
|
2726
|
+
flex-direction: column;
|
|
2727
|
+
min-height: 0;
|
|
2728
|
+
border-right: var(--hairline-strong);
|
|
2729
|
+
background: var(--surface);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
.rail__head {
|
|
2733
|
+
display: grid;
|
|
2734
|
+
gap: var(--space-2xs);
|
|
2735
|
+
padding: var(--space-sm);
|
|
2736
|
+
border-bottom: var(--hairline-strong);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
.rail__body {
|
|
2740
|
+
overflow-y: auto;
|
|
2741
|
+
padding: var(--space-2xs) 0 var(--space-lg);
|
|
2742
|
+
scrollbar-width: thin;
|
|
2743
|
+
scrollbar-color: var(--border-strong) transparent;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
.rail__body::-webkit-scrollbar {
|
|
2747
|
+
width: 10px;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
.rail__body::-webkit-scrollbar-thumb {
|
|
2751
|
+
background: var(--border-strong);
|
|
2752
|
+
border: 3px solid var(--surface);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
.field {
|
|
2756
|
+
display: flex;
|
|
2757
|
+
align-items: center;
|
|
2758
|
+
gap: var(--space-2xs);
|
|
2759
|
+
padding: 0.375rem var(--space-2xs);
|
|
2760
|
+
background: var(--base);
|
|
2761
|
+
border: 1px solid var(--border);
|
|
2762
|
+
color: var(--text-muted);
|
|
2763
|
+
transition: border-color var(--duration-fast) ease;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
.field:focus-within {
|
|
2767
|
+
border-color: var(--border-input);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
.field__input {
|
|
2771
|
+
flex: 1;
|
|
2772
|
+
min-width: 0;
|
|
2773
|
+
border: 0;
|
|
2774
|
+
outline: none;
|
|
2775
|
+
background: none;
|
|
2776
|
+
color: var(--text);
|
|
2777
|
+
font-size: var(--text-small);
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
.field__input::placeholder {
|
|
2781
|
+
color: var(--text-muted);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
.field kbd {
|
|
2785
|
+
flex: none;
|
|
2786
|
+
padding: 0 0.25rem;
|
|
2787
|
+
border: 1px solid var(--border);
|
|
2788
|
+
color: var(--text-muted);
|
|
2789
|
+
font-family: var(--font-mono);
|
|
2790
|
+
font-size: var(--text-caption);
|
|
2791
|
+
line-height: 1.4;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
.segmented {
|
|
2795
|
+
display: grid;
|
|
2796
|
+
grid-template-columns: 1fr 1fr;
|
|
2797
|
+
border: 1px solid var(--border);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
.segmented button {
|
|
2801
|
+
padding: 0.25rem var(--space-2xs);
|
|
2802
|
+
color: var(--text-secondary);
|
|
2803
|
+
background: var(--base);
|
|
2804
|
+
font-family: var(--font-mono);
|
|
2805
|
+
font-size: var(--text-caption);
|
|
2806
|
+
letter-spacing: 0.02em;
|
|
2807
|
+
transition:
|
|
2808
|
+
color var(--duration-fast) ease,
|
|
2809
|
+
background-color var(--duration-fast) ease;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
.segmented button + button {
|
|
2813
|
+
border-left: 1px solid var(--border);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.segmented button:hover {
|
|
2817
|
+
color: var(--text);
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
.segmented button[aria-pressed='true'] {
|
|
2821
|
+
color: var(--accent);
|
|
2822
|
+
background: var(--accent-soft);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/* \u2500\u2500 Rail groups and rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2826
|
+
.group {
|
|
2827
|
+
margin-top: var(--space-2xs);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
.group__head {
|
|
2831
|
+
display: flex;
|
|
2832
|
+
align-items: baseline;
|
|
2833
|
+
gap: var(--space-2xs);
|
|
2834
|
+
width: 100%;
|
|
2835
|
+
padding: var(--space-3xs) var(--space-sm);
|
|
2836
|
+
color: var(--text-muted);
|
|
2837
|
+
font-family: var(--font-mono);
|
|
2838
|
+
font-size: var(--text-caption);
|
|
2839
|
+
letter-spacing: 0.04em;
|
|
2840
|
+
text-transform: uppercase;
|
|
2841
|
+
text-align: left;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
.group__head:hover {
|
|
2845
|
+
color: var(--text-secondary);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
.group__count {
|
|
2849
|
+
margin-left: auto;
|
|
2850
|
+
color: var(--text-muted);
|
|
2851
|
+
letter-spacing: 0;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
.group__path {
|
|
2855
|
+
padding: 0 var(--space-sm) var(--space-3xs);
|
|
2856
|
+
color: var(--text-muted);
|
|
2857
|
+
font-family: var(--font-mono);
|
|
2858
|
+
font-size: var(--text-caption);
|
|
2859
|
+
overflow: hidden;
|
|
2860
|
+
text-overflow: ellipsis;
|
|
2861
|
+
white-space: nowrap;
|
|
2862
|
+
text-align: left;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
.row {
|
|
2866
|
+
display: grid;
|
|
2867
|
+
gap: 0.125rem;
|
|
2868
|
+
width: 100%;
|
|
2869
|
+
padding: var(--space-3xs) var(--space-sm);
|
|
2870
|
+
border-left: 2px solid transparent;
|
|
2871
|
+
color: var(--text-secondary);
|
|
2872
|
+
text-align: left;
|
|
2873
|
+
transition:
|
|
2874
|
+
color var(--duration-fast) ease,
|
|
2875
|
+
background-color var(--duration-fast) ease,
|
|
2876
|
+
border-color var(--duration-fast) ease;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
.row:hover {
|
|
2880
|
+
color: var(--text);
|
|
2881
|
+
background: var(--surface-elevated);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
.row[aria-current='true'] {
|
|
2885
|
+
color: var(--text);
|
|
2886
|
+
background: var(--accent-soft);
|
|
2887
|
+
border-left-color: var(--accent);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
.row__name {
|
|
2891
|
+
font-family: var(--font-mono);
|
|
2892
|
+
font-size: var(--text-caption);
|
|
2893
|
+
overflow: hidden;
|
|
2894
|
+
text-overflow: ellipsis;
|
|
2895
|
+
white-space: nowrap;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
.row__meta {
|
|
2899
|
+
color: var(--text-muted);
|
|
2900
|
+
font-family: var(--font-mono);
|
|
2901
|
+
font-size: var(--text-caption);
|
|
2902
|
+
overflow: hidden;
|
|
2903
|
+
text-overflow: ellipsis;
|
|
2904
|
+
white-space: nowrap;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
.row--dir .row__name {
|
|
2908
|
+
color: var(--text-secondary);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
.row--file[aria-current='true'] .row__name {
|
|
2912
|
+
color: var(--accent);
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
.row--empty {
|
|
2916
|
+
padding: var(--space-2xs) var(--space-sm);
|
|
2917
|
+
color: var(--text-muted);
|
|
2918
|
+
font-family: var(--font-mono);
|
|
2919
|
+
font-size: var(--text-caption);
|
|
2920
|
+
border-left: 2px dashed var(--border);
|
|
2921
|
+
margin-left: var(--space-sm);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
.tree {
|
|
2925
|
+
display: grid;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
.tree__children {
|
|
2929
|
+
padding-left: var(--space-xs);
|
|
2930
|
+
border-left: var(--hairline);
|
|
2931
|
+
margin-left: var(--space-sm);
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
.tree__toggle {
|
|
2935
|
+
display: inline-flex;
|
|
2936
|
+
align-items: center;
|
|
2937
|
+
gap: var(--space-3xs);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
.tree__chevron {
|
|
2941
|
+
transition: transform var(--duration-fast) ease;
|
|
2942
|
+
color: var(--text-muted);
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
.tree__chevron[data-open='true'] {
|
|
2946
|
+
transform: rotate(90deg);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
/* \u2500\u2500 Panes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2950
|
+
.pane {
|
|
2951
|
+
min-width: 0;
|
|
2952
|
+
overflow-y: auto;
|
|
2953
|
+
padding: var(--space-lg) var(--space-xl) var(--space-2xl);
|
|
2954
|
+
scrollbar-width: thin;
|
|
2955
|
+
scrollbar-color: var(--border-strong) transparent;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
.pane::-webkit-scrollbar {
|
|
2959
|
+
width: 10px;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
.pane::-webkit-scrollbar-thumb {
|
|
2963
|
+
background: var(--border-strong);
|
|
2964
|
+
border: 3px solid var(--base);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
.eyebrow {
|
|
2968
|
+
display: inline-flex;
|
|
2969
|
+
align-items: center;
|
|
2970
|
+
gap: var(--space-3xs);
|
|
2971
|
+
margin-bottom: var(--space-2xs);
|
|
2972
|
+
padding: 0.125rem var(--space-2xs);
|
|
2973
|
+
background: var(--accent-soft);
|
|
2974
|
+
border: 1px solid var(--accent-border);
|
|
2975
|
+
color: var(--accent);
|
|
2976
|
+
font-family: var(--font-mono);
|
|
2977
|
+
font-size: var(--text-caption);
|
|
2978
|
+
letter-spacing: 0.04em;
|
|
2979
|
+
text-transform: uppercase;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
.detail__title {
|
|
2983
|
+
font-family: var(--font-mono);
|
|
2984
|
+
font-size: clamp(1.5rem, 2.6vw, var(--text-h2));
|
|
2985
|
+
font-weight: 600;
|
|
2986
|
+
line-height: var(--leading-h2);
|
|
2987
|
+
letter-spacing: -0.02em;
|
|
2988
|
+
overflow-wrap: anywhere;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
.detail__lede {
|
|
2992
|
+
max-width: 68ch;
|
|
2993
|
+
margin-top: var(--space-2xs);
|
|
2994
|
+
color: var(--text-secondary);
|
|
2995
|
+
font-size: var(--text-body);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
.detail__actions {
|
|
2999
|
+
display: flex;
|
|
3000
|
+
flex-wrap: wrap;
|
|
3001
|
+
gap: var(--space-2xs);
|
|
3002
|
+
margin-top: var(--space-sm);
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
.meta {
|
|
3006
|
+
display: grid;
|
|
3007
|
+
margin-top: var(--space-lg);
|
|
3008
|
+
border-top: var(--hairline-strong);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
.meta__row {
|
|
3012
|
+
display: grid;
|
|
3013
|
+
grid-template-columns: 132px minmax(0, 1fr);
|
|
3014
|
+
gap: var(--space-sm);
|
|
3015
|
+
align-items: baseline;
|
|
3016
|
+
padding: var(--space-2xs) 0;
|
|
3017
|
+
border-bottom: var(--hairline);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
.meta__key {
|
|
3021
|
+
color: var(--text-muted);
|
|
3022
|
+
font-family: var(--font-mono);
|
|
3023
|
+
font-size: var(--text-caption);
|
|
3024
|
+
letter-spacing: 0.04em;
|
|
3025
|
+
text-transform: uppercase;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
.meta__value {
|
|
3029
|
+
min-width: 0;
|
|
3030
|
+
color: var(--text-secondary);
|
|
3031
|
+
font-family: var(--font-mono);
|
|
3032
|
+
font-size: var(--text-caption);
|
|
3033
|
+
overflow-wrap: anywhere;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
.meta__value--strong {
|
|
3037
|
+
color: var(--text);
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
/* \u2500\u2500 Document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3041
|
+
.doc-head {
|
|
3042
|
+
max-width: var(--text-column);
|
|
3043
|
+
padding-bottom: var(--space-sm);
|
|
3044
|
+
border-bottom: var(--hairline-strong);
|
|
3045
|
+
margin-bottom: var(--space-md);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
.doc-head__path {
|
|
3049
|
+
color: var(--text-muted);
|
|
3050
|
+
font-family: var(--font-mono);
|
|
3051
|
+
font-size: var(--text-caption);
|
|
3052
|
+
overflow-wrap: anywhere;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
.doc-head__title {
|
|
3056
|
+
margin-top: var(--space-3xs);
|
|
3057
|
+
font-size: clamp(1.5rem, 2.6vw, var(--text-h2));
|
|
3058
|
+
font-weight: 600;
|
|
3059
|
+
line-height: var(--leading-h2);
|
|
3060
|
+
letter-spacing: -0.02em;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
.doc-head__stats {
|
|
3064
|
+
display: flex;
|
|
3065
|
+
flex-wrap: wrap;
|
|
3066
|
+
gap: var(--space-2xs) var(--space-xs);
|
|
3067
|
+
margin-top: var(--space-2xs);
|
|
3068
|
+
color: var(--text-muted);
|
|
3069
|
+
font-family: var(--font-mono);
|
|
3070
|
+
font-size: var(--text-caption);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
.doc {
|
|
3074
|
+
max-width: var(--text-column);
|
|
3075
|
+
color: var(--text-secondary);
|
|
3076
|
+
font-size: var(--text-body);
|
|
3077
|
+
line-height: var(--leading-body);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
.doc > * + * {
|
|
3081
|
+
margin-top: var(--space-sm);
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
.doc h1,
|
|
3085
|
+
.doc h2,
|
|
3086
|
+
.doc h3,
|
|
3087
|
+
.doc h4,
|
|
3088
|
+
.doc h5,
|
|
3089
|
+
.doc h6 {
|
|
3090
|
+
color: var(--text);
|
|
3091
|
+
font-weight: 600;
|
|
3092
|
+
scroll-margin-top: calc(var(--header-height) + var(--space-md));
|
|
3093
|
+
text-wrap: balance;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
.doc h1 {
|
|
3097
|
+
margin-top: var(--space-lg);
|
|
3098
|
+
font-size: var(--text-h3);
|
|
3099
|
+
line-height: var(--leading-h3);
|
|
3100
|
+
letter-spacing: -0.015em;
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
.doc h2 {
|
|
3104
|
+
margin-top: var(--space-lg);
|
|
3105
|
+
padding-top: var(--space-2xs);
|
|
3106
|
+
border-top: var(--hairline);
|
|
3107
|
+
font-size: var(--text-h4);
|
|
3108
|
+
line-height: var(--leading-h3);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
.doc h3 {
|
|
3112
|
+
margin-top: var(--space-md);
|
|
3113
|
+
font-size: var(--text-body);
|
|
3114
|
+
color: var(--text);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
.doc h4,
|
|
3118
|
+
.doc h5,
|
|
3119
|
+
.doc h6 {
|
|
3120
|
+
margin-top: var(--space-sm);
|
|
3121
|
+
font-size: var(--text-small);
|
|
3122
|
+
color: var(--text-secondary);
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
.doc strong {
|
|
3126
|
+
color: var(--text);
|
|
3127
|
+
font-weight: 600;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
.doc em {
|
|
3131
|
+
font-style: italic;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
.doc del {
|
|
3135
|
+
color: var(--text-muted);
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
.doc a {
|
|
3139
|
+
text-decoration-style: dashed;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
.doc ul,
|
|
3143
|
+
.doc ol {
|
|
3144
|
+
padding-left: 1.35em;
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
.doc li + li {
|
|
3148
|
+
margin-top: 0.25em;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
.doc li > ul,
|
|
3152
|
+
.doc li > ol {
|
|
3153
|
+
margin-top: 0.25em;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
.doc li::marker {
|
|
3157
|
+
color: var(--text-muted);
|
|
3158
|
+
font-family: var(--font-mono);
|
|
3159
|
+
font-size: var(--text-caption);
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/* Las listas de tareas ya traen su propio control: el bullet sobra. */
|
|
3163
|
+
.doc li:has(> input[type='checkbox']) {
|
|
3164
|
+
list-style: none;
|
|
3165
|
+
margin-left: -1.15em;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
.doc input[type='checkbox'] {
|
|
3169
|
+
appearance: none;
|
|
3170
|
+
width: 13px;
|
|
3171
|
+
height: 13px;
|
|
3172
|
+
margin-right: 0.5em;
|
|
3173
|
+
border: 1px solid var(--border-input);
|
|
3174
|
+
vertical-align: -1px;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
.doc input[type='checkbox'][checked] {
|
|
3178
|
+
background: var(--accent);
|
|
3179
|
+
border-color: var(--accent);
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
.doc blockquote {
|
|
3183
|
+
padding: var(--space-2xs) var(--space-sm);
|
|
3184
|
+
border-left: 2px solid var(--accent-border);
|
|
3185
|
+
background: var(--surface);
|
|
3186
|
+
color: var(--text-secondary);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
.doc blockquote p + p {
|
|
3190
|
+
margin-top: var(--space-2xs);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
.doc hr {
|
|
3194
|
+
height: 0;
|
|
3195
|
+
border: 0;
|
|
3196
|
+
border-top: var(--hairline-strong);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
.doc code {
|
|
3200
|
+
padding: 0.125em 0.375em;
|
|
3201
|
+
background: var(--accent-soft);
|
|
3202
|
+
color: var(--accent);
|
|
3203
|
+
font-family: var(--font-mono);
|
|
3204
|
+
font-size: 0.86em;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
.doc pre {
|
|
3208
|
+
overflow-x: auto;
|
|
3209
|
+
padding: var(--space-xs) var(--space-sm);
|
|
3210
|
+
background: var(--code-bg);
|
|
3211
|
+
border: 1px solid var(--border);
|
|
3212
|
+
scrollbar-width: thin;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
.doc pre code {
|
|
3216
|
+
padding: 0;
|
|
3217
|
+
background: none;
|
|
3218
|
+
color: var(--syn-variable);
|
|
3219
|
+
font-size: var(--text-code);
|
|
3220
|
+
line-height: 1.55;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
.doc table {
|
|
3224
|
+
width: 100%;
|
|
3225
|
+
border-collapse: collapse;
|
|
3226
|
+
font-size: var(--text-small);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
.doc th,
|
|
3230
|
+
.doc td {
|
|
3231
|
+
padding: var(--space-3xs) var(--space-2xs);
|
|
3232
|
+
border: 1px solid var(--border);
|
|
3233
|
+
text-align: left;
|
|
3234
|
+
vertical-align: top;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
.doc th {
|
|
3238
|
+
background: var(--surface);
|
|
3239
|
+
color: var(--text);
|
|
3240
|
+
font-family: var(--font-mono);
|
|
3241
|
+
font-size: var(--text-caption);
|
|
3242
|
+
letter-spacing: 0.02em;
|
|
3243
|
+
font-weight: 500;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
.doc td {
|
|
3247
|
+
font-family: var(--font-mono);
|
|
3248
|
+
font-size: var(--text-caption);
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
.doc .md-img {
|
|
3252
|
+
display: inline-flex;
|
|
3253
|
+
padding: 0 var(--space-3xs);
|
|
3254
|
+
border: 1px dashed var(--border-strong);
|
|
3255
|
+
color: var(--text-muted);
|
|
3256
|
+
font-family: var(--font-mono);
|
|
3257
|
+
font-size: var(--text-caption);
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
/* \u2500\u2500 Table of contents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3261
|
+
.toc {
|
|
3262
|
+
overflow-y: auto;
|
|
3263
|
+
padding: var(--space-lg) var(--space-sm);
|
|
3264
|
+
border-left: var(--hairline-strong);
|
|
3265
|
+
background: var(--surface);
|
|
3266
|
+
scrollbar-width: thin;
|
|
3267
|
+
scrollbar-color: var(--border-strong) transparent;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
.toc__title {
|
|
3271
|
+
color: var(--text-muted);
|
|
3272
|
+
font-family: var(--font-mono);
|
|
3273
|
+
font-size: var(--text-caption);
|
|
3274
|
+
letter-spacing: 0.04em;
|
|
3275
|
+
text-transform: uppercase;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
.toc__list {
|
|
3279
|
+
display: grid;
|
|
3280
|
+
gap: 0.125rem;
|
|
3281
|
+
margin-top: var(--space-2xs);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
.toc__link {
|
|
3285
|
+
display: block;
|
|
3286
|
+
padding: 0.125rem var(--space-2xs);
|
|
3287
|
+
border-left: 2px solid transparent;
|
|
3288
|
+
color: var(--text-secondary);
|
|
3289
|
+
font-size: var(--text-caption);
|
|
3290
|
+
text-decoration: none;
|
|
3291
|
+
overflow: hidden;
|
|
3292
|
+
text-overflow: ellipsis;
|
|
3293
|
+
white-space: nowrap;
|
|
3294
|
+
transition:
|
|
3295
|
+
color var(--duration-fast) ease,
|
|
3296
|
+
border-color var(--duration-fast) ease;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
.toc__link:hover {
|
|
3300
|
+
color: var(--text);
|
|
3301
|
+
border-left-color: var(--border-strong);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
.toc__link[data-level='3'] {
|
|
3305
|
+
padding-left: var(--space-sm);
|
|
3306
|
+
color: var(--text-muted);
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
.toc__link[data-level='4'],
|
|
3310
|
+
.toc__link[data-level='5'],
|
|
3311
|
+
.toc__link[data-level='6'] {
|
|
3312
|
+
display: none;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
/* \u2500\u2500 States \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3316
|
+
.empty {
|
|
3317
|
+
display: grid;
|
|
3318
|
+
gap: var(--space-2xs);
|
|
3319
|
+
max-width: 560px;
|
|
3320
|
+
margin: var(--space-2xl) auto;
|
|
3321
|
+
padding: var(--space-lg);
|
|
3322
|
+
border: var(--hairline-strong);
|
|
3323
|
+
background: var(--surface);
|
|
3324
|
+
text-align: center;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
.empty__title {
|
|
3328
|
+
color: var(--text);
|
|
3329
|
+
font-size: var(--text-h4);
|
|
3330
|
+
font-weight: 600;
|
|
3331
|
+
letter-spacing: -0.01em;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
.empty__text {
|
|
3335
|
+
color: var(--text-secondary);
|
|
3336
|
+
font-size: var(--text-small);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
.empty__path {
|
|
3340
|
+
margin-top: var(--space-2xs);
|
|
3341
|
+
padding: var(--space-2xs);
|
|
3342
|
+
background: var(--base);
|
|
3343
|
+
border: 1px solid var(--border);
|
|
3344
|
+
color: var(--text-muted);
|
|
3345
|
+
font-family: var(--font-mono);
|
|
3346
|
+
font-size: var(--text-caption);
|
|
3347
|
+
overflow-wrap: anywhere;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
.empty__hint {
|
|
3351
|
+
color: var(--text-muted);
|
|
3352
|
+
font-family: var(--font-mono);
|
|
3353
|
+
font-size: var(--text-caption);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
.loading {
|
|
3357
|
+
padding: var(--space-lg) var(--space-sm);
|
|
3358
|
+
color: var(--text-muted);
|
|
3359
|
+
font-family: var(--font-mono);
|
|
3360
|
+
font-size: var(--text-caption);
|
|
3361
|
+
animation: pulse 1.6s ease-in-out infinite;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
@keyframes pulse {
|
|
3365
|
+
0%,
|
|
3366
|
+
100% {
|
|
3367
|
+
opacity: 0.45;
|
|
3368
|
+
}
|
|
3369
|
+
50% {
|
|
3370
|
+
opacity: 1;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
/* \u2500\u2500 Status bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3375
|
+
.statusbar {
|
|
3376
|
+
position: fixed;
|
|
3377
|
+
inset: auto 0 0 0;
|
|
3378
|
+
z-index: 40;
|
|
3379
|
+
display: flex;
|
|
3380
|
+
align-items: center;
|
|
3381
|
+
gap: var(--space-sm);
|
|
3382
|
+
height: 36px;
|
|
3383
|
+
padding-inline: var(--container-pad);
|
|
3384
|
+
background: var(--base);
|
|
3385
|
+
border-top: var(--hairline-strong);
|
|
3386
|
+
color: var(--text-muted);
|
|
3387
|
+
font-family: var(--font-mono);
|
|
3388
|
+
font-size: var(--text-caption);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
.statusbar__item {
|
|
3392
|
+
overflow: hidden;
|
|
3393
|
+
text-overflow: ellipsis;
|
|
3394
|
+
white-space: nowrap;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
.statusbar__spacer {
|
|
3398
|
+
flex: 1;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
.statusbar__dot {
|
|
3402
|
+
color: var(--success);
|
|
3403
|
+
animation: pulse 2.4s ease-in-out infinite;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
.statusbar kbd {
|
|
3407
|
+
padding: 0 0.25rem;
|
|
3408
|
+
border: 1px solid var(--border);
|
|
3409
|
+
color: var(--text-secondary);
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
/* \u2500\u2500 Responsive: the landing page collapses the same way \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3413
|
+
@media (max-width: 1100px) {
|
|
3414
|
+
.view--specs {
|
|
3415
|
+
grid-template-columns: 280px minmax(0, 1fr);
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
.toc {
|
|
3419
|
+
display: none;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
@media (max-width: 820px) {
|
|
3424
|
+
.shell {
|
|
3425
|
+
padding-inline: 0;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
.topbar__inner,
|
|
3429
|
+
.statusbar {
|
|
3430
|
+
padding-inline: var(--space-sm);
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
.brand__div,
|
|
3434
|
+
.brand__page {
|
|
3435
|
+
display: none;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
.view--skills,
|
|
3439
|
+
.view--specs {
|
|
3440
|
+
grid-template-columns: minmax(0, 1fr);
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
.rail {
|
|
3444
|
+
border-right: 0;
|
|
3445
|
+
border-bottom: var(--hairline-strong);
|
|
3446
|
+
max-height: 42vh;
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
.pane {
|
|
3450
|
+
padding: var(--space-md) var(--space-sm) var(--space-xl);
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
.statusbar__hint {
|
|
3454
|
+
display: none;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3459
|
+
*,
|
|
3460
|
+
*::before,
|
|
3461
|
+
*::after {
|
|
3462
|
+
animation-duration: 0.01ms !important;
|
|
3463
|
+
animation-iteration-count: 1 !important;
|
|
3464
|
+
transition-duration: 0.01ms !important;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
`;
|
|
3468
|
+
|
|
3469
|
+
// src/studio/ui/tokens.ts
|
|
3470
|
+
var TOKENS_CSS = `
|
|
3471
|
+
:root {
|
|
3472
|
+
color-scheme: dark;
|
|
3473
|
+
|
|
3474
|
+
/* Surfaces */
|
|
3475
|
+
--base: #07090f;
|
|
3476
|
+
--surface: #0b0e16;
|
|
3477
|
+
--surface-elevated: #121623;
|
|
3478
|
+
--code-bg: #121623;
|
|
3479
|
+
|
|
3480
|
+
/* Text: AA on all three surfaces */
|
|
3481
|
+
--text: #eef2f8;
|
|
3482
|
+
--text-secondary: #a8b2c4;
|
|
3483
|
+
--text-muted: #77839a;
|
|
3484
|
+
|
|
3485
|
+
/* Electric blue accent scale */
|
|
3486
|
+
--accent: #8aa2ff;
|
|
3487
|
+
--accent-hover: #a6b8ff;
|
|
3488
|
+
--accent-active: #6f8ae6;
|
|
3489
|
+
--accent-soft: #16203a;
|
|
3490
|
+
--accent-soft-hover: #1d2a4d;
|
|
3491
|
+
--accent-border: #3d4a7a;
|
|
3492
|
+
|
|
3493
|
+
/* Semantics */
|
|
3494
|
+
--success: #4ade80;
|
|
3495
|
+
--warning: #fbbf24;
|
|
3496
|
+
--error: #f87171;
|
|
3497
|
+
--on-accent: #07090f;
|
|
3498
|
+
|
|
3499
|
+
/* Borders */
|
|
3500
|
+
--border: #1e2433;
|
|
3501
|
+
--border-strong: #2c3446;
|
|
3502
|
+
--border-input: #5f6b8a;
|
|
3503
|
+
|
|
3504
|
+
/* Structural lines are dashed by design; control borders stay solid */
|
|
3505
|
+
--hairline: 1px dashed var(--border);
|
|
3506
|
+
--hairline-strong: 1px dashed var(--border-strong);
|
|
3507
|
+
|
|
3508
|
+
/* Focus */
|
|
3509
|
+
--focus-ring: var(--accent);
|
|
3510
|
+
--focus-ring-offset: var(--base);
|
|
3511
|
+
|
|
3512
|
+
/* Only two shadows exist */
|
|
3513
|
+
--shadow-pop: 0 8px 24px rgb(0 0 0 / 0.28);
|
|
3514
|
+
--shadow-header: 0 1px 0 var(--border);
|
|
3515
|
+
|
|
3516
|
+
/* Scrim */
|
|
3517
|
+
--scrim: rgb(3 6 12 / 0.6);
|
|
3518
|
+
|
|
3519
|
+
/* Typography */
|
|
3520
|
+
--font-sans: 'Inter Variable', 'Inter', system-ui, -apple-system, 'Segoe UI',
|
|
3521
|
+
Roboto, sans-serif;
|
|
3522
|
+
--font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace,
|
|
3523
|
+
SFMono-Regular, Menlo, Consolas, monospace;
|
|
3524
|
+
|
|
3525
|
+
--text-display: 3.75rem;
|
|
3526
|
+
--text-h1: 2.75rem;
|
|
3527
|
+
--text-h2: 2.125rem;
|
|
3528
|
+
--text-h3: 1.5rem;
|
|
3529
|
+
--text-h4: 1.1875rem;
|
|
3530
|
+
--text-body: 1.0625rem;
|
|
3531
|
+
--text-small: 0.9375rem;
|
|
3532
|
+
--text-caption: 0.8125rem;
|
|
3533
|
+
--text-code: 0.875rem;
|
|
3534
|
+
|
|
3535
|
+
--leading-display: 1.1;
|
|
3536
|
+
--leading-h1: 1.15;
|
|
3537
|
+
--leading-h2: 1.2;
|
|
3538
|
+
--leading-h3: 1.3;
|
|
3539
|
+
--leading-body: 1.6;
|
|
3540
|
+
|
|
3541
|
+
/* Spacing */
|
|
3542
|
+
--space-3xs: 0.25rem;
|
|
3543
|
+
--space-2xs: 0.5rem;
|
|
3544
|
+
--space-xs: 0.75rem;
|
|
3545
|
+
--space-sm: 1rem;
|
|
3546
|
+
--space-md: 1.5rem;
|
|
3547
|
+
--space-lg: 2rem;
|
|
3548
|
+
--space-xl: 3rem;
|
|
3549
|
+
--space-2xl: 4rem;
|
|
3550
|
+
--space-3xl: 6rem;
|
|
3551
|
+
--space-4xl: 7.5rem;
|
|
3552
|
+
|
|
3553
|
+
/* Radii: square corners everywhere, roles kept for a one-line rollback */
|
|
3554
|
+
--radius-sm: 0px;
|
|
3555
|
+
--radius-md: 0px;
|
|
3556
|
+
--radius-pill: 0px;
|
|
3557
|
+
|
|
3558
|
+
/* Layout */
|
|
3559
|
+
--container: 1300px;
|
|
3560
|
+
--container-pad: 48px;
|
|
3561
|
+
--text-column: 800px;
|
|
3562
|
+
--hero-column: 560px;
|
|
3563
|
+
--header-height: 72px;
|
|
3564
|
+
|
|
3565
|
+
/* Motion */
|
|
3566
|
+
--duration-fast: 150ms;
|
|
3567
|
+
--duration-base: 250ms;
|
|
3568
|
+
--duration-reveal: 400ms;
|
|
3569
|
+
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
3570
|
+
|
|
3571
|
+
/* Syntax, >= 4.5 on --code-bg in both themes */
|
|
3572
|
+
--syn-comment: #77839a;
|
|
3573
|
+
--syn-keyword: #a78bfa;
|
|
3574
|
+
--syn-string: #86efac;
|
|
3575
|
+
--syn-number: #fbbf24;
|
|
3576
|
+
--syn-function: #7dd3fc;
|
|
3577
|
+
--syn-type: #f0abfc;
|
|
3578
|
+
--syn-variable: #e6edf7;
|
|
3579
|
+
--syn-tag: #f87171;
|
|
3580
|
+
--syn-operator: #9aa5b8;
|
|
3581
|
+
--syn-punctuation: #8b95a8;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
/* Light theme: same roles, measured values. Both blocks are the same theme:
|
|
3585
|
+
one answers the system preference when the user has not chosen, the other
|
|
3586
|
+
answers the stored choice. Keep them identical. */
|
|
3587
|
+
[data-theme='light'] {
|
|
3588
|
+
color-scheme: light;
|
|
3589
|
+
|
|
3590
|
+
--base: #ffffff;
|
|
3591
|
+
--surface: #f7f8fb;
|
|
3592
|
+
--surface-elevated: #eef1f7;
|
|
3593
|
+
--code-bg: #eef1f7;
|
|
3594
|
+
|
|
3595
|
+
--text: #0d1220;
|
|
3596
|
+
--text-secondary: #454f63;
|
|
3597
|
+
--text-muted: #5c6678;
|
|
3598
|
+
|
|
3599
|
+
--accent: #3d54d4;
|
|
3600
|
+
--accent-hover: #2b3ea8;
|
|
3601
|
+
--accent-active: #24337f;
|
|
3602
|
+
--accent-soft: #eaeeff;
|
|
3603
|
+
--accent-soft-hover: #dde3ff;
|
|
3604
|
+
--accent-border: #b9c2ee;
|
|
3605
|
+
|
|
3606
|
+
--success: #166534;
|
|
3607
|
+
--warning: #78350f;
|
|
3608
|
+
--error: #b91c1c;
|
|
3609
|
+
--on-accent: #ffffff;
|
|
3610
|
+
|
|
3611
|
+
--border: #dfe3ec;
|
|
3612
|
+
--border-strong: #c3cad8;
|
|
3613
|
+
--border-input: #828b9c;
|
|
3614
|
+
|
|
3615
|
+
--shadow-pop: 0 8px 24px rgb(13 18 32 / 0.1);
|
|
3616
|
+
--scrim: rgb(13 18 32 / 0.5);
|
|
3617
|
+
|
|
3618
|
+
--syn-comment: #5c6678;
|
|
3619
|
+
--syn-keyword: #7c3aed;
|
|
3620
|
+
--syn-string: #166534;
|
|
3621
|
+
--syn-number: #92400e;
|
|
3622
|
+
--syn-function: #0e7490;
|
|
3623
|
+
--syn-type: #a21caf;
|
|
3624
|
+
--syn-variable: #0d1220;
|
|
3625
|
+
--syn-tag: #b91c1c;
|
|
3626
|
+
--syn-operator: #5c6678;
|
|
3627
|
+
--syn-punctuation: #5c6678;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
@media (prefers-color-scheme: light) {
|
|
3631
|
+
:root:not([data-theme]) {
|
|
3632
|
+
color-scheme: light;
|
|
3633
|
+
|
|
3634
|
+
--base: #ffffff;
|
|
3635
|
+
--surface: #f7f8fb;
|
|
3636
|
+
--surface-elevated: #eef1f7;
|
|
3637
|
+
--code-bg: #eef1f7;
|
|
3638
|
+
|
|
3639
|
+
--text: #0d1220;
|
|
3640
|
+
--text-secondary: #454f63;
|
|
3641
|
+
--text-muted: #5c6678;
|
|
3642
|
+
|
|
3643
|
+
--accent: #3d54d4;
|
|
3644
|
+
--accent-hover: #2b3ea8;
|
|
3645
|
+
--accent-active: #24337f;
|
|
3646
|
+
--accent-soft: #eaeeff;
|
|
3647
|
+
--accent-soft-hover: #dde3ff;
|
|
3648
|
+
--accent-border: #b9c2ee;
|
|
3649
|
+
|
|
3650
|
+
--success: #166534;
|
|
3651
|
+
--warning: #78350f;
|
|
3652
|
+
--error: #b91c1c;
|
|
3653
|
+
--on-accent: #ffffff;
|
|
3654
|
+
|
|
3655
|
+
--border: #dfe3ec;
|
|
3656
|
+
--border-strong: #c3cad8;
|
|
3657
|
+
--border-input: #828b9c;
|
|
3658
|
+
|
|
3659
|
+
--shadow-pop: 0 8px 24px rgb(13 18 32 / 0.1);
|
|
3660
|
+
--scrim: rgb(13 18 32 / 0.5);
|
|
3661
|
+
|
|
3662
|
+
--syn-comment: #5c6678;
|
|
3663
|
+
--syn-keyword: #7c3aed;
|
|
3664
|
+
--syn-string: #166534;
|
|
3665
|
+
--syn-number: #92400e;
|
|
3666
|
+
--syn-function: #0e7490;
|
|
3667
|
+
--syn-type: #a21caf;
|
|
3668
|
+
--syn-variable: #0d1220;
|
|
3669
|
+
--syn-tag: #b91c1c;
|
|
3670
|
+
--syn-operator: #5c6678;
|
|
3671
|
+
--syn-punctuation: #5c6678;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
`;
|
|
3675
|
+
|
|
3676
|
+
// src/studio/ui/document.ts
|
|
3677
|
+
var THEME_BOOTSTRAP = `(function(){try{var t=localStorage.getItem('fwinit:theme');if(t==='light'||t==='dark'){document.documentElement.dataset.theme=t;}}catch(e){}})();`;
|
|
3678
|
+
var ICON = {
|
|
3679
|
+
search: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14 14"/></svg>',
|
|
3680
|
+
moon: '<svg class="icon-moon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M13.2 9.4A5.6 5.6 0 0 1 6.6 2.8a5.6 5.6 0 1 0 6.6 6.6Z"/></svg>',
|
|
3681
|
+
sun: '<svg class="icon-sun" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><circle cx="8" cy="8" r="3.2"/><path d="M8 1v1.6M8 13.4V15M1 8h1.6M13.4 8H15M3.05 3.05l1.13 1.13M11.82 11.82l1.13 1.13M12.95 3.05l-1.13 1.13M4.18 11.82l-1.13 1.13"/></svg>'
|
|
3682
|
+
};
|
|
3683
|
+
function escapeHtml(value) {
|
|
3684
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3685
|
+
}
|
|
3686
|
+
var FAVICON = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%2316203a'/%3E%3Crect x='0.5' y='0.5' width='31' height='31' fill='none' stroke='%238aa2ff'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='16' font-weight='700' fill='%238aa2ff' text-anchor='middle'%3ES%3C/text%3E%3C/svg%3E";
|
|
3687
|
+
function renderStudioHtml(options) {
|
|
3688
|
+
const meta = JSON.stringify({
|
|
3689
|
+
version: options.version,
|
|
3690
|
+
projectRoot: options.projectRoot,
|
|
3691
|
+
home: options.home ?? ""
|
|
3692
|
+
}).replace(/</g, "\\u003c");
|
|
3693
|
+
return `<!doctype html>
|
|
3694
|
+
<html lang="es">
|
|
3695
|
+
<head>
|
|
3696
|
+
<meta charset="utf-8" />
|
|
3697
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3698
|
+
<meta name="color-scheme" content="dark light" />
|
|
3699
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
3700
|
+
<title>fwinit studio</title>
|
|
3701
|
+
<meta name="description" content="Vista local de las skills instaladas para opencode, pi y Gentle Shell, y de las specs de este proyecto." />
|
|
3702
|
+
<link rel="icon" href="${FAVICON}" />
|
|
3703
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
3704
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
3705
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..700;1,14..32,400..600&family=JetBrains+Mono:wght@400;500;600&display=swap" />
|
|
3706
|
+
<script>${THEME_BOOTSTRAP}</script>
|
|
3707
|
+
<style>${TOKENS_CSS}</style>
|
|
3708
|
+
<style>${STUDIO_CSS}</style>
|
|
3709
|
+
</head>
|
|
3710
|
+
<body>
|
|
3711
|
+
<a class="skip" href="#main">Saltar al contenido</a>
|
|
3712
|
+
<header class="topbar">
|
|
3713
|
+
<div class="topbar__inner">
|
|
3714
|
+
<a class="brand" href="#/skills" aria-label="fwinit studio, inicio">
|
|
3715
|
+
<span class="brand__glyph" aria-hidden="true">S</span>
|
|
3716
|
+
<span class="brand__name">fwinit</span>
|
|
3717
|
+
<span class="brand__div" aria-hidden="true">/</span>
|
|
3718
|
+
<span class="brand__page">studio</span>
|
|
3719
|
+
</a>
|
|
3720
|
+
<nav class="tabs" aria-label="Secciones">
|
|
3721
|
+
<button type="button" class="tab" role="tab" data-view="skills" aria-selected="true">
|
|
3722
|
+
Skills <span class="tab__count" data-count="skills">\u2014</span>
|
|
3723
|
+
</button>
|
|
3724
|
+
<button type="button" class="tab" role="tab" data-view="specs" aria-selected="false">
|
|
3725
|
+
Specs <span class="tab__count" data-count="specs">\u2014</span>
|
|
3726
|
+
</button>
|
|
3727
|
+
</nav>
|
|
3728
|
+
<div class="topbar__actions">
|
|
3729
|
+
<span class="badge badge--version">v${escapeHtml(options.version)}</span>
|
|
3730
|
+
<button type="button" class="btn btn--icon theme-toggle" data-theme-toggle aria-pressed="false" aria-label="Cambiar al tema claro">
|
|
3731
|
+
${ICON.moon}${ICON.sun}
|
|
3732
|
+
</button>
|
|
3733
|
+
</div>
|
|
3734
|
+
</div>
|
|
3735
|
+
</header>
|
|
3736
|
+
|
|
3737
|
+
<main id="main" class="shell">
|
|
3738
|
+
<section class="view view--skills" data-view-panel="skills" data-active="true" aria-label="Skills instaladas">
|
|
3739
|
+
<aside class="rail">
|
|
3740
|
+
<div class="rail__head">
|
|
3741
|
+
<div class="field">
|
|
3742
|
+
${ICON.search}
|
|
3743
|
+
<input class="field__input" type="search" data-search="skills" placeholder="Filtrar skills" aria-label="Filtrar skills" autocomplete="off" spellcheck="false" />
|
|
3744
|
+
<kbd>/</kbd>
|
|
3745
|
+
</div>
|
|
3746
|
+
<div class="segmented" role="group" aria-label="Alcance">
|
|
3747
|
+
<button type="button" data-scope="global" aria-pressed="true">Globales</button>
|
|
3748
|
+
<button type="button" data-scope="project" aria-pressed="false">Proyecto</button>
|
|
3749
|
+
</div>
|
|
3750
|
+
</div>
|
|
3751
|
+
<div class="rail__body" data-list="skills">
|
|
3752
|
+
<p class="loading">Leyendo las skills instaladas\u2026</p>
|
|
3753
|
+
</div>
|
|
3754
|
+
</aside>
|
|
3755
|
+
<section class="pane" data-detail="skills">
|
|
3756
|
+
<p class="loading">Leyendo las skills instaladas\u2026</p>
|
|
3757
|
+
</section>
|
|
3758
|
+
</section>
|
|
3759
|
+
|
|
3760
|
+
<section class="view view--specs" data-view-panel="specs" data-active="false" aria-label="Specs del proyecto">
|
|
3761
|
+
<aside class="rail">
|
|
3762
|
+
<div class="rail__head">
|
|
3763
|
+
<div class="field">
|
|
3764
|
+
${ICON.search}
|
|
3765
|
+
<input class="field__input" type="search" data-search="specs" placeholder="Filtrar specs" aria-label="Filtrar specs" autocomplete="off" spellcheck="false" />
|
|
3766
|
+
<kbd>/</kbd>
|
|
3767
|
+
</div>
|
|
3768
|
+
</div>
|
|
3769
|
+
<div class="rail__body" data-list="specs">
|
|
3770
|
+
<p class="loading">Buscando specs/\u2026</p>
|
|
3771
|
+
</div>
|
|
3772
|
+
</aside>
|
|
3773
|
+
<section class="pane" data-detail="specs">
|
|
3774
|
+
<p class="loading">Buscando specs/\u2026</p>
|
|
3775
|
+
</section>
|
|
3776
|
+
<aside class="toc" data-toc aria-label="En esta p\xE1gina">
|
|
3777
|
+
<p class="toc__title">En esta p\xE1gina</p>
|
|
3778
|
+
<div class="toc__list" data-toc-list></div>
|
|
3779
|
+
</aside>
|
|
3780
|
+
</section>
|
|
3781
|
+
</main>
|
|
3782
|
+
|
|
3783
|
+
<footer class="statusbar">
|
|
3784
|
+
<span class="statusbar__dot" aria-hidden="true">\u25CF</span>
|
|
3785
|
+
<span class="statusbar__item" data-status-root title="">${escapeHtml(options.projectRoot)}</span>
|
|
3786
|
+
<span class="statusbar__spacer"></span>
|
|
3787
|
+
<span class="statusbar__item" data-status-counts></span>
|
|
3788
|
+
<span class="statusbar__item statusbar__hint"><kbd>/</kbd> filtrar \xB7 <kbd>1</kbd><kbd>2</kbd> vista \xB7 <kbd>t</kbd> tema</span>
|
|
3789
|
+
</footer>
|
|
3790
|
+
|
|
3791
|
+
<script type="application/json" id="studio-meta">${meta}</script>
|
|
3792
|
+
<script>${CLIENT_JS}</script>
|
|
3793
|
+
</body>
|
|
3794
|
+
</html>
|
|
3795
|
+
`;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
// src/studio/open-browser.ts
|
|
3799
|
+
import { spawn, spawnSync } from "child_process";
|
|
3800
|
+
function openBrowser(url) {
|
|
3801
|
+
const platform = process.platform;
|
|
3802
|
+
if (platform === "darwin") {
|
|
3803
|
+
return trySpawn("open", [url]);
|
|
3804
|
+
}
|
|
3805
|
+
if (platform === "win32") {
|
|
3806
|
+
const result = spawnSync("cmd", ["/c", "start", '""', url], {
|
|
3807
|
+
stdio: "ignore",
|
|
3808
|
+
windowsHide: true
|
|
3809
|
+
});
|
|
3810
|
+
return !result.error && result.status === 0;
|
|
3811
|
+
}
|
|
3812
|
+
for (const command of ["xdg-open", "gio", "sensible-browser", "x-www-browser"]) {
|
|
3813
|
+
const args = command === "gio" ? ["open", url] : [url];
|
|
3814
|
+
if (trySpawn(command, args)) return true;
|
|
3815
|
+
}
|
|
3816
|
+
const configured = process.env.BROWSER?.trim();
|
|
3817
|
+
if (configured) return trySpawn(configured, [url]);
|
|
3818
|
+
return false;
|
|
3819
|
+
}
|
|
3820
|
+
function trySpawn(command, args) {
|
|
3821
|
+
try {
|
|
3822
|
+
const child = spawn(command, args, {
|
|
3823
|
+
stdio: "ignore",
|
|
3824
|
+
detached: true,
|
|
3825
|
+
windowsHide: true
|
|
3826
|
+
});
|
|
3827
|
+
child.on("error", () => {
|
|
3828
|
+
});
|
|
3829
|
+
child.unref();
|
|
3830
|
+
return true;
|
|
3831
|
+
} catch {
|
|
3832
|
+
return false;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
// src/studio/server.ts
|
|
3837
|
+
var HOST = "127.0.0.1";
|
|
3838
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
3839
|
+
function isAllowedHost(header, port) {
|
|
3840
|
+
if (typeof header !== "string") return false;
|
|
3841
|
+
const value = header.trim().toLowerCase();
|
|
3842
|
+
if (value === "") return false;
|
|
3843
|
+
if (value.startsWith("[")) {
|
|
3844
|
+
const close = value.indexOf("]");
|
|
3845
|
+
if (close === -1) return false;
|
|
3846
|
+
const hostname2 = value.slice(0, close + 1);
|
|
3847
|
+
const rest = value.slice(close + 1);
|
|
3848
|
+
if (!LOOPBACK_HOSTNAMES.has(hostname2)) return false;
|
|
3849
|
+
if (rest === "") return true;
|
|
3850
|
+
return rest.startsWith(":") && rest.slice(1) !== "" && rest.slice(1) === String(port);
|
|
3851
|
+
}
|
|
3852
|
+
const colon = value.lastIndexOf(":");
|
|
3853
|
+
const hostname = colon === -1 ? value : value.slice(0, colon);
|
|
3854
|
+
if (!LOOPBACK_HOSTNAMES.has(hostname)) return false;
|
|
3855
|
+
if (colon === -1) return true;
|
|
3856
|
+
const requestedPort = value.slice(colon + 1);
|
|
3857
|
+
return requestedPort !== "" && requestedPort === String(port);
|
|
3858
|
+
}
|
|
3859
|
+
var SECURITY_HEADERS = {
|
|
3860
|
+
"X-Content-Type-Options": "nosniff",
|
|
3861
|
+
"X-Frame-Options": "DENY",
|
|
3862
|
+
"Referrer-Policy": "no-referrer",
|
|
3863
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
3864
|
+
// Everything is inline by design: one document, one stylesheet, one script.
|
|
3865
|
+
// Only the font files are remote, and only from Google Fonts.
|
|
3866
|
+
"Content-Security-Policy": [
|
|
3867
|
+
"default-src 'none'",
|
|
3868
|
+
"base-uri 'none'",
|
|
3869
|
+
"form-action 'none'",
|
|
3870
|
+
"frame-ancestors 'none'",
|
|
3871
|
+
"img-src data:",
|
|
3872
|
+
"style-src 'unsafe-inline' https://fonts.googleapis.com",
|
|
3873
|
+
"font-src https://fonts.gstatic.com",
|
|
3874
|
+
"script-src 'unsafe-inline'",
|
|
3875
|
+
"connect-src 'self'"
|
|
3876
|
+
].join("; ")
|
|
3877
|
+
};
|
|
3878
|
+
var SPEC_ERROR_STATUS = {
|
|
3879
|
+
invalid: 400,
|
|
3880
|
+
outside: 403,
|
|
3881
|
+
missing: 404,
|
|
3882
|
+
unsupported: 415
|
|
3883
|
+
};
|
|
3884
|
+
function sendJson(response, status, body) {
|
|
3885
|
+
const payload = JSON.stringify(body);
|
|
3886
|
+
response.writeHead(status, {
|
|
3887
|
+
...SECURITY_HEADERS,
|
|
3888
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
3889
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
3890
|
+
"Cache-Control": "no-store"
|
|
3891
|
+
});
|
|
3892
|
+
response.end(payload);
|
|
3893
|
+
}
|
|
3894
|
+
function sendHtml(response, html) {
|
|
3895
|
+
response.writeHead(200, {
|
|
3896
|
+
...SECURITY_HEADERS,
|
|
3897
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
3898
|
+
"Content-Length": Buffer.byteLength(html),
|
|
3899
|
+
"Cache-Control": "no-store"
|
|
3900
|
+
});
|
|
3901
|
+
response.end(html);
|
|
3902
|
+
}
|
|
3903
|
+
function sendText(response, status, text) {
|
|
3904
|
+
response.writeHead(status, {
|
|
3905
|
+
...SECURITY_HEADERS,
|
|
3906
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3907
|
+
"Content-Length": Buffer.byteLength(text),
|
|
3908
|
+
"Cache-Control": "no-store"
|
|
3909
|
+
});
|
|
3910
|
+
response.end(text);
|
|
3911
|
+
}
|
|
3912
|
+
async function startStudio(options = {}) {
|
|
3913
|
+
const projectRoot = resolve3(options.projectRoot ?? process.cwd());
|
|
3914
|
+
const version = options.version ?? "0.0.0";
|
|
3915
|
+
const home = options.home;
|
|
3916
|
+
let listeningPort = options.port ?? 0;
|
|
3917
|
+
const server = createServer((request, response) => {
|
|
3918
|
+
handle(request, response).catch((error) => {
|
|
3919
|
+
if (response.headersSent) {
|
|
3920
|
+
response.end();
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3923
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3924
|
+
process.stderr.write(`fwinit studio: ${message}
|
|
3925
|
+
`);
|
|
3926
|
+
sendJson(response, 500, { error: "No se pudo completar la solicitud." });
|
|
3927
|
+
});
|
|
3928
|
+
});
|
|
3929
|
+
async function handle(request, response) {
|
|
3930
|
+
const method = request.method ?? "GET";
|
|
3931
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
3932
|
+
response.setHeader("Allow", "GET, HEAD");
|
|
3933
|
+
sendJson(response, 405, { error: "M\xE9todo no permitido." });
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3936
|
+
const hostPort = listeningPort || request.socket.localPort || 0;
|
|
3937
|
+
if (!isAllowedHost(request.headers.host, hostPort)) {
|
|
3938
|
+
sendJson(response, 403, { error: "Host no permitido." });
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3941
|
+
const url2 = new URL(request.url ?? "/", `http://${HOST}`);
|
|
3942
|
+
let pathname;
|
|
3943
|
+
try {
|
|
3944
|
+
pathname = decodeURIComponent(url2.pathname);
|
|
3945
|
+
} catch {
|
|
3946
|
+
sendJson(response, 400, { error: "Ruta inv\xE1lida." });
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
3950
|
+
sendHtml(
|
|
3951
|
+
response,
|
|
3952
|
+
renderStudioHtml({
|
|
3953
|
+
version,
|
|
3954
|
+
projectRoot,
|
|
3955
|
+
home: home ?? process.env.HOME ?? ""
|
|
3956
|
+
})
|
|
3957
|
+
);
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
if (pathname === "/health") {
|
|
3961
|
+
sendJson(response, 200, { ok: true, projectRoot });
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
if (pathname === "/api/meta") {
|
|
3965
|
+
sendJson(response, 200, {
|
|
3966
|
+
version,
|
|
3967
|
+
projectRoot,
|
|
3968
|
+
home: home ?? process.env.HOME ?? "",
|
|
3969
|
+
platform: process.platform
|
|
3970
|
+
});
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
if (pathname === "/api/skills") {
|
|
3974
|
+
sendJson(response, 200, await discoverSkills({ projectRoot, home }));
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
if (pathname === "/api/specs") {
|
|
3978
|
+
sendJson(response, 200, await buildSpecsTree(projectRoot));
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
if (pathname === "/api/specs/file") {
|
|
3982
|
+
const relativePath = url2.searchParams.get("path");
|
|
3983
|
+
if (!relativePath) {
|
|
3984
|
+
sendJson(response, 400, { error: "Falta el par\xE1metro path." });
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
const specsRoot = join4(projectRoot, "specs");
|
|
3988
|
+
try {
|
|
3989
|
+
const file = await readSpecFile({ specsRoot, projectRoot }, relativePath);
|
|
3990
|
+
sendJson(response, 200, file);
|
|
3991
|
+
} catch (error) {
|
|
3992
|
+
if (error instanceof SpecPathError) {
|
|
3993
|
+
sendJson(response, SPEC_ERROR_STATUS[error.code] ?? 400, {
|
|
3994
|
+
error: error.message,
|
|
3995
|
+
code: error.code
|
|
3996
|
+
});
|
|
3997
|
+
return;
|
|
3998
|
+
}
|
|
3999
|
+
throw error;
|
|
4000
|
+
}
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
sendText(response, 404, "Not found");
|
|
4004
|
+
}
|
|
4005
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
4006
|
+
const onError = (error) => {
|
|
4007
|
+
server.off("listening", onListening);
|
|
4008
|
+
rejectListen(error);
|
|
4009
|
+
};
|
|
4010
|
+
const onListening = () => {
|
|
4011
|
+
server.off("error", onError);
|
|
4012
|
+
resolveListen();
|
|
4013
|
+
};
|
|
4014
|
+
server.once("error", onError);
|
|
4015
|
+
server.once("listening", onListening);
|
|
4016
|
+
server.listen(options.port ?? 0, HOST);
|
|
4017
|
+
});
|
|
4018
|
+
const address = server.address();
|
|
4019
|
+
const port = address?.port ?? 0;
|
|
4020
|
+
listeningPort = port;
|
|
4021
|
+
const url = `http://${HOST}:${port}`;
|
|
4022
|
+
const opened = options.open === false ? false : openBrowser(url);
|
|
4023
|
+
void opened;
|
|
4024
|
+
return {
|
|
4025
|
+
address: { url, host: HOST, port },
|
|
4026
|
+
close: () => new Promise((resolveClose) => {
|
|
4027
|
+
server.close(() => resolveClose());
|
|
4028
|
+
server.closeAllConnections?.();
|
|
4029
|
+
})
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
|
|
733
4033
|
// src/prompts.ts
|
|
734
4034
|
import inquirer from "inquirer";
|
|
735
4035
|
async function selectScope() {
|
|
@@ -878,24 +4178,24 @@ async function askGlobalSkillsToo() {
|
|
|
878
4178
|
}
|
|
879
4179
|
|
|
880
4180
|
// src/global-skills.ts
|
|
881
|
-
import { mkdir as mkdir2, cp as cp2, rm as rm2, readdir as
|
|
882
|
-
import { join as
|
|
4181
|
+
import { mkdir as mkdir2, cp as cp2, rm as rm2, readdir as readdir4, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
4182
|
+
import { join as join5 } from "path";
|
|
883
4183
|
function resolveGlobalTargets() {
|
|
884
4184
|
const home = process.env.HOME;
|
|
885
4185
|
if (!home) return [];
|
|
886
|
-
const gentleShellRoot = process.env.GENTLE_SHELL_HOME ||
|
|
4186
|
+
const gentleShellRoot = process.env.GENTLE_SHELL_HOME || join5(home, ".gentle-shell", "agent");
|
|
887
4187
|
return [
|
|
888
4188
|
{
|
|
889
4189
|
id: "opencode",
|
|
890
4190
|
label: `OpenCode global (${home}/.config/opencode)`,
|
|
891
|
-
root:
|
|
4191
|
+
root: join5(home, ".config", "opencode"),
|
|
892
4192
|
skillsDir: "skills",
|
|
893
4193
|
commandsDir: "command"
|
|
894
4194
|
},
|
|
895
4195
|
{
|
|
896
4196
|
id: "pi",
|
|
897
4197
|
label: `Pi global (${home}/.pi/agent)`,
|
|
898
|
-
root:
|
|
4198
|
+
root: join5(home, ".pi", "agent"),
|
|
899
4199
|
skillsDir: "skills",
|
|
900
4200
|
commandsDir: "prompts"
|
|
901
4201
|
},
|
|
@@ -909,7 +4209,7 @@ function resolveGlobalTargets() {
|
|
|
909
4209
|
{
|
|
910
4210
|
id: "agents",
|
|
911
4211
|
label: `Agent Skills global (${home}/.agents/skills)`,
|
|
912
|
-
root:
|
|
4212
|
+
root: join5(home, ".agents"),
|
|
913
4213
|
skillsDir: "skills"
|
|
914
4214
|
}
|
|
915
4215
|
];
|
|
@@ -923,11 +4223,11 @@ function filterBundleByNames(only) {
|
|
|
923
4223
|
async function ensureGlobalOpencodeConfig() {
|
|
924
4224
|
const home = process.env.HOME;
|
|
925
4225
|
if (!home) return;
|
|
926
|
-
const configPath =
|
|
4226
|
+
const configPath = join5(home, ".config", "opencode", "opencode.json");
|
|
927
4227
|
try {
|
|
928
4228
|
let data;
|
|
929
4229
|
try {
|
|
930
|
-
const content = await
|
|
4230
|
+
const content = await readFile4(configPath, "utf-8");
|
|
931
4231
|
data = JSON.parse(content);
|
|
932
4232
|
} catch {
|
|
933
4233
|
data = { agent: {} };
|
|
@@ -957,7 +4257,7 @@ function mapPlanToTarget(plan, target) {
|
|
|
957
4257
|
if (item.to.startsWith("skills/")) {
|
|
958
4258
|
mapped.push({
|
|
959
4259
|
from: item.from,
|
|
960
|
-
to:
|
|
4260
|
+
to: join5(target.root, target.skillsDir, item.to.slice("skills/".length)),
|
|
961
4261
|
recursive: item.recursive
|
|
962
4262
|
});
|
|
963
4263
|
continue;
|
|
@@ -966,7 +4266,7 @@ function mapPlanToTarget(plan, target) {
|
|
|
966
4266
|
if (!target.commandsDir) continue;
|
|
967
4267
|
mapped.push({
|
|
968
4268
|
from: item.from,
|
|
969
|
-
to:
|
|
4269
|
+
to: join5(target.root, target.commandsDir, item.to.slice("commands/".length)),
|
|
970
4270
|
recursive: item.recursive
|
|
971
4271
|
});
|
|
972
4272
|
continue;
|
|
@@ -985,7 +4285,7 @@ async function installGlobalSkills(repoRoot, targets, bundle = AGENT_BUNDLE) {
|
|
|
985
4285
|
await rm2(item.to, { recursive: true, force: true });
|
|
986
4286
|
}
|
|
987
4287
|
await mkdir2(
|
|
988
|
-
item.recursive === true ? item.to :
|
|
4288
|
+
item.recursive === true ? item.to : join5(item.to, ".."),
|
|
989
4289
|
{ recursive: true }
|
|
990
4290
|
);
|
|
991
4291
|
await cp2(item.from, item.to, {
|
|
@@ -1011,22 +4311,22 @@ async function uninstallGlobalSkills(targets, bundle = AGENT_BUNDLE) {
|
|
|
1011
4311
|
try {
|
|
1012
4312
|
const removed = [];
|
|
1013
4313
|
for (const name of skillNames) {
|
|
1014
|
-
await rm2(
|
|
4314
|
+
await rm2(join5(target.root, target.skillsDir, name), {
|
|
1015
4315
|
recursive: true,
|
|
1016
4316
|
force: true
|
|
1017
4317
|
});
|
|
1018
4318
|
removed.push(`${target.skillsDir}/${name}`);
|
|
1019
4319
|
}
|
|
1020
4320
|
if (target.commandsDir) {
|
|
1021
|
-
const cmdDir =
|
|
4321
|
+
const cmdDir = join5(target.root, target.commandsDir);
|
|
1022
4322
|
for (const name of skillNames) {
|
|
1023
|
-
await rm2(
|
|
4323
|
+
await rm2(join5(cmdDir, `${name}.md`), { force: true });
|
|
1024
4324
|
removed.push(`${target.commandsDir}/${name}.md`);
|
|
1025
4325
|
try {
|
|
1026
4326
|
const prefix = `${name}-`;
|
|
1027
|
-
for (const f of await
|
|
4327
|
+
for (const f of await readdir4(cmdDir)) {
|
|
1028
4328
|
if (f.startsWith(prefix) && f.endsWith(".md")) {
|
|
1029
|
-
await rm2(
|
|
4329
|
+
await rm2(join5(cmdDir, f), { force: true });
|
|
1030
4330
|
removed.push(`${target.commandsDir}/${f}`);
|
|
1031
4331
|
}
|
|
1032
4332
|
}
|
|
@@ -1044,7 +4344,7 @@ async function uninstallGlobalSkills(targets, bundle = AGENT_BUNDLE) {
|
|
|
1044
4344
|
|
|
1045
4345
|
// src/utils.ts
|
|
1046
4346
|
import { existsSync } from "fs";
|
|
1047
|
-
import { resolve } from "path";
|
|
4347
|
+
import { resolve as resolve4 } from "path";
|
|
1048
4348
|
var PM_RUNNER = {
|
|
1049
4349
|
npm: "npm",
|
|
1050
4350
|
pnpm: "pnpm",
|
|
@@ -1075,7 +4375,7 @@ var BUN_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
1075
4375
|
"rebuild"
|
|
1076
4376
|
]);
|
|
1077
4377
|
function getProjectPath(name) {
|
|
1078
|
-
return
|
|
4378
|
+
return resolve4(process.cwd(), name);
|
|
1079
4379
|
}
|
|
1080
4380
|
function projectExists(name) {
|
|
1081
4381
|
return existsSync(getProjectPath(name));
|
|
@@ -1144,10 +4444,10 @@ function adaptScriptToNode(script) {
|
|
|
1144
4444
|
return s;
|
|
1145
4445
|
}
|
|
1146
4446
|
async function adaptToNodeRuntime(projectPath) {
|
|
1147
|
-
const { readFile:
|
|
1148
|
-
const { join:
|
|
1149
|
-
const pkgPath =
|
|
1150
|
-
const pkg = JSON.parse(await
|
|
4447
|
+
const { readFile: readFile6, writeFile: writeFile3, readdir: readdir5 } = await import("fs/promises");
|
|
4448
|
+
const { join: join7 } = await import("path");
|
|
4449
|
+
const pkgPath = join7(projectPath, "package.json");
|
|
4450
|
+
const pkg = JSON.parse(await readFile6(pkgPath, "utf-8"));
|
|
1151
4451
|
for (const key of Object.keys(pkg.scripts ?? {})) {
|
|
1152
4452
|
pkg.scripts[key] = adaptScriptToNode(pkg.scripts[key]);
|
|
1153
4453
|
}
|
|
@@ -1157,27 +4457,27 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
1157
4457
|
delete pkg.devDependencies["bun-types"];
|
|
1158
4458
|
await writeFile3(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1159
4459
|
async function walk(dir) {
|
|
1160
|
-
const entries = await
|
|
4460
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
1161
4461
|
for (const entry of entries) {
|
|
1162
|
-
const full =
|
|
4462
|
+
const full = join7(dir, entry.name);
|
|
1163
4463
|
if (entry.isDirectory()) {
|
|
1164
4464
|
if (entry.name !== "node_modules") await walk(full);
|
|
1165
4465
|
} else if (entry.name.endsWith(".test.ts")) {
|
|
1166
|
-
const content = await
|
|
4466
|
+
const content = await readFile6(full, "utf-8");
|
|
1167
4467
|
if (content.includes("bun:test")) {
|
|
1168
4468
|
await writeFile3(full, content.replaceAll('from "bun:test"', 'from "vitest"'));
|
|
1169
4469
|
}
|
|
1170
4470
|
}
|
|
1171
4471
|
}
|
|
1172
4472
|
}
|
|
1173
|
-
const srcDir =
|
|
4473
|
+
const srcDir = join7(projectPath, "src");
|
|
1174
4474
|
try {
|
|
1175
4475
|
await walk(srcDir);
|
|
1176
4476
|
} catch {
|
|
1177
4477
|
}
|
|
1178
|
-
const tsconfigPath =
|
|
4478
|
+
const tsconfigPath = join7(projectPath, "tsconfig.json");
|
|
1179
4479
|
try {
|
|
1180
|
-
const tsconfig = JSON.parse(await
|
|
4480
|
+
const tsconfig = JSON.parse(await readFile6(tsconfigPath, "utf-8"));
|
|
1181
4481
|
if (tsconfig.compilerOptions?.types?.includes("bun-types")) {
|
|
1182
4482
|
delete tsconfig.compilerOptions.types;
|
|
1183
4483
|
await writeFile3(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
@@ -1187,8 +4487,8 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
1187
4487
|
}
|
|
1188
4488
|
async function writeRootGitignore(projectPath) {
|
|
1189
4489
|
const { writeFile: writeFile3, access } = await import("fs/promises");
|
|
1190
|
-
const { join:
|
|
1191
|
-
const gitignorePath =
|
|
4490
|
+
const { join: join7 } = await import("path");
|
|
4491
|
+
const gitignorePath = join7(projectPath, ".gitignore");
|
|
1192
4492
|
try {
|
|
1193
4493
|
await access(gitignorePath);
|
|
1194
4494
|
} catch {
|
|
@@ -1201,12 +4501,12 @@ async function writeRootGitignore(projectPath) {
|
|
|
1201
4501
|
async function gitInit(projectPath) {
|
|
1202
4502
|
const { execFile } = await import("child_process");
|
|
1203
4503
|
try {
|
|
1204
|
-
await new Promise((
|
|
4504
|
+
await new Promise((resolve5, reject) => {
|
|
1205
4505
|
execFile(
|
|
1206
4506
|
"git",
|
|
1207
4507
|
["init"],
|
|
1208
4508
|
{ cwd: projectPath },
|
|
1209
|
-
(err) => err ? reject(err) :
|
|
4509
|
+
(err) => err ? reject(err) : resolve5()
|
|
1210
4510
|
);
|
|
1211
4511
|
});
|
|
1212
4512
|
return true;
|
|
@@ -1216,8 +4516,8 @@ async function gitInit(projectPath) {
|
|
|
1216
4516
|
}
|
|
1217
4517
|
async function writeRootFiles(info) {
|
|
1218
4518
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
1219
|
-
const { join:
|
|
1220
|
-
const projectPath =
|
|
4519
|
+
const { join: join7 } = await import("path");
|
|
4520
|
+
const projectPath = resolve4(process.cwd(), info.projectName);
|
|
1221
4521
|
await writeRootGitignore(projectPath);
|
|
1222
4522
|
const installB = info.backend.postInit?.install ? resolveCommand(info.backend.postInit.install, info.pm) : `${info.pm} install`;
|
|
1223
4523
|
const devB = info.backend.postInit?.dev ? resolveCommand(info.backend.postInit.dev, info.pm) : `${info.pm} run dev`;
|
|
@@ -1252,7 +4552,7 @@ async function writeRootFiles(info) {
|
|
|
1252
4552
|
"```",
|
|
1253
4553
|
""
|
|
1254
4554
|
].join("\n");
|
|
1255
|
-
await writeFile3(
|
|
4555
|
+
await writeFile3(join7(projectPath, "README.md"), readme);
|
|
1256
4556
|
}
|
|
1257
4557
|
|
|
1258
4558
|
// src/index.ts
|
|
@@ -1271,15 +4571,15 @@ var AGENT_DIRS = [".opencode", ".pi", ".agents"];
|
|
|
1271
4571
|
async function installAgentSkills(repoRoot, projectPath) {
|
|
1272
4572
|
const plan = await planAgentBundle(repoRoot);
|
|
1273
4573
|
for (const agentDir of AGENT_DIRS) {
|
|
1274
|
-
const root =
|
|
4574
|
+
const root = join6(projectPath, agentDir);
|
|
1275
4575
|
for (const dir of plan.skillDirs) {
|
|
1276
|
-
await mkdir3(
|
|
4576
|
+
await mkdir3(join6(root, dir), { recursive: true });
|
|
1277
4577
|
}
|
|
1278
4578
|
for (const dir of plan.commandDirs) {
|
|
1279
|
-
await mkdir3(
|
|
4579
|
+
await mkdir3(join6(root, dir), { recursive: true });
|
|
1280
4580
|
}
|
|
1281
4581
|
for (const item of plan.installs) {
|
|
1282
|
-
await cp3(item.from,
|
|
4582
|
+
await cp3(item.from, join6(root, item.to), {
|
|
1283
4583
|
recursive: item.recursive === true
|
|
1284
4584
|
});
|
|
1285
4585
|
}
|
|
@@ -1316,7 +4616,7 @@ async function installGlobalSkillsWithSpinner(targets, bundle) {
|
|
|
1316
4616
|
async function readTemplatePostInit(dir) {
|
|
1317
4617
|
try {
|
|
1318
4618
|
const tj = JSON.parse(
|
|
1319
|
-
await
|
|
4619
|
+
await readFile5(join6(dir, "template.json"), "utf-8")
|
|
1320
4620
|
);
|
|
1321
4621
|
return tj.postInit;
|
|
1322
4622
|
} catch {
|
|
@@ -1326,14 +4626,14 @@ async function readTemplatePostInit(dir) {
|
|
|
1326
4626
|
async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm, org) {
|
|
1327
4627
|
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
1328
4628
|
const projectName = basename2(projectPath);
|
|
1329
|
-
const backendPath =
|
|
1330
|
-
const frontendPath =
|
|
4629
|
+
const backendPath = join6(projectPath, "backend");
|
|
4630
|
+
const frontendPath = join6(projectPath, "frontend");
|
|
1331
4631
|
await copyTemplate(
|
|
1332
|
-
|
|
4632
|
+
join6(repoRoot, "templates", backendTpl.folder),
|
|
1333
4633
|
backendPath
|
|
1334
4634
|
);
|
|
1335
4635
|
await copyTemplate(
|
|
1336
|
-
|
|
4636
|
+
join6(repoRoot, "templates", frontendTpl.folder),
|
|
1337
4637
|
frontendPath
|
|
1338
4638
|
);
|
|
1339
4639
|
await substituteTemplate(backendPath, projectName, backendTpl.folder, {
|
|
@@ -1493,6 +4793,64 @@ program.command("skills").description(
|
|
|
1493
4793
|
process.exit(1);
|
|
1494
4794
|
}
|
|
1495
4795
|
});
|
|
4796
|
+
program.command("studio").description(
|
|
4797
|
+
"Abrir en el navegador las skills instaladas (OpenCode, Pi, Gentle Shell) y las specs/ del proyecto"
|
|
4798
|
+
).option(
|
|
4799
|
+
"--port <puerto>",
|
|
4800
|
+
"Puerto fijo para el servidor local (por defecto lo elige el sistema)"
|
|
4801
|
+
).option("--no-open", "No abrir el navegador autom\xE1ticamente").action(async (opts) => {
|
|
4802
|
+
const rawPort = opts.port?.trim();
|
|
4803
|
+
const port = rawPort === void 0 ? 0 : rawPort === "" ? NaN : Number(rawPort);
|
|
4804
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
4805
|
+
console.error(chalk.red(`
|
|
4806
|
+
\u2716 Error: puerto inv\xE1lido: ${opts.port}
|
|
4807
|
+
`));
|
|
4808
|
+
process.exit(1);
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
const projectRoot = process.cwd();
|
|
4812
|
+
const home = process.env.HOME;
|
|
4813
|
+
let skillsLine = "no disponible";
|
|
4814
|
+
let specsLine = "no disponible";
|
|
4815
|
+
try {
|
|
4816
|
+
const report = await discoverSkills({ projectRoot, home });
|
|
4817
|
+
skillsLine = `${report.totals.global + report.totals.project} (${report.totals.global} globales \xB7 ${report.totals.project} del proyecto)`;
|
|
4818
|
+
} catch {
|
|
4819
|
+
}
|
|
4820
|
+
try {
|
|
4821
|
+
const specs = await buildSpecsTree(projectRoot);
|
|
4822
|
+
specsLine = specs.found ? `${specs.totals.files} archivos en specs/` : "sin carpeta specs/ (creala con la skill create-specs)";
|
|
4823
|
+
} catch {
|
|
4824
|
+
}
|
|
4825
|
+
try {
|
|
4826
|
+
const studio = await startStudio({
|
|
4827
|
+
port,
|
|
4828
|
+
projectRoot,
|
|
4829
|
+
version: getCliVersion(),
|
|
4830
|
+
open: opts.open !== false
|
|
4831
|
+
});
|
|
4832
|
+
console.log(chalk.bold("\n fwinit studio\n"));
|
|
4833
|
+
console.log(` ${chalk.dim("Proyecto")} ${projectRoot}`);
|
|
4834
|
+
console.log(` ${chalk.dim("Skills")} ${skillsLine}`);
|
|
4835
|
+
console.log(` ${chalk.dim("Specs")} ${specsLine}`);
|
|
4836
|
+
console.log(` ${chalk.dim("URL")} ${chalk.cyan(studio.address.url)}`);
|
|
4837
|
+
console.log(`
|
|
4838
|
+
${chalk.dim("Ctrl+C para cerrar")}
|
|
4839
|
+
`);
|
|
4840
|
+
const shutdown = () => {
|
|
4841
|
+
void studio.close().finally(() => process.exit(0));
|
|
4842
|
+
};
|
|
4843
|
+
process.once("SIGINT", shutdown);
|
|
4844
|
+
process.once("SIGTERM", shutdown);
|
|
4845
|
+
} catch (error) {
|
|
4846
|
+
console.error(
|
|
4847
|
+
chalk.red(`
|
|
4848
|
+
\u2716 Error: no se pudo abrir el studio: ${error.message}
|
|
4849
|
+
`)
|
|
4850
|
+
);
|
|
4851
|
+
process.exit(1);
|
|
4852
|
+
}
|
|
4853
|
+
});
|
|
1496
4854
|
program.argument("[template]", "Template a usar (o 'fullstack' para backend + frontend)").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option(
|
|
1497
4855
|
"-o, --org <domain>",
|
|
1498
4856
|
"Organizaci\xF3n reverse-domain para el bundle id de Flutter (ej: ar.miempresa)"
|
|
@@ -1645,7 +5003,7 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
1645
5003
|
spinner.succeed("Template descargado");
|
|
1646
5004
|
} else {
|
|
1647
5005
|
const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
|
|
1648
|
-
const codePath = useLayerLayout ?
|
|
5006
|
+
const codePath = useLayerLayout ? join6(projectPath, layer) : projectPath;
|
|
1649
5007
|
spinner.text = "Creando proyecto...";
|
|
1650
5008
|
await copyTemplate(templatePath, codePath);
|
|
1651
5009
|
await substituteTemplate(codePath, projectName, template.folder, {
|