@cloudgrid-io/mcp 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/corpus/capability-map.md +14 -1
- package/src/corpus/templates/semantic-search/AGENTS.md +192 -0
- package/src/corpus/templates/semantic-search/README.md +74 -0
- package/src/corpus/templates/semantic-search/cloudgrid.yaml +217 -0
- package/src/corpus/templates/semantic-search/index.md +97 -0
- package/src/corpus/templates/semantic-search/services/backend/requirements.txt +7 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/__init__.py +0 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/answer.py +90 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/auth.py +65 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/config.py +110 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/db.py +89 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/embeddings.py +44 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/indexing.py +222 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/main.py +259 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/search.py +222 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/source.py +259 -0
- package/src/corpus/templates/semantic-search/services/backend/src/main.py +12 -0
- package/src/corpus/templates/semantic-search/services/web/index.html +12 -0
- package/src/corpus/templates/semantic-search/services/web/package-lock.json +1680 -0
- package/src/corpus/templates/semantic-search/services/web/package.json +19 -0
- package/src/corpus/templates/semantic-search/services/web/src/App.jsx +55 -0
- package/src/corpus/templates/semantic-search/services/web/src/api.js +125 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/Dashboard.jsx +61 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/ManagerLogin.jsx +39 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/SearchView.jsx +166 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/Settings.jsx +148 -0
- package/src/corpus/templates/semantic-search/services/web/src/main.jsx +10 -0
- package/src/corpus/templates/semantic-search/services/web/src/styles.css +78 -0
- package/src/corpus/templates/semantic-search/services/web/vite.config.js +10 -0
- package/src/corpus/workflows/semantic-search.md +133 -0
- package/src/tools.js +73 -12
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f6f7f9;
|
|
3
|
+
--card: #ffffff;
|
|
4
|
+
--ink: #16181d;
|
|
5
|
+
--muted: #626875;
|
|
6
|
+
--accent: #2f6df6;
|
|
7
|
+
--accent-soft: #e6eefe;
|
|
8
|
+
--border: #e2e5ea;
|
|
9
|
+
--mark: #ffe9a8;
|
|
10
|
+
}
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
15
|
+
background: var(--bg);
|
|
16
|
+
color: var(--ink);
|
|
17
|
+
line-height: 1.6;
|
|
18
|
+
}
|
|
19
|
+
a { color: var(--accent); }
|
|
20
|
+
.container { max-width: 860px; margin: 0 auto; padding: 24px 16px 80px; }
|
|
21
|
+
header.site { text-align: center; padding: 32px 0 12px; }
|
|
22
|
+
header.site h1 { margin: 0; font-size: 2rem; }
|
|
23
|
+
header.site p { color: var(--muted); margin: 6px 0 0; }
|
|
24
|
+
.card {
|
|
25
|
+
background: var(--card);
|
|
26
|
+
border: 1px solid var(--border);
|
|
27
|
+
border-radius: 12px;
|
|
28
|
+
padding: 18px;
|
|
29
|
+
margin-bottom: 16px;
|
|
30
|
+
}
|
|
31
|
+
.searchbar { display: flex; gap: 8px; }
|
|
32
|
+
.searchbar input[type="text"] {
|
|
33
|
+
flex: 1; padding: 12px 14px; font-size: 1.05rem;
|
|
34
|
+
border: 1px solid var(--border); border-radius: 10px; background: #fff;
|
|
35
|
+
}
|
|
36
|
+
button {
|
|
37
|
+
cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px;
|
|
38
|
+
font-size: 1rem; background: var(--accent); color: #fff;
|
|
39
|
+
}
|
|
40
|
+
button.ghost { background: var(--accent-soft); color: var(--accent); }
|
|
41
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
42
|
+
.modes { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }
|
|
43
|
+
.modes .chip {
|
|
44
|
+
padding: 7px 14px; border-radius: 999px; border: 1px solid var(--border);
|
|
45
|
+
background: #fff; color: var(--muted); font-size: 0.9rem; cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
.modes .chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
48
|
+
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
|
49
|
+
.filters input, .filters select {
|
|
50
|
+
padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff;
|
|
51
|
+
}
|
|
52
|
+
.result { border-bottom: 1px solid var(--border); padding: 14px 0; }
|
|
53
|
+
.result:last-child { border-bottom: none; }
|
|
54
|
+
.result h3 { margin: 0 0 4px; font-size: 1.15rem; }
|
|
55
|
+
.result .meta { color: var(--muted); font-size: 0.85rem; margin-bottom: 6px; }
|
|
56
|
+
.passage { background: #f8fafc; border-radius: 8px; padding: 10px 12px; }
|
|
57
|
+
.passage mark, .passage .hl { background: var(--mark); padding: 0 2px; border-radius: 3px; }
|
|
58
|
+
.note { color: var(--muted); font-size: 0.9rem; }
|
|
59
|
+
.banner {
|
|
60
|
+
background: #fff6df; border: 1px solid #f0d98a; border-radius: 10px;
|
|
61
|
+
padding: 10px 14px; margin-bottom: 14px; font-size: 0.92rem;
|
|
62
|
+
}
|
|
63
|
+
.toggle { display: flex; align-items: center; gap: 8px; }
|
|
64
|
+
.answer-box { background: #eef2f8; border-radius: 10px; padding: 14px; white-space: pre-wrap; }
|
|
65
|
+
.citations { font-size: 0.85rem; color: var(--muted); margin-top: 8px; }
|
|
66
|
+
nav.top { display: flex; justify-content: center; gap: 16px; margin-top: 10px; font-size: 0.9rem; }
|
|
67
|
+
nav.top a { cursor: pointer; }
|
|
68
|
+
label.field { display: block; margin: 10px 0 4px; font-weight: 600; font-size: 0.9rem; }
|
|
69
|
+
input.text-input, textarea.text-input {
|
|
70
|
+
width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff;
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
}
|
|
73
|
+
table.usage { width: 100%; border-collapse: collapse; }
|
|
74
|
+
table.usage td, table.usage th { border-bottom: 1px solid var(--border); padding: 6px 8px; text-align: left; }
|
|
75
|
+
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
|
|
76
|
+
.stat { background: #f8fafc; border-radius: 10px; padding: 12px; text-align: center; }
|
|
77
|
+
.stat .num { font-size: 1.6rem; font-weight: 700; color: var(--accent); }
|
|
78
|
+
.stat .lbl { font-size: 0.8rem; color: var(--muted); }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
|
|
4
|
+
// Backend base URL is baked in at build time. Same-origin by default (the
|
|
5
|
+
// backend is mounted at /backend on the same entity), overridable via
|
|
6
|
+
// VITE_BACKEND_URL for split-origin dev.
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react()],
|
|
9
|
+
build: { outDir: "dist" },
|
|
10
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: semantic-search
|
|
3
|
+
when: "search over my documents / PDFs / notes / knowledge base, semantic search, document search, find across my files, searchable archive. Needs a database → runtime → local edition."
|
|
4
|
+
needs: database
|
|
5
|
+
deploy: runtime
|
|
6
|
+
editions: local
|
|
7
|
+
capabilities_note: "Document search — needs a database (Mongo) for the documents + chunks collections. Two services: a React (Vite) static frontend at / and a FastAPI backend at /backend. Hybrid search = Mongo $text (lexical) + in-app NumPy cosine over chunk embedding arrays (semantic) + metadata filters, blended to the document level. Runtime app, async build, local edition only. Declare needs:{database:true}. NO needs:vector — pgvector runtime role-grant is blocked (#1545), so embeddings live in the Mongo chunks collection and are cosine-ranked in-app. NO active cron — a Python type:cron refresh is blocked (#1585); ship the manager 'Refresh now' endpoint. Health is green with no secrets; startup ensure_indexes() so $text never 500s on an empty catalog. Pluggable source (dropbox|local|url) + embeddings (OpenAI-compatible) each behind one config point, all secrets from env."
|
|
8
|
+
summary: "Build a document-search app on the grid — index a folder of PDFs/DOCX/TXT/MD, then search by keyword + meaning + metadata, with a grounded answer mode and manager admin. React static frontend at / + FastAPI backend at /backend, backed by grid-shared Mongo. Edition-gate first, scaffold, put the code under services/web/ + services/backend/, declare needs:{database:true} (NO vector #1545, NO active cron #1585), set the embeddings + source + manager secrets, deploy async, poll to a live URL, then Refresh now to index."
|
|
9
|
+
recipe: "edition-gate -> auth+grid -> gridctl_init -> fetch template + read AGENTS.md -> put code under services/web + services/backend, set cloudgrid.yaml active fields -> grid secrets set (embeddings + source + MANAGER_PASSWORD_HASH) -> optional grid dev -> grid plug (async, poll) -> Refresh now to index -> return live URL"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Workflow: semantic-search
|
|
13
|
+
|
|
14
|
+
The user wants to **search over their own documents** — a searchable archive of
|
|
15
|
+
PDFs / DOCX / notes / a knowledge base, by keyword *and* by meaning, not just a
|
|
16
|
+
static page. That is a **persistent runtime app**: a React (Vite) static frontend
|
|
17
|
+
at `/` and a FastAPI backend at `/backend`, backed by the grid-shared Mongo. The
|
|
18
|
+
backend indexes a document source into `documents` + `chunks` collections and
|
|
19
|
+
serves **hybrid search** — Mongo `$text` (lexical) + in-app NumPy cosine over the
|
|
20
|
+
chunk embedding arrays (semantic) + metadata filters, blended and aggregated to
|
|
21
|
+
the document level with a best-passage highlight.
|
|
22
|
+
|
|
23
|
+
This template ships **real, runnable code** — fetch it, read `AGENTS.md`, adapt
|
|
24
|
+
the metadata naming to the user's domain, wire secrets, deploy. Be honest that a
|
|
25
|
+
runtime deploy is async and needs the local edition.
|
|
26
|
+
|
|
27
|
+
## 1. Edition check FIRST (hard gate)
|
|
28
|
+
|
|
29
|
+
A document-search app is a built + deployed multi-service container. It requires
|
|
30
|
+
the **local edition** (Claude Desktop / Claude Code) or the CLI.
|
|
31
|
+
|
|
32
|
+
- **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a runtime app —
|
|
33
|
+
hosted is inline-only and can only publish static pages. Say so plainly, offer a
|
|
34
|
+
static explainer page instead, and STOP the runtime path here.
|
|
35
|
+
- **Local edition:** continue.
|
|
36
|
+
|
|
37
|
+
## 2. Auth + grid
|
|
38
|
+
|
|
39
|
+
1. Ensure signed in: `gridctl_login_status`; if not, `gridctl_login`.
|
|
40
|
+
2. A grid is required. Respect the grid picker: if the user has more than one
|
|
41
|
+
grid, ask which to use; do not assume a target.
|
|
42
|
+
|
|
43
|
+
## 3. Scaffold
|
|
44
|
+
|
|
45
|
+
`gridctl_init` an app `<name>` FIRST — it creates the entity +
|
|
46
|
+
`.cloudgrid/link.json` and a starter `cloudgrid.yaml` with empty `services: {}`
|
|
47
|
+
(`plug` needs a linked directory).
|
|
48
|
+
|
|
49
|
+
## 4. Fetch the template and READ AGENTS.md
|
|
50
|
+
|
|
51
|
+
`gridctl_fetch("template", "semantic-search")`. Read **`AGENTS.md`** before
|
|
52
|
+
writing anything — it defines the file tree, the `documents` + `chunks`
|
|
53
|
+
collections + fields, the Mongo injection, the pluggable source adapter, the
|
|
54
|
+
embeddings config point, the secrets, the deploy flow, and the four baked-in
|
|
55
|
+
learnings you must NOT regress (health-without-secrets, startup index, `$text`
|
|
56
|
+
guard, no vector / no cron).
|
|
57
|
+
|
|
58
|
+
## 5. Put the code under services/ and fill cloudgrid.yaml
|
|
59
|
+
|
|
60
|
+
**Every service's code MUST live under `services/<name>/`** — `path:` is the URL
|
|
61
|
+
mount, NOT the filesystem path (`web` → `services/web/`, `backend` →
|
|
62
|
+
`services/backend/`). Set `cloudgrid.yaml` to the template's active fields:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
name: my-search
|
|
66
|
+
services:
|
|
67
|
+
web:
|
|
68
|
+
type: static
|
|
69
|
+
path: /
|
|
70
|
+
build:
|
|
71
|
+
command: npm run build
|
|
72
|
+
output: dist
|
|
73
|
+
node_version: "22"
|
|
74
|
+
backend:
|
|
75
|
+
type: python
|
|
76
|
+
path: /backend
|
|
77
|
+
needs:
|
|
78
|
+
database: true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Declare `needs: { database: true }`** — the deployer provisions Mongo and
|
|
82
|
+
injects `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`), read lazily in
|
|
83
|
+
`app/db.py`. `requires:` is the deprecated v1 alias; never author it and never set
|
|
84
|
+
`needs:` and `requires:` together (the validator rejects the combination).
|
|
85
|
+
|
|
86
|
+
**NO `needs: vector`** — the pgvector runtime role-grant is blocked on platform
|
|
87
|
+
issue **#1545**, so this template stores embeddings in the Mongo `chunks`
|
|
88
|
+
collection (a float array) and cosine-ranks in-app. **NO active cron** — a Python
|
|
89
|
+
`type: cron` refresh is blocked on **#1585**; the cron service stays commented and
|
|
90
|
+
the manager "Refresh now" endpoint does the indexing.
|
|
91
|
+
|
|
92
|
+
## 6. Configure the source + embeddings (one config point each)
|
|
93
|
+
|
|
94
|
+
- **Source** — `SOURCE_TYPE` selects the adapter (`dropbox` default | `local` |
|
|
95
|
+
`url`). Set it plus that adapter's secrets:
|
|
96
|
+
- dropbox: `DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET`, `DROPBOX_REFRESH_TOKEN`,
|
|
97
|
+
optional `DROPBOX_FOLDER_PATH`.
|
|
98
|
+
- local: `LOCAL_SOURCE_PATH` (or add `needs: { disk: true }` → `DISK_PATH`).
|
|
99
|
+
- url: `URL_SOURCE_MANIFEST` (newline/comma-separated URLs).
|
|
100
|
+
- **Embeddings** — `EMBEDDINGS_API_KEY` (OpenAI by default;
|
|
101
|
+
`EMBEDDINGS_BASE_URL` / `EMBEDDINGS_MODEL` to point elsewhere).
|
|
102
|
+
- **Manager** — `MANAGER_PASSWORD_HASH` = sha256 hex of the manager password.
|
|
103
|
+
|
|
104
|
+
Store all of these with `grid secrets set KEY=VALUE` (never commit them; the app
|
|
105
|
+
reads them lazily inside functions, never at import). Non-secret public config →
|
|
106
|
+
`gridctl_env`. Do NOT set `DATABASE_MONGODB_URL` yourself — the grid injects it.
|
|
107
|
+
|
|
108
|
+
The app starts and passes `GET /backend/health` with **none** of these set;
|
|
109
|
+
search/indexing degrade to a clear "not indexed / missing secret" state.
|
|
110
|
+
|
|
111
|
+
## 7. (Optional) Run locally
|
|
112
|
+
|
|
113
|
+
Mention the user can `grid dev` to run locally against the injected dev Mongo
|
|
114
|
+
before deploying. Don't require it.
|
|
115
|
+
|
|
116
|
+
## 8. Deploy (async)
|
|
117
|
+
|
|
118
|
+
Deploy the folder with `gridctl_plug`. A **runtime deploy is ASYNC**: the first
|
|
119
|
+
response is `status: "building"`, NOT a live URL yet.
|
|
120
|
+
- Poll `gridctl_status` (or the returned poll_url) until the entity is live.
|
|
121
|
+
- Surface a liveness signal while it builds — never a bare silent wait.
|
|
122
|
+
- Only once it is live, return the deployed app URL (NOT the build/log link).
|
|
123
|
+
|
|
124
|
+
## 9. Index + iterate
|
|
125
|
+
|
|
126
|
+
Sign in as manager and hit **"Refresh now"** (Settings) to index the source into
|
|
127
|
+
Mongo, then search. Give the user the live app URL — the deliverable. To iterate,
|
|
128
|
+
re-plug the SAME entity so it updates the same URL.
|
|
129
|
+
|
|
130
|
+
Keep it honest: this ships real code you adapt, the build is async and
|
|
131
|
+
local-edition only, Mongo is injected by `needs`, embeddings live in Mongo (no
|
|
132
|
+
pgvector until #1545), and refresh is a manual endpoint until the cron unblocks
|
|
133
|
+
(#1585).
|
package/src/tools.js
CHANGED
|
@@ -867,6 +867,30 @@ function composePlugUrl(data) {
|
|
|
867
867
|
return `https://${grid}.cloudgrid.io/${slug}`;
|
|
868
868
|
}
|
|
869
869
|
|
|
870
|
+
// Parse the top-level `name:` from a cloudgrid.yaml manifest (issue #48). The
|
|
871
|
+
// manifest is the source of truth for the entity name on a create, but the
|
|
872
|
+
// inline-create wire never forwarded it, so an `artifact_files` create landed as
|
|
873
|
+
// an auto `drop-XXXX` slug. Deliberately a tiny top-level-scalar scan (no YAML
|
|
874
|
+
// dep, matching the rest of this module): the FIRST unindented `name:` key wins,
|
|
875
|
+
// nested `services:`/`needs:` `name:` keys (indented) are ignored, and quotes +
|
|
876
|
+
// inline `# comments` are stripped. Returns null when absent/unparseable.
|
|
877
|
+
export function parseManifestName(yaml) {
|
|
878
|
+
if (typeof yaml !== "string" || yaml.length === 0) return null;
|
|
879
|
+
for (const rawLine of yaml.split(/\r?\n/)) {
|
|
880
|
+
// Top-level keys only — a leading space/tab means it is nested under a map.
|
|
881
|
+
if (/^\s/.test(rawLine)) continue;
|
|
882
|
+
const m = /^name:\s*(.+?)\s*$/.exec(rawLine);
|
|
883
|
+
if (!m) continue;
|
|
884
|
+
let val = m[1];
|
|
885
|
+
// Strip an inline comment on an unquoted scalar.
|
|
886
|
+
if (!/^["']/.test(val)) val = val.replace(/\s+#.*$/, "").trim();
|
|
887
|
+
// Strip surrounding quotes.
|
|
888
|
+
val = val.replace(/^(["'])(.*)\1$/, "$2").trim();
|
|
889
|
+
return val.length > 0 ? val : null;
|
|
890
|
+
}
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
870
894
|
// The public URL of a `/plug` response: the server-composed `url` verbatim
|
|
871
895
|
// (canonical, flat-arch-aware — the unified plug spec), falling back to client-side composition
|
|
872
896
|
// ONLY when the server left it empty (its composition is best-effort).
|
|
@@ -1958,6 +1982,24 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1958
1982
|
}
|
|
1959
1983
|
if (useAnonWire && ctx.state.anonCookie) headers["Cookie"] = ctx.state.anonCookie;
|
|
1960
1984
|
|
|
1985
|
+
// ── CREATE manifest injection (issue #48) ───────────────────────────────────
|
|
1986
|
+
// On a folder-walk create, a `cloudgrid.yaml` on disk is walked into the tree
|
|
1987
|
+
// and — because directory reads surface it before the nested `services/…`
|
|
1988
|
+
// files — rides the multipart body as the FIRST `artifact` part, with the
|
|
1989
|
+
// walk's uniform `application/octet-stream` content-type. The runtime build
|
|
1990
|
+
// orchestrator relies on the manifest leading the bundle (it drives the
|
|
1991
|
+
// service graph + the entity name). The inline `artifact_files` create used to
|
|
1992
|
+
// APPEND the `cloudgrid_yaml` manifest LAST and as `text/plain`, so a
|
|
1993
|
+
// multi-service runtime rolled out with no service graph (0 replicas /
|
|
1994
|
+
// rollout_failed) and an auto `drop-XXXX` name. Fold the manifest into the
|
|
1995
|
+
// artifact list as the first entry (deduping any `cloudgrid.yaml` the caller
|
|
1996
|
+
// already inlined) so both create paths emit a byte-equivalent bundle.
|
|
1997
|
+
if (!isEdit && cloudgrid_yaml) {
|
|
1998
|
+
const manifest = { path: "cloudgrid.yaml", buffer: Buffer.from(cloudgrid_yaml, "utf8") };
|
|
1999
|
+
const rest = artifacts.filter((a) => a.path !== "cloudgrid.yaml");
|
|
2000
|
+
artifacts = [manifest, ...rest];
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1961
2003
|
// ── Wire assembly ───────────────────────────────────────────────────────────
|
|
1962
2004
|
const form = new FormData();
|
|
1963
2005
|
for (const a of artifacts) {
|
|
@@ -1976,15 +2018,17 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1976
2018
|
"cloudgrid.yaml",
|
|
1977
2019
|
);
|
|
1978
2020
|
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2021
|
+
}
|
|
2022
|
+
// Honor the manifest name (issue #48): send `name:` parsed from `cloudgrid_yaml`
|
|
2023
|
+
// as an explicit name/slug hint so the created entity uses it instead of an
|
|
2024
|
+
// auto `drop-XXXX` slug. Harmless if the server owns slug generation; on a
|
|
2025
|
+
// re-plug the entity's name is authoritative, so only send on create.
|
|
2026
|
+
if (!isEdit) {
|
|
2027
|
+
const manifestName = parseManifestName(cloudgrid_yaml);
|
|
2028
|
+
if (manifestName) {
|
|
2029
|
+
form.append("name", manifestName);
|
|
2030
|
+
form.append("slug", manifestName);
|
|
2031
|
+
}
|
|
1988
2032
|
}
|
|
1989
2033
|
if (hints?.kind) {
|
|
1990
2034
|
// `kind_hint` is what the create orchestrator reads; `hints_kind` is the
|
|
@@ -2086,10 +2130,27 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
2086
2130
|
...(freshOwnerToken ? { owner_token: freshOwnerToken } : {}),
|
|
2087
2131
|
};
|
|
2088
2132
|
|
|
2133
|
+
// Accurate status (issue #48): a runtime create/edit is an ASYNC build — the
|
|
2134
|
+
// server replies `status: "building"` (+ a poll_url) while the rollout is still
|
|
2135
|
+
// in flight. Do NOT claim "Live"/"Updated in place" for a build that has not
|
|
2136
|
+
// finished; that reported success for apps that then rolled-out-failed. Only
|
|
2137
|
+
// the terminal states get the live wording; anything still building points at
|
|
2138
|
+
// the poll_url and gridctl_status.
|
|
2139
|
+
const isBuilding = data.status === "building" || Boolean(data.poll_url);
|
|
2089
2140
|
const lines = [];
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2141
|
+
if (isBuilding) {
|
|
2142
|
+
lines.push(
|
|
2143
|
+
isEdit
|
|
2144
|
+
? `Building (async): ${url} — the update is deploying, not live yet.`
|
|
2145
|
+
: `Building (async): ${url} — the deploy is in progress, not live yet.`,
|
|
2146
|
+
);
|
|
2147
|
+
lines.push(
|
|
2148
|
+
data.poll_url
|
|
2149
|
+
? `Poll ${data.poll_url} or run gridctl_status until it is ready (trace ${data.trace_id ?? "n/a"}). Do not report it as live until then.`
|
|
2150
|
+
: "Run gridctl_status until it is ready. Do not report it as live until then.",
|
|
2151
|
+
);
|
|
2152
|
+
} else {
|
|
2153
|
+
lines.push(isEdit ? `Updated in place: ${url}` : `Live: ${url}`);
|
|
2093
2154
|
}
|
|
2094
2155
|
if (data.entity_id) {
|
|
2095
2156
|
lines.push(
|