@upgraide/ui-notes-cli 0.1.3 → 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/commands/batch.ts +116 -0
- package/commands/changelog.ts +150 -0
- package/commands/explain.ts +67 -0
- package/commands/schema.ts +33 -0
- package/index.ts +56 -2
- package/package.json +3 -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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,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/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';
|
|
@@ -25,13 +25,17 @@ Commands:
|
|
|
25
25
|
login Login and create API key
|
|
26
26
|
pull Fetch open notes
|
|
27
27
|
context AI-optimized digest of open notes
|
|
28
|
+
explain <id> Full context for a single note
|
|
28
29
|
resolve <id> Mark note as resolved
|
|
29
30
|
wontfix <id> Mark note as won't fix
|
|
30
31
|
comment <id> "text" Add a comment to a note
|
|
31
32
|
comments <id> List comments for a note
|
|
32
33
|
resolve-by-component <name> [msg] Bulk resolve notes by component
|
|
33
34
|
resolve-by-selector <sel> [msg] Bulk resolve notes by selector
|
|
35
|
+
changelog Generate changelog from resolved notes
|
|
34
36
|
visual-diff Capture & compare page screenshots
|
|
37
|
+
batch <file|-> Execute NDJSON batch operations
|
|
38
|
+
schema List or print API resource schemas
|
|
35
39
|
config View/edit configuration
|
|
36
40
|
|
|
37
41
|
Options:
|
|
@@ -47,7 +51,11 @@ Options:
|
|
|
47
51
|
--root-dir <dir> Source root for file resolution (default: cwd)
|
|
48
52
|
--compare Compare current screenshots against previous (visual-diff)
|
|
49
53
|
--width <px> Viewport width for screenshots (default: 1280)
|
|
50
|
-
--full-page Capture full page height (default: true)
|
|
54
|
+
--full-page Capture full page height (default: true)
|
|
55
|
+
--since <ref> Git ref or ISO date for changelog start
|
|
56
|
+
--resource <name> Resource name for schema command
|
|
57
|
+
--concurrency <n> Concurrent batch operations (default: 5)
|
|
58
|
+
--dry-run Preview batch operations without executing`;
|
|
51
59
|
|
|
52
60
|
function parseFlags(args: string[]): { flags: Record<string, string>; arrays: Record<string, string[]>; positional: string[] } {
|
|
53
61
|
const flags: Record<string, string> = {};
|
|
@@ -116,6 +124,13 @@ async function main() {
|
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
126
|
|
|
127
|
+
// Schema command is local-only, no API key needed
|
|
128
|
+
if (command === 'schema') {
|
|
129
|
+
const { schema } = await import('./commands/schema.js');
|
|
130
|
+
await schema(getConfig(), flags.resource);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
const config = getConfig();
|
|
120
135
|
if (!config.apiKey) {
|
|
121
136
|
console.error('No API key configured. Run: uinotes config set apiKey <your-key>');
|
|
@@ -230,6 +245,20 @@ async function main() {
|
|
|
230
245
|
});
|
|
231
246
|
break;
|
|
232
247
|
|
|
248
|
+
case 'explain': {
|
|
249
|
+
const id = positional[1];
|
|
250
|
+
if (!id) {
|
|
251
|
+
console.error('Usage: uinotes explain <note-id> [--format markdown|json]');
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
const { explain } = await import('./commands/explain.js');
|
|
255
|
+
await explain(config, id, {
|
|
256
|
+
project,
|
|
257
|
+
format: (flags.format as 'markdown' | 'json') || undefined,
|
|
258
|
+
});
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
233
262
|
case 'resolve': {
|
|
234
263
|
const id = positional[1];
|
|
235
264
|
if (!id) {
|
|
@@ -288,6 +317,16 @@ async function main() {
|
|
|
288
317
|
break;
|
|
289
318
|
}
|
|
290
319
|
|
|
320
|
+
case 'changelog': {
|
|
321
|
+
const { changelog } = await import('./commands/changelog.js');
|
|
322
|
+
await changelog(config, {
|
|
323
|
+
project,
|
|
324
|
+
since: flags.since,
|
|
325
|
+
format: (flags.format as 'markdown' | 'json' | 'conventional') || undefined,
|
|
326
|
+
});
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
291
330
|
case 'visual-diff': {
|
|
292
331
|
const { visualDiff } = await import('./commands/visual-diff.js');
|
|
293
332
|
await visualDiff(config, {
|
|
@@ -300,6 +339,21 @@ async function main() {
|
|
|
300
339
|
break;
|
|
301
340
|
}
|
|
302
341
|
|
|
342
|
+
case 'batch': {
|
|
343
|
+
const source = positional[1];
|
|
344
|
+
if (!source) {
|
|
345
|
+
console.error('Usage: uinotes batch <file|-> [--concurrency 5] [--dry-run]');
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
const { batch } = await import('./commands/batch.js');
|
|
349
|
+
await batch(config, source, {
|
|
350
|
+
project,
|
|
351
|
+
concurrency: flags.concurrency ? parseInt(flags.concurrency) : undefined,
|
|
352
|
+
dryRun: flags['dry-run'] === 'true',
|
|
353
|
+
});
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
303
357
|
case 'resolve-by-selector': {
|
|
304
358
|
const sel = positional[1];
|
|
305
359
|
if (!sel) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upgraide/ui-notes-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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,8 @@
|
|
|
11
11
|
"*.ts",
|
|
12
12
|
"commands/*.ts",
|
|
13
13
|
"formatters/*.ts",
|
|
14
|
-
"resolvers/*.ts"
|
|
14
|
+
"resolvers/*.ts",
|
|
15
|
+
"schemas/*.json"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
17
18
|
"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
|
+
}
|