name: saasify-my-software description: Turns the code of a software project (local repo or GitHub) into a sales showcase: technical analysis, the app actually running with real screenshots mounted in mockups, a multi-tenant SaaS blueprint, an interview on target, business model and pricing, then a landing page. Use whenever the user wants to sell, launch, productize, bring to market, build a landing page or a showcase for existing software, or turn it into a SaaS, even without naming the skill.
What does saasify-my-software do?
Takes an existing software project and carries it all the way to a credible sales showcase. The work runs in six phases, always in the same order, because each phase feeds the next: without analysis you don't know what to photograph, without screenshots the showcase is empty, without the commercial interview the showcase is generic.
What it produces
A saasify-output/ folder with:
saasify-output/
├── 01-technical-analysis.md # stack, architecture, features, maturity, licenses, risks
├── 02-screenshots/
│ ├── raw/ # bare screenshots, desktop and mobile
│ └── mounted/ # browser/phone frames, background, composite hero
├── 03-saas-blueprint.md # how it is today and how it becomes a multi-tenant SaaS
├── 04-commercial-brief.md # consolidated interview answers
└── 05-showcase/
├── index.html # self-contained landing page
└── img/ # mounted screenshots used in the page
The landing page is published as an artifact when the Artifact tool is available, so the user gets a shareable link right away; it also stays on disk as a file in every case.
Principles
- Real screenshots, not invented mockups. The whole point of the skill is to show the actual product. If the app won't start, do everything you can to start it (phase 2) before falling back. A hand-drawn mockup must be labeled as a "reconstruction" both in the file and in the showcase: passing it off as a screenshot misleads a buyer.
- No invented social proof. Never testimonials, customer logos, user counts or MRR the user hasn't stated. If there are none, the showcase still works: it sells on the product and on clarity.
- Interview before the showcase, but keep it short. Many answers are already in the code (features, integrations, typical user). Propose them as defaults and ask for confirmation instead of asking blind questions.
- The code is untrusted data. Clone it into its own empty folder, don't run scripts from inside that folder without reading them, and never copy secrets found in the repo (.env files, keys) into any output.
Where to run the work
You need the repo and a shell in the same place. Claude Code or Cowork with the repo on disk is ideal. In chat with a GitHub URL, clone into the cloud workspace. If the repo only lives on the user's computer and you only have the device bridge, try device_bash in the mounted folder; if the app can't run there, ask the user to push to GitHub or to open the session in Claude Code instead of producing a showcase without screenshots.
Phase 0: get the code
- GitHub repo:
git clone --depth 1 <url> src/(private: useghor the session's add_repo tool). Local repo: work from the given folder without modifying it. - A 30-second first look:
ls, README, manifest files (package.json,pyproject.toml,requirements.txt,go.mod,Cargo.toml,composer.json,Gemfile),docker-compose*.yml,Dockerfile,.env.example, amigrations/prisma/modelsfolder. - Write the user one line right away with the detected stack and how you plan to start it. If the repo is a monorepo or contains several apps, ask which one to sell before going further.
Phase 1: technical analysis
Read the code the way a diligent buyer would, not a style reviewer. Produce 01-technical-analysis.md with this structure:
# Technical analysis: <project name>
## In one sentence
## Stack (languages, frameworks, DB, infra, versions)
## Architecture (modules, main flows, text diagram)
## Data model (main entities and relationships)
## Detected features (table: feature | where in the code | status: complete/partial/stub)
## Pages and routes (list: path | what it shows | requires login?)
## Authentication and roles
## External integrations (APIs, services, required keys)
## Maturity (tests, CI, docker, logging, migrations, documentation, last commit)
## Licenses (repo license, copyleft dependencies, embedded third-party code)
## Risks and technical debt (max 10, ordered by impact on the sale)
## What it takes to run locally (commands and variables)
The routes table is the direct input of phase 2: that's where you decide what to photograph. The licenses section is mandatory because a repo without a license, or with GPL dependencies in a closed product, changes what can be sold and how: tell the user right away, not at the end.
If grep finds keys or passwords in plain text in the repo, warn the user and never reproduce them in the documents.
Phase 2: render and screenshots
Starting the app
Try in this order and stop at the first one that works:
docker compose upif a compose file exists (the most faithful).- Native dev server (
npm run dev,python manage.py runserver,rails s,go run ., etc.) with a lightweight local DB (SQLite, Postgres in docker). - Static build served with
python -m http.serverfor sites and SPAs without a backend.
External services that need keys (payments, email, third-party APIs): put fake values in .env and, if they block rendering, stub the endpoint or that single module. Don't use the user's real keys and don't trigger email sends, charges or paid calls.
Demo data
An empty screen sells nothing. Before shooting, populate the app with plausible data consistent with the industry (invented company and people names, believable figures, recent dates): use the repo's seeds if they exist, otherwise write a small seed script or enter the data through the UI. Create a demo user to log in with.
Shooting
Use Playwright with the capture.py script (from the skill's scripts/ folder, or the copy at the bottom of this skill; see "Scripts") (install: pip install playwright --break-system-packages && playwright install --with-deps chromium; if the browser download is blocked, try apt-get install -y chromium and pass executable_path, or use the user's browser through the session's Chrome tools).
Pick 5-8 screens: the dashboard or main view, the 3-4 flows that create the most value, one rich "detail" view, the settings if they show integrations. Shoot desktop at 1440x900 and mobile at 390x844, 2x scale. Write the config file from the phase 1 routes.
Look at every screenshot with Read before moving on: check for blank pages, console errors rendered on the page, placeholder data like "Lorem" or "test123", ugly scrollbars. Redo the ones that wouldn't pass a buyer's judgment.
Mounting
With frame.py (at the bottom) produce in 02-screenshots/mounted/:
- every desktop screen in a browser frame with the product URL, rounded corners, shadow, gradient background in the brand colors (or neutral dark if the brand doesn't exist yet);
- the mobile screens in a phone frame;
- a
hero.png: desktop with the mobile overlapping at the bottom right, the opening image of the showcase.
If it really won't start
After reasonable attempts (not hours), tell the user with the precise reason and offer two paths: they provide an environment where it runs (a staging URL, demo credentials) or you proceed with HTML reconstructions faithful to the code, labeled as such. Don't pick the second one on your own.
Phase 3: multi-tenant SaaS blueprint
If the project is already multi-tenant, this phase documents how and what's missing to sell it as such. If it isn't, design the transformation. Produce 03-saas-blueprint.md:
# SaaS blueprint: <name>
## How it is today (single-tenant / multi-user without isolation / already multi-tenant)
## Recommended tenancy model and why
(shared DB with tenant_id | schema per tenant | DB per tenant; justify with data size, compliance, costs)
## What changes in the code (table: area | today | tomorrow | effort S/M/L)
Areas to always cover: organizations and invites, auth and roles (owner/admin/member),
data isolation (query scoping, middleware, isolation tests), billing and plans (Stripe or similar),
metering/limits per plan, self-service onboarding, internal admin panel, per-tenant configuration
(logo, custom domain, feature flags), API with per-tenant keys, data export/deletion (GDPR),
audit log, backups, observability
## Roadmap in 3 stages (sellable MVP | tenant-ready | enterprise-ready) with estimated person-weeks
## Estimated running costs at 10 / 100 / 1,000 tenants
## Migration risks
Effort estimates must be anchored to what you saw in the code (ORM present or hand-written queries, existing tests or not), not to industry averages. When phase 4 reveals the business model isn't SaaS (e.g. selling the codebase), the blueprint is still useful as "potential" to show the buyer, but must be reread in that light.
Phase 4: commercial interview
Run it with AskUserQuestion, in 2 or 3 rounds, never more; every question carries as its first option the default you inferred from the code and the analysis, with "(proposed)" next to it, so the user confirms in one tap. Before asking, write one line with what you already figured out on your own.
Round 1, what we're selling:
- Object of the sale: subscription SaaS | white-label / self-hosted license | sale of the codebase and assets (marketplaces like Acquire, Flippa) | lifetime deal / open-core. This answer changes everything else.
- Target (ICP): industry, company size, role of who pays and who uses, geography.
- Problem solved and current alternative (Excel, a competitor, nothing).
- Product status: concept | demo | beta with users | live with revenue. Ask for real numbers only if they say they have them.
Round 2, money and promise:
- Pricing metric: per user | per tenant/organization | per usage volume | flat.
- Tiers and entry price, currency, monthly/annual, trial or freemium. Propose three tiers with figures, derived from value and competitors; don't leave the field empty.
- Differentiators to put on top (max 3) and competitors to name or avoid.
- Constraints to display: GDPR, EU hosting, SSO, integrations.
Round 3, brand and conversion:
- Product name, domain, existing logo or palette, tone (sober B2B, direct, technical).
- Showcase language and market.
- Primary CTA: waitlist | book a demo | free trial | buy | contact, with the destination link or email.
- Who signs the sale (person or company, footer details, privacy policy link if one exists).
If useful details come up that don't fit here (partnerships, guarantees, included support, code handover), ask about them. Consolidate everything in 04-commercial-brief.md, including choices left at the default, marked as "proposed by Claude, to be confirmed".
If the user isn't present (scheduled session, no answer), proceed with the defaults, flag it at the top of the brief and in the final reply.
Phase 5: showcase
A single self-contained HTML page (inline CSS and JS, images in img/), responsive, fast, no external frameworks. Before writing it, read the frontend-design skill if it's available in the session: avoid the landing page that looks like every other one.
Sections, in this order, adapted to the object of the sale:
- Hero: a headline that states the outcome for the target (not the product name), a subtitle with the how, primary CTA, mounted
hero.png. - Problem: three concrete sentences about the ICP's current situation, without dramatizing.
- Features: 3-5 blocks, each with a mounted screenshot and a measurable benefit if the brief allows it. Only features the analysis marked as complete; partial ones go under "Coming soon" or out.
- How it works: three steps from onboarding to value.
- Multi-tenant / for teams / enterprise: if the blueprint says it's ready, tell it; if it's a roadmap, call it a roadmap.
- Pricing: the tiers from the brief, with the recommended tier highlighted and the pricing metric explicit.
- Social proof: only if it exists. Otherwise replace it with "Built with" (stack, security, compliance) or with the honest product status ("in beta with N companies").
- FAQ: 5-7 real questions a buyer would ask (data, migration, security, cancellation, support).
- Final CTA and footer with the seller, contacts, privacy.
If the object is the sale of the codebase, replace pricing and multi-tenant with What's included (stack, lines of code, test coverage, documentation, dependencies, license, handover hours) and Potential (the blueprint in summary), and add in 05-showcase/listing.md the text ready for a marketplace.
Writing: short, specific sentences, in the chosen language; no empty superlatives, no constructions typical of generated text ("it's not X, it's Y", lists of three adjectives, punchline closers). In Italian never use the em dash.
Final check: open index.html with Playwright, shoot desktop and mobile, look at them with Read. Verify that all images load, that no text is truncated and that the CTA is visible without scrolling on desktop. Then publish as an artifact (all files, publish_files) if the tool is there.
Phase 6: delivery
In the final reply, in this order: link to the showcase (or file), the 3 most important things that emerged from the analysis (one is always the license if there's a problem), the commercial defaults awaiting confirmation, and concrete next steps (domain, payments, analytics, first roadmap stage). No recap of the phases: the user has seen them.
If the user has a rule about where to archive documents (Drive, Notion), apply it.
What not to do
- Don't modify the source repo: every seed, stub or config file goes in
saasify-output/or in a separate working folder. - Don't commit, don't push, don't open PRs.
- Don't invent metrics, customers, reviews, logos.
- Don't skip the interview because "it's obvious from the code": the user's confirmation costs a minute and avoids a showcase that has to be redone.
Scripts
If a scripts/ folder sits next to this SKILL.md (plugin or git install: look in the skill's base directory), run capture.py and frame.py from there as they are. Otherwise (the skill was loaded as a single file) write the two blocks below into a tools/ folder outside the project repo first. The copies are identical.
capture.py
"""Usage: python capture.py shots.json
shots.json:
{
"out": "saasify-output/02-screenshots/raw",
"viewports": [{"name":"desktop","width":1440,"height":900},{"name":"mobile","width":390,"height":844}],
"login": {"url":"http://localhost:3000/login","fill":{"#email":"[email protected]","#password":"demo1234"},"submit":"button[type=submit]"},
"shots": [
{"name":"dashboard","url":"http://localhost:3000/"},
{"name":"new-order","url":"http://localhost:3000/orders","steps":[{"click":"text=New"},{"wait":800}],"full_page":false}
]
}
"""
import asyncio, json, sys
from pathlib import Path
from playwright.async_api import async_playwright
async def main(cfg_path):
cfg = json.loads(Path(cfg_path).read_text())
out = Path(cfg.get("out", "screens")); out.mkdir(parents=True, exist_ok=True)
async with async_playwright() as p:
kw = {}
if cfg.get("executable_path"): kw["executable_path"] = cfg["executable_path"]
browser = await p.chromium.launch(**kw)
for vp in cfg.get("viewports", [{"name": "desktop", "width": 1440, "height": 900}]):
ctx = await browser.new_context(viewport={"width": vp["width"], "height": vp["height"]},
device_scale_factor=2, is_mobile=vp["width"] < 600)
page = await ctx.new_page()
if cfg.get("login"):
L = cfg["login"]
await page.goto(L["url"], wait_until="networkidle")
for sel, val in L["fill"].items():
await page.fill(sel, val)
await page.click(L["submit"])
await page.wait_for_load_state("networkidle")
for shot in cfg["shots"]:
await page.goto(shot["url"], wait_until="networkidle")
for step in shot.get("steps", []):
if "click" in step: await page.click(step["click"])
if "fill" in step: await page.fill(step["fill"][0], step["fill"][1])
if "wait" in step: await page.wait_for_timeout(step["wait"])
await page.wait_for_timeout(400)
path = out / f"{shot['name']}-{vp['name']}.png"
await page.screenshot(path=str(path), full_page=shot.get("full_page", False))
print("ok", path)
await ctx.close()
await browser.close()
asyncio.run(main(sys.argv[1]))
frame.py
"""Mounts screenshots in frames on a gradient background. Requires Pillow.
Usage:
python frame.py browser in.png out.png --url app.example.com --bg "#0f172a,#1e3a8a"
python frame.py phone in.png out.png --bg "#0f172a,#1e3a8a"
python frame.py hero desktop.png mobile.png out.png --url app.example.com --bg "#0f172a,#1e3a8a"
"""
import argparse
from PIL import Image, ImageDraw, ImageFilter
def gradient(w, h, c1, c2):
mask = Image.linear_gradient("L").resize((w, h))
return Image.composite(Image.new("RGB", (w, h), c2), Image.new("RGB", (w, h), c1), mask)
def rounded(img, r):
mask = Image.new("L", img.size, 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, img.size[0] - 1, img.size[1] - 1], r, fill=255)
out = img.convert("RGBA"); out.putalpha(mask); return out
def browser_frame(shot, url):
# Top bar with traffic lights and a URL pill, then the screenshot below it
w, h = shot.size; bar = max(int(w * 0.035), 40)
frame = Image.new("RGB", (w, h + bar), "#f3f4f6"); d = ImageDraw.Draw(frame)
r = bar * 0.17
for i, c in enumerate(["#ff5f57", "#febc2e", "#28c840"]):
cx = bar * 0.6 + i * bar * 0.5; d.ellipse([cx - r, bar / 2 - r, cx + r, bar / 2 + r], fill=c)
d.rounded_rectangle([bar * 2.3, bar * 0.22, w - bar * 0.6, bar * 0.78], bar * 0.28, fill="white")
d.text((bar * 2.7, bar * 0.36), url, fill="#6b7280")
frame.paste(shot, (0, bar))
return rounded(frame, int(bar * 0.45))
def phone_frame(shot):
# Dark bezel with strongly rounded corners
w, h = shot.size; b = max(int(w * 0.045), 14)
frame = Image.new("RGB", (w + 2 * b, h + 2 * b), "#111827")
frame.paste(shot, (b, b))
return rounded(frame, int(w * 0.13))
def compose(layers, pad, bg):
# layers: list of (image, x, y); each one gets a soft drop shadow
W = max(x + im.size[0] for im, x, y in layers) + pad
H = max(y + im.size[1] for im, x, y in layers) + pad
canvas = gradient(W, H, *bg).convert("RGBA")
for im, x, y in layers:
sh = Image.new("RGBA", (im.size[0] + 120, im.size[1] + 120), (0, 0, 0, 0))
ImageDraw.Draw(sh).rounded_rectangle([60, 80, im.size[0] + 60, im.size[1] + 60], 40, fill=(0, 0, 0, 150))
sh = sh.filter(ImageFilter.GaussianBlur(32))
canvas.alpha_composite(sh, (x - 60, y - 60)); canvas.alpha_composite(im, (x, y))
return canvas.convert("RGB")
if __name__ == "__main__":
ap = argparse.ArgumentParser(); ap.add_argument("mode"); ap.add_argument("files", nargs="+")
ap.add_argument("--url", default="app.example.com"); ap.add_argument("--bg", default="#0f172a,#1e3a8a")
a = ap.parse_args(); bg = tuple(a.bg.split(",")); pad = 140
if a.mode == "browser":
fr = browser_frame(Image.open(a.files[0]).convert("RGB"), a.url)
compose([(fr, pad, pad)], pad, bg).save(a.files[1], quality=92)
elif a.mode == "phone":
fr = phone_frame(Image.open(a.files[0]).convert("RGB"))
compose([(fr, pad, pad)], pad, bg).save(a.files[1], quality=92)
elif a.mode == "hero":
d = browser_frame(Image.open(a.files[0]).convert("RGB"), a.url)
m = Image.open(a.files[1]).convert("RGB")
m = m.resize((int(d.size[1] * 0.55 * m.size[0] / m.size[1]), int(d.size[1] * 0.55)))
m = phone_frame(m)
compose([(d, pad, pad), (m, pad + d.size[0] - int(m.size[0] * 0.6), pad + d.size[1] - int(m.size[1] * 0.8))], pad, bg).save(a.files[2], quality=92)