Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .claude/skills/audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ You are a **dependency maintenance engineer**. This is a recurring chore (typica
## This repo's specifics

- **Package manager: pnpm 10.x** (pinned via `packageManager`). On pnpm 10, `pnpm audit --fix` resolves advisories by writing pins into the `pnpm.overrides` block of `package.json` — this repo already carries a substantial hardening `overrides` block (lodash, minimatch, rollup, serialize-javascript, ajv, picomatch, …). (If pnpm is ever bumped to 11+, bare `--fix` errors with `ERR_PNPM_INVALID_FIX_OPTION` — use `--fix=update` first, then `--fix=override`.)
- **No lint/format/typecheck scripts.** There is no `pnpm fix` / `pnpm ready` gate. The proof a dep bump didn't break anything is the **test suite + production build** (`pnpm test:all` + `pnpm build`).
- **The local gate is `pnpm ready`** (see the `ready` skill) — its composition in `package.json` is the source of truth. For a dep bump, at minimum run `pnpm test:all` + `pnpm build`; running full `pnpm ready` is better proof.
- **Real test suite exists:** `pnpm test` (unit, jsdom), `pnpm test:browser` (Chromium via Playwright), `pnpm test:all` (both), `pnpm test:e2e` (Playwright E2E). Always run at least `pnpm test:all` + `pnpm build`.
- **Work lands on `dev`**, commits go directly there (no feature branch). A `dev → main` PR ships *everything* on `dev`, not just the dep patch.
- **Work lands on `dev`**, commits go directly there (no feature branch). A `dev → main` PR ships _everything_ on `dev`, not just the dep patch.
- If `pnpm install` aborts with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`, prefix with `CI=true`.

## Ground rules

- **No changes → no commit, no PR.** If `pnpm audit` is already clean or `--fix` changes nothing, STOP and report. Never create empty commits or PRs.
- **Security patches are a `chore(deps)` — no version bump.** Don't touch `package.json` `version`.
- **Stage only the dependency files.** Never `git add -A` / `git add .` — a stray file must never ride along in a deps commit. (This repo has no formatter that rewrites other files, so the dep commit should contain *only* `package.json` + `pnpm-lock.yaml`.)
- **Stage only the dependency files.** Never `git add -A` / `git add .` — a stray file must never ride along in a deps commit. (This repo has no formatter that rewrites other files, so the dep commit should contain _only_ `package.json` + `pnpm-lock.yaml`.)
- **Before opening a dev → main PR**, check `git log --oneline origin/main..dev`. If `dev` has unfinished feature work, do NOT open the PR — commit + push the patch and report, leaving the release PR to whoever owns it.

## Step 1: Preconditions
Expand Down Expand Up @@ -60,9 +60,9 @@ pnpm audit # what's vulnerable right now?
pnpm audit --fix # writes pnpm.overrides pins into package.json
```

> ⚠️ **CRITICAL — `--fix` REWRITES the whole overrides block, it does not just append.** It prints a reassuring summary like `2 overrides were added to package.json`, but it actually regenerates the managed `overrides` from the *current* advisory feed and **silently drops manually-curated hardening overrides whose advisories have aged out, and weakens others to looser version bounds.** The summary line is a lie of omission — do NOT trust it. This repo's overrides block is load-bearing; losing entries silently re-opens patched vulnerabilities.
> ⚠️ **CRITICAL — `--fix` REWRITES the whole overrides block, it does not just append.** It prints a reassuring summary like `2 overrides were added to package.json`, but it actually regenerates the managed `overrides` from the _current_ advisory feed and **silently drops manually-curated hardening overrides whose advisories have aged out, and weakens others to looser version bounds.** The summary line is a lie of omission — do NOT trust it. This repo's overrides block is load-bearing; losing entries silently re-opens patched vulnerabilities.
>
> **MANDATORY after `pnpm audit --fix`:** run `git diff package.json` and read the FULL overrides diff. Every `-` line is a guard that was removed. If anything other than pure additions appears (removed entries, or a pin moving to a *lower/weaker* version), the regenerated block is a regression. Restore the previous block (`git checkout HEAD -- package.json` to start over, or hand-merge) so the result is **old overrides + new advisory fixes**, never *replacement*.
> **MANDATORY after `pnpm audit --fix`:** run `git diff package.json` and read the FULL overrides diff. Every `-` line is a guard that was removed. If anything other than pure additions appears (removed entries, or a pin moving to a _lower/weaker_ version), the regenerated block is a regression. Restore the previous block (`git checkout HEAD -- package.json` to start over, or hand-merge) so the result is **old overrides + new advisory fixes**, never _replacement_.

```bash
git diff package.json # READ the full overrides diff — additive only?
Expand Down Expand Up @@ -133,16 +133,16 @@ Only if `dev` is shippable (see Ground rules). Use the **create-pr** skill. PR b

## Quick reference

| Situation | Action |
| ------------------------------------------ | ------------------------------------------------------------- |
| `pnpm audit` already clean | STOP — "nothing to patch", no commit/PR |
| `--fix` + install changed nothing | STOP — report, no commit/PR |
| `--fix` diff REMOVES/weakens an override | Regression — restore old block, keep old overrides + new fix |
| Working tree dirty at start | STOP — ask user to stash/commit first |
| Not on `dev` | Confirm with user before proceeding |
| `pnpm test:all` / `pnpm build` fails | STOP — report, let user decide |
| `dev` has unfinished feature work | Commit + push patch, SKIP the dev→main PR, report |
| Advisory has no patched version | Note it in the summary; don't block |
| Existing override looks stale | Note it for the user; don't auto-prune |
| Staging | `git add package.json pnpm-lock.yaml` only — never `-A` / `.` |
| Version bump | Never — security patches are `chore(deps)`, no bump |
| Situation | Action |
| ---------------------------------------- | ------------------------------------------------------------- |
| `pnpm audit` already clean | STOP — "nothing to patch", no commit/PR |
| `--fix` + install changed nothing | STOP — report, no commit/PR |
| `--fix` diff REMOVES/weakens an override | Regression — restore old block, keep old overrides + new fix |
| Working tree dirty at start | STOP — ask user to stash/commit first |
| Not on `dev` | Confirm with user before proceeding |
| `pnpm test:all` / `pnpm build` fails | STOP — report, let user decide |
| `dev` has unfinished feature work | Commit + push patch, SKIP the dev→main PR, report |
| Advisory has no patched version | Note it in the summary; don't block |
| Existing override looks stale | Note it for the user; don't auto-prune |
| Staging | `git add package.json pnpm-lock.yaml` only — never `-A` / `.` |
| Version bump | Never — security patches are `chore(deps)`, no bump |
37 changes: 37 additions & 0 deletions .claude/skills/ready/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: ready
description: Use when asked to "/ready", "run the ready checks", or "is this ready for a PR?" — and proactively before opening any dev → main PR or after finishing a session of work in this repo.
---

# Ready Gate

You are the **release gatekeeper**. `/ready` means: prove the project is shippable *right now*, in this session, and fix what isn't. Browser and E2E tests are local-only **by policy** (not in CI to save compute) — this gate is the ONLY place they run. Skipping a tier here means it runs nowhere.

> The user explicitly invoking `/ready` authorizes committing fixes to `dev`. It does NOT authorize pushing or opening a PR — those are separate requests.

## The Iron Rule

**The project is "ready" only if one uninterrupted `pnpm ready` run goes green after your last change.** Not before your last change. Not inferred from a previous run, CI, or a clean `git status`.

## Steps

1. **Preconditions:** confirm you're on `dev`; if `node_modules` is stale or missing, `pnpm install` (prefix `CI=true` if it aborts). Note what's uncommitted — you must know what you're gating.
2. **Run the gate:** `pnpm ready`. Its composition in `package.json` is the source of truth (tests + build; lint/format steps may be added over time — never bypass parts of the chain by running steps selectively and declaring victory).
3. **On failure — fix the root cause:**
- Diagnose against documented invariants (CLAUDE.md) — they arbitrate "regression" vs "intended change".
- While iterating, rerun just the failing tier/file (`pnpm vitest run tests/unit/x.test.js`, `pnpm test:e2e --project=chromium <file>`) for speed.
- **Never make the gate green by weakening it:** no deleting/skipping tests, no loosening assertions, no `--reporter` tricks. If a test fails because behavior changed *intentionally*, update test + docs together and say so; if intent is ambiguous, STOP and ask the user.
- Known environmental flake: the network-monitor E2E pings google.com and needs network. Re-run once; if it's genuinely environmental, report it — don't "fix" the test.
4. **Re-run the full gate** (`pnpm ready`, from the top) after the last fix. Any fix can break another tier.
5. **Commit fixes** (if any): stage only the files you changed — never `git add -A`. One commit per logical fix.
6. **Report:** what ran, what failed, what you fixed and why, and the final verdict backed by the clean run.

## Red Flags — you are rationalizing

| Excuse | Reality |
|--------|---------|
| "Unit tests passed, that's enough" | Browser/E2E/build run nowhere else. Run the full gate. |
| "Tree matches HEAD / this commit already passed" | Prove it now. Environments drift; deps changed; that's the point of `/ready`. |
| "It's late / user wants to wrap up quickly" | The gate IS the wrap-up. A broken PR costs more than 3 minutes of tests. |
| "I'll just update the failing test" | Only if the behavior change is documented and intentional — otherwise fix the code. |
| "E2E is slow, I'll skip WebKit/Firefox" | Local E2E covers all three engines because CI covers none. |
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
2 changes: 2 additions & 0 deletions .github/CI_CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Deployment is handled automatically by Vercel.
**File**: `.github/workflows/unit-tests.yml`

This workflow runs on every push to main/dev branches and on all pull requests. It:

- Sets up Node.js and pnpm
- Installs dependencies
- Runs unit tests with Vitest (jsdom)
Expand All @@ -41,6 +42,7 @@ pnpm ready # unit + browser + E2E tests, then a production build
### Deployment

Deployment is handled automatically by Vercel. When changes are pushed to the main branch, Vercel automatically:

- Detects the changes
- Builds the application
- Deploys to production at [passwords.roga.dev](https://passwords.roga.dev)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/guard-main-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: Guard main source branch

on:
pull_request:
branches: [ main ]
branches: [main]

jobs:
verify-source:
Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,36 @@ name: Unit Tests

on:
push:
branches: [ main, dev ]
branches: [main, dev]
pull_request:
branches: [ main, dev ]
branches: [main, dev]

jobs:
test:
name: Run Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install


- name: Lint
run: pnpm lint

- name: Check formatting
run: pnpm format:check

- name: Run unit tests
run: pnpm test
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
dist
dev-dist
coverage
node_modules
.claude
pnpm-lock.yaml
tests/browser/__screenshots__
*.min.*
5 changes: 5 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss"]
}
7 changes: 6 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"recommendations": ["Vue.volar"]
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"EditorConfig.EditorConfig"
]
}
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Run a single test file: `pnpm vitest run tests/unit/urlParams.test.js`
## Architecture

- **State management**: Reactive store via `src/stores/settingsStore.js` (Vue `reactive()` composable, not Vuex/Pinia). Single source of truth consumed by all components via `useSettings()`. All mutators (`updateSetting()`, `toggleSetting()`, `setSettings()`) enforce a cross-field invariant via `enforceInvariants()` — it auto-disables `ruleNoLeadingSpecial` when both letter sets are excluded. Always mutate settings through these mutators (never `Object.assign` onto the reactive object) so the invariant holds.
- **Password generation**: Pure function in `src/utils/password.js` — takes options object, returns password string or `"Error: ..."` string on failure. Error strings are checked with `startsWith('Error:')` in the UI. Key invariants: lengths outside 1–128 are rejected with an error (the UI clamps input to this range before it reaches the generator); each enabled (and not fully excluded) character set contributes at least one guaranteed character; `getRandomInt()` uses rejection sampling over `Uint32Array` to avoid modulo bias; the result is Fisher-Yates shuffled. The `ruleNoLeadingSpecial` rule runs *after* shuffling, swapping the leading char with the first letter found.
- **Password generation**: Pure function in `src/utils/password.js` — takes options object, returns password string or `"Error: ..."` string on failure. Error strings are checked with `startsWith('Error:')` in the UI. Key invariants: lengths outside 1–128 are rejected with an error (the UI clamps input to this range before it reaches the generator); each enabled (and not fully excluded) character set contributes at least one guaranteed character; `getRandomInt()` uses rejection sampling over `Uint32Array` to avoid modulo bias; the result is Fisher-Yates shuffled. The `ruleNoLeadingSpecial` rule runs _after_ shuffling, swapping the leading char with the first letter found.
- **URL params**: `src/utils/urlParams.js` handles bidirectional sync between settings and URL query string (`?len=24&exUpper&exSym`). Boolean params use presence-means-true convention. Only non-default values are serialized (keeps URLs clean). Settings load from URL on mount (via `setSettings()` so store invariants apply), and URL updates on setting changes via `history.replaceState`, debounced 200ms because Safari throttles `replaceState` calls. The keys are remapped (`length`→`len`, `excludeUppercase`→`exUpper`, etc.) in the `PARAM_KEYS` map.
- **Component hierarchy**: `App.vue` → `PasswordGenerator.vue` (orchestrator) → `OptionsPanel.vue`, `KeyboardExcluder.vue`, `NetworkMonitor.vue`
- **Network monitor**: `NetworkMonitor.vue` monkey-patches `window.fetch`, `XMLHttpRequest`, and `navigator.sendBeacon` to log outbound requests, demonstrating that generation is fully local. Interception is lazy (starts only when the panel opens) and originals are restored on unmount.
Expand All @@ -34,6 +34,7 @@ Run a single test file: `pnpm vitest run tests/unit/urlParams.test.js`
## Testing

Three test tiers with separate configs:

- **Unit tests** (`tests/unit/`): `vitest.config.js`, jsdom environment
- **Browser tests** (`tests/browser/`): `vitest.browser.config.js`, real Chromium via Playwright
- **E2E tests** (`tests/e2e/`): `playwright.config.js`, runs against dev server (auto-started locally)
Expand Down
13 changes: 12 additions & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,38 +49,49 @@ npm run test:e2e:ui
You can manually test URL parameters by appending them to your localhost URL:

#### Basic length change:

```
http://localhost:5173/?len=24
```

#### Exclude character types:

```
http://localhost:5173/?exLower&exNum
```

This excludes lowercase letters and numbers.

#### Specific excluded characters:

```
http://localhost:5173/?exc=abc123
```

This excludes the characters a, b, c, 1, 2, and 3.

#### Combined settings:

```
http://localhost:5173/?len=32&exSym&exc=%40%23%24
```

This sets length to 32, excludes symbols, and specifically excludes @, #, and $ (URL encoded).

#### Rule application:

```
http://localhost:5173/?ruleNoLead&len=15
```

This applies the rule to disallow leading numbers/symbols and sets length to 15.

#### Everything at once:

```
http://localhost:5173/?len=18&exUpper&exSym&ruleNoLead&exc=xyz789
```

This sets length to 18, excludes uppercase letters and symbols, disallows leading numbers/symbols, and excludes the characters x, y, z, 7, 8, and 9.

### Network Monitoring Test Case
Expand Down Expand Up @@ -110,4 +121,4 @@ When using the network monitoring panel:
1. Clicking "Ping Google" should send a HEAD request
2. The request should appear in the network log
3. The response should appear in the network log with latency information
4. A success message with latency should be displayed
4. A success message with latency should be displayed
62 changes: 62 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import vitest from '@vitest/eslint-plugin'
import playwright from 'eslint-plugin-playwright'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
import globals from 'globals'

// Flat config (ESLint 9+/10). ESLint owns correctness + Vue template rules;
// Prettier owns formatting. `skipFormatting` MUST stay last so it can disable
// every stylistic rule and leave whitespace decisions entirely to Prettier.
export default [
{
name: 'app/ignores',
ignores: [
'dist/',
'dev-dist/',
'coverage/',
'.claude/',
'tests/browser/__screenshots__/',
],
},

js.configs.recommended,
...pluginVue.configs['flat/recommended'],

{
name: 'app/browser-source',
files: ['src/**/*.{js,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser },
},
},

{
name: 'app/node-config',
files: ['*.config.js', '*.config.mjs'],
languageOptions: {
globals: { ...globals.node },
},
},

{
// Unit (jsdom) + browser (Playwright-backed vitest) tests. `globals: true`
// in the vitest configs means describe/it/expect/vi are ambient.
...vitest.configs.recommended,
name: 'app/vitest',
files: ['tests/unit/**/*.{js,vue}', 'tests/browser/**/*.{js,vue}'],
languageOptions: {
globals: { ...globals.browser, ...vitest.environments.env.globals },
},
},

{
...playwright.configs['flat/recommended'],
name: 'app/playwright',
files: ['tests/e2e/**/*.{js,spec.js}'],
},

skipFormatting,
]
Loading
Loading