@t1mmen/srtd
Version:
Supabase Repeatable Template Definitions (srtd): 🪄 Live-reloading SQL templates for Supabase DX. Make your database changes reviewable and migrations maintainable! 🚀
159 lines • 7.4 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { Select, Spinner } from '@inkjs/ui';
import figures from 'figures';
import { glob } from 'glob';
import { Box, Text, useApp } from 'ink';
import { argument } from 'pastel';
import React, { useCallback, useEffect, useState } from 'react';
import zod from 'zod';
import Branding from '../components/Branding.js';
import Quittable from '../components/Quittable.js';
import { COLOR_ERROR, COLOR_SUCCESS, COLOR_WARNING } from '../components/customTheme.js';
import { getConfig } from '../utils/config.js';
import { loadBuildLog } from '../utils/loadBuildLog.js';
import { saveBuildLog } from '../utils/saveBuildLog.js';
export const args = zod
.array(zod.string())
.optional()
.describe(argument({
name: 'templates',
description: 'Template files to promote (optional)',
}));
export default function Promote({ args: templateArgs }) {
const { exit } = useApp();
const [templates, setTemplates] = useState([]);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
const [config, setConfig] = useState(null);
const findWipTemplates = useCallback(async () => {
try {
const config = await getConfig(process.cwd());
setConfig(config);
const templatePath = path.join(process.cwd(), config.templateDir);
const pattern = `**/*${config.wipIndicator}*.sql`;
const matches = await glob(pattern, { cwd: templatePath });
const fullPaths = matches.map(m => path.join(templatePath, m));
setTemplates(fullPaths);
return fullPaths;
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(`Failed to find WIP templates: ${msg}`);
return [];
}
}, []);
const promoteTemplate = useCallback(async (templatePath) => {
try {
const config = await getConfig(process.cwd());
const newPath = templatePath.replace(config.wipIndicator, '');
// Load build log before file operations
const buildLog = await loadBuildLog(process.cwd(), 'local');
const relOldPath = path.relative(process.cwd(), templatePath);
const relNewPath = path.relative(process.cwd(), newPath);
// Check if source file exists
await fs.access(templatePath);
// Rename the file
await fs.rename(templatePath, newPath);
// Update build logs if template was tracked
if (buildLog.templates[relOldPath]) {
buildLog.templates[relNewPath] = buildLog.templates[relOldPath];
delete buildLog.templates[relOldPath];
await saveBuildLog(process.cwd(), buildLog, 'local');
}
const templateName = path.basename(newPath, '.sql');
setSuccess(`Successfully promoted ${templateName}`);
// Exit after short delay
setTimeout(() => exit(), 0);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(`Failed to promote template: ${msg}`);
}
}, [exit]);
useEffect(() => {
const init = async () => {
const config = await getConfig(process.cwd());
await findWipTemplates();
if (templateArgs?.length) {
// Handle CLI mode
const templateName = templateArgs[0];
const templateDir = path.join(process.cwd(), config.templateDir);
const pattern = `**/*${templateName}*`;
const matches = await glob(pattern, { cwd: templateDir });
const isWip = config.wipIndicator && templateName?.includes(config.wipIndicator);
if (matches.length === 0 || !isWip) {
setError(`No WIP template found matching: ${templateName} in ${config.templateDir}`);
exit();
}
if (!isWip) {
setError(`Template is not a WIP template: ${templateName}`);
exit();
}
const match = matches[0] ? path.join(templateDir, matches[0]) : '';
if (!match) {
setError(`No valid match found for template: ${templateName} in ${config.templateDir}`);
exit();
}
await promoteTemplate(match);
}
};
init();
}, [templateArgs, findWipTemplates, promoteTemplate, exit]);
// UI status displays
if (error) {
return (React.createElement(Box, { flexDirection: "column" },
React.createElement(Branding, { subtitle: "\uD83D\uDE80 Promote WIP template" }),
React.createElement(Text, { color: COLOR_ERROR },
figures.cross,
" ",
error)));
}
if (success) {
return (React.createElement(Box, { flexDirection: "column" },
React.createElement(Branding, { subtitle: "\uD83D\uDE80 Promote WIP template" }),
React.createElement(Box, { gap: 1, flexDirection: "column" },
React.createElement(Text, { color: COLOR_SUCCESS },
figures.tick,
" ",
success),
React.createElement(Text, { dimColor: true }, "Run `build` command to generate migrations"))));
}
// Interactive UI mode
if (!templateArgs?.length) {
if (templates.length === 0) {
return (React.createElement(Box, { flexDirection: "column" },
React.createElement(Branding, { subtitle: "\uD83D\uDE80 Promote WIP template" }),
React.createElement(Text, { color: COLOR_WARNING },
figures.warning,
" No WIP templates found in ",
config?.templateDir,
' ',
React.createElement(Text, { bold: true },
"(",
config?.wipIndicator,
")")),
React.createElement(Quittable, null)));
}
const menuItems = templates.map(t => ({
label: path.basename(t),
value: t,
}));
return (React.createElement(Box, { flexDirection: "column" },
React.createElement(Branding, { subtitle: "\uD83D\uDE80 Promote WIP template" }),
React.createElement(Box, { gap: 1, marginBottom: 1 },
React.createElement(Text, null, "Select a template to promote"),
React.createElement(Text, { dimColor: true },
"(removes ",
config?.wipIndicator,
" in filename)")),
React.createElement(Box, { gap: 1, flexDirection: "column" },
React.createElement(Select, { visibleOptionCount: 30, options: menuItems, onChange: promoteTemplate })),
React.createElement(Quittable, null)));
}
// Loading state for CLI mode
return (React.createElement(Box, { flexDirection: "column" },
React.createElement(Branding, { subtitle: "\uD83D\uDE80 Promote WIP template" }),
React.createElement(Spinner, { label: "Loading..." })));
}
//# sourceMappingURL=promote.js.map