PluginBench
Skill
Review
Audit score 70

community-publish

starchild-ai-agent/official-skills

How to install community-publish

npx skills add https://github.com/starchild-ai-agent/official-skills --skill community-publish
Claude Code
Cursor
Windsurf
Cline
Full instructions (SKILL.md)

Source of truth, from starchild-ai-agent/official-skills.


name: community-publish version: 0.14.1 description: | Publish previews, list on dashboard, open-source projects to community GitHub.

Use when the user wants to share, publish, list, or open-source what they built (e.g. make this dashboard public, share my project, push to GitHub). delivery: script metadata: starchild: emoji: ๐Ÿ“ฆ skillKey: community-publish user-invocable: true disable-model-invocation: false


Three independent actions

This skill handles three completely different kinds of sharing. They are NOT stages of one flow and NOT mutually exclusive โ€” a project can be in any combination.

ActionWhat "share" means hereAudience canApplies to
publish_preview(preview_id)Allocate a public URL https://community.iamstarchild.com/{user_id}-{slug}Open the URL if they know it โ€” point-to-point accessAny running service
list_in_dashboard(slug)Show the listing on the public Project DashboardDiscover and browse to it from the galleryA previously-published preview
open_source(project_dir)Push project source to the community GitHub repoFork the code and run their own copyAny project (task, service, script)

Critical: do NOT auto-list when publishing. publish_preview() only allocates the URL. Listing is a separate, deliberate user decision. If the user just says "publish my preview" / "ๅ…ฌๅผ€" without mentioning the dashboard, only call publish_preview(). After it succeeds, you may mention that list_in_dashboard() exists if they want others to discover it.

Service lifecycle (start / stop / health check) lives in the preview tool. This skill only handles the share side.


Visibility model โ€” read this before answering anything about who can see a project

A project's "publicness" is two orthogonal switches, not one:

