google-agents-cli-scaffold
Project scaffolding, deployment configuration, and CI/CD setup for Google ADK agents.
recovery-feature-scaffold
Scaffold complete features for the Steps to Recovery app including database schema, encrypted storage, offline sync, React Query hooks, screens, and tests. Use when adding new data models (gratitude lists, resentments, daily inventory), creating new journaling features, building step work tools, ...
Full skill instructions
Generate complete, production-ready features for Steps to Recovery with one command.
# From project root
cd .claude/skills/recovery-feature-scaffold/scripts
node scaffold.js <FeatureName>
# Example - creates a complete gratitude list feature
node scaffold.js GratitudeList
This generates 9 files in under 1 second with all boilerplate wired up.
This generates:
feature: GratitudeList
apps/mobile/src/
├── features/gratitude-list/
│ ├── types.ts # TypeScript interfaces
│ ├── hooks/
│ │ └── useGratitude.ts # React Query + encryption
│ ├── screens/
│ │ ├── GratitudeListScreen.tsx
│ │ └── GratitudeDetailScreen.tsx
│ ├── components/
│ │ └── GratitudeCard.tsx
│ └── __tests__/
│ └── gratitude.test.ts
├── lib/
│ └── database/
│ └── migrations/
│ └── 007_add_gratitude_list.sql
└── navigation/
└── AppNavigator.tsx # Auto-updated
supabase/migrations/
└── 007_add_gratitude_list.sql # RLS policies included
Follow this 5-step workflow when script isn't available:
Create src/features/<feature>/types.ts:
export interface GratitudeItem {
id: string;
user_id: string;
encrypted_content: string;
category: 'people' | 'things' | 'experiences' | 'other';
created_at: string;
updated_at: string;
}
export interface CreateGratitudeInput {
content: string;
category: GratitudeItem['category'];
}
export interface UpdateGratitudeInput {
id: string;
content?: string;
category?: GratitudeItem['category'];
}
SQLite migration (src/lib/database/migrations/XXX_add_feature.sql):
-- SQLite migration
CREATE TABLE IF NOT EXISTS gratitude_items (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
encrypted_content TEXT NOT NULL,
category TEXT CHECK (category IN ('people', 'things', 'experiences', 'other')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_gratitude_user ON gratitude_items(user_id);
CREATE INDEX IF NOT EXISTS idx_gratitude_created ON gratitude_items(created_at DESC);
Supabase migration (supabase/migrations/XXX_add_feature.sql):
-- Supabase table
CREATE TABLE IF NOT EXISTS public.gratitude_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
encrypted_content TEXT NOT NULL,
category TEXT CHECK (category IN ('people', 'things', 'experiences', 'other')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
-- RLS Policies
ALTER TABLE public.gratitude_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access their own gratitude items"
ON public.gratitude_items FOR ALL
USING (auth.uid() = user_id);
-- Trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_gratitude_items_updated_at
BEFORE UPDATE ON public.gratitude_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
src/features/<feature>/hooks/useFeature.ts:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDatabase } from '../../../contexts/DatabaseContext';
import { encryptContent, decryptContent } from '../../../utils/encryption';
import { generateUUID } from '../../../utils/uuid';
import type { GratitudeItem, CreateGratitudeInput, UpdateGratitudeInput } from '../types';
const FEATURE_KEY = 'gratitude-items';
export function useGratitudeItems() {
const { db, userId } = useDatabase();
return useQuery({
queryKey: [FEATURE_KEY],
queryFn: async (): Promise<GratitudeItem[]> => {
if (!db) throw new Error('Database not initialized');
const items = await db.getAllAsync<GratitudeItem>(
'SELECT * FROM gratitude_items WHERE user_id = ? ORDER BY created_at DESC',
userId,
);
// Decrypt content for display
return Promise.all(
items.map(async (item) => ({
...item,
content: await decryptContent(item.encrypted_content),
})),
);
},
enabled: !!db && !!userId,
});
}
export function useCreateGratitude() {
const { db, userId } = useDatabase();
const queryClient = useQueryClient();
const { enqueueSync } = useSyncQueue();
return useMutation({
mutationFn: async (input: CreateGratitudeInput): Promise<GratitudeItem> => {
if (!db) throw new Error('Database not initialized');
const id = generateUUID();
const now = Date.now();
const encrypted = await encryptContent(input.content);
await db.runAsync(
`INSERT INTO gratitude_items (id, user_id, encrypted_content, category, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`,
id,
userId,
encrypted,
input.category,
now,
now,
);
const item: GratitudeItem = {
id,
user_id: userId!,
encrypted_content: encrypted,
category: input.category,
created_at: now.toString(),
updated_at: now.toString(),
};
// Queue for sync
await enqueueSync('gratitude_items', id, 'INSERT', item);
return item;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [FEATURE_KEY] });
},
});
}
export function useUpdateGratitude() {
const { db } = useDatabase();
const queryClient = useQueryClient();
const { enqueueSync } = useSyncQueue();
return useMutation({
mutationFn: async (input: UpdateGratitudeInput): Promise<void> => {
if (!db) throw new Error('Database not initialized');
const now = Date.now();
const updates: string[] = [];
const values: (string | number)[] = [];
if (input.content) {
updates.push('encrypted_content = ?');
values.push(await encryptContent(input.content));
}
if (input.category) {
updates.push('category = ?');
values.push(input.category);
}
updates.push('updated_at = ?');
values.push(now);
values.push(input.id);
await db.runAsync(`UPDATE gratitude_items SET ${updates.join(', ')} WHERE id = ?`, ...values);
// Queue for sync
await enqueueSync('gratitude_items', input.id, 'UPDATE', { id: input.id });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [FEATURE_KEY] });
},
});
}
export function useDeleteGratitude() {
const { db } = useDatabase();
const queryClient = useQueryClient();
const { enqueueSync } = useSyncQueue();
return useMutation({
mutationFn: async (id: string): Promise<void> => {
if (!db) throw new Error('Database not initialized');
await db.runAsync('DELETE FROM gratitude_items WHERE id = ?', id);
// Queue for sync
await enqueueSync('gratitude_items', id, 'DELETE');
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [FEATURE_KEY] });
},
});
}
src/features/<feature>/screens/FeatureListScreen.tsx:
import { useGratitudeItems, useDeleteGratitude } from '../hooks/useGratitude';
import { GratitudeCard } from '../components/GratitudeCard';
import { EmptyState } from '../../../components/EmptyState';
import { Button } from '../../../components/ui/Button';
export function GratitudeListScreen({ navigation }): React.ReactElement {
const { data: items, isLoading } = useGratitudeItems();
const deleteMutation = useDeleteGratitude();
if (isLoading) return <LoadingScreen />;
return (
<View className="flex-1 bg-slate-900">
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<GratitudeCard
item={item}
onPress={() => navigation.navigate('GratitudeDetail', { id: item.id })}
onDelete={() => deleteMutation.mutate(item.id)}
/>
)}
ListEmptyComponent={
<EmptyState
icon="Heart"
title="No Gratitude Items"
description="Start building your gratitude practice by adding your first item."
/>
}
/>
<FloatingActionButton
onPress={() => navigation.navigate('GratitudeDetail', { id: 'new' })}
/>
</View>
);
}
Update src/navigation/AppNavigator.tsx:
import { GratitudeListScreen } from '../features/gratitude-list/screens/GratitudeListScreen';
import { GratitudeDetailScreen } from '../features/gratitude-list/screens/GratitudeDetailScreen';
// Add to stack navigator
<Stack.Screen
name="GratitudeList"
component={GratitudeListScreen}
options={{ title: 'Gratitude' }}
/>
<Stack.Screen
name="GratitudeDetail"
component={GratitudeDetailScreen}
options={{ title: 'Gratitude Item' }}
/>
Update src/navigation/types.ts:
export type RootStackParamList = {
// ... existing screens
GratitudeList: undefined;
GratitudeDetail: { id: string };
};
Single text field + category/tags.
Rich text content + mood/feelings + date.
Multiple items with checkboxes + progress tracking.
Contact info + relationship type + notes.
encryptContent() before storingenqueueSync() after every writequeryClient.invalidateQueries() after mutationsuser_id and created_at columnsSee references/example-output.md for full generated code from node scaffold.js GratitudeList.
| Feature Type | Command | Use Case |
|---|---|---|
| Simple List | node scaffold.js GratitudeList | Gratitude, affirmations, quotes |
| Journal Entry | node scaffold.js DailyInventory | Reflections, step work |
| Checklist | node scaffold.js StepOneTasks | Step work tasks, goals |
| Relationships | node scaffold.js SponsorContact | People, sponsors, contacts |
Script auto-detects next migration number. If you have conflicts, manually rename files.
Add to tsconfig.json paths if needed:
"@/features/*": ["./src/features/*"]
Ensure you're logged in:
npx supabase login
npx supabase link --project-ref tbiunmmvfbakwlzykpwq
Project scaffolding, deployment configuration, and CI/CD setup for Google ADK agents.
Set up tracing, logging, and monitoring for deployed ADK agents across Cloud Trace, BigQuery, and third-party platforms.
Enterprise Azure infrastructure architect generating Bicep or Terraform from workload descriptions.
Plan and configure production-ready Azure Kubernetes Service clusters with Day-0 and Day-1 best practices.
Raw mechanical interfaces fusing Swiss typographic print with military terminal aesthetics. Rigid grids, extreme type scale contrast, utilitarian color, analog degradation effects. For data-heavy dashboards, portfolios, or editorial sites that need to feel like declassified blueprints.
Web search, scraping, extraction, crawling, and monitoring via ScrapeGraph AI CLI.
Skill for working with Firebase Hosting (Classic). Use this when you want to deploy static web apps, Single Page Apps (SPAs), or simple microservices. Do NOT use for Firebase App Hosting.
Deploy and manage web apps with Firebase App Hosting. Use this skill when deploying Next.js/Angular apps with backends.
Deploy applications and websites to Vercel. Use when the user requests deployment actions like "deploy my app", "deploy and give me the link", "push this live", or "create a preview deployment".
Build SEO-optimized pages at scale using templates, data, and proven playbook patterns.
Design and build isolated, reusable Convex backend components with clear boundaries and app-facing wrappers.
Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".
Opus.ai: Revolutionize Your Web Experience
Build a no-code AI app in minutes.
An IDE for code migration from legacy to modern frameworks through coding agents.
Automate CGI animation in live-action scenes
Branded artistic QR-code concepts
Launch a website in seconds with AI.
Streamline Your Coding Experience with AI Code Helper
Convert any screenshot or design to clean code.