Compare commits

..

2 Commits

Author SHA1 Message Date
d89a58c0ab deploy builtin tools 2026-02-09 20:59:10 -08:00
8f37274cee toes env -g 2026-02-09 20:58:07 -08:00
3 changed files with 118 additions and 26 deletions

View File

@ -11,7 +11,30 @@ source "$ROOT_DIR/scripts/config.sh"
git push origin main
# SSH to target and update
ssh "$HOST" "cd $DEST && git pull origin main && bun run build && sudo systemctl restart toes.service"
ssh "$HOST" "cd $DEST && git pull origin main && bun run build"
# Sync default apps/tools from repo to APPS_DIR
echo "=> Syncing default apps..."
ssh "$HOST" bash -s "$DEST" "$APPS_DIR" <<'SCRIPT'
DEST="$1"
APPS_DIR="$2"
for app_dir in "$DEST"/apps/*/; do
app=$(basename "$app_dir")
for version_dir in "$app_dir"*/; do
[ -d "$version_dir" ] || continue
version=$(basename "$version_dir")
[ -f "$version_dir/package.json" ] || continue
target="$APPS_DIR/$app/$version"
mkdir -p "$target"
cp -a "$version_dir"/. "$target"/
rm -f "$APPS_DIR/$app/current"
echo " $app/$version"
(cd "$target" && bun install --frozen-lockfile 2>/dev/null || bun install)
done
done
SCRIPT
ssh "$HOST" "sudo systemctl restart toes.service"
echo "=> Deployed to $HOST"
echo "=> Visit $URL"

View File

@ -7,11 +7,62 @@ interface EnvVar {
value: string
}
export async function envList(name: string | undefined) {
function parseKeyValue(keyOrKeyValue: string, valueArg?: string): { key: string, value: string } | null {
if (valueArg !== undefined) {
const key = keyOrKeyValue.trim()
if (!key) { console.error('Key cannot be empty'); return null }
return { key, value: valueArg }
}
const eqIndex = keyOrKeyValue.indexOf('=')
if (eqIndex === -1) {
console.error('Invalid format. Use: KEY value or KEY=value')
return null
}
const key = keyOrKeyValue.slice(0, eqIndex).trim()
if (!key) { console.error('Key cannot be empty'); return null }
return { key, value: keyOrKeyValue.slice(eqIndex + 1) }
}
async function globalEnvSet(keyOrKeyValue: string, valueArg?: string) {
const parsed = parseKeyValue(keyOrKeyValue, valueArg)
if (!parsed) return
const { key, value } = parsed
try {
const result = await post<{ ok: boolean, error?: string }>('/api/env', { key, value })
if (result?.ok) {
console.log(color.green(`Set global ${color.bold(key.toUpperCase())}`))
} else {
console.error(result?.error ?? 'Failed to set variable')
}
} catch (error) {
handleError(error)
}
}
export async function envList(name: string | undefined, opts: { global?: boolean }) {
if (opts.global) {
const vars = await get<EnvVar[]>('/api/env')
console.log(color.bold().cyan('Global Environment Variables'))
console.log()
if (!vars || vars.length === 0) {
console.log(color.gray(' No global environment variables set'))
return
}
for (const v of vars) {
console.log(` ${color.bold(v.key)}=${color.gray(v.value)}`)
}
return
}
const appName = resolveAppName(name)
if (!appName) return
const vars = await get<EnvVar[]>(`/api/apps/${appName}/env`)
const [vars, globalVars] = await Promise.all([
get<EnvVar[]>(`/api/apps/${appName}/env`),
get<EnvVar[]>('/api/env'),
])
if (!vars) {
console.error(`App not found: ${appName}`)
return
@ -20,7 +71,9 @@ export async function envList(name: string | undefined) {
console.log(color.bold().cyan(`Environment Variables for ${appName}`))
console.log()
if (vars.length === 0) {
const appKeys = new Set(vars.map(v => v.key))
if (vars.length === 0 && (!globalVars || globalVars.length === 0)) {
console.log(color.gray(' No environment variables set'))
return
}
@ -28,31 +81,30 @@ export async function envList(name: string | undefined) {
for (const v of vars) {
console.log(` ${color.bold(v.key)}=${color.gray(v.value)}`)
}
if (globalVars && globalVars.length > 0) {
const inherited = globalVars.filter(v => !appKeys.has(v.key))
if (inherited.length > 0) {
if (vars.length > 0) console.log()
console.log(color.gray(' Inherited from global:'))
for (const v of inherited) {
console.log(` ${color.bold(v.key)}=${color.gray(v.value)}`)
}
}
}
}
export async function envSet(name: string | undefined, keyOrKeyValue: string, valueArg?: string) {
let key: string
let value: string
if (valueArg !== undefined) {
// KEY value format
key = keyOrKeyValue.trim()
value = valueArg
} else {
// KEY=value format
const eqIndex = keyOrKeyValue.indexOf('=')
if (eqIndex === -1) {
console.error('Invalid format. Use: KEY value or KEY=value')
return
}
key = keyOrKeyValue.slice(0, eqIndex).trim()
value = keyOrKeyValue.slice(eqIndex + 1)
export async function envSet(name: string | undefined, keyOrKeyValue: string, valueArg: string | undefined, opts: { global?: boolean }) {
// With --global, args shift: name becomes key, key becomes value
if (opts.global) {
const actualKey = name ?? keyOrKeyValue
const actualValue = name ? keyOrKeyValue : valueArg
return globalEnvSet(actualKey, actualValue)
}
if (!key) {
console.error('Key cannot be empty')
return
}
const parsed = parseKeyValue(keyOrKeyValue, valueArg)
if (!parsed) return
const { key, value } = parsed
const appName = resolveAppName(name)
if (!appName) return
@ -70,7 +122,21 @@ export async function envSet(name: string | undefined, keyOrKeyValue: string, va
}
}
export async function envRm(name: string | undefined, key: string) {
export async function envRm(name: string | undefined, key: string, opts: { global?: boolean }) {
// With --global, args shift: name becomes key
if (opts.global) {
const actualKey = name ?? key
if (!actualKey) {
console.error('Key is required')
return
}
const ok = await del(`/api/env/${actualKey.toUpperCase()}`)
if (ok) {
console.log(color.green(`Removed global ${color.bold(actualKey.toUpperCase())}`))
}
return
}
if (!key) {
console.error('Key is required')
return

View File

@ -231,6 +231,7 @@ const env = program
.helpGroup('Config:')
.description('Manage environment variables')
.argument('[name]', 'app name (uses current directory if omitted)')
.option('-g, --global', 'manage global variables shared by all apps')
.action(envList)
env
@ -239,6 +240,7 @@ env
.argument('[name]', 'app name (uses current directory if omitted)')
.argument('<key>', 'variable name')
.argument('[value]', 'variable value (or use KEY=value format)')
.option('-g, --global', 'set a global variable shared by all apps')
.action(envSet)
env
@ -246,6 +248,7 @@ env
.description('Remove an environment variable')
.argument('[name]', 'app name (uses current directory if omitted)')
.argument('<key>', 'variable name to remove')
.option('-g, --global', 'remove a global variable')
.action(envRm)
program