// Redmine REST API utility // Usage: node deploy/redmine-api.mjs [args...] // Allow self-signed / mismatched certs when calling via IP process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const BASE_URL = 'https://81.200.150.204'; const API_KEY = 'fca900011c01bf5c83862a5107c4d798fb6a4ef8'; async function request(method, path, body) { const url = `${BASE_URL}${path}`; const opts = { method, headers: { 'X-Redmine-API-Key': API_KEY, 'Content-Type': 'application/json', }, }; if (body) opts.body = JSON.stringify(body); const res = await fetch(url, opts); const text = await res.text(); if (!res.ok) { throw new Error(`${res.status} ${res.statusText}: ${text}`); } return text ? JSON.parse(text) : null; } // --- Projects --- export async function listProjects() { const data = await request('GET', '/projects.json'); return data.projects; } export async function getProject(idOrIdentifier) { const data = await request('GET', `/projects/${idOrIdentifier}.json`); return data.project; } export async function createProject(name, identifier, description = '') { const data = await request('POST', '/projects.json', { project: { name, identifier, description }, }); return data.project; } // --- Issues --- export async function listIssues(projectIdentifier, params = {}) { const query = new URLSearchParams(params); if (projectIdentifier) query.set('project_id', projectIdentifier); const data = await request('GET', `/issues.json?${query}`); return data.issues; } export async function getIssue(id) { const data = await request('GET', `/issues/${id}.json`); return data.issue; } export async function createIssue(projectIdentifier, subject, description = '', extras = {}) { const data = await request('POST', '/issues.json', { issue: { project_id: projectIdentifier, subject, description, ...extras, }, }); return data.issue; } export async function updateIssue(id, fields) { await request('PUT', `/issues/${id}.json`, { issue: fields }); return { id, ...fields }; } export async function deleteIssue(id) { await request('DELETE', `/issues/${id}.json`); return { id, deleted: true }; } // --- Wiki --- export async function listWikiPages(projectId) { const data = await request('GET', `/projects/${projectId}/wiki/index.json`); return data.wiki_pages; } export async function getWikiPage(projectId, title) { const data = await request('GET', `/projects/${projectId}/wiki/${encodeURIComponent(title)}.json`); return data.wiki_page; } export async function putWikiPage(projectId, title, text, comments = '') { const body = { wiki_page: { text } }; if (comments) body.wiki_page.comments = comments; await request('PUT', `/projects/${projectId}/wiki/${encodeURIComponent(title)}.json`, body); return { project: projectId, title, updated: true }; } export async function deleteWikiPage(projectId, title) { await request('DELETE', `/projects/${projectId}/wiki/${encodeURIComponent(title)}.json`); return { project: projectId, title, deleted: true }; } // --- Trackers, Statuses, Priorities (reference data) --- export async function listTrackers() { const data = await request('GET', '/trackers.json'); return data.trackers; } export async function listStatuses() { const data = await request('GET', '/issue_statuses.json'); return data.issue_statuses; } // --- CLI --- const commands = { 'list-projects': async () => { const projects = await listProjects(); console.table(projects.map(p => ({ id: p.id, name: p.name, identifier: p.identifier }))); }, 'get-project': async ([id]) => { const project = await getProject(id); console.log(JSON.stringify(project, null, 2)); }, 'create-project': async ([name, identifier, description]) => { const project = await createProject(name, identifier, description); console.log('Created project:', project.id, project.identifier); }, 'list-issues': async ([project, ...rest]) => { const params = {}; for (const arg of rest) { const [k, v] = arg.split('='); params[k] = v; } const issues = await listIssues(project, params); console.table(issues.map(i => ({ id: i.id, tracker: i.tracker?.name, status: i.status?.name, subject: i.subject, }))); }, 'get-issue': async ([id]) => { const issue = await getIssue(id); console.log(JSON.stringify(issue, null, 2)); }, 'create-issue': async ([project, subject, description]) => { const issue = await createIssue(project, subject, description); console.log('Created issue:', issue.id, '-', issue.subject); }, 'update-issue': async ([id, ...fields]) => { const data = {}; for (const f of fields) { const [k, v] = f.split('='); data[k] = v; } const result = await updateIssue(id, data); console.log('Updated issue:', result.id); }, 'delete-issue': async ([id]) => { await deleteIssue(id); console.log('Deleted issue:', id); }, 'list-trackers': async () => { const trackers = await listTrackers(); console.table(trackers.map(t => ({ id: t.id, name: t.name }))); }, 'list-statuses': async () => { const statuses = await listStatuses(); console.table(statuses.map(s => ({ id: s.id, name: s.name }))); }, 'list-wiki': async ([project]) => { const pages = await listWikiPages(project); console.table(pages.map(p => ({ title: p.title, version: p.version, created: p.created_on }))); }, 'get-wiki': async ([project, title]) => { const page = await getWikiPage(project, title); console.log(`# ${page.title} (v${page.version})\n`); console.log(page.text); }, 'put-wiki': async ([project, title, ...textParts]) => { const text = textParts.join(' '); const result = await putWikiPage(project, title, text, 'Updated via CLI'); console.log('Updated wiki page:', result.title); }, 'delete-wiki': async ([project, title]) => { await deleteWikiPage(project, title); console.log('Deleted wiki page:', title); }, }; // Only run CLI when this file is executed directly const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/').replace(/^.*:/, '')); if (isMain) { const [command, ...args] = process.argv.slice(2); if (!command || !commands[command]) { console.log('Usage: node deploy/redmine-api.mjs [args...]'); console.log('Commands:', Object.keys(commands).join(', ')); process.exit(1); } try { await commands[command](args); } catch (e) { console.error('Error:', e.message); process.exit(1); } }