fwinit 1.6.0 → 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/README.md +2 -2
- package/dist/index.js +3430 -65
- 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 = [
|
|
@@ -656,6 +656,13 @@ var AGENT_BUNDLE = [
|
|
|
656
656
|
{
|
|
657
657
|
// Biblioteca de estilos: skills/design/ con una subskill por estilo y sus
|
|
658
658
|
// commands (/design, /design-<estilo>) que leen specs/frontend/02-design.md.
|
|
659
|
+
// Adentro vive también el catálogo search-driven ui-ux-pro-max (Python
|
|
660
|
+
// stdlib, offline): no es un estilo fijo sino una base consultable que
|
|
661
|
+
// genera un design system, pero comparte familia, carpeta y comando
|
|
662
|
+
// (/design-ui-ux-pro-max). Como la biblioteca se copia entera, el catálogo
|
|
663
|
+
// viaja con su data/ y sus scripts/ — si sólo viajara su SKILL.md, cada
|
|
664
|
+
// búsqueda devolvería 0 resultados y el agente caería a defaults genéricos
|
|
665
|
+
// sin avisar.
|
|
659
666
|
kind: "library",
|
|
660
667
|
dir: "design",
|
|
661
668
|
required: false
|
|
@@ -672,33 +679,33 @@ async function exists(path) {
|
|
|
672
679
|
}
|
|
673
680
|
async function listMarkdown(dir) {
|
|
674
681
|
try {
|
|
675
|
-
const { readdir:
|
|
676
|
-
return (await
|
|
682
|
+
const { readdir: readdir5 } = await import("fs/promises");
|
|
683
|
+
return (await readdir5(dir)).filter((f) => f.endsWith(".md"));
|
|
677
684
|
} catch {
|
|
678
685
|
return [];
|
|
679
686
|
}
|
|
680
687
|
}
|
|
681
688
|
async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
682
|
-
const { join:
|
|
689
|
+
const { join: join7 } = await import("path");
|
|
683
690
|
const installs = [];
|
|
684
691
|
const skillDirs = [];
|
|
685
692
|
const missingRequired = [];
|
|
686
693
|
for (const entry of bundle) {
|
|
687
|
-
const src =
|
|
694
|
+
const src = join7(repoRoot, "skills", entry.dir);
|
|
688
695
|
if (entry.kind === "library") {
|
|
689
696
|
if (!await exists(src)) {
|
|
690
697
|
if (entry.required) missingRequired.push(entry.dir);
|
|
691
698
|
continue;
|
|
692
699
|
}
|
|
693
700
|
installs.push({ from: src, to: `skills/${entry.dir}`, recursive: true });
|
|
694
|
-
const cmds =
|
|
701
|
+
const cmds = join7(src, "commands");
|
|
695
702
|
for (const file of await listMarkdown(cmds)) {
|
|
696
|
-
installs.push({ from:
|
|
703
|
+
installs.push({ from: join7(cmds, file), to: `commands/${file}` });
|
|
697
704
|
}
|
|
698
705
|
continue;
|
|
699
706
|
}
|
|
700
|
-
const skill =
|
|
701
|
-
const command =
|
|
707
|
+
const skill = join7(src, "SKILL.md");
|
|
708
|
+
const command = join7(src, "command.md");
|
|
702
709
|
if (!await exists(skill)) {
|
|
703
710
|
if (entry.required) missingRequired.push(entry.dir);
|
|
704
711
|
continue;
|
|
@@ -711,17 +718,3316 @@ async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
|
711
718
|
missingRequired.push(`${entry.dir}/command.md`);
|
|
712
719
|
}
|
|
713
720
|
for (const extra of entry.extras ?? []) {
|
|
714
|
-
const from =
|
|
715
|
-
if (await exists(from))
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
721
|
+
const from = join7(src, extra);
|
|
722
|
+
if (!await exists(from)) continue;
|
|
723
|
+
installs.push({
|
|
724
|
+
from,
|
|
725
|
+
to: `skills/${entry.dir}/${extra}`,
|
|
726
|
+
recursive: true
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return { installs, skillDirs, commandDirs: ["commands"], missingRequired };
|
|
731
|
+
}
|
|
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');
|
|
720
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;
|
|
721
4000
|
}
|
|
4001
|
+
return;
|
|
722
4002
|
}
|
|
4003
|
+
sendText(response, 404, "Not found");
|
|
723
4004
|
}
|
|
724
|
-
|
|
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
|
+
};
|
|
725
4031
|
}
|
|
726
4032
|
|
|
727
4033
|
// src/prompts.ts
|
|
@@ -872,24 +4178,24 @@ async function askGlobalSkillsToo() {
|
|
|
872
4178
|
}
|
|
873
4179
|
|
|
874
4180
|
// src/global-skills.ts
|
|
875
|
-
import { mkdir as mkdir2, cp as cp2, rm as rm2, readdir as
|
|
876
|
-
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";
|
|
877
4183
|
function resolveGlobalTargets() {
|
|
878
4184
|
const home = process.env.HOME;
|
|
879
4185
|
if (!home) return [];
|
|
880
|
-
const gentleShellRoot = process.env.GENTLE_SHELL_HOME ||
|
|
4186
|
+
const gentleShellRoot = process.env.GENTLE_SHELL_HOME || join5(home, ".gentle-shell", "agent");
|
|
881
4187
|
return [
|
|
882
4188
|
{
|
|
883
4189
|
id: "opencode",
|
|
884
4190
|
label: `OpenCode global (${home}/.config/opencode)`,
|
|
885
|
-
root:
|
|
4191
|
+
root: join5(home, ".config", "opencode"),
|
|
886
4192
|
skillsDir: "skills",
|
|
887
4193
|
commandsDir: "command"
|
|
888
4194
|
},
|
|
889
4195
|
{
|
|
890
4196
|
id: "pi",
|
|
891
4197
|
label: `Pi global (${home}/.pi/agent)`,
|
|
892
|
-
root:
|
|
4198
|
+
root: join5(home, ".pi", "agent"),
|
|
893
4199
|
skillsDir: "skills",
|
|
894
4200
|
commandsDir: "prompts"
|
|
895
4201
|
},
|
|
@@ -903,7 +4209,7 @@ function resolveGlobalTargets() {
|
|
|
903
4209
|
{
|
|
904
4210
|
id: "agents",
|
|
905
4211
|
label: `Agent Skills global (${home}/.agents/skills)`,
|
|
906
|
-
root:
|
|
4212
|
+
root: join5(home, ".agents"),
|
|
907
4213
|
skillsDir: "skills"
|
|
908
4214
|
}
|
|
909
4215
|
];
|
|
@@ -917,11 +4223,11 @@ function filterBundleByNames(only) {
|
|
|
917
4223
|
async function ensureGlobalOpencodeConfig() {
|
|
918
4224
|
const home = process.env.HOME;
|
|
919
4225
|
if (!home) return;
|
|
920
|
-
const configPath =
|
|
4226
|
+
const configPath = join5(home, ".config", "opencode", "opencode.json");
|
|
921
4227
|
try {
|
|
922
4228
|
let data;
|
|
923
4229
|
try {
|
|
924
|
-
const content = await
|
|
4230
|
+
const content = await readFile4(configPath, "utf-8");
|
|
925
4231
|
data = JSON.parse(content);
|
|
926
4232
|
} catch {
|
|
927
4233
|
data = { agent: {} };
|
|
@@ -951,7 +4257,7 @@ function mapPlanToTarget(plan, target) {
|
|
|
951
4257
|
if (item.to.startsWith("skills/")) {
|
|
952
4258
|
mapped.push({
|
|
953
4259
|
from: item.from,
|
|
954
|
-
to:
|
|
4260
|
+
to: join5(target.root, target.skillsDir, item.to.slice("skills/".length)),
|
|
955
4261
|
recursive: item.recursive
|
|
956
4262
|
});
|
|
957
4263
|
continue;
|
|
@@ -960,7 +4266,7 @@ function mapPlanToTarget(plan, target) {
|
|
|
960
4266
|
if (!target.commandsDir) continue;
|
|
961
4267
|
mapped.push({
|
|
962
4268
|
from: item.from,
|
|
963
|
-
to:
|
|
4269
|
+
to: join5(target.root, target.commandsDir, item.to.slice("commands/".length)),
|
|
964
4270
|
recursive: item.recursive
|
|
965
4271
|
});
|
|
966
4272
|
continue;
|
|
@@ -979,7 +4285,7 @@ async function installGlobalSkills(repoRoot, targets, bundle = AGENT_BUNDLE) {
|
|
|
979
4285
|
await rm2(item.to, { recursive: true, force: true });
|
|
980
4286
|
}
|
|
981
4287
|
await mkdir2(
|
|
982
|
-
item.recursive === true ? item.to :
|
|
4288
|
+
item.recursive === true ? item.to : join5(item.to, ".."),
|
|
983
4289
|
{ recursive: true }
|
|
984
4290
|
);
|
|
985
4291
|
await cp2(item.from, item.to, {
|
|
@@ -1005,22 +4311,22 @@ async function uninstallGlobalSkills(targets, bundle = AGENT_BUNDLE) {
|
|
|
1005
4311
|
try {
|
|
1006
4312
|
const removed = [];
|
|
1007
4313
|
for (const name of skillNames) {
|
|
1008
|
-
await rm2(
|
|
4314
|
+
await rm2(join5(target.root, target.skillsDir, name), {
|
|
1009
4315
|
recursive: true,
|
|
1010
4316
|
force: true
|
|
1011
4317
|
});
|
|
1012
4318
|
removed.push(`${target.skillsDir}/${name}`);
|
|
1013
4319
|
}
|
|
1014
4320
|
if (target.commandsDir) {
|
|
1015
|
-
const cmdDir =
|
|
4321
|
+
const cmdDir = join5(target.root, target.commandsDir);
|
|
1016
4322
|
for (const name of skillNames) {
|
|
1017
|
-
await rm2(
|
|
4323
|
+
await rm2(join5(cmdDir, `${name}.md`), { force: true });
|
|
1018
4324
|
removed.push(`${target.commandsDir}/${name}.md`);
|
|
1019
4325
|
try {
|
|
1020
4326
|
const prefix = `${name}-`;
|
|
1021
|
-
for (const f of await
|
|
4327
|
+
for (const f of await readdir4(cmdDir)) {
|
|
1022
4328
|
if (f.startsWith(prefix) && f.endsWith(".md")) {
|
|
1023
|
-
await rm2(
|
|
4329
|
+
await rm2(join5(cmdDir, f), { force: true });
|
|
1024
4330
|
removed.push(`${target.commandsDir}/${f}`);
|
|
1025
4331
|
}
|
|
1026
4332
|
}
|
|
@@ -1038,7 +4344,7 @@ async function uninstallGlobalSkills(targets, bundle = AGENT_BUNDLE) {
|
|
|
1038
4344
|
|
|
1039
4345
|
// src/utils.ts
|
|
1040
4346
|
import { existsSync } from "fs";
|
|
1041
|
-
import { resolve } from "path";
|
|
4347
|
+
import { resolve as resolve4 } from "path";
|
|
1042
4348
|
var PM_RUNNER = {
|
|
1043
4349
|
npm: "npm",
|
|
1044
4350
|
pnpm: "pnpm",
|
|
@@ -1069,7 +4375,7 @@ var BUN_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
1069
4375
|
"rebuild"
|
|
1070
4376
|
]);
|
|
1071
4377
|
function getProjectPath(name) {
|
|
1072
|
-
return
|
|
4378
|
+
return resolve4(process.cwd(), name);
|
|
1073
4379
|
}
|
|
1074
4380
|
function projectExists(name) {
|
|
1075
4381
|
return existsSync(getProjectPath(name));
|
|
@@ -1138,10 +4444,10 @@ function adaptScriptToNode(script) {
|
|
|
1138
4444
|
return s;
|
|
1139
4445
|
}
|
|
1140
4446
|
async function adaptToNodeRuntime(projectPath) {
|
|
1141
|
-
const { readFile:
|
|
1142
|
-
const { join:
|
|
1143
|
-
const pkgPath =
|
|
1144
|
-
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"));
|
|
1145
4451
|
for (const key of Object.keys(pkg.scripts ?? {})) {
|
|
1146
4452
|
pkg.scripts[key] = adaptScriptToNode(pkg.scripts[key]);
|
|
1147
4453
|
}
|
|
@@ -1151,27 +4457,27 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
1151
4457
|
delete pkg.devDependencies["bun-types"];
|
|
1152
4458
|
await writeFile3(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1153
4459
|
async function walk(dir) {
|
|
1154
|
-
const entries = await
|
|
4460
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
1155
4461
|
for (const entry of entries) {
|
|
1156
|
-
const full =
|
|
4462
|
+
const full = join7(dir, entry.name);
|
|
1157
4463
|
if (entry.isDirectory()) {
|
|
1158
4464
|
if (entry.name !== "node_modules") await walk(full);
|
|
1159
4465
|
} else if (entry.name.endsWith(".test.ts")) {
|
|
1160
|
-
const content = await
|
|
4466
|
+
const content = await readFile6(full, "utf-8");
|
|
1161
4467
|
if (content.includes("bun:test")) {
|
|
1162
4468
|
await writeFile3(full, content.replaceAll('from "bun:test"', 'from "vitest"'));
|
|
1163
4469
|
}
|
|
1164
4470
|
}
|
|
1165
4471
|
}
|
|
1166
4472
|
}
|
|
1167
|
-
const srcDir =
|
|
4473
|
+
const srcDir = join7(projectPath, "src");
|
|
1168
4474
|
try {
|
|
1169
4475
|
await walk(srcDir);
|
|
1170
4476
|
} catch {
|
|
1171
4477
|
}
|
|
1172
|
-
const tsconfigPath =
|
|
4478
|
+
const tsconfigPath = join7(projectPath, "tsconfig.json");
|
|
1173
4479
|
try {
|
|
1174
|
-
const tsconfig = JSON.parse(await
|
|
4480
|
+
const tsconfig = JSON.parse(await readFile6(tsconfigPath, "utf-8"));
|
|
1175
4481
|
if (tsconfig.compilerOptions?.types?.includes("bun-types")) {
|
|
1176
4482
|
delete tsconfig.compilerOptions.types;
|
|
1177
4483
|
await writeFile3(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
@@ -1181,8 +4487,8 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
1181
4487
|
}
|
|
1182
4488
|
async function writeRootGitignore(projectPath) {
|
|
1183
4489
|
const { writeFile: writeFile3, access } = await import("fs/promises");
|
|
1184
|
-
const { join:
|
|
1185
|
-
const gitignorePath =
|
|
4490
|
+
const { join: join7 } = await import("path");
|
|
4491
|
+
const gitignorePath = join7(projectPath, ".gitignore");
|
|
1186
4492
|
try {
|
|
1187
4493
|
await access(gitignorePath);
|
|
1188
4494
|
} catch {
|
|
@@ -1195,12 +4501,12 @@ async function writeRootGitignore(projectPath) {
|
|
|
1195
4501
|
async function gitInit(projectPath) {
|
|
1196
4502
|
const { execFile } = await import("child_process");
|
|
1197
4503
|
try {
|
|
1198
|
-
await new Promise((
|
|
4504
|
+
await new Promise((resolve5, reject) => {
|
|
1199
4505
|
execFile(
|
|
1200
4506
|
"git",
|
|
1201
4507
|
["init"],
|
|
1202
4508
|
{ cwd: projectPath },
|
|
1203
|
-
(err) => err ? reject(err) :
|
|
4509
|
+
(err) => err ? reject(err) : resolve5()
|
|
1204
4510
|
);
|
|
1205
4511
|
});
|
|
1206
4512
|
return true;
|
|
@@ -1210,8 +4516,8 @@ async function gitInit(projectPath) {
|
|
|
1210
4516
|
}
|
|
1211
4517
|
async function writeRootFiles(info) {
|
|
1212
4518
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
1213
|
-
const { join:
|
|
1214
|
-
const projectPath =
|
|
4519
|
+
const { join: join7 } = await import("path");
|
|
4520
|
+
const projectPath = resolve4(process.cwd(), info.projectName);
|
|
1215
4521
|
await writeRootGitignore(projectPath);
|
|
1216
4522
|
const installB = info.backend.postInit?.install ? resolveCommand(info.backend.postInit.install, info.pm) : `${info.pm} install`;
|
|
1217
4523
|
const devB = info.backend.postInit?.dev ? resolveCommand(info.backend.postInit.dev, info.pm) : `${info.pm} run dev`;
|
|
@@ -1226,7 +4532,7 @@ async function writeRootFiles(info) {
|
|
|
1226
4532
|
"",
|
|
1227
4533
|
`- \`backend/\` \u2014 ${info.backend.description}`,
|
|
1228
4534
|
`- \`frontend/\` \u2014 ${info.frontend.description}`,
|
|
1229
|
-
"- `.opencode/`, `.pi/` y `.agents/` \u2014 skills de agentes: `/create-prompt` para detallar tu idea, `/create-specs` para generar las specs, `/create-specs-from-code` para documentar un proyecto que ya existe, + biblioteca de dise\xF1o (dark-luxury, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism). Mismo bundle en las 3 ubicaciones: OpenCode lee `.opencode/`; Pi y Gentle Shell leen `.pi/` (skills + prompt templates); los agentes Agent Skills leen `.agents/`",
|
|
4535
|
+
"- `.opencode/`, `.pi/` y `.agents/` \u2014 skills de agentes: `/create-prompt` para detallar tu idea, `/create-specs` para generar las specs, `/create-specs-from-code` para documentar un proyecto que ya existe, + biblioteca de dise\xF1o con `/design` (estilos dark-luxury, docs-starship, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism) y el cat\xE1logo search-driven `/design-ui-ux-pro-max` (requiere Python 3). Mismo bundle en las 3 ubicaciones: OpenCode lee `.opencode/`; Pi y Gentle Shell leen `.pi/` (skills + prompt templates); los agentes Agent Skills leen `.agents/`",
|
|
1230
4536
|
"- `specs/` \u2014 especificaciones del proyecto",
|
|
1231
4537
|
"",
|
|
1232
4538
|
"## Backend",
|
|
@@ -1246,7 +4552,7 @@ async function writeRootFiles(info) {
|
|
|
1246
4552
|
"```",
|
|
1247
4553
|
""
|
|
1248
4554
|
].join("\n");
|
|
1249
|
-
await writeFile3(
|
|
4555
|
+
await writeFile3(join7(projectPath, "README.md"), readme);
|
|
1250
4556
|
}
|
|
1251
4557
|
|
|
1252
4558
|
// src/index.ts
|
|
@@ -1265,15 +4571,15 @@ var AGENT_DIRS = [".opencode", ".pi", ".agents"];
|
|
|
1265
4571
|
async function installAgentSkills(repoRoot, projectPath) {
|
|
1266
4572
|
const plan = await planAgentBundle(repoRoot);
|
|
1267
4573
|
for (const agentDir of AGENT_DIRS) {
|
|
1268
|
-
const root =
|
|
4574
|
+
const root = join6(projectPath, agentDir);
|
|
1269
4575
|
for (const dir of plan.skillDirs) {
|
|
1270
|
-
await mkdir3(
|
|
4576
|
+
await mkdir3(join6(root, dir), { recursive: true });
|
|
1271
4577
|
}
|
|
1272
4578
|
for (const dir of plan.commandDirs) {
|
|
1273
|
-
await mkdir3(
|
|
4579
|
+
await mkdir3(join6(root, dir), { recursive: true });
|
|
1274
4580
|
}
|
|
1275
4581
|
for (const item of plan.installs) {
|
|
1276
|
-
await cp3(item.from,
|
|
4582
|
+
await cp3(item.from, join6(root, item.to), {
|
|
1277
4583
|
recursive: item.recursive === true
|
|
1278
4584
|
});
|
|
1279
4585
|
}
|
|
@@ -1310,7 +4616,7 @@ async function installGlobalSkillsWithSpinner(targets, bundle) {
|
|
|
1310
4616
|
async function readTemplatePostInit(dir) {
|
|
1311
4617
|
try {
|
|
1312
4618
|
const tj = JSON.parse(
|
|
1313
|
-
await
|
|
4619
|
+
await readFile5(join6(dir, "template.json"), "utf-8")
|
|
1314
4620
|
);
|
|
1315
4621
|
return tj.postInit;
|
|
1316
4622
|
} catch {
|
|
@@ -1320,14 +4626,14 @@ async function readTemplatePostInit(dir) {
|
|
|
1320
4626
|
async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm, org) {
|
|
1321
4627
|
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
1322
4628
|
const projectName = basename2(projectPath);
|
|
1323
|
-
const backendPath =
|
|
1324
|
-
const frontendPath =
|
|
4629
|
+
const backendPath = join6(projectPath, "backend");
|
|
4630
|
+
const frontendPath = join6(projectPath, "frontend");
|
|
1325
4631
|
await copyTemplate(
|
|
1326
|
-
|
|
4632
|
+
join6(repoRoot, "templates", backendTpl.folder),
|
|
1327
4633
|
backendPath
|
|
1328
4634
|
);
|
|
1329
4635
|
await copyTemplate(
|
|
1330
|
-
|
|
4636
|
+
join6(repoRoot, "templates", frontendTpl.folder),
|
|
1331
4637
|
frontendPath
|
|
1332
4638
|
);
|
|
1333
4639
|
await substituteTemplate(backendPath, projectName, backendTpl.folder, {
|
|
@@ -1399,7 +4705,8 @@ async function runSkillsFlow(opts = {}) {
|
|
|
1399
4705
|
console.log(chalk.bold("\nSkills disponibles:\n"));
|
|
1400
4706
|
console.log(chalk.dim(" create-specs \u2014 specs para proyecto nuevo (/create-specs)"));
|
|
1401
4707
|
console.log(chalk.dim(" create-specs-from-code \u2014 specs desde c\xF3digo existente (/create-specs-from-code)"));
|
|
1402
|
-
console.log(chalk.dim("
|
|
4708
|
+
console.log(chalk.dim(" create-prompt \u2014 detalla una idea en specs/prompt.md (/create-prompt)"));
|
|
4709
|
+
console.log(chalk.dim(" design/ \u2014 dark-luxury, glassmorphism, minimal-dashboard, minimal-light, neo-brutalist, docs-starship + cat\xE1logo ui-ux-pro-max (/design-<estilo>; el cat\xE1logo requiere Python 3)\n"));
|
|
1403
4710
|
let bundle;
|
|
1404
4711
|
if (opts.only) {
|
|
1405
4712
|
const names = opts.only.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -1472,7 +4779,7 @@ program.command("skills").description(
|
|
|
1472
4779
|
"Destinos separados por coma: opencode, pi, gentle-shell, agents (o 'all'). Saltea la pregunta"
|
|
1473
4780
|
).option(
|
|
1474
4781
|
"-o, --only <skills>",
|
|
1475
|
-
"Solo estas skills, separadas por coma: create-specs, create-specs-from-code, design"
|
|
4782
|
+
"Solo estas skills, separadas por coma: create-specs, create-specs-from-code, create-prompt, design"
|
|
1476
4783
|
).option(
|
|
1477
4784
|
"-r, --remove",
|
|
1478
4785
|
"Eliminar en vez de instalar (combinar con --global y opcionalmente --only)"
|
|
@@ -1486,6 +4793,64 @@ program.command("skills").description(
|
|
|
1486
4793
|
process.exit(1);
|
|
1487
4794
|
}
|
|
1488
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
|
+
});
|
|
1489
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(
|
|
1490
4855
|
"-o, --org <domain>",
|
|
1491
4856
|
"Organizaci\xF3n reverse-domain para el bundle id de Flutter (ej: ar.miempresa)"
|
|
@@ -1638,7 +5003,7 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
1638
5003
|
spinner.succeed("Template descargado");
|
|
1639
5004
|
} else {
|
|
1640
5005
|
const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
|
|
1641
|
-
const codePath = useLayerLayout ?
|
|
5006
|
+
const codePath = useLayerLayout ? join6(projectPath, layer) : projectPath;
|
|
1642
5007
|
spinner.text = "Creando proyecto...";
|
|
1643
5008
|
await copyTemplate(templatePath, codePath);
|
|
1644
5009
|
await substituteTemplate(codePath, projectName, template.folder, {
|