spec-layer 0.5.0 → 0.6.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 +100 -17
- package/dist/cli.js +1029 -276
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -559,7 +559,8 @@ var require_sha256 = __commonJS({
|
|
|
559
559
|
import { parseArgs } from "node:util";
|
|
560
560
|
|
|
561
561
|
// src/commands.ts
|
|
562
|
-
import {
|
|
562
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
563
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
563
564
|
|
|
564
565
|
// ../extractor/src/statesMatrix.ts
|
|
565
566
|
var STATE_ORDER = [
|
|
@@ -1611,15 +1612,15 @@ function styleMember(p, property, scopes, path, name) {
|
|
|
1611
1612
|
});
|
|
1612
1613
|
return null;
|
|
1613
1614
|
}
|
|
1614
|
-
const
|
|
1615
|
-
if ("omit" in
|
|
1616
|
-
if (
|
|
1615
|
+
const converted2 = dtcgLiteral(property.resolved, scopes, p.options.values);
|
|
1616
|
+
if ("omit" in converted2) {
|
|
1617
|
+
if (converted2.omit === "unit_not_expressible") {
|
|
1617
1618
|
reportOnce(p, {
|
|
1618
1619
|
code: "unit_not_expressible",
|
|
1619
1620
|
severity: "info",
|
|
1620
1621
|
path,
|
|
1621
1622
|
message: `The ${name} unit is not a DTCG dimension unit; the value is kept under $extensions.`,
|
|
1622
|
-
details: { property: name, ...
|
|
1623
|
+
details: { property: name, ...converted2.details }
|
|
1623
1624
|
});
|
|
1624
1625
|
const d = property.resolved;
|
|
1625
1626
|
return { extension: { value: d.number, unit: d.unit } };
|
|
@@ -1629,11 +1630,11 @@ function styleMember(p, property, scopes, path, name) {
|
|
|
1629
1630
|
severity: "warning",
|
|
1630
1631
|
path,
|
|
1631
1632
|
message: `The ${name} property has a type DTCG cannot state and was omitted.`,
|
|
1632
|
-
details: { property: name, ...
|
|
1633
|
+
details: { property: name, ...converted2.details }
|
|
1633
1634
|
});
|
|
1634
1635
|
return null;
|
|
1635
1636
|
}
|
|
1636
|
-
return { value:
|
|
1637
|
+
return { value: converted2.$value };
|
|
1637
1638
|
}
|
|
1638
1639
|
var TYPOGRAPHY_MEMBERS = [
|
|
1639
1640
|
["font_family", "fontFamily", []],
|
|
@@ -1733,8 +1734,8 @@ function effectLeaf(p, style, path) {
|
|
|
1733
1734
|
}
|
|
1734
1735
|
const raw = effect[field];
|
|
1735
1736
|
if (raw === void 0) continue;
|
|
1736
|
-
const
|
|
1737
|
-
if (!("omit" in
|
|
1737
|
+
const converted2 = dtcgLiteral(raw, [], p.options.values);
|
|
1738
|
+
if (!("omit" in converted2)) shadow[name] = converted2.$value;
|
|
1738
1739
|
}
|
|
1739
1740
|
shadow.inset = effect.type === "inner_shadow";
|
|
1740
1741
|
shadows.push(shadow);
|
|
@@ -2000,7 +2001,7 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2000
2001
|
}
|
|
2001
2002
|
return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
|
|
2002
2003
|
}
|
|
2003
|
-
const
|
|
2004
|
+
const converted2 = projectedLiteral(p, token, value.value, (override) => {
|
|
2004
2005
|
reportOnce(p, {
|
|
2005
2006
|
code: "unit_override_conflicts_with_scope",
|
|
2006
2007
|
severity: "warning",
|
|
@@ -2009,18 +2010,18 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2009
2010
|
details: { id: token.id, override, scopes: [...token.scopes] }
|
|
2010
2011
|
});
|
|
2011
2012
|
});
|
|
2012
|
-
if ("omit" in
|
|
2013
|
+
if ("omit" in converted2) {
|
|
2013
2014
|
reportOnce(p, {
|
|
2014
|
-
code:
|
|
2015
|
+
code: converted2.omit,
|
|
2015
2016
|
severity: "warning",
|
|
2016
2017
|
path,
|
|
2017
2018
|
mode,
|
|
2018
|
-
message:
|
|
2019
|
-
details: { id: token.id, ...
|
|
2019
|
+
message: converted2.omit === "type_not_expressible" ? `DTCG has no ${String(converted2.details.type)} type; the value was omitted.` : `DTCG dimensions take px or rem; a ${String(converted2.details.unit)} value was omitted.`,
|
|
2020
|
+
details: { id: token.id, ...converted2.details }
|
|
2020
2021
|
});
|
|
2021
2022
|
return null;
|
|
2022
2023
|
}
|
|
2023
|
-
return { $type:
|
|
2024
|
+
return { $type: converted2.$type, $value: converted2.$value, ...description };
|
|
2024
2025
|
}
|
|
2025
2026
|
function dtcgExportFiles(out) {
|
|
2026
2027
|
const text = (v) => `${JSON.stringify(v, null, 2)}
|
|
@@ -2033,6 +2034,465 @@ function dtcgExportFiles(out) {
|
|
|
2033
2034
|
return files;
|
|
2034
2035
|
}
|
|
2035
2036
|
|
|
2037
|
+
// ../extractor/src/v5/outputs/naming.ts
|
|
2038
|
+
var NAME_CASES = ["kebab", "camel", "pascal", "snake", "constant"];
|
|
2039
|
+
function splitWords(segment) {
|
|
2040
|
+
return segment.replace(new RegExp("(\\p{Ll})(\\p{Lu})", "gu"), "$1 $2").split(/[^\p{L}\p{N}]+/u).filter((w) => w.length > 0);
|
|
2041
|
+
}
|
|
2042
|
+
function pathWords(path) {
|
|
2043
|
+
return path.split(".").flatMap((seg) => {
|
|
2044
|
+
const words = splitWords(seg);
|
|
2045
|
+
return words.length > 0 ? words : ["_"];
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
function joinWords(words, nameCase) {
|
|
2049
|
+
const lower = words.map((w) => w.toLowerCase());
|
|
2050
|
+
const cap = (w) => w.charAt(0).toUpperCase() + w.slice(1);
|
|
2051
|
+
switch (nameCase) {
|
|
2052
|
+
case "kebab":
|
|
2053
|
+
return lower.join("-");
|
|
2054
|
+
case "snake":
|
|
2055
|
+
return lower.join("_");
|
|
2056
|
+
case "constant":
|
|
2057
|
+
return lower.join("_").toUpperCase();
|
|
2058
|
+
case "camel":
|
|
2059
|
+
return lower.map((w, i) => i === 0 ? w : cap(w)).join("");
|
|
2060
|
+
case "pascal":
|
|
2061
|
+
return lower.map(cap).join("");
|
|
2062
|
+
default: {
|
|
2063
|
+
const exhaustive = nameCase;
|
|
2064
|
+
return exhaustive;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
function deriveName(path, nameCase) {
|
|
2069
|
+
return joinWords(pathWords(path), nameCase);
|
|
2070
|
+
}
|
|
2071
|
+
function sortReport(entries) {
|
|
2072
|
+
return [...entries].sort((a, b) => compareCodeUnits(a.path, b.path) || compareCodeUnits(a.code, b.code) || compareCodeUnits(a.mode ?? "", b.mode ?? ""));
|
|
2073
|
+
}
|
|
2074
|
+
function resolveNames(paths, meta, rules) {
|
|
2075
|
+
const report2 = [];
|
|
2076
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2077
|
+
for (const path of [...paths].sort(compareCodeUnits)) {
|
|
2078
|
+
let name = null;
|
|
2079
|
+
let source = "derived";
|
|
2080
|
+
const declared = rules.codeSyntaxKey ? meta[path]?.code_syntax?.[rules.codeSyntaxKey] : void 0;
|
|
2081
|
+
if (declared !== void 0) {
|
|
2082
|
+
name = rules.acceptDeclared(declared);
|
|
2083
|
+
if (name !== null) {
|
|
2084
|
+
source = "code_syntax";
|
|
2085
|
+
} else {
|
|
2086
|
+
report2.push({
|
|
2087
|
+
code: "code_syntax_not_usable",
|
|
2088
|
+
severity: "info",
|
|
2089
|
+
path,
|
|
2090
|
+
message: `The declared ${rules.codeSyntaxKey} identifier "${declared}" is not a name this format can use; the name was derived instead.`,
|
|
2091
|
+
details: { declared, platform: rules.codeSyntaxKey ?? "" }
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (name === null) name = rules.affix(deriveName(path, rules.nameCase));
|
|
2096
|
+
const list = candidates.get(name) ?? [];
|
|
2097
|
+
list.push({ path, source });
|
|
2098
|
+
candidates.set(name, list);
|
|
2099
|
+
}
|
|
2100
|
+
const names = /* @__PURE__ */ new Map();
|
|
2101
|
+
const map = {};
|
|
2102
|
+
for (const [name, list] of candidates) {
|
|
2103
|
+
if (list.length > 1) {
|
|
2104
|
+
const collided = list.map((c) => c.path).sort(compareCodeUnits);
|
|
2105
|
+
for (const c of list) {
|
|
2106
|
+
report2.push({
|
|
2107
|
+
code: "name_collision",
|
|
2108
|
+
severity: "error",
|
|
2109
|
+
path: c.path,
|
|
2110
|
+
message: `${list.length} tokens would share the name ${name}; all were omitted.`,
|
|
2111
|
+
details: { name, paths: collided }
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
names.set(list[0].path, name);
|
|
2117
|
+
map[list[0].path] = { name, source: list[0].source };
|
|
2118
|
+
}
|
|
2119
|
+
const sortedMap = Object.fromEntries(Object.entries(map).sort(([a], [b]) => compareCodeUnits(a, b)));
|
|
2120
|
+
return { names, map: sortedMap, report: sortReport(report2) };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// ../extractor/src/v5/outputs/css.ts
|
|
2124
|
+
var CSS_DEFAULTS = {
|
|
2125
|
+
case: "kebab",
|
|
2126
|
+
root: ":root",
|
|
2127
|
+
modeSelector: '[data-theme="{mode}"]'
|
|
2128
|
+
};
|
|
2129
|
+
var CSS_HEADER_PREFIX = "/* Generated by spec-layer";
|
|
2130
|
+
var EXT = "com.spec-layer";
|
|
2131
|
+
var CUSTOM_PROPERTY = /^--[A-Za-z0-9_-]+$/;
|
|
2132
|
+
var BARE_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
2133
|
+
function acceptCssDeclared(declared) {
|
|
2134
|
+
if (CUSTOM_PROPERTY.test(declared)) return declared;
|
|
2135
|
+
if (BARE_IDENT.test(declared)) return `--${declared}`;
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
var asRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
|
|
2139
|
+
function collectLeaves(tree, prefix, out) {
|
|
2140
|
+
const node = asRecord(tree);
|
|
2141
|
+
if (!node) return;
|
|
2142
|
+
if ("$value" in node) {
|
|
2143
|
+
const ours = asRecord(asRecord(node.$extensions)?.[EXT]);
|
|
2144
|
+
out.push({
|
|
2145
|
+
path: prefix.join("."),
|
|
2146
|
+
type: typeof node.$type === "string" ? node.$type : "",
|
|
2147
|
+
value: node.$value,
|
|
2148
|
+
ext: ours
|
|
2149
|
+
});
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
for (const key of Object.keys(node)) {
|
|
2153
|
+
if (key.startsWith("$")) continue;
|
|
2154
|
+
collectLeaves(node[key], [...prefix, key], out);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
var unpointer = (s) => s.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2158
|
+
var refFile = (src) => {
|
|
2159
|
+
const r = asRecord(src);
|
|
2160
|
+
return r && typeof r.$ref === "string" ? r.$ref : null;
|
|
2161
|
+
};
|
|
2162
|
+
function sourcesOf(resolver) {
|
|
2163
|
+
const out = [];
|
|
2164
|
+
for (const { $ref } of resolver.resolutionOrder) {
|
|
2165
|
+
const set = /^#\/sets\/(.+)$/.exec($ref);
|
|
2166
|
+
const mod = /^#\/modifiers\/(.+)$/.exec($ref);
|
|
2167
|
+
if (set) {
|
|
2168
|
+
const label = unpointer(set[1]);
|
|
2169
|
+
for (const src of resolver.sets[label]?.sources ?? []) {
|
|
2170
|
+
const file = refFile(src);
|
|
2171
|
+
if (file) out.push({ collection: label, mode: null, file, isDefault: true });
|
|
2172
|
+
}
|
|
2173
|
+
} else if (mod) {
|
|
2174
|
+
const label = unpointer(mod[1]);
|
|
2175
|
+
const m = resolver.modifiers[label];
|
|
2176
|
+
if (!m) continue;
|
|
2177
|
+
for (const [context, srcs] of Object.entries(m.contexts)) {
|
|
2178
|
+
for (const src of srcs) {
|
|
2179
|
+
const file = refFile(src);
|
|
2180
|
+
if (file) out.push({ collection: label, mode: context, file, isDefault: context === m.default });
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return out;
|
|
2186
|
+
}
|
|
2187
|
+
var modeSlug = (file) => file.replace(/\.json$/, "").split(".").slice(1).join(".");
|
|
2188
|
+
var collectionSlug = (file) => file.split(".")[0];
|
|
2189
|
+
var REF = /^\{(.+)\}$/;
|
|
2190
|
+
function hexChannels(hex) {
|
|
2191
|
+
const at = (i) => parseInt(hex.slice(i, i + 2), 16);
|
|
2192
|
+
return [at(1), at(3), at(5)];
|
|
2193
|
+
}
|
|
2194
|
+
var quoteFamily = (f) => `"${f.replace(/["\\]/g, "\\$&")}"`;
|
|
2195
|
+
function report(ctx, entry2) {
|
|
2196
|
+
ctx.report.push({ ...entry2, path: ctx.path, ...ctx.mode !== void 0 ? { mode: ctx.mode } : {} });
|
|
2197
|
+
}
|
|
2198
|
+
function cssValue(ctx, type, value, property) {
|
|
2199
|
+
const member = property !== void 0 ? { property } : {};
|
|
2200
|
+
if (typeof value === "string") {
|
|
2201
|
+
const ref = REF.exec(value);
|
|
2202
|
+
if (ref) {
|
|
2203
|
+
const name = ctx.alive.has(ref[1]) ? ctx.names.get(ref[1]) : void 0;
|
|
2204
|
+
if (name !== void 0) return `var(${name})`;
|
|
2205
|
+
report(ctx, {
|
|
2206
|
+
code: "reference_target_omitted",
|
|
2207
|
+
severity: "warning",
|
|
2208
|
+
message: `References ${ref[1]}, which this output does not emit; the value was omitted.`,
|
|
2209
|
+
details: { target: ref[1], ...member }
|
|
2210
|
+
});
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
switch (type) {
|
|
2215
|
+
case "color": {
|
|
2216
|
+
if (typeof value === "string") return value;
|
|
2217
|
+
const c = asRecord(value);
|
|
2218
|
+
if (c && typeof c.hex === "string" && typeof c.alpha === "number") {
|
|
2219
|
+
if (c.alpha === 1) return c.hex;
|
|
2220
|
+
const [r, g, b] = hexChannels(c.hex);
|
|
2221
|
+
return `rgb(${r} ${g} ${b} / ${c.alpha})`;
|
|
2222
|
+
}
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
case "dimension":
|
|
2226
|
+
case "duration": {
|
|
2227
|
+
if (typeof value === "string") return value;
|
|
2228
|
+
const d = asRecord(value);
|
|
2229
|
+
if (d && typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
case "number":
|
|
2233
|
+
case "fontWeight":
|
|
2234
|
+
if (typeof value === "number") return String(value);
|
|
2235
|
+
break;
|
|
2236
|
+
case "cubicBezier":
|
|
2237
|
+
if (Array.isArray(value) && value.length === 4 && value.every((n) => typeof n === "number")) {
|
|
2238
|
+
return `cubic-bezier(${value.join(", ")})`;
|
|
2239
|
+
}
|
|
2240
|
+
break;
|
|
2241
|
+
case "fontFamily":
|
|
2242
|
+
if (typeof value === "string") return quoteFamily(value);
|
|
2243
|
+
if (Array.isArray(value) && value.every((f) => typeof f === "string")) {
|
|
2244
|
+
return value.map(quoteFamily).join(", ");
|
|
2245
|
+
}
|
|
2246
|
+
break;
|
|
2247
|
+
default:
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
report(ctx, {
|
|
2251
|
+
code: "not_expressible",
|
|
2252
|
+
severity: "warning",
|
|
2253
|
+
message: `CSS has no form for this ${type || "untyped"} value; it was omitted.`,
|
|
2254
|
+
details: { type, value, ...member }
|
|
2255
|
+
});
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
var TEXT_TRANSFORM = { original: "none", upper: "uppercase", lower: "lowercase", title: "capitalize" };
|
|
2259
|
+
var TEXT_DECORATION = { none: "none", underline: "underline", strikethrough: "line-through" };
|
|
2260
|
+
var TYPOGRAPHY_MEMBERS2 = [
|
|
2261
|
+
["fontFamily", "fontFamily"],
|
|
2262
|
+
["fontSize", "dimension"],
|
|
2263
|
+
["fontWeight", "fontWeight"],
|
|
2264
|
+
["lineHeight", "number"],
|
|
2265
|
+
["letterSpacing", "dimension"]
|
|
2266
|
+
];
|
|
2267
|
+
var TEXT_MEMBERS = [
|
|
2268
|
+
["textTransform", "textCase", TEXT_TRANSFORM],
|
|
2269
|
+
["textDecoration", "textDecoration", TEXT_DECORATION]
|
|
2270
|
+
];
|
|
2271
|
+
function typographyMemberKeys(value, ext) {
|
|
2272
|
+
const keys = [];
|
|
2273
|
+
for (const [key] of TYPOGRAPHY_MEMBERS2) {
|
|
2274
|
+
if (key in value) keys.push(key);
|
|
2275
|
+
}
|
|
2276
|
+
for (const key of ["lineHeight", "letterSpacing"]) {
|
|
2277
|
+
if (key in value) continue;
|
|
2278
|
+
const d = asRecord(ext[key]);
|
|
2279
|
+
if (d && typeof d.value === "number" && typeof d.unit === "string") keys.push(key);
|
|
2280
|
+
}
|
|
2281
|
+
for (const [key, extKey] of TEXT_MEMBERS) {
|
|
2282
|
+
if (typeof ext[extKey] === "string") keys.push(key);
|
|
2283
|
+
}
|
|
2284
|
+
return keys;
|
|
2285
|
+
}
|
|
2286
|
+
function converted(ctx, property, from, to) {
|
|
2287
|
+
report(ctx, {
|
|
2288
|
+
code: "value_converted",
|
|
2289
|
+
severity: "info",
|
|
2290
|
+
message: `${property} was restated from ${String(from.value)}${String(from.unit)} as ${to}.`,
|
|
2291
|
+
details: { property, from, to }
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
function extensionDimension(ctx, ext, key, name, percentTo) {
|
|
2295
|
+
const d = asRecord(ext[key]);
|
|
2296
|
+
if (!d || typeof d.value !== "number" || typeof d.unit !== "string") return null;
|
|
2297
|
+
if (d.unit === "%") {
|
|
2298
|
+
const to = percentTo(canonicalNumber(d.value / 100));
|
|
2299
|
+
converted(ctx, key, d, to);
|
|
2300
|
+
return `${name}: ${to};`;
|
|
2301
|
+
}
|
|
2302
|
+
return `${name}: ${d.value}${d.unit};`;
|
|
2303
|
+
}
|
|
2304
|
+
function typographyDecls(ctx, leaf, names) {
|
|
2305
|
+
const decls = [];
|
|
2306
|
+
const declaredPaths = [];
|
|
2307
|
+
const value = asRecord(leaf.value) ?? {};
|
|
2308
|
+
const ext = leaf.ext ?? {};
|
|
2309
|
+
const nameFor = (key) => names.get(`${leaf.path}.${key}`);
|
|
2310
|
+
for (const [key, type] of TYPOGRAPHY_MEMBERS2) {
|
|
2311
|
+
if (!(key in value)) continue;
|
|
2312
|
+
const name = nameFor(key);
|
|
2313
|
+
if (name === void 0) continue;
|
|
2314
|
+
const css = cssValue(ctx, type, value[key], key);
|
|
2315
|
+
if (css !== null) {
|
|
2316
|
+
decls.push(`${name}: ${css};`);
|
|
2317
|
+
declaredPaths.push(`${leaf.path}.${key}`);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (!("lineHeight" in value)) {
|
|
2321
|
+
const name = nameFor("lineHeight");
|
|
2322
|
+
if (name !== void 0) {
|
|
2323
|
+
const d = extensionDimension(ctx, ext, "lineHeight", name, (n) => String(n));
|
|
2324
|
+
if (d) {
|
|
2325
|
+
decls.push(d);
|
|
2326
|
+
declaredPaths.push(`${leaf.path}.lineHeight`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (!("letterSpacing" in value)) {
|
|
2331
|
+
const name = nameFor("letterSpacing");
|
|
2332
|
+
if (name !== void 0) {
|
|
2333
|
+
const d = extensionDimension(ctx, ext, "letterSpacing", name, (n) => `${n}em`);
|
|
2334
|
+
if (d) {
|
|
2335
|
+
decls.push(d);
|
|
2336
|
+
declaredPaths.push(`${leaf.path}.letterSpacing`);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
for (const [key, extKey, table] of TEXT_MEMBERS) {
|
|
2341
|
+
const raw = ext[extKey];
|
|
2342
|
+
if (typeof raw !== "string") continue;
|
|
2343
|
+
const name = nameFor(key);
|
|
2344
|
+
if (name === void 0) continue;
|
|
2345
|
+
const css = table[raw];
|
|
2346
|
+
if (css !== void 0) {
|
|
2347
|
+
decls.push(`${name}: ${css};`);
|
|
2348
|
+
declaredPaths.push(`${leaf.path}.${key}`);
|
|
2349
|
+
} else {
|
|
2350
|
+
report(ctx, {
|
|
2351
|
+
code: "not_expressible",
|
|
2352
|
+
severity: "info",
|
|
2353
|
+
message: `CSS text properties have no form for ${extKey} "${raw}"; it was omitted.`,
|
|
2354
|
+
details: { property: extKey, value: raw }
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return { decls, declaredPaths };
|
|
2359
|
+
}
|
|
2360
|
+
var SHADOW_MEMBERS = [
|
|
2361
|
+
["offsetX", "dimension"],
|
|
2362
|
+
["offsetY", "dimension"],
|
|
2363
|
+
["blur", "dimension"],
|
|
2364
|
+
["spread", "dimension"],
|
|
2365
|
+
["color", "color"]
|
|
2366
|
+
];
|
|
2367
|
+
function shadowDecl(ctx, leaf, name) {
|
|
2368
|
+
const layers = Array.isArray(leaf.value) ? leaf.value : [];
|
|
2369
|
+
const omit = (reason, message) => {
|
|
2370
|
+
report(ctx, { code: "not_expressible", severity: "warning", message, details: { reason } });
|
|
2371
|
+
return null;
|
|
2372
|
+
};
|
|
2373
|
+
if (layers.length === 0) return omit("no_visible_shadow", "The style has no visible shadow, so no box-shadow was written.");
|
|
2374
|
+
const parts = [];
|
|
2375
|
+
for (const layer of layers) {
|
|
2376
|
+
const l = asRecord(layer);
|
|
2377
|
+
if (!l) return omit("layer_not_an_object", "A shadow layer is not an object; the style was omitted.");
|
|
2378
|
+
const members = [];
|
|
2379
|
+
for (const [key, type] of SHADOW_MEMBERS) {
|
|
2380
|
+
if (!(key in l)) return omit(`missing_${key}`, `A shadow layer has no ${key}, which box-shadow needs; the style was omitted.`);
|
|
2381
|
+
const css = cssValue(ctx, type, l[key], key);
|
|
2382
|
+
if (css === null) return null;
|
|
2383
|
+
members.push(css);
|
|
2384
|
+
}
|
|
2385
|
+
parts.push(`${l.inset === true ? "inset " : ""}${members.join(" ")}`);
|
|
2386
|
+
}
|
|
2387
|
+
return `${name}: ${parts.join(", ")};`;
|
|
2388
|
+
}
|
|
2389
|
+
var commentSafe = (text) => text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
2390
|
+
function headerText(header, nameCase) {
|
|
2391
|
+
return `${CSS_HEADER_PREFIX} from library ${commentSafe(header.libraryId)}, foundation ${header.contentHash}, ${header.platform}/${header.format}/${nameCase}.
|
|
2392
|
+
Do not edit. Change the design in Figma, republish, and run spec-layer pull. */`;
|
|
2393
|
+
}
|
|
2394
|
+
function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
|
|
2395
|
+
const entries = [];
|
|
2396
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2397
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
2398
|
+
for (const s of sources) {
|
|
2399
|
+
const perCollection = modes?.[s.collection];
|
|
2400
|
+
const selector = s.isDefault ? root : (perCollection ?? template).replace(/\{mode\}/g, modeSlug(s.file)).replace(/\{collection\}/g, collectionSlug(s.file));
|
|
2401
|
+
const decls = [];
|
|
2402
|
+
for (const leaf of leavesByFile.get(s.file) ?? []) {
|
|
2403
|
+
const ctx = { names, alive, report: entries, path: leaf.path, ...s.mode !== null ? { mode: s.mode } : {} };
|
|
2404
|
+
if (leaf.type === "typography") {
|
|
2405
|
+
const t = typographyDecls(ctx, leaf, names);
|
|
2406
|
+
decls.push(...t.decls);
|
|
2407
|
+
for (const p of t.declaredPaths) declared.add(p);
|
|
2408
|
+
} else {
|
|
2409
|
+
const name = names.get(leaf.path);
|
|
2410
|
+
if (name === void 0) continue;
|
|
2411
|
+
if (leaf.type === "shadow") {
|
|
2412
|
+
const d = shadowDecl(ctx, leaf, name);
|
|
2413
|
+
if (d !== null) {
|
|
2414
|
+
decls.push(d);
|
|
2415
|
+
declared.add(leaf.path);
|
|
2416
|
+
}
|
|
2417
|
+
} else {
|
|
2418
|
+
const v = cssValue(ctx, leaf.type, leaf.value);
|
|
2419
|
+
if (v !== null) {
|
|
2420
|
+
decls.push(`${name}: ${v};`);
|
|
2421
|
+
declared.add(leaf.path);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (decls.length === 0) continue;
|
|
2427
|
+
const lines = blocks.get(selector) ?? [];
|
|
2428
|
+
lines.push(` /* ${commentSafe(`${s.collection}${s.mode !== null ? `, ${s.mode}` : ""}`)} */`, ...decls.map((d) => ` ${d}`));
|
|
2429
|
+
blocks.set(selector, lines);
|
|
2430
|
+
}
|
|
2431
|
+
return { blocks, entries, declared };
|
|
2432
|
+
}
|
|
2433
|
+
function cssOutput(exp, header, options = {}) {
|
|
2434
|
+
const nameCase = options.case ?? CSS_DEFAULTS.case;
|
|
2435
|
+
const root = options.root ?? CSS_DEFAULTS.root;
|
|
2436
|
+
const template = options.modeSelector ?? CSS_DEFAULTS.modeSelector;
|
|
2437
|
+
const sources = sourcesOf(exp.resolver);
|
|
2438
|
+
const leavesByFile = /* @__PURE__ */ new Map();
|
|
2439
|
+
for (const s of sources) {
|
|
2440
|
+
if (!leavesByFile.has(s.file)) {
|
|
2441
|
+
const out = [];
|
|
2442
|
+
collectLeaves(exp.files[s.file] ?? {}, [], out);
|
|
2443
|
+
leavesByFile.set(s.file, out);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
const candidatePaths = /* @__PURE__ */ new Set();
|
|
2447
|
+
for (const leaves of leavesByFile.values()) {
|
|
2448
|
+
for (const leaf of leaves) {
|
|
2449
|
+
if (leaf.type === "typography") {
|
|
2450
|
+
const value = asRecord(leaf.value) ?? {};
|
|
2451
|
+
const ext = leaf.ext ?? {};
|
|
2452
|
+
for (const key of typographyMemberKeys(value, ext)) candidatePaths.add(`${leaf.path}.${key}`);
|
|
2453
|
+
} else {
|
|
2454
|
+
candidatePaths.add(leaf.path);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
const resolved2 = resolveNames([...candidatePaths], exp.meta, {
|
|
2459
|
+
codeSyntaxKey: "WEB",
|
|
2460
|
+
acceptDeclared: acceptCssDeclared,
|
|
2461
|
+
affix: (body2) => `--${body2}`,
|
|
2462
|
+
nameCase
|
|
2463
|
+
});
|
|
2464
|
+
const names = resolved2.names;
|
|
2465
|
+
const emit = (alive2) => emitPass(sources, leavesByFile, names, alive2, root, template, options.modes);
|
|
2466
|
+
let alive = new Set(names.keys());
|
|
2467
|
+
let pass = emit(alive);
|
|
2468
|
+
for (; ; ) {
|
|
2469
|
+
if (pass.declared.size === alive.size) break;
|
|
2470
|
+
alive = pass.declared;
|
|
2471
|
+
pass = emit(alive);
|
|
2472
|
+
}
|
|
2473
|
+
const map = {};
|
|
2474
|
+
for (const [path, entry2] of Object.entries(resolved2.map)) {
|
|
2475
|
+
if (alive.has(path)) map[path] = entry2;
|
|
2476
|
+
}
|
|
2477
|
+
const entries = [...resolved2.report, ...pass.entries];
|
|
2478
|
+
const shared = [...new Set(sources.filter((s) => !s.isDefault && options.modes?.[s.collection] === void 0).map((s) => s.collection))].sort(compareCodeUnits);
|
|
2479
|
+
if (shared.length > 1) {
|
|
2480
|
+
for (const collection of shared) {
|
|
2481
|
+
entries.push({
|
|
2482
|
+
code: "mode_selector_shared",
|
|
2483
|
+
severity: "warning",
|
|
2484
|
+
path: collection,
|
|
2485
|
+
message: `${shared.length} collections with modes share the selector template ${template}; one attribute cannot carry both axes. Declare a selector per collection under outputs[].modes.`,
|
|
2486
|
+
details: { modifiers: shared, selector: template }
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const body = [...pass.blocks].flatMap(([selector, lines]) => [`${selector} {`, ...lines, "}", ""]);
|
|
2491
|
+
const text = `${[headerText(header, nameCase), "", ...body].join("\n").trimEnd()}
|
|
2492
|
+
`;
|
|
2493
|
+
return { text, map, report: sortReport(entries) };
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2036
2496
|
// ../extractor/src/v5/componentContext.ts
|
|
2037
2497
|
var import_js_sha2563 = __toESM(require_sha256(), 1);
|
|
2038
2498
|
|
|
@@ -2101,6 +2561,9 @@ function parseLibraryBundle(input) {
|
|
|
2101
2561
|
};
|
|
2102
2562
|
}
|
|
2103
2563
|
|
|
2564
|
+
// ../extractor/src/libraryBundleHash.ts
|
|
2565
|
+
var import_js_sha2564 = __toESM(require_sha256(), 1);
|
|
2566
|
+
|
|
2104
2567
|
// src/bundle.ts
|
|
2105
2568
|
function parseBundle(raw) {
|
|
2106
2569
|
try {
|
|
@@ -2121,8 +2584,8 @@ function parseBundle(raw) {
|
|
|
2121
2584
|
}
|
|
2122
2585
|
|
|
2123
2586
|
// src/config.ts
|
|
2124
|
-
import { readFileSync as
|
|
2125
|
-
import { join as
|
|
2587
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "node:fs";
|
|
2588
|
+
import { join as join3 } from "node:path";
|
|
2126
2589
|
|
|
2127
2590
|
// src/credentials.ts
|
|
2128
2591
|
import { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
|
|
@@ -2155,6 +2618,268 @@ function writeCredentials(cwd, stored) {
|
|
|
2155
2618
|
return { replaced };
|
|
2156
2619
|
}
|
|
2157
2620
|
|
|
2621
|
+
// src/detect.ts
|
|
2622
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2623
|
+
import { join as join2 } from "node:path";
|
|
2624
|
+
var CODE_SYNTAX_KEY = {
|
|
2625
|
+
web: "WEB",
|
|
2626
|
+
ios: "iOS",
|
|
2627
|
+
android: "ANDROID",
|
|
2628
|
+
flutter: null
|
|
2629
|
+
};
|
|
2630
|
+
var PLATFORMS = ["web", "ios", "android", "flutter"];
|
|
2631
|
+
var AGENT_HOSTS = ["claude", "cursor", "copilot", "windsurf", "gemini", "agents-md"];
|
|
2632
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
2633
|
+
function readPackageJson(cwd) {
|
|
2634
|
+
const path = join2(cwd, "package.json");
|
|
2635
|
+
if (!existsSync2(path)) return null;
|
|
2636
|
+
let parsed;
|
|
2637
|
+
try {
|
|
2638
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
2639
|
+
} catch {
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2643
|
+
const record = parsed;
|
|
2644
|
+
const deps = {};
|
|
2645
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
2646
|
+
const block = record[field];
|
|
2647
|
+
if (typeof block !== "object" || block === null) continue;
|
|
2648
|
+
for (const [name, range] of Object.entries(block)) {
|
|
2649
|
+
if (typeof range === "string") deps[name] = range;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return { deps };
|
|
2653
|
+
}
|
|
2654
|
+
function majorOf(range) {
|
|
2655
|
+
const m = /^[\^~>=<\s]*v?(\d+)/.exec(range.trim());
|
|
2656
|
+
return m ? Number(m[1]) : null;
|
|
2657
|
+
}
|
|
2658
|
+
var DEP_SIGNALS = [
|
|
2659
|
+
{ dep: "react", platform: "web", framework: "react" },
|
|
2660
|
+
{ dep: "next", platform: "web", framework: "next" },
|
|
2661
|
+
{ dep: "vue", platform: "web", framework: "vue" },
|
|
2662
|
+
{ dep: "nuxt", platform: "web", framework: "nuxt" },
|
|
2663
|
+
{ dep: "svelte", platform: "web", framework: "svelte" },
|
|
2664
|
+
{ dep: "@sveltejs/kit", platform: "web", framework: "sveltekit" },
|
|
2665
|
+
{ dep: "@angular/core", platform: "web", framework: "angular" },
|
|
2666
|
+
{ dep: "solid-js", platform: "web", framework: "solid" },
|
|
2667
|
+
{ dep: "lit", platform: "web", framework: "lit" },
|
|
2668
|
+
{ dep: "astro", platform: "web", framework: "astro" },
|
|
2669
|
+
{ dep: "react-native", platform: "ios", framework: "react-native" },
|
|
2670
|
+
{ dep: "expo", platform: "ios", framework: "expo" },
|
|
2671
|
+
{ dep: "tailwindcss", platform: "web", tokenTool: "tailwind" },
|
|
2672
|
+
{ dep: "styled-components", platform: "web", framework: "styled-components" },
|
|
2673
|
+
{ dep: "@emotion/react", platform: "web", framework: "emotion" },
|
|
2674
|
+
{ dep: "sass", platform: "web", framework: "sass" },
|
|
2675
|
+
{ dep: "@vanilla-extract/css", platform: "web", framework: "vanilla-extract" },
|
|
2676
|
+
{ dep: "@stitches/react", platform: "web", framework: "stitches" },
|
|
2677
|
+
{ dep: "@pandacss/dev", platform: "web", framework: "panda" },
|
|
2678
|
+
{ dep: "style-dictionary", tokenTool: "style-dictionary" },
|
|
2679
|
+
{ dep: "@tokens-studio/sd-transforms", tokenTool: "tokens-studio" },
|
|
2680
|
+
{ dep: "typescript", language: "typescript" }
|
|
2681
|
+
];
|
|
2682
|
+
var FILE_SIGNALS = [
|
|
2683
|
+
{ test: (n) => n === "Package.swift", signal: "Swift package", platform: "ios", language: "swift" },
|
|
2684
|
+
{ test: (n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace"), signal: "Xcode project", platform: "ios", language: "swift" },
|
|
2685
|
+
{ test: (n) => n === "Podfile", signal: "CocoaPods", platform: "ios" },
|
|
2686
|
+
{ test: (n) => /^build\.gradle(\.kts)?$/.test(n) || /^settings\.gradle(\.kts)?$/.test(n), signal: "Gradle build", platform: "android", language: "kotlin" },
|
|
2687
|
+
{ test: (n) => n === "AndroidManifest.xml", signal: "Android manifest", platform: "android" },
|
|
2688
|
+
{ test: (n) => n === "pubspec.yaml", signal: "Flutter or Dart package", platform: "flutter", language: "dart" },
|
|
2689
|
+
{ test: (n) => n === "tsconfig.json", signal: "TypeScript config", language: "typescript" },
|
|
2690
|
+
{ test: (n) => n === "package.json", signal: "npm package", language: "javascript" },
|
|
2691
|
+
{ test: (n) => n === "deno.json" || n === "deno.jsonc", signal: "Deno config", language: "typescript" },
|
|
2692
|
+
{ test: (n) => n === "Cargo.toml", signal: "Cargo manifest", language: "rust" },
|
|
2693
|
+
{ test: (n) => n === "go.mod", signal: "Go module", language: "go" },
|
|
2694
|
+
{ test: (n) => n === "pyproject.toml" || n === "requirements.txt", signal: "Python project", language: "python" },
|
|
2695
|
+
{ test: (n) => n === "Gemfile", signal: "Ruby bundle", language: "ruby" },
|
|
2696
|
+
{ test: (n) => n === "composer.json", signal: "Composer package", language: "php" },
|
|
2697
|
+
{ test: (n) => n.endsWith(".csproj") || n.endsWith(".sln"), signal: ".NET project", language: "csharp" },
|
|
2698
|
+
{ test: (n) => n === "pom.xml", signal: "Maven build", language: "java" },
|
|
2699
|
+
{ test: (n) => /^tailwind\.config\.(js|cjs|mjs|ts)$/.test(n), signal: "Tailwind config", platform: "web", tokenTool: "tailwind" },
|
|
2700
|
+
{ test: (n) => /^(style-dictionary\.config|sd\.config)\.(js|cjs|mjs|ts|json)$/.test(n), signal: "Style Dictionary config", tokenTool: "style-dictionary" },
|
|
2701
|
+
{ test: (n) => n === "index.html" || n === "vite.config.ts" || n === "vite.config.js", signal: "web entry", platform: "web" },
|
|
2702
|
+
{ test: (n) => n === "CLAUDE.md" || n === ".claude", signal: "Claude Code", agent: "claude" },
|
|
2703
|
+
{ test: (n) => n === ".cursor" || n === ".cursorrules", signal: "Cursor", agent: "cursor" },
|
|
2704
|
+
{ test: (n) => n === ".windsurf" || n === ".windsurfrules", signal: "Windsurf", agent: "windsurf" },
|
|
2705
|
+
{ test: (n) => n === "GEMINI.md", signal: "Gemini CLI", agent: "gemini" },
|
|
2706
|
+
{ test: (n) => n === "AGENTS.md", signal: "AGENTS.md", agent: "agents-md" }
|
|
2707
|
+
];
|
|
2708
|
+
function detectRepo(cwd) {
|
|
2709
|
+
const platforms = [];
|
|
2710
|
+
const languages = [];
|
|
2711
|
+
const frameworks = [];
|
|
2712
|
+
const tokenTools = [];
|
|
2713
|
+
const agents = [];
|
|
2714
|
+
const evidence = [];
|
|
2715
|
+
let styleDictionaryMajor = null;
|
|
2716
|
+
let names = [];
|
|
2717
|
+
try {
|
|
2718
|
+
names = readdirSync(cwd).sort();
|
|
2719
|
+
} catch {
|
|
2720
|
+
names = [];
|
|
2721
|
+
}
|
|
2722
|
+
for (const name of names) {
|
|
2723
|
+
for (const rule of FILE_SIGNALS) {
|
|
2724
|
+
if (!rule.test(name)) continue;
|
|
2725
|
+
evidence.push({ signal: rule.signal, file: name });
|
|
2726
|
+
if (rule.platform) platforms.push(rule.platform);
|
|
2727
|
+
if (rule.language) languages.push(rule.language);
|
|
2728
|
+
if (rule.framework) frameworks.push(rule.framework);
|
|
2729
|
+
if (rule.tokenTool) tokenTools.push(rule.tokenTool);
|
|
2730
|
+
if (rule.agent) agents.push(rule.agent);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
if (existsSync2(join2(cwd, ".github", "copilot-instructions.md")) || existsSync2(join2(cwd, ".github", "instructions"))) {
|
|
2734
|
+
evidence.push({ signal: "GitHub Copilot", file: ".github/copilot-instructions.md" });
|
|
2735
|
+
agents.push("copilot");
|
|
2736
|
+
}
|
|
2737
|
+
const pkg = readPackageJson(cwd);
|
|
2738
|
+
if (pkg) {
|
|
2739
|
+
for (const rule of DEP_SIGNALS) {
|
|
2740
|
+
const range = pkg.deps[rule.dep];
|
|
2741
|
+
if (range === void 0) continue;
|
|
2742
|
+
evidence.push({ signal: `${rule.dep} dependency`, file: "package.json" });
|
|
2743
|
+
if (rule.platform) platforms.push(rule.platform);
|
|
2744
|
+
if (rule.framework) frameworks.push(rule.framework);
|
|
2745
|
+
if (rule.tokenTool) tokenTools.push(rule.tokenTool);
|
|
2746
|
+
if (rule.language) languages.push(rule.language);
|
|
2747
|
+
if (rule.dep === "style-dictionary") styleDictionaryMajor = majorOf(range);
|
|
2748
|
+
}
|
|
2749
|
+
if (pkg.deps["react-native"] !== void 0 || pkg.deps.expo !== void 0) platforms.push("android");
|
|
2750
|
+
}
|
|
2751
|
+
const order = (p) => PLATFORMS.indexOf(p);
|
|
2752
|
+
const hostOrder = (a) => AGENT_HOSTS.indexOf(a);
|
|
2753
|
+
return {
|
|
2754
|
+
platforms: uniq(platforms).sort((a, b) => order(a) - order(b)),
|
|
2755
|
+
languages: uniq(languages).sort(),
|
|
2756
|
+
frameworks: uniq(frameworks).sort(),
|
|
2757
|
+
tokenTools: uniq(tokenTools).sort(),
|
|
2758
|
+
agents: uniq(agents).sort((a, b) => hostOrder(a) - hostOrder(b)),
|
|
2759
|
+
styleDictionaryMajor,
|
|
2760
|
+
evidence
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
function isPlatform(value) {
|
|
2764
|
+
return PLATFORMS.includes(value);
|
|
2765
|
+
}
|
|
2766
|
+
function isAgentHost(value) {
|
|
2767
|
+
return AGENT_HOSTS.includes(value);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/outputs.ts
|
|
2771
|
+
import {
|
|
2772
|
+
existsSync as existsSync3,
|
|
2773
|
+
mkdirSync,
|
|
2774
|
+
readFileSync as readFileSync3,
|
|
2775
|
+
renameSync,
|
|
2776
|
+
rmSync,
|
|
2777
|
+
writeFileSync as writeFileSync2
|
|
2778
|
+
} from "node:fs";
|
|
2779
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2780
|
+
var FORMATS = [
|
|
2781
|
+
{ platform: "web", format: "css", defaultPath: "spec-layer/tokens.css", defaultCase: "kebab", headerPrefix: CSS_HEADER_PREFIX }
|
|
2782
|
+
];
|
|
2783
|
+
var knownFormats = () => FORMATS.map((f) => `${f.platform}/${f.format}`).join(", ");
|
|
2784
|
+
var specOf = (platform, format) => FORMATS.find((f) => f.platform === platform && f.format === format) ?? null;
|
|
2785
|
+
function defaultOutputs(platforms) {
|
|
2786
|
+
return FORMATS.filter((f) => f.defaultPath !== null && platforms.includes(f.platform)).map((f) => ({ platform: f.platform, format: f.format, path: f.defaultPath, case: f.defaultCase }));
|
|
2787
|
+
}
|
|
2788
|
+
function withDefaults(existing, platforms) {
|
|
2789
|
+
const covered = new Set(existing.map((o) => o.platform));
|
|
2790
|
+
return [...existing, ...defaultOutputs(platforms.filter((p) => !covered.has(p)))];
|
|
2791
|
+
}
|
|
2792
|
+
var outputId = (o) => `${o.platform}-${o.format}`;
|
|
2793
|
+
function parseOutput(value, index) {
|
|
2794
|
+
const at = `speclayer.json outputs[${index}]`;
|
|
2795
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${at} must be an object.`);
|
|
2796
|
+
const r = value;
|
|
2797
|
+
if (typeof r.platform !== "string" || typeof r.format !== "string") {
|
|
2798
|
+
throw new Error(`${at} needs "platform" and "format". Known: ${knownFormats()}.`);
|
|
2799
|
+
}
|
|
2800
|
+
const spec = specOf(r.platform, r.format);
|
|
2801
|
+
if (!spec) throw new Error(`${at}: unknown platform/format "${r.platform}/${r.format}". Known: ${knownFormats()}.`);
|
|
2802
|
+
const str = (key) => {
|
|
2803
|
+
if (r[key] !== void 0 && typeof r[key] !== "string") throw new Error(`${at} "${key}" must be a string.`);
|
|
2804
|
+
return r[key];
|
|
2805
|
+
};
|
|
2806
|
+
const path = str("path") ?? spec.defaultPath;
|
|
2807
|
+
if (path === null) throw new Error(`${at} needs "path": ${spec.platform} has no default location.`);
|
|
2808
|
+
const nameCase = str("case");
|
|
2809
|
+
if (nameCase !== void 0 && !NAME_CASES.includes(nameCase)) {
|
|
2810
|
+
throw new Error(`${at} "case" takes ${NAME_CASES.join(", ")}.`);
|
|
2811
|
+
}
|
|
2812
|
+
const root = str("root");
|
|
2813
|
+
const modeSelector = str("modeSelector");
|
|
2814
|
+
let modes;
|
|
2815
|
+
if (r.modes !== void 0) {
|
|
2816
|
+
const m = r.modes;
|
|
2817
|
+
if (typeof m !== "object" || m === null || Array.isArray(m) || !Object.values(m).every((v) => typeof v === "string")) {
|
|
2818
|
+
throw new Error(`${at} "modes" must map collection names to selector strings.`);
|
|
2819
|
+
}
|
|
2820
|
+
modes = m;
|
|
2821
|
+
}
|
|
2822
|
+
return {
|
|
2823
|
+
platform: spec.platform,
|
|
2824
|
+
format: spec.format,
|
|
2825
|
+
path,
|
|
2826
|
+
case: nameCase ?? spec.defaultCase,
|
|
2827
|
+
...root !== void 0 ? { root } : {},
|
|
2828
|
+
...modeSelector !== void 0 ? { modeSelector } : {},
|
|
2829
|
+
...modes ? { modes } : {}
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function renderOutput(exp, o, header) {
|
|
2833
|
+
switch (o.format) {
|
|
2834
|
+
case "css":
|
|
2835
|
+
return cssOutput(exp, { ...header, platform: o.platform, format: o.format }, {
|
|
2836
|
+
case: o.case,
|
|
2837
|
+
...o.root !== void 0 ? { root: o.root } : {},
|
|
2838
|
+
...o.modeSelector !== void 0 ? { modeSelector: o.modeSelector } : {},
|
|
2839
|
+
...o.modes ? { modes: o.modes } : {}
|
|
2840
|
+
});
|
|
2841
|
+
default: {
|
|
2842
|
+
const exhaustive = o.format;
|
|
2843
|
+
return exhaustive;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
var inside = (parent, child) => {
|
|
2848
|
+
const rel = relative(parent, child);
|
|
2849
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
2850
|
+
};
|
|
2851
|
+
function outputPathProblem(cwd, outDir, o) {
|
|
2852
|
+
const root = resolve(cwd);
|
|
2853
|
+
const abs = resolve(cwd, o.path);
|
|
2854
|
+
if (!inside(root, abs) || abs === root) return `${o.path} is outside this directory. Choose a path inside the repository.`;
|
|
2855
|
+
if (inside(resolve(cwd, outDir), abs)) {
|
|
2856
|
+
return `${o.path} is inside ${outDir}, which pull replaces wholesale. Choose a path outside it.`;
|
|
2857
|
+
}
|
|
2858
|
+
if (existsSync3(abs)) {
|
|
2859
|
+
const prefix = specOf(o.platform, o.format)?.headerPrefix ?? CSS_HEADER_PREFIX;
|
|
2860
|
+
let head;
|
|
2861
|
+
try {
|
|
2862
|
+
head = readFileSync3(abs, "utf8").slice(0, prefix.length);
|
|
2863
|
+
} catch {
|
|
2864
|
+
return `${o.path} exists and could not be read.`;
|
|
2865
|
+
}
|
|
2866
|
+
if (head !== prefix) return `${o.path} exists and was not written by spec-layer. Choose another path or remove the file.`;
|
|
2867
|
+
}
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2870
|
+
function writeOutputFile(cwd, o, text) {
|
|
2871
|
+
const abs = resolve(cwd, o.path);
|
|
2872
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
2873
|
+
const partial = `${abs}.partial`;
|
|
2874
|
+
writeFileSync2(partial, text);
|
|
2875
|
+
try {
|
|
2876
|
+
renameSync(partial, abs);
|
|
2877
|
+
} catch (err) {
|
|
2878
|
+
rmSync(partial, { force: true });
|
|
2879
|
+
throw err;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2158
2883
|
// src/config.ts
|
|
2159
2884
|
var DEFAULT_API = "https://api.spec-layer.com";
|
|
2160
2885
|
var DEFAULT_OUT_DIR = ".speclayer";
|
|
@@ -2164,12 +2889,12 @@ function parseInclude(value) {
|
|
|
2164
2889
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidConfig();
|
|
2165
2890
|
const record = value;
|
|
2166
2891
|
if (record.foundation !== void 0 && typeof record.foundation !== "boolean") throw invalidConfig();
|
|
2167
|
-
if (record.components !== void 0 && !(Array.isArray(record.components) && record.components.every((c) => typeof c === "string"))) {
|
|
2892
|
+
if (record.components !== void 0 && record.components !== null && !(Array.isArray(record.components) && record.components.every((c) => typeof c === "string"))) {
|
|
2168
2893
|
throw invalidConfig();
|
|
2169
2894
|
}
|
|
2170
2895
|
return {
|
|
2171
2896
|
foundation: record.foundation === void 0 ? true : record.foundation,
|
|
2172
|
-
components: record.components
|
|
2897
|
+
components: record.components ?? null
|
|
2173
2898
|
};
|
|
2174
2899
|
}
|
|
2175
2900
|
function parseDtcg(value) {
|
|
@@ -2189,12 +2914,31 @@ function parseDtcg(value) {
|
|
|
2189
2914
|
}
|
|
2190
2915
|
return out;
|
|
2191
2916
|
}
|
|
2917
|
+
function parsePlatforms(value) {
|
|
2918
|
+
if (!Array.isArray(value) || !value.every((p) => typeof p === "string" && isPlatform(p))) {
|
|
2919
|
+
throw new Error(`speclayer.json "platforms" must be an array of ${PLATFORMS.join(", ")}.`);
|
|
2920
|
+
}
|
|
2921
|
+
return [...new Set(value)];
|
|
2922
|
+
}
|
|
2923
|
+
function parseOutputs(value) {
|
|
2924
|
+
if (!Array.isArray(value)) throw new Error('speclayer.json "outputs" must be an array.');
|
|
2925
|
+
const outputs = value.map((v, i) => parseOutput(v, i));
|
|
2926
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2927
|
+
for (const output of outputs) {
|
|
2928
|
+
const key = `${output.platform}/${output.format}`;
|
|
2929
|
+
if (seen.has(key)) {
|
|
2930
|
+
throw new Error(`speclayer.json "outputs" lists ${output.platform}/${output.format} more than once. Keep one entry per platform and format.`);
|
|
2931
|
+
}
|
|
2932
|
+
seen.add(key);
|
|
2933
|
+
}
|
|
2934
|
+
return outputs;
|
|
2935
|
+
}
|
|
2192
2936
|
function readConfig(cwd) {
|
|
2193
|
-
const path =
|
|
2194
|
-
if (!
|
|
2937
|
+
const path = join3(cwd, CONFIG_NAME);
|
|
2938
|
+
if (!existsSync4(path)) return null;
|
|
2195
2939
|
let parsed;
|
|
2196
2940
|
try {
|
|
2197
|
-
parsed = JSON.parse(
|
|
2941
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2198
2942
|
} catch {
|
|
2199
2943
|
throw invalidConfig();
|
|
2200
2944
|
}
|
|
@@ -2204,7 +2948,9 @@ function readConfig(cwd) {
|
|
|
2204
2948
|
...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
|
|
2205
2949
|
...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
|
|
2206
2950
|
...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
|
|
2207
|
-
...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {}
|
|
2951
|
+
...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {},
|
|
2952
|
+
...record.platforms !== void 0 ? { platforms: parsePlatforms(record.platforms) } : {},
|
|
2953
|
+
...record.outputs !== void 0 ? { outputs: parseOutputs(record.outputs) } : {}
|
|
2208
2954
|
};
|
|
2209
2955
|
}
|
|
2210
2956
|
function writeConfig(cwd, config) {
|
|
@@ -2212,15 +2958,17 @@ function writeConfig(cwd, config) {
|
|
|
2212
2958
|
libraryId: config.libraryId,
|
|
2213
2959
|
outDir: config.outDir,
|
|
2214
2960
|
...config.include ? { include: config.include } : {},
|
|
2215
|
-
...config.dtcg ? { dtcg: config.dtcg } : {}
|
|
2961
|
+
...config.dtcg ? { dtcg: config.dtcg } : {},
|
|
2962
|
+
...config.platforms && config.platforms.length > 0 ? { platforms: config.platforms } : {},
|
|
2963
|
+
...config.outputs ? { outputs: config.outputs } : {}
|
|
2216
2964
|
};
|
|
2217
|
-
|
|
2965
|
+
writeFileSync3(join3(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
|
|
2218
2966
|
`);
|
|
2219
2967
|
}
|
|
2220
2968
|
function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
2221
2969
|
const config = readConfig(cwd);
|
|
2222
2970
|
const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
|
|
2223
|
-
const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId(
|
|
2971
|
+
const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId(join3(cwd, outDir));
|
|
2224
2972
|
const supplied = flags.key || env.SPEC_LAYER_KEY || null;
|
|
2225
2973
|
let storedKey = null;
|
|
2226
2974
|
let storedKeyFor;
|
|
@@ -2239,6 +2987,8 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
|
2239
2987
|
key: supplied ?? storedKey,
|
|
2240
2988
|
...config?.include ? { include: config.include } : {},
|
|
2241
2989
|
...config?.dtcg ? { dtcg: config.dtcg } : {},
|
|
2990
|
+
...config?.platforms ? { platforms: config.platforms } : {},
|
|
2991
|
+
...config?.outputs ? { outputs: config.outputs } : {},
|
|
2242
2992
|
...storedKeyFor ? { storedKeyFor } : {}
|
|
2243
2993
|
};
|
|
2244
2994
|
}
|
|
@@ -2277,8 +3027,8 @@ async function fetchBundle(opts) {
|
|
|
2277
3027
|
}
|
|
2278
3028
|
|
|
2279
3029
|
// src/files.ts
|
|
2280
|
-
import { mkdirSync, writeFileSync as
|
|
2281
|
-
import { join as
|
|
3030
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, rmSync as rmSync2, renameSync as renameSync2, existsSync as existsSync5 } from "node:fs";
|
|
3031
|
+
import { join as join4, dirname as dirname2, relative as relative2, resolve as resolve2, isAbsolute as isAbsolute2 } from "node:path";
|
|
2282
3032
|
|
|
2283
3033
|
// src/selection.ts
|
|
2284
3034
|
var DEFAULT_SELECTION = { foundation: true, components: null };
|
|
@@ -2317,19 +3067,27 @@ function slugify(name) {
|
|
|
2317
3067
|
return slug2 || "component";
|
|
2318
3068
|
}
|
|
2319
3069
|
function readManifest(outDir) {
|
|
2320
|
-
const path =
|
|
2321
|
-
if (!
|
|
3070
|
+
const path = join4(outDir, "manifest.json");
|
|
3071
|
+
if (!existsSync5(path)) return null;
|
|
2322
3072
|
try {
|
|
2323
|
-
|
|
3073
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3074
|
+
parsed.artifacts = parsed.artifacts.map((artifact) => {
|
|
3075
|
+
const { aiPath, ...rest } = artifact;
|
|
3076
|
+
return {
|
|
3077
|
+
...rest,
|
|
3078
|
+
path: rest.path ?? aiPath ?? null
|
|
3079
|
+
};
|
|
3080
|
+
});
|
|
3081
|
+
return parsed;
|
|
2324
3082
|
} catch {
|
|
2325
3083
|
return null;
|
|
2326
3084
|
}
|
|
2327
3085
|
}
|
|
2328
3086
|
function readLocalBundle(outDir) {
|
|
2329
|
-
const path =
|
|
2330
|
-
if (!
|
|
3087
|
+
const path = join4(outDir, "bundle.json");
|
|
3088
|
+
if (!existsSync5(path)) return null;
|
|
2331
3089
|
try {
|
|
2332
|
-
return parseBundle(
|
|
3090
|
+
return parseBundle(readFileSync5(path, "utf8"));
|
|
2333
3091
|
} catch {
|
|
2334
3092
|
throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
|
|
2335
3093
|
}
|
|
@@ -2356,11 +3114,11 @@ function componentSlugs(bundle) {
|
|
|
2356
3114
|
});
|
|
2357
3115
|
}
|
|
2358
3116
|
function assertReplaceable(outDir, cwd) {
|
|
2359
|
-
const rel =
|
|
2360
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
3117
|
+
const rel = relative2(resolve2(cwd), resolve2(outDir));
|
|
3118
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
2361
3119
|
throw new Error('The output directory must sit inside the current directory, not be "." or a parent of it.');
|
|
2362
3120
|
}
|
|
2363
|
-
if (
|
|
3121
|
+
if (existsSync5(outDir) && !existsSync5(join4(outDir, "manifest.json")) && readdirSync2(outDir).length > 0) {
|
|
2364
3122
|
throw new Error(`${outDir} exists and was not written by spec-layer pull. Choose an empty or new directory.`);
|
|
2365
3123
|
}
|
|
2366
3124
|
}
|
|
@@ -2369,44 +3127,62 @@ function writeBundleFiles(opts) {
|
|
|
2369
3127
|
const selection = opts.selection ?? DEFAULT_SELECTION;
|
|
2370
3128
|
const selected = selectComponents(opts.bundle, selection);
|
|
2371
3129
|
const slugs = componentSlugs(opts.bundle);
|
|
3130
|
+
const outputs = opts.outputs ?? [];
|
|
3131
|
+
const willWriteFoundation = Boolean(opts.bundle.foundation) && selection.foundation;
|
|
3132
|
+
if (willWriteFoundation) {
|
|
3133
|
+
for (const o of outputs) {
|
|
3134
|
+
const problem = outputPathProblem(opts.cwd, opts.outDir, o);
|
|
3135
|
+
if (problem) throw new Error(problem);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
2372
3138
|
const staging = `${opts.outDir}.partial`;
|
|
2373
|
-
|
|
3139
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
2374
3140
|
const written = [];
|
|
3141
|
+
const deliverables = [];
|
|
3142
|
+
const json = (v) => `${JSON.stringify(v, null, 2)}
|
|
3143
|
+
`;
|
|
2375
3144
|
const put = (rel, content) => {
|
|
2376
|
-
const path =
|
|
2377
|
-
|
|
2378
|
-
|
|
3145
|
+
const path = join4(staging, rel);
|
|
3146
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
3147
|
+
writeFileSync4(path, content);
|
|
2379
3148
|
written.push(rel);
|
|
2380
3149
|
};
|
|
2381
3150
|
try {
|
|
2382
3151
|
put("bundle.json", opts.raw);
|
|
2383
3152
|
const artifacts = [];
|
|
2384
3153
|
if (opts.bundle.foundation) {
|
|
2385
|
-
let
|
|
3154
|
+
let path = null;
|
|
2386
3155
|
if (selection.foundation) {
|
|
2387
3156
|
const artifact = opts.bundle.foundation.artifact;
|
|
2388
3157
|
if (validateLevel1(artifact).some((d) => d.severity === "error")) {
|
|
2389
3158
|
throw new Error("The published Foundation context did not pass schema validation. Republish from the plugin, then pull again.");
|
|
2390
3159
|
}
|
|
2391
|
-
const
|
|
2392
|
-
for (const [name, text] of Object.entries(
|
|
2393
|
-
|
|
3160
|
+
const exp = foundationDtcg(artifact, opts.dtcg ?? {});
|
|
3161
|
+
for (const [name, text] of Object.entries(dtcgExportFiles(exp))) put(`tokens/${name}`, text);
|
|
3162
|
+
path = "tokens/resolver.json";
|
|
3163
|
+
const header = { libraryId: opts.libraryId, contentHash: opts.bundle.foundation.artifact.spec_layer.export.content_hash };
|
|
3164
|
+
for (const output of outputs) {
|
|
3165
|
+
const rendered = renderOutput(exp, output, header);
|
|
3166
|
+
put(`outputs/${outputId(output)}.map.json`, json(rendered.map));
|
|
3167
|
+
put(`outputs/${outputId(output)}.report.json`, json(rendered.report));
|
|
3168
|
+
deliverables.push({ output, text: rendered.text });
|
|
3169
|
+
}
|
|
2394
3170
|
}
|
|
2395
3171
|
artifacts.push({
|
|
2396
3172
|
kind: "foundation",
|
|
2397
3173
|
name: "foundation",
|
|
2398
3174
|
contentHash: opts.bundle.foundation.artifact.spec_layer.export.content_hash,
|
|
2399
|
-
|
|
3175
|
+
path
|
|
2400
3176
|
});
|
|
2401
3177
|
}
|
|
2402
3178
|
opts.bundle.components.forEach((component, i) => {
|
|
2403
|
-
const
|
|
2404
|
-
if (
|
|
3179
|
+
const path = selected[i] ? `components/${slugs[i]}.yaml` : null;
|
|
3180
|
+
if (path) put(path, component.ai);
|
|
2405
3181
|
artifacts.push({
|
|
2406
3182
|
kind: "component",
|
|
2407
3183
|
name: component.name,
|
|
2408
3184
|
contentHash: component.artifact.spec_layer.export.content_hash,
|
|
2409
|
-
|
|
3185
|
+
path
|
|
2410
3186
|
});
|
|
2411
3187
|
});
|
|
2412
3188
|
const manifest = {
|
|
@@ -2417,23 +3193,29 @@ function writeBundleFiles(opts) {
|
|
|
2417
3193
|
extractorVersion: opts.bundle.extractorVersion,
|
|
2418
3194
|
selection,
|
|
2419
3195
|
artifacts,
|
|
2420
|
-
...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {}
|
|
3196
|
+
...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {},
|
|
3197
|
+
...opts.platforms && opts.platforms.length > 0 ? { platforms: opts.platforms } : {},
|
|
3198
|
+
...opts.outputs ? { outputs: opts.outputs } : {}
|
|
2421
3199
|
};
|
|
2422
|
-
put("manifest.json",
|
|
2423
|
-
`);
|
|
3200
|
+
put("manifest.json", json(manifest));
|
|
2424
3201
|
} catch (err) {
|
|
2425
|
-
|
|
3202
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
2426
3203
|
throw err;
|
|
2427
3204
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
3205
|
+
rmSync2(opts.outDir, { recursive: true, force: true });
|
|
3206
|
+
renameSync2(staging, opts.outDir);
|
|
3207
|
+
const outputPaths = [];
|
|
3208
|
+
for (const d of deliverables) {
|
|
3209
|
+
writeOutputFile(opts.cwd, d.output, d.text);
|
|
3210
|
+
outputPaths.push(d.output.path);
|
|
3211
|
+
}
|
|
3212
|
+
return { written, outputs: outputPaths };
|
|
2431
3213
|
}
|
|
2432
3214
|
|
|
2433
3215
|
// src/gitignore.ts
|
|
2434
|
-
import { readFileSync as
|
|
3216
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "node:fs";
|
|
2435
3217
|
import { spawnSync } from "node:child_process";
|
|
2436
|
-
import { join as
|
|
3218
|
+
import { join as join5, dirname as dirname3, resolve as resolve3 } from "node:path";
|
|
2437
3219
|
var COMMENT = "# Spec Layer pull key, not for committing";
|
|
2438
3220
|
function git(cwd, args) {
|
|
2439
3221
|
const res = spawnSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
@@ -2441,10 +3223,10 @@ function git(cwd, args) {
|
|
|
2441
3223
|
return { ranGit: true, status: res.status, stdout: res.stdout ?? "" };
|
|
2442
3224
|
}
|
|
2443
3225
|
function insideWorkTreeWithoutGit(cwd) {
|
|
2444
|
-
let dir =
|
|
3226
|
+
let dir = resolve3(cwd);
|
|
2445
3227
|
for (; ; ) {
|
|
2446
|
-
if (
|
|
2447
|
-
const parent =
|
|
3228
|
+
if (existsSync6(join5(dir, ".git"))) return true;
|
|
3229
|
+
const parent = dirname3(dir);
|
|
2448
3230
|
if (parent === dir) return false;
|
|
2449
3231
|
dir = parent;
|
|
2450
3232
|
}
|
|
@@ -2460,18 +3242,18 @@ function ensureIgnored(cwd, fileName) {
|
|
|
2460
3242
|
if (inWorkTree.status !== 0 || inWorkTree.stdout.trim() !== "true") return { kind: "not-a-repo" };
|
|
2461
3243
|
const checkIgnore = git(cwd, ["check-ignore", "-q", fileName]);
|
|
2462
3244
|
if (checkIgnore.ranGit && checkIgnore.status === 0) return { kind: "already" };
|
|
2463
|
-
const path =
|
|
2464
|
-
const existed =
|
|
3245
|
+
const path = join5(cwd, ".gitignore");
|
|
3246
|
+
const existed = existsSync6(path);
|
|
2465
3247
|
try {
|
|
2466
3248
|
if (!existed) {
|
|
2467
|
-
|
|
3249
|
+
writeFileSync5(path, `${COMMENT}
|
|
2468
3250
|
${fileName}
|
|
2469
3251
|
`);
|
|
2470
3252
|
} else {
|
|
2471
|
-
const body =
|
|
3253
|
+
const body = readFileSync6(path, "utf8");
|
|
2472
3254
|
if (!hasEntryLine(body, fileName)) {
|
|
2473
3255
|
const lead = body.length === 0 || body.endsWith("\n") ? "" : "\n";
|
|
2474
|
-
|
|
3256
|
+
writeFileSync5(path, `${body}${lead}${COMMENT}
|
|
2475
3257
|
${fileName}
|
|
2476
3258
|
`);
|
|
2477
3259
|
}
|
|
@@ -2484,158 +3266,9 @@ ${fileName}
|
|
|
2484
3266
|
return existed ? { kind: "added" } : { kind: "created" };
|
|
2485
3267
|
}
|
|
2486
3268
|
|
|
2487
|
-
// src/detect.ts
|
|
2488
|
-
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
2489
|
-
import { join as join5 } from "node:path";
|
|
2490
|
-
var CODE_SYNTAX_KEY = {
|
|
2491
|
-
web: "WEB",
|
|
2492
|
-
ios: "iOS",
|
|
2493
|
-
android: "ANDROID",
|
|
2494
|
-
flutter: null
|
|
2495
|
-
};
|
|
2496
|
-
var PLATFORMS = ["web", "ios", "android", "flutter"];
|
|
2497
|
-
var AGENT_HOSTS = ["claude", "cursor", "copilot", "windsurf", "gemini", "agents-md"];
|
|
2498
|
-
var uniq = (xs) => [...new Set(xs)];
|
|
2499
|
-
function readPackageJson(cwd) {
|
|
2500
|
-
const path = join5(cwd, "package.json");
|
|
2501
|
-
if (!existsSync5(path)) return null;
|
|
2502
|
-
let parsed;
|
|
2503
|
-
try {
|
|
2504
|
-
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
2505
|
-
} catch {
|
|
2506
|
-
return null;
|
|
2507
|
-
}
|
|
2508
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2509
|
-
const record = parsed;
|
|
2510
|
-
const deps = {};
|
|
2511
|
-
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
2512
|
-
const block = record[field];
|
|
2513
|
-
if (typeof block !== "object" || block === null) continue;
|
|
2514
|
-
for (const [name, range] of Object.entries(block)) {
|
|
2515
|
-
if (typeof range === "string") deps[name] = range;
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
return { deps };
|
|
2519
|
-
}
|
|
2520
|
-
function majorOf(range) {
|
|
2521
|
-
const m = /^[\^~>=<\s]*v?(\d+)/.exec(range.trim());
|
|
2522
|
-
return m ? Number(m[1]) : null;
|
|
2523
|
-
}
|
|
2524
|
-
var DEP_SIGNALS = [
|
|
2525
|
-
{ dep: "react", platform: "web", framework: "react" },
|
|
2526
|
-
{ dep: "next", platform: "web", framework: "next" },
|
|
2527
|
-
{ dep: "vue", platform: "web", framework: "vue" },
|
|
2528
|
-
{ dep: "nuxt", platform: "web", framework: "nuxt" },
|
|
2529
|
-
{ dep: "svelte", platform: "web", framework: "svelte" },
|
|
2530
|
-
{ dep: "@sveltejs/kit", platform: "web", framework: "sveltekit" },
|
|
2531
|
-
{ dep: "@angular/core", platform: "web", framework: "angular" },
|
|
2532
|
-
{ dep: "solid-js", platform: "web", framework: "solid" },
|
|
2533
|
-
{ dep: "lit", platform: "web", framework: "lit" },
|
|
2534
|
-
{ dep: "astro", platform: "web", framework: "astro" },
|
|
2535
|
-
{ dep: "react-native", platform: "ios", framework: "react-native" },
|
|
2536
|
-
{ dep: "expo", platform: "ios", framework: "expo" },
|
|
2537
|
-
{ dep: "tailwindcss", platform: "web", tokenTool: "tailwind" },
|
|
2538
|
-
{ dep: "styled-components", platform: "web", framework: "styled-components" },
|
|
2539
|
-
{ dep: "@emotion/react", platform: "web", framework: "emotion" },
|
|
2540
|
-
{ dep: "sass", platform: "web", framework: "sass" },
|
|
2541
|
-
{ dep: "@vanilla-extract/css", platform: "web", framework: "vanilla-extract" },
|
|
2542
|
-
{ dep: "@stitches/react", platform: "web", framework: "stitches" },
|
|
2543
|
-
{ dep: "@pandacss/dev", platform: "web", framework: "panda" },
|
|
2544
|
-
{ dep: "style-dictionary", tokenTool: "style-dictionary" },
|
|
2545
|
-
{ dep: "@tokens-studio/sd-transforms", tokenTool: "tokens-studio" },
|
|
2546
|
-
{ dep: "typescript", language: "typescript" }
|
|
2547
|
-
];
|
|
2548
|
-
var FILE_SIGNALS = [
|
|
2549
|
-
{ test: (n) => n === "Package.swift", signal: "Swift package", platform: "ios", language: "swift" },
|
|
2550
|
-
{ test: (n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace"), signal: "Xcode project", platform: "ios", language: "swift" },
|
|
2551
|
-
{ test: (n) => n === "Podfile", signal: "CocoaPods", platform: "ios" },
|
|
2552
|
-
{ test: (n) => /^build\.gradle(\.kts)?$/.test(n) || /^settings\.gradle(\.kts)?$/.test(n), signal: "Gradle build", platform: "android", language: "kotlin" },
|
|
2553
|
-
{ test: (n) => n === "AndroidManifest.xml", signal: "Android manifest", platform: "android" },
|
|
2554
|
-
{ test: (n) => n === "pubspec.yaml", signal: "Flutter or Dart package", platform: "flutter", language: "dart" },
|
|
2555
|
-
{ test: (n) => n === "tsconfig.json", signal: "TypeScript config", language: "typescript" },
|
|
2556
|
-
{ test: (n) => n === "package.json", signal: "npm package", language: "javascript" },
|
|
2557
|
-
{ test: (n) => n === "deno.json" || n === "deno.jsonc", signal: "Deno config", language: "typescript" },
|
|
2558
|
-
{ test: (n) => n === "Cargo.toml", signal: "Cargo manifest", language: "rust" },
|
|
2559
|
-
{ test: (n) => n === "go.mod", signal: "Go module", language: "go" },
|
|
2560
|
-
{ test: (n) => n === "pyproject.toml" || n === "requirements.txt", signal: "Python project", language: "python" },
|
|
2561
|
-
{ test: (n) => n === "Gemfile", signal: "Ruby bundle", language: "ruby" },
|
|
2562
|
-
{ test: (n) => n === "composer.json", signal: "Composer package", language: "php" },
|
|
2563
|
-
{ test: (n) => n.endsWith(".csproj") || n.endsWith(".sln"), signal: ".NET project", language: "csharp" },
|
|
2564
|
-
{ test: (n) => n === "pom.xml", signal: "Maven build", language: "java" },
|
|
2565
|
-
{ test: (n) => /^tailwind\.config\.(js|cjs|mjs|ts)$/.test(n), signal: "Tailwind config", platform: "web", tokenTool: "tailwind" },
|
|
2566
|
-
{ test: (n) => /^(style-dictionary\.config|sd\.config)\.(js|cjs|mjs|ts|json)$/.test(n), signal: "Style Dictionary config", tokenTool: "style-dictionary" },
|
|
2567
|
-
{ test: (n) => n === "index.html" || n === "vite.config.ts" || n === "vite.config.js", signal: "web entry", platform: "web" },
|
|
2568
|
-
{ test: (n) => n === "CLAUDE.md" || n === ".claude", signal: "Claude Code", agent: "claude" },
|
|
2569
|
-
{ test: (n) => n === ".cursor" || n === ".cursorrules", signal: "Cursor", agent: "cursor" },
|
|
2570
|
-
{ test: (n) => n === ".windsurf" || n === ".windsurfrules", signal: "Windsurf", agent: "windsurf" },
|
|
2571
|
-
{ test: (n) => n === "GEMINI.md", signal: "Gemini CLI", agent: "gemini" },
|
|
2572
|
-
{ test: (n) => n === "AGENTS.md", signal: "AGENTS.md", agent: "agents-md" }
|
|
2573
|
-
];
|
|
2574
|
-
function detectRepo(cwd) {
|
|
2575
|
-
const platforms = [];
|
|
2576
|
-
const languages = [];
|
|
2577
|
-
const frameworks = [];
|
|
2578
|
-
const tokenTools = [];
|
|
2579
|
-
const agents = [];
|
|
2580
|
-
const evidence = [];
|
|
2581
|
-
let styleDictionaryMajor = null;
|
|
2582
|
-
let names = [];
|
|
2583
|
-
try {
|
|
2584
|
-
names = readdirSync2(cwd).sort();
|
|
2585
|
-
} catch {
|
|
2586
|
-
names = [];
|
|
2587
|
-
}
|
|
2588
|
-
for (const name of names) {
|
|
2589
|
-
for (const rule of FILE_SIGNALS) {
|
|
2590
|
-
if (!rule.test(name)) continue;
|
|
2591
|
-
evidence.push({ signal: rule.signal, file: name });
|
|
2592
|
-
if (rule.platform) platforms.push(rule.platform);
|
|
2593
|
-
if (rule.language) languages.push(rule.language);
|
|
2594
|
-
if (rule.framework) frameworks.push(rule.framework);
|
|
2595
|
-
if (rule.tokenTool) tokenTools.push(rule.tokenTool);
|
|
2596
|
-
if (rule.agent) agents.push(rule.agent);
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
if (existsSync5(join5(cwd, ".github", "copilot-instructions.md")) || existsSync5(join5(cwd, ".github", "instructions"))) {
|
|
2600
|
-
evidence.push({ signal: "GitHub Copilot", file: ".github/copilot-instructions.md" });
|
|
2601
|
-
agents.push("copilot");
|
|
2602
|
-
}
|
|
2603
|
-
const pkg = readPackageJson(cwd);
|
|
2604
|
-
if (pkg) {
|
|
2605
|
-
for (const rule of DEP_SIGNALS) {
|
|
2606
|
-
const range = pkg.deps[rule.dep];
|
|
2607
|
-
if (range === void 0) continue;
|
|
2608
|
-
evidence.push({ signal: `${rule.dep} dependency`, file: "package.json" });
|
|
2609
|
-
if (rule.platform) platforms.push(rule.platform);
|
|
2610
|
-
if (rule.framework) frameworks.push(rule.framework);
|
|
2611
|
-
if (rule.tokenTool) tokenTools.push(rule.tokenTool);
|
|
2612
|
-
if (rule.language) languages.push(rule.language);
|
|
2613
|
-
if (rule.dep === "style-dictionary") styleDictionaryMajor = majorOf(range);
|
|
2614
|
-
}
|
|
2615
|
-
if (pkg.deps["react-native"] !== void 0 || pkg.deps.expo !== void 0) platforms.push("android");
|
|
2616
|
-
}
|
|
2617
|
-
const order = (p) => PLATFORMS.indexOf(p);
|
|
2618
|
-
const hostOrder = (a) => AGENT_HOSTS.indexOf(a);
|
|
2619
|
-
return {
|
|
2620
|
-
platforms: uniq(platforms).sort((a, b) => order(a) - order(b)),
|
|
2621
|
-
languages: uniq(languages).sort(),
|
|
2622
|
-
frameworks: uniq(frameworks).sort(),
|
|
2623
|
-
tokenTools: uniq(tokenTools).sort(),
|
|
2624
|
-
agents: uniq(agents).sort((a, b) => hostOrder(a) - hostOrder(b)),
|
|
2625
|
-
styleDictionaryMajor,
|
|
2626
|
-
evidence
|
|
2627
|
-
};
|
|
2628
|
-
}
|
|
2629
|
-
function isPlatform(value) {
|
|
2630
|
-
return PLATFORMS.includes(value);
|
|
2631
|
-
}
|
|
2632
|
-
function isAgentHost(value) {
|
|
2633
|
-
return AGENT_HOSTS.includes(value);
|
|
2634
|
-
}
|
|
2635
|
-
|
|
2636
3269
|
// src/skill.ts
|
|
2637
|
-
import { existsSync as
|
|
2638
|
-
import { dirname as
|
|
3270
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3271
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
2639
3272
|
|
|
2640
3273
|
// src/tools.ts
|
|
2641
3274
|
var OK_OR_ERROR = { "0": "success", "1": "usage error, bad key or id, or a network or server failure" };
|
|
@@ -2643,18 +3276,24 @@ var LOCAL_ONLY = { "0": "success", "1": "no local pull, or a usage error" };
|
|
|
2643
3276
|
var TOOLS = [
|
|
2644
3277
|
{
|
|
2645
3278
|
name: "setup",
|
|
2646
|
-
usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--only foundation|components] [--component NAME]...",
|
|
3279
|
+
usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
2647
3280
|
summary: "Records the library id, stores the pull key in a gitignored speclayer.local.json, then pulls.",
|
|
2648
3281
|
when: "Once, with the command the plugin's Publish screen hands out. Re-run it after the key is rotated.",
|
|
2649
3282
|
network: true,
|
|
2650
3283
|
needsKey: true,
|
|
2651
|
-
writes: [
|
|
3284
|
+
writes: [
|
|
3285
|
+
"speclayer.json",
|
|
3286
|
+
"speclayer.local.json",
|
|
3287
|
+
".gitignore (one line, when inside a git repo)",
|
|
3288
|
+
"<outDir>/",
|
|
3289
|
+
"outputs[].path from speclayer.json (default spec-layer/tokens.css for web), written in place"
|
|
3290
|
+
],
|
|
2652
3291
|
exits: OK_OR_ERROR
|
|
2653
3292
|
},
|
|
2654
3293
|
{
|
|
2655
3294
|
name: "init",
|
|
2656
|
-
usage: "spec-layer init --id lib_... [--out DIR] [--only foundation|components] [--component NAME]...",
|
|
2657
|
-
summary: "Writes speclayer.json so later commands need no flags. Stores no key and reaches no server.",
|
|
3295
|
+
usage: "spec-layer init --id lib_... [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
3296
|
+
summary: "Writes speclayer.json, with the platforms and default outputs, so later commands need no flags. Stores no key and reaches no server.",
|
|
2658
3297
|
when: "A repo that supplies the key from SPEC_LAYER_KEY instead of a stored file.",
|
|
2659
3298
|
network: false,
|
|
2660
3299
|
needsKey: false,
|
|
@@ -2663,12 +3302,12 @@ var TOOLS = [
|
|
|
2663
3302
|
},
|
|
2664
3303
|
{
|
|
2665
3304
|
name: "pull",
|
|
2666
|
-
usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--only foundation|components] [--component NAME]...",
|
|
3305
|
+
usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
2667
3306
|
summary: "Fetches the published library and writes it under the output directory (default .speclayer/).",
|
|
2668
|
-
when: "After setup, whenever status says the local copy is behind, or after changing the include or dtcg block.",
|
|
3307
|
+
when: "After setup, whenever status says the local copy is behind, or after changing the include or dtcg block, or the outputs block.",
|
|
2669
3308
|
network: true,
|
|
2670
3309
|
needsKey: true,
|
|
2671
|
-
writes: ["<outDir>/"],
|
|
3310
|
+
writes: ["<outDir>/", "outputs[].path from speclayer.json (default spec-layer/tokens.css for web), written in place"],
|
|
2672
3311
|
exits: OK_OR_ERROR
|
|
2673
3312
|
},
|
|
2674
3313
|
{
|
|
@@ -2713,7 +3352,7 @@ var TOOLS = [
|
|
|
2713
3352
|
},
|
|
2714
3353
|
{
|
|
2715
3354
|
name: "skill",
|
|
2716
|
-
usage: "spec-layer skill [--install] [--agent claude|cursor|copilot|windsurf|gemini|agents-md]... [--platform web|ios|android|flutter] [--json] [--out DIR]",
|
|
3355
|
+
usage: "spec-layer skill [--install] [--agent claude|cursor|copilot|windsurf|gemini|agents-md]... [--platform web|ios|android|flutter]... [--json] [--out DIR]",
|
|
2717
3356
|
summary: "Prints a guide for a coding agent, adapted to this repository's stack and to the last pull; --install writes it where the agent reads instructions.",
|
|
2718
3357
|
when: "Right after setup, and again after a pull that adds components or after the codebase changes stack.",
|
|
2719
3358
|
network: false,
|
|
@@ -2768,9 +3407,9 @@ function countNumberTokens(tree) {
|
|
|
2768
3407
|
return n;
|
|
2769
3408
|
}
|
|
2770
3409
|
function readJson(path) {
|
|
2771
|
-
if (!
|
|
3410
|
+
if (!existsSync7(path)) return null;
|
|
2772
3411
|
try {
|
|
2773
|
-
return JSON.parse(
|
|
3412
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2774
3413
|
} catch {
|
|
2775
3414
|
return null;
|
|
2776
3415
|
}
|
|
@@ -2778,13 +3417,13 @@ function readJson(path) {
|
|
|
2778
3417
|
function summarizePull(cwd, outDir, manifest) {
|
|
2779
3418
|
if (!manifest) return null;
|
|
2780
3419
|
const absOut = join6(cwd, outDir);
|
|
2781
|
-
const components = manifest.artifacts.filter((a) => a.kind === "component").map((a) => ({ name: a.name, path: a.
|
|
3420
|
+
const components = manifest.artifacts.filter((a) => a.kind === "component").map((a) => ({ name: a.name, path: a.path ? `${outDir}/${a.path}` : null }));
|
|
2782
3421
|
const foundationEntry = manifest.artifacts.find((a) => a.kind === "foundation") ?? null;
|
|
2783
3422
|
let foundation = null;
|
|
2784
3423
|
if (foundationEntry) {
|
|
2785
3424
|
const tokensDir = join6(absOut, "tokens");
|
|
2786
3425
|
const resolver = readJson(join6(tokensDir, "resolver.json"));
|
|
2787
|
-
const
|
|
3426
|
+
const report2 = readJson(join6(tokensDir, "report.json"));
|
|
2788
3427
|
let tokenFiles = [];
|
|
2789
3428
|
try {
|
|
2790
3429
|
tokenFiles = readdirSync3(tokensDir).filter((f) => f.endsWith(".json") && !RESERVED.has(f)).sort();
|
|
@@ -2797,13 +3436,13 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
2797
3436
|
unitlessNumbers += countNumberTokens(readJson(join6(tokensDir, file)));
|
|
2798
3437
|
}
|
|
2799
3438
|
const reportCounts = {};
|
|
2800
|
-
if (Array.isArray(
|
|
2801
|
-
for (const entry2 of
|
|
3439
|
+
if (Array.isArray(report2)) {
|
|
3440
|
+
for (const entry2 of report2) {
|
|
2802
3441
|
if (typeof entry2?.code === "string") reportCounts[entry2.code] = (reportCounts[entry2.code] ?? 0) + 1;
|
|
2803
3442
|
}
|
|
2804
3443
|
}
|
|
2805
3444
|
foundation = {
|
|
2806
|
-
written: foundationEntry.
|
|
3445
|
+
written: foundationEntry.path !== null && resolver !== null,
|
|
2807
3446
|
sets: resolver ? Object.keys(resolver.sets ?? {}) : [],
|
|
2808
3447
|
modifiers: resolver ? Object.entries(resolver.modifiers ?? {}).map(([name, m]) => ({
|
|
2809
3448
|
name,
|
|
@@ -2821,7 +3460,19 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
2821
3460
|
publishedAt: manifest.publishedAt,
|
|
2822
3461
|
pluginVersion: manifest.pluginVersion,
|
|
2823
3462
|
components,
|
|
2824
|
-
foundation
|
|
3463
|
+
foundation,
|
|
3464
|
+
outputs: (manifest.outputs ?? []).map((o) => ({
|
|
3465
|
+
platform: o.platform,
|
|
3466
|
+
format: o.format,
|
|
3467
|
+
path: o.path,
|
|
3468
|
+
case: o.case,
|
|
3469
|
+
modeSelector: o.modeSelector ?? '[data-theme="{mode}"]',
|
|
3470
|
+
modes: o.modes ?? {},
|
|
3471
|
+
// manifest.outputs records the configured list regardless of whether the
|
|
3472
|
+
// Foundation was written; the map file exists only when it was actually
|
|
3473
|
+
// rendered, so it is the on-disk proof a sentence can point to.
|
|
3474
|
+
written: existsSync7(join6(absOut, "outputs", `${o.platform}-${o.format}.map.json`))
|
|
3475
|
+
}))
|
|
2825
3476
|
};
|
|
2826
3477
|
}
|
|
2827
3478
|
var code = (s) => `\`${s}\``;
|
|
@@ -2848,7 +3499,7 @@ function stackSection(input) {
|
|
|
2848
3499
|
""
|
|
2849
3500
|
);
|
|
2850
3501
|
} else {
|
|
2851
|
-
const label = platformSource === "flag" ? "chosen with --platform" : "detected";
|
|
3502
|
+
const label = platformSource === "flag" ? "chosen with --platform" : platformSource === "config" ? "set in speclayer.json" : "detected";
|
|
2852
3503
|
lines.push(`Target platform${platforms.length > 1 ? "s" : ""} (${label}): ${platforms.join(", ")}.`, "");
|
|
2853
3504
|
}
|
|
2854
3505
|
for (const platform of platforms) {
|
|
@@ -2856,10 +3507,41 @@ function stackSection(input) {
|
|
|
2856
3507
|
const tokensDir = `${input.outDir}/tokens/`;
|
|
2857
3508
|
if (platform === "web") {
|
|
2858
3509
|
lines.push("### Web", "");
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
3510
|
+
const cssOut = pull?.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
|
|
3511
|
+
if (cssOut) {
|
|
3512
|
+
const mapPath = `${input.outDir}/outputs/web-css.map.json`;
|
|
3513
|
+
lines.push(
|
|
3514
|
+
`The CSS custom property for every token is in ${code(mapPath)}: source "code_syntax" when the designer declared it in Figma, "derived" when the CLI built it from the DTCG path by the stated rule (${cssOut.case} case, collection root included). Use those names; never invent a third. ${code(`${tokensDir}spec-layer.meta.json`)} still holds the raw ${code("code_syntax.WEB")} the designer declared.`,
|
|
3515
|
+
""
|
|
3516
|
+
);
|
|
3517
|
+
lines.push(
|
|
3518
|
+
`Import ${code(cssOut.path)} from the root stylesheet. It holds every set and every default mode at ${code(":root")}; every other mode is a block under ${code(cssOut.modeSelector)}. To switch, set ${code("data-theme")} on ${code("<html>")} (or whatever the selector names). Wire it to prefers-color-scheme yourself if the OS should choose; the file never assumes that.`,
|
|
3519
|
+
""
|
|
3520
|
+
);
|
|
3521
|
+
if (profile.tokenTools.includes("style-dictionary") || profile.tokenTools.includes("tokens-studio")) {
|
|
3522
|
+
lines.push(
|
|
3523
|
+
`${code(cssOut.path)} is a projection of the same ${code("tokens/")} files, not a second source. Import one or the other.`,
|
|
3524
|
+
""
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
} else {
|
|
3528
|
+
lines.push(
|
|
3529
|
+
`Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code(`code_syntax.${key}`)}, when the designer declared one in Figma. When a token has no WEB entry, use the DTCG path as it appears in the token file and say in your change that the code name is not declared in Figma.`,
|
|
3530
|
+
""
|
|
3531
|
+
);
|
|
3532
|
+
const configuredNotWritten = pull?.outputs.find((o) => o.platform === "web" && !o.written) ?? null;
|
|
3533
|
+
if (configuredNotWritten) {
|
|
3534
|
+
lines.push(
|
|
3535
|
+
`A web/css output is configured at ${code(configuredNotWritten.path)} but was not written, because the last pull did not write the Foundation. Pull with the Foundation selected to write it.`,
|
|
3536
|
+
""
|
|
3537
|
+
);
|
|
3538
|
+
} else if (pull?.foundation?.written) {
|
|
3539
|
+
lines.push(
|
|
3540
|
+
`No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${code("spec-layer/tokens.css")}.`,
|
|
3541
|
+
""
|
|
3542
|
+
);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
2863
3545
|
if (profile.tokenTools.includes("tailwind")) {
|
|
2864
3546
|
lines.push(
|
|
2865
3547
|
`Tailwind is present (${code("tailwindcss")}). Map DTCG ${code("color")} tokens to the theme's color scale and ${code("dimension")} tokens to spacing, radius, or font size by the collection and group they sit in. Keep the mapping in one place and reference token paths, not copied values, so a republish moves the code with it.`,
|
|
@@ -2937,7 +3619,10 @@ function pullSection(input) {
|
|
|
2937
3619
|
} else {
|
|
2938
3620
|
lines.push("- This library has no Foundation, so there is no tokens/ directory.");
|
|
2939
3621
|
}
|
|
2940
|
-
lines.push(`- ${code(`${outDir}/
|
|
3622
|
+
lines.push(`- ${code(`${outDir}/components/`)}: one YAML per component.`);
|
|
3623
|
+
for (const o of pull.outputs) {
|
|
3624
|
+
lines.push(o.written ? `- ${code(o.path)}: ${o.platform}/${o.format} token file, ${o.case} names, modes under ${code(o.modeSelector)}. Names and provenance: ${code(`${outDir}/outputs/${o.platform}-${o.format}.map.json`)}; what it could not express: ${code(`${outDir}/outputs/${o.platform}-${o.format}.report.json`)}.` : `- ${code(o.path)}: ${o.platform}/${o.format} token file, configured but not written by the last pull (the Foundation was not written). Nothing is on disk at that path from Spec Layer.`);
|
|
3625
|
+
}
|
|
2941
3626
|
lines.push("");
|
|
2942
3627
|
if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
|
|
2943
3628
|
lines.push("### Token collections", "");
|
|
@@ -2993,11 +3678,13 @@ function buildSkillGuide(input) {
|
|
|
2993
3678
|
);
|
|
2994
3679
|
lines.push("## How to use it", "");
|
|
2995
3680
|
lines.push(`1. Run ${code("npx spec-layer status")}. Exit 0 means the local copy is current; exit 2 means run ${code("npx spec-layer pull")} first.`);
|
|
2996
|
-
lines.push(`2. Building or changing a component: read its YAML under ${code(`${outDir}/
|
|
3681
|
+
lines.push(`2. Building or changing a component: read its YAML under ${code(`${outDir}/components/`)}, or ${code("npx spec-layer show component NAME")}. ${code("api")} gives variants, states, booleans, and slots; ${code("anatomy")} names the parts; ${code("references.bindings")} says which token each part's property uses and under which ${code("when")} conditions; ${code("unbound")} lists values that are hardcoded in Figma.`);
|
|
2997
3682
|
lines.push(`3. Working with colors, spacing, type, or effects: start at ${code(`${outDir}/tokens/resolver.json`)}, load the set and mode files it names, and look up ${code("code_syntax")} in ${code("spec-layer.meta.json")} for the name the designer declared for your platform.`);
|
|
2998
3683
|
lines.push(`4. Reference tokens by name in code; never paste a resolved value where a token exists. A value the design system does not define is not a token: say so in your change rather than adding one.`);
|
|
2999
3684
|
lines.push(`5. An ${code("unbound")} entry is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it.`);
|
|
3000
|
-
|
|
3685
|
+
const writtenOutputs = input.pull?.outputs.filter((o) => o.written) ?? [];
|
|
3686
|
+
const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code(o.path)).join(", ")} either: pull replaces ${writtenOutputs.length === 1 ? "it" : "them"} in place.` : "";
|
|
3687
|
+
lines.push(`6. Never edit files under ${code(outDir + "/")}: the next pull replaces the whole directory.${outputNote} Configuration lives in ${code("speclayer.json")}. Never commit ${code(CREDENTIALS_NAME)}, and never print or copy the pull key.`);
|
|
3001
3688
|
lines.push("");
|
|
3002
3689
|
lines.push(...pullSection(input));
|
|
3003
3690
|
lines.push(...stackSection(input));
|
|
@@ -3078,19 +3765,19 @@ ${BLOCK_END}
|
|
|
3078
3765
|
function installSkill(cwd, host, guide) {
|
|
3079
3766
|
const target = installTarget(host);
|
|
3080
3767
|
const abs = join6(cwd, target.path);
|
|
3081
|
-
const existing =
|
|
3768
|
+
const existing = existsSync7(abs) ? readFileSync7(abs, "utf8") : null;
|
|
3082
3769
|
const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
|
|
3083
3770
|
if (existing === next) return { path: target.path, result: "unchanged" };
|
|
3084
|
-
|
|
3085
|
-
|
|
3771
|
+
mkdirSync3(dirname4(abs), { recursive: true });
|
|
3772
|
+
writeFileSync6(abs, next);
|
|
3086
3773
|
return { path: target.path, result: existing === null ? "created" : "updated" };
|
|
3087
3774
|
}
|
|
3088
3775
|
|
|
3089
3776
|
// src/version.ts
|
|
3090
|
-
import { readFileSync as
|
|
3777
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3091
3778
|
function cliVersion() {
|
|
3092
3779
|
try {
|
|
3093
|
-
const parsed = JSON.parse(
|
|
3780
|
+
const parsed = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8"));
|
|
3094
3781
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
3095
3782
|
} catch {
|
|
3096
3783
|
return "unknown";
|
|
@@ -3108,8 +3795,8 @@ function manifestReader() {
|
|
|
3108
3795
|
}
|
|
3109
3796
|
function sameOutput(a, b) {
|
|
3110
3797
|
const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
|
|
3111
|
-
const
|
|
3112
|
-
return selectionKey(a.selection) === selectionKey(b.selection) &&
|
|
3798
|
+
const key = (v) => JSON.stringify(sortKeys(v ?? {}));
|
|
3799
|
+
return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []);
|
|
3113
3800
|
}
|
|
3114
3801
|
function sortKeys(value) {
|
|
3115
3802
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
@@ -3118,6 +3805,35 @@ function sortKeys(value) {
|
|
|
3118
3805
|
}
|
|
3119
3806
|
return value;
|
|
3120
3807
|
}
|
|
3808
|
+
function platformsFromFlags(flags, io2) {
|
|
3809
|
+
if (flags.platform === void 0 || flags.platform.length === 0) return void 0;
|
|
3810
|
+
const out = [];
|
|
3811
|
+
for (const value of flags.platform) {
|
|
3812
|
+
if (!isPlatform(value)) {
|
|
3813
|
+
io2.err(`--platform takes ${PLATFORMS.join(", ")}, not "${value}".`);
|
|
3814
|
+
return null;
|
|
3815
|
+
}
|
|
3816
|
+
if (!out.includes(value)) out.push(value);
|
|
3817
|
+
}
|
|
3818
|
+
return out;
|
|
3819
|
+
}
|
|
3820
|
+
function resolvePlatforms(cwd, fromFlags, config) {
|
|
3821
|
+
if (fromFlags) return { platforms: fromFlags, source: "flag" };
|
|
3822
|
+
if (config?.platforms && config.platforms.length > 0) return { platforms: config.platforms, source: "config" };
|
|
3823
|
+
const detected = detectRepo(cwd).platforms;
|
|
3824
|
+
return { platforms: detected, source: detected.length > 0 ? "detected" : "none" };
|
|
3825
|
+
}
|
|
3826
|
+
function outputsForRun(fromFlags, config, platforms) {
|
|
3827
|
+
if (fromFlags) return withDefaults(config?.outputs ?? [], fromFlags);
|
|
3828
|
+
return config?.outputs ?? defaultOutputs(platforms);
|
|
3829
|
+
}
|
|
3830
|
+
var NO_PLATFORM_NOTE = `No target platform detected, so no token file was written for your code. Pass --platform ${PLATFORMS.join("|")}, or add outputs to speclayer.json.`;
|
|
3831
|
+
function platformsMissingFormat(platforms) {
|
|
3832
|
+
return platforms.filter((p) => !FORMATS.some((f) => f.platform === p));
|
|
3833
|
+
}
|
|
3834
|
+
function missingFormatNote(platforms) {
|
|
3835
|
+
return `No token file exists yet for ${platforms.join(", ")}: no output format is available for that platform. Web has css.`;
|
|
3836
|
+
}
|
|
3121
3837
|
var errorText = (err) => err instanceof Error ? err.message : String(err);
|
|
3122
3838
|
function runInit(cwd, flags, io2) {
|
|
3123
3839
|
if (!flags.id) {
|
|
@@ -3131,9 +3847,24 @@ function runInit(cwd, flags, io2) {
|
|
|
3131
3847
|
io2.err(errorText(err));
|
|
3132
3848
|
return 1;
|
|
3133
3849
|
}
|
|
3850
|
+
const fromFlags = platformsFromFlags(flags, io2);
|
|
3851
|
+
if (fromFlags === null) return 1;
|
|
3852
|
+
const { platforms, source } = resolvePlatforms(cwd, fromFlags, null);
|
|
3853
|
+
const outputs = defaultOutputs(platforms);
|
|
3134
3854
|
const outDir = flags.out ?? DEFAULT_OUT_DIR;
|
|
3135
|
-
writeConfig(cwd, {
|
|
3136
|
-
|
|
3855
|
+
writeConfig(cwd, {
|
|
3856
|
+
libraryId: flags.id,
|
|
3857
|
+
outDir,
|
|
3858
|
+
...include ? { include } : {},
|
|
3859
|
+
...platforms.length > 0 ? { platforms } : {},
|
|
3860
|
+
...outputs.length > 0 ? { outputs } : {}
|
|
3861
|
+
});
|
|
3862
|
+
io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}${platforms.length > 0 ? `, platforms ${platforms.join(", ")}` : ""}).`);
|
|
3863
|
+
for (const o of outputs) io2.out(`Token file for ${o.platform}: ${o.path} (${o.format}, ${o.case} names), written by the next pull.`);
|
|
3864
|
+
if (source === "flag" || source === "detected") {
|
|
3865
|
+
const missing = platformsMissingFormat(platforms);
|
|
3866
|
+
if (missing.length > 0) io2.out(missingFormatNote(missing));
|
|
3867
|
+
}
|
|
3137
3868
|
io2.out(`The pull key is not stored here. Run spec-layer setup to store it in ${CREDENTIALS_NAME}, or set SPEC_LAYER_KEY.`);
|
|
3138
3869
|
return 0;
|
|
3139
3870
|
}
|
|
@@ -3193,16 +3924,22 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
|
|
|
3193
3924
|
} catch {
|
|
3194
3925
|
existing = null;
|
|
3195
3926
|
}
|
|
3927
|
+
const fromFlags = platformsFromFlags(flags, io2);
|
|
3928
|
+
if (fromFlags === null) return 1;
|
|
3196
3929
|
const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
|
|
3197
3930
|
const keptInclude = include ?? existing?.include ?? null;
|
|
3198
3931
|
const keptDtcg = existing?.dtcg ?? null;
|
|
3932
|
+
const { platforms } = resolvePlatforms(cwd, fromFlags, existing);
|
|
3933
|
+
const outputs = withDefaults(existing?.outputs ?? [], platforms);
|
|
3199
3934
|
writeConfig(cwd, {
|
|
3200
3935
|
libraryId: flags.id,
|
|
3201
3936
|
outDir,
|
|
3202
3937
|
...keptInclude ? { include: keptInclude } : {},
|
|
3203
|
-
...keptDtcg ? { dtcg: keptDtcg } : {}
|
|
3938
|
+
...keptDtcg ? { dtcg: keptDtcg } : {},
|
|
3939
|
+
...platforms.length > 0 ? { platforms } : {},
|
|
3940
|
+
...existing?.outputs !== void 0 || outputs.length > 0 ? { outputs } : {}
|
|
3204
3941
|
});
|
|
3205
|
-
io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}).`);
|
|
3942
|
+
io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}${platforms.length > 0 ? `, platforms ${platforms.join(", ")}` : ""}).`);
|
|
3206
3943
|
const ignored = ensureIgnored(cwd, CREDENTIALS_NAME);
|
|
3207
3944
|
switch (ignored.kind) {
|
|
3208
3945
|
case "refused":
|
|
@@ -3260,11 +3997,17 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
3260
3997
|
io2.err(errorText(err));
|
|
3261
3998
|
return 1;
|
|
3262
3999
|
}
|
|
4000
|
+
const fromFlags = platformsFromFlags(flags, io2);
|
|
4001
|
+
if (fromFlags === null) return 1;
|
|
4002
|
+
const { platforms, source } = resolvePlatforms(cwd, fromFlags, opts);
|
|
4003
|
+
const outputs = outputsForRun(fromFlags, opts, platforms);
|
|
3263
4004
|
const manifest = manifestAt(join7(cwd, opts.outDir));
|
|
4005
|
+
const foundationOnDisk = Boolean(manifest?.artifacts.find((a) => a.kind === "foundation")?.path);
|
|
4006
|
+
const willWriteFoundation = selection.foundation && foundationOnDisk;
|
|
3264
4007
|
const etag = manifest && sameOutput(
|
|
3265
|
-
{ selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg },
|
|
3266
|
-
{ selection, dtcg: opts.dtcg }
|
|
3267
|
-
) ? manifest.bundleHash : void 0;
|
|
4008
|
+
{ selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg, outputs: manifest.outputs },
|
|
4009
|
+
{ selection, dtcg: opts.dtcg, outputs }
|
|
4010
|
+
) && (!willWriteFoundation || outputs.every((o) => existsSync8(resolve4(cwd, o.path)) && existsSync8(join7(cwd, opts.outDir, "outputs", `${outputId(o)}.map.json`)))) ? manifest.bundleHash : void 0;
|
|
3268
4011
|
const result = await fetchBundle({
|
|
3269
4012
|
api: opts.api,
|
|
3270
4013
|
libraryId: opts.libraryId,
|
|
@@ -3281,10 +4024,11 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
3281
4024
|
return 0;
|
|
3282
4025
|
}
|
|
3283
4026
|
let written;
|
|
4027
|
+
let outputPaths = [];
|
|
3284
4028
|
try {
|
|
3285
4029
|
const bundle = parseBundle(result.raw);
|
|
3286
4030
|
const selected = selectComponents(bundle, selection);
|
|
3287
|
-
|
|
4031
|
+
const writeResult = writeBundleFiles({
|
|
3288
4032
|
outDir: join7(cwd, opts.outDir),
|
|
3289
4033
|
cwd,
|
|
3290
4034
|
raw: result.raw,
|
|
@@ -3293,8 +4037,12 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
3293
4037
|
libraryId: opts.libraryId,
|
|
3294
4038
|
publishedAt: result.publishedAt,
|
|
3295
4039
|
bundleHash: result.bundleHash,
|
|
3296
|
-
dtcg: opts.dtcg
|
|
4040
|
+
dtcg: opts.dtcg,
|
|
4041
|
+
platforms,
|
|
4042
|
+
outputs
|
|
3297
4043
|
});
|
|
4044
|
+
written = writeResult.written;
|
|
4045
|
+
outputPaths = writeResult.outputs;
|
|
3298
4046
|
io2.out(
|
|
3299
4047
|
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} (published ${result.publishedAt}).`
|
|
3300
4048
|
);
|
|
@@ -3303,6 +4051,15 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
3303
4051
|
return 1;
|
|
3304
4052
|
}
|
|
3305
4053
|
io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
|
|
4054
|
+
for (const path of outputPaths) {
|
|
4055
|
+
const o = outputs.find((x) => x.path === path);
|
|
4056
|
+
if (o) io2.out(`Wrote ${path} (${o.platform}/${o.format}, ${o.case} names).`);
|
|
4057
|
+
}
|
|
4058
|
+
if (outputPaths.length === 0 && selection.foundation && source === "none" && opts.outputs === void 0) io2.out(NO_PLATFORM_NOTE);
|
|
4059
|
+
if (source === "flag" || source === "config") {
|
|
4060
|
+
const missing = platformsMissingFormat(platforms);
|
|
4061
|
+
if (missing.length > 0) io2.out(missingFormatNote(missing));
|
|
4062
|
+
}
|
|
3306
4063
|
return 0;
|
|
3307
4064
|
}
|
|
3308
4065
|
async function runStatus(cwd, flags, env, io2, fetcher) {
|
|
@@ -3341,11 +4098,15 @@ function runList(cwd, flags, io2) {
|
|
|
3341
4098
|
return 1;
|
|
3342
4099
|
}
|
|
3343
4100
|
io2.out(`Library ${manifest.libraryId}, published ${manifest.publishedAt}.`);
|
|
3344
|
-
const rows = manifest.artifacts.map((a) => [a.kind, a.name, a.
|
|
4101
|
+
const rows = manifest.artifacts.map((a) => [a.kind, a.name, a.path ?? "not written", a.contentHash]);
|
|
3345
4102
|
const widths = [0, 1, 2].map((i) => Math.max(...rows.map((r) => r[i].length)));
|
|
3346
4103
|
for (const row of rows) {
|
|
3347
4104
|
io2.out(row.map((cell, i) => i < 3 ? cell.padEnd(widths[i]) : cell).join(" "));
|
|
3348
4105
|
}
|
|
4106
|
+
for (const o of manifest.outputs ?? []) {
|
|
4107
|
+
const written = existsSync8(join7(outDir, "outputs", `${o.platform}-${o.format}.map.json`));
|
|
4108
|
+
io2.out(["output".padEnd(widths[0]), `${o.platform}/${o.format}`.padEnd(widths[1]), written ? o.path : "not written"].join(" "));
|
|
4109
|
+
}
|
|
3349
4110
|
return 0;
|
|
3350
4111
|
}
|
|
3351
4112
|
var SHOW_USAGE = 'spec-layer show takes "foundation" or "component NAME".';
|
|
@@ -3410,19 +4171,9 @@ function collectSkillInput(cwd, flags, io2) {
|
|
|
3410
4171
|
}
|
|
3411
4172
|
const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
|
|
3412
4173
|
const profile = detectRepo(cwd);
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
if (!isPlatform(flags.platform)) {
|
|
3417
|
-
io2.err(`--platform takes ${PLATFORMS.join(", ")}, not "${flags.platform}".`);
|
|
3418
|
-
return null;
|
|
3419
|
-
}
|
|
3420
|
-
platforms = [flags.platform];
|
|
3421
|
-
platformSource = "flag";
|
|
3422
|
-
} else {
|
|
3423
|
-
platforms = profile.platforms;
|
|
3424
|
-
platformSource = platforms.length > 0 ? "detected" : "none";
|
|
3425
|
-
}
|
|
4174
|
+
const fromFlags = platformsFromFlags(flags, io2);
|
|
4175
|
+
if (fromFlags === null) return null;
|
|
4176
|
+
const { platforms, source: platformSource } = resolvePlatforms(cwd, fromFlags, config);
|
|
3426
4177
|
const pull = summarizePull(cwd, outDir, readManifest(join7(cwd, outDir)));
|
|
3427
4178
|
return { profile, platforms, platformSource, outDir, config, pull, version: cliVersion() };
|
|
3428
4179
|
}
|
|
@@ -3484,17 +4235,18 @@ function runSkill(cwd, flags, io2) {
|
|
|
3484
4235
|
var USAGE = `spec-layer <command>
|
|
3485
4236
|
|
|
3486
4237
|
Commands:
|
|
3487
|
-
setup --id lib_... --key sl_... [--out DIR] [selection]
|
|
4238
|
+
setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]...
|
|
3488
4239
|
store the key, then pull
|
|
3489
|
-
init --id lib_... [--out DIR] [selection]
|
|
3490
|
-
|
|
4240
|
+
init --id lib_... [--out DIR] [selection] [--platform P]...
|
|
4241
|
+
write speclayer.json
|
|
4242
|
+
pull [--id lib_...] [--key sl_...] [selection] [--platform P]...
|
|
3491
4243
|
fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/
|
|
3492
4244
|
status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
|
|
3493
4245
|
list list every artifact in the last pull
|
|
3494
4246
|
show foundation | component NAME [--canonical]
|
|
3495
4247
|
print one artifact (foundation: the DTCG document; component: its AI YAML; --canonical for JSON)
|
|
3496
4248
|
tools [--json] list every command with what it reaches and writes
|
|
3497
|
-
skill [--install] [--agent HOST]... [--platform P] [--json]
|
|
4249
|
+
skill [--install] [--agent HOST]... [--platform P]... [--json]
|
|
3498
4250
|
print a guide for a coding agent, adapted to this repo and the last pull;
|
|
3499
4251
|
--install writes it for claude, cursor, copilot, windsurf, gemini, or agents-md
|
|
3500
4252
|
|
|
@@ -3504,6 +4256,7 @@ Selection (setup, pull and init; flags replace the include block in speclayer.js
|
|
|
3504
4256
|
|
|
3505
4257
|
Options:
|
|
3506
4258
|
--api URL override the API origin (default https://api.spec-layer.com)
|
|
4259
|
+
--platform web|ios|android|flutter the target this repo builds for (repeatable); applies to setup, init, pull, and skill; setup and init store it, pull uses it for the run
|
|
3507
4260
|
The pull key comes from --key, SPEC_LAYER_KEY, or speclayer.local.json written by setup.`;
|
|
3508
4261
|
var io = {
|
|
3509
4262
|
out: (l) => console.log(l),
|
|
@@ -3529,7 +4282,7 @@ async function main() {
|
|
|
3529
4282
|
json: { type: "boolean" },
|
|
3530
4283
|
install: { type: "boolean" },
|
|
3531
4284
|
agent: { type: "string", multiple: true },
|
|
3532
|
-
platform: { type: "string" }
|
|
4285
|
+
platform: { type: "string", multiple: true }
|
|
3533
4286
|
}
|
|
3534
4287
|
}));
|
|
3535
4288
|
} catch {
|