@upgraide/ui-notes-cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,10 +45,12 @@ uinotes config
45
45
 
46
46
  # Set a value
47
47
  uinotes config set apiUrl https://my-api.example.com
48
- uinotes config set defaultProject my-app
48
+
49
+ # Bind a project to the current directory
50
+ uinotes set project my-app
49
51
  ```
50
52
 
51
- **Config keys:** `apiUrl`, `apiKey`, `defaultProject`
53
+ **Config keys:** `apiUrl`, `apiKey`
52
54
 
53
55
  ### `uinotes projects`
54
56
 
@@ -122,11 +124,19 @@ The CLI looks for config in two places (local takes priority):
122
124
  ```json
123
125
  {
124
126
  "apiUrl": "http://localhost:3000",
125
- "apiKey": "your-api-key",
126
- "defaultProject": "my-app"
127
+ "apiKey": "your-api-key"
127
128
  }
128
129
  ```
129
130
 
131
+ ### Project Binding
132
+
133
+ Run `uinotes set project <slug>` to create a `.uinotes` file in the current directory. This binds the project to the directory tree so all commands automatically use it — no `--project` flag needed.
134
+
135
+ The CLI resolves the project in this order:
136
+ 1. `--project` flag (explicit override)
137
+ 2. `.uinotes` file in current directory or nearest parent
138
+ 3. No project (commands that require one will error)
139
+
130
140
  API keys are redacted when displayed with `uinotes config`.
131
141
 
132
142
  ## Global Options
package/api.ts CHANGED
@@ -11,7 +11,7 @@ export async function apiFetch(
11
11
  'x-api-key': config.apiKey,
12
12
  };
13
13
 
14
- const targetProject = project || config.defaultProject;
14
+ const targetProject = project || config.project;
15
15
  if (targetProject) {
16
16
  headers['x-project'] = targetProject;
17
17
  }
@@ -31,9 +31,9 @@ async function bulkResolve(
31
31
  filterValue: string,
32
32
  opts: BulkResolveOptions,
33
33
  ): Promise<void> {
34
- const project = opts.project || config.defaultProject;
34
+ const project = opts.project || config.project;
35
35
  if (!project) {
36
- console.error('No project specified. Use --project or set defaultProject in config.');
36
+ console.error('No project specified. Use --project or run: uinotes set project <slug>');
37
37
  process.exit(1);
38
38
  }
39
39
 
@@ -1,6 +1,6 @@
1
1
  import { getConfig, getConfigPath, setConfigValue } from '../config.js';
2
2
 
3
- const VALID_KEYS = ['apiUrl', 'apiKey', 'defaultProject'];
3
+ const VALID_KEYS = ['apiUrl', 'apiKey'];
4
4
 
5
5
  function redactKey(value: string): string {
6
6
  if (!value || value.length <= 4) return '****';
@@ -55,7 +55,7 @@ export async function context(config: Config, opts: ContextOptions): Promise<voi
55
55
  const output = formatNotesForAI(notes, {
56
56
  format: opts.format || 'markdown',
57
57
  componentFilter: opts.component,
58
- projectName: opts.project || config.defaultProject || 'unknown',
58
+ projectName: opts.project || config.project || 'unknown',
59
59
  resolvedNotes: resolvedMap,
60
60
  });
61
61
 
@@ -0,0 +1,58 @@
1
+ import type { Config } from '../config.js';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ interface InstallSkillsOptions {
6
+ dir?: string;
7
+ check?: boolean;
8
+ }
9
+
10
+ const DEFAULT_DIR = '.claude/skills';
11
+
12
+ export async function installSkills(_config: Config, opts: InstallSkillsOptions): Promise<void> {
13
+ const targetDir = opts.dir || join(process.cwd(), DEFAULT_DIR);
14
+ const skillsSourceDir = join(import.meta.dir, '..', 'skills');
15
+
16
+ // List available skills
17
+ let skillFiles: string[];
18
+ try {
19
+ skillFiles = readdirSync(skillsSourceDir).filter(f => f.endsWith('.md'));
20
+ } catch {
21
+ console.error('Could not read bundled skill files.');
22
+ process.exit(1);
23
+ }
24
+
25
+ if (skillFiles.length === 0) {
26
+ console.error('No skill files found in package.');
27
+ process.exit(1);
28
+ }
29
+
30
+ if (opts.check) {
31
+ // Check installed vs bundled
32
+ console.log('Skill status:');
33
+ for (const file of skillFiles) {
34
+ const targetPath = join(targetDir, file);
35
+ if (existsSync(targetPath)) {
36
+ console.log(` ${file}: installed`);
37
+ } else {
38
+ console.log(` ${file}: not installed`);
39
+ }
40
+ }
41
+ return;
42
+ }
43
+
44
+ // Install skills
45
+ mkdirSync(targetDir, { recursive: true });
46
+
47
+ for (const file of skillFiles) {
48
+ const sourcePath = join(skillsSourceDir, file);
49
+ const targetPath = join(targetDir, file);
50
+ const content = readFileSync(sourcePath, 'utf-8');
51
+ writeFileSync(targetPath, content);
52
+ }
53
+
54
+ console.log(`Installed ${skillFiles.length} skills to ${targetDir}/`);
55
+ for (const file of skillFiles) {
56
+ console.log(` ${file}`);
57
+ }
58
+ }
@@ -0,0 +1,9 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function setProject(slug: string): void {
5
+ const filePath = join(process.cwd(), '.uinotes');
6
+ writeFileSync(filePath, JSON.stringify({ project: slug }, null, 2) + '\n');
7
+ console.log(`Bound project "${slug}" to ${process.cwd()}`);
8
+ console.log(`Created .uinotes — commit this file so your team and AI agents inherit it.`);
9
+ }
@@ -37,9 +37,9 @@ async function loadPlaywright() {
37
37
  }
38
38
 
39
39
  export async function visualDiff(config: Config, opts: VisualDiffOptions): Promise<void> {
40
- const project = opts.project || config.defaultProject;
40
+ const project = opts.project || config.project;
41
41
  if (!project) {
42
- console.error('Error: --project is required (or set defaultProject in config)');
42
+ console.error('Error: --project is required (or run: uinotes set project <slug>)');
43
43
  process.exit(1);
44
44
  }
45
45
 
package/config.ts CHANGED
@@ -11,7 +11,7 @@ export interface ResolveConfig {
11
11
  export interface Config {
12
12
  apiUrl: string;
13
13
  apiKey: string;
14
- defaultProject?: string;
14
+ project?: string;
15
15
  resolve?: ResolveConfig;
16
16
  }
17
17
 
@@ -23,11 +23,34 @@ const DEFAULT_CONFIG: Config = {
23
23
  apiKey: '',
24
24
  };
25
25
 
26
- function readJsonFile(path: string): Config | null {
26
+ function readJsonFile(path: string): Record<string, any> | null {
27
27
  if (!existsSync(path)) return null;
28
28
  return JSON.parse(readFileSync(path, 'utf-8'));
29
29
  }
30
30
 
31
+ /**
32
+ * Walk up the directory tree from cwd looking for a .uinotes file
33
+ * that binds a project slug to this directory tree.
34
+ */
35
+ function findDirectoryProject(startDir: string): string | undefined {
36
+ let dir = startDir;
37
+ while (true) {
38
+ const filePath = join(dir, '.uinotes');
39
+ if (existsSync(filePath)) {
40
+ try {
41
+ const content = JSON.parse(readFileSync(filePath, 'utf-8'));
42
+ if (content.project) return content.project;
43
+ } catch {
44
+ // Invalid JSON — skip
45
+ }
46
+ }
47
+ const parent = dirname(dir);
48
+ if (parent === dir) break; // reached filesystem root
49
+ dir = parent;
50
+ }
51
+ return undefined;
52
+ }
53
+
31
54
  export function getConfigPath(): string {
32
55
  if (existsSync(LOCAL_PATH)) return LOCAL_PATH;
33
56
  return GLOBAL_PATH;
@@ -36,14 +59,22 @@ export function getConfigPath(): string {
36
59
  export function getConfig(): Config {
37
60
  // Per-repo config takes priority
38
61
  const local = readJsonFile(LOCAL_PATH);
39
- if (local) return { ...DEFAULT_CONFIG, ...local };
62
+ const base = local
63
+ ? { ...DEFAULT_CONFIG, ...local }
64
+ : (() => {
65
+ const global = readJsonFile(GLOBAL_PATH);
66
+ if (global) return { ...DEFAULT_CONFIG, ...global };
67
+ // Create default global config
68
+ writeFileSync(GLOBAL_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n');
69
+ return { ...DEFAULT_CONFIG };
70
+ })();
40
71
 
41
- const global = readJsonFile(GLOBAL_PATH);
42
- if (global) return { ...DEFAULT_CONFIG, ...global };
72
+ // Resolve project from .uinotes directory binding (if not already set)
73
+ if (!base.project) {
74
+ base.project = findDirectoryProject(process.cwd());
75
+ }
43
76
 
44
- // Create default global config
45
- writeFileSync(GLOBAL_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n');
46
- return DEFAULT_CONFIG;
77
+ return base as Config;
47
78
  }
48
79
 
49
80
  export function setConfigValue(key: string, value: string): Config {
package/index.ts CHANGED
@@ -22,6 +22,7 @@ Commands:
22
22
  projects add-url <slug> <pattern> Add URL pattern to project
23
23
  projects remove-url <slug> <pattern> Remove URL pattern
24
24
  projects delete <slug> Archive a project
25
+ set project <slug> Bind project to current directory
25
26
  login Login and create API key
26
27
  pull Fetch open notes
27
28
  context AI-optimized digest of open notes
@@ -36,6 +37,7 @@ Commands:
36
37
  visual-diff Capture & compare page screenshots
37
38
  batch <file|-> Execute NDJSON batch operations
38
39
  schema List or print API resource schemas
40
+ install-skills Install Claude Code skill files
39
41
  config View/edit configuration
40
42
 
41
43
  Options:
@@ -53,6 +55,8 @@ Options:
53
55
  --width <px> Viewport width for screenshots (default: 1280)
54
56
  --full-page Capture full page height (default: true)
55
57
  --since <ref> Git ref or ISO date for changelog start
58
+ --dir <path> Target directory for skill installation
59
+ --check Check installed skill versions
56
60
  --resource <name> Resource name for schema command
57
61
  --concurrency <n> Concurrent batch operations (default: 5)
58
62
  --dry-run Preview batch operations without executing`;
