@upgraide/ui-notes-cli 0.1.3 → 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 +14 -4
- package/api.ts +1 -1
- package/commands/batch.ts +116 -0
- package/commands/bulk-resolve.ts +2 -2
- package/commands/changelog.ts +150 -0
- package/commands/config.ts +1 -1
- package/commands/context.ts +1 -1
- package/commands/explain.ts +67 -0
- package/commands/install-skills.ts +58 -0
- package/commands/schema.ts +33 -0
- package/commands/set.ts +9 -0
- package/commands/visual-diff.ts +2 -2
- package/config.ts +39 -8
- package/index.ts +88 -3
- package/package.json +4 -2
- package/schemas/comments.json +14 -0
- package/schemas/notes.json +24 -0
- package/schemas/projects.json +15 -0
- package/schemas/webhooks.json +16 -0
- package/skills/uinotes-batch.md +23 -0
- package/skills/uinotes-changelog.md +22 -0
- package/skills/uinotes-comment.md +19 -0
- package/skills/uinotes-context.md +22 -0
- package/skills/uinotes-explain.md +21 -0
- package/skills/uinotes-resolve.md +26 -0
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
|
-
|
|
48
|
+
|
|
49
|
+
# Bind a project to the current directory
|
|
50
|
+
uinotes set project my-app
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
**Config keys:** `apiUrl`, `apiKey
|
|
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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { apiFetch } from '../api.js';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
interface BatchOp {
|
|
6
|
+
op: 'resolve' | 'wontfix' | 'comment' | 'reopen';
|
|
7
|
+
id: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface BatchOptions {
|
|
13
|
+
project?: string;
|
|
14
|
+
concurrency?: number;
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function batch(config: Config, source: string, opts: BatchOptions): Promise<void> {
|
|
19
|
+
let input: string;
|
|
20
|
+
if (source === '-') {
|
|
21
|
+
input = await readStdin();
|
|
22
|
+
} else {
|
|
23
|
+
input = readFileSync(source, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = input.trim().split('\n').filter(l => l.trim());
|
|
27
|
+
const ops: BatchOp[] = [];
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
try {
|
|
31
|
+
ops.push(JSON.parse(lines[i]));
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(`Line ${i + 1}: invalid JSON — skipped`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (ops.length === 0) {
|
|
38
|
+
console.log('No operations to execute.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (opts.dryRun) {
|
|
43
|
+
console.log(`Dry run: ${ops.length} operations`);
|
|
44
|
+
for (let i = 0; i < ops.length; i++) {
|
|
45
|
+
console.log(`[${i + 1}/${ops.length}] ${ops[i].op} ${ops[i].id}`);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const concurrency = opts.concurrency || 5;
|
|
51
|
+
let succeeded = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < ops.length; i += concurrency) {
|
|
55
|
+
const chunk = ops.slice(i, i + concurrency);
|
|
56
|
+
await Promise.allSettled(
|
|
57
|
+
chunk.map(async (op, idx) => {
|
|
58
|
+
const globalIdx = i + idx;
|
|
59
|
+
try {
|
|
60
|
+
await executeOp(config, op, opts.project);
|
|
61
|
+
succeeded++;
|
|
62
|
+
console.log(`[${globalIdx + 1}/${ops.length}] ${op.op} ${op.id} ✓`);
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
failed++;
|
|
65
|
+
console.log(`[${globalIdx + 1}/${ops.length}] ${op.op} ${op.id} ✗ ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`Batch complete: ${succeeded}/${ops.length} succeeded${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function executeOp(config: Config, op: BatchOp, project?: string): Promise<void> {
|
|
75
|
+
switch (op.op) {
|
|
76
|
+
case 'resolve':
|
|
77
|
+
await apiFetch(config, `/notes/${op.id}`, {
|
|
78
|
+
method: 'PATCH',
|
|
79
|
+
body: { status: 'resolved', resolution: op.message },
|
|
80
|
+
project,
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
case 'wontfix':
|
|
84
|
+
await apiFetch(config, `/notes/${op.id}`, {
|
|
85
|
+
method: 'PATCH',
|
|
86
|
+
body: { status: 'wontfix', resolution: op.message },
|
|
87
|
+
project,
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
case 'comment':
|
|
91
|
+
if (!op.text) throw new Error('Missing "text" field for comment operation');
|
|
92
|
+
await apiFetch(config, `/notes/${op.id}/comments`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
body: { body: op.text },
|
|
95
|
+
project,
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
case 'reopen':
|
|
99
|
+
await apiFetch(config, `/notes/${op.id}`, {
|
|
100
|
+
method: 'PATCH',
|
|
101
|
+
body: { status: 'open' },
|
|
102
|
+
project,
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown operation: ${(op as any).op}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readStdin(): Promise<string> {
|
|
111
|
+
const chunks: Buffer[] = [];
|
|
112
|
+
for await (const chunk of process.stdin) {
|
|
113
|
+
chunks.push(Buffer.from(chunk));
|
|
114
|
+
}
|
|
115
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
116
|
+
}
|
package/commands/bulk-resolve.ts
CHANGED
|
@@ -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.
|
|
34
|
+
const project = opts.project || config.project;
|
|
35
35
|
if (!project) {
|
|
36
|
-
console.error('No project specified. Use --project or set
|
|
36
|
+
console.error('No project specified. Use --project or run: uinotes set project <slug>');
|
|
37
37
|
process.exit(1);
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { apiFetch } from '../api.js';
|
|
3
|
+
|
|
4
|
+
interface ChangelogOptions {
|
|
5
|
+
project?: string;
|
|
6
|
+
since?: string;
|
|
7
|
+
format?: 'markdown' | 'json' | 'conventional';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Note {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
body: string;
|
|
14
|
+
component: string | null;
|
|
15
|
+
resolved_at: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TYPE_ORDER = ['bug', 'ux', 'feature', 'question'];
|
|
19
|
+
|
|
20
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
21
|
+
bug: 'Bugs',
|
|
22
|
+
ux: 'UX',
|
|
23
|
+
feature: 'Features',
|
|
24
|
+
question: 'Questions',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TYPE_CONVENTIONAL: Record<string, string> = {
|
|
28
|
+
bug: 'fix',
|
|
29
|
+
ux: 'style',
|
|
30
|
+
feature: 'feat',
|
|
31
|
+
question: 'docs',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function isISODate(value: string): boolean {
|
|
35
|
+
return /^\d{4}-\d{2}-\d{2}/.test(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveTimestamp(since: string): string {
|
|
39
|
+
if (isISODate(since)) {
|
|
40
|
+
return since;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Treat as git ref, resolve to ISO timestamp
|
|
44
|
+
const result = Bun.spawnSync(['git', 'log', '-1', '--format=%cI', since]);
|
|
45
|
+
const output = result.stdout.toString().trim();
|
|
46
|
+
|
|
47
|
+
if (result.exitCode !== 0 || !output) {
|
|
48
|
+
throw new Error(`Could not resolve git ref "${since}". Make sure it exists.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatMarkdown(notes: Note[]): string {
|
|
55
|
+
const grouped: Record<string, Note[]> = {};
|
|
56
|
+
|
|
57
|
+
for (const note of notes) {
|
|
58
|
+
if (!grouped[note.type]) grouped[note.type] = [];
|
|
59
|
+
grouped[note.type].push(note);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lines: string[] = ['## UI Notes Resolved', ''];
|
|
63
|
+
|
|
64
|
+
for (const type of TYPE_ORDER) {
|
|
65
|
+
const group = grouped[type];
|
|
66
|
+
if (!group || group.length === 0) continue;
|
|
67
|
+
|
|
68
|
+
lines.push(`### ${TYPE_LABELS[type] || type}`);
|
|
69
|
+
for (const note of group) {
|
|
70
|
+
const shortId = note.id.slice(0, 8);
|
|
71
|
+
const body = note.body.split('\n')[0].trim();
|
|
72
|
+
lines.push(`- ${body} (#${shortId})`);
|
|
73
|
+
}
|
|
74
|
+
lines.push('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatConventional(notes: Note[]): string {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const type of TYPE_ORDER) {
|
|
84
|
+
const group = notes.filter((n) => n.type === type);
|
|
85
|
+
for (const note of group) {
|
|
86
|
+
const prefix = TYPE_CONVENTIONAL[note.type] || note.type;
|
|
87
|
+
const scope = note.component || 'ui';
|
|
88
|
+
const body = note.body.split('\n')[0].trim();
|
|
89
|
+
const shortId = note.id.slice(0, 8);
|
|
90
|
+
lines.push(`${prefix}(${scope}): ${body} (note:${shortId})`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatJSON(notes: Note[]): string {
|
|
98
|
+
const items = notes.map((n) => ({
|
|
99
|
+
id: n.id,
|
|
100
|
+
type: n.type,
|
|
101
|
+
body: n.body,
|
|
102
|
+
component: n.component,
|
|
103
|
+
resolved_at: n.resolved_at,
|
|
104
|
+
}));
|
|
105
|
+
return JSON.stringify(items, null, 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function changelog(config: Config, opts: ChangelogOptions): Promise<void> {
|
|
109
|
+
const since = opts.since;
|
|
110
|
+
if (!since) {
|
|
111
|
+
console.error('--since is required. Provide a git ref (e.g., HEAD~5, v1.2.0) or ISO date (e.g., 2026-02-01).');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const timestamp = resolveTimestamp(since);
|
|
116
|
+
const format = opts.format || 'markdown';
|
|
117
|
+
|
|
118
|
+
const params = new URLSearchParams();
|
|
119
|
+
params.set('status', 'all');
|
|
120
|
+
params.set('resolved_after', timestamp);
|
|
121
|
+
params.set('limit', '500');
|
|
122
|
+
|
|
123
|
+
const path = `/notes?${params.toString()}`;
|
|
124
|
+
const response = (await apiFetch(config, path, { project: opts.project })) as {
|
|
125
|
+
ok: boolean;
|
|
126
|
+
data: { notes: Note[] };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const notes = response.data.notes
|
|
130
|
+
.filter((n) => n.resolved_at)
|
|
131
|
+
.sort((a, b) => (a.resolved_at! > b.resolved_at! ? 1 : -1));
|
|
132
|
+
|
|
133
|
+
if (notes.length === 0) {
|
|
134
|
+
console.log('No resolved notes found since ' + timestamp);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
switch (format) {
|
|
139
|
+
case 'json':
|
|
140
|
+
console.log(formatJSON(notes));
|
|
141
|
+
break;
|
|
142
|
+
case 'conventional':
|
|
143
|
+
console.log(formatConventional(notes));
|
|
144
|
+
break;
|
|
145
|
+
case 'markdown':
|
|
146
|
+
default:
|
|
147
|
+
console.log(formatMarkdown(notes));
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/commands/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getConfig, getConfigPath, setConfigValue } from '../config.js';
|
|
2
2
|
|
|
3
|
-
const VALID_KEYS = ['apiUrl', 'apiKey'
|
|
3
|
+
const VALID_KEYS = ['apiUrl', 'apiKey'];
|
|
4
4
|
|
|
5
5
|
function redactKey(value: string): string {
|
|
6
6
|
if (!value || value.length <= 4) return '****';
|
package/commands/context.ts
CHANGED
|
@@ -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.
|
|
58
|
+
projectName: opts.project || config.project || 'unknown',
|
|
59
59
|
resolvedNotes: resolvedMap,
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { apiFetch } from '../api.js';
|
|
3
|
+
|
|
4
|
+
interface ExplainOptions {
|
|
5
|
+
format?: 'markdown' | 'json';
|
|
6
|
+
project?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function explain(config: Config, noteId: string, opts: ExplainOptions): Promise<void> {
|
|
10
|
+
const response = await apiFetch(config, `/notes/${noteId}/explain`, {
|
|
11
|
+
project: opts.project,
|
|
12
|
+
}) as any;
|
|
13
|
+
|
|
14
|
+
const { note, comments, history, visual_diffs } = response.data;
|
|
15
|
+
|
|
16
|
+
if (opts.format === 'json') {
|
|
17
|
+
console.log(JSON.stringify(response.data, null, 2));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Default: markdown format
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
lines.push(`# Note ${note.id} — ${note.type} (${note.status})`);
|
|
24
|
+
lines.push('');
|
|
25
|
+
if (note.component) lines.push(`**Component**: ${note.component}`);
|
|
26
|
+
if (note.selector) lines.push(`**Selector**: \`${note.selector}\``);
|
|
27
|
+
lines.push(`**URL**: ${note.url}`);
|
|
28
|
+
if (note.author) lines.push(`**Author**: ${note.author}`);
|
|
29
|
+
lines.push(`**Created**: ${note.created_at}`);
|
|
30
|
+
if (note.resolved_at) lines.push(`**Resolved**: ${note.resolved_at}`);
|
|
31
|
+
if (note.screenshot_url) lines.push(`**Screenshot**: ${note.screenshot_url}`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push(`> ${note.body}`);
|
|
34
|
+
|
|
35
|
+
if (comments.length > 0) {
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push(`## Comments (${comments.length})`);
|
|
38
|
+
lines.push('');
|
|
39
|
+
for (const c of comments) {
|
|
40
|
+
lines.push(`**${c.author}** (${c.created_at}):`);
|
|
41
|
+
lines.push(`> ${c.body}`);
|
|
42
|
+
lines.push('');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (history.length > 0) {
|
|
47
|
+
lines.push('## History');
|
|
48
|
+
lines.push('');
|
|
49
|
+
for (const h of history) {
|
|
50
|
+
lines.push(`- ${h.changed_at}: ${h.field} ${h.old_value ?? '(none)'} → ${h.new_value ?? '(none)'}${h.changed_by ? ` (${h.changed_by})` : ''}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (visual_diffs.length > 0) {
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push(`## Visual Diffs (${visual_diffs.length})`);
|
|
57
|
+
lines.push('');
|
|
58
|
+
for (const vd of visual_diffs) {
|
|
59
|
+
lines.push(`- ${vd.created_at}: diff ${vd.diff_percentage != null ? `${vd.diff_percentage}%` : 'N/A'}`);
|
|
60
|
+
if (vd.before_url) lines.push(` Before: ${vd.before_url}`);
|
|
61
|
+
if (vd.after_url) lines.push(` After: ${vd.after_url}`);
|
|
62
|
+
if (vd.diff_url) lines.push(` Diff: ${vd.diff_url}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(lines.join('\n'));
|
|
67
|
+
}
|
|
@@ -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,33 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const RESOURCES = ['notes', 'comments', 'projects', 'webhooks'];
|
|
6
|
+
|
|
7
|
+
export async function schema(_config: Config, resource?: string): Promise<void> {
|
|
8
|
+
if (!resource) {
|
|
9
|
+
console.log('Available schemas:');
|
|
10
|
+
for (const r of RESOURCES) {
|
|
11
|
+
console.log(` ${r}`);
|
|
12
|
+
}
|
|
13
|
+
console.log('\nUsage: uinotes schema --resource <name>');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!RESOURCES.includes(resource)) {
|
|
18
|
+
console.error(`Unknown resource: ${resource}`);
|
|
19
|
+
console.error(`Available: ${RESOURCES.join(', ')}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const schemaDir = join(import.meta.dir, '..', 'schemas');
|
|
24
|
+
const schemaPath = join(schemaDir, `${resource}.json`);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
28
|
+
console.log(content);
|
|
29
|
+
} catch {
|
|
30
|
+
console.error(`Schema file not found: ${schemaPath}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/commands/set.ts
ADDED
|
@@ -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
|
+
}
|
package/commands/visual-diff.ts
CHANGED
|
@@ -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.
|
|
40
|
+
const project = opts.project || config.project;
|
|
41
41
|
if (!project) {
|
|
42
|
-
console.error('Error: --project is required (or set
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
if (
|
|
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
|
-
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { getConfig } from './config.js';
|
|
4
4
|
import { configShow, configSet } from './commands/config.js';
|
|
@@ -22,16 +22,22 @@ 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
|
|
29
|
+
explain <id> Full context for a single note
|
|
28
30
|
resolve <id> Mark note as resolved
|
|
29
31
|
wontfix <id> Mark note as won't fix
|
|
30
32
|
comment <id> "text" Add a comment to a note
|
|
31
33
|
comments <id> List comments for a note
|
|
32
34
|
resolve-by-component <name> [msg] Bulk resolve notes by component
|
|
33
35
|
resolve-by-selector <sel> [msg] Bulk resolve notes by selector
|
|
36
|
+
changelog Generate changelog from resolved notes
|
|
34
37
|
visual-diff Capture & compare page screenshots
|
|
38
|
+
batch <file|-> Execute NDJSON batch operations
|
|
39
|
+
schema List or print API resource schemas
|
|
40
|
+
install-skills Install Claude Code skill files
|
|
35
41
|
config View/edit configuration
|
|
36
42
|
|
|
37
43
|
Options:
|
|
@@ -47,7 +53,13 @@ Options:
|
|
|
47
53
|
--root-dir <dir> Source root for file resolution (default: cwd)
|
|
48
54
|
--compare Compare current screenshots against previous (visual-diff)
|
|
49
55
|
--width <px> Viewport width for screenshots (default: 1280)
|
|
50
|
-
--full-page Capture full page height (default: true)
|
|
56
|
+
--full-page Capture full page height (default: true)
|
|
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
|
|
60
|
+
--resource <name> Resource name for schema command
|
|
61
|
+
--concurrency <n> Concurrent batch operations (default: 5)
|
|
62
|
+
--dry-run Preview batch operations without executing`;
|
|
51
63
|
|
|
52
64
|
function parseFlags(args: string[]): { flags: Record<string, string>; arrays: Record<string, string[]>; positional: string[] } {
|
|
53
65
|
const flags: Record<string, string> = {};
|
|
@@ -116,13 +128,47 @@ async function main() {
|
|
|
116
128
|
return;
|
|
117
129
|
}
|
|
118
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
|
+
|
|
148
|
+
// Schema command is local-only, no API key needed
|
|
149
|
+
if (command === 'schema') {
|
|
150
|
+
const { schema } = await import('./commands/schema.js');
|
|
151
|
+
await schema(getConfig(), flags.resource);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
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
|
+
|
|
119
165
|
const config = getConfig();
|
|
120
166
|
if (!config.apiKey) {
|
|
121
167
|
console.error('No API key configured. Run: uinotes config set apiKey <your-key>');
|
|
122
168
|
process.exit(1);
|
|
123
169
|
}
|
|
124
170
|
|
|
125
|
-
const project = flags.project;
|
|
171
|
+
const project = flags.project || config.project;
|
|
126
172
|
|
|
127
173
|
switch (command) {
|
|
128
174
|
case 'projects': {
|
|
@@ -230,6 +276,20 @@ async function main() {
|
|
|
230
276
|
});
|
|
231
277
|
break;
|
|
232
278
|
|
|
279
|
+
case 'explain': {
|
|
280
|
+
const id = positional[1];
|
|
281
|
+
if (!id) {
|
|
282
|
+
console.error('Usage: uinotes explain <note-id> [--format markdown|json]');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const { explain } = await import('./commands/explain.js');
|
|
286
|
+
await explain(config, id, {
|
|
287
|
+
project,
|
|
288
|
+
format: (flags.format as 'markdown' | 'json') || undefined,
|
|
289
|
+
});
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
233
293
|
case 'resolve': {
|
|
234
294
|
const id = positional[1];
|
|
235
295
|
if (!id) {
|
|
@@ -288,6 +348,16 @@ async function main() {
|
|
|
288
348
|
break;
|
|
289
349
|
}
|
|
290
350
|
|
|
351
|
+
case 'changelog': {
|
|
352
|
+
const { changelog } = await import('./commands/changelog.js');
|
|
353
|
+
await changelog(config, {
|
|
354
|
+
project,
|
|
355
|
+
since: flags.since,
|
|
356
|
+
format: (flags.format as 'markdown' | 'json' | 'conventional') || undefined,
|
|
357
|
+
});
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
291
361
|
case 'visual-diff': {
|
|
292
362
|
const { visualDiff } = await import('./commands/visual-diff.js');
|
|
293
363
|
await visualDiff(config, {
|
|
@@ -300,6 +370,21 @@ async function main() {
|
|
|
300
370
|
break;
|
|
301
371
|
}
|
|
302
372
|
|
|
373
|
+
case 'batch': {
|
|
374
|
+
const source = positional[1];
|
|
375
|
+
if (!source) {
|
|
376
|
+
console.error('Usage: uinotes batch <file|-> [--concurrency 5] [--dry-run]');
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
const { batch } = await import('./commands/batch.js');
|
|
380
|
+
await batch(config, source, {
|
|
381
|
+
project,
|
|
382
|
+
concurrency: flags.concurrency ? parseInt(flags.concurrency) : undefined,
|
|
383
|
+
dryRun: flags['dry-run'] === 'true',
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
303
388
|
case 'resolve-by-selector': {
|
|
304
389
|
const sel = positional[1];
|
|
305
390
|
if (!sel) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upgraide/ui-notes-cli",
|
|
3
|
-
"version": "0.1
|
|
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"],
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"*.ts",
|
|
12
12
|
"commands/*.ts",
|
|
13
13
|
"formatters/*.ts",
|
|
14
|
-
"resolvers/*.ts"
|
|
14
|
+
"resolvers/*.ts",
|
|
15
|
+
"schemas/*.json",
|
|
16
|
+
"skills/*.md"
|
|
15
17
|
],
|
|
16
18
|
"engines": {
|
|
17
19
|
"bun": ">=1.0.0"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Comment",
|
|
4
|
+
"description": "A comment on a note",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "string", "description": "Unique comment identifier" },
|
|
8
|
+
"note_id": { "type": "string", "description": "ID of the parent note" },
|
|
9
|
+
"author": { "type": "string", "description": "Comment author" },
|
|
10
|
+
"body": { "type": "string", "description": "Comment text" },
|
|
11
|
+
"created_at": { "type": "string", "format": "date-time", "description": "Creation timestamp" }
|
|
12
|
+
},
|
|
13
|
+
"required": ["id", "note_id", "author", "body", "created_at"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Note",
|
|
4
|
+
"description": "A UI feedback note attached to a page element",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "string", "description": "Unique note identifier" },
|
|
8
|
+
"project": { "type": "string", "description": "Project slug this note belongs to" },
|
|
9
|
+
"type": { "type": "string", "enum": ["bug", "ux", "feature", "question"], "description": "Type of feedback" },
|
|
10
|
+
"status": { "type": "string", "enum": ["open", "resolved", "wontfix"], "description": "Current status" },
|
|
11
|
+
"body": { "type": "string", "description": "Note content/description" },
|
|
12
|
+
"url": { "type": "string", "description": "Page URL where the note was created" },
|
|
13
|
+
"component": { "type": ["string", "null"], "description": "Component name from data attributes" },
|
|
14
|
+
"text_content": { "type": ["string", "null"], "description": "Text content of the annotated element" },
|
|
15
|
+
"selector": { "type": ["string", "null"], "description": "CSS selector for the annotated element" },
|
|
16
|
+
"tag": { "type": ["string", "null"], "description": "HTML tag of the annotated element" },
|
|
17
|
+
"author": { "type": ["string", "null"], "description": "Author email or name" },
|
|
18
|
+
"screenshot_url": { "type": ["string", "null"], "description": "URL to the screenshot capture" },
|
|
19
|
+
"resolution": { "type": ["string", "null"], "description": "Resolution message when resolved" },
|
|
20
|
+
"resolved_at": { "type": ["string", "null"], "format": "date-time", "description": "When the note was resolved" },
|
|
21
|
+
"created_at": { "type": "string", "format": "date-time", "description": "Creation timestamp" }
|
|
22
|
+
},
|
|
23
|
+
"required": ["id", "project", "type", "status", "body", "url", "created_at"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Project",
|
|
4
|
+
"description": "A UI Notes project that groups notes",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "string", "description": "Unique project identifier" },
|
|
8
|
+
"slug": { "type": "string", "description": "URL-friendly project identifier" },
|
|
9
|
+
"name": { "type": "string", "description": "Display name" },
|
|
10
|
+
"url_patterns": { "type": "array", "items": { "type": "string" }, "description": "URL glob patterns for auto-matching" },
|
|
11
|
+
"archived": { "type": "boolean", "description": "Whether the project is archived" },
|
|
12
|
+
"created_at": { "type": "string", "format": "date-time", "description": "Creation timestamp" }
|
|
13
|
+
},
|
|
14
|
+
"required": ["id", "slug", "name", "created_at"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Webhook",
|
|
4
|
+
"description": "A webhook endpoint for receiving note events",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "string", "description": "Unique webhook identifier" },
|
|
8
|
+
"project_id": { "type": "string", "description": "Project this webhook belongs to" },
|
|
9
|
+
"url": { "type": "string", "description": "Webhook delivery URL" },
|
|
10
|
+
"secret": { "type": ["string", "null"], "description": "HMAC signing secret" },
|
|
11
|
+
"events": { "type": "array", "items": { "type": "string" }, "description": "Events to subscribe to" },
|
|
12
|
+
"active": { "type": "boolean", "description": "Whether the webhook is active" },
|
|
13
|
+
"created_at": { "type": "string", "format": "date-time", "description": "Creation timestamp" }
|
|
14
|
+
},
|
|
15
|
+
"required": ["id", "project_id", "url", "events", "active", "created_at"]
|
|
16
|
+
}
|
|
@@ -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.
|