jira.mjs
120 lines 4.4 KB
Raw
sha256:41d741fb345c4abdb640838aa3d847de02ccffd7a39fce04894e743e683b50d0 fix(security): pin patched transitive deps to clear Dependa… Human minor ⚠ breaking 7 days ago
1 /**
2 * Jira CSV export importer. Parses CSV from Jira Cloud/Server export.
3 * One note per issue; frontmatter: source: jira, source_id: issue key, project, summary; body: description + comments if present.
4 */
5
6 import fs from 'fs';
7 import path from 'path';
8 import { writeNote } from '../write.mjs';
9 import { normalizeSlug } from '../vault.mjs';
10 import { buildRowObjectForJson } from './tabular-import.mjs';
11
12 /**
13 * Parse a CSV line respecting quoted fields (handles commas inside quotes).
14 * @param {string} line
15 * @returns {string[]}
16 */
17 function parseCSVLine(line) {
18 const out = [];
19 let i = 0;
20 while (i < line.length) {
21 if (line[i] === '"') {
22 i++;
23 let field = '';
24 while (i < line.length) {
25 if (line[i] === '"') {
26 i++;
27 if (line[i] === '"') {
28 field += '"';
29 i++;
30 } else break;
31 } else {
32 field += line[i++];
33 }
34 }
35 out.push(field);
36 } else {
37 let field = '';
38 while (i < line.length && line[i] !== ',') {
39 field += line[i++];
40 }
41 out.push(field.trim());
42 if (line[i] === ',') i++;
43 }
44 }
45 return out;
46 }
47
48 /**
49 * @param {string} input - Path to Jira CSV file (or folder containing one .csv)
50 * @param {{ vaultPath: string, outputBase: string, project?: string, tags: string[], dryRun: boolean }} ctx
51 * @returns {Promise<{ imported: { path: string, source_id?: string }[], count: number }>}
52 */
53 export async function importJira(input, ctx) {
54 const { vaultPath, outputBase, project, tags, dryRun } = ctx;
55 const absInput = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
56 if (!fs.existsSync(absInput)) {
57 throw new Error(`Input not found: ${input}`);
58 }
59
60 let csvPath = absInput;
61 if (fs.statSync(absInput).isDirectory()) {
62 const files = fs.readdirSync(absInput).filter((f) => f.endsWith('.csv'));
63 if (files.length === 0) throw new Error('No .csv file found in folder. Export issues from Jira as CSV first.');
64 csvPath = path.join(absInput, files[0]);
65 } else if (!absInput.endsWith('.csv')) {
66 throw new Error('Jira import expects a .csv file (or folder containing one). Export from Jira: list or search → Export CSV.');
67 }
68
69 const raw = fs.readFileSync(csvPath, 'utf8');
70 const lines = raw.split(/\r?\n/).filter((l) => l.trim());
71 if (lines.length < 2) {
72 return { imported: [], count: 0 };
73 }
74
75 const header = parseCSVLine(lines[0]);
76 const keyIdx = header.findIndex((h) => /^(Issue key|Key|key)$/i.test(h));
77 const summaryIdx = header.findIndex((h) => /^Summary$/i.test(h));
78 const descIdx = header.findIndex((h) => /^Description$/i.test(h));
79 const projIdx = header.findIndex((h) => /^Project$/i.test(h));
80
81 const imported = [];
82 const now = new Date().toISOString().slice(0, 10);
83
84 const nCols = header.length;
85 for (let rowNum = 1; rowNum < lines.length; rowNum++) {
86 const row = parseCSVLine(lines[rowNum]);
87 if (row.length < 2) continue;
88 const issueKey = keyIdx >= 0 ? (row[keyIdx] || '').trim() : `row-${rowNum}`;
89 if (!issueKey) continue;
90 const summary = summaryIdx >= 0 ? (row[summaryIdx] || '').trim() : '';
91 const description = descIdx >= 0 ? (row[descIdx] || '').trim() : '';
92 const proj = projIdx >= 0 ? (row[projIdx] || '').trim() : '';
93 const labelHeaders = header.map((h, i) => h.trim() || `column_${i}`);
94 const rowCells = Array.from({ length: nCols }, (_, i) => (i < row.length ? row[i] : ''));
95 const fullRowJson = buildRowObjectForJson(labelHeaders, rowCells);
96 const jsonStr = JSON.stringify(fullRowJson, null, 2);
97 let body = summary ? `# ${summary}\n\n${description}` : description || '(no description)';
98 body += '\n\n## All CSV fields (JSON)\n\n```json\n' + jsonStr + '\n```\n';
99 const safeName = issueKey.replace(/[^a-zA-Z0-9-_]/g, '_') + '.md';
100 const outputRel = path.join(outputBase, safeName).replace(/\\/g, '/');
101
102 const frontmatter = {
103 source: 'jira',
104 source_id: issueKey,
105 date: now,
106 import_column_headers: JSON.stringify(labelHeaders),
107 ...(summary && { title: summary }),
108 ...(project && { project: normalizeSlug(project) }),
109 ...(proj && !project && { project: normalizeSlug(proj) }),
110 ...(tags.length && { tags }),
111 };
112
113 if (!dryRun) {
114 writeNote(vaultPath, outputRel, { body, frontmatter });
115 }
116 imported.push({ path: outputRel, source_id: issueKey });
117 }
118
119 return { imported, count: imported.length };
120 }
File History 1 commit
sha256:41d741fb345c4abdb640838aa3d847de02ccffd7a39fce04894e743e683b50d0 fix(security): pin patched transitive deps to clear Dependa… Human minor 7 days ago