mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
fix(skill): target local files for detect, never a URL (#159)
Rework context-signals' detect target after review: a URL meant a costly Puppeteer render (and a probed port might not even be this project), and the index.html-or-bail fallback failed most real apps (no root index.html). New priority: (1) the scannable markup/style files in the dirty git tree (what the user is working on, small and local); (2) a local source dir (src / app / components / pages / public — the detector walks these and skips node_modules / dist / build); (3) a root index.html, else the project root as a last resort when there's code. Emits `scan.targets` (a list) + `scan.via`. Never a URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f7f2bfc800
commit
0047981a95
@@ -35,7 +35,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
Two hard typographic ceilings you currently miss:
|
||||
- Hero clamp() max ≤ 6rem. 8–11rem (128–176px) reads as comically loud, not bold.
|
||||
@@ -158,7 +157,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .agents/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .agents/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -152,7 +151,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .claude/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .claude/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -148,7 +147,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .cursor/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .cursor/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -149,7 +148,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .gemini/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .gemini/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -150,7 +149,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .github/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .github/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -148,7 +147,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .kiro/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .kiro/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -152,7 +151,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .opencode/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .opencode/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -150,7 +149,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .pi/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .pi/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -152,7 +151,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .qoder/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .qoder/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -152,7 +151,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -150,7 +149,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -150,7 +149,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .trae/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .trae/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes.
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing.
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed".
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
|
||||
|
||||
#### Layout
|
||||
|
||||
@@ -152,7 +151,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node .claude/skills/impeccable/scripts/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node .claude/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -40,7 +40,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points.
|
||||
- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. <!-- rule:skill-typo-no-all-caps-body -->
|
||||
- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. <!-- rule:skill-typo-hero-ceiling -->
|
||||
- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". <!-- rule:skill-typo-tracking-floor -->
|
||||
- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. <!-- rule:skill-typo-text-wrap-balance -->
|
||||
|
||||
<codex>
|
||||
Two hard typographic ceilings you currently miss:
|
||||
@@ -171,7 +170,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, detailed be
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`.
|
||||
- Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`.
|
||||
|
||||
**If `scan.detectTarget` is set, run `node {{scripts_path}}/detect.mjs --json <scan.detectTarget>` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
**If `scan.targets` is non-empty, run `node {{scripts_path}}/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
|
||||
|
||||
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
|
||||
2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target.
|
||||
|
||||
@@ -150,35 +150,45 @@ async function devServerSignals() {
|
||||
return { running: open.length > 0, ports: open };
|
||||
}
|
||||
|
||||
// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
|
||||
const SCANNABLE_EXT = new Set([
|
||||
'.html', '.htm', '.css', '.scss',
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
/**
|
||||
* What the agent could point the bundled detector (`detect.mjs`) at. The
|
||||
* detector is HTML/CSS oriented, so a rendered page (dev server) or a static
|
||||
* HTML entry is a far better target than a raw source tree. This script does
|
||||
* NOT run the detector itself — it just surfaces the target so the agent can
|
||||
* run `node <scripts>/detect.mjs --json <target>` (bundled, dep-free, fast)
|
||||
* and fold the hits into its recommendation.
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
* may not even belong to this project. An HTML *file* or a source tree is
|
||||
* scanned by the cheap, jsdom-free static engine. This script does NOT run the
|
||||
* detector; it just surfaces the target(s) so the agent can run
|
||||
* `node <scripts>/detect.mjs --json <targets>` and fold the hits in.
|
||||
*/
|
||||
function scanTarget(cwd, devServer) {
|
||||
if (devServer.running && devServer.ports.length) {
|
||||
return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' };
|
||||
function scanTargets(cwd, git) {
|
||||
// 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
|
||||
// what the user is working on, it's a small set, and it's local.
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) {
|
||||
if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' };
|
||||
}
|
||||
for (const dir of ['.', 'public', 'dist', 'build']) {
|
||||
try {
|
||||
const abs = path.join(cwd, dir);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const html = fs.readdirSync(abs).find((f) => f.endsWith('.html'));
|
||||
if (html) return { detectTarget: path.join(dir, html), via: 'html' };
|
||||
} catch { /* ignore unreadable dir */ }
|
||||
}
|
||||
return { detectTarget: null, via: null };
|
||||
// 2. Otherwise scan the local source dirs that exist.
|
||||
const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
|
||||
if (dirs.length) return { targets: dirs, via: 'source-dir' };
|
||||
// 3. A root HTML entry, or the project root as a last resort when there's
|
||||
// code but no conventional source dir (walkDir still skips heavy dirs).
|
||||
if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
|
||||
if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
|
||||
return { targets: [], via: null };
|
||||
}
|
||||
|
||||
export async function gatherSignals(cwd = process.cwd()) {
|
||||
const ctx = loadContext(cwd);
|
||||
const devServer = await devServerSignals();
|
||||
const git = gitSignals(cwd);
|
||||
return {
|
||||
setup: {
|
||||
hasProduct: ctx.hasProduct,
|
||||
@@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) {
|
||||
register: extractRegister(ctx.product),
|
||||
},
|
||||
critique: { latest: latestCritique(cwd) },
|
||||
git: gitSignals(cwd),
|
||||
devServer,
|
||||
scan: scanTarget(cwd, devServer),
|
||||
git,
|
||||
devServer: await devServerSignals(),
|
||||
scan: scanTargets(cwd, git),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -109,25 +109,36 @@ describe('gatherSignals', () => {
|
||||
assert.ok(Array.isArray(s.devServer.ports));
|
||||
});
|
||||
|
||||
it('points scan.detectTarget at an HTML entry when one exists', async () => {
|
||||
write('public/index.html', '<!doctype html><title>x</title>');
|
||||
it('targets a local source dir (never a URL), even with a dev server up', async () => {
|
||||
write('src/App.tsx', 'export default 1;');
|
||||
const s = await gatherSignals(scratch);
|
||||
// A live dev server (if one happens to run on the host) wins; otherwise
|
||||
// the static HTML entry is the target.
|
||||
if (!s.devServer.running) {
|
||||
assert.equal(s.scan.detectTarget, 'public/index.html');
|
||||
assert.equal(s.scan.via, 'html');
|
||||
} else {
|
||||
assert.equal(s.scan.via, 'dev-server');
|
||||
}
|
||||
assert.equal(s.scan.via, 'source-dir');
|
||||
assert.deepEqual(s.scan.targets, ['src']);
|
||||
// No target is ever an http(s) URL.
|
||||
assert.ok(s.scan.targets.every((t) => !/^https?:/.test(t)));
|
||||
});
|
||||
|
||||
it('has a null scan.detectTarget when there is nothing scannable', async () => {
|
||||
it('prefers the dirty tree: scans changed markup/style files', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
git('init', '-q');
|
||||
git('config', 'user.email', 't@example.com');
|
||||
git('config', 'user.name', 'Test');
|
||||
write('src/Hero.tsx', 'export const Hero = () => null;\n');
|
||||
write('README.md', 'x\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'init');
|
||||
write('src/Hero.tsx', 'export const Hero = () => 2;\n'); // dirty
|
||||
write('README.md', 'y\n'); // dirty but not scannable
|
||||
const s = await gatherSignals(scratch);
|
||||
if (!s.devServer.running) {
|
||||
assert.equal(s.scan.detectTarget, null);
|
||||
assert.equal(s.scan.via, null);
|
||||
}
|
||||
assert.equal(s.scan.via, 'git-changes');
|
||||
assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // README.md filtered out
|
||||
});
|
||||
|
||||
it('has empty scan.targets only when there is no code at all', async () => {
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.deepEqual(s.scan.targets, []);
|
||||
assert.equal(s.scan.via, null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user