1885990081
CI / build (push) Waiting to run
CI / docker (push) Blocked by required conditions
CI / helm (push) Waiting to run
CI / push-validation (push) Waiting to run
CI / status-check (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / security (push) Waiting to run
CI / quality (push) Waiting to run
CI / unit_tests (push) Waiting to run
CI / integration_tests (push) Waiting to run
CI / e2e_tests (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / benchmark-publish (push) Waiting to run
167 lines
5.2 KiB
TypeScript
167 lines
5.2 KiB
TypeScript
#!/usr/bin/env -S npx --yes tsx
|
|
// =============================================================================
|
|
// extract.ts — Extract a value from source text using regex and store it
|
|
//
|
|
// Part of: .opencode/skills/templating-vault/scripts/
|
|
//
|
|
// The ONLY way to set literal variables. The LLM provides a regex pattern;
|
|
// this script applies it against the source text (from a vault key or file)
|
|
// and stores the captured value. The LLM never sees or reproduces the raw
|
|
// value — it only provides the regex.
|
|
//
|
|
// USAGE:
|
|
// extract.ts --source-key <key> --key <target_key> --regex <pattern>
|
|
// [--capture-group <n>] [--owner <id>]
|
|
// [--config <path>] [--store-dir <path>]
|
|
//
|
|
// extract.ts --source-file <path> --key <target_key> --regex <pattern>
|
|
// [--capture-group <n>] [--owner <id>]
|
|
// [--config <path>] [--store-dir <path>]
|
|
//
|
|
// OUTPUT (stdout): JSON result
|
|
// =============================================================================
|
|
import * as fs from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
parseCommonFlags, getFlag, hasFlag, output, log,
|
|
loadConfig, readValue, resolveStore, writeValue,
|
|
loadKeyMeta, saveKeyMeta, validateValue,
|
|
type KeyMeta,
|
|
} from './api.ts';
|
|
|
|
async function main(): Promise<void> {
|
|
const { config: cfgPath, storeDirs, rest } = parseCommonFlags(process.argv);
|
|
const config = loadConfig(cfgPath, storeDirs.length > 0 ? storeDirs : undefined);
|
|
|
|
const sourceKey = getFlag(rest, '--source-key');
|
|
const sourceFile = getFlag(rest, '--source-file');
|
|
const targetKey = getFlag(rest, '--key');
|
|
const regexStr = getFlag(rest, '--regex');
|
|
const captureGroupStr = getFlag(rest, '--capture-group');
|
|
const owner = getFlag(rest, '--owner');
|
|
const writeOnce = hasFlag(rest, '--write-once');
|
|
|
|
if (!targetKey) {
|
|
output({ extracted: false, error: '--key is required' });
|
|
process.exit(1);
|
|
}
|
|
if (!regexStr) {
|
|
output({ extracted: false, error: '--regex is required' });
|
|
process.exit(1);
|
|
}
|
|
if (!sourceKey && !sourceFile) {
|
|
output({ extracted: false, error: '--source-key or --source-file is required' });
|
|
process.exit(1);
|
|
}
|
|
|
|
// Check access
|
|
const resolved = resolveStore(config, targetKey);
|
|
if (resolved.mapping.access === 'readonly') {
|
|
output({ extracted: false, error: `Store for prefix "${resolved.mapping.prefix}" is readonly` });
|
|
process.exit(1);
|
|
}
|
|
|
|
// Load source text
|
|
let sourceText: string;
|
|
if (sourceKey) {
|
|
const val = readValue(config, sourceKey);
|
|
if (val === null) {
|
|
output({ extracted: false, error: `Source key "${sourceKey}" not found in vault` });
|
|
process.exit(1);
|
|
}
|
|
sourceText = val;
|
|
} else {
|
|
if (!fs.existsSync(sourceFile!)) {
|
|
output({ extracted: false, error: `Source file "${sourceFile}" not found` });
|
|
process.exit(1);
|
|
}
|
|
sourceText = fs.readFileSync(sourceFile!, 'utf-8');
|
|
}
|
|
|
|
// Apply regex
|
|
const captureGroup = captureGroupStr ? parseInt(captureGroupStr, 10) : 1;
|
|
let regex: RegExp;
|
|
try {
|
|
regex = new RegExp(regexStr, 'm');
|
|
} catch (err) {
|
|
output({
|
|
extracted: false,
|
|
error: `Invalid regex: ${err instanceof Error ? err.message : String(err)}`,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
const match = regex.exec(sourceText);
|
|
if (!match) {
|
|
// Provide context hints without exposing full source
|
|
const lines = sourceText.split('\n');
|
|
output({
|
|
extracted: false,
|
|
error: 'Regex did not match any text in the source',
|
|
hints: {
|
|
source_lines: lines.length,
|
|
source_chars: sourceText.length,
|
|
first_200_chars: sourceText.slice(0, 200),
|
|
regex_used: regexStr,
|
|
},
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
if (captureGroup >= match.length) {
|
|
output({
|
|
extracted: false,
|
|
error: `Capture group ${captureGroup} does not exist. Regex matched ${match.length - 1} group(s).`,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
const extractedValue = match[captureGroup] ?? match[0];
|
|
|
|
// Check write-once on target
|
|
const meta = loadKeyMeta(config, targetKey);
|
|
if (meta.write_once && readValue(config, targetKey) !== null) {
|
|
output({
|
|
extracted: false,
|
|
error: `Key "${targetKey}" is write-once and already has a value`,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
// Validate
|
|
const validationError = validateValue(extractedValue, meta);
|
|
if (validationError) {
|
|
output({
|
|
extracted: false,
|
|
error: `Validation failed for "${targetKey}": ${validationError}`,
|
|
extracted_length: extractedValue.length,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
// Store value (using --force-literal internally to bypass literal check in writeValue)
|
|
writeValue(config, targetKey, extractedValue);
|
|
|
|
// Update metadata
|
|
const updatedMeta: KeyMeta = { ...meta };
|
|
if (owner) updatedMeta.owner = owner;
|
|
if (writeOnce) updatedMeta.write_once = true;
|
|
if (owner || writeOnce) {
|
|
saveKeyMeta(config, targetKey, updatedMeta);
|
|
}
|
|
|
|
output({
|
|
extracted: true,
|
|
key: targetKey,
|
|
stored: true,
|
|
value_length: extractedValue.length,
|
|
});
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
main().catch((err: unknown) => {
|
|
output({ extracted: false, error: String(err instanceof Error ? err.message : err) });
|
|
process.exit(1);
|
|
});
|
|
}
|