refira-cli 0.1.2 → 0.2.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 +8 -5
- package/dist/index.js +878 -148
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1950,13 +1950,26 @@ class CliApiClient {
|
|
|
1950
1950
|
const body = await res.json();
|
|
1951
1951
|
return body.data;
|
|
1952
1952
|
}
|
|
1953
|
-
async pushPreview(projectId, pageIdentifier, html) {
|
|
1953
|
+
async pushPreview(projectId, pageIdentifier, html, expectedRevision) {
|
|
1954
1954
|
const url = `${this.apiUrl}/api/cli/projects/${projectId}/pages/${pageIdentifier}/preview`;
|
|
1955
|
+
const headers = this.getHeaders();
|
|
1956
|
+
if (expectedRevision) {
|
|
1957
|
+
headers["If-Match"] = expectedRevision;
|
|
1958
|
+
}
|
|
1955
1959
|
const res = await fetch(url, {
|
|
1956
1960
|
method: "POST",
|
|
1957
|
-
headers
|
|
1958
|
-
body: JSON.stringify({
|
|
1961
|
+
headers,
|
|
1962
|
+
body: JSON.stringify({
|
|
1963
|
+
html,
|
|
1964
|
+
...expectedRevision !== undefined ? { expected_revision: expectedRevision } : {}
|
|
1965
|
+
})
|
|
1959
1966
|
});
|
|
1967
|
+
if (res.status === 409) {
|
|
1968
|
+
const errJson = await res.json().catch(() => ({}));
|
|
1969
|
+
const error = new Error(errJson.message ?? "Page has been modified concurrently. Fetch latest revision before applying updates.");
|
|
1970
|
+
error.code = "REVISION_CONFLICT";
|
|
1971
|
+
throw error;
|
|
1972
|
+
}
|
|
1960
1973
|
if (!res.ok) {
|
|
1961
1974
|
const err = await res.text();
|
|
1962
1975
|
throw new Error(`Failed to push preview (${res.status}): ${err}`);
|
|
@@ -1964,13 +1977,68 @@ class CliApiClient {
|
|
|
1964
1977
|
const body = await res.json();
|
|
1965
1978
|
return body.data;
|
|
1966
1979
|
}
|
|
1980
|
+
async getAnnotations(projectId, pageIdentifier) {
|
|
1981
|
+
const url = `${this.apiUrl}/api/cli/projects/${projectId}/pages/${pageIdentifier}/annotations`;
|
|
1982
|
+
const res = await fetch(url, {
|
|
1983
|
+
headers: this.getHeaders()
|
|
1984
|
+
});
|
|
1985
|
+
if (!res.ok) {
|
|
1986
|
+
const err = await res.text();
|
|
1987
|
+
throw new Error(`Failed to fetch annotations (${res.status}): ${err}`);
|
|
1988
|
+
}
|
|
1989
|
+
const body = await res.json();
|
|
1990
|
+
return body.data;
|
|
1991
|
+
}
|
|
1992
|
+
async cleanServer() {
|
|
1993
|
+
const url = `${this.apiUrl}/api/system/clean`;
|
|
1994
|
+
const res = await fetch(url, {
|
|
1995
|
+
method: "POST",
|
|
1996
|
+
headers: this.getHeaders()
|
|
1997
|
+
});
|
|
1998
|
+
if (!res.ok) {
|
|
1999
|
+
const err = await res.text();
|
|
2000
|
+
throw new Error(`Failed to clean server artifacts (${res.status}): ${err}`);
|
|
2001
|
+
}
|
|
2002
|
+
const body = await res.json();
|
|
2003
|
+
return body.data;
|
|
2004
|
+
}
|
|
2005
|
+
async startGenerating(projectId, pageIdentifier, layoutSlug) {
|
|
2006
|
+
const url = `${this.apiUrl}/api/cli/projects/${projectId}/pages/${pageIdentifier}/start-generating`;
|
|
2007
|
+
const res = await fetch(url, {
|
|
2008
|
+
method: "POST",
|
|
2009
|
+
headers: this.getHeaders(),
|
|
2010
|
+
...layoutSlug ? { body: JSON.stringify({ layout: layoutSlug }) } : {}
|
|
2011
|
+
});
|
|
2012
|
+
if (!res.ok) {
|
|
2013
|
+
const err = await res.text();
|
|
2014
|
+
throw new Error(`Failed to signal generation start (${res.status}): ${err}`);
|
|
2015
|
+
}
|
|
2016
|
+
const body = await res.json();
|
|
2017
|
+
return body.data;
|
|
2018
|
+
}
|
|
2019
|
+
async selectActiveLayout(projectId, selection) {
|
|
2020
|
+
const url = `${this.apiUrl}/api/cli/projects/${projectId}/layout/select`;
|
|
2021
|
+
const res = await fetch(url, {
|
|
2022
|
+
method: "POST",
|
|
2023
|
+
headers: this.getHeaders(),
|
|
2024
|
+
body: JSON.stringify(selection)
|
|
2025
|
+
});
|
|
2026
|
+
if (!res.ok) {
|
|
2027
|
+
const err = await res.text();
|
|
2028
|
+
throw new Error(`Failed to select active layout (${res.status}): ${err}`);
|
|
2029
|
+
}
|
|
2030
|
+
const body = await res.json();
|
|
2031
|
+
return body.data;
|
|
2032
|
+
}
|
|
1967
2033
|
}
|
|
1968
2034
|
|
|
1969
2035
|
// src/config.ts
|
|
1970
2036
|
import fs from "node:fs";
|
|
1971
2037
|
import os from "node:os";
|
|
1972
2038
|
import path from "node:path";
|
|
1973
|
-
var
|
|
2039
|
+
var REFIRA_DIR = ".refira";
|
|
2040
|
+
var REFIRA_CONFIG_FILE = path.join(REFIRA_DIR, ".refirarc");
|
|
2041
|
+
var ROOT_CONFIG_FILE = ".refirarc";
|
|
1974
2042
|
var GLOBAL_CONFIG_FILE = path.join(os.homedir(), ".refirarc");
|
|
1975
2043
|
function loadConfig() {
|
|
1976
2044
|
let config = {
|
|
@@ -1978,23 +2046,21 @@ function loadConfig() {
|
|
|
1978
2046
|
apiKey: process.env.REFIRA_API_KEY,
|
|
1979
2047
|
projectId: process.env.REFIRA_PROJECT_ID
|
|
1980
2048
|
};
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
config = { ...config, ...parsed };
|
|
1992
|
-
} catch {}
|
|
2049
|
+
const candidateFiles = [REFIRA_CONFIG_FILE, ROOT_CONFIG_FILE, GLOBAL_CONFIG_FILE];
|
|
2050
|
+
for (const file of candidateFiles) {
|
|
2051
|
+
if (fs.existsSync(file)) {
|
|
2052
|
+
try {
|
|
2053
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
2054
|
+
const parsed = JSON.parse(raw);
|
|
2055
|
+
config = { ...config, ...parsed };
|
|
2056
|
+
break;
|
|
2057
|
+
} catch {}
|
|
2058
|
+
}
|
|
1993
2059
|
}
|
|
1994
2060
|
return config;
|
|
1995
2061
|
}
|
|
1996
2062
|
function saveConfig(updates, isGlobal = false) {
|
|
1997
|
-
const targetPath = isGlobal ? GLOBAL_CONFIG_FILE :
|
|
2063
|
+
const targetPath = isGlobal ? GLOBAL_CONFIG_FILE : REFIRA_CONFIG_FILE;
|
|
1998
2064
|
let current = {};
|
|
1999
2065
|
if (fs.existsSync(targetPath)) {
|
|
2000
2066
|
try {
|
|
@@ -2002,18 +2068,79 @@ function saveConfig(updates, isGlobal = false) {
|
|
|
2002
2068
|
} catch {
|
|
2003
2069
|
current = {};
|
|
2004
2070
|
}
|
|
2071
|
+
} else if (!isGlobal && fs.existsSync(ROOT_CONFIG_FILE)) {
|
|
2072
|
+
try {
|
|
2073
|
+
current = JSON.parse(fs.readFileSync(ROOT_CONFIG_FILE, "utf-8"));
|
|
2074
|
+
} catch {
|
|
2075
|
+
current = {};
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (!isGlobal) {
|
|
2079
|
+
const dir = path.dirname(targetPath);
|
|
2080
|
+
if (!fs.existsSync(dir)) {
|
|
2081
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2082
|
+
}
|
|
2005
2083
|
}
|
|
2006
2084
|
const merged = { ...current, ...updates };
|
|
2007
2085
|
fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2008
2086
|
}
|
|
2009
2087
|
|
|
2088
|
+
// src/commands/annotations.ts
|
|
2089
|
+
function formatAnnotationsMarkdown(pageSlug, annotations) {
|
|
2090
|
+
if (annotations.length === 0) {
|
|
2091
|
+
return `No open annotations found for page '${pageSlug}'.`;
|
|
2092
|
+
}
|
|
2093
|
+
const lines = [
|
|
2094
|
+
`# Open Annotations for page: ${pageSlug} (${annotations.length})`,
|
|
2095
|
+
"",
|
|
2096
|
+
"| ID | Element | Selector | Comment | Date |",
|
|
2097
|
+
"|---|---|---|---|---|"
|
|
2098
|
+
];
|
|
2099
|
+
for (const ann of annotations) {
|
|
2100
|
+
const el = ann.element_id;
|
|
2101
|
+
const selector = ann.selector_path || "-";
|
|
2102
|
+
const cleanComment = ann.content.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2103
|
+
const date = ann.created_at ? new Date(ann.created_at).toISOString().split("T")[0] : "-";
|
|
2104
|
+
lines.push(`| \`${ann.id}\` | \`${el}\` | \`${selector}\` | ${cleanComment} | ${date} |`);
|
|
2105
|
+
}
|
|
2106
|
+
return lines.join(`
|
|
2107
|
+
`);
|
|
2108
|
+
}
|
|
2109
|
+
async function annotationsCommand(opts) {
|
|
2110
|
+
const pageSlug = opts.page?.trim().toLowerCase();
|
|
2111
|
+
if (!pageSlug) {
|
|
2112
|
+
console.error("Error: --page <slug> is required.");
|
|
2113
|
+
process.exit(1);
|
|
2114
|
+
}
|
|
2115
|
+
const config = loadConfig();
|
|
2116
|
+
if (!config.apiKey || !config.projectId) {
|
|
2117
|
+
console.error("Error: Authentication required. Run `refira auth login` first.");
|
|
2118
|
+
process.exit(1);
|
|
2119
|
+
}
|
|
2120
|
+
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
2121
|
+
try {
|
|
2122
|
+
const res = await client.getAnnotations(config.projectId, pageSlug);
|
|
2123
|
+
const format = opts.format ?? "table";
|
|
2124
|
+
if (format === "json") {
|
|
2125
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const output = formatAnnotationsMarkdown(pageSlug, res.annotations);
|
|
2129
|
+
console.log(output);
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2132
|
+
console.error(`Failed to fetch annotations: ${msg}`);
|
|
2133
|
+
process.exit(1);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2010
2137
|
// src/commands/auth.ts
|
|
2011
2138
|
async function loginCommand(opts) {
|
|
2012
2139
|
const current = loadConfig();
|
|
2013
2140
|
const apiUrl = opts.apiUrl ?? current.apiUrl ?? "http://localhost:3001";
|
|
2014
2141
|
const apiKey = opts.apiKey ?? current.apiKey;
|
|
2015
2142
|
if (!apiKey) {
|
|
2016
|
-
console.error("
|
|
2143
|
+
console.error("Error: API key is required. Provide --api-key <rfr_...>");
|
|
2017
2144
|
process.exit(1);
|
|
2018
2145
|
}
|
|
2019
2146
|
console.log(`Verifying authentication with ${apiUrl}...`);
|
|
@@ -2021,44 +2148,150 @@ async function loginCommand(opts) {
|
|
|
2021
2148
|
try {
|
|
2022
2149
|
const result = await client.verifyAuth();
|
|
2023
2150
|
saveConfig({ apiUrl, apiKey, projectId: result.project_id }, opts.global ?? false);
|
|
2024
|
-
console.log("
|
|
2151
|
+
console.log("Authentication successful.");
|
|
2025
2152
|
console.log(` Project ID: ${result.project_id}`);
|
|
2026
2153
|
console.log(` User ID: ${result.user_id}`);
|
|
2027
2154
|
} catch (err) {
|
|
2028
2155
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2029
|
-
console.error(
|
|
2156
|
+
console.error(`Authentication failed: ${msg}`);
|
|
2030
2157
|
process.exit(1);
|
|
2031
2158
|
}
|
|
2032
2159
|
}
|
|
2033
2160
|
async function statusCommand() {
|
|
2034
2161
|
const config = loadConfig();
|
|
2035
2162
|
if (!config.apiKey) {
|
|
2036
|
-
console.log("
|
|
2163
|
+
console.log("No active Refira session found. Run `refira auth login --api-key <key>` to connect.");
|
|
2037
2164
|
return;
|
|
2038
2165
|
}
|
|
2039
2166
|
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
2040
2167
|
try {
|
|
2041
2168
|
const result = await client.verifyAuth();
|
|
2042
|
-
console.log("
|
|
2169
|
+
console.log("Active Refira Session:");
|
|
2043
2170
|
console.log(` API Endpoint: ${config.apiUrl}`);
|
|
2044
2171
|
console.log(` Project ID: ${result.project_id}`);
|
|
2045
2172
|
console.log(` User ID: ${result.user_id}`);
|
|
2046
2173
|
} catch (err) {
|
|
2047
2174
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2048
|
-
console.error(
|
|
2175
|
+
console.error(`Session invalid or expired: ${msg}`);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// src/commands/clean.ts
|
|
2180
|
+
import fs2 from "node:fs";
|
|
2181
|
+
import path2 from "node:path";
|
|
2182
|
+
var LOCAL_CLEAN_TARGETS = [path2.join(REFIRA_DIR, "work"), "tmp", ".cache"];
|
|
2183
|
+
function discoverLocalCleanFiles(cwd = process.cwd()) {
|
|
2184
|
+
const discovered = [];
|
|
2185
|
+
for (const target of LOCAL_CLEAN_TARGETS) {
|
|
2186
|
+
const fullPath = path2.resolve(cwd, target);
|
|
2187
|
+
if (fs2.existsSync(fullPath)) {
|
|
2188
|
+
discovered.push(fullPath);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return discovered;
|
|
2192
|
+
}
|
|
2193
|
+
async function cleanCommand(opts) {
|
|
2194
|
+
const isDryRun = opts.dryRun === true;
|
|
2195
|
+
const isAll = opts.all === true;
|
|
2196
|
+
console.log(isDryRun ? "Refira clean (dry run mode):" : "Refira clean:");
|
|
2197
|
+
const targets = discoverLocalCleanFiles();
|
|
2198
|
+
if (targets.length === 0) {
|
|
2199
|
+
console.log(" No local scratch or work directories found to clean.");
|
|
2200
|
+
} else {
|
|
2201
|
+
for (const target of targets) {
|
|
2202
|
+
if (isDryRun) {
|
|
2203
|
+
console.log(` [dry-run] Would remove: ${target}`);
|
|
2204
|
+
} else {
|
|
2205
|
+
try {
|
|
2206
|
+
fs2.rmSync(target, { recursive: true, force: true });
|
|
2207
|
+
console.log(` Removed: ${target}`);
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2210
|
+
console.warn(` Warning: Failed to remove ${target}: ${msg}`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (isAll) {
|
|
2216
|
+
if (isDryRun) {
|
|
2217
|
+
console.log(" [dry-run] Would invoke server system clean (POST /api/system/clean)");
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
const config = loadConfig();
|
|
2221
|
+
if (!config.apiKey) {
|
|
2222
|
+
console.warn(" Warning: Server clean skipped because no API key is configured.");
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
console.log(" Invoking server system clean...");
|
|
2226
|
+
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
2227
|
+
try {
|
|
2228
|
+
const serverResult = await client.cleanServer();
|
|
2229
|
+
console.log(` Server clean: ${serverResult.message}`);
|
|
2230
|
+
console.log(` Purged: ${serverResult.purged.export_files} export files, ${serverResult.purged.share_tokens} expired share tokens`);
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2233
|
+
console.warn(` Warning: Server clean failed: ${msg}`);
|
|
2234
|
+
}
|
|
2049
2235
|
}
|
|
2236
|
+
console.log(isDryRun ? "Dry run finished." : "Clean finished.");
|
|
2050
2237
|
}
|
|
2051
2238
|
|
|
2052
2239
|
// src/commands/context.ts
|
|
2240
|
+
function extractSupportedSlots(slug) {
|
|
2241
|
+
const normalized = slug.toLowerCase();
|
|
2242
|
+
if (normalized.includes("blank") || normalized.includes("empty")) {
|
|
2243
|
+
return ["{{ content }}"];
|
|
2244
|
+
}
|
|
2245
|
+
return ["{{ content }}", "{{ title }}"];
|
|
2246
|
+
}
|
|
2247
|
+
function printLayoutsSection(layouts) {
|
|
2248
|
+
console.log(`
|
|
2249
|
+
Available Shared Layouts (${layouts.length}):`);
|
|
2250
|
+
if (layouts.length === 0) {
|
|
2251
|
+
console.log(" (No shared layouts configured)");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
layouts.forEach((layout, idx) => {
|
|
2255
|
+
const defaultTag = layout.is_default ? "yes" : "no";
|
|
2256
|
+
const slots = extractSupportedSlots(layout.slug).join(", ");
|
|
2257
|
+
console.log(` ${idx + 1}. ${layout.name} [slug: ${layout.slug}] (default: ${defaultTag})`);
|
|
2258
|
+
console.log(` Supported Slots: ${slots}`);
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
function formatPropsSchema(propsSchema) {
|
|
2262
|
+
if (!propsSchema || Object.keys(propsSchema).length === 0) {
|
|
2263
|
+
return { summary: "(none)", exampleAttr: "" };
|
|
2264
|
+
}
|
|
2265
|
+
const entries = Object.entries(propsSchema);
|
|
2266
|
+
const summary = entries.map(([k, v]) => `${k}="${String(v)}"`).join(", ");
|
|
2267
|
+
const exampleAttr = ` ${entries.map(([k, v]) => `${k}="${String(v)}"`).join(" ")}`;
|
|
2268
|
+
return { summary, exampleAttr };
|
|
2269
|
+
}
|
|
2270
|
+
function printComponentsSection(components) {
|
|
2271
|
+
console.log(`
|
|
2272
|
+
Reusable Component Catalog (${components.length}):`);
|
|
2273
|
+
if (components.length === 0) {
|
|
2274
|
+
console.log(" (No reusable components registered yet)");
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
components.forEach((comp, idx) => {
|
|
2278
|
+
const { summary, exampleAttr } = formatPropsSchema(comp.props_schema);
|
|
2279
|
+
console.log(` ${idx + 1}. ${comp.name} [tag: <x-${comp.name}></x-${comp.name}> or <x-${comp.name} />]`);
|
|
2280
|
+
console.log(` Props: ${summary}`);
|
|
2281
|
+
console.log(` Example: <x-${comp.name}${exampleAttr}></x-${comp.name}>`);
|
|
2282
|
+
});
|
|
2283
|
+
console.log(" NOTE: Do NOT copy-paste repetitive markup for registered components.");
|
|
2284
|
+
console.log(" Always use <x-...> macro tags in your page templates.");
|
|
2285
|
+
}
|
|
2053
2286
|
async function contextCommand(opts) {
|
|
2054
2287
|
const config = loadConfig();
|
|
2055
2288
|
const projectId = opts.projectId ?? config.projectId;
|
|
2056
2289
|
if (!config.apiKey) {
|
|
2057
|
-
console.error("
|
|
2290
|
+
console.error("Error: API key required. Run `refira auth login --api-key <key>`.");
|
|
2058
2291
|
process.exit(1);
|
|
2059
2292
|
}
|
|
2060
2293
|
if (!projectId) {
|
|
2061
|
-
console.error("
|
|
2294
|
+
console.error("Error: Project ID required. Run `refira init --project-id <id>` or pass --project-id.");
|
|
2062
2295
|
process.exit(1);
|
|
2063
2296
|
}
|
|
2064
2297
|
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
@@ -2068,44 +2301,295 @@ async function contextCommand(opts) {
|
|
|
2068
2301
|
const resolvedProjectId = data.context?.project_id ?? data.context?.project?.id ?? projectId;
|
|
2069
2302
|
console.log(`
|
|
2070
2303
|
======================================================================`);
|
|
2071
|
-
console.log(
|
|
2304
|
+
console.log(`REFIRA DESIGN CONTEXT: ${projectName} (${resolvedProjectId})`);
|
|
2072
2305
|
console.log("======================================================================");
|
|
2073
2306
|
const font = extractFontFamily(data.context?.tokens?.typography, data.context?.project?.fontFamily ?? "Inter");
|
|
2074
2307
|
console.log(`
|
|
2075
|
-
|
|
2308
|
+
Primary Typography: Google Fonts "${font}"`);
|
|
2076
2309
|
const colorTokens = data.context?.tokens?.colors ?? data.context?.tokens?.color;
|
|
2077
2310
|
if (colorTokens) {
|
|
2078
2311
|
console.log(`
|
|
2079
|
-
|
|
2312
|
+
Design Color Tokens:`);
|
|
2080
2313
|
for (const [tokenName, tokenVal] of Object.entries(colorTokens)) {
|
|
2081
2314
|
const displayVal = typeof tokenVal === "string" ? tokenVal : tokenVal?.hex ?? tokenVal?.oklch ?? JSON.stringify(tokenVal);
|
|
2082
2315
|
console.log(` --${tokenName}: ${displayVal}`);
|
|
2083
2316
|
}
|
|
2084
2317
|
}
|
|
2318
|
+
const layouts = data.layouts ?? data.context?.layouts ?? [];
|
|
2319
|
+
const activeLayout = data.active_layout ?? data.context?.active_layout ?? layouts.find((l) => l.is_active) ?? layouts.find((l) => l.is_default) ?? null;
|
|
2320
|
+
console.log(`
|
|
2321
|
+
Active Project Layout Focus:`);
|
|
2322
|
+
if (activeLayout) {
|
|
2323
|
+
console.log(` ${activeLayout.name} [slug: ${activeLayout.slug}] (ID: ${activeLayout.id})`);
|
|
2324
|
+
console.log(` Supported Slots: ${extractSupportedSlots(activeLayout.slug).join(", ")}`);
|
|
2325
|
+
} else {
|
|
2326
|
+
console.log(" (None selected - standalone fallback)");
|
|
2327
|
+
}
|
|
2328
|
+
printLayoutsSection(layouts);
|
|
2329
|
+
const components = data.components ?? data.context?.components ?? [];
|
|
2330
|
+
printComponentsSection(components);
|
|
2085
2331
|
console.log(`
|
|
2086
|
-
|
|
2332
|
+
Existing Pages Roster & Archetypes (${data.pages.length}):`);
|
|
2087
2333
|
if (data.pages.length === 0) {
|
|
2088
2334
|
console.log(" (No pages generated yet)");
|
|
2089
2335
|
} else {
|
|
2090
2336
|
data.pages.forEach((p, idx) => {
|
|
2091
|
-
|
|
2337
|
+
const pageLayout = p.layout_id ? layouts.find((l) => l.id === p.layout_id) : null;
|
|
2338
|
+
const archetype = pageLayout ? `${pageLayout.name} [${pageLayout.slug}]` : "Standalone";
|
|
2339
|
+
console.log(` ${idx + 1}. ${p.name} [slug: ${p.slug}] (status: ${p.status}, archetype: ${archetype})`);
|
|
2092
2340
|
});
|
|
2093
2341
|
}
|
|
2094
2342
|
console.log(`======================================================================
|
|
2095
2343
|
`);
|
|
2096
2344
|
} catch (err) {
|
|
2097
2345
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2098
|
-
console.error(
|
|
2346
|
+
console.error(`Failed to retrieve design context: ${msg}`);
|
|
2099
2347
|
process.exit(1);
|
|
2100
2348
|
}
|
|
2101
2349
|
}
|
|
2102
2350
|
|
|
2351
|
+
// src/commands/guide.ts
|
|
2352
|
+
var GUIDE_TOPICS = ["aria", "forms", "modals", "tables"];
|
|
2353
|
+
var GUIDES = {
|
|
2354
|
+
aria: `# Accessible ARIA Patterns for Refira Prototypes
|
|
2355
|
+
|
|
2356
|
+
## 1. Principles
|
|
2357
|
+
- Always prefer semantic HTML elements (<button>, <nav>, <main>, <header>, <footer>) over generic <div> with ARIA roles.
|
|
2358
|
+
- Use \`aria-expanded="true|false"\` on disclosure buttons (accordions, dropdown menus).
|
|
2359
|
+
- Use \`aria-controls="<id>"\` linking the trigger to the toggled panel.
|
|
2360
|
+
- Use \`aria-current="page"\` on active navigation links.
|
|
2361
|
+
- Use \`aria-live="polite"\` for dynamic status updates (toasts, counter badges).
|
|
2362
|
+
|
|
2363
|
+
## 2. Accessible Disclosure Pattern
|
|
2364
|
+
\`\`\`html
|
|
2365
|
+
<button
|
|
2366
|
+
type="button"
|
|
2367
|
+
id="toggle-btn"
|
|
2368
|
+
aria-expanded="false"
|
|
2369
|
+
aria-controls="panel-1"
|
|
2370
|
+
class="px-4 py-2 bg-gray-100 rounded text-sm font-medium focus:ring-2 focus:ring-blue-500"
|
|
2371
|
+
>
|
|
2372
|
+
Toggle Details
|
|
2373
|
+
</button>
|
|
2374
|
+
|
|
2375
|
+
<div
|
|
2376
|
+
id="panel-1"
|
|
2377
|
+
hidden
|
|
2378
|
+
class="p-4 mt-2 border rounded bg-white"
|
|
2379
|
+
>
|
|
2380
|
+
Content revealed when expanded.
|
|
2381
|
+
</div>
|
|
2382
|
+
|
|
2383
|
+
<script>
|
|
2384
|
+
const btn = document.getElementById('toggle-btn');
|
|
2385
|
+
const panel = document.getElementById('panel-1');
|
|
2386
|
+
btn.addEventListener('click', () => {
|
|
2387
|
+
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
|
2388
|
+
btn.setAttribute('aria-expanded', String(!expanded));
|
|
2389
|
+
panel.hidden = expanded;
|
|
2390
|
+
});
|
|
2391
|
+
</script>
|
|
2392
|
+
\`\`\`
|
|
2393
|
+
`,
|
|
2394
|
+
forms: `# Accessible Forms & Validation Patterns for Refira Prototypes
|
|
2395
|
+
|
|
2396
|
+
## 1. Principles
|
|
2397
|
+
- Every input must have an explicitly associated \`<label for="<id>">\`.
|
|
2398
|
+
- Required fields must specify \`required\` and \`aria-required="true"\`.
|
|
2399
|
+
- Invalid inputs must be marked with \`aria-invalid="true"\` and linked to error text via \`aria-describedby="<error-id>"\`.
|
|
2400
|
+
- Buttons inside forms must explicitly declare \`type="submit"\` or \`type="button"\`.
|
|
2401
|
+
|
|
2402
|
+
## 2. Form Group with Error State
|
|
2403
|
+
\`\`\`html
|
|
2404
|
+
<form id="sample-form" novalidate class="space-y-4 max-w-md">
|
|
2405
|
+
<div>
|
|
2406
|
+
<label for="email-field" class="block text-sm font-medium text-gray-700 mb-1">
|
|
2407
|
+
Email address <span class="text-red-500" aria-hidden="true">*</span>
|
|
2408
|
+
</label>
|
|
2409
|
+
<input
|
|
2410
|
+
type="email"
|
|
2411
|
+
id="email-field"
|
|
2412
|
+
name="email"
|
|
2413
|
+
required
|
|
2414
|
+
aria-required="true"
|
|
2415
|
+
aria-describedby="email-error"
|
|
2416
|
+
class="w-full px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
2417
|
+
placeholder="you@example.com"
|
|
2418
|
+
/>
|
|
2419
|
+
<p id="email-error" class="hidden text-xs text-red-600 mt-1" role="alert">
|
|
2420
|
+
Please enter a valid email address.
|
|
2421
|
+
</p>
|
|
2422
|
+
</div>
|
|
2423
|
+
<button
|
|
2424
|
+
type="submit"
|
|
2425
|
+
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md font-medium text-sm hover:bg-blue-700"
|
|
2426
|
+
>
|
|
2427
|
+
Submit
|
|
2428
|
+
</button>
|
|
2429
|
+
</form>
|
|
2430
|
+
|
|
2431
|
+
<script>
|
|
2432
|
+
const form = document.getElementById('sample-form');
|
|
2433
|
+
const input = document.getElementById('email-field');
|
|
2434
|
+
const error = document.getElementById('email-error');
|
|
2435
|
+
|
|
2436
|
+
form.addEventListener('submit', (e) => {
|
|
2437
|
+
e.preventDefault();
|
|
2438
|
+
const isValid = input.value.includes('@') && input.value.includes('.');
|
|
2439
|
+
input.setAttribute('aria-invalid', String(!isValid));
|
|
2440
|
+
error.classList.toggle('hidden', isValid);
|
|
2441
|
+
});
|
|
2442
|
+
</script>
|
|
2443
|
+
\`\`\`
|
|
2444
|
+
`,
|
|
2445
|
+
modals: `# Accessible Modal Dialog Pattern for Refira Prototypes
|
|
2446
|
+
|
|
2447
|
+
## 1. Principles
|
|
2448
|
+
- Use the native HTML5 \`<dialog>\` element or a container with \`role="dialog"\` and \`aria-modal="true"\`.
|
|
2449
|
+
- Provide \`aria-labelledby="<title-id>"\` pointing to the modal heading.
|
|
2450
|
+
- Close on Escape key and restore focus to the triggering element.
|
|
2451
|
+
- Backdrop click closes the modal dialog.
|
|
2452
|
+
|
|
2453
|
+
## 2. Accessible Native Dialog Pattern
|
|
2454
|
+
\`\`\`html
|
|
2455
|
+
<button
|
|
2456
|
+
type="button"
|
|
2457
|
+
id="open-dialog-btn"
|
|
2458
|
+
class="px-4 py-2 bg-blue-600 text-white rounded text-sm font-medium hover:bg-blue-700"
|
|
2459
|
+
>
|
|
2460
|
+
Open Settings Modal
|
|
2461
|
+
</button>
|
|
2462
|
+
|
|
2463
|
+
<dialog
|
|
2464
|
+
id="settings-dialog"
|
|
2465
|
+
aria-labelledby="dialog-title"
|
|
2466
|
+
class="rounded-lg shadow-xl p-0 backdrop:bg-black/50 max-w-lg w-full"
|
|
2467
|
+
>
|
|
2468
|
+
<div class="p-6">
|
|
2469
|
+
<div class="flex items-center justify-between mb-4">
|
|
2470
|
+
<h2 id="dialog-title" class="text-lg font-semibold text-gray-900">
|
|
2471
|
+
Account Settings
|
|
2472
|
+
</h2>
|
|
2473
|
+
<button
|
|
2474
|
+
type="button"
|
|
2475
|
+
id="close-dialog-btn"
|
|
2476
|
+
aria-label="Close dialog"
|
|
2477
|
+
class="text-gray-400 hover:text-gray-600"
|
|
2478
|
+
>
|
|
2479
|
+
✕
|
|
2480
|
+
</button>
|
|
2481
|
+
</div>
|
|
2482
|
+
<p class="text-sm text-gray-600 mb-6">
|
|
2483
|
+
Modify your account preferences below.
|
|
2484
|
+
</p>
|
|
2485
|
+
<div class="flex justify-end gap-2">
|
|
2486
|
+
<button
|
|
2487
|
+
type="button"
|
|
2488
|
+
id="cancel-dialog-btn"
|
|
2489
|
+
class="px-3 py-1.5 border rounded text-sm text-gray-700 hover:bg-gray-50"
|
|
2490
|
+
>
|
|
2491
|
+
Cancel
|
|
2492
|
+
</button>
|
|
2493
|
+
<button
|
|
2494
|
+
type="button"
|
|
2495
|
+
class="px-3 py-1.5 bg-blue-600 text-white rounded text-sm font-medium hover:bg-blue-700"
|
|
2496
|
+
>
|
|
2497
|
+
Save Changes
|
|
2498
|
+
</button>
|
|
2499
|
+
</div>
|
|
2500
|
+
</div>
|
|
2501
|
+
</dialog>
|
|
2502
|
+
|
|
2503
|
+
<script>
|
|
2504
|
+
const openBtn = document.getElementById('open-dialog-btn');
|
|
2505
|
+
const closeBtn = document.getElementById('close-dialog-btn');
|
|
2506
|
+
const cancelBtn = document.getElementById('cancel-dialog-btn');
|
|
2507
|
+
const dialog = document.getElementById('settings-dialog');
|
|
2508
|
+
|
|
2509
|
+
openBtn.addEventListener('click', () => dialog.showModal());
|
|
2510
|
+
closeBtn.addEventListener('click', () => dialog.close());
|
|
2511
|
+
cancelBtn.addEventListener('click', () => dialog.close());
|
|
2512
|
+
|
|
2513
|
+
dialog.addEventListener('click', (e) => {
|
|
2514
|
+
if (e.target === dialog) dialog.close();
|
|
2515
|
+
});
|
|
2516
|
+
</script>
|
|
2517
|
+
\`\`\`
|
|
2518
|
+
`,
|
|
2519
|
+
tables: `# Accessible Data Table Patterns for Refira Prototypes
|
|
2520
|
+
|
|
2521
|
+
## 1. Principles
|
|
2522
|
+
- Always include a \`<caption>\` describing the table's purpose or summarizing its data.
|
|
2523
|
+
- Header cells must use \`<th scope="col">\` for column headers and \`<th scope="row">\` for row headers.
|
|
2524
|
+
- For sortable tables, use \`aria-sort="ascending|descending|none"\` on sortable \`<th>\` headers with an accessible \`<button>\`.
|
|
2525
|
+
- Maintain tabular layout responsiveness via horizontal overflow containers (\`overflow-x-auto\`).
|
|
2526
|
+
|
|
2527
|
+
## 2. Accessible Sortable Table Pattern
|
|
2528
|
+
\`\`\`html
|
|
2529
|
+
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
|
2530
|
+
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
|
2531
|
+
<caption class="sr-only">Project pages and deployment revisions</caption>
|
|
2532
|
+
<thead class="bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
2533
|
+
<tr>
|
|
2534
|
+
<th scope="col" class="px-6 py-3">
|
|
2535
|
+
<button type="button" class="group flex items-center gap-1 font-semibold" aria-sort="ascending">
|
|
2536
|
+
Page Title
|
|
2537
|
+
<span class="text-gray-400 group-hover:text-gray-600" aria-hidden="true">▲</span>
|
|
2538
|
+
</button>
|
|
2539
|
+
</th>
|
|
2540
|
+
<th scope="col" class="px-6 py-3">Slug</th>
|
|
2541
|
+
<th scope="col" class="px-6 py-3">Status</th>
|
|
2542
|
+
<th scope="col" class="px-6 py-3 text-right">Actions</th>
|
|
2543
|
+
</tr>
|
|
2544
|
+
</thead>
|
|
2545
|
+
<tbody class="divide-y divide-gray-200 bg-white">
|
|
2546
|
+
<tr>
|
|
2547
|
+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
|
2548
|
+
Overview Dashboard
|
|
2549
|
+
</th>
|
|
2550
|
+
<td class="px-6 py-4 text-gray-500">dashboard</td>
|
|
2551
|
+
<td class="px-6 py-4">
|
|
2552
|
+
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
2553
|
+
Approved
|
|
2554
|
+
</span>
|
|
2555
|
+
</td>
|
|
2556
|
+
<td class="px-6 py-4 text-right">
|
|
2557
|
+
<button type="button" class="text-blue-600 hover:text-blue-900 font-medium">Edit</button>
|
|
2558
|
+
</td>
|
|
2559
|
+
</tr>
|
|
2560
|
+
</tbody>
|
|
2561
|
+
</table>
|
|
2562
|
+
</div>
|
|
2563
|
+
\`\`\`
|
|
2564
|
+
`
|
|
2565
|
+
};
|
|
2566
|
+
function getGuideContent(topic) {
|
|
2567
|
+
const normalized = topic.trim().toLowerCase();
|
|
2568
|
+
return GUIDES[normalized] ?? null;
|
|
2569
|
+
}
|
|
2570
|
+
function guideCommand(topic) {
|
|
2571
|
+
const content = getGuideContent(topic);
|
|
2572
|
+
if (!content) {
|
|
2573
|
+
console.error(`Error: Unknown guide topic '${topic}'.`);
|
|
2574
|
+
console.error(`Supported topics: ${GUIDES_TOPICS_LIST}`);
|
|
2575
|
+
process.exit(1);
|
|
2576
|
+
}
|
|
2577
|
+
console.log(content);
|
|
2578
|
+
}
|
|
2579
|
+
var GUIDES_TOPICS_LIST = GUIDE_TOPICS.join(", ");
|
|
2580
|
+
|
|
2103
2581
|
// src/commands/init.ts
|
|
2104
|
-
import
|
|
2105
|
-
import
|
|
2582
|
+
import fs3 from "node:fs";
|
|
2583
|
+
import path3 from "node:path";
|
|
2106
2584
|
|
|
2107
|
-
// src/templates/
|
|
2108
|
-
|
|
2585
|
+
// src/templates/rules-template.ts
|
|
2586
|
+
var REFIRA_SENTINEL_START = "<!-- REFIRA:START -->";
|
|
2587
|
+
var REFIRA_SENTINEL_END = "<!-- REFIRA:END -->";
|
|
2588
|
+
var REFIRA_SENTINEL_BLOCK = `${REFIRA_SENTINEL_START}
|
|
2589
|
+
### Refira UI Prototypes
|
|
2590
|
+
All prototype pages are located in \`.refira/pages/\`. When creating or editing prototypes, strictly adhere to [.refira/RULES.md](.refira/RULES.md) and activate skill \`refira\` (\`.agents/skills/refira/SKILL.md\`).
|
|
2591
|
+
${REFIRA_SENTINEL_END}`;
|
|
2592
|
+
function generateRefiraRules(opts) {
|
|
2109
2593
|
return `# Refira Project Instructions
|
|
2110
2594
|
|
|
2111
2595
|
**Project Name:** ${opts.projectName}
|
|
@@ -2116,29 +2600,43 @@ function generateAgentsGuide(opts) {
|
|
|
2116
2600
|
---
|
|
2117
2601
|
|
|
2118
2602
|
## 1. Core Architecture Invariants
|
|
2119
|
-
1. **
|
|
2120
|
-
2. **
|
|
2121
|
-
3. **
|
|
2122
|
-
4. **No
|
|
2123
|
-
5. **
|
|
2124
|
-
6. **
|
|
2603
|
+
1. **Workspace Location:** All prototype pages reside strictly in \`.refira/pages/<slug>.html\`.
|
|
2604
|
+
2. **Output Format:** Standalone HTML5 + Tailwind CSS CDN only.
|
|
2605
|
+
3. **Typography:** Load and use Google Fonts "${opts.fontFamily}".
|
|
2606
|
+
4. **No UI Frameworks:** Prohibited from using React, Vue, Svelte, Angular, Solid, or JSX syntax (\`className=\`, \`onClick={...}\`, \`<Component />\`).
|
|
2607
|
+
5. **Strict Anchor & Navigation Invariant:** Prohibited from creating cross-page navigation, external links, empty \`href=""\`, or \`javascript:\` navigation in \`<a href="...">\`. Anchor links MUST point only to in-page section hashes (e.g. \`href="#pricing"\`) or dummy hash (\`href="#"\`). Target attributes (\`target="_blank"\`, \`target="_top"\`) and external \`<form action="...">\` are prohibited. For interactive triggers, use \`<button type="button">\`.
|
|
2608
|
+
6. **No Raw Emojis:** Do not include raw emojis (e.g. icons, smileys, unicode pictographs) in HTML markup. Use SVG icon libraries (Lucide Icons or Heroicons).
|
|
2609
|
+
7. **Permitted Graphics:** Three.js, GSAP, Spline Viewer, and Lucide Icons via CDN are explicitly allowed.
|
|
2125
2610
|
|
|
2126
2611
|
---
|
|
2127
2612
|
|
|
2128
|
-
## 2.
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2613
|
+
## 2. Reusable Components & Layout Macros
|
|
2614
|
+
1. **Mandatory Context Check Protocol:** Sebelum membuat halaman baru di \`.refira/pages/\`, AI Agent WAJIB menjalankan \`refira context\` untuk memeriksa acuan layout aktif (\`Active Project Layout Focus\`), riwayat archetype halaman, dan katalog komponen terkini.
|
|
2615
|
+
2. **Alur A Decision Protocol:** Jika maksud halaman baru terdeteksi berbeda kelompok dengan Active Layout (misal: halaman publik saat layout aktif adalah dashboard), AI Agent DILARANG mengubah layout diam-diam atau memaksakan layout yang salah. Tanyakan konfirmasi terlebih dahulu di terminal/chat kepada pengguna, lalu setelah disetujui jalankan \`refira layout select <slug>\`.
|
|
2616
|
+
3. **Architectural Shell & Real Components:** Layout adalah acuan arsitektur. Anda wajib membuat komponen nyata untuk shell arsitektur tersebut (misal navbar / sidebar) dan menggunakannya di halaman-halaman selanjutnya menggunakan tag \`<x-...>\`.
|
|
2617
|
+
4. **No Duplicate Markup:** DILARANG meng-copas markup navbar, sidebar, footer, atau elemen UI lain yang sudah terdaftar di katalog komponen proyek (\`refira context\`).
|
|
2618
|
+
5. **Macro Usage:** Wajib gunakan tag makro \`<x-nama-komponen></x-nama-komponen>\` atau \`<x-nama-komponen />\` dengan atribut props yang didefinisikan pada props schema. Komponen akan di-expand secara otomatis oleh Refira Layout Engine saat push.
|
|
2619
|
+
6. **Shared Layouts & Slots:** Halaman yang terikat pada shared layout harus fokus mengisi konten pada slot \`{{ content }}\` dan judul pada \`{{ title }}\` (jika ada), tanpa perlu mengulang boilerplate \`<head>\` atau kerangka shell yang sudah disediakan layout.
|
|
2133
2620
|
|
|
2134
2621
|
---
|
|
2135
2622
|
|
|
2136
|
-
## 3.
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2623
|
+
## 3. Refira CLI Command Surface
|
|
2624
|
+
- \`refira context\`: Inspect project design tokens, colors, typography, shared layouts, and existing pages.
|
|
2625
|
+
- \`refira layout select <slug>\`: Select active project layout focus by slug.
|
|
2626
|
+
- \`refira layout status\`: Inspect active project layout focus and available layouts.
|
|
2627
|
+
- \`refira scaffold --page <slug>\`: Create a clean HTML5 starter template in \`.refira/pages/<slug>.html\` and trigger live generating indicator on Refira Canvas.
|
|
2628
|
+
- \`refira push <slug> --expected-revision <hash>\` (or \`refira preview\`): Run deterministic harness check and stream updates to Refira Canvas.
|
|
2629
|
+
- \`refira annotations --page <slug>\`: Ingest open canvas reviewer comments and element selectors.
|
|
2630
|
+
- \`refira guide <topic>\`: Consult JIT interaction guidelines (aria, forms, modals, tables).
|
|
2631
|
+
- \`refira clean [--all]\`: Purge local agent scratch directories (\`.refira/work/\`).
|
|
2632
|
+
|
|
2633
|
+
---
|
|
2634
|
+
|
|
2635
|
+
## 4. Mandatory Delivery Invariant (Push to Canvas)
|
|
2636
|
+
- **Local HTML files are NOT the final deliverable.** Refira Canvas is the live source of truth.
|
|
2637
|
+
- **Strictly Prohibited:** Stopping, ending your response, or reporting completion to the user after only writing a local HTML file.
|
|
2638
|
+
- **Autonomous Execution:** You MUST execute \`refira push <slug> --expected-revision <hash>\` via your command-line execution tool. Do NOT ask the user to run it manually.
|
|
2639
|
+
- **Success Criteria:** A task is ONLY complete when \`refira push\` succeeds with exit code 0 and streams to Refira Canvas.
|
|
2142
2640
|
`;
|
|
2143
2641
|
}
|
|
2144
2642
|
|
|
@@ -2197,25 +2695,45 @@ When presenting features, data, or product capabilities:
|
|
|
2197
2695
|
|
|
2198
2696
|
## 5. Prototype Workflow in Refira
|
|
2199
2697
|
|
|
2200
|
-
1. Check current tokens: \`refira context\`
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2698
|
+
1. Check current tokens & layout focus: \`refira context\`
|
|
2699
|
+
- Inspect Active Project Layout Focus and existing page archetypes.
|
|
2700
|
+
- If the new page requires a different architectural archetype, ask user for confirmation and run \`refira layout select <slug>\`.
|
|
2701
|
+
2. Generate base page & trigger canvas pulse: \`refira scaffold --page <slug> --layout <slug>\`
|
|
2702
|
+
3. Fill layout using real reusable components (\`<x-...>\`) and Tailwind classes applying the aesthetic principles above.
|
|
2703
|
+
4. Stream to Refira Canvas: \`refira push <slug>.html --page <slug>\`
|
|
2704
|
+
5. Mandatory: Never stop after writing local files. Always execute \`refira push\` to complete the task.
|
|
2204
2705
|
`;
|
|
2205
2706
|
}
|
|
2206
2707
|
|
|
2207
2708
|
// src/commands/init.ts
|
|
2709
|
+
function appendSentinelIfPresent(filePath) {
|
|
2710
|
+
if (!fs3.existsSync(filePath)) {
|
|
2711
|
+
return false;
|
|
2712
|
+
}
|
|
2713
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
2714
|
+
if (!content.includes(REFIRA_SENTINEL_START)) {
|
|
2715
|
+
const separator = content.endsWith(`
|
|
2716
|
+
`) ? `
|
|
2717
|
+
` : `
|
|
2718
|
+
|
|
2719
|
+
`;
|
|
2720
|
+
fs3.writeFileSync(filePath, `${content}${separator}${REFIRA_SENTINEL_BLOCK}
|
|
2721
|
+
`, "utf-8");
|
|
2722
|
+
return true;
|
|
2723
|
+
}
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2208
2726
|
async function initCommand(opts) {
|
|
2209
2727
|
const config = loadConfig();
|
|
2210
2728
|
const apiUrl = opts.apiUrl ?? config.apiUrl;
|
|
2211
2729
|
const apiKey = opts.apiKey ?? config.apiKey;
|
|
2212
2730
|
const projectId = opts.projectId ?? config.projectId;
|
|
2213
2731
|
if (!apiKey) {
|
|
2214
|
-
console.error("
|
|
2732
|
+
console.error("Error: API key is required. Run `refira auth login --api-key <key>` or pass --api-key.");
|
|
2215
2733
|
process.exit(1);
|
|
2216
2734
|
}
|
|
2217
2735
|
if (!projectId) {
|
|
2218
|
-
console.error("
|
|
2736
|
+
console.error("Error: Project ID is required. Pass --project-id <uuid>");
|
|
2219
2737
|
process.exit(1);
|
|
2220
2738
|
}
|
|
2221
2739
|
console.log(`Initializing Refira workspace for project ${projectId}...`);
|
|
@@ -2224,36 +2742,124 @@ async function initCommand(opts) {
|
|
|
2224
2742
|
const data = await client.getProjectContext(projectId);
|
|
2225
2743
|
const projectName = data.context?.project_name ?? data.context?.project?.name ?? "Refira Project";
|
|
2226
2744
|
const fontFamily = extractFontFamily(data.context?.tokens?.typography, data.context?.project?.fontFamily ?? "Inter");
|
|
2227
|
-
const
|
|
2745
|
+
const pagesDir = path3.join(REFIRA_DIR, "pages");
|
|
2746
|
+
const workDir = path3.join(REFIRA_DIR, "work");
|
|
2747
|
+
fs3.mkdirSync(pagesDir, { recursive: true });
|
|
2748
|
+
fs3.mkdirSync(workDir, { recursive: true });
|
|
2749
|
+
const rulesContent = generateRefiraRules({
|
|
2228
2750
|
projectName,
|
|
2229
2751
|
projectId,
|
|
2230
2752
|
fontFamily,
|
|
2231
2753
|
apiUrl
|
|
2232
2754
|
});
|
|
2233
|
-
|
|
2234
|
-
console.log(" Created
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2755
|
+
fs3.writeFileSync(path3.join(REFIRA_DIR, "RULES.md"), rulesContent, "utf-8");
|
|
2756
|
+
console.log(" Created .refira/RULES.md");
|
|
2757
|
+
const updatedAgents = appendSentinelIfPresent("AGENTS.md");
|
|
2758
|
+
if (updatedAgents) {
|
|
2759
|
+
console.log(" Appended Refira sentinel link to AGENTS.md");
|
|
2760
|
+
}
|
|
2761
|
+
const updatedClaude = appendSentinelIfPresent("CLAUDE.md");
|
|
2762
|
+
if (updatedClaude) {
|
|
2763
|
+
console.log(" Appended Refira sentinel link to CLAUDE.md");
|
|
2764
|
+
}
|
|
2765
|
+
const skillDir = path3.join(".agents", "skills", "refira");
|
|
2766
|
+
fs3.mkdirSync(skillDir, { recursive: true });
|
|
2239
2767
|
const skillContent = generateRefiraSkill();
|
|
2240
|
-
|
|
2768
|
+
fs3.writeFileSync(path3.join(skillDir, "SKILL.md"), skillContent, "utf-8");
|
|
2241
2769
|
console.log(" Created .agents/skills/refira/SKILL.md");
|
|
2242
2770
|
saveConfig({ apiUrl, apiKey, projectId }, false);
|
|
2243
|
-
console.log(" Saved project configuration to .refirarc");
|
|
2771
|
+
console.log(" Saved project configuration to .refira/.refirarc");
|
|
2772
|
+
console.log(`
|
|
2773
|
+
Refira workspace initialized successfully.`);
|
|
2774
|
+
console.log(" Run `refira context` to inspect design tokens and layout templates.");
|
|
2775
|
+
console.log(" Run `refira scaffold --page <name>` to create your first page.");
|
|
2776
|
+
} catch (err) {
|
|
2777
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2778
|
+
console.error(`Initialization failed: ${msg}`);
|
|
2779
|
+
process.exit(1);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// src/commands/layout.ts
|
|
2784
|
+
async function layoutSelectCommand(slug, opts) {
|
|
2785
|
+
const config = loadConfig();
|
|
2786
|
+
const projectId = opts.projectId ?? config.projectId;
|
|
2787
|
+
if (!config.apiKey) {
|
|
2788
|
+
console.error("Error: API key required. Run `refira auth login --api-key <key>`.");
|
|
2789
|
+
process.exit(1);
|
|
2790
|
+
}
|
|
2791
|
+
if (!projectId) {
|
|
2792
|
+
console.error("Error: Project ID required. Run `refira init --project-id <id>` or pass --project-id.");
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
2796
|
+
try {
|
|
2797
|
+
const result = await client.selectActiveLayout(projectId, { layout_slug: slug });
|
|
2798
|
+
console.log(`
|
|
2799
|
+
Active layout focus selected successfully:`);
|
|
2800
|
+
console.log(` Project ID: ${result.project_id}`);
|
|
2801
|
+
console.log(` Active Layout: ${result.layout_name} [slug: ${result.layout_slug}]`);
|
|
2802
|
+
console.log(` Layout ID: ${result.active_layout_id}
|
|
2803
|
+
`);
|
|
2804
|
+
} catch (err) {
|
|
2805
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2806
|
+
console.error(`Failed to select active layout: ${msg}`);
|
|
2807
|
+
process.exit(1);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
async function layoutStatusCommand(opts) {
|
|
2811
|
+
const config = loadConfig();
|
|
2812
|
+
const projectId = opts.projectId ?? config.projectId;
|
|
2813
|
+
if (!config.apiKey) {
|
|
2814
|
+
console.error("Error: API key required. Run `refira auth login --api-key <key>`.");
|
|
2815
|
+
process.exit(1);
|
|
2816
|
+
}
|
|
2817
|
+
if (!projectId) {
|
|
2818
|
+
console.error("Error: Project ID required. Run `refira init --project-id <id>` or pass --project-id.");
|
|
2819
|
+
process.exit(1);
|
|
2820
|
+
}
|
|
2821
|
+
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
2822
|
+
try {
|
|
2823
|
+
const data = await client.getProjectContext(projectId);
|
|
2824
|
+
const projectName = data.context?.project_name ?? data.context?.project?.name ?? "Refira Project";
|
|
2825
|
+
const layouts = data.layouts ?? data.context?.layouts ?? [];
|
|
2826
|
+
const activeLayout = data.active_layout ?? data.context?.active_layout ?? layouts.find((l) => l.is_active) ?? layouts.find((l) => l.is_default) ?? null;
|
|
2827
|
+
console.log(`
|
|
2828
|
+
======================================================================`);
|
|
2829
|
+
console.log(`REFIRA PROJECT LAYOUT STATUS: ${projectName}`);
|
|
2830
|
+
console.log("======================================================================");
|
|
2831
|
+
console.log(`
|
|
2832
|
+
Active Project Layout Focus:`);
|
|
2833
|
+
if (activeLayout) {
|
|
2834
|
+
console.log(` Name: ${activeLayout.name}`);
|
|
2835
|
+
console.log(` Slug: ${activeLayout.slug}`);
|
|
2836
|
+
console.log(` ID: ${activeLayout.id}`);
|
|
2837
|
+
console.log(` Default: ${activeLayout.is_default ? "yes" : "no"}`);
|
|
2838
|
+
} else {
|
|
2839
|
+
console.log(" (None selected - using fallback or standalone)");
|
|
2840
|
+
}
|
|
2244
2841
|
console.log(`
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2842
|
+
Available Project Layouts (${layouts.length}):`);
|
|
2843
|
+
if (layouts.length === 0) {
|
|
2844
|
+
console.log(" (No layouts available)");
|
|
2845
|
+
} else {
|
|
2846
|
+
layouts.forEach((l, idx) => {
|
|
2847
|
+
const isActive = activeLayout?.id === l.id || l.is_active;
|
|
2848
|
+
console.log(` ${idx + 1}. ${l.name} [slug: ${l.slug}] (active: ${isActive ? "yes" : "no"}, default: ${l.is_default ? "yes" : "no"})`);
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
console.log(`======================================================================
|
|
2852
|
+
`);
|
|
2248
2853
|
} catch (err) {
|
|
2249
2854
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2250
|
-
console.error(
|
|
2855
|
+
console.error(`Failed to retrieve layout status: ${msg}`);
|
|
2251
2856
|
process.exit(1);
|
|
2252
2857
|
}
|
|
2253
2858
|
}
|
|
2254
2859
|
|
|
2255
2860
|
// src/commands/preview.ts
|
|
2256
|
-
import
|
|
2861
|
+
import fs4 from "node:fs";
|
|
2862
|
+
import path4 from "node:path";
|
|
2257
2863
|
|
|
2258
2864
|
// ../../node_modules/.bun/entities@4.5.0/node_modules/entities/lib/esm/generated/decode-data-html.js
|
|
2259
2865
|
var decode_data_html_default = new Uint16Array("ᵁ<Õıʊҝջאٵ۞ޢߖࠏઑඡ༉༦ረዡᐕᒝᓃᓟᔥ\x00\x00\x00\x00\x00\x00ᕫᛍᦍᰒᷝ↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀\uD835\uDD04rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀\uD835\uDD38plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀\uD835\uDC9Cign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀\uD835\uDD05pf;쀀\uD835\uDD39eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀\uD835\uDC9EpĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀\uD835\uDD07Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\x00\x00\x00͔͂\x00Ѕf;쀀\uD835\uDD3Bƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲϏϢϸontourIntegraìȹoɴ\x00\x00ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\x00\x00ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\x00ц\x00ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\x00ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀\uD835\uDC9Frok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀\uD835\uDD08rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\x00\x00ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀\uD835\uDD3Csilon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲy;䐤r;쀀\uD835\uDD09lledɓ֗\x00\x00֣mallSquare;旼erySmallSquare;斪Ͱֺ\x00ֿ\x00\x00ׄf;쀀\uD835\uDD3DAll;戀riertrf;愱còJTabcdfgorstרׯؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀\uD835\uDD0A;拙pf;쀀\uD835\uDD3Eeater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀\uD835\uDCA2;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\x00ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\x00ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀\uD835\uDD40a;䎙cr;愐ilde;䄨ǫޚ\x00ޞcy;䐆l耻Ï䃏ʀcfosuެ߂ߐĀiyޱrc;䄴;䐙r;쀀\uD835\uDD0Dpf;쀀\uD835\uDD41ǣ߇\x00ߌr;쀀\uD835\uDCA5rcy;䐈kcy;䐄HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶dil;䄶;䐚r;쀀\uD835\uDD0Epf;쀀\uD835\uDD42cr;쀀\uD835\uDCA6րJTaceflmostࠥࠩࠬࡐࡣসে্ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४ĀnrࢃgleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\x00ࣃbleBracket;柦nǔࣈ\x00࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀\uD835\uDD0FĀ;eঽা拘ftarrow;懚idot;䄿ƀnpwਖਛgȀLRlr৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀\uD835\uDD43erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼અઋp;椅y;䐜Ādl੯iumSpace;恟lintrf;愳r;쀀\uD835\uDD10nusPlus;戓pf;쀀\uD835\uDD44cò੶;䎜ҀJacefostuણધભીଔଙඑඞcy;䐊cute;䅃ƀaeyહાron;䅇dil;䅅;䐝ƀgswે૰ativeƀMTV૨ediumSpace;怋hiĀcn૦ëeryThiîtedĀGLଆreaterGreateòٳessLesóੈLine;䀊r;쀀\uD835\uDD11ȀBnptଢନଷreak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪௫ఄ಄ದൡඅ櫬Āoungruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater;EFGLSTஶஷ扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨setĀ;Eೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀\uD835\uDCA9ilde耻Ñ䃑;䎝܀Eacdfgmoprstuvලෂෛ෧ขภยา฿ไlig;䅒cute耻Ó䃓Āiyීrc耻Ô䃔;䐞blac;䅐r;쀀\uD835\uDD12rave耻Ò䃒ƀaei෮ෲcr;䅌ga;䎩cron;䎟pf;쀀\uD835\uDD46enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀\uD835\uDCAAash耻Ø䃘iŬืde耻Õ䃕es;樷ml耻Ö䃖erĀBP๋Āar๐๓r;怾acĀek๚;揞et;掴arenthesis;揜ҀacfhilorsງຊຏຒດຝະrtialD;戂y;䐟r;쀀\uD835\uDD13i;䎦;䎠usMinus;䂱Āipຢອncareplanåڝf;愙Ȁ;eio຺ູ檻cedesȀ;EST່້扺qual;檯lantEqual;扼ilde;找me;怳Ādpuct;戏ortionĀ;aȥl;戝Āci༁༆r;쀀\uD835\uDCAB;䎨ȀUfos༑༖༛༟OT耻\"䀢r;쀀\uD835\uDD14pf;愚cr;쀀\uD835\uDCACBEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻®䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r»ཹo;䎡ghtЀACDFTUVa࿁ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL憒ar;懥eftArrow;懄eiling;按oǵ\x00စbleBracket;柧nǔည\x00နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀\uD835\uDD16ortȀDLRUᄪᄴᄾᅉownArrow»ОeftArrow»࢚ightArrow»pArrow;憑gma;䎣allCircle;战pf;쀀\uD835\uDD4Aɲᅭ\x00\x00ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀\uD835\uDCAEar;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Tháྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et»ሓրHRSacfhiorsሾቄቕቱቶኟዂወዑORN耻Þ䃞ADE;愢ĀHcቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀\uD835\uDD17ĀeiቻDzኀ\x00ኇefore;戴a;䎘ĀcnኘkSpace;쀀 Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀\uD835\uDD4BipleDot;惛Āctዖዛr;쀀\uD835\uDCAFrok;䅦ૡዷጎጚጦ\x00ጬጱ\x00\x00\x00\x00\x00ጸጽ፷ᎅ\x00ᐄᐊᐐĀcrዻጁute耻Ú䃚rĀ;oጇገ憟cir;楉rǣጓ\x00y;䐎ve;䅬Āiyጞጣrc耻Û䃛;䐣blac;䅰r;쀀\uD835\uDD18rave耻Ù䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻on;䅲f;쀀\uD835\uDD4CЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥ownáϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀\uD835\uDCB0ilde;䅨ml耻Ü䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀\uD835\uDD19pf;쀀\uD835\uDD4Dcr;쀀\uD835\uDCB1dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀\uD835\uDD1Apf;쀀\uD835\uDD4Ecr;쀀\uD835\uDCB2Ȁfiosᓋᓐᓒᓘr;쀀\uD835\uDD1B;䎞pf;쀀\uD835\uDD4Fcr;쀀\uD835\uDCB3ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻Ý䃝Āiyᔉᔍrc;䅶;䐫r;쀀\uD835\uDD1Cpf;쀀\uD835\uDD50cr;쀀\uD835\uDCB4ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\x00ᕛoWidtèa;䎖r;愨pf;愤cr;쀀\uD835\uDCB5ᖃᖊᖐ\x00ᖰᖶᖿ\x00\x00\x00\x00ᗆᗛᗫᙟ᙭\x00ᚕ᚛ᚲᚹ\x00ᚾcute耻á䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻â䃢te肻´̆;䐰lig耻æ䃦Ā;r²ᖺ;쀀\uD835\uDD1Erave耻à䃠ĀepᗊᗖĀfpᗏᗔsym;愵èᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\x00\x00ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e»ᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢»¹arr;捼Āgpᙣᙧon;䄅f;쀀\uD835\uDD52;Eaeiopᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;eᚒñᚃing耻å䃥ƀctyᚡᚦᚨr;쀀\uD835\uDCB6;䀪mpĀ;eᚯñʈilde耻ã䃣ml耻ä䃤Āciᛂᛈoninôɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰᝃᝈ០៦ᠹᡐᜍ᥈ᥰot;櫭ĀcrᛶkȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e»ᜭrkĀ;tbrk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯᝳ;䎲;愶een;扬r;쀀\uD835\uDD1Fgcostuvwឍឝឳេ៕៛ƀaiuបពរðݠrc;旯p»፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\x00\x00ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄eåᑄåᒭarow;植ƀakoᠦᠵĀcn៲ᠣkƀlst֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘斴own;斾eft;旂ight;斸k;搣Ʊᠫ\x00ᠳƲᠯ\x00ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀\uD835\uDD53Ā;tᏋᡣom»Ꮜtie;拈DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ;敛;敘;攘;攔;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģbar耻¦䂦Ȁceioᥑᥖᥚᥠr;쀀\uD835\uDCB7mi;恏mĀ;elƀ;bhᥨᥩᥫ䁜;槅sub;柈ŬᥴlĀ;e怢t»pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\x00᧨ᨑᨕᨲ\x00ᨷᩐ\x00\x00᪴\x00\x00᫁\x00\x00ᬡᬮ᭒\x00᯽\x00ᰌƀcprᦲute;䄇̀;abcdsᦿᧀᧄ᧕᧙戩nd;橄rcup;橉Āau᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁îړȀaeiu᧰᧻ᨁᨅǰ᧵\x00᧸s;橍on;䄍dil耻ç䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻¸ƭptyv;榲t脀¢;eᨭᨮ䂢räƲr;쀀\uD835\uDD20ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark»ᩈ;䏇r;Ecefms᩠ᩢᩫ᪤᪪旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\x00\x00᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖»ཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it»᪼ˬ᫇\x00ᬊonĀ;eᫍᫎ䀺Ā;qÇÆɭ\x00\x00aĀ;t䀬;䁀ƀ;fl戁îᅠeĀmxent»eóɍǧ\x00ᬇĀ;dኻᬂot;橭nôɆƀfryᬐᬔᬗ;쀀\uD835\uDD54oäɔ脀©;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀\uD835\uDCB8Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯delprvw᭠᭬᭷ᮂᮬᯔarrĀlr᭨᭪;椸;椵ɰ᭲\x00\x00᭵r;拞c;拟arrĀ;pᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\x00\x00ᯒreã᭳uã᭵ee;拎edge;拏en耻¤䂤earrowĀlrᯮ᯳eft»ᮀight»ᮽeäᯝĀciᰁᰇoninôǷnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰻᰿ᱝᱩᱵᲞᲬᲷᴍᵻᶑᶫᶻ᷆᷍ròar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸òᄳhĀ;vᱚᱛ怐»ऊūᱡᱧarow;椏aã̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻°䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀\uD835\uDD21arĀlrᲳᲵ»ࣜ»သʀaegsv᳂᳖᳜᳠mƀ;oș᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀÷;o᳧ᳰntimes;拇nø᳷cy;䑒cɯᴆ\x00\x00ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀\uD835\uDD55ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedgåúnƀadhᄮᵝᵧownarrowóᲃarpoonĀlrᵲᵶefôᲴighôᲶŢᵿᶅkaro÷གɯᶊ\x00\x00ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀\uD835\uDCB9;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃ròЩaòྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄĀDoḆᴴoôĀcsḎḔute耻é䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻ê䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀\uD835\uDD22ƀ;rsṐṑṗ檚ave耻è䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et»ẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀\uD835\uDD56ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on»ớ;䏵ȀcsuvỪỳἋἣĀioữḱrc»Ḯɩỹ\x00\x00ỻíՈantĀglἂἆtr»ṝess»ṺƀaeiἒἚls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯oô͒ĀahὉὋ;䎷耻ð䃰Āmrὓὗl耻ë䃫o;悬ƀcipὡὤὧl;䀡sôծĀeoὬὴctatioîՙnentialåչৡᾒ\x00ᾞ\x00ᾡᾧ\x00\x00ῆῌ\x00ΐ\x00ῦῪ \x00 ⁚llingdotseñṄy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\x00\x00᾽g;耀ffig;耀ffl;쀀\uD835\uDD23lig;耀filig;쀀fjƀaltῙῡt;晭ig;耀flns;斱of;䆒ǰ΅\x00ῳf;쀀\uD835\uDD57ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao⁕Ācs‑⁒ႉ‸⁅⁈\x00⁐β•‥‧\x00耻½䂽;慓耻¼䂼;慕;慙;慛Ƴ‴\x00‶;慔;慖ʴ‾⁁\x00\x00⁃耻¾䂾;慗;慜5;慘ƶ⁌\x00⁎;慚;慝8;慞l;恄wn;挢cr;쀀\uD835\uDCBBࢀEabcdefgijlnorstv₂₉₥₰₴⃰℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽ƀ;qsؾٌlanô٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀\uD835\uDD24Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox»ℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀\uD835\uDD58Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqrⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\x00proør;楸qĀlqؿ↖lesó₈ií٫Āen↣↭rtneqq;쀀≩︀Å↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽ròΠȀilmr⇐⇔⇗⇛rsðᒄf»․ilôکĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it»∊lip;怦con;抹r;쀀\uD835\uDD25sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀\uD835\uDD59bar;怕ƀclt≯≴≸r;쀀\uD835\uDCBDasè⇴rok;䄧Ābp⊂⊇ull;恃hen»ᱛૡ⊣\x00⊪\x00⊸⋅⋎\x00⋕⋳\x00\x00⋸⌢⍧⍢⍿\x00⎆⎪⎴cute耻í䃭ƀ;iyݱ⊰⊵rc耻î䃮;䐸Ācx⊼⊿y;䐵cl耻¡䂡ĀfrΟ⋉;쀀\uD835\uDD26rave耻ì䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓inåގarôܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝doô⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙eróᕣã⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀\uD835\uDD5Aa;䎹uest耻¿䂿Āci⎊⎏r;쀀\uD835\uDCBEnʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\x00⎼cy;䑖l耻ï䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀\uD835\uDD27ath;䈷pf;쀀\uD835\uDD5Bǣ⏬\x00⏱r;쀀\uD835\uDCBFrcy;䑘kcy;䑔Ѐacfghjos␋␖␢ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀\uD835\uDD28reen;䄸cy;䑅cy;䑜pf;쀀\uD835\uDD5Ccr;쀀\uD835\uDCC0ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼ròòΕail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\x00⒪\x00⒱\x00\x00\x00\x00\x00⒵Ⓔ\x00ⓆⓈⓍ\x00⓹ute;䄺mptyv;榴raîࡌbda;䎻gƀ;dlࢎⓁⓃ;榑åࢎ;檅uo耻«䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝ë≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼ìࢰâ┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□aé⓶arpoonĀdu▯▴own»њp»०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoonóquigarro÷⇰hreetimes;拋ƀ;qs▋ও◺lanôবʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋pproøⓆot;拖qĀgq♃♅ôউgtò⒌ôছiíলƀilr♕࣡♚sht;楼;쀀\uD835\uDD29Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖rò◁orneòᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che»⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox»⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽rëࣁgƀlmr⛿✍✔eftĀar০✇ightá৲apsto;柼ightá৽parrowĀlr✥✩efô⓭ight;憬ƀafl✶✹✽r;榅;쀀\uD835\uDD5Dus;樭imes;樴š❋❏st;戗áፎƀ;ef❗❘᠀旊nge»❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇ròࢨorneòᶌarĀ;d➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀\uD835\uDCC1mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹reå◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀Å⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻¯䂯Āet⡗⡙;時Ā;e⡞⡟朠se»⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻owîҌefôएðᏑker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle»ᘦr;쀀\uD835\uDD2Ao;愧ƀcdn⢯⢴⣉ro耻µ䂵Ȁ;acdᑤ⢽⣀⣄sôᚧir;櫰ot肻·Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛ò−ðઁĀdp⣩⣮els;抧f;쀀\uD835\uDD5EĀct⣸⣽r;쀀\uD835\uDCC2pos»ᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la»˞ute;䅄g;쀀∠⃒ʀ;Eiop⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉roøurĀ;a⧓⧔普lĀ;s⧓ସdz⧟\x00⧣p肻 ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\x00⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸uiöୣĀei⩊⩎ar;椨íistĀ;sடr;쀀\uD835\uDD2BȀEest⩦⩹⩼ƀ;qs⩭ƀ;qs⩴lanôií௪Ā;rஶ⪁»ஷƀAap⪊⪍⪑rò⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚AEadest⪷⪺⪾⫂⫅⫶⫹rò⥦;쀀≦̸rr;憚r;急Ȁ;fqs⫎⫣⫯tĀar⫔⫙rro÷⫁ightarro÷⪐ƀ;qs⪺⫪lanôౕĀ;sౕ⫴»శiíౝĀ;rవ⫾iĀ;eచథiäඐĀpt⬌⬑f;쀀\uD835\uDD5F膀¬;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast⭕⭚⭟lleìl;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳uåಥĀ;cಘ⭸Ā;eಒ⭽ñಘȀAait⮈⮋⮝⮧rò⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow»⮕riĀ;eೋೖchimpqu⮽⯍⯙⬄⯤⯯Ȁ;cerല⯆ഷ⯉uå;쀀\uD835\uDCC3ortɭ⬅\x00\x00⯖ará⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭ååഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗñസȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇìௗlde耻ñ䃱çృiangleĀlrⱒⱜeftĀ;eచⱚñదightĀ;eೋⱥñĀ;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ⴭ\x00ⴸⵈⵠⵥⶄᬇ\x00\x00ⶍⶫ\x00ⷈⷎ\x00ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻ó䃳ĀiyⴼⵅrĀ;cⵂ耻ô䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācrir;榿;쀀\uD835\uDD2Cͯ\x00\x00\x00ⶂn;䋛ave耻ò䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕⶥⶨrò᪀Āirⶠr;榾oss;榻nå๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀\uD835\uDD60ƀaelⷔǒr;榷rp;榹;adiosvⷪⷫⷮ⸈⸍⸐⸖戨rò᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f»ⷿ耻ª䂪耻º䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧ò⸁ash耻ø䃸l;折iŬⸯ⸴de耻õ䃵esĀ;aǛ⸺s;樶ml耻ö䃶bar;挽ૡ\x00\x00⺀⺝\x00⺢⺹\x00\x00⻋ຜ\x00⼓\x00\x00⼫⾼\x00⿈rȀ;astЃ脀¶;l䂶leìЃɩ\x00\x00m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀\uD835\uDD2Dƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕maô੶ne;明ƀ;tv⺿⻀⻈䏀chfork»´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎ö⇴sҀ;abcdemst⻳ᤈ⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻±ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀\uD835\uDD61nd耻£䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷uå໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾pproø⽃urlyeñ໙ñ໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨iíໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺ð⽵ƀdfp⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t⾴ïrel;抰Āci⿀⿅r;쀀\uD835\uDCC5;䏈ncsp;怈̀fiopsu⋢⿱r;쀀\uD835\uDD2Epf;쀀\uD835\uDD62rime;恗cr;쀀\uD835\uDCC6ƀaeo⿸〉〓tĀei々rnionóڰnt;樖stĀ;e【】䀿ñἙô༔ABHabcdefhilmnoprstuxけさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがròႳòϝail;検aròᱥar;楤cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕iãᅮmptyv;榳gȀ;del࿑らるろ;榒;榥å࿑uo耻»䂻rր;abcfhlpstwガクシスゼゾダッデナp;極Ā;fゴs;椠;椳s;椞ë≝ð✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶aló༞ƀabrョリヮrò៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗ìâヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜnåႻarôྩt;断ƀilrㅩဣㅮsht;楽;쀀\uD835\uDD2FĀaoㅷㆆrĀduㅽㅿ»ѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘rrowĀ;tㆭaéトarpoonĀduㆻㆿowîㅾp»႒eftĀah㇊㇐rrowóarpoonóՑightarrows;應quigarro÷ニhreetimes;拌g;䋚ingdotseñἲƀahm㈍㈐㈓ròaòՑ;怏oustĀ;a㈞掱che»mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾rëဃƀafl㉇㉊㉎r;榆;쀀\uD835\uDD63us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒arò㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀\uD835\uDCC7Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠reåㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\x00㍺㎤\x00\x00㏬㏰\x00㐨㑈㑚㒭㒱㓊㓱\x00㘖\x00\x00㘳cute;䅛quï➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\x00㋼;檸on;䅡uåᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓iíሄ;䑁otƀ;be㌴ᵇ㌵担;橦Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒ë∨Ā;oਸ਼t耻§䂧i;䀻war;椩mĀin㍩ðnuóñt;朶rĀ;o㍶⁕쀀\uD835\uDD30Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\x00\x00㎜iäᑤaraì耻䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;qኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲aròᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetmé㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀\uD835\uDD64aĀdr㑍ЂesĀ;u㑔㑕晠it»㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍ñᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝ñᆮƀ;afᅻ㒦ְrť㒫ֱ»ᅼaròᅈȀcemt㒹㒾㓂㓅r;쀀\uD835\uDCC8tmîñiì㐕aræᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psiloîỠhé⺯s»⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦pproø㋺urlyeñᇾñᇳƀaes㖂㖈㌛pproø㌚qñ㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻¹䂹耻²䂲耻³䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨ë∮Ā;oਫwar;椪lig耻ß䃟㙑㙝㙠ዎ㙳㙹\x00㙾㛂\x00\x00\x00\x00\x00㛛㜃\x00㜉㝬\x00\x00\x00㞇ɲ㙖\x00\x00㙛get;挖;䏄rëƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀\uD835\uDD31Ȁeiko㚆㚝㚵㚼Dz㚋\x00㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮pproøim»ኬsðኞĀas㚺㚮ðrn耻þ䃾Ǭ̟㛆⋧es膀×;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀á⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀\uD835\uDD65rk;櫚á㍢rime;怴ƀaip㜏㜒㝤dåቈadempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own»ᶻeftĀ;e⠀㜾ñम;扜ightĀ;e㊪㝋ñၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀\uD835\uDCC9;䑆cy;䑛rok;䅧Āio㞋㞎xôheadĀlr㞗㞠eftarro÷ࡏightarrow»ཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶ròϭar;楣Ācr㟜㟢ute耻ú䃺òᅐrǣ㟪\x00㟭y;䑞ve;䅭Āiy㟵㟺rc耻û䃻;䑃ƀabh㠃㠆㠋ròᎭlac;䅱aòᏃĀir㠓㠘sht;楾;쀀\uD835\uDD32rave耻ù䃹š㠧㠱rĀlr㠬㠮»ॗ»ႃlk;斀Āct㠹㡍ɯ㠿\x00\x00㡊rnĀ;e㡅㡆挜r»㡆op;挏ri;旸Āal㡖㡚cr;䅫肻¨͉Āgp㡢㡦on;䅳f;쀀\uD835\uDD66̀adhlsuᅋ㡸㡽፲㢑㢠ownáᎳarpoonĀlr㢈㢌efô㠭ighô㠯iƀ;hl㢙㢚㢜䏅»ᏺon»㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\x00\x00㣁rnĀ;e㢼㢽挝r»㢽op;挎ng;䅯ri;旹cr;쀀\uD835\uDCCAƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨»᠓Āam㣯㣲rò㢨l耻ü䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠ròϷarĀ;v㤦㤧櫨;櫩asèϡĀnr㤲㤷grt;榜eknprst㓣㥆㥋㥒㥝㥤㦖appá␕othinçẖƀhir㓫⻈㥙opô⾵Ā;hᎷ㥢ïㆍĀiu㥩㥭gmá㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟etá㚜iangleĀlr㦪㦯eft»थight»ၑy;䐲ash»ံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨaòᑩr;쀀\uD835\uDD33tré㦮suĀbp㧯㧱»ജ»൙pf;쀀\uD835\uDD67roðtré㦴Ācu㨆㨋r;쀀\uD835\uDCCBĀbp㨐㨘nĀEe㦀㨖»㥾nĀEe㦒㨞»㦐igzag;榚cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀\uD835\uDD34pf;쀀\uD835\uDD68Ā;eᑹ㩦atèᑹcr;쀀\uD835\uDCCCૣណ㪇\x00㪋\x00㪐㪛\x00\x00㪝㪨㪫㪯\x00\x00㫃㫎\x00㫘ៜtré៑r;쀀\uD835\uDD35ĀAa㪔㪗ròσrò৶;䎾ĀAa㪡㪤ròθrò৫að✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀\uD835\uDD69imåឲĀAa㫇㫊ròώròਁĀcq㫒ីr;쀀\uD835\uDCCDĀpt៖㫜ré។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻ý䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻¥䂥r;쀀\uD835\uDD36cy;䑗pf;쀀\uD835\uDD6Acr;쀀\uD835\uDCCEĀcm㬦㬩y;䑎l耻ÿ䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡træᕟa;䎶r;쀀\uD835\uDD37cy;䐶grarr;懝pf;쀀\uD835\uDD6Bcr;쀀\uD835\uDCCFĀjn㮅㮇;怍j;怌".split("").map((c) => c.charCodeAt(0)));
|
|
@@ -3633,12 +4239,14 @@ function checkTagName(name, violations) {
|
|
|
3633
4239
|
}
|
|
3634
4240
|
function checkAttributes(name, attribs, violations) {
|
|
3635
4241
|
for (const [attrKey, attrVal] of Object.entries(attribs)) {
|
|
3636
|
-
|
|
4242
|
+
const lowerAttr = attrKey.toLowerCase();
|
|
4243
|
+
const lowerTag = name.toLowerCase();
|
|
4244
|
+
if (lowerAttr === "classname") {
|
|
3637
4245
|
violations.push({
|
|
3638
4246
|
type: "JSX_ATTRIBUTE",
|
|
3639
|
-
target:
|
|
3640
|
-
message: `Prohibited JSX attribute '
|
|
3641
|
-
remediation: `Change '
|
|
4247
|
+
target: attrKey,
|
|
4248
|
+
message: `Prohibited JSX attribute '${attrKey}' detected on <${name}>.`,
|
|
4249
|
+
remediation: `Change '${attrKey}' to the standard HTML 'class' attribute.`
|
|
3642
4250
|
});
|
|
3643
4251
|
}
|
|
3644
4252
|
if (/^on[A-Z]/.test(attrKey) || attrKey.startsWith("@") || attrKey.startsWith("v-") || attrKey.startsWith("*")) {
|
|
@@ -3649,15 +4257,52 @@ function checkAttributes(name, attribs, violations) {
|
|
|
3649
4257
|
remediation: "Remove framework event bindings. Use vanilla HTML and minimal vanilla JavaScript if needed."
|
|
3650
4258
|
});
|
|
3651
4259
|
}
|
|
3652
|
-
if (
|
|
4260
|
+
if (lowerAttr.startsWith("on") && (attrVal.includes("location") || attrVal.includes("window.open") || attrVal.includes("history."))) {
|
|
4261
|
+
violations.push({
|
|
4262
|
+
type: "NAVIGATION_PROHIBITED",
|
|
4263
|
+
target: `${attrKey}="${attrVal}"`,
|
|
4264
|
+
message: `Inline navigation script '${attrVal}' detected on <${name}>.`,
|
|
4265
|
+
remediation: "Remove inline navigation scripts. Prototypes run in a sandboxed canvas and cannot navigate across pages."
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
if (lowerTag === "a") {
|
|
4269
|
+
if (lowerAttr === "href") {
|
|
4270
|
+
const trimmed = attrVal.trim();
|
|
4271
|
+
const isSafeHash = trimmed.startsWith("#");
|
|
4272
|
+
const isSafeVoid = trimmed === "javascript:void(0)" || trimmed === "javascript:;";
|
|
4273
|
+
if (!isSafeHash && !isSafeVoid) {
|
|
4274
|
+
violations.push({
|
|
4275
|
+
type: "NAVIGATION_PROHIBITED",
|
|
4276
|
+
target: `${attrKey}="${attrVal}"`,
|
|
4277
|
+
message: `Cross-page or external navigation '<a ${attrKey}="${attrVal}">' is strictly prohibited.`,
|
|
4278
|
+
remediation: 'Refira prototypes run in a sandboxed iframe. Anchor links must point to in-page section hashes (e.g. href="#features") or placeholder (href="#"). If you need action triggers, use <button type="button">.'
|
|
4279
|
+
});
|
|
4280
|
+
} else if (trimmed.includes("location") || trimmed.includes("window.open")) {
|
|
4281
|
+
violations.push({
|
|
4282
|
+
type: "NAVIGATION_PROHIBITED",
|
|
4283
|
+
target: `${attrKey}="${attrVal}"`,
|
|
4284
|
+
message: `Navigation statement detected inside href: '${attrVal}'.`,
|
|
4285
|
+
remediation: "Remove navigation statements from href."
|
|
4286
|
+
});
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
if (lowerAttr === "target" && attrVal.trim().length > 0) {
|
|
4290
|
+
violations.push({
|
|
4291
|
+
type: "NAVIGATION_PROHIBITED",
|
|
4292
|
+
target: `${attrKey}="${attrVal}"`,
|
|
4293
|
+
message: `Target attribute '${attrKey}="${attrVal}"' on <a> is prohibited.`,
|
|
4294
|
+
remediation: "Remove the target attribute. Prototypes must remain within the Refira Canvas viewport."
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
if (lowerTag === "form" && lowerAttr === "action") {
|
|
3653
4299
|
const trimmed = attrVal.trim();
|
|
3654
|
-
|
|
3655
|
-
if (!isInternal && trimmed.length > 0) {
|
|
4300
|
+
if (trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("javascript:void")) {
|
|
3656
4301
|
violations.push({
|
|
3657
4302
|
type: "NAVIGATION_PROHIBITED",
|
|
3658
|
-
target:
|
|
3659
|
-
message: `
|
|
3660
|
-
remediation:
|
|
4303
|
+
target: `${attrKey}="${attrVal}"`,
|
|
4304
|
+
message: `Form action '${attrVal}' is prohibited in standalone prototypes.`,
|
|
4305
|
+
remediation: 'Remove or replace form action with action="#" or handle submission client-side without page reload.'
|
|
3661
4306
|
});
|
|
3662
4307
|
}
|
|
3663
4308
|
}
|
|
@@ -3747,7 +4392,7 @@ function formatHarnessErrorReport(violations, filePath) {
|
|
|
3747
4392
|
const lines = [
|
|
3748
4393
|
"",
|
|
3749
4394
|
"======================================================================",
|
|
3750
|
-
"
|
|
4395
|
+
"[REFIRA HARNESS] VALIDATION FAILED — EXIT CODE 1",
|
|
3751
4396
|
"======================================================================",
|
|
3752
4397
|
`Target File: ${filePath}`,
|
|
3753
4398
|
`Total Violations Found: ${violations.length}`,
|
|
@@ -3764,7 +4409,7 @@ function formatHarnessErrorReport(violations, filePath) {
|
|
|
3764
4409
|
lines.push("");
|
|
3765
4410
|
});
|
|
3766
4411
|
lines.push("======================================================================");
|
|
3767
|
-
lines.push("
|
|
4412
|
+
lines.push("ACTION REQUIRED FOR AI AGENT:");
|
|
3768
4413
|
lines.push("1. Review the remediation instructions above for each violation.");
|
|
3769
4414
|
lines.push("2. Edit the HTML file to eliminate prohibited framework syntax, emojis, or external links.");
|
|
3770
4415
|
lines.push("3. Re-run `refira preview <file.html> --page <page>` until validation succeeds.");
|
|
@@ -3777,12 +4422,12 @@ function formatHarnessSuccessReport(pageSlug, previewUrl) {
|
|
|
3777
4422
|
const lines = [
|
|
3778
4423
|
"",
|
|
3779
4424
|
"======================================================================",
|
|
3780
|
-
"
|
|
4425
|
+
"[REFIRA HARNESS] VALIDATION PASSED — EXIT CODE 0",
|
|
3781
4426
|
"======================================================================",
|
|
3782
4427
|
`Page '${pageSlug}' markup conforms to Refira HTML5 + Tailwind invariants.`
|
|
3783
4428
|
];
|
|
3784
4429
|
if (previewUrl) {
|
|
3785
|
-
lines.push(
|
|
4430
|
+
lines.push(`Live Preview Updated: ${previewUrl}`);
|
|
3786
4431
|
}
|
|
3787
4432
|
lines.push("======================================================================");
|
|
3788
4433
|
lines.push("");
|
|
@@ -3791,45 +4436,73 @@ function formatHarnessSuccessReport(pageSlug, previewUrl) {
|
|
|
3791
4436
|
}
|
|
3792
4437
|
|
|
3793
4438
|
// src/commands/preview.ts
|
|
3794
|
-
|
|
3795
|
-
|
|
4439
|
+
function resolveTargetFile(fileArg, pageSlug) {
|
|
4440
|
+
if (fileArg && fs4.existsSync(fileArg)) {
|
|
4441
|
+
return fileArg;
|
|
4442
|
+
}
|
|
4443
|
+
if (fileArg && fs4.existsSync(path4.join(REFIRA_DIR, "pages", fileArg))) {
|
|
4444
|
+
return path4.join(REFIRA_DIR, "pages", fileArg);
|
|
4445
|
+
}
|
|
4446
|
+
if (fileArg && fs4.existsSync(path4.join(REFIRA_DIR, "pages", `${fileArg}.html`))) {
|
|
4447
|
+
return path4.join(REFIRA_DIR, "pages", `${fileArg}.html`);
|
|
4448
|
+
}
|
|
4449
|
+
if (pageSlug) {
|
|
4450
|
+
const refiraPage = path4.join(REFIRA_DIR, "pages", `${pageSlug}.html`);
|
|
4451
|
+
if (fs4.existsSync(refiraPage))
|
|
4452
|
+
return refiraPage;
|
|
4453
|
+
const rootPage = `${pageSlug}.html`;
|
|
4454
|
+
if (fs4.existsSync(rootPage))
|
|
4455
|
+
return rootPage;
|
|
4456
|
+
}
|
|
4457
|
+
return null;
|
|
4458
|
+
}
|
|
4459
|
+
async function previewCommand(fileArg, opts) {
|
|
4460
|
+
const pageSlug = opts?.page?.trim().toLowerCase() ?? (fileArg ? path4.basename(fileArg, ".html").toLowerCase() : undefined);
|
|
3796
4461
|
if (!pageSlug) {
|
|
3797
|
-
console.error("
|
|
4462
|
+
console.error("Error: Target page is required. Specify <file> or --page <slug>.");
|
|
3798
4463
|
process.exit(1);
|
|
3799
4464
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
4465
|
+
const resolvedPath = resolveTargetFile(fileArg, pageSlug);
|
|
4466
|
+
if (!resolvedPath) {
|
|
4467
|
+
console.error(`Error: File for page '${pageSlug}' not found. Looked in '${path4.join(REFIRA_DIR, "pages", `${pageSlug}.html`)}'.`);
|
|
3802
4468
|
process.exit(1);
|
|
3803
4469
|
}
|
|
3804
|
-
const content =
|
|
3805
|
-
console.log(
|
|
4470
|
+
const content = fs4.readFileSync(resolvedPath, "utf-8");
|
|
4471
|
+
console.log(`Inspecting '${resolvedPath}' with Refira Agent Harness...`);
|
|
3806
4472
|
const validation = validateHtmlMarkup(content);
|
|
3807
4473
|
if (!validation.valid) {
|
|
3808
|
-
const errorReport = formatHarnessErrorReport(validation.violations,
|
|
4474
|
+
const errorReport = formatHarnessErrorReport(validation.violations, resolvedPath);
|
|
3809
4475
|
console.error(errorReport);
|
|
3810
4476
|
process.exit(1);
|
|
3811
4477
|
}
|
|
3812
4478
|
const config = loadConfig();
|
|
3813
4479
|
if (!config.apiKey || !config.projectId) {
|
|
3814
4480
|
console.log(formatHarnessSuccessReport(pageSlug));
|
|
3815
|
-
console.log("
|
|
4481
|
+
console.log("Offline mode: Markup is valid. To stream live preview to Canvas, run `refira auth login`.");
|
|
3816
4482
|
process.exit(0);
|
|
3817
4483
|
}
|
|
3818
|
-
console.log(
|
|
4484
|
+
console.log(`Harness passed! Streaming '${pageSlug}' to Refira Canvas...`);
|
|
3819
4485
|
const client = new CliApiClient(config.apiUrl, config.apiKey);
|
|
3820
4486
|
try {
|
|
3821
|
-
const result = await client.pushPreview(config.projectId, pageSlug, content);
|
|
4487
|
+
const result = await client.pushPreview(config.projectId, pageSlug, content, opts?.expectedRevision);
|
|
3822
4488
|
console.log(formatHarnessSuccessReport(pageSlug, result.preview_url));
|
|
3823
4489
|
process.exit(0);
|
|
3824
4490
|
} catch (err) {
|
|
4491
|
+
const isConflict = err?.code === "REVISION_CONFLICT" || err instanceof Error && err.message.includes("modified concurrently");
|
|
4492
|
+
if (isConflict) {
|
|
4493
|
+
console.error(`Error: Page '${pageSlug}' has been modified concurrently (REVISION_CONFLICT).`);
|
|
4494
|
+
console.error("Please fetch the latest revision with `refira context` and re-apply changes.");
|
|
4495
|
+
process.exit(1);
|
|
4496
|
+
}
|
|
3825
4497
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3826
|
-
console.error(
|
|
4498
|
+
console.error(`Failed to stream preview to Refira: ${msg}`);
|
|
3827
4499
|
process.exit(1);
|
|
3828
4500
|
}
|
|
3829
4501
|
}
|
|
3830
4502
|
|
|
3831
4503
|
// src/commands/scaffold.ts
|
|
3832
|
-
import
|
|
4504
|
+
import fs5 from "node:fs";
|
|
4505
|
+
import path5 from "node:path";
|
|
3833
4506
|
|
|
3834
4507
|
// src/templates/scaffold-template.ts
|
|
3835
4508
|
function generateScaffoldHtml(opts) {
|
|
@@ -3903,78 +4576,135 @@ function generateScaffoldHtml(opts) {
|
|
|
3903
4576
|
</html>
|
|
3904
4577
|
`;
|
|
3905
4578
|
}
|
|
4579
|
+
function generateLayoutScaffoldHtml(opts) {
|
|
4580
|
+
const macroNotice = opts.componentMacros && opts.componentMacros.length > 0 ? `
|
|
4581
|
+
<!-- Available component macros: ${opts.componentMacros.map((m) => `<x-${m} />`).join(", ")} -->` : "";
|
|
4582
|
+
return `<!-- ====================================================================== -->
|
|
4583
|
+
<!-- [REFIRA LAYOUT BINDING: ${opts.layoutSlug}]
|
|
4584
|
+
<!-- Project: ${opts.projectName} | Page: ${opts.pageName}
|
|
4585
|
+
<!-- AI Agent: This page is bound to shared layout '${opts.layoutSlug}'.
|
|
4586
|
+
<!-- Write ONLY the inner UI content for slot {{ content }}.
|
|
4587
|
+
<!-- Do NOT write standard document tags (doctype, html, head, body).${macroNotice}
|
|
4588
|
+
<!-- ====================================================================== -->
|
|
4589
|
+
|
|
4590
|
+
<!-- [REFIRA CANVAS SLOT: START] -->
|
|
4591
|
+
<div class="space-y-6">
|
|
4592
|
+
<!-- Page Header -->
|
|
4593
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-6 border-b border-slate-200">
|
|
4594
|
+
<div>
|
|
4595
|
+
<h1 class="text-2xl font-bold tracking-tight text-slate-900">${opts.pageName}</h1>
|
|
4596
|
+
<p class="text-sm text-slate-500 mt-1">Start crafting the content for ${opts.pageName.toLowerCase()}.</p>
|
|
4597
|
+
</div>
|
|
4598
|
+
<div class="flex items-center gap-3">
|
|
4599
|
+
<button type="button" class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 transition">
|
|
4600
|
+
Action
|
|
4601
|
+
</button>
|
|
4602
|
+
</div>
|
|
4603
|
+
</div>
|
|
4604
|
+
|
|
4605
|
+
<!-- Content Section -->
|
|
4606
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
4607
|
+
<div class="p-6 bg-white rounded-xl border border-slate-200 shadow-sm space-y-2">
|
|
4608
|
+
<p class="text-sm font-medium text-slate-500">Overview</p>
|
|
4609
|
+
<p class="text-2xl font-bold text-slate-900">${opts.pageName}</p>
|
|
4610
|
+
</div>
|
|
4611
|
+
</div>
|
|
4612
|
+
</div>
|
|
4613
|
+
<!-- [REFIRA CANVAS SLOT: END] -->
|
|
4614
|
+
`;
|
|
4615
|
+
}
|
|
3906
4616
|
|
|
3907
4617
|
// src/commands/scaffold.ts
|
|
4618
|
+
async function resolveProjectDetails(apiUrl, apiKey, projectId, pageSlug, layoutSlug) {
|
|
4619
|
+
const client = new CliApiClient(apiUrl, apiKey);
|
|
4620
|
+
const data = await client.getProjectContext(projectId);
|
|
4621
|
+
const projectName = data.context?.project_name ?? data.context?.project?.name ?? "Refira Project";
|
|
4622
|
+
const fontFamily = extractFontFamily(data.context?.tokens?.typography, data.context?.project?.fontFamily ?? "Inter");
|
|
4623
|
+
let colorTokens;
|
|
4624
|
+
const colors = data.context?.tokens?.colors ?? data.context?.tokens?.color;
|
|
4625
|
+
if (colors) {
|
|
4626
|
+
const parsed = {};
|
|
4627
|
+
for (const [k, v] of Object.entries(colors)) {
|
|
4628
|
+
parsed[k] = typeof v === "string" ? v : v?.hex ?? v?.oklch ?? "";
|
|
4629
|
+
}
|
|
4630
|
+
colorTokens = parsed;
|
|
4631
|
+
}
|
|
4632
|
+
const rawComponents = data.context?.components ?? data.components ?? [];
|
|
4633
|
+
const componentMacros = rawComponents.map((c) => c.name);
|
|
4634
|
+
try {
|
|
4635
|
+
await client.startGenerating(projectId, pageSlug, layoutSlug);
|
|
4636
|
+
console.log(`Activated live generation indicator on Refira Canvas for '${pageSlug}'`);
|
|
4637
|
+
} catch {}
|
|
4638
|
+
return { projectName, fontFamily, colorTokens, componentMacros };
|
|
4639
|
+
}
|
|
3908
4640
|
async function scaffoldCommand(opts) {
|
|
3909
4641
|
const pageSlug = opts.page.trim().toLowerCase();
|
|
3910
4642
|
if (!pageSlug) {
|
|
3911
|
-
console.error("
|
|
4643
|
+
console.error("Error: --page <slug> is required.");
|
|
3912
4644
|
process.exit(1);
|
|
3913
4645
|
}
|
|
4646
|
+
const layoutSlug = opts.layout?.trim().toLowerCase();
|
|
3914
4647
|
const config = loadConfig();
|
|
3915
|
-
let
|
|
3916
|
-
|
|
3917
|
-
|
|
4648
|
+
let details = {
|
|
4649
|
+
projectName: "Refira Project",
|
|
4650
|
+
fontFamily: "Inter",
|
|
4651
|
+
colorTokens: undefined,
|
|
4652
|
+
componentMacros: undefined
|
|
4653
|
+
};
|
|
3918
4654
|
if (config.apiKey && config.projectId) {
|
|
3919
4655
|
try {
|
|
3920
|
-
|
|
3921
|
-
const data = await client.getProjectContext(config.projectId);
|
|
3922
|
-
projectName = data.context?.project_name ?? data.context?.project?.name ?? projectName;
|
|
3923
|
-
fontFamily = extractFontFamily(data.context?.tokens?.typography, data.context?.project?.fontFamily ?? fontFamily);
|
|
3924
|
-
const colors = data.context?.tokens?.colors ?? data.context?.tokens?.color;
|
|
3925
|
-
if (colors) {
|
|
3926
|
-
const parsed = {};
|
|
3927
|
-
for (const [k, v] of Object.entries(colors)) {
|
|
3928
|
-
parsed[k] = typeof v === "string" ? v : v?.hex ?? v?.oklch ?? "";
|
|
3929
|
-
}
|
|
3930
|
-
colorTokens = parsed;
|
|
3931
|
-
}
|
|
4656
|
+
details = await resolveProjectDetails(config.apiUrl, config.apiKey, config.projectId, pageSlug, layoutSlug);
|
|
3932
4657
|
} catch {}
|
|
3933
4658
|
}
|
|
3934
4659
|
const pageName = pageSlug.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
3935
|
-
const html =
|
|
4660
|
+
const html = layoutSlug ? generateLayoutScaffoldHtml({
|
|
3936
4661
|
pageName,
|
|
3937
|
-
projectName,
|
|
3938
|
-
|
|
3939
|
-
|
|
4662
|
+
projectName: details.projectName,
|
|
4663
|
+
layoutSlug,
|
|
4664
|
+
componentMacros: details.componentMacros
|
|
4665
|
+
}) : generateScaffoldHtml({
|
|
4666
|
+
pageName,
|
|
4667
|
+
projectName: details.projectName,
|
|
4668
|
+
fontFamily: details.fontFamily,
|
|
4669
|
+
colorTokens: details.colorTokens
|
|
3940
4670
|
});
|
|
3941
|
-
const
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
console.log(`
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
async function skillInstallCommand(opts) {
|
|
3956
|
-
const baseDir = opts.global ? path3.join(os2.homedir(), ".agents", "skills", "refira") : path3.join(process.cwd(), ".agents", "skills", "refira");
|
|
3957
|
-
fs5.mkdirSync(baseDir, { recursive: true });
|
|
3958
|
-
const targetFile = path3.join(baseDir, "SKILL.md");
|
|
3959
|
-
const content = generateRefiraSkill();
|
|
3960
|
-
fs5.writeFileSync(targetFile, content, "utf-8");
|
|
3961
|
-
console.log("✅ Refira Design Craftsmanship Skill installed successfully!");
|
|
3962
|
-
console.log(` Location: ${targetFile}`);
|
|
3963
|
-
console.log(` Scope: ${opts.global ? "Global (~/.agents/skills/refira)" : "Local (.agents/skills/refira)"}`);
|
|
4671
|
+
const defaultDir = path5.join(REFIRA_DIR, "pages");
|
|
4672
|
+
const targetPath = opts.output ?? path5.join(defaultDir, `${pageSlug}.html`);
|
|
4673
|
+
const targetDir = path5.dirname(targetPath);
|
|
4674
|
+
if (!fs5.existsSync(targetDir)) {
|
|
4675
|
+
fs5.mkdirSync(targetDir, { recursive: true });
|
|
4676
|
+
}
|
|
4677
|
+
fs5.writeFileSync(targetPath, html, "utf-8");
|
|
4678
|
+
console.log(`Scaffolded starter page '${targetPath}' successfully.`);
|
|
4679
|
+
if (layoutSlug) {
|
|
4680
|
+
console.log(` Layout: ${layoutSlug} (slot fragment)`);
|
|
4681
|
+
} else {
|
|
4682
|
+
console.log(` Font: ${details.fontFamily}`);
|
|
4683
|
+
}
|
|
4684
|
+
console.log(" Slot: <!-- [REFIRA CANVAS SLOT: START] -->");
|
|
3964
4685
|
console.log(`
|
|
3965
|
-
|
|
4686
|
+
Mandatory Agent Completion Gate:`);
|
|
4687
|
+
console.log(` 1. Write your UI layout within the designated slot in '${targetPath}'.`);
|
|
4688
|
+
console.log(` 2. You MUST execute: refira push ${pageSlug}`);
|
|
4689
|
+
console.log(" DO NOT declare completion until `refira push` succeeds.");
|
|
3966
4690
|
}
|
|
3967
4691
|
|
|
3968
4692
|
// src/index.ts
|
|
3969
4693
|
var program2 = new Command2;
|
|
3970
|
-
program2.name("refira").description("Refira CLI tool and AI Agent Harness for deterministic prototype generation").version("0.
|
|
4694
|
+
program2.name("refira").description("Refira CLI tool and AI Agent Harness for deterministic prototype generation").version("0.2.0");
|
|
3971
4695
|
var auth = program2.command("auth").description("Manage Refira API authentication and sessions");
|
|
3972
4696
|
auth.command("login").description("Login to Refira using an API key").option("--api-url <url>", "Refira backend API base URL", "http://localhost:3001").option("--api-key <key>", "Project API key (rfr_...)").option("-g, --global", "Save credentials globally in user home directory", false).action(loginCommand);
|
|
3973
4697
|
auth.command("status").description("Verify and display current Refira authentication session").action(statusCommand);
|
|
3974
|
-
program2.command("init").description("Initialize a Refira project workspace
|
|
4698
|
+
program2.command("init").description("Initialize a Refira project workspace (.refira/RULES.md, pages/, and skills)").requiredOption("--project-id <id>", "Target project UUID").option("--api-url <url>", "Refira backend API base URL").option("--api-key <key>", "Project API key (rfr_...)").action(initCommand);
|
|
3975
4699
|
program2.command("context").description("Inspect design tokens, color palette, typography, and page roster").option("--project-id <id>", "Project UUID").action(contextCommand);
|
|
3976
|
-
program2.command("scaffold").description("Generate a clean HTML5 + Tailwind starter template for a page").requiredOption("--page <slug>", "Page slug (e.g. checkout, dashboard, landing)").option("--output <path>", "Destination file path").action(scaffoldCommand);
|
|
3977
|
-
program2.command("preview").description("Inspect HTML markup with Agent Harness and stream to Refira Canvas").argument("
|
|
4700
|
+
program2.command("scaffold").description("Generate a clean HTML5 + Tailwind starter template for a page").requiredOption("--page <slug>", "Page slug (e.g. checkout, dashboard, landing)").option("--layout <slug>", "Bind page to an existing shared layout").option("--output <path>", "Destination file path").action(scaffoldCommand);
|
|
4701
|
+
program2.command("preview").alias("push").description("Inspect HTML markup with Agent Harness and stream to Refira Canvas").argument("[file]", "Path to HTML file to preview").option("--page <slug>", "Target page slug").option("--expected-revision <hash>", "Expected SHA-256 revision hash for OCC concurrency guard").action((file, options) => previewCommand(file, options));
|
|
4702
|
+
program2.command("annotations").description("Fetch open canvas annotations to guide AI agent page revisions").requiredOption("--page <slug>", "Target page slug or ID").option("--format <format>", "Output format (table|json|md)", "table").action(annotationsCommand);
|
|
4703
|
+
program2.command("guide").description("Display JIT interaction pattern guidance (aria, forms, modals, tables)").argument("<topic>", "Interaction topic (aria, forms, modals, tables)").action(guideCommand);
|
|
4704
|
+
program2.command("clean").description("Clean up local agent work directories and scratch files").option("--all", "Also invoke server system clean to purge expired tokens and export bundles", false).option("--dry-run", "List targets to be removed without deleting", false).action(cleanCommand);
|
|
3978
4705
|
var skill = program2.command("skill").description("Manage Refira Agent Skills");
|
|
3979
|
-
skill.command("install").description("Install the Refira design craftsmanship skill (.agents/skills/refira/SKILL.md)").option("-g, --global", "Install globally into user home directory (~/.agents/skills/refira)", false)
|
|
4706
|
+
skill.command("install").description("Install the Refira design craftsmanship skill (.agents/skills/refira/SKILL.md)").option("-g, --global", "Install globally into user home directory (~/.agents/skills/refira)", false);
|
|
4707
|
+
var layout = program2.command("layout").description("Inspect and select active project layout focus");
|
|
4708
|
+
layout.command("select").description("Set active project layout focus by slug").argument("<slug>", "Layout slug (e.g. dashboard, landing, auth)").option("--project-id <id>", "Target project UUID").action((slug, options) => layoutSelectCommand(slug, options));
|
|
4709
|
+
layout.command("status").description("Display current active layout focus and available project layouts").option("--project-id <id>", "Target project UUID").action(layoutStatusCommand);
|
|
3980
4710
|
program2.parse();
|