tide-commander 1.139.2 → 1.141.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/CHANGELOG.md +5432 -0
- package/dist/assets/{BossLogsModal-BuknVw0t.js → BossLogsModal-DUh9mA0g.js} +1 -1
- package/dist/assets/{BossSpawnModal-Bk_GVUor.js → BossSpawnModal-CGEysDmo.js} +1 -1
- package/dist/assets/{ControlsModal-YxXtwKJV.js → ControlsModal-DKlL4BtE.js} +1 -1
- package/dist/assets/{DockerLogsModal-CblkdNqt.js → DockerLogsModal-Dmbe01S9.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D_jdlmTI.js → EmbeddedEditor-Bx_zcxVC.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CGXFB4Xr.js → GmailOAuthSetup-CIPQc_WY.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-Btgu3TOp.js → GoogleOAuthSetup-CnOo59V4.js} +1 -1
- package/dist/assets/{IframeModal-yvc5X_Yh.js → IframeModal-ByDSCNMW.js} +1 -1
- package/dist/assets/{IntegrationsPanel-zQ7-X7PF.js → IntegrationsPanel-CaiJ__1J.js} +2 -2
- package/dist/assets/{LogViewerModal-DGzIeeJQ.js → LogViewerModal-C-w4cekL.js} +1 -1
- package/dist/assets/{MonitoringModal-DySuIczM.js → MonitoringModal-DIJd8oHM.js} +1 -1
- package/dist/assets/{PM2LogsModal-Dq5ZLh0W.js → PM2LogsModal-BFelxtQL.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-D_6yMJIx.js → RestoreArchivedAreaModal-BYTl2kIG.js} +1 -1
- package/dist/assets/{Scene2DCanvas-tZKB12LU.js → Scene2DCanvas-CJgfmvZA.js} +1 -1
- package/dist/assets/{SceneManager-BeJLz_RT.js → SceneManager-dbNZbSKp.js} +1 -1
- package/dist/assets/{SkillsPanel-gPP_MHFb.js → SkillsPanel-CwVwPU5t.js} +2 -2
- package/dist/assets/{SlackMultiInstanceSetup-C7Qvm39W.js → SlackMultiInstanceSetup-DgjwCx-F.js} +1 -1
- package/dist/assets/{SpawnModal-D3-Z5mzv.js → SpawnModal-DLdyhgtN.js} +1 -1
- package/dist/assets/{StatisticsModal-DCAF9Dsa.js → StatisticsModal-Dd1yh0YT.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DTbryouu.js → SubordinateAssignmentModal-bGplpTfz.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-i9h5lRli.js → TriggerManagerPanel-BmdbFUJB.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-DY8YA4bD.js → WorkflowEditorPanel-BRGVEv4g.js} +1 -1
- package/dist/assets/{area-logos-CFnhy-W8.js → area-logos-CPBT7k6a.js} +1 -1
- package/dist/assets/{index-Cjtcr4_w.js → index-BIfX5_Xq.js} +6 -6
- package/dist/assets/index-C09uD--W.js +2 -0
- package/dist/assets/{index-DAlpCHwk.js → index-Cd31ErRd.js} +1 -1
- package/dist/assets/{index-B1ZW-nQg.js → index-CjdSuvf1.js} +1 -1
- package/dist/assets/{index-BCTUaC9_.js → index-Clk0mGIC.js} +1 -1
- package/dist/assets/index-DIWScVQf.js +1 -0
- package/dist/assets/{index-ochVZ1qG.js → index-DPPtFIs3.js} +3 -3
- package/dist/assets/{index-CC0xW5So.js → index-DX8uai_5.js} +3 -3
- package/dist/assets/index-YDzuES6L.js +2 -0
- package/dist/assets/main-BCym1dpG.css +1 -0
- package/dist/assets/main-CAE1L3Sb.js +256 -0
- package/dist/assets/{web-KIxT1AKQ.js → web-B0Nj3gOp.js} +1 -1
- package/dist/assets/{web-f3jpYwOI.js → web-B5mCc_qm.js} +1 -1
- package/dist/assets/{web-CQHAjGLu.js → web-CaEzTKao.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/config.json +9 -1
- package/dist/src/packages/server/data/builtin-skills/browser-control.js +7 -5
- package/dist/src/packages/server/data/builtin-skills/create-building.js +7 -2
- package/dist/src/packages/server/data/builtin-skills/http-requests.js +51 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/data/builtin-skills/release-pipeline.js +10 -1
- package/dist/src/packages/server/data/index.js +65 -0
- package/dist/src/packages/server/routes/browser.js +4 -1
- package/dist/src/packages/server/routes/http-requests.js +148 -0
- package/dist/src/packages/server/routes/index.js +3 -1
- package/dist/src/packages/server/routes/system.js +30 -0
- package/dist/src/packages/server/services/building-service.js +5 -1
- package/dist/src/packages/server/services/claude-usage-service.js +101 -3
- package/dist/src/packages/server/services/http-requests-service.js +450 -0
- package/dist/src/packages/server/services/index.js +1 -0
- package/dist/src/packages/server/websocket/handler.js +2 -1
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +75 -0
- package/dist/src/packages/shared/building-types.js +1 -0
- package/dist/src/packages/shared/http-requests-types.js +7 -0
- package/dist/src/packages/shared/types.js +1 -0
- package/package.json +3 -2
- package/dist/assets/index-B5TMKZMf.js +0 -2
- package/dist/assets/index-BL0c4Fee.js +0 -1
- package/dist/assets/index-C9wQ_Bkk.js +0 -2
- package/dist/assets/main-CpuU-ex-.js +0 -243
- package/dist/assets/main-d2YCqass.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cQ as t}from"./main-CAE1L3Sb.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cQ as a}from"./main-CAE1L3Sb.js";import{ImpactStyle as i,NotificationType as r}from"./index-YDzuES6L.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cQ as s}from"./main-CAE1L3Sb.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-CAE1L3Sb.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-BCym1dpG.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -581,7 +581,15 @@
|
|
|
581
581
|
"close": "Close",
|
|
582
582
|
"stuckHint": "Taking longer than expected?",
|
|
583
583
|
"reload": "Reload",
|
|
584
|
-
"recheck": "Recheck"
|
|
584
|
+
"recheck": "Recheck",
|
|
585
|
+
"updated": "Updated to v{{version}}",
|
|
586
|
+
"viewChangelog": "View changelog",
|
|
587
|
+
"changelogTitle": "What's new in v{{version}}",
|
|
588
|
+
"changelogTitleAll": "Changelog",
|
|
589
|
+
"changelogLoading": "Loading changelog…",
|
|
590
|
+
"changelogError": "Couldn't load the changelog.",
|
|
591
|
+
"changelogEmpty": "No release notes for this version.",
|
|
592
|
+
"viewOnGithub": "View on GitHub"
|
|
585
593
|
},
|
|
586
594
|
"shortcuts": {
|
|
587
595
|
"pressKeys": "Press keys...",
|
|
@@ -31,10 +31,12 @@ matter what's focused. \`/console\`, \`/network\` and \`/errors\` are SCOPED to
|
|
|
31
31
|
- **Multiple agents can drive different tabs at the same time.** Each agent must target its
|
|
32
32
|
own tab by \`tab\`/\`tabId\`; never rely on "the active tab".
|
|
33
33
|
- **Do NOT call \`/api/browser/tab/activate\` just to drive.** Clicks/typing/reads work on a
|
|
34
|
-
**background, unfocused tab** (the user may be on another screen/monitor) —
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
**background, unfocused tab** (the user may be on another screen/monitor) — activate ONLY
|
|
35
|
+
if the user explicitly asks, or you need a full-viewport screenshot (element-clip
|
|
36
|
+
screenshots don't need it either). Note: \`/tab/activate\` now makes the tab the active
|
|
37
|
+
tab of its window WITHOUT stealing the user's OS focus (their current app stays in front) —
|
|
38
|
+
enough for \`captureVisibleTab\`. Pass \`"focusWindow":true\` to ALSO raise the window to the
|
|
39
|
+
foreground; that interrupts whatever the user is doing, so use it only when they ask.
|
|
38
40
|
|
|
39
41
|
## Reads (always available)
|
|
40
42
|
- \`POST /api/browser/dom\` \`{selector, all?}\` → outerHTML/text/box/styles + **\`field\`** (live \`value\`/\`checked\`/\`selectedText\`/\`disabled\` for input/textarea/select). Read form state from \`field\`, NOT from the value attribute (React often doesn't mirror it) and NOT via \`evaluate\` (its content-script fallback is CSP-blocked). Bulky \`<svg>\` icon markup is stripped to a compact \`<svg data-icon>\` placeholder by default (token saver) — pass \`"keepSvg":true\` to keep it.
|
|
@@ -54,7 +56,7 @@ matter what's focused. \`/console\`, \`/network\` and \`/errors\` are SCOPED to
|
|
|
54
56
|
- \`POST /api/browser/wait\` \`{selector | text | ms}\` · \`/evaluate\` \`{expression}\` → \`{value}\`
|
|
55
57
|
- \`POST /api/browser/drag\` \`{from, to}\` · \`/dialog\` \`{accept?, promptText?}\` · \`/cdp-raw\` \`{method, params?}\`
|
|
56
58
|
- \`POST /api/browser/batch\` \`{tab|tabId, steps:[{cmd, …}]}\` — runs ANY steps in order (mixed click/type/wait/…), stops at first failure unless the step has \`continueOnError\`. Use for non-form multi-step flows; for pure form-filling prefer \`/fill\`.
|
|
57
|
-
- Tabs: \`POST /api/browser/tab/{open,close,activate}\`
|
|
59
|
+
- Tabs: \`POST /api/browser/tab/{open,close,activate}\` — \`tab/open\` opens in the BACKGROUND by default (drive/read it there without interrupting the user); pass \`"active":true\` only to actually switch the user to the new tab. \`tab/activate\` won't steal OS focus unless you pass \`"focusWindow":true\`.
|
|
58
60
|
|
|
59
61
|
Every call returns \`{ok:true, result}\` or \`{ok:false, error}\`. **If you pipe to \`jq\`, use
|
|
60
62
|
the right paths — wrong paths return \`null\`, which is NOT the API failing** (it's your
|
|
@@ -47,7 +47,7 @@ ${BT}name${BT}, ${BT}type${BT}, and ${BT}position${BT} are always required. Styl
|
|
|
47
47
|
${BT3}typescript
|
|
48
48
|
{
|
|
49
49
|
name: string, // Display name
|
|
50
|
-
type: 'server'|'link'|'database'|'docker'|'monitor'|'folder'|'boss'|'terminal'|'tests',
|
|
50
|
+
type: 'server'|'link'|'database'|'docker'|'monitor'|'folder'|'boss'|'terminal'|'tests'|'http',
|
|
51
51
|
position: { x: number, z: number },
|
|
52
52
|
style?: 'server-rack'|'tower'|'dome'|'pyramid'|'desktop'
|
|
53
53
|
| 'filing-cabinet'|'satellite'|'crystal'|'factory'|'command-center',
|
|
@@ -58,7 +58,7 @@ ${BT3}typescript
|
|
|
58
58
|
docker?: { ... }, // Docker type — see below
|
|
59
59
|
database?: { connections: [...] }, // Database type
|
|
60
60
|
terminal?: { ... }, // Terminal type
|
|
61
|
-
folderPath?: string, // Folder + tests types — required
|
|
61
|
+
folderPath?: string, // Folder + tests + http types — required
|
|
62
62
|
urls?: [{ label, url }], // Link type — required
|
|
63
63
|
commands?: { start, stop, restart, healthCheck, logs }, // Non-PM2 server custom commands
|
|
64
64
|
subordinateBuildingIds?: string[], // Boss type
|
|
@@ -221,6 +221,11 @@ Requires ${BT}ttyd${BT} installed; ${BT}saveSession: true${BT} also needs ${BT}t
|
|
|
221
221
|
- ${BT}{"name":"Docs","type":"link","style":"tower","position":{"x":10,"z":0},"urls":[{"label":"Internal Wiki","url":"https://wiki.example.com"}]}${BT}
|
|
222
222
|
- ${BT}{"name":"Host Metrics","type":"monitor","style":"satellite","position":{"x":12,"z":4}}${BT}
|
|
223
223
|
|
|
224
|
+
### Tests / HTTP requests
|
|
225
|
+
|
|
226
|
+
- Tests browser (scan + run a folder's test suite): ${BT}{"name":"Core Tests","type":"tests","style":"dome","position":{"x":6,"z":3},"folderPath":"/home/user/projects/core"}${BT}
|
|
227
|
+
- HTTP request tests (IntelliJ-style .http file runner; picks up ${BT}http-client.env.json${BT} / ${BT}http-client.private.env.json${BT} environments in the folder): ${BT}{"name":"Core API Requests","type":"http","style":"satellite","position":{"x":7,"z":3},"folderPath":"/home/user/projects/core/http-requests"}${BT}
|
|
228
|
+
|
|
224
229
|
### Boss
|
|
225
230
|
|
|
226
231
|
Subordinates must already exist — get their IDs from ${BT}GET /api/buildings${BT} first.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const httpRequests = {
|
|
2
|
+
slug: 'http-requests',
|
|
3
|
+
name: 'HTTP Request Tests',
|
|
4
|
+
description: 'Browse and fire IntelliJ-style .http request files via the Tide Commander http-requests API; results render as rich cards in the terminal (method, status, timing, body preview).',
|
|
5
|
+
allowedTools: ['Bash(curl:*)', 'Bash(jq:*)'],
|
|
6
|
+
// Not assigned to anyone by default — enable it per agent/class in the Skills panel.
|
|
7
|
+
assignedAgentClasses: [],
|
|
8
|
+
content: `# HTTP Request Tests
|
|
9
|
+
|
|
10
|
+
Run requests from IntelliJ-style \`.http\` / \`.rest\` files through \`/api/http-requests/*\` (never by hand-crafting the HTTP call yourself) so each result is persisted to the building's history and renders as a **rich card** in the user's terminal — method, status pill, timing, size and a body preview instead of raw JSON.
|
|
11
|
+
|
|
12
|
+
Use this when asked to fire/test HTTP requests from a project's \`.http\` files, smoke-test API endpoints defined there, or report on past request runs. The folder usually comes from an \`http\`-type building — find it with \`GET /api/buildings\` (look for \`"type":"http"\`, use its \`folderPath\`).
|
|
13
|
+
|
|
14
|
+
All calls use the API Calling Convention scaffolding (host, \`-H "X-Auth-Token: ..."\`). No exclamation marks in commands.
|
|
15
|
+
|
|
16
|
+
## 1. Scan — list files, requests and environments
|
|
17
|
+
|
|
18
|
+
\`POST /api/http-requests/scan\`
|
|
19
|
+
\`\`\`json
|
|
20
|
+
{"path":"/abs/path/to/http-folder"}
|
|
21
|
+
\`\`\`
|
|
22
|
+
Returns every parsed file with its requests (0-based \`index\`, \`name\`, \`method\`, \`url\`, \`variables\`) plus \`environments\` (from \`http-client.env.json\` / \`http-client.private.env.json\`) and \`envFiles\`. Pipe through \`jq\` to keep it short, e.g. \`jq '{environments, files: [.files[] | {relFile, requests: [.requests[] | {index, name, method}]}]}'\`.
|
|
23
|
+
|
|
24
|
+
## 2. Run one request
|
|
25
|
+
|
|
26
|
+
\`POST /api/http-requests/run\`
|
|
27
|
+
\`\`\`json
|
|
28
|
+
{"agentId":"YOUR_AGENT_ID","path":"/abs/path/to/http-folder","relFile":"payment-order.http","requestIndex":2,"env":"dev"}
|
|
29
|
+
\`\`\`
|
|
30
|
+
- \`agentId\` (IMPORTANT) — your agent ID from the system prompt. With it, a live card appears in the user's terminal under your curl the moment the request fires (spinner → method/status/time/body, with Re-run and copy-curl buttons). Without it, nothing renders.
|
|
31
|
+
- \`requestIndex\` — the request's 0-based \`index\` from the scan.
|
|
32
|
+
- \`env\` (optional but usually required) — environment name for \`{{variable}}\` resolution; omit only when the file has no variables. \`unresolvedVariables\` in the response tells you what was missing.
|
|
33
|
+
|
|
34
|
+
Synchronous: the response IS the result (\`ok\`, \`status\`, \`timeMs\`, \`sizeBytes\`, \`headers\`, \`body\`, resolved \`request\`, \`runId\`). 60s timeout, 2 MB body cap.
|
|
35
|
+
|
|
36
|
+
Silence the response with \`-o /dev/null\` if you don't need to inspect it (the card already shows everything to the user), or print it as-is (plain \`curl -s\`, no jq filter) — printed run JSON also renders as a card. To READ a field yourself, fetch \`GET /runs/RUN_ID\` afterwards and jq that.
|
|
37
|
+
|
|
38
|
+
## 3. Run several / "run all in a file"
|
|
39
|
+
|
|
40
|
+
Run each request as its OWN curl command (one Bash call per request, agentId included) so every run gets its own live card in the terminal. Avoid shell for-loops — a loop is a single Bash line, so only the last run's card would attach to it. Do not parallelize.
|
|
41
|
+
|
|
42
|
+
## 4. History
|
|
43
|
+
|
|
44
|
+
\`GET /api/http-requests/history?folder=/abs/path&limit=20\` — recent executed requests for that folder (newest first). Printing this JSON also renders as a card (compact list of runs). \`GET /api/http-requests/runs/RUN_ID\` returns one stored run with the full request/response detail.
|
|
45
|
+
|
|
46
|
+
## Rules
|
|
47
|
+
- A non-2xx status or \`"ok":false\` (connection refused, timeout) is a **normal result to report**, not an API error — summarize which requests passed/failed and why.
|
|
48
|
+
- Prefer the building's configured folder; for ad-hoc paths anything under the home directory works.
|
|
49
|
+
- Requests hit real endpoints. Fire GETs freely; for POST/PUT/PATCH/DELETE against non-test systems, confirm with the user first unless they explicitly asked.
|
|
50
|
+
- After a batch, finish with a short written summary (counts + failures); the cards complement it, they don't replace it.`,
|
|
51
|
+
};
|
|
@@ -29,6 +29,7 @@ import { workflowBuilder } from './workflow-builder.js';
|
|
|
29
29
|
import { backupRestore } from './backup-restore.js';
|
|
30
30
|
import { browserControl } from './browser-control.js';
|
|
31
31
|
import { runTests } from './run-tests.js';
|
|
32
|
+
import { httpRequests } from './http-requests.js';
|
|
32
33
|
/**
|
|
33
34
|
* All built-in skills that ship with Tide Commander
|
|
34
35
|
*/
|
|
@@ -57,6 +58,7 @@ export const BUILTIN_SKILLS = [
|
|
|
57
58
|
backupRestore,
|
|
58
59
|
browserControl,
|
|
59
60
|
runTests,
|
|
61
|
+
httpRequests,
|
|
60
62
|
];
|
|
61
63
|
/**
|
|
62
64
|
* Get the ID for a built-in skill based on its slug
|
|
@@ -62,6 +62,13 @@ git pull --rebase origin $(git branch --show-current)
|
|
|
62
62
|
\`\`\`
|
|
63
63
|
On conflicts: STOP immediately and report. Do NOT auto-resolve.
|
|
64
64
|
|
|
65
|
+
Verify the changelog packaging contract (fail fast — before doing any build/publish work):
|
|
66
|
+
|
|
67
|
+
\`\`\`bash
|
|
68
|
+
npm pkg get files | grep -q '"CHANGELOG.md"' || echo "MISSING: CHANGELOG.md not in package.json files"
|
|
69
|
+
\`\`\`
|
|
70
|
+
\`CHANGELOG.md\` MUST be listed in package.json \`files\`. It ships in the npm tarball and is served by the server at \`GET /api/system/changelog\`; the in-app changelog (post-update banner + Settings → About) reads this local packaged file instead of the GitHub API, which 403'd on rate limits (60 req/hour). If the check prints \`MISSING\`: STOP and report — do NOT publish, or the in-app changelog breaks.
|
|
71
|
+
|
|
65
72
|
### Phase 2: Quality Gates (3 parallel sub-agents)
|
|
66
73
|
|
|
67
74
|
Spawn all 3 Agent calls in a single response, each using the curl template above with its command, checking output and exitCode, and instructed not to fix any issues:
|
|
@@ -105,7 +112,7 @@ npm version <patch|minor|major> --no-git-tag-version
|
|
|
105
112
|
git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~20")..HEAD
|
|
106
113
|
\`\`\`
|
|
107
114
|
|
|
108
|
-
Add the new version entry at the top of \`CHANGELOG.md\` (below the header), Keep a Changelog format. Only include sections that have entries; write concise, user-facing descriptions.
|
|
115
|
+
Add the new version entry at the top of \`CHANGELOG.md\` (directly below the \`# Changelog\` header), Keep a Changelog format. Only include sections that have entries; write concise, user-facing descriptions. The changelog is a committed, packaged (package.json \`files\`), server-served artifact — it MUST be updated every release so the bundled file stays current.
|
|
109
116
|
|
|
110
117
|
\`\`\`markdown
|
|
111
118
|
## [X.Y.Z] - YYYY-MM-DD
|
|
@@ -123,6 +130,8 @@ Add the new version entry at the top of \`CHANGELOG.md\` (below the header), Kee
|
|
|
123
130
|
- (from \`remove:\` or \`delete:\` commits)
|
|
124
131
|
\`\`\`
|
|
125
132
|
|
|
133
|
+
CRITICAL — keep the per-version header EXACTLY \`## [X.Y.Z] - YYYY-MM-DD\` (Keep a Changelog): one header per release, newest at the top. The in-app changelog modal extracts a single release by matching the line \`## [X.Y.Z]\`; any other header format breaks the per-version view (the full-changelog view still works). Do NOT drop the \`[\` \`]\` brackets, the version number, or change the \` - YYYY-MM-DD\` suffix.
|
|
134
|
+
|
|
126
135
|
### Phase 6: Commit, Tag, Push (run yourself — sequential git ops)
|
|
127
136
|
|
|
128
137
|
\`\`\`bash
|
|
@@ -923,3 +923,68 @@ export function saveTestRun(run) {
|
|
|
923
923
|
log.error(' Failed to save test run:', err);
|
|
924
924
|
}
|
|
925
925
|
}
|
|
926
|
+
// ============================================================================
|
|
927
|
+
// HTTP Run History (persisted to http-runs/ — one file per run + an index)
|
|
928
|
+
// Mirrors the test-run history above for the "http" (.http requests) building.
|
|
929
|
+
// ============================================================================
|
|
930
|
+
const HTTP_RUNS_DIR = path.join(DATA_DIR, 'http-runs');
|
|
931
|
+
const HTTP_RUNS_INDEX_FILE = path.join(HTTP_RUNS_DIR, 'index.json');
|
|
932
|
+
const MAX_HTTP_RUNS = 300;
|
|
933
|
+
function ensureHttpRunsDir() {
|
|
934
|
+
if (!fs.existsSync(HTTP_RUNS_DIR)) {
|
|
935
|
+
fs.mkdirSync(HTTP_RUNS_DIR, { recursive: true });
|
|
936
|
+
log.log(` Created http runs directory: ${HTTP_RUNS_DIR}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function getHttpRunFile(runId) {
|
|
940
|
+
return path.join(HTTP_RUNS_DIR, `${runId}.json`);
|
|
941
|
+
}
|
|
942
|
+
function summarizeHttpRun(run) {
|
|
943
|
+
const { result: _result, ...summary } = run;
|
|
944
|
+
return summary;
|
|
945
|
+
}
|
|
946
|
+
/** List recent http runs (newest first); optionally scoped to one folder. */
|
|
947
|
+
export function loadHttpRunSummaries(limit = 50, folder) {
|
|
948
|
+
ensureHttpRunsDir();
|
|
949
|
+
const data = safeReadJsonSync(HTTP_RUNS_INDEX_FILE, 'Http runs index');
|
|
950
|
+
let runs = data?.runs || [];
|
|
951
|
+
if (folder)
|
|
952
|
+
runs = runs.filter((r) => r.folder === folder);
|
|
953
|
+
return limit > 0 ? runs.slice(0, limit) : runs;
|
|
954
|
+
}
|
|
955
|
+
/** Read a single stored http run (full request/response detail) by id. */
|
|
956
|
+
export function loadHttpRun(runId) {
|
|
957
|
+
return safeReadJsonSync(getHttpRunFile(runId), `Http run ${runId}`);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Persist an executed request: write its full file, prepend a summary to the
|
|
961
|
+
* index, cap at MAX_HTTP_RUNS, prune per-run files that fell out.
|
|
962
|
+
*/
|
|
963
|
+
export function saveHttpRun(run) {
|
|
964
|
+
ensureHttpRunsDir();
|
|
965
|
+
try {
|
|
966
|
+
atomicWriteJsonSync(getHttpRunFile(run.runId), run);
|
|
967
|
+
const existing = loadHttpRunSummaries(0).filter((r) => r.runId !== run.runId);
|
|
968
|
+
const runs = [summarizeHttpRun(run), ...existing];
|
|
969
|
+
const kept = runs.slice(0, MAX_HTTP_RUNS);
|
|
970
|
+
const dropped = runs.slice(MAX_HTTP_RUNS);
|
|
971
|
+
atomicWriteJsonSync(HTTP_RUNS_INDEX_FILE, {
|
|
972
|
+
runs: kept,
|
|
973
|
+
savedAt: Date.now(),
|
|
974
|
+
version: '1.0.0',
|
|
975
|
+
});
|
|
976
|
+
for (const d of dropped) {
|
|
977
|
+
try {
|
|
978
|
+
const f = getHttpRunFile(d.runId);
|
|
979
|
+
if (fs.existsSync(f))
|
|
980
|
+
fs.unlinkSync(f);
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
// best-effort prune
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
log.error(' Failed to save http run:', err);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
@@ -139,7 +139,10 @@ router.post('/cdp-raw', (req, res) => relay(res, 'cdp_raw', { ...target(req), me
|
|
|
139
139
|
// ── tabs (open / close / activate) ──
|
|
140
140
|
router.post('/tab/open', (req, res) => relay(res, 'tab_open', { url: req.body?.url, active: req.body?.active }));
|
|
141
141
|
router.post('/tab/close', (req, res) => relay(res, 'tab_close', target(req)));
|
|
142
|
-
|
|
142
|
+
// Activating a tab makes it the ACTIVE tab of its window (so full-viewport screenshots
|
|
143
|
+
// work) WITHOUT stealing the user's OS focus. Pass focusWindow:true to also raise the
|
|
144
|
+
// window to the foreground (interrupts whatever the user is doing) — opt-in only.
|
|
145
|
+
router.post('/tab/activate', (req, res) => relay(res, 'tab_activate', { ...target(req), focusWindow: req.body?.focusWindow }));
|
|
143
146
|
// ── fallback: drive a SEPARATE Chrome via puppeteer-core over the debug port ──
|
|
144
147
|
// For a throwaway --user-data-dir profile or Chrome for Testing (not your live
|
|
145
148
|
// session). Requires Chrome launched with --remote-debugging-port. See cdp-service.ts.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Request Tests Routes (IntelliJ-style .http files)
|
|
3
|
+
*
|
|
4
|
+
* REST API for the "http" building: scan a folder for .http files and fire
|
|
5
|
+
* individual requests. Runs are synchronous — a single HTTP request resolves
|
|
6
|
+
* quickly (60s server-side timeout), so no runId/WebSocket plumbing is needed.
|
|
7
|
+
*/
|
|
8
|
+
import { Router } from 'express';
|
|
9
|
+
import { httpRequestsService } from '../services/index.js';
|
|
10
|
+
import { createLogger, generateId } from '../utils/index.js';
|
|
11
|
+
import { saveHttpRun, loadHttpRun, loadHttpRunSummaries } from '../data/index.js';
|
|
12
|
+
const log = createLogger('HttpRequestsRoute');
|
|
13
|
+
const router = Router();
|
|
14
|
+
// Store for broadcasting via WebSocket (wired from the websocket handler).
|
|
15
|
+
let broadcastFn = null;
|
|
16
|
+
export function setBroadcast(fn) {
|
|
17
|
+
broadcastFn = fn;
|
|
18
|
+
}
|
|
19
|
+
function broadcast(message) {
|
|
20
|
+
broadcastFn?.(message);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* POST /api/http-requests/scan - Parse every .http/.rest file under a folder.
|
|
24
|
+
* Body: { path: string }
|
|
25
|
+
*/
|
|
26
|
+
router.post('/scan', (req, res) => {
|
|
27
|
+
const { path: targetPath } = req.body;
|
|
28
|
+
if (!targetPath) {
|
|
29
|
+
res.status(400).json({ error: 'Missing required field: path' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!httpRequestsService.isSafeFolder(targetPath)) {
|
|
33
|
+
res.status(400).json({ error: 'Refusing to scan folders outside your home directory.' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
res.json(httpRequestsService.scanFolder(targetPath));
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* POST /api/http-requests/run - Execute one request from a .http file.
|
|
40
|
+
* Body: { path, relFile, requestIndex, env? } — responds with HttpRunResult
|
|
41
|
+
* once the request completes (or fails / times out).
|
|
42
|
+
*/
|
|
43
|
+
router.post('/run', async (req, res) => {
|
|
44
|
+
const { path: targetPath, relFile, requestIndex, env, agentId } = req.body;
|
|
45
|
+
if (!targetPath || !relFile || typeof requestIndex !== 'number') {
|
|
46
|
+
res.status(400).json({ error: 'Missing required fields: path, relFile, requestIndex' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!httpRequestsService.isSafeFolder(targetPath)) {
|
|
50
|
+
res.status(400).json({ error: 'Refusing to run requests outside your home directory.' });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const startAgentId = typeof agentId === 'string' && agentId ? agentId : undefined;
|
|
54
|
+
const runId = generateId();
|
|
55
|
+
try {
|
|
56
|
+
// Announce the run when an agent fired it, so its terminal shows a live
|
|
57
|
+
// inline card immediately (spinner → result), like test runs.
|
|
58
|
+
if (startAgentId) {
|
|
59
|
+
const item = httpRequestsService.peekRequest(targetPath, relFile, requestIndex);
|
|
60
|
+
broadcast({
|
|
61
|
+
type: 'http_run_started',
|
|
62
|
+
payload: {
|
|
63
|
+
runId,
|
|
64
|
+
agentId: startAgentId,
|
|
65
|
+
folder: targetPath,
|
|
66
|
+
relFile,
|
|
67
|
+
requestIndex,
|
|
68
|
+
requestName: item?.name || `${relFile}#${requestIndex}`,
|
|
69
|
+
method: item?.method || 'GET',
|
|
70
|
+
url: item?.url || '',
|
|
71
|
+
env,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const result = await httpRequestsService.executeRequest(targetPath, relFile, requestIndex, env);
|
|
76
|
+
log.log(`${result.request.method} ${result.request.url} → ` +
|
|
77
|
+
(result.ok ? `${result.status} in ${result.timeMs}ms` : `error: ${result.error}`));
|
|
78
|
+
// Persist to history (best-effort — the response never waits on failures
|
|
79
|
+
// beyond the synchronous write).
|
|
80
|
+
const stored = {
|
|
81
|
+
runId,
|
|
82
|
+
folder: targetPath,
|
|
83
|
+
relFile,
|
|
84
|
+
requestIndex,
|
|
85
|
+
requestName: result.requestName || `${result.request.method} ${result.request.url}`,
|
|
86
|
+
method: result.request.method,
|
|
87
|
+
url: result.request.url,
|
|
88
|
+
env,
|
|
89
|
+
ok: result.ok,
|
|
90
|
+
status: result.status,
|
|
91
|
+
timeMs: result.timeMs,
|
|
92
|
+
sizeBytes: result.sizeBytes,
|
|
93
|
+
error: result.error,
|
|
94
|
+
finishedAt: Date.now(),
|
|
95
|
+
result,
|
|
96
|
+
};
|
|
97
|
+
saveHttpRun(stored);
|
|
98
|
+
if (startAgentId) {
|
|
99
|
+
broadcast({
|
|
100
|
+
type: 'http_run_completed',
|
|
101
|
+
payload: { runId, agentId: startAgentId, result },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
res.json({ ...result, runId });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// Close the inline card on hard failures too (parse error, missing file).
|
|
108
|
+
if (startAgentId) {
|
|
109
|
+
broadcast({
|
|
110
|
+
type: 'http_run_completed',
|
|
111
|
+
payload: {
|
|
112
|
+
runId,
|
|
113
|
+
agentId: startAgentId,
|
|
114
|
+
result: {
|
|
115
|
+
ok: false,
|
|
116
|
+
request: { method: 'GET', url: '', headers: [] },
|
|
117
|
+
timeMs: 0,
|
|
118
|
+
error: err?.message || 'Failed to run request',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
res.status(400).json({ error: err?.message || 'Failed to run request' });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
/**
|
|
127
|
+
* GET /api/http-requests/history - Recent executed requests (newest first).
|
|
128
|
+
* Query: ?folder=<abs path> scopes to one building's folder; ?limit=N (default
|
|
129
|
+
* 50, max 200). Summaries only — fetch /runs/:runId for the full detail.
|
|
130
|
+
*/
|
|
131
|
+
router.get('/history', (req, res) => {
|
|
132
|
+
const raw = parseInt(req.query.limit, 10);
|
|
133
|
+
const limit = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 200) : 50;
|
|
134
|
+
const folder = typeof req.query.folder === 'string' && req.query.folder ? req.query.folder : undefined;
|
|
135
|
+
res.json({ runs: loadHttpRunSummaries(limit, folder) });
|
|
136
|
+
});
|
|
137
|
+
/**
|
|
138
|
+
* GET /api/http-requests/runs/:runId - Full stored run (request + response).
|
|
139
|
+
*/
|
|
140
|
+
router.get('/runs/:runId', (req, res) => {
|
|
141
|
+
const run = loadHttpRun(req.params.runId);
|
|
142
|
+
if (!run) {
|
|
143
|
+
res.status(404).json({ error: 'Run not found' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
res.json(run);
|
|
147
|
+
});
|
|
148
|
+
export default router;
|
|
@@ -11,6 +11,7 @@ import agentPromptRouter from './agent-prompt.js';
|
|
|
11
11
|
import notificationsRouter, { setBroadcast as setNotificationBroadcast } from './notifications.js';
|
|
12
12
|
import execRouter, { setBroadcast as setExecBroadcast } from './exec.js';
|
|
13
13
|
import testsRouter, { setBroadcast as setTestsBroadcast } from './tests.js';
|
|
14
|
+
import httpRequestsRouter, { setBroadcast as setHttpRequestsBroadcast } from './http-requests.js';
|
|
14
15
|
import focusAgentRouter, { setBroadcast as setFocusAgentBroadcast } from './focus-agent.js';
|
|
15
16
|
import customModelsRouter from './custom-models.js';
|
|
16
17
|
import configRouter from './config.js';
|
|
@@ -44,6 +45,7 @@ router.use('/folders', foldersRouter);
|
|
|
44
45
|
router.use('/notify', notificationsRouter);
|
|
45
46
|
router.use('/exec', execRouter);
|
|
46
47
|
router.use('/tests', testsRouter);
|
|
48
|
+
router.use('/http-requests', httpRequestsRouter);
|
|
47
49
|
router.use('/focus-agent', focusAgentRouter);
|
|
48
50
|
router.use('/custom-models', customModelsRouter);
|
|
49
51
|
router.use('/tts', ttsRouter);
|
|
@@ -83,5 +85,5 @@ router.use('/', permissionsRouter);
|
|
|
83
85
|
// Agent prompt routes — root-level: /api/agent-prompt and /api/agent-prompt/:id/respond
|
|
84
86
|
router.use('/', agentPromptRouter);
|
|
85
87
|
// Export the broadcast setters for WebSocket handler to use
|
|
86
|
-
export { setNotificationBroadcast, setExecBroadcast, setTestsBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast, setSkillsBroadcast };
|
|
88
|
+
export { setNotificationBroadcast, setExecBroadcast, setTestsBroadcast, setHttpRequestsBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast, setSkillsBroadcast };
|
|
87
89
|
export default router;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Endpoints for inspecting / updating the running Tide Commander install.
|
|
4
4
|
*/
|
|
5
5
|
import { Router } from 'express';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
6
8
|
import { createLogger } from '../utils/logger.js';
|
|
7
9
|
import { fetchLatestNpmVersion, getVersionRelation } from '../../shared/version.js';
|
|
8
10
|
import { getInstallInfo, isAutoUpdateSupported, runNpmGlobalUpdate, schedulePostUpdateRestart, } from '../services/self-update-service.js';
|
|
@@ -45,6 +47,34 @@ router.get('/install-info', async (_req, res) => {
|
|
|
45
47
|
res.status(500).json({ error: message });
|
|
46
48
|
}
|
|
47
49
|
});
|
|
50
|
+
/**
|
|
51
|
+
* GET /api/system/changelog
|
|
52
|
+
*
|
|
53
|
+
* Serves the bundled CHANGELOG.md (Keep a Changelog markdown) so the UI can
|
|
54
|
+
* render release notes locally — no GitHub API, no rate limits, works offline
|
|
55
|
+
* and always matches the installed version.
|
|
56
|
+
*/
|
|
57
|
+
router.get('/changelog', (_req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const installRoot = getInstallInfo().installRoot;
|
|
60
|
+
if (!installRoot) {
|
|
61
|
+
res.status(404).json({ error: 'Could not locate install root.' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const changelogPath = path.join(installRoot, 'CHANGELOG.md');
|
|
65
|
+
if (!fs.existsSync(changelogPath)) {
|
|
66
|
+
res.status(404).json({ error: 'CHANGELOG.md not found.' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const content = fs.readFileSync(changelogPath, 'utf8');
|
|
70
|
+
res.type('text/markdown').send(content);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const message = err.message;
|
|
74
|
+
log.error(`Failed to read changelog: ${message}`);
|
|
75
|
+
res.status(500).json({ error: message });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
48
78
|
/**
|
|
49
79
|
* POST /api/system/self-update
|
|
50
80
|
*
|
|
@@ -1088,7 +1088,7 @@ export async function handleBuildingSync(newBuildings, _broadcast) {
|
|
|
1088
1088
|
// Validation & CRUD (Phase 1 - REST API support)
|
|
1089
1089
|
// ============================================================================
|
|
1090
1090
|
const VALID_TYPES = new Set([
|
|
1091
|
-
'server', 'link', 'database', 'docker', 'monitor', 'folder', 'boss', 'terminal', 'tests',
|
|
1091
|
+
'server', 'link', 'database', 'docker', 'monitor', 'folder', 'boss', 'terminal', 'tests', 'http',
|
|
1092
1092
|
]);
|
|
1093
1093
|
const VALID_STYLES = new Set([
|
|
1094
1094
|
'server-rack', 'tower', 'dome', 'pyramid', 'desktop',
|
|
@@ -1113,6 +1113,7 @@ const DEFAULT_STYLE_BY_TYPE = {
|
|
|
1113
1113
|
boss: 'command-center',
|
|
1114
1114
|
terminal: 'desktop',
|
|
1115
1115
|
tests: 'dome',
|
|
1116
|
+
http: 'satellite',
|
|
1116
1117
|
};
|
|
1117
1118
|
function generateBuildingId(name) {
|
|
1118
1119
|
const slug = name
|
|
@@ -1203,6 +1204,9 @@ export function validateBuilding(input) {
|
|
|
1203
1204
|
if (input.type === 'tests' && !input.folderPath) {
|
|
1204
1205
|
errors.push('folderPath is required for tests buildings (the folder whose tests it browses)');
|
|
1205
1206
|
}
|
|
1207
|
+
if (input.type === 'http' && !input.folderPath) {
|
|
1208
|
+
errors.push('folderPath is required for http buildings (the folder containing the .http request files)');
|
|
1209
|
+
}
|
|
1206
1210
|
if (input.type === 'link' && (!input.urls || input.urls.length === 0)) {
|
|
1207
1211
|
errors.push('urls is required (non-empty array) for link buildings');
|
|
1208
1212
|
}
|