@upgraide/ui-notes-cli 0.1.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/api.ts +36 -0
- package/commands/config.ts +31 -0
- package/commands/login.ts +76 -0
- package/commands/projects.ts +135 -0
- package/commands/pull.ts +22 -0
- package/commands/resolve.ts +30 -0
- package/config.ts +52 -0
- package/index.ts +231 -0
- package/package.json +17 -0
package/api.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
|
|
3
|
+
export async function apiFetch(
|
|
4
|
+
config: Config,
|
|
5
|
+
path: string,
|
|
6
|
+
options: { method?: string; body?: unknown; project?: string } = {},
|
|
7
|
+
): Promise<unknown> {
|
|
8
|
+
const { method = 'GET', body, project } = options;
|
|
9
|
+
|
|
10
|
+
const headers: Record<string, string> = {
|
|
11
|
+
'x-api-key': config.apiKey,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const targetProject = project || config.defaultProject;
|
|
15
|
+
if (targetProject) {
|
|
16
|
+
headers['x-project'] = targetProject;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (body) {
|
|
20
|
+
headers['Content-Type'] = 'application/json';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const res = await fetch(`${config.apiUrl}${path}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const json = (await res.json()) as { ok: boolean; error?: string; data?: unknown };
|
|
30
|
+
|
|
31
|
+
if (!json.ok) {
|
|
32
|
+
throw new Error(json.error || `API error (${res.status})`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return json;
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getConfig, getConfigPath, setConfigValue } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const VALID_KEYS = ['apiUrl', 'apiKey', 'defaultProject'];
|
|
4
|
+
|
|
5
|
+
function redactKey(value: string): string {
|
|
6
|
+
if (!value || value.length <= 4) return '****';
|
|
7
|
+
return value.slice(0, 4) + '****';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function configShow(): void {
|
|
11
|
+
const config = getConfig();
|
|
12
|
+
const path = getConfigPath();
|
|
13
|
+
console.log(
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
path,
|
|
16
|
+
config: {
|
|
17
|
+
...config,
|
|
18
|
+
apiKey: redactKey(config.apiKey),
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function configSet(key: string, value: string): void {
|
|
25
|
+
if (!VALID_KEYS.includes(key)) {
|
|
26
|
+
console.error(`Invalid config key: ${key}. Valid keys: ${VALID_KEYS.join(', ')}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const updated = setConfigValue(key, value);
|
|
30
|
+
console.log(JSON.stringify({ ok: true, key, value: key === 'apiKey' ? redactKey(value) : value }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { setConfigValue } from '../config.js';
|
|
3
|
+
|
|
4
|
+
export async function login(config: Config) {
|
|
5
|
+
const readline = await import('readline');
|
|
6
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
|
|
8
|
+
const ask = (q: string): Promise<string> =>
|
|
9
|
+
new Promise((resolve) => rl.question(q, resolve));
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const apiUrl = config.apiUrl || (await ask('API URL (default: http://localhost:3000): ')) || 'http://localhost:3000';
|
|
13
|
+
const email = await ask('Email: ');
|
|
14
|
+
const password = await ask('Password: ');
|
|
15
|
+
|
|
16
|
+
if (!email || !password) {
|
|
17
|
+
console.error('Email and password are required.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Sign in
|
|
22
|
+
const signInRes = await fetch(`${apiUrl}/api/auth/sign-in/email`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ email, password }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!signInRes.ok) {
|
|
29
|
+
const err = await signInRes.json().catch(() => ({}));
|
|
30
|
+
console.error('Login failed:', (err as any).message || signInRes.statusText);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Extract session cookie
|
|
35
|
+
const cookies = signInRes.headers.getSetCookie?.() || [];
|
|
36
|
+
const sessionCookie = cookies.find((c) => c.startsWith('better-auth.session_token='));
|
|
37
|
+
|
|
38
|
+
if (!sessionCookie) {
|
|
39
|
+
console.error('Login succeeded but no session token received.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cookieValue = sessionCookie.split(';')[0];
|
|
44
|
+
|
|
45
|
+
// Create an API key using the session
|
|
46
|
+
const keyName = `cli-${new Date().toISOString().slice(0, 10)}`;
|
|
47
|
+
const createKeyRes = await fetch(`${apiUrl}/api/auth/api-key/create`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Cookie: cookieValue,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ name: keyName, prefix: 'uin_' }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!createKeyRes.ok) {
|
|
57
|
+
const err = await createKeyRes.json().catch(() => ({}));
|
|
58
|
+
console.error('Failed to create API key:', (err as any).message || createKeyRes.statusText);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const keyData = await createKeyRes.json() as { key?: string };
|
|
63
|
+
if (!keyData.key) {
|
|
64
|
+
console.error('Failed to create API key: no key in response');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Save config
|
|
69
|
+
setConfigValue('apiUrl', apiUrl);
|
|
70
|
+
setConfigValue('apiKey', keyData.key);
|
|
71
|
+
|
|
72
|
+
console.log(JSON.stringify({ ok: true, message: 'Logged in and API key saved', keyName }));
|
|
73
|
+
} finally {
|
|
74
|
+
rl.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { apiFetch } from '../api.js';
|
|
3
|
+
|
|
4
|
+
interface Project {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
urlPatterns: string[];
|
|
8
|
+
createdAt: string;
|
|
9
|
+
archived?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ProjectsResponse {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
data: { projects: Project[] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProjectResponse {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
data: Project;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function listProjects(config: Config, includeArchived = false): Promise<void> {
|
|
23
|
+
const url = includeArchived ? '/projects?archived=true' : '/projects';
|
|
24
|
+
const data = await apiFetch(config, url) as ProjectsResponse;
|
|
25
|
+
|
|
26
|
+
if (!data.data?.projects?.length) {
|
|
27
|
+
console.log('No projects found. Create one with: uinotes projects create <slug> <name> --url <pattern>');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('\nProjects:\n');
|
|
32
|
+
for (const p of data.data.projects) {
|
|
33
|
+
const archived = p.archived ? ' (archived)' : '';
|
|
34
|
+
console.log(` ${p.slug}${archived}`);
|
|
35
|
+
console.log(` Name: ${p.name}`);
|
|
36
|
+
if (p.urlPatterns?.length) {
|
|
37
|
+
console.log(` URLs: ${p.urlPatterns.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
console.log(` Created: ${p.createdAt}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function createProject(config: Config, slug: string, name: string, urlPatterns: string[] = []): Promise<void> {
|
|
44
|
+
const data = await apiFetch(config, '/projects', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify({ slug, name, urlPatterns }),
|
|
47
|
+
}) as ProjectResponse;
|
|
48
|
+
|
|
49
|
+
console.log(`✓ Created project: ${data.data.slug}`);
|
|
50
|
+
console.log(` Name: ${data.data.name}`);
|
|
51
|
+
if (data.data.urlPatterns?.length) {
|
|
52
|
+
console.log(` URLs: ${data.data.urlPatterns.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function updateProject(
|
|
57
|
+
config: Config,
|
|
58
|
+
slug: string,
|
|
59
|
+
updates: { name?: string; archived?: boolean }
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const data = await apiFetch(config, `/projects/${slug}`, {
|
|
62
|
+
method: 'PATCH',
|
|
63
|
+
body: JSON.stringify(updates),
|
|
64
|
+
}) as ProjectResponse;
|
|
65
|
+
|
|
66
|
+
console.log(`✓ Updated project: ${data.data.slug}`);
|
|
67
|
+
if (updates.name) console.log(` Name: ${data.data.name}`);
|
|
68
|
+
if (updates.archived !== undefined) {
|
|
69
|
+
console.log(` Archived: ${data.data.archived}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function deleteProject(config: Config, slug: string): Promise<void> {
|
|
74
|
+
await apiFetch(config, `/projects/${slug}`, {
|
|
75
|
+
method: 'DELETE',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(`✓ Archived project: ${slug}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getProject(config: Config, slug: string): Promise<void> {
|
|
82
|
+
const data = await apiFetch(config, `/projects/${slug}`) as ProjectResponse;
|
|
83
|
+
|
|
84
|
+
console.log(`\nProject: ${data.data.slug}\n`);
|
|
85
|
+
console.log(` Name: ${data.data.name}`);
|
|
86
|
+
if (data.data.urlPatterns?.length) {
|
|
87
|
+
console.log(` URL patterns:`);
|
|
88
|
+
for (const p of data.data.urlPatterns) {
|
|
89
|
+
console.log(` - ${p}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.log(` URL patterns: (none)`);
|
|
93
|
+
}
|
|
94
|
+
console.log(` Created: ${data.data.createdAt}`);
|
|
95
|
+
console.log(` Archived: ${data.data.archived || false}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function addUrlPattern(config: Config, slug: string, pattern: string): Promise<void> {
|
|
99
|
+
// First get current patterns
|
|
100
|
+
const current = await apiFetch(config, `/projects/${slug}`) as ProjectResponse;
|
|
101
|
+
const patterns = current.data.urlPatterns || [];
|
|
102
|
+
|
|
103
|
+
if (patterns.includes(pattern)) {
|
|
104
|
+
console.log(`Pattern already exists: ${pattern}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
patterns.push(pattern);
|
|
109
|
+
|
|
110
|
+
const data = await apiFetch(config, `/projects/${slug}`, {
|
|
111
|
+
method: 'PATCH',
|
|
112
|
+
body: JSON.stringify({ urlPatterns: patterns }),
|
|
113
|
+
}) as ProjectResponse;
|
|
114
|
+
|
|
115
|
+
console.log(`✓ Added URL pattern to ${slug}: ${pattern}`);
|
|
116
|
+
console.log(` All patterns: ${data.data.urlPatterns?.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function removeUrlPattern(config: Config, slug: string, pattern: string): Promise<void> {
|
|
120
|
+
// First get current patterns
|
|
121
|
+
const current = await apiFetch(config, `/projects/${slug}`) as ProjectResponse;
|
|
122
|
+
const patterns = (current.data.urlPatterns || []).filter((p: string) => p !== pattern);
|
|
123
|
+
|
|
124
|
+
const data = await apiFetch(config, `/projects/${slug}`, {
|
|
125
|
+
method: 'PATCH',
|
|
126
|
+
body: JSON.stringify({ urlPatterns: patterns }),
|
|
127
|
+
}) as ProjectResponse;
|
|
128
|
+
|
|
129
|
+
console.log(`✓ Removed URL pattern from ${slug}: ${pattern}`);
|
|
130
|
+
if (data.data.urlPatterns?.length) {
|
|
131
|
+
console.log(` Remaining: ${data.data.urlPatterns.join(', ')}`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(` No patterns remaining`);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/commands/pull.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { apiFetch } from '../api.js';
|
|
3
|
+
|
|
4
|
+
interface PullOptions {
|
|
5
|
+
project?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
limit?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function pull(config: Config, opts: PullOptions): Promise<void> {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
if (opts.status) params.set('status', opts.status);
|
|
14
|
+
if (opts.type) params.set('type', opts.type);
|
|
15
|
+
if (opts.limit) params.set('limit', opts.limit);
|
|
16
|
+
|
|
17
|
+
const qs = params.toString();
|
|
18
|
+
const path = `/notes${qs ? `?${qs}` : ''}`;
|
|
19
|
+
|
|
20
|
+
const data = await apiFetch(config, path, { project: opts.project });
|
|
21
|
+
console.log(JSON.stringify(data));
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import { apiFetch } from '../api.js';
|
|
3
|
+
|
|
4
|
+
export async function resolve(
|
|
5
|
+
config: Config,
|
|
6
|
+
id: string,
|
|
7
|
+
message: string | undefined,
|
|
8
|
+
project?: string,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const data = await apiFetch(config, `/notes/${id}`, {
|
|
11
|
+
method: 'PATCH',
|
|
12
|
+
body: { status: 'resolved', resolution: message },
|
|
13
|
+
project,
|
|
14
|
+
});
|
|
15
|
+
console.log(JSON.stringify(data));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function wontfix(
|
|
19
|
+
config: Config,
|
|
20
|
+
id: string,
|
|
21
|
+
message: string | undefined,
|
|
22
|
+
project?: string,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const data = await apiFetch(config, `/notes/${id}`, {
|
|
25
|
+
method: 'PATCH',
|
|
26
|
+
body: { status: 'wontfix', resolution: message },
|
|
27
|
+
project,
|
|
28
|
+
});
|
|
29
|
+
console.log(JSON.stringify(data));
|
|
30
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
defaultProject?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const GLOBAL_PATH = join(homedir(), '.uinotes.json');
|
|
12
|
+
const LOCAL_PATH = join(process.cwd(), '.uinotes.json');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG: Config = {
|
|
15
|
+
apiUrl: 'http://localhost:3000',
|
|
16
|
+
apiKey: '',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function readJsonFile(path: string): Config | null {
|
|
20
|
+
if (!existsSync(path)) return null;
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getConfigPath(): string {
|
|
25
|
+
if (existsSync(LOCAL_PATH)) return LOCAL_PATH;
|
|
26
|
+
return GLOBAL_PATH;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getConfig(): Config {
|
|
30
|
+
// Per-repo config takes priority
|
|
31
|
+
const local = readJsonFile(LOCAL_PATH);
|
|
32
|
+
if (local) return { ...DEFAULT_CONFIG, ...local };
|
|
33
|
+
|
|
34
|
+
const global = readJsonFile(GLOBAL_PATH);
|
|
35
|
+
if (global) return { ...DEFAULT_CONFIG, ...global };
|
|
36
|
+
|
|
37
|
+
// Create default global config
|
|
38
|
+
writeFileSync(GLOBAL_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n');
|
|
39
|
+
return DEFAULT_CONFIG;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setConfigValue(key: string, value: string): Config {
|
|
43
|
+
const path = getConfigPath();
|
|
44
|
+
const config = existsSync(path)
|
|
45
|
+
? JSON.parse(readFileSync(path, 'utf-8'))
|
|
46
|
+
: { ...DEFAULT_CONFIG };
|
|
47
|
+
|
|
48
|
+
(config as any)[key] = value;
|
|
49
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
50
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + '\n');
|
|
51
|
+
return config;
|
|
52
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { getConfig } from './config.js';
|
|
4
|
+
import { configShow, configSet } from './commands/config.js';
|
|
5
|
+
import { listProjects, createProject, updateProject, deleteProject, getProject, addUrlPattern, removeUrlPattern } from './commands/projects.js';
|
|
6
|
+
import { pull } from './commands/pull.js';
|
|
7
|
+
import { resolve, wontfix } from './commands/resolve.js';
|
|
8
|
+
import { login } from './commands/login.js';
|
|
9
|
+
|
|
10
|
+
const HELP = `uinotes - CLI for UI Notes
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
uinotes <command> [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
projects List all projects
|
|
17
|
+
projects create <slug> <name> Create a new project
|
|
18
|
+
projects get <slug> Get project details
|
|
19
|
+
projects rename <slug> <name> Rename a project
|
|
20
|
+
projects add-url <slug> <pattern> Add URL pattern to project
|
|
21
|
+
projects remove-url <slug> <pattern> Remove URL pattern
|
|
22
|
+
projects delete <slug> Archive a project
|
|
23
|
+
login Login and create API key
|
|
24
|
+
pull Fetch open notes
|
|
25
|
+
resolve <id> Mark note as resolved
|
|
26
|
+
wontfix <id> Mark note as won't fix
|
|
27
|
+
config View/edit configuration
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--help, -h Show help
|
|
31
|
+
--project <name> Override project for this request
|
|
32
|
+
--archived Include archived projects in list
|
|
33
|
+
--url <pattern> URL pattern for project create (repeatable)`;
|
|
34
|
+
|
|
35
|
+
function parseFlags(args: string[]): { flags: Record<string, string>; arrays: Record<string, string[]>; positional: string[] } {
|
|
36
|
+
const flags: Record<string, string> = {};
|
|
37
|
+
const arrays: Record<string, string[]> = { url: [] };
|
|
38
|
+
const positional: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (arg.startsWith('--')) {
|
|
43
|
+
const key = arg.slice(2);
|
|
44
|
+
const next = args[i + 1];
|
|
45
|
+
if (next && !next.startsWith('--')) {
|
|
46
|
+
// Repeatable flags go into arrays
|
|
47
|
+
if (key === 'url') {
|
|
48
|
+
arrays.url.push(next);
|
|
49
|
+
} else {
|
|
50
|
+
flags[key] = next;
|
|
51
|
+
}
|
|
52
|
+
i++;
|
|
53
|
+
} else {
|
|
54
|
+
flags[key] = 'true';
|
|
55
|
+
}
|
|
56
|
+
} else if (arg === '-h') {
|
|
57
|
+
flags['help'] = 'true';
|
|
58
|
+
} else {
|
|
59
|
+
positional.push(arg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { flags, arrays, positional };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
const args = process.argv.slice(2);
|
|
68
|
+
const { flags, arrays, positional } = parseFlags(args);
|
|
69
|
+
|
|
70
|
+
if (flags.help || positional.length === 0) {
|
|
71
|
+
console.log(HELP);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const command = positional[0];
|
|
76
|
+
|
|
77
|
+
// Login command doesn't need API key validation
|
|
78
|
+
if (command === 'login') {
|
|
79
|
+
const config = getConfig();
|
|
80
|
+
await login(config);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Config command doesn't need API key validation
|
|
85
|
+
if (command === 'config') {
|
|
86
|
+
const sub = positional[1];
|
|
87
|
+
if (sub === 'set') {
|
|
88
|
+
const key = positional[2];
|
|
89
|
+
const value = positional[3];
|
|
90
|
+
if (!key || !value) {
|
|
91
|
+
console.error('Usage: uinotes config set <key> <value>');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
configSet(key, value);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Default to show
|
|
98
|
+
configShow();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const config = getConfig();
|
|
103
|
+
if (!config.apiKey) {
|
|
104
|
+
console.error('No API key configured. Run: uinotes config set apiKey <your-key>');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const project = flags.project;
|
|
109
|
+
|
|
110
|
+
switch (command) {
|
|
111
|
+
case 'projects': {
|
|
112
|
+
const sub = positional[1];
|
|
113
|
+
|
|
114
|
+
if (!sub) {
|
|
115
|
+
// List projects
|
|
116
|
+
await listProjects(config, flags.archived === 'true');
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
switch (sub) {
|
|
121
|
+
case 'create': {
|
|
122
|
+
const slug = positional[2];
|
|
123
|
+
const name = positional.slice(3).join(' ') || positional[3];
|
|
124
|
+
if (!slug || !name) {
|
|
125
|
+
console.error('Usage: uinotes projects create <slug> <name> [--url <pattern>...]');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
await createProject(config, slug, name, arrays.url);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'get': {
|
|
133
|
+
const slug = positional[2];
|
|
134
|
+
if (!slug) {
|
|
135
|
+
console.error('Usage: uinotes projects get <slug>');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
await getProject(config, slug);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'rename': {
|
|
143
|
+
const slug = positional[2];
|
|
144
|
+
const name = positional.slice(3).join(' ') || positional[3];
|
|
145
|
+
if (!slug || !name) {
|
|
146
|
+
console.error('Usage: uinotes projects rename <slug> <new-name>');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
await updateProject(config, slug, { name });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'add-url': {
|
|
154
|
+
const slug = positional[2];
|
|
155
|
+
const pattern = positional[3];
|
|
156
|
+
if (!slug || !pattern) {
|
|
157
|
+
console.error('Usage: uinotes projects add-url <slug> <pattern>');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
await addUrlPattern(config, slug, pattern);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case 'remove-url': {
|
|
165
|
+
const slug = positional[2];
|
|
166
|
+
const pattern = positional[3];
|
|
167
|
+
if (!slug || !pattern) {
|
|
168
|
+
console.error('Usage: uinotes projects remove-url <slug> <pattern>');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
await removeUrlPattern(config, slug, pattern);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case 'delete': {
|
|
176
|
+
const slug = positional[2];
|
|
177
|
+
if (!slug) {
|
|
178
|
+
console.error('Usage: uinotes projects delete <slug>');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
await deleteProject(config, slug);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
console.error(`Unknown projects subcommand: ${sub}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'pull':
|
|
193
|
+
await pull(config, {
|
|
194
|
+
project,
|
|
195
|
+
status: flags.status,
|
|
196
|
+
type: flags.type,
|
|
197
|
+
limit: flags.limit,
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'resolve': {
|
|
202
|
+
const id = positional[1];
|
|
203
|
+
if (!id) {
|
|
204
|
+
console.error('Usage: uinotes resolve <id> [message]');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
await resolve(config, id, positional[2], project);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'wontfix': {
|
|
212
|
+
const id = positional[1];
|
|
213
|
+
if (!id) {
|
|
214
|
+
console.error('Usage: uinotes wontfix <id> [message]');
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
await wontfix(config, id, positional[2], project);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
default:
|
|
222
|
+
console.error(`Unknown command: ${command}`);
|
|
223
|
+
console.error('Run uinotes --help for usage');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
main().catch((err) => {
|
|
229
|
+
console.error(err.message);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@upgraide/ui-notes-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for UI Notes - pull and manage UI feedback notes",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": ["cli", "ui", "notes", "feedback", "ui-notes"],
|
|
7
|
+
"bin": {
|
|
8
|
+
"uinotes": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"*.ts",
|
|
12
|
+
"commands/*.ts"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"bun": ">=1.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|