--- 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. | Action | What "share" means here | Audience can | Applies 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 access | Any running service | | `list_in_dashboard(slug)` | Show the listing on the public Project Dashboard | **Discover and browse** to it from the gallery | A previously-published preview | | `open_source(project_dir)` | Push project source to the community GitHub repo | Fork the code and run their own copy | Any 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: | Switch | Off state | On state | Flipped by | |---|---|---|---| | **URL access** | Visiting the URL returns 404 / no service | URL works for anyone who has the link | `publish_preview` / `unpublish_preview` | | **Dashboard discoverability** | Listing row has `is_public=false` (or doesn't exist) โ€” invisible in the public gallery | Listing row has `is_public=true` โ€” appears in `/projects` | `list_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 | type | What it is | Eligible for `publish_preview()`? | |---|---|---| | `task` | Scheduled cron/interval job | No (no HTTP port) | | `service` | Long-running HTTP service (dashboard, API, page) | **Yes** โ€” any service can be exposed at a public URL | | `script` | One-shot script | No (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 phrasing | Action | |---|---| | "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 phrasing | Action | Notes | |---|---|---| | "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 rereading | Ask 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`: ```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. | Order | What happens | |---|---| | `open_source` first โ†’ `publish_preview` second | open_source records pending entry; publish_preview consumes it and links | | `publish_preview` first โ†’ `open_source` second | publish_preview records pending entry (needs `publisher_code_slug` arg); open_source consumes it and links | | Both at once / rename later | Re-run either side with updated binding; gateway re-links | ### Calling publish_preview with binding When publishing the URL FIRST and the code will follow: ```python 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: ```python {"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: ```python 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 ```bash 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 | Symptom | Cause | Fix | |---|---|---| | `publish_preview`: `Preview not found` | Wrong preview_id, or service was stopped | Check `/data/previews.json`, restart with `preview(action='serve')` | | `publish_preview`: `429 Too many published previews` | Hit 20-per-user gateway cap | `unpublish_preview()` something old first | | `publish_preview`: `FLY_MACHINE_ID not set` | Running locally, not in Starchild container | URL publish only works in the production container | | `open_source`: `400 Validation failed: env names not in .env.example` | Listed `MY_KEY` in `env_required` but forgot `.env.example` | Add the missing key to `.env.example` | | `open_source`: `400 Possible secret detected` | Secret scanner found a real-looking API key | Move 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)