flawless-ui-ux 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +21 -13
- package/bin/flawless-ui-ux.js +9 -3
- package/docs/assets/logo.svg +1 -0
- package/package.json +1 -1
- package/skills/flawless-ui-ux/references/inspection.md +1 -1
- package/src/install.js +94 -0
- package/src/options.js +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Native CLI install command for the bundled skill, without a repository checkout or Bash.
|
|
12
|
+
- Codex, OpenCode, and both-agent selection, global installation with CODEX_HOME/XDG_CONFIG_HOME support, and explicit project-local installation.
|
|
13
|
+
- Preflight of all selected destinations, preservation of existing files/directories/links, project-boundary checks, and cleanup of newly created skill directories after copy failures.
|
|
14
|
+
- Text/JSON installation reports and package-level installation tests under Node and Bun.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Made npm, pnpm, and Bun skill installation the primary README setup path; retained the Bash installer as an alternative.
|
|
19
|
+
|
|
20
|
+
## 0.1.1
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Added a black (#000000) background to the README logo so the white wordmark remains readable on light pages.
|
|
25
|
+
- Updated the README image URL to use the matching published package asset.
|
|
26
|
+
|
|
7
27
|
## 0.1.0
|
|
8
28
|
|
|
9
29
|
Initial public release, consolidating the pre-release development iterations.
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://cdn.jsdelivr.net/npm/flawless-ui-ux@0.1.
|
|
2
|
+
<img src="https://cdn.jsdelivr.net/npm/flawless-ui-ux@0.1.1/docs/assets/logo.svg" alt="flawless-ui-ux" width="334" height="80">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">Design interfaces around the product. Refine them against the result.</p>
|
|
@@ -8,32 +8,40 @@ flawless-ui-ux helps coding agents create, redesign, and refine web interfaces w
|
|
|
8
8
|
|
|
9
9
|
## Setup
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Choose your package manager to install the skill for **Codex and OpenCode globally**. The CLI requires **Node 22.12+**; no checkout or Bash is needed.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx flawless-ui-ux@latest
|
|
15
|
-
pnpm dlx flawless-ui-ux@latest
|
|
16
|
-
bunx flawless-ui-ux@latest
|
|
14
|
+
npx flawless-ui-ux@latest install
|
|
15
|
+
pnpm dlx flawless-ui-ux@latest install
|
|
16
|
+
bunx flawless-ui-ux@latest install
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The examples below use `npx
|
|
19
|
+
Run one of these commands. The examples below use `npx`; replace it with `pnpm dlx` or `bunx` to use the same operations.
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### Choose an agent and scope
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
For a specific agent, or a project-local installation:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
npx flawless-ui-ux install --codex
|
|
27
|
+
npx flawless-ui-ux install --opencode --project .
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Use `--all-agents`
|
|
30
|
+
The default is both agents globally. Use `--codex`, `--opencode`, or `--all-agents` to select targets. `--project <directory>` installs only in that project; `--global` makes the default scope explicit. Start a new agent session if needed for discovery.
|
|
31
|
+
|
|
32
|
+
Existing installations are never overwritten, including links and local changes. All selected destinations are checked before copying. Review or remove an existing installation yourself before reinstalling. Global paths honor `CODEX_HOME` and `XDG_CONFIG_HOME`; project paths are `.agents/skills/flawless-ui-ux` for Codex and `.opencode/skills/flawless-ui-ux` for OpenCode.
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>Alternative: install from a checkout with Bash</summary>
|
|
31
36
|
|
|
32
37
|
```bash
|
|
33
|
-
bash
|
|
38
|
+
bash ./install.sh --codex
|
|
39
|
+
bash ./install.sh --opencode
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
Use `--all-agents` for both. For project-local installation, run `bash /path/to/flawless-ui-ux/install.sh --all-agents --project` from the target project's directory. On Windows, the Bash alternative requires Git Bash or WSL.
|
|
43
|
+
|
|
44
|
+
</details>
|
|
37
45
|
|
|
38
46
|
## Use the skill
|
|
39
47
|
|
package/bin/flawless-ui-ux.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parse } from '../src/options.js';
|
|
|
3
3
|
import { context } from '../src/context.js';
|
|
4
4
|
import { inspect } from '../src/inspect.js';
|
|
5
5
|
import { verify } from '../src/verify.js';
|
|
6
|
+
import { install } from '../src/install.js';
|
|
6
7
|
import { VERSION } from '../src/shared.js';
|
|
7
8
|
|
|
8
9
|
const args = process.argv.slice(2);
|
|
@@ -10,21 +11,26 @@ try {
|
|
|
10
11
|
const { operation, options } = parse([...args]);
|
|
11
12
|
if (operation === 'version') console.log(VERSION);
|
|
12
13
|
else if (operation === 'help') console.log(`flawless-ui-ux ${VERSION}
|
|
14
|
+
install [--codex | --opencode | --all-agents] [--global | --project DIR]
|
|
15
|
+
[--format text|json]
|
|
13
16
|
context [--project DIR] [--format text|json]
|
|
14
17
|
inspect [--project DIR] --source FILE [--source FILE ...] [--url URL]
|
|
15
18
|
[--selector CSS] [--viewport WIDTHxHEIGHT ...] [--output NEW_DIR]
|
|
16
19
|
[--timeout MS] [--browser-executable FILE] [--format text|json]
|
|
17
20
|
verify RUN_JSON [--output NEW_DIR] [--browser-executable FILE] [--format text|json]
|
|
18
21
|
|
|
22
|
+
install defaults to both agents globally. Existing installations are preserved.
|
|
19
23
|
inspect requires a source or URL. Defaults: 1440x900 and 390x844, 15000 ms.
|
|
20
|
-
|
|
24
|
+
Inspection does not start servers, download browsers, edit sources, or submit forms.
|
|
21
25
|
FLAWLESS_BROWSER_EXECUTABLE explicitly selects an installed Chromium executable.
|
|
22
26
|
Exit codes: 0 completed; 1 confirmed errors; 2 invalid/incomplete.`);
|
|
23
27
|
else {
|
|
24
|
-
const report = await ({ context, inspect, verify }[operation])(options);
|
|
28
|
+
const report = await ({ install, context, inspect, verify }[operation])(options);
|
|
25
29
|
if (options.format === 'json') console.log(JSON.stringify(report, null, 2));
|
|
26
30
|
else {
|
|
27
|
-
|
|
31
|
+
if (operation === 'install') {
|
|
32
|
+
for (const target of report.installations) console.log(`Installed flawless-ui-ux for ${target.agent} (${target.scope}): ${target.path}`);
|
|
33
|
+
} else console.log(`${operation}: ${report.project}\nCoverage: ${JSON.stringify(report.coverage)} (${report.completion})`);
|
|
28
34
|
for (const f of report.findings) console.log(`[${f.status}/${f.severity}] ${f.checkId} ${f.target}: ${f.observation}\n ${f.recommendation}`);
|
|
29
35
|
if (report.operation === 'context') console.log(JSON.stringify({ sources: report.sources, missing: report.missing, saved: report.saved, decisions: report.decisions }, null, 2));
|
|
30
36
|
if (report.comparison) console.log(`Comparison: ${JSON.stringify(report.comparison)}`);
|
package/docs/assets/logo.svg
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" width="334" height="80" viewBox="0 0 334 80" role="img" aria-labelledby="title"><title id="title">flawless-ui-ux</title>
|
|
2
|
+
<rect width="334" height="80" fill="#000000"/>
|
|
2
3
|
<g transform="translate(20 20) scale(1.666667)"><path d="M4 20l1-5L16 4a2.83 2.83 0 0 1 4 4L9 19l-5 1Z" fill="#0ce491" stroke="#0ce491" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></g>
|
|
3
4
|
<g fill="#ffffff" transform="translate(72 52) scale(0.018 -0.018)">
|
|
4
5
|
<path transform="translate(0 0)" d="M679 933L679 1080L969 1080L969 933ZM243 0L243 1139Q243 1180 246.5 1224Q250 1268 264.5 1309.5Q279 1351 311 1386Q348 1426 392 1444Q436 1462 481.5 1466Q527 1470 568 1470L705 1470L705 1322L578 1322Q499 1322 460 1283Q421 1244 421 1167L421 0ZM60 933L60 1080L705 1080L705 933ZM954 0L954 1470L1132 1470L1132 0Z"/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Executable inspection
|
|
2
2
|
|
|
3
|
-
Use the
|
|
3
|
+
Use the CLI when its executable, package runner, or checkout is available. Its install operation copies the bundled agent instructions; running context, inspect, or verify does not install the skill. Do not change installation or dependencies during a review unless authorized. Otherwise use available host tools and state which checks remain unperformed.
|
|
4
4
|
|
|
5
5
|
Replace `flawless-ui-ux` below with `node /known/checkout/bin/flawless-ui-ux.js` when using a checkout. Use the actual project, running server URL/port, and affected sources:
|
|
6
6
|
|
package/src/install.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { projectRoot, within, VERSION } from './shared.js';
|
|
6
|
+
|
|
7
|
+
const skillName = 'flawless-ui-ux';
|
|
8
|
+
const bundledSkill = fileURLToPath(new URL('../skills/flawless-ui-ux/', import.meta.url));
|
|
9
|
+
|
|
10
|
+
// Resolve the nearest existing parent before writing. Project installs cannot
|
|
11
|
+
// escape through an existing directory symlink or junction.
|
|
12
|
+
async function destinationPath(destination, project) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.lstat(destination);
|
|
15
|
+
throw new Error(`Installation already exists: ${destination}`);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code !== 'ENOENT') throw error;
|
|
18
|
+
}
|
|
19
|
+
let parent = path.dirname(destination);
|
|
20
|
+
while (true) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.lstat(parent);
|
|
23
|
+
if (!(await fs.stat(parent)).isDirectory()) throw new Error(`Installation parent is not a directory: ${parent}`);
|
|
24
|
+
const real = await fs.realpath(parent);
|
|
25
|
+
if (project && !within(project, real)) throw new Error(`Installation parent escapes the project: ${parent}`);
|
|
26
|
+
return path.join(real, path.relative(parent, destination));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code !== 'ENOENT') throw error;
|
|
29
|
+
// An existing broken link is a conflict, not a missing parent to create.
|
|
30
|
+
try { await fs.lstat(parent); throw new Error(`Installation parent is a broken link: ${parent}`); }
|
|
31
|
+
catch (linkError) { if (linkError.code !== 'ENOENT') throw linkError; }
|
|
32
|
+
const next = path.dirname(parent);
|
|
33
|
+
if (next === parent) throw new Error(`No usable installation parent: ${destination}`);
|
|
34
|
+
parent = next;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function rollback(created) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
for (const entry of [...created].reverse()) {
|
|
42
|
+
try {
|
|
43
|
+
const current = await fs.lstat(entry.path);
|
|
44
|
+
const real = await fs.realpath(entry.path);
|
|
45
|
+
// Recursive removal is limited to the exact directory allocated by this
|
|
46
|
+
// invocation, with its identity unchanged and no root symlink followed.
|
|
47
|
+
if (!current.isDirectory() || current.isSymbolicLink() || real !== entry.path ||
|
|
48
|
+
current.dev !== entry.device || current.ino !== entry.inode ||
|
|
49
|
+
path.basename(real) !== skillName || !within(path.dirname(entry.path), real)) {
|
|
50
|
+
throw new Error('Directory identity changed; manual cleanup is required.');
|
|
51
|
+
}
|
|
52
|
+
await fs.rm(real, { recursive: true, force: true });
|
|
53
|
+
} catch (error) { errors.push(`${entry.path}: ${error.message}`); }
|
|
54
|
+
}
|
|
55
|
+
return errors;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function install(options, runtime = {}) {
|
|
59
|
+
const env = runtime.env || process.env;
|
|
60
|
+
const home = runtime.home || os.homedir();
|
|
61
|
+
const project = options.project ? await projectRoot(options.project) : null;
|
|
62
|
+
const agent = options.agent || 'all';
|
|
63
|
+
if (!['all', 'codex', 'opencode'].includes(agent)) throw new Error('Unknown installation agent.');
|
|
64
|
+
if (!(await fs.stat(path.join(bundledSkill, 'SKILL.md'))).isFile()) throw new Error('The package does not contain an installable skill.');
|
|
65
|
+
const entries = await fs.readdir(bundledSkill);
|
|
66
|
+
const candidates = [
|
|
67
|
+
{ agent: 'codex', path: project ? path.join(project, '.agents', 'skills', skillName) : path.resolve(env.CODEX_HOME || path.join(home, '.codex'), 'skills', skillName) },
|
|
68
|
+
{ agent: 'opencode', path: project ? path.join(project, '.opencode', 'skills', skillName) : path.resolve(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode', 'skills', skillName) },
|
|
69
|
+
].filter(target => agent === 'all' || target.agent === agent);
|
|
70
|
+
// Preflight every target before creating any installation directories.
|
|
71
|
+
const targets = [];
|
|
72
|
+
for (const candidate of candidates) targets.push({ ...candidate, path: await destinationPath(candidate.path, project) });
|
|
73
|
+
if (targets.length === 2 && (within(targets[0].path, targets[1].path) || within(targets[1].path, targets[0].path))) throw new Error('Agent installation destinations overlap. Check CODEX_HOME and XDG_CONFIG_HOME.');
|
|
74
|
+
const created = [];
|
|
75
|
+
try {
|
|
76
|
+
for (const target of targets) {
|
|
77
|
+
await fs.mkdir(path.dirname(target.path), { recursive: true });
|
|
78
|
+
// Exclusive mkdir also preserves a destination created after preflight.
|
|
79
|
+
await fs.mkdir(target.path);
|
|
80
|
+
const identity = await fs.lstat(target.path);
|
|
81
|
+
created.push({ path: target.path, device: identity.dev, inode: identity.ino });
|
|
82
|
+
for (const entry of entries) await fs.cp(path.join(bundledSkill, entry), path.join(target.path, entry), { recursive: true, force: false, errorOnExist: true, verbatimSymlinks: true });
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const cleanup = await rollback(created);
|
|
86
|
+
throw new Error(`Installation failed: ${error.message}${cleanup.length ? ` Cleanup incomplete: ${cleanup.join('; ')}` : ' Newly created skill directories were removed; empty parent directories may remain.'}`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: 1, toolVersion: VERSION, operation: 'install', project,
|
|
90
|
+
completion: 'complete', coverage: { installation: 'completed' },
|
|
91
|
+
installations: targets.map(target => ({ ...target, scope: project ? 'project' : 'global' })),
|
|
92
|
+
findings: [], limits: [], exitCode: 0,
|
|
93
|
+
};
|
|
94
|
+
}
|
package/src/options.js
CHANGED
|
@@ -21,20 +21,30 @@ export function parse(argv) {
|
|
|
21
21
|
const options = { project: '.', sources: [], viewports: [], timeout: 15000, format: 'text' };
|
|
22
22
|
if (['--help', '-h', undefined].includes(operation)) return { operation: 'help', options };
|
|
23
23
|
if (operation === '--version') return { operation: 'version', options };
|
|
24
|
-
if (!['context', 'inspect', 'verify'].includes(operation)) throw new Error(`Unknown operation: ${operation}`);
|
|
24
|
+
if (!['install', 'context', 'inspect', 'verify'].includes(operation)) throw new Error(`Unknown operation: ${operation}`);
|
|
25
|
+
if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) return { operation: 'help', options };
|
|
26
|
+
if (operation === 'install') { options.project = undefined; options.agent = 'all'; }
|
|
25
27
|
if (operation === 'verify') {
|
|
26
28
|
options.baseline = argv.shift();
|
|
27
29
|
if (!options.baseline || options.baseline.startsWith('--')) throw new Error('verify requires a run.json path.');
|
|
28
30
|
}
|
|
29
31
|
const seen = new Set();
|
|
30
|
-
const allowed = operation === 'context' ? ['project', 'format'] : operation === 'verify' ? ['output', 'format', 'browser-executable'] : ['project', 'source', 'url', 'selector', 'viewport', 'output', 'format', 'timeout', 'browser-executable'];
|
|
32
|
+
const allowed = operation === 'install' ? ['codex', 'opencode', 'all-agents', 'global', 'project', 'format'] : operation === 'context' ? ['project', 'format'] : operation === 'verify' ? ['output', 'format', 'browser-executable'] : ['project', 'source', 'url', 'selector', 'viewport', 'output', 'format', 'timeout', 'browser-executable'];
|
|
31
33
|
while (argv.length) {
|
|
32
34
|
const flag = argv.shift();
|
|
33
35
|
if (!flag.startsWith('--') || !allowed.includes(flag.slice(2))) throw new Error(`Unknown option: ${flag}`);
|
|
34
|
-
const name = flag.slice(2)
|
|
35
|
-
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`);
|
|
36
|
+
const name = flag.slice(2);
|
|
36
37
|
if (seen.has(name) && !['source', 'viewport'].includes(name)) throw new Error(`Repeated option: ${flag}`);
|
|
38
|
+
if (operation === 'install' && ['codex', 'opencode', 'all-agents'].includes(name)) {
|
|
39
|
+
if (['codex', 'opencode', 'all-agents'].some(agent => seen.has(agent))) throw new Error('Select one agent option, or use --all-agents for both.');
|
|
40
|
+
options.agent = name === 'all-agents' ? 'all' : name;
|
|
41
|
+
seen.add(name);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
37
44
|
seen.add(name);
|
|
45
|
+
if (operation === 'install' && name === 'global') continue;
|
|
46
|
+
const value = argv.shift();
|
|
47
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`);
|
|
38
48
|
if (name === 'source') options.sources.push(value);
|
|
39
49
|
else if (name === 'viewport') {
|
|
40
50
|
const match = /^(\d+)x(\d+)$/.exec(value);
|
|
@@ -43,6 +53,7 @@ export function parse(argv) {
|
|
|
43
53
|
} else options[{ 'browser-executable': 'browserExecutable' }[name] || name] = name === 'timeout' ? Number(value) : value;
|
|
44
54
|
}
|
|
45
55
|
if (!['text', 'json'].includes(options.format)) throw new Error('Format must be text or json.');
|
|
56
|
+
if (operation === 'install' && seen.has('global') && seen.has('project')) throw new Error('Choose --global or --project, not both.');
|
|
46
57
|
if (operation === 'inspect' && options.url) options.browserExecutable ||= process.env.FLAWLESS_BROWSER_EXECUTABLE;
|
|
47
58
|
if (operation === 'inspect') validateInspection(options);
|
|
48
59
|
return { operation, options };
|