@@ -124,6 +128,23 @@ async function main() {
124
128
  return;
125
129
  }
126
130
 
131
+ // set command is local-only, no API key needed
132
+ if (command === 'set') {
133
+ const sub = positional[1];
134
+ if (sub === 'project') {
135
+ const slug = positional[2];
136
+ if (!slug) {
137
+ console.error('Usage: uinotes set project <slug>');
138
+ process.exit(1);
139
+ }
140
+ const { setProject } = await import('./commands/set.js');
141
+ setProject(slug);
142
+ return;
143
+ }
144
+ console.error(`Unknown set subcommand: ${sub}`);
145
+ process.exit(1);
146
+ }
147
+
127
148
  // Schema command is local-only, no API key needed
128
149
  if (command === 'schema') {
129
150
  const { schema } = await import('./commands/schema.js');
@@ -131,13 +152,23 @@ async function main() {
131
152
  return;
132
153
  }
133
154
 
155
+ // install-skills is local-only, no API key needed
156
+ if (command === 'install-skills') {
157
+ const { installSkills } = await import('./commands/install-skills.js');
158
+ await installSkills(getConfig(), {
159
+ dir: flags.dir,
160
+ check: flags.check === 'true',
161
+ });
162
+ return;
163
+ }
164
+
134
165
  const config = getConfig();
135
166
  if (!config.apiKey) {
136
167
  console.error('No API key configured. Run: uinotes config set apiKey <your-key>');
137
168
  process.exit(1);
138
169
  }
139
170
 
140
- const project = flags.project;
171
+ const project = flags.project || config.project;
141
172
 
142
173
  switch (command) {
143
174
  case 'projects': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upgraide/ui-notes-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for UI Notes - pull and manage UI feedback notes",
5
5
  "license": "MIT",
6
6
  "keywords": ["cli", "ui", "notes", "feedback", "ui-notes"],
@@ -12,7 +12,8 @@
12
12
  "commands/*.ts",
13
13
  "formatters/*.ts",
14
14
  "resolvers/*.ts",
15
- "schemas/*.json"
15
+ "schemas/*.json",
16
+ "skills/*.md"
16
17
  ],
17
18
  "engines": {
18
19
  "bun": ">=1.0.0"
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: uinotes-batch
3
+ description: Execute multiple note operations in a single call using NDJSON. Use for bulk triage of multiple notes.
4
+ ---
5
+
6
+ # uinotes-batch
7
+
8
+ Execute multiple operations (resolve, wontfix, comment, reopen) in one invocation.
9
+
10
+ ## When to use
11
+ - After triaging multiple notes and deciding actions for each
12
+ - When resolving several notes as part of a single fix
13
+ - For bulk operations that span multiple notes
14
+
15
+ ## Command
16
+ ```bash
17
+ echo '{"op":"resolve","id":"abc123","message":"Fixed overflow"}
18
+ {"op":"comment","id":"def456","text":"Needs design input"}
19
+ {"op":"wontfix","id":"ghi789","message":"Deprecated component"}' | uinotes batch -
20
+ ```
21
+
22
+ Operations: `resolve`, `wontfix`, `comment` (requires `text`), `reopen`.
23
+ Add `--dry-run` to preview without executing.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: uinotes-changelog
3
+ description: Generate a changelog from resolved UI notes. Use after resolving notes to create release notes or PR descriptions.
4
+ ---
5
+
6
+ # uinotes-changelog
7
+
8
+ Generate a changelog from notes resolved since a git ref or date.
9
+
10
+ ## When to use
11
+ - After resolving notes, to generate a summary for PR descriptions
12
+ - For release notes listing UI fixes
13
+ - To audit what was fixed in a time period
14
+
15
+ ## Command
16
+ ```bash
17
+ uinotes changelog --since HEAD~5
18
+ uinotes changelog --since v1.2.0 --format conventional
19
+ uinotes changelog --since 2026-02-01 --format json
20
+ ```
21
+
22
+ Formats: `markdown` (default), `conventional` (conventional commits), `json`.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: uinotes-comment
3
+ description: Add a comment to a UI note. Use to document progress, ask questions, or provide status updates on notes.
4
+ ---
5
+
6
+ # uinotes-comment
7
+
8
+ Add comments to notes to document progress or communicate.
9
+
10
+ ## When to use
11
+ - To document what was tried or what's blocking a fix
12
+ - To ask clarifying questions about a note
13
+ - To provide a status update before resolving
14
+
15
+ ## Command
16
+ ```bash
17
+ uinotes comment <note-id> "Comment text here"
18
+ uinotes comments <note-id> # List existing comments
19
+ ```
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: uinotes-context
3
+ description: Fetch AI-optimized context of open UI notes for the current project. Use when asked to check for UI bugs, feedback, or notes.
4
+ ---
5
+
6
+ # uinotes-context
7
+
8
+ Run `uinotes context` to get a structured digest of all open UI notes.
9
+
10
+ ## When to use
11
+ - At the start of a task to understand what UI issues exist
12
+ - Before making UI changes to check for related notes
13
+ - When the user asks about UI bugs, feedback, or design issues
14
+
15
+ ## Command
16
+ ```bash
17
+ uinotes context --format markdown
18
+ ```
19
+
20
+ Add `--resolve-files --root-dir .` to map notes to source files.
21
+ Add `--type bug` to filter by type (bug, ux, feature, question).
22
+ Add `--project <slug>` to target a specific project.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: uinotes-explain
3
+ description: Get full context for a single note including comments, history, and visual diffs. Use when investigating a specific note.
4
+ ---
5
+
6
+ # uinotes-explain
7
+
8
+ Get fully-hydrated context for a single note in one call.
9
+
10
+ ## When to use
11
+ - When investigating a specific note before fixing it
12
+ - When you need comments, history, or visual diff context
13
+ - When triaging a single note
14
+
15
+ ## Command
16
+ ```bash
17
+ uinotes explain <note-id>
18
+ uinotes explain <note-id> --format json
19
+ ```
20
+
21
+ Returns: note body, type, status, component, selector, URL, screenshot URL, all comments, status history, and visual diffs.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: uinotes-resolve
3
+ description: Mark a UI note as resolved after fixing it. Use after completing a fix for a UI issue.
4
+ ---
5
+
6
+ # uinotes-resolve
7
+
8
+ Mark notes as resolved after fixing the issue they describe.
9
+
10
+ ## When to use
11
+ - After fixing a bug, UX issue, or implementing a feature request described in a note
12
+ - After verifying a fix addresses the feedback
13
+
14
+ ## Commands
15
+ ```bash
16
+ # Resolve with a message explaining the fix
17
+ uinotes resolve <note-id> "Fixed by updating the component"
18
+
19
+ # Mark as won't fix with explanation
20
+ uinotes wontfix <note-id> "Working as intended"
21
+
22
+ # Bulk resolve all notes for a component
23
+ uinotes resolve-by-component <ComponentName> --message "Refactored component"
24
+ ```
25
+
26
+ Always include a resolution message describing what was done.