## PiCloud CMS PoC — declarative manifest. ## Deploy: pic apply --env dev (from this directory, after `pic login`). ## Everything (scripts, routes, triggers, interceptors, config) lives here. [project] slug = "cms" name = "PiCloud CMS PoC" [app] slug = "cms" name = "PiCloud CMS" description = "A WordPress-inspired CMS whose backend is 100% PiCloud Rhai scripts." # ------------------------------------------------------------------ config # NOTE: env-specific values (e.g. site_base_url) live in the overlay files # picloud.dev.toml / picloud.prod.toml, NOT in inline [vars.] sections # (an inline [vars.dev] here would parse as a var literally NAMED "dev"). See # FINDINGS F-005. # NOTE: var KEYS must be kebab-case ([a-z0-9-], no underscores) — see FINDINGS F-008. [vars] site-title = "My PiCloud Blog" posts-per-page = "10" comment-moderation = "manual" # "manual" | "auto" notify-from-email = "blog@example.com" signups-open = "true" # ------------------------------------------------------------------ secrets # Values are pushed out-of-band via `pic secrets set ` (never in the # manifest). `setup-token` gates the one-time admin bootstrap endpoint. [secrets] names = ["setup-token"] # ------------------------------------------------------------------ scripts [[scripts]] name = "hello" file = "scripts/hello.rhai" kind = "endpoint" description = "Skeleton smoke-test endpoint." # --- auth (shared module + endpoints) --- [[scripts]] name = "auth" file = "scripts/auth/auth.rhai" kind = "module" description = "Role-check helper module — the CMS access boundary." [[scripts]] name = "auth_register" file = "scripts/auth/register.rhai" description = "Reader self-signup." [[scripts]] name = "auth_login" file = "scripts/auth/login.rhai" description = "Email+password -> session token." [[scripts]] name = "auth_logout" file = "scripts/auth/logout.rhai" description = "Invalidate the current session." [[scripts]] name = "auth_me" file = "scripts/auth/me.rhai" description = "Current user + roles." [[scripts]] name = "auth_bootstrap" file = "scripts/auth/bootstrap.rhai" description = "One-time first-admin creation (setup-token gated)." [[scripts]] name = "admin_users_list" file = "scripts/auth/admin_users_list.rhai" description = "List app-users + roles (admin)." [[scripts]] name = "admin_user_roles" file = "scripts/auth/admin_user_roles.rhai" description = "Grant/revoke user roles (admin)." # --- shared util module --- [[scripts]] name = "util" file = "scripts/util.rhai" kind = "module" description = "slugify / doc-shaping / CAS counter helpers." # --- posts --- [[scripts]] name = "posts_list" file = "scripts/posts/posts_list.rhai" description = "Public list of published posts." [[scripts]] name = "post_get" file = "scripts/posts/post_get.rhai" description = "Public single post by slug (+ view counter)." [[scripts]] name = "posts_admin_list" file = "scripts/posts/posts_admin_list.rhai" description = "Dashboard post list (author+)." [[scripts]] name = "post_create" file = "scripts/posts/post_create.rhai" description = "Create a post (author+)." [[scripts]] name = "post_update" file = "scripts/posts/post_update.rhai" description = "Update a post (own-only unless admin)." [[scripts]] name = "post_delete" file = "scripts/posts/post_delete.rhai" description = "Delete a post (admin)." # --- pages (one script, method-dispatched) --- [[scripts]] name = "pages" file = "scripts/pages/pages.rhai" description = "Page CRUD (public reads + admin writes)." # --- comments --- [[scripts]] name = "comment_sanitize" file = "scripts/comments/comment_sanitize.rhai" description = "docs before-interceptor: escape HTML + set moderation status." [[scripts]] name = "comment_create" file = "scripts/comments/comment_create.rhai" description = "Submit a comment (public, rate-limited)." [[scripts]] name = "comments_for_post" file = "scripts/comments/comments_for_post.rhai" description = "Public approved comments for a post." [[scripts]] name = "comments_admin" file = "scripts/comments/comments_admin.rhai" description = "Comment moderation (admin)." [[scripts]] name = "comment_on_approve" file = "scripts/comments/comment_on_approve.rhai" description = "docs trigger: publish approved comment to SSE feed." # --- notifications --- [[scripts]] name = "posts_on_publish" file = "scripts/posts/posts_on_publish.rhai" description = "docs trigger: enqueue reader notifications on publish." [[scripts]] name = "notify_drain" file = "scripts/notify/notify_drain.rhai" description = "queue trigger: send notification emails." [[scripts]] name = "posts_scheduled_publish" file = "scripts/posts/posts_scheduled_publish.rhai" description = "cron trigger: publish due scheduled posts." # --- tags --- [[scripts]] name = "tags_list" file = "scripts/tags/tags_list.rhai" description = "Public tag cloud (distinct tags + counts)." # --- media --- [[scripts]] name = "media_upload" file = "scripts/media/media_upload.rhai" description = "Upload an image (author+, base64)." [[scripts]] name = "media_get" file = "scripts/media/media_get.rhai" description = "Fetch an image as base64 (public)." # --- workflow: editorial publish pipeline --- [[scripts]] name = "wf_validate" file = "scripts/workflow/wf_validate.rhai" description = "Workflow step: validate content." [[scripts]] name = "wf_enrich" file = "scripts/workflow/wf_enrich.rhai" description = "Workflow step: auto-excerpt + reading time." [[scripts]] name = "wf_seo" file = "scripts/workflow/wf_seo.rhai" description = "Workflow step: SEO scoring." [[scripts]] name = "wf_publish" file = "scripts/workflow/wf_publish.rhai" description = "Workflow step: publish (chains into notifications)." [[scripts]] name = "wf_finalize" file = "scripts/workflow/wf_finalize.rhai" description = "Workflow step: finalize run." [[scripts]] name = "pipeline_start" file = "scripts/workflow/pipeline_start.rhai" description = "Start the editorial pipeline on a post (author+)." [[scripts]] name = "pipeline_dag" file = "scripts/workflow/pipeline_dag.rhai" description = "Return the pipeline DAG for the visual editor." [[scripts]] name = "pipeline_run" file = "scripts/workflow/pipeline_run.rhai" description = "Return a run's live per-step status." # ------------------------------------------------------------------ routes [[routes]] script = "hello" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/hello" dispatch_mode = "sync" [[routes]] script = "auth_register" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/auth/register" [[routes]] script = "auth_login" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/auth/login" [[routes]] script = "auth_logout" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/auth/logout" [[routes]] script = "auth_me" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/auth/me" [[routes]] script = "auth_bootstrap" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/auth/bootstrap" # --- admin user management --- [[routes]] script = "admin_users_list" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/admin/users" [[routes]] script = "admin_user_roles" method = "PUT" host_kind = "any" path_kind = "param" path = "/cms/admin/users/:id/roles" # --- posts (public) --- [[routes]] script = "posts_list" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/posts" [[routes]] script = "post_get" method = "GET" host_kind = "any" path_kind = "param" path = "/cms/posts/:slug" # --- posts (admin) --- [[routes]] script = "posts_admin_list" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/admin/posts" [[routes]] script = "post_create" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/admin/posts" [[routes]] script = "post_update" method = "PUT" host_kind = "any" path_kind = "param" path = "/cms/admin/posts/:id" [[routes]] script = "post_delete" method = "DELETE" host_kind = "any" path_kind = "param" path = "/cms/admin/posts/:id" # --- pages --- [[routes]] script = "pages" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/pages" [[routes]] script = "pages" method = "GET" host_kind = "any" path_kind = "param" path = "/cms/pages/:slug" [[routes]] script = "pages" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/admin/pages" [[routes]] script = "pages" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/admin/pages" [[routes]] script = "pages" method = "PUT" host_kind = "any" path_kind = "param" path = "/cms/admin/pages/:id" [[routes]] script = "pages" method = "DELETE" host_kind = "any" path_kind = "param" path = "/cms/admin/pages/:id" # --- comments --- [[routes]] script = "comment_create" method = "POST" host_kind = "any" path_kind = "param" path = "/cms/posts/:id/comments" [[routes]] script = "comments_for_post" method = "GET" host_kind = "any" path_kind = "param" path = "/cms/posts/:id/comments" [[routes]] script = "comments_admin" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/admin/comments" [[routes]] script = "comments_admin" method = "PUT" host_kind = "any" path_kind = "param" path = "/cms/admin/comments/:id" [[routes]] script = "comments_admin" method = "DELETE" host_kind = "any" path_kind = "param" path = "/cms/admin/comments/:id" # --- tags --- [[routes]] script = "tags_list" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/tags" # --- media --- [[routes]] script = "media_upload" method = "POST" host_kind = "any" path_kind = "exact" path = "/cms/admin/media" [[routes]] script = "media_get" method = "GET" host_kind = "any" path_kind = "param" path = "/cms/media/:id" # --- workflow --- [[routes]] script = "pipeline_start" method = "POST" host_kind = "any" path_kind = "param" path = "/cms/admin/posts/:id/pipeline" [[routes]] script = "pipeline_dag" method = "GET" host_kind = "any" path_kind = "exact" path = "/cms/admin/workflow" [[routes]] script = "pipeline_run" method = "GET" host_kind = "any" path_kind = "param" path = "/cms/admin/workflow/runs/:id" # ------------------------------------------------------------------ triggers # docs trigger: fire posts_on_publish when a post is created/updated. [[triggers.docs]] script = "posts_on_publish" collection_glob = "posts" ops = ["create", "update"] retry_max_attempts = 3 # docs trigger: publish an approved comment to the SSE feed. [[triggers.docs]] script = "comment_on_approve" collection_glob = "comments" ops = ["update"] retry_max_attempts = 2 # queue trigger: drain post-notifications, send emails. [[triggers.queue]] script = "notify_drain" queue_name = "post-notifications" retry_max_attempts = 5 # cron trigger: publish due scheduled posts every minute (6-field: sec min hr ...). [[triggers.cron]] script = "posts_scheduled_publish" schedule = "0 * * * * *" timezone = "UTC" # ------------------------------------------------------------------ interceptors [[interceptors]] script = "comment_sanitize" service = "docs" ops = ["create"] phase = "before" timeout_ms = 500 # ------------------------------------------------------------------ workflows # Editorial publish pipeline (durable DAG). validate → (enrich ∥ seo) → publish # → finalize. Each step reads post_id from its input mapping; `when` gates the # branch on validation passing. Per-step progress is read back via the platform # `workflow::run_status` SDK (F-038) — no app-side run tracking needed. [[workflows]] name = "editorial_pipeline" [[workflows.steps]] name = "validate" function = "wf_validate" input = { post_id = "{{ input.post_id }}" } [[workflows.steps]] name = "enrich" function = "wf_enrich" depends_on = ["validate"] when = "steps.validate.output.ok == true" input = { post_id = "{{ input.post_id }}" } [[workflows.steps]] name = "seo" function = "wf_seo" depends_on = ["validate"] when = "steps.validate.output.ok == true" input = { post_id = "{{ input.post_id }}" } [[workflows.steps]] name = "publish" function = "wf_publish" depends_on = ["enrich", "seo"] when = "steps.validate.output.ok == true" input = { post_id = "{{ input.post_id }}" } [[workflows.steps]] name = "finalize" function = "wf_finalize" depends_on = ["publish"] input = { post_id = "{{ input.post_id }}" }