@sitar_fiercer4c/skills 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/LICENSE +5 -0
- package/README.md +75 -0
- package/bin/install.js +45 -0
- package/package.json +29 -0
- package/skills/architecture-walkthrough/SKILL.md +223 -0
- package/skills/architecture-walkthrough/references/sections.md +29 -0
- package/skills/architecture-walkthrough/scripts/check_structure.py +200 -0
- package/skills/autotest-webapp-ui/SKILL.md +58 -0
- package/skills/backend-code-review/SKILL.md +386 -0
- package/skills/backend-code-review/references/report-format.md +333 -0
- package/skills/backend-code-review/scripts/list_routes.py +269 -0
- package/skills/backend-code-review/scripts/sweep.py +550 -0
- package/skills/backend-code-review/scripts/verify_citations.py +201 -0
- package/skills/be-brief/SKILL.md +18 -0
- package/skills/clarke-list-excel/SKILL.md +51 -0
- package/skills/clarke-list-excel/references/output-schema.md +125 -0
- package/skills/clarke-list-excel/scripts/clarke_common.py +251 -0
- package/skills/clarke-list-excel/scripts/clarke_extract.py +487 -0
- package/skills/clarke-list-excel/scripts/load_clarke.py +322 -0
- package/skills/clarke-list-excel/scripts/run_all.py +63 -0
- package/skills/datalab-api/SKILL.md +163 -0
- package/skills/datalab-api/references/parameters-and-payload.md +121 -0
- package/skills/datalab-api/references/table-selection.md +35 -0
- package/skills/datalab-api/scripts/datalab_tables.py +365 -0
- package/skills/find-test-seam/SKILL.md +41 -0
- package/skills/frontend-code-review/SKILL.md +247 -0
- package/skills/frontend-code-review-2/SKILL.md +192 -0
- package/skills/frontend-code-review-2/scripts/fetch_pr_comments.py +65 -0
- package/skills/frontend-code-review-2/scripts/render_report.py +139 -0
- package/skills/murtaza-breif/SKILL.md +143 -0
- package/skills/murtaza-breif/scripts/save_brief.py +128 -0
- package/skills/pdf-to-json/SKILL.md +42 -0
- package/skills/pdf-to-json/references/output-schema.md +168 -0
- package/skills/pdf-to-json/scripts/extract_figures.py +319 -0
- package/skills/pdf-to-json/scripts/load_mongo.py +287 -0
- package/skills/pdf-to-json/scripts/pdf_extract.py +1313 -0
- package/skills/record-api-traffic/SKILL.md +434 -0
- package/skills/record-api-traffic/references/reading-recordings.md +224 -0
- package/skills/record-api-traffic/scripts/check-schema.mjs +184 -0
- package/skills/record-api-traffic/scripts/dump-quotation.mjs +67 -0
- package/skills/record-api-traffic/scripts/dump-source-excel.mjs +75 -0
- package/skills/record-api-traffic/scripts/lib/repo.mjs +109 -0
- package/skills/record-api-traffic/scripts/preflight.py +528 -0
- package/skills/record-api-traffic/scripts/record-api-traffic.py +720 -0
- package/skills/refac-wrt-business-goal/SKILL.md +305 -0
- package/skills/refac-wrt-business-goal/references/critic.md +170 -0
- package/skills/system-resource-triage/SKILL.md +180 -0
- package/skills/system-resource-triage/scripts/reap.sh +116 -0
- package/skills/system-resource-triage/scripts/triage.sh +111 -0
- package/skills/using-git-worktrees/SKILL.md +167 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Does the app still look the way the dump scripts assume?
|
|
3
|
+
//
|
|
4
|
+
// node check-schema.mjs --repo <repo> [--json]
|
|
5
|
+
//
|
|
6
|
+
// The dump scripts moved out of the app repo, so nothing makes them travel
|
|
7
|
+
// with a schema change any more. A renamed field would not crash: findById()
|
|
8
|
+
// still returns a document, `quotation.sourceWorkbookBuffer` is just
|
|
9
|
+
// undefined, and dump-source-excel silently falls through to the Graph
|
|
10
|
+
// refetch — or writes a zero-byte file. This check turns that into a loud
|
|
11
|
+
// failure before a session starts.
|
|
12
|
+
//
|
|
13
|
+
// Exit codes: 0 = aligned 1 = drift 2 = cannot verify
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { importRepo, importDependency } from './lib/repo.mjs';
|
|
19
|
+
|
|
20
|
+
// Every field the two dump scripts touch, and what it costs if it moves.
|
|
21
|
+
const FIELDS = [
|
|
22
|
+
['sourceWorkbookBuffer', true, 'Buffer',
|
|
23
|
+
'dump-source-excel reads the xlsx bytes from here'],
|
|
24
|
+
['sourceAttachmentName', true, 'String',
|
|
25
|
+
'names the dumped file and supplies its extension'],
|
|
26
|
+
['sourceFileType', true, 'String',
|
|
27
|
+
'extension fallback when the attachment name has none'],
|
|
28
|
+
['user', true, null,
|
|
29
|
+
'first argument to getAttachmentBuffer on the refetch path'],
|
|
30
|
+
['sourceMessageId', true, 'String', 'identifies the mail to refetch from'],
|
|
31
|
+
['sourceAttachmentId', true, 'String', 'identifies the attachment to refetch'],
|
|
32
|
+
['referenceNumber', false, 'String', 'summary label'],
|
|
33
|
+
['title', false, 'String', 'summary label fallback'],
|
|
34
|
+
['status', false, 'String', 'printed in the summary'],
|
|
35
|
+
['quotationItems', false, 'Array', 'item count in the summary'],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const checks = [];
|
|
39
|
+
const add = (status, name, detail) => checks.push({ status, name, detail });
|
|
40
|
+
|
|
41
|
+
function parse() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
let repo = null;
|
|
44
|
+
let json = false;
|
|
45
|
+
let probeEmail = false;
|
|
46
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
47
|
+
if (args[i] === '--repo') { repo = args[i + 1]; i += 1; }
|
|
48
|
+
else if (args[i].startsWith('--repo=')) repo = args[i].slice(7);
|
|
49
|
+
else if (args[i] === '--json') json = true;
|
|
50
|
+
else if (args[i] === '--probe-email') probeEmail = true;
|
|
51
|
+
}
|
|
52
|
+
if (!repo) {
|
|
53
|
+
console.error('Usage: node check-schema.mjs --repo <repo> [--json]');
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
return { repo: path.resolve(repo), json, probeEmail };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// email.service pulls in ~20 modules (S3, pdf, datalab). An import-time throw
|
|
60
|
+
// is catchable, but an import-time process.exit would take the whole check
|
|
61
|
+
// down with it, so probe it in a child.
|
|
62
|
+
async function probeEmail(backend) {
|
|
63
|
+
try {
|
|
64
|
+
const mod = await importRepo(backend, 'src/services/email.service.js');
|
|
65
|
+
const fn = mod.default?.getAttachmentBuffer;
|
|
66
|
+
if (typeof fn !== 'function') {
|
|
67
|
+
return { status: 'FAIL', detail: 'default export has no getAttachmentBuffer()' };
|
|
68
|
+
}
|
|
69
|
+
if (fn.length !== 3) {
|
|
70
|
+
return {
|
|
71
|
+
status: 'FAIL',
|
|
72
|
+
detail: `getAttachmentBuffer takes ${fn.length} args, expected 3 `
|
|
73
|
+
+ '(userId, messageId, attachmentId)',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { status: 'ok', detail: 'getAttachmentBuffer(userId, messageId, attachmentId)' };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { status: 'warn', detail: `could not load: ${err.message}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const { repo, json, probeEmail: isProbe } = parse();
|
|
84
|
+
const backend = path.join(repo, 'Backend');
|
|
85
|
+
|
|
86
|
+
if (isProbe) {
|
|
87
|
+
console.log(JSON.stringify(await probeEmail(backend)));
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(backend)) {
|
|
92
|
+
console.error(`Not an app repo (no Backend/): ${repo}`);
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
if (!fs.existsSync(path.join(backend, 'node_modules'))) {
|
|
96
|
+
if (json) {
|
|
97
|
+
console.log(JSON.stringify({
|
|
98
|
+
verifiable: false,
|
|
99
|
+
reason: 'Backend/node_modules missing; the model cannot be loaded',
|
|
100
|
+
checks: [],
|
|
101
|
+
}, null, 2));
|
|
102
|
+
} else {
|
|
103
|
+
console.log('\n Cannot verify: Backend/node_modules missing.\n');
|
|
104
|
+
}
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let Quotation;
|
|
109
|
+
try {
|
|
110
|
+
Quotation = (await importRepo(backend, 'src/models/quotation.model.js')).default;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
add('FAIL', 'quotation model', `cannot load src/models/quotation.model.js: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Quotation) {
|
|
116
|
+
if (!Quotation.schema) {
|
|
117
|
+
add('FAIL', 'quotation model', 'default export is not a Mongoose model');
|
|
118
|
+
} else {
|
|
119
|
+
for (const [field, required, expected, why] of FIELDS) {
|
|
120
|
+
const schemaPath = Quotation.schema.path(field);
|
|
121
|
+
if (!schemaPath) {
|
|
122
|
+
add(required ? 'FAIL' : 'warn', `field ${field}`,
|
|
123
|
+
`missing from the schema — ${why}`);
|
|
124
|
+
} else if (expected && schemaPath.instance !== expected) {
|
|
125
|
+
add(required ? 'FAIL' : 'warn', `field ${field}`,
|
|
126
|
+
`is ${schemaPath.instance}, expected ${expected} — ${why}`);
|
|
127
|
+
} else {
|
|
128
|
+
add('ok', `field ${field}`, schemaPath.instance || 'present');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const connectDB = (await importRepo(backend, 'src/config/db.js')).default;
|
|
136
|
+
if (typeof connectDB !== 'function') {
|
|
137
|
+
add('FAIL', 'db.js', 'default export is not a function');
|
|
138
|
+
} else {
|
|
139
|
+
add('ok', 'db.js', 'default export is connectDB()');
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
add('FAIL', 'db.js', `cannot load src/config/db.js: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const child = spawnSync(process.execPath,
|
|
146
|
+
[fileURLToPath(import.meta.url), '--repo', repo, '--probe-email'],
|
|
147
|
+
{ encoding: 'utf8', timeout: 60000 });
|
|
148
|
+
let emailResult;
|
|
149
|
+
try {
|
|
150
|
+
emailResult = JSON.parse(child.stdout.trim().split('\n').at(-1));
|
|
151
|
+
} catch {
|
|
152
|
+
emailResult = {
|
|
153
|
+
status: 'warn',
|
|
154
|
+
detail: `probe did not report (exit ${child.status}): `
|
|
155
|
+
+ `${(child.stderr || '').trim().split('\n').at(-1) || 'no output'}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
add(emailResult.status, 'email.service', emailResult.detail);
|
|
159
|
+
|
|
160
|
+
const failures = checks.filter((c) => c.status === 'FAIL');
|
|
161
|
+
const warnings = checks.filter((c) => c.status === 'warn');
|
|
162
|
+
|
|
163
|
+
if (json) {
|
|
164
|
+
console.log(JSON.stringify({
|
|
165
|
+
verifiable: true, aligned: !failures.length, checks,
|
|
166
|
+
}, null, 2));
|
|
167
|
+
} else {
|
|
168
|
+
const width = Math.max(...checks.map((c) => c.name.length));
|
|
169
|
+
console.log(`\n SCHEMA ALIGNMENT — dump scripts vs ${repo}\n`);
|
|
170
|
+
for (const c of checks) {
|
|
171
|
+
console.log(` ${c.status.padEnd(4)} ${c.name.padEnd(width)} ${c.detail}`);
|
|
172
|
+
}
|
|
173
|
+
console.log(failures.length
|
|
174
|
+
? `\n ${failures.length} mismatch(es). The dump scripts need updating before use.\n`
|
|
175
|
+
: `\n Aligned${warnings.length ? ` (${warnings.length} warning(s))` : ''}.\n`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return failures.length ? 1 : 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
182
|
+
console.error('FATAL:', err.message);
|
|
183
|
+
process.exit(2);
|
|
184
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dump a quotation document verbatim from MongoDB, so a recording session has
|
|
3
|
+
// its real output alongside the API traffic. Unlike GET /api/quotations/:id
|
|
4
|
+
// this is the stored document, not the API's serialisation of it.
|
|
5
|
+
//
|
|
6
|
+
// node dump-quotation.mjs --repo <repo> <quotationId> [runDir]
|
|
7
|
+
//
|
|
8
|
+
// Writes into the skill's own recordings workspace, never into the repo.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import {
|
|
12
|
+
parseArgs, artifactsDir, loadEnv, importRepo, importDependency,
|
|
13
|
+
} from './lib/repo.mjs';
|
|
14
|
+
|
|
15
|
+
const USAGE = 'node dump-quotation.mjs --repo <repo> <quotationId> [runDir]';
|
|
16
|
+
const { repo, backend, positional } = parseArgs(process.argv, USAGE);
|
|
17
|
+
const [quotationId, runDir] = positional;
|
|
18
|
+
if (!quotationId) {
|
|
19
|
+
console.error(`Usage: ${USAGE}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
loadEnv(backend);
|
|
24
|
+
const mongoose = (await importDependency(backend, 'mongoose')).default;
|
|
25
|
+
const connectDB = (await importRepo(backend, 'src/config/db.js')).default;
|
|
26
|
+
const Quotation = (await importRepo(backend, 'src/models/quotation.model.js')).default;
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const outDir = artifactsDir(repo, runDir);
|
|
30
|
+
|
|
31
|
+
await connectDB();
|
|
32
|
+
|
|
33
|
+
const quotation = await Quotation.findById(quotationId).lean();
|
|
34
|
+
if (!quotation) {
|
|
35
|
+
console.error(`No quotation ${quotationId}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Megabytes of xlsx bytes would make the JSON unreadable; dump-source-excel.mjs
|
|
40
|
+
// writes them out as a real file instead.
|
|
41
|
+
// lean() skips Mongoose casting, so a Buffer field arrives as a BSON Binary
|
|
42
|
+
// whose .length is a method rather than a byte count.
|
|
43
|
+
const raw = quotation.sourceWorkbookBuffer;
|
|
44
|
+
const bufferBytes = raw ? (Buffer.isBuffer(raw) ? raw.length : raw.length()) : 0;
|
|
45
|
+
quotation.sourceWorkbookBuffer = bufferBytes
|
|
46
|
+
? { bytes: bufferBytes, note: 'see dump-source-excel.mjs' }
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
50
|
+
const outPath = path.join(outDir, `${quotationId}.json`);
|
|
51
|
+
const json = JSON.stringify(quotation, null, 2);
|
|
52
|
+
fs.writeFileSync(outPath, json);
|
|
53
|
+
|
|
54
|
+
console.log(`\n ${quotation.referenceNumber || quotation.title || '(untitled)'}`);
|
|
55
|
+
console.log(` status : ${quotation.status}`);
|
|
56
|
+
console.log(` items : ${quotation.quotationItems?.length ?? 0}`);
|
|
57
|
+
console.log(` size : ${json.length} bytes of JSON (+ ${bufferBytes} bytes of xlsx omitted)`);
|
|
58
|
+
console.log(` written: ${outPath}\n`);
|
|
59
|
+
|
|
60
|
+
await mongoose.connection.close();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main().catch(async (err) => {
|
|
64
|
+
console.error('FATAL:', err.message);
|
|
65
|
+
await mongoose.connection.close().catch(() => {});
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dump the original attachment a quotation was built from, so a recording
|
|
3
|
+
// session has its real input alongside the API traffic.
|
|
4
|
+
//
|
|
5
|
+
// node dump-source-excel.mjs --repo <repo> <quotationId> [runDir]
|
|
6
|
+
//
|
|
7
|
+
// Writes into the skill's own recordings workspace, never into the repo.
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
parseArgs, artifactsDir, loadEnv, importRepo, importDependency,
|
|
12
|
+
} from './lib/repo.mjs';
|
|
13
|
+
|
|
14
|
+
const USAGE = 'node dump-source-excel.mjs --repo <repo> <quotationId> [runDir]';
|
|
15
|
+
const { repo, backend, positional } = parseArgs(process.argv, USAGE);
|
|
16
|
+
const [quotationId, runDir] = positional;
|
|
17
|
+
if (!quotationId) {
|
|
18
|
+
console.error(`Usage: ${USAGE}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
loadEnv(backend);
|
|
23
|
+
const mongoose = (await importDependency(backend, 'mongoose')).default;
|
|
24
|
+
const connectDB = (await importRepo(backend, 'src/config/db.js')).default;
|
|
25
|
+
const Quotation = (await importRepo(backend, 'src/models/quotation.model.js')).default;
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const outDir = artifactsDir(repo, runDir);
|
|
29
|
+
|
|
30
|
+
await connectDB();
|
|
31
|
+
|
|
32
|
+
const quotation = await Quotation.findById(quotationId).lean();
|
|
33
|
+
if (!quotation) {
|
|
34
|
+
console.error(`No quotation ${quotationId}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// lean() skips Mongoose casting, so a Buffer field arrives as a BSON Binary
|
|
39
|
+
// whose .length is a method — every size check below would misread it.
|
|
40
|
+
let buffer = quotation.sourceWorkbookBuffer;
|
|
41
|
+
if (buffer && !Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer.buffer ?? buffer);
|
|
42
|
+
let source = 'mongo (sourceWorkbookBuffer)';
|
|
43
|
+
|
|
44
|
+
// Null for CSV attachments, legacy quotations, and documents that hit the
|
|
45
|
+
// BSON size cap — the buffer is the first field dropped there. email.service
|
|
46
|
+
// drags in most of the backend, so only load it if this path is taken.
|
|
47
|
+
if (!buffer?.length) {
|
|
48
|
+
console.log('sourceWorkbookBuffer empty, refetching from Outlook...');
|
|
49
|
+
const emailService = (await importRepo(backend, 'src/services/email.service.js')).default;
|
|
50
|
+
({ buffer } = await emailService.getAttachmentBuffer(
|
|
51
|
+
quotation.user,
|
|
52
|
+
quotation.sourceMessageId,
|
|
53
|
+
quotation.sourceAttachmentId
|
|
54
|
+
));
|
|
55
|
+
source = 'microsoft graph';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
59
|
+
const ext = path.extname(quotation.sourceAttachmentName) || `.${quotation.sourceFileType}`;
|
|
60
|
+
const outPath = path.join(outDir, `${quotationId}-source${ext}`);
|
|
61
|
+
fs.writeFileSync(outPath, buffer);
|
|
62
|
+
|
|
63
|
+
console.log(`\n ${quotation.sourceAttachmentName}`);
|
|
64
|
+
console.log(` from : ${source}`);
|
|
65
|
+
console.log(` bytes : ${buffer.length}`);
|
|
66
|
+
console.log(` written: ${outPath}\n`);
|
|
67
|
+
|
|
68
|
+
await mongoose.connection.close();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch(async (err) => {
|
|
72
|
+
console.error('FATAL:', err.message);
|
|
73
|
+
await mongoose.connection.close().catch(() => {});
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Helpers for running app-repo code from outside the app repo.
|
|
2
|
+
//
|
|
3
|
+
// These scripts live in the skill, not in the repo they operate on, so no
|
|
4
|
+
// bare specifier resolves: `import mongoose` here would look in the skill's
|
|
5
|
+
// own (nonexistent) node_modules. Everything is resolved explicitly from the
|
|
6
|
+
// repo path instead.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const SKILL = path.resolve(import.meta.dirname, '../..');
|
|
14
|
+
export const WORKSPACE = path.join(SKILL, '..', 'record-api-traffic-workspace', 'recordings');
|
|
15
|
+
|
|
16
|
+
export function parseArgs(argv, usage) {
|
|
17
|
+
const args = argv.slice(2);
|
|
18
|
+
const positional = [];
|
|
19
|
+
let repo = null;
|
|
20
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
21
|
+
if (args[i] === '--repo') {
|
|
22
|
+
repo = args[i + 1];
|
|
23
|
+
i += 1;
|
|
24
|
+
} else if (args[i].startsWith('--repo=')) {
|
|
25
|
+
repo = args[i].slice(7);
|
|
26
|
+
} else {
|
|
27
|
+
positional.push(args[i]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!repo) {
|
|
31
|
+
console.error(`Usage: ${usage}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
repo = path.resolve(repo);
|
|
35
|
+
if (!fs.existsSync(path.join(repo, 'Backend'))) {
|
|
36
|
+
console.error(`Not an app repo (no Backend/): ${repo}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return { repo, backend: path.join(repo, 'Backend'), positional };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mirrors workspace_slug() in record-api-traffic.py: the last two path
|
|
43
|
+
// segments, so two clones of the same repo get separate recording folders.
|
|
44
|
+
export function workspaceSlug(repo) {
|
|
45
|
+
return repo
|
|
46
|
+
.split(path.sep)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.slice(-2)
|
|
49
|
+
.join('-')
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
52
|
+
.replace(/^-|-$/g, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Artifacts belong beside the traffic they came from, so default to the
|
|
56
|
+
// newest run folder for this repo. Pass a run dir to target an older session.
|
|
57
|
+
export function artifactsDir(repo, explicit) {
|
|
58
|
+
if (explicit) return path.resolve(explicit, 'artifacts');
|
|
59
|
+
const base = path.join(WORKSPACE, workspaceSlug(repo));
|
|
60
|
+
const runs = fs.existsSync(base)
|
|
61
|
+
? fs.readdirSync(base, { withFileTypes: true })
|
|
62
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('run-'))
|
|
63
|
+
.map((e) => e.name)
|
|
64
|
+
.sort()
|
|
65
|
+
: [];
|
|
66
|
+
if (!runs.length) {
|
|
67
|
+
console.error(`No recordings for ${repo} under ${base}. Record a session first, `
|
|
68
|
+
+ 'or pass a run directory explicitly.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
return path.join(base, runs.at(-1), 'artifacts');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The repo's db.js reads process.env, and its dotenv.config() resolves .env
|
|
75
|
+
// from the working directory -- which is the skill, not the repo. Load it
|
|
76
|
+
// here so the scripts work from anywhere. dotenv does not override existing
|
|
77
|
+
// values, so these win.
|
|
78
|
+
export function loadEnv(backend) {
|
|
79
|
+
const file = path.join(backend, '.env');
|
|
80
|
+
if (!fs.existsSync(file)) {
|
|
81
|
+
console.error(`Missing ${file}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
87
|
+
const eq = trimmed.indexOf('=');
|
|
88
|
+
const key = trimmed.slice(0, eq).trim().replace(/^export\s+/, '');
|
|
89
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
90
|
+
const quote = value[0];
|
|
91
|
+
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
|
|
92
|
+
value = value.slice(1, -1);
|
|
93
|
+
}
|
|
94
|
+
process.env[key] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function importRepo(backend, relative) {
|
|
99
|
+
const file = path.join(backend, relative);
|
|
100
|
+
if (!fs.existsSync(file)) throw new Error(`missing ${file}`);
|
|
101
|
+
return import(pathToFileURL(file).href);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Bare specifiers resolve from the importing file's directory, so ask Node to
|
|
105
|
+
// resolve them as if from inside the repo, then import the resulting path.
|
|
106
|
+
export function importDependency(backend, specifier) {
|
|
107
|
+
const require = createRequire(path.join(backend, 'package.json'));
|
|
108
|
+
return import(pathToFileURL(require.resolve(specifier)).href);
|
|
109
|
+
}
|