SwitchOff stateOn stateFlipped by
URL accessVisiting the URL returns 404 / no serviceURL works for anyone who has the linkpublish_preview / unpublish_preview
Dashboard discoverabilityListing row has is_public=false (or doesn't exist) โ€” invisible in the public galleryListing row has is_public=true โ€” appears in /projectslist_in_dashboard / unlist_from_dashboard

Public URL โ‰  public discovery. A preview can be URL-reachable but undiscoverable (the default state right after publish_preview), or listed but URL-down (preview stopped after being listed), or any other combination. Never collapse these into "is it public yet".

Status questions are read-only operations. Whenever the user asks anything like:

  • "is it visible / public / discoverable yet?"
  • "can other people find it?"
  • "ไธŠๆžถไบ†ๅ— / ๅœจ dashboard ไธŠๅ— / ๅˆซไบบ่ƒฝ็œ‹ๅˆฐๅ—"
  • "is the listing live?"

The authoritative answer comes ONLY from a fresh get_listing_status(slug) call. Do NOT infer the answer from "I called publish_preview earlier so it must be visible" โ€” that's exactly the trap (publish_preview leaves is_public=false). Treat your own past actions as suggestive but never authoritative for a state question.


Project types โ€” three only

typeWhat it isEligible for publish_preview()?
taskScheduled cron/interval jobNo (no HTTP port)
serviceLong-running HTTP service (dashboard, API, page)Yes โ€” any service can be exposed at a public URL
scriptOne-shot scriptNo (no HTTP port)

Note: there is no preview type. If you encounter older docs mentioning it, treat as service.


Routing โ€” match user intent to the right surface

User requests fall into two fundamentally different categories. Mixing them up is the #1 source of wrong answers in this skill.

A. Status intents โ€” user wants to know current state

The user is asking a question about how things stand right now. The answer is data, not an action. Always reach for a read endpoint first; never answer from memory of past actions.

Sample phrasingAction
"is it visible / public / discoverable / live for others?"get_listing_status(slug)
"ไธŠๆžถไบ†ๅ— / ๅœจ dashboard ไธŠๅ— / ๅˆซไบบ่ƒฝไธ่ƒฝ็œ‹ๅˆฐ / ๅˆซไบบ่ƒฝๆœๅˆฐๅ—"get_listing_status(slug)
"what URLs do I have published?" / "ๆˆ‘ๅ‘ๅธƒไบ†ๅ“ชไบ›"list_published_previews()
"what's open-sourced?" / "้ƒฝๆœ‰ๅ“ชไบ›ๅผ€ๆบไปฃ็ "list_open_source(...)

B. Action intents โ€” user wants to change state

Sample phrasingActionNotes
"publish" / "share" / "make public" / "ๅ…ฌๅผ€" / "ๅ‘ๅธƒ" (no qualifier)publish_preview(preview_id)Allocates the URL only. Listing is NOT auto-flipped.
"publish the URL" / "share the link" / "let people visit" / "ๅ…ฌๅผ€่ฎฟ้—ฎ"publish_preview(preview_id)Same.
"list on the dashboard" / "ไธŠๆžถ" / "show on community" / "make discoverable" / "let people find this" / "ๅ‘ๅˆฐๅนฟๅœบ"list_in_dashboard(slug)Requires the preview to already exist.
"publish AND list" / "publish and put on dashboard" / "ๅ‘ๅธƒๅนถไธŠๆžถ"publish_preview() THEN list_in_dashboard()Two separate calls in order.
"remove from dashboard" / "ไธ‹ๆžถ" / "unlist" / "hide from gallery"unlist_from_dashboard(slug)Preview URL stays alive.
"open source" / "open-source the code" / "share the code" / "let others fork" / "ๅผ€ๆบไปฃ็ "open_source(project_dir)Explicit code-sharing intent.
"unpublish the URL" / "take down the link"unpublish_preview(slug)Listing row stays.
"remove the open source" / "delete from GitHub"remove_open_source(slug)
"fork" / "install someone's project"fork(source)
Ambiguous after rereadingAsk one question, don't guess"Do you want it (a) just shareable by URL, (b) also discoverable on the public dashboard, or (c) also have the code open-sourced on GitHub?"

Cross-link via publisher: binding

When the same project has BOTH a public URL AND open-sourced code, you want them paired so the frontend renders "View Source" on the listing card and "Visit Live Demo" on the code card. This skill drives that pairing through one explicit binding in project.yaml โ€” no name guessing, no fuzzy matching, no follow-up dialogues.

How to declare the binding

Add a publisher: block to project.yaml:

name: my-app                      # GitHub catalog slug + default for both sides
type: service
version: 1.0.0
publisher:
  code_slug: my-app               # OPTIONAL โ€” defaults to manifest.name
  public_slug: my-app-pub         # OPTIONAL โ€” URL suffix; defaults to code_slug

Both fields are optional. If omitted, both default to manifest.name. Set them only when you want different slugs on each side (e.g. short URL dash for code my-detailed-dashboard).

Either side can be published first

The gateway holds a pending entry until the second side arrives. No ordering requirement, no manual link step.

OrderWhat happens
open_source first โ†’ publish_preview secondopen_source records pending entry; publish_preview consumes it and links
publish_preview first โ†’ open_source secondpublish_preview records pending entry (needs publisher_code_slug arg); open_source consumes it and links
Both at once / rename laterRe-run either side with updated binding; gateway re-links

Calling publish_preview with binding

When publishing the URL FIRST and the code will follow:

publish_preview(
    preview_id="my-app-a3f1",
    slug="my-app-pub",
    publisher_code_slug="my-app",   # so future open_source(my-app) auto-links here
)

If publisher_code_slug is omitted, no pending entry is recorded โ€” the code side will need to declare publisher.public_slug itself in project.yaml to wire the link.

Return value

Both functions return a publisher field showing what binding was sent and a hint describing the cross-link state:

{"ok": True, "url": "...", "publisher": {"code_slug": "my-app"},
 "hint": "Cross-link binding declared. If that code is already open-sourced, it's now linked. If not, the link is pending..."}

You don't need to react to the hint โ€” it's informational. The gateway handles wiring automatically.

Manual repair (rare)

If a pairing was wired wrong (e.g. after a rename or a slug typo), use:

link_to_listing(listing_slug="2004-my-app-pub", code_slug="my-app")

This skips the binding flow and writes the link directly. Don't use this for normal publishing โ€” fix project.yaml's publisher: block instead so the binding survives future republishes.


Architecture

                community.iamstarchild.com (single gateway domain)
                              โ”‚
            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
            โ”‚                                   โ”‚
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚  /api/register   โ”‚                โ”‚/api/code-projectsโ”‚
   โ”‚  /api/unregister โ”‚                โ”‚ /publish, /list, โ”‚
   โ”‚  /api/list       โ”‚                โ”‚ /unpublish, ...  โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚                                   โ”‚
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ DB: route table  โ”‚                โ”‚ GitHub:          โ”‚
   โ”‚ + project_       โ”‚                โ”‚ Starchild-ai-    โ”‚
   โ”‚   listings       โ”‚                โ”‚ agent/community- โ”‚
   โ”‚ + publisher_     โ”‚                โ”‚ projects         โ”‚
   โ”‚   pending_links  โ”‚                โ”‚                  โ”‚
   โ”‚                  โ”‚                โ”‚ Permanent.       โ”‚
   โ”‚ Service stays up โ”‚                โ”‚                  โ”‚
   โ”‚ as long as your  โ”‚                โ”‚                  โ”‚
   โ”‚ container runs.  โ”‚                โ”‚                  โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     publish_preview()                    open_source()

publisher_pending_links is the cross-link table. Either side writes a pending row, the other side consumes it on arrival.


publish_preview() โ€” public URL

publish_preview(preview_id, slug="", title="", publisher_code_slug="")

Map a running service to https://community.iamstarchild.com/{user_id}-{slug}.

  • preview_id: from preview(action='serve'). Must be status=running.
  • slug: URL suffix only (lowercase alphanumeric + hyphens, 3-50 chars). User_id prefix is added automatically โ€” pass 'my-app', NOT '1463-my-app'.
  • title: display name for the listing.
  • publisher_code_slug: optional cross-link binding to a code project's slug. Sets up the pending entry so the eventual open_source() call auto-links.

Returns {"ok": True, "url": "...", "publisher": {...}, "hint": "..."}.

Constraints:

  • Max 20 published previews per user (gateway returns 429 over).
  • Service must be running. Stops working when the container goes down (visitors see offline page).
  • Slug stays bound to the port โ€” stop and re-serve, the URL stays valid.
  • Only works inside the Starchild Fly container (needs FLY_MACHINE_ID).
  • Listing visibility default is is_public=false. A successful publish_preview allocates the URL but creates the listing row in PRIVATE state โ€” strangers cannot discover the project on the dashboard. Discovery requires a separate list_in_dashboard() call. Do NOT tell the user "your project is now public" after only calling publish_preview โ€” say "the URL is live" and offer list_in_dashboard as the next step if they want it discoverable.

Companions:

  • unpublish_preview(slug) โ€” remove the public URL. Slug accepts full {user_id}-{suffix} or just suffix.
  • list_published_previews() โ€” all currently published preview URLs for this user.

list_in_dashboard() โ€” show on Project Dashboard

list_in_dashboard(slug, name=None, description="", cover_url=None, tags=None)

Make a published preview discoverable in the public Project Dashboard at https://community.iamstarchild.com/projects. Without this, the preview URL works but is invisible to anyone who doesn't already know it.

  • slug: the full slug returned by publish_preview() (i.e. {user_id}-{suffix}). The gateway's ownership check uses this exact value.
  • name: dashboard card display name. Defaults to slug.
  • description: โ‰ค500 chars.
  • cover_url: must be on storage.googleapis.com, image.thum.io, or api.microlink.io. Other domains rejected with 400. If omitted, the gateway captures a screenshot asynchronously.
  • tags: โ‰ค5 tags, โ‰ค20 chars each.

Returns {"ok": True, "listing": {...}, "url": "...", "dashboard_url": "..."}.

Constraints:

  • Requires publish_preview() to have run first for the same slug โ€” returns 404 with a clear error otherwise.
  • Idempotent: calling again with different name/tags updates the existing listing.
  • Listings created via publish_preview() start as private (not on dashboard) โ€” list_in_dashboard() is the ONLY way to make them discoverable.

Companions:

  • unlist_from_dashboard(slug) โ€” remove from dashboard, keep URL alive.
  • get_listing_status(slug) โ€” read-only check: returns {ok, exists, is_public, listing}. Note: only public listings are observable through this โ€” if the gateway returns 404, the listing is either nonexistent OR private (no way to distinguish).

open_source() โ€” push code to GitHub

open_source(project_dir, version_bump="patch", message="", make_discoverable=True)

Push project source to community-projects/projects/{user_id}/{slug}/ on GitHub. Versioning is delegated entirely to git history โ€” there's no {version}/ snapshot directory and no per-type bucket above the slug. The type field inside project.yaml stays around as runtime metadata (so forks know whether to schedule, run-once, or expose a service) but no longer affects the storage path.

  • project_dir: e.g. output/projects/my-task
  • version_bump: patch | minor | major | none
  • message: commit message body describing what this version changed. You (the agent) should always compose this based on the actual code changes you made in this session โ€” never leave it blank if you know what changed, never ask the user to write it. Aim for one to three short lines. Don't list every file; describe the user-visible change. If the user explicitly said "just publish", use a one-line summary like "Initial publish" or "Re-publish without changes".
  • make_discoverable (default True): "open source" matches the user-intuition default of "make this fully public". After pushing the code, if a paired preview/listing row exists for the same slug (via publisher.public_slug / publisher.code_slug / manifest.name), it is automatically flipped to public on the Project Dashboard. Pure libraries / pure scripts with no preview row are skipped silently โ€” no side effect. Pass make_discoverable=False when the listing should stay private.
  • Returns {"ok": True, "github_url": ..., "version": ..., "publisher": {...}, "auto_list": {...}, "hint": "..."}
    • auto_list.attempted: whether the auto-list path was tried (False when make_discoverable=False)
    • auto_list.listed: True on success, False otherwise
    • auto_list.reason: "no_preview_for_slug" (pure code case โ€” expected) or "list_failed" with error field

Commit message style โ€” write like a normal git commit body:

โœ… "Add WebSocket reconnect on dropped connections; refactor prompt builder for shared state." โœ… "Fix funding-rate sign convention; add unit test for negative-funding path." โŒ "Updated 3 files" (uninformative) โŒ "Modified src/index.html, src/main.py, project.yaml" (lists files instead of intent) โŒ "User asked to publish" (describes the request, not the change)

Companions:

  • fork(source, dest_dir=None) โ€” install someone else's open-sourced project locally
    • source: "user_id/slug" (always pulls current state โ€” older snapshots live in GitHub commit history)
    • For task type: registers as paused, returns next_step instructions
    • For service type: returns ready-to-serve info
  • list_open_source(type=None, tag=None, user=None, q=None) โ€” browse the GitHub catalog
  • get_open_source(source) โ€” fetch one project's full metadata
  • remove_open_source(slug) โ€” delete project directory from GitHub catalog (owner only). Git history of the deletion + previous commits is preserved in the repo's commit log.
  • validate_open_source(project_dir) โ€” pre-flight check before publishing

Project structure

Every project under output/projects/{slug}/:

project.yaml      # metadata (name, version, type, env_required, sc_proxy, publisher)
PROJECT.md        # required sections: What / Required env / How to start / Outputs / Troubleshooting
.env.example      # all env vars with placeholder values
.gitignore        # secrets blacklist
src/
  โ”œโ”€โ”€ run.py       # for type=task (must start: # -*- task-system: v3 -*-)
  โ”œโ”€โ”€ index.html   # for type=service (or app.py + frontend)
  โ””โ”€โ”€ main.py      # for type=script

Don't conflate the two list functions

list_published_previews() returns live URLs (preview side). list_open_source() returns open-sourced code (GitHub side). Different datasets โ€” never quote one number to answer a question about the other.


Usage from a bash block

python3 - <<'EOF'
import sys
sys.path.insert(0, "/data/workspace/skills/community-publish")
from exports import (
    # Public URL
    publish_preview, unpublish_preview, list_published_previews,
    # Open source code
    open_source, remove_open_source, fork,
    list_open_source, get_open_source, validate_open_source,
    # Manual repair (rare)
    link_to_listing,
)

# Cross-linked publish: declare publisher in project.yaml first, then both
# sides auto-pair regardless of order.
print(publish_preview(preview_id="my-app-a3f1", slug="my-app",
                      publisher_code_slug="my-app"))
print(open_source("output/projects/my-app", version_bump="patch"))
EOF

Behavioral rules

  • Show the diff before open_source(). After validate_open_source, summarize what's about to be pushed (file list, version, type, tags, env_required) and ask for confirmation. Exception: explicit "publish without confirmation" or re-publish of a known good project.
  • Never auto-run setup.sh on fork. Show the command, let the user confirm.
  • Always collect env in one batch on fork. Read project's env_required, diff against workspace/.env, call request_env_input ONCE with the missing keys. Don't ask one-by-one.
  • Slug rules: lowercase alphanumeric + hyphens, 3-50 chars, no leading/trailing hyphen. Skill auto-strips duplicate {user_id}- prefix if you accidentally include it.
  • Version rules (open_source): strict semver. Re-publishing same version is rejected. New version must be > current latest.
  • Type immutability (open_source): once published as task, can't change to service later. Pick a different slug.
  • URL โ‰  code: a public URL going down (container off) does NOT remove the open-source code, and vice versa. They're independent.
  • Don't manually call link_to_listing() in the normal flow. The publisher: binding handles cross-linking. Manual link is only for repair.

Common gotchas

SymptomCauseFix
publish_preview: Preview not foundWrong preview_id, or service was stoppedCheck /data/previews.json, restart with preview(action='serve')
publish_preview: 429 Too many published previewsHit 20-per-user gateway capunpublish_preview() something old first
publish_preview: FLY_MACHINE_ID not setRunning locally, not in Starchild containerURL publish only works in the production container
open_source: 400 Validation failed: env names not in .env.exampleListed MY_KEY in env_required but forgot .env.exampleAdd the missing key to .env.example
open_source: 400 Possible secret detectedSecret scanner found a real-looking API keyMove to env var; .env.example value should be your-key-here

| open_source: 400 Type cannot change after publish | Trying to switch task โ†” service โ†” script | Pick a different slug | | remove_open_source: 403 Permission denied | Trying to remove someone else's project | Only the owner can remove | | Cross-link not appearing on frontend | Binding mismatch between sides | Check both sides' slugs match the publisher: block; or use link_to_listing to repair | | Forked task doesn't run | Auto-registered as paused | Tell user: scheduled_task(action='activate', job_id={id}) |


References

  • lib/manifest.py โ€” project.yaml parser/writer + semver helpers
  • lib/validate.py โ€” local pre-publish validation (mirrors gateway-side checks)
  • lib/install.py โ€” type-specific install handlers (task/service/script)
  • lib/gateway.py โ€” HTTP client for /api/register (URL side) and /api/code-projects/* (code side)