machinaos 0.0.87 → 0.0.89
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/.env.template +39 -0
- package/.machina/workflows/AI Assistant_example_workflow-1779017037684-e2e5da7a.json +1 -1
- package/.machina/workflows/AI Employee_example_workflow-1779102911870-cbc76c82.json +6 -6
- package/README.md +6 -6
- package/bin/cli.js +6 -1
- package/cli/cli.py +87 -0
- package/cli/commands/build.py +33 -3
- package/cli/commands/clean.py +13 -4
- package/cli/commands/deploy/__init__.py +13 -0
- package/cli/commands/deploy/_secrets.py +65 -0
- package/cli/commands/deploy/_state.py +56 -0
- package/cli/commands/deploy/_terraform.py +68 -0
- package/cli/commands/deploy/destroy.py +34 -0
- package/cli/commands/deploy/providers/__init__.py +34 -0
- package/cli/commands/deploy/providers/_base.py +46 -0
- package/cli/commands/deploy/providers/aws.py +45 -0
- package/cli/commands/deploy/providers/gcp.py +89 -0
- package/cli/commands/deploy/status.py +48 -0
- package/cli/commands/deploy/up.py +146 -0
- package/cli/commands/serve.py +98 -0
- package/cli/terraform/gcp/main.tf +116 -0
- package/cli/terraform/gcp/outputs.tf +9 -0
- package/cli/terraform/gcp/startup.sh.tftpl +65 -0
- package/cli/terraform/gcp/variables.tf +54 -0
- package/client/dist/assets/{ActionBar-Bg1hW3t6.js → ActionBar-DqL2u6-G.js} +1 -1
- package/client/dist/assets/{ApiKeyInput-DqeO8LWg.js → ApiKeyInput-DmIFsu5L.js} +1 -1
- package/client/dist/assets/ApiKeyPanel-BJGWpSo3.js +1 -0
- package/client/dist/assets/ApiUsageSection-B9FBHYF_.js +1 -0
- package/client/dist/assets/{EmailPanel-B-Wsn1Cy.js → EmailPanel-DDsRt9xV.js} +1 -1
- package/client/dist/assets/{OAuthPanel-C2tVcDmU.js → OAuthPanel-DQiCai8Z.js} +1 -1
- package/client/dist/assets/{QrPairingPanel-ChNvq1Zt.js → QrPairingPanel-DmVb3FlK.js} +1 -1
- package/client/dist/assets/{RateLimitSection-ByFuEORg.js → RateLimitSection-CQstfsLJ.js} +1 -1
- package/client/dist/assets/{StatusCard-Pzhnd_Bf.js → StatusCard-B4JY4t4C.js} +1 -1
- package/client/dist/assets/index-CW3r0-Ob.js +165 -0
- package/client/dist/assets/index-DrhPs19g.css +1 -0
- package/client/dist/assets/{vendor-misc-C4VxKHs5.js → vendor-misc-DBQ988Ev.js} +1 -1
- package/client/dist/assets/{vendor-radix-Dnos29jG.js → vendor-radix-BHmt2CkM.js} +1 -1
- package/client/dist/index.html +4 -4
- package/client/package.json +3 -3
- package/client/src/Dashboard.tsx +3 -4
- package/client/src/components/ConditionalEdge.tsx +2 -2
- package/client/src/components/ParameterRenderer.tsx +209 -809
- package/client/src/components/PricingConfigModal.tsx +27 -66
- package/client/src/components/SquareNode.tsx +1 -1
- package/client/src/components/TriggerNode.tsx +13 -8
- package/client/src/components/credentials/CredentialsModal.tsx +1 -1
- package/client/src/components/credentials/catalogueAdapter.ts +1 -0
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +9 -1
- package/client/src/components/credentials/sections/ApiUsageSection.tsx +4 -4
- package/client/src/components/credentials/sections/LlmUsageSection.tsx +7 -7
- package/client/src/components/credentials/types.ts +2 -0
- package/client/src/components/onboarding/steps/ApiKeyStep.tsx +1 -1
- package/client/src/components/onboarding/steps/CanvasStep.tsx +12 -13
- package/client/src/components/parameterPanel/MiddleSection.tsx +4 -5
- package/client/src/components/ui/ComponentPalette.tsx +2 -4
- package/client/src/components/ui/ErrorBoundary.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +33 -5
- package/client/src/components/ui/ThemeSwitcher.tsx +1 -1
- package/client/src/components/ui/TopToolbar.tsx +6 -6
- package/client/src/components/ui/action-button.tsx +18 -13
- package/client/src/components/ui/settingsPanel/schema.ts +7 -1
- package/client/src/contexts/WebSocketContext.tsx +4 -4
- package/client/src/hooks/useCatalogueQuery.ts +2 -0
- package/client/src/index.css +149 -297
- package/client/src/lib/workflowOps.ts +13 -1
- package/client/src/main.tsx +4 -0
- package/client/src/styles/theme.ts +29 -29
- package/client/src/themes/animations.css +193 -0
- package/client/src/themes/atomic.css +78 -68
- package/client/src/themes/base.css +139 -78
- package/client/src/themes/cyber.css +77 -90
- package/client/src/themes/dark.css +72 -25
- package/client/src/themes/edo.css +71 -65
- package/client/src/themes/greek.css +74 -68
- package/client/src/themes/light.css +98 -0
- package/client/src/themes/plague.css +79 -73
- package/client/src/themes/renaissance.css +81 -92
- package/client/src/themes/rot.css +71 -65
- package/client/src/themes/steampunk.css +77 -71
- package/client/src/themes/surveillance.css +82 -95
- package/client/src/themes/wasteland.css +80 -74
- package/client/tailwind.config.js +53 -50
- package/package.json +11 -4
- package/scripts/install.js +9 -1
- package/server/config/credential_providers.json +2 -1
- package/server/config/llm_defaults.json +57 -45
- package/server/config/model_registry.json +687 -1297
- package/server/config/node_allowlist.json +2 -1
- package/server/core/config.py +65 -3
- package/server/core/container.py +36 -1
- package/server/core/database.py +44 -48
- package/server/core/paths.py +88 -103
- package/server/main.py +80 -0
- package/server/middleware/auth.py +12 -2
- package/server/models/database.py +5 -1
- package/server/nodejs/package.json +1 -1
- package/server/nodes/README.md +25 -2
- package/server/nodes/agent/claude_code_agent/_oauth.py +54 -26
- package/server/nodes/browser/_install.py +14 -11
- package/server/nodes/browser/_service.py +100 -0
- package/server/nodes/browser/browser/__init__.py +4 -2
- package/server/nodes/code/monty_executor/__init__.py +267 -0
- package/server/nodes/code/monty_executor/icon.svg +1 -0
- package/server/nodes/code/monty_executor/meta.json +3 -0
- package/server/nodes/filesystem/_backend.py +16 -1
- package/server/nodes/filesystem/file_modify/__init__.py +4 -1
- package/server/nodes/filesystem/file_read/__init__.py +20 -3
- package/server/nodes/filesystem/fs_search/__init__.py +4 -1
- package/server/nodes/search/perplexity_search/__init__.py +10 -9
- package/server/nodes/stripe/_handlers.py +57 -14
- package/server/nodes/stripe/_install.py +15 -8
- package/server/nodes/tool/agent_builder/__init__.py +574 -46
- package/server/nodes/tool/write_todos/__init__.py +29 -2
- package/server/nodes/utility/team_monitor/__init__.py +3 -1
- package/server/nodes/visuals.json +3 -0
- package/server/nodes/whatsapp/_install.py +71 -0
- package/server/nodes/whatsapp/_runtime.py +34 -5
- package/server/pyproject.toml +19 -1
- package/server/requirements.txt +591 -108
- package/server/services/ai.py +320 -159
- package/server/services/events/cli.py +17 -0
- package/server/services/events/daemon.py +10 -3
- package/server/services/handlers/todo.py +4 -1
- package/server/services/handlers/tools.py +15 -2
- package/server/services/llm/__init__.py +22 -1
- package/server/services/llm/protocol.py +5 -1
- package/server/services/llm/providers/__init__.py +26 -1
- package/server/services/llm/providers/_compat.py +109 -0
- package/server/services/llm/providers/anthropic.py +21 -0
- package/server/services/llm/providers/gemini.py +72 -9
- package/server/services/llm/providers/openai.py +24 -0
- package/server/services/llm/providers/openrouter.py +19 -0
- package/server/services/llm/registry.py +111 -0
- package/server/services/llm/unifier.py +132 -0
- package/server/services/llm/vertex.py +28 -0
- package/server/services/model_registry.py +37 -8
- package/server/services/node_executor.py +1 -0
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +83 -9
- package/server/services/plugin/tool.py +25 -6
- package/server/services/process_service.py +19 -5
- package/server/services/temporal/_install.py +36 -10
- package/server/services/temporal/_runtime.py +22 -0
- package/server/services/temporal/agent_activities.py +179 -3
- package/server/services/temporal/agent_workflow.py +170 -26
- package/server/services/temporal/client.py +82 -11
- package/server/services/temporal/executor.py +1 -0
- package/server/services/temporal/worker.py +38 -8
- package/server/services/temporal/workflow.py +8 -0
- package/server/services/user_auth.py +3 -2
- package/server/services/workflow.py +10 -1
- package/server/services/workflow_ops.py +4 -0
- package/server/skills/assistant/agent-builder-skill/SKILL.md +107 -80
- package/server/skills/coding_agent/file-modify-skill/SKILL.md +2 -0
- package/server/skills/coding_agent/file-read-skill/SKILL.md +2 -0
- package/server/skills/coding_agent/fs-search-skill/SKILL.md +2 -0
- package/server/skills/coding_agent/monty-skill/SKILL.md +116 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +2 -2
- package/server/skills/web_agent/browser-skill/SKILL.md +8 -0
- package/server/tests/conftest.py +3 -5
- package/server/tests/llm/test_live_providers.py +289 -0
- package/server/tests/llm/test_max_tokens_resolution.py +136 -0
- package/server/tests/llm/test_plugin_shape.py +168 -0
- package/server/tests/llm/test_provider_self_registration.py +82 -0
- package/server/tests/llm/test_unifier_incompatible_models_filter.py +106 -0
- package/server/tests/llm/test_unifier_typed_errors.py +145 -0
- package/server/tests/llm/test_vertex_key.py +254 -0
- package/server/tests/llm/test_wiring.py +65 -56
- package/server/tests/nodes/_compat.py +2 -1
- package/server/tests/nodes/test_agent_builder.py +587 -3
- package/server/tests/nodes/test_ai_tools.py +37 -0
- package/server/tests/nodes/test_browser_service.py +110 -0
- package/server/tests/nodes/test_code_fs_process.py +61 -2
- package/server/tests/nodes/test_monty_executor.py +289 -0
- package/server/tests/nodes/test_stripe_plugin.py +3 -2
- package/server/tests/nodes/test_web_automation.py +21 -0
- package/server/tests/services/test_agent_loop_rebind.py +355 -0
- package/server/tests/temporal/test_agent_workflow.py +338 -3
- package/server/tests/temporal/test_client_readiness.py +114 -0
- package/server/tests/temporal/test_terminate_sweep.py +84 -0
- package/server/tests/temporal/test_worker_restart.py +78 -0
- package/server/tests/test_output_contract.py +203 -0
- package/server/tests/test_plugin_contract.py +124 -0
- package/server/uv.lock +346 -350
- package/client/dist/assets/ApiKeyPanel-BaGAyu8Z.js +0 -1
- package/client/dist/assets/ApiUsageSection-C_pELnWe.js +0 -1
- package/client/dist/assets/index-D-LxTbwD.js +0 -165
- package/client/dist/assets/index-DxmbVskS.css +0 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""aws-cli adapter -- placeholder (the AWS Terraform module is a follow-on).
|
|
2
|
+
|
|
3
|
+
Implements the ``ProviderCli`` shape so the resolver/wiring is uniform, but
|
|
4
|
+
aborts with a clear message until the ``cli/terraform/aws/`` module lands. The
|
|
5
|
+
real adapter will mirror gcp.py: ``aws sts get-caller-identity`` for auth, the
|
|
6
|
+
default credential chain for Terraform, region from ``aws configure get region``
|
|
7
|
+
/ ``$AWS_REGION``, and a security-group + EC2 instance module.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from cli._common import error_block
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AwsCli:
|
|
18
|
+
name = "aws"
|
|
19
|
+
|
|
20
|
+
def _not_yet(self) -> None:
|
|
21
|
+
error_block(
|
|
22
|
+
"The aws provider is not implemented yet.",
|
|
23
|
+
["Use --provider gcp for now; the AWS module is a planned follow-on."],
|
|
24
|
+
)
|
|
25
|
+
raise typer.Exit(code=1)
|
|
26
|
+
|
|
27
|
+
def check(self) -> None:
|
|
28
|
+
self._not_yet()
|
|
29
|
+
|
|
30
|
+
def authed_account(self) -> str | None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def resolve_context(self, *, region, zone, project) -> dict:
|
|
34
|
+
self._not_yet()
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
def ensure_terraform_auth(self) -> None:
|
|
38
|
+
self._not_yet()
|
|
39
|
+
|
|
40
|
+
def enable_apis(self, ctx: dict) -> None:
|
|
41
|
+
self._not_yet()
|
|
42
|
+
|
|
43
|
+
def tfvars_extra(self, ctx: dict) -> dict:
|
|
44
|
+
self._not_yet()
|
|
45
|
+
return {}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""gcloud adapter -- Stage 1 of ``machina deploy --provider gcp``.
|
|
2
|
+
|
|
3
|
+
Uses the operator's installed + authenticated ``gcloud`` CLI for auth/context/
|
|
4
|
+
API-enablement; Terraform's ``google`` provider then reuses the same
|
|
5
|
+
Application Default Credentials to create the resources.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from cli._common import error_block
|
|
13
|
+
from cli.colors import console
|
|
14
|
+
from cli.run import capture, run
|
|
15
|
+
|
|
16
|
+
_REQUIRED_APIS = [
|
|
17
|
+
"compute.googleapis.com",
|
|
18
|
+
"storage.googleapis.com",
|
|
19
|
+
"iam.googleapis.com",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _clean(value: str | None) -> str | None:
|
|
24
|
+
"""Normalise gcloud config reads (``(unset)`` / empty -> None)."""
|
|
25
|
+
if not value:
|
|
26
|
+
return None
|
|
27
|
+
v = value.strip()
|
|
28
|
+
return None if v in ("", "(unset)") else v
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GcpCli:
|
|
32
|
+
name = "gcp"
|
|
33
|
+
|
|
34
|
+
def check(self) -> None:
|
|
35
|
+
if capture(["gcloud", "version"]) is None:
|
|
36
|
+
error_block(
|
|
37
|
+
"The gcloud CLI was not found on PATH.",
|
|
38
|
+
["Install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install"],
|
|
39
|
+
)
|
|
40
|
+
raise typer.Exit(code=1)
|
|
41
|
+
|
|
42
|
+
def authed_account(self) -> str | None:
|
|
43
|
+
return _clean(
|
|
44
|
+
capture(
|
|
45
|
+
["gcloud", "auth", "list", "--filter=status:ACTIVE", "--format=value(account)"]
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def resolve_context(self, *, region, zone, project) -> dict:
|
|
50
|
+
account = self.authed_account()
|
|
51
|
+
if not account:
|
|
52
|
+
error_block(
|
|
53
|
+
"gcloud is not logged in.",
|
|
54
|
+
["Run: gcloud auth login"],
|
|
55
|
+
)
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
|
|
58
|
+
proj = project or _clean(capture(["gcloud", "config", "get-value", "project"]))
|
|
59
|
+
if not proj:
|
|
60
|
+
error_block(
|
|
61
|
+
"No GCP project is set.",
|
|
62
|
+
["Pass --project <id>, or run: gcloud config set project <id>"],
|
|
63
|
+
)
|
|
64
|
+
raise typer.Exit(code=1)
|
|
65
|
+
|
|
66
|
+
reg = region or _clean(capture(["gcloud", "config", "get-value", "compute/region"])) or "us-central1"
|
|
67
|
+
zn = zone or _clean(capture(["gcloud", "config", "get-value", "compute/zone"])) or "us-central1-a"
|
|
68
|
+
|
|
69
|
+
console.print(f" gcloud account: {account}")
|
|
70
|
+
console.print(f" project={proj} region={reg} zone={zn}")
|
|
71
|
+
return {"project": proj, "region": reg, "zone": zn}
|
|
72
|
+
|
|
73
|
+
def ensure_terraform_auth(self) -> None:
|
|
74
|
+
# Terraform's google provider authenticates via Application Default
|
|
75
|
+
# Credentials. print-access-token exits non-zero (capture -> None) when
|
|
76
|
+
# ADC is not configured.
|
|
77
|
+
if capture(["gcloud", "auth", "application-default", "print-access-token"]) is None:
|
|
78
|
+
error_block(
|
|
79
|
+
"Terraform needs Application Default Credentials.",
|
|
80
|
+
["Run: gcloud auth application-default login"],
|
|
81
|
+
)
|
|
82
|
+
raise typer.Exit(code=1)
|
|
83
|
+
|
|
84
|
+
def enable_apis(self, ctx: dict) -> None:
|
|
85
|
+
console.log("Enabling required GCP APIs (compute, storage, iam)...")
|
|
86
|
+
run(["gcloud", "services", "enable", *_REQUIRED_APIS, "--project", ctx["project"]])
|
|
87
|
+
|
|
88
|
+
def tfvars_extra(self, ctx: dict) -> dict:
|
|
89
|
+
return {"project": ctx["project"], "region": ctx["region"], "zone": ctx["zone"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""``machina deploy status`` -- show the deployment's URL + health."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from cli._common import preflight
|
|
11
|
+
from cli.colors import console
|
|
12
|
+
|
|
13
|
+
from . import _state, _terraform
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def status_command() -> None:
|
|
17
|
+
# Establish the same DATA_DIR context as `deploy up` (load_config sets it)
|
|
18
|
+
# so workdir() resolves to the directory `up` created.
|
|
19
|
+
preflight()
|
|
20
|
+
meta = _state.read_meta()
|
|
21
|
+
if meta is None:
|
|
22
|
+
console.print("[yellow]No 'machinaos' deployment found.[/]")
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
|
|
25
|
+
wd = _state.workdir()
|
|
26
|
+
ip = _terraform.tf_output(wd, "external_ip")
|
|
27
|
+
url = _terraform.tf_output(wd, "url") or (
|
|
28
|
+
f"http://{ip}:{meta.get('port', 8080)}" if ip else None
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
console.print(" Deployment: machinaos")
|
|
32
|
+
console.print(f" Provider: {meta.get('provider', '?')}")
|
|
33
|
+
console.print(f" External IP: {ip or '(unknown / destroyed)'}")
|
|
34
|
+
console.print(f" URL: {url or '(unknown)'}")
|
|
35
|
+
console.print(f" Login email: {meta.get('owner_email', '?')}")
|
|
36
|
+
|
|
37
|
+
if not url:
|
|
38
|
+
raise typer.Exit(code=1)
|
|
39
|
+
|
|
40
|
+
healthy = False
|
|
41
|
+
try:
|
|
42
|
+
with urllib.request.urlopen(url.rstrip("/") + "/health", timeout=5) as resp: # noqa: S310
|
|
43
|
+
healthy = resp.status == 200
|
|
44
|
+
except (urllib.error.URLError, OSError):
|
|
45
|
+
healthy = False
|
|
46
|
+
console.print(f" Health: {'[green]OK[/]' if healthy else '[yellow]not ready[/]'}")
|
|
47
|
+
if not healthy:
|
|
48
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""``machina deploy up`` -- provision + run MachinaOs on a fresh cloud VM.
|
|
2
|
+
|
|
3
|
+
Two stages:
|
|
4
|
+
STAGE 1 (cloud CLI): the provider adapter verifies the CLI is installed +
|
|
5
|
+
authenticated, resolves project/region/zone, ensures Terraform auth, and
|
|
6
|
+
enables the required cloud APIs.
|
|
7
|
+
STAGE 2 (Terraform): generate secrets + owner creds -> (local source) npm
|
|
8
|
+
pack -> write tfvars -> ``terraform init`` + ``apply`` -> read outputs ->
|
|
9
|
+
poll ``/health`` -> print URL + credentials.
|
|
10
|
+
|
|
11
|
+
The VM instance is always named ``machinaos`` (one deployment per project).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
|
|
23
|
+
from cli._common import error_block, preflight
|
|
24
|
+
from cli.colors import console
|
|
25
|
+
from cli.run import capture
|
|
26
|
+
|
|
27
|
+
from . import _secrets, _state, _terraform
|
|
28
|
+
from .providers import get_provider
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _npm_pack(root: Path) -> str:
|
|
32
|
+
"""Run ``npm pack`` in the repo root; return the abs path to the tarball."""
|
|
33
|
+
console.log("Packaging local build (npm pack)...")
|
|
34
|
+
out = capture(["npm", "pack"], cwd=root)
|
|
35
|
+
if not out:
|
|
36
|
+
error_block(
|
|
37
|
+
"`npm pack` produced no output.",
|
|
38
|
+
["Ensure Node/npm are installed and you are in a MachinaOs checkout."],
|
|
39
|
+
)
|
|
40
|
+
raise typer.Exit(code=1)
|
|
41
|
+
tarball = out.strip().splitlines()[-1].strip()
|
|
42
|
+
path = (root / tarball).resolve()
|
|
43
|
+
if not path.is_file():
|
|
44
|
+
error_block(f"npm pack reported {tarball!r} but it is not on disk.", [str(path)])
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
return str(path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _poll_health(url: str, *, attempts: int = 40, delay: float = 15.0) -> bool:
|
|
50
|
+
"""Poll ``<url>/health`` until 200 or attempts exhausted (~10 min)."""
|
|
51
|
+
health = url.rstrip("/") + "/health"
|
|
52
|
+
for i in range(1, attempts + 1):
|
|
53
|
+
try:
|
|
54
|
+
with urllib.request.urlopen(health, timeout=5) as resp: # noqa: S310
|
|
55
|
+
if resp.status == 200:
|
|
56
|
+
return True
|
|
57
|
+
except (urllib.error.URLError, OSError):
|
|
58
|
+
pass
|
|
59
|
+
console.log(f"waiting for the VM to finish provisioning... ({i}/{attempts})")
|
|
60
|
+
time.sleep(delay)
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def up_command(
|
|
65
|
+
*,
|
|
66
|
+
provider: str,
|
|
67
|
+
region: str | None,
|
|
68
|
+
zone: str | None,
|
|
69
|
+
machine_type: str,
|
|
70
|
+
port: int,
|
|
71
|
+
owner_email: str,
|
|
72
|
+
owner_password: str | None,
|
|
73
|
+
source: str,
|
|
74
|
+
version: str,
|
|
75
|
+
allow_cidr: str,
|
|
76
|
+
project: str | None,
|
|
77
|
+
) -> None:
|
|
78
|
+
_, root = preflight()
|
|
79
|
+
|
|
80
|
+
# --- STAGE 1: cloud CLI (auth + context + APIs) ------------------------
|
|
81
|
+
cli = get_provider(provider)
|
|
82
|
+
cli.check()
|
|
83
|
+
_terraform.ensure_terraform()
|
|
84
|
+
ctx = cli.resolve_context(region=region, zone=zone, project=project)
|
|
85
|
+
cli.ensure_terraform_auth()
|
|
86
|
+
cli.enable_apis(ctx)
|
|
87
|
+
|
|
88
|
+
if _state.exists() and (_state.workdir() / "terraform.tfstate").exists():
|
|
89
|
+
console.print(
|
|
90
|
+
"[yellow]A 'machinaos' deployment already exists. Re-running will "
|
|
91
|
+
"re-apply Terraform (safe), or run `machina deploy destroy` first.[/]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# --- STAGE 2: secrets + source + Terraform -----------------------------
|
|
95
|
+
pw = owner_password or _secrets.new_password()
|
|
96
|
+
pw_generated = owner_password is None
|
|
97
|
+
app_env = _secrets.build_app_env(owner_email=owner_email, owner_password=pw, port=port)
|
|
98
|
+
|
|
99
|
+
pack_tarball = _npm_pack(root) if source == "local" else ""
|
|
100
|
+
|
|
101
|
+
tfvars: dict = {
|
|
102
|
+
"machine_type": machine_type,
|
|
103
|
+
"port": port,
|
|
104
|
+
"allow_cidr": allow_cidr,
|
|
105
|
+
"app_env": app_env,
|
|
106
|
+
"source_mode": source,
|
|
107
|
+
"machinaos_version": version,
|
|
108
|
+
"pack_tarball": pack_tarball,
|
|
109
|
+
}
|
|
110
|
+
tfvars.update(cli.tfvars_extra(ctx))
|
|
111
|
+
|
|
112
|
+
wd = _state.workdir()
|
|
113
|
+
_terraform.prepare_workdir(wd, provider)
|
|
114
|
+
_terraform.write_tfvars(wd, tfvars)
|
|
115
|
+
_state.write_meta({"provider": provider, "port": port, "owner_email": owner_email})
|
|
116
|
+
|
|
117
|
+
console.log("terraform init...")
|
|
118
|
+
_terraform.tf(wd, "init", "-input=false")
|
|
119
|
+
console.log("terraform apply (creating cloud resources)...")
|
|
120
|
+
_terraform.tf(wd, "apply", "-auto-approve", "-input=false")
|
|
121
|
+
|
|
122
|
+
ip = _terraform.tf_output(wd, "external_ip")
|
|
123
|
+
url = _terraform.tf_output(wd, "url") or (f"http://{ip}:{port}" if ip else None)
|
|
124
|
+
|
|
125
|
+
console.print()
|
|
126
|
+
console.print(" [bold green]VM 'machinaos' provisioned.[/]")
|
|
127
|
+
if ip:
|
|
128
|
+
console.print(f" External IP: {ip}")
|
|
129
|
+
if url:
|
|
130
|
+
console.print(f" URL: {url}")
|
|
131
|
+
console.print(f" Login email: {owner_email}")
|
|
132
|
+
if pw_generated:
|
|
133
|
+
console.print(f" Login password (save this now): [bold]{pw}[/]")
|
|
134
|
+
else:
|
|
135
|
+
console.print(" Login password: (the one you provided)")
|
|
136
|
+
console.print()
|
|
137
|
+
|
|
138
|
+
if url:
|
|
139
|
+
console.log("The VM is installing MachinaOs (Node + npm + build); this takes a few minutes.")
|
|
140
|
+
if _poll_health(url):
|
|
141
|
+
console.print(f" [bold green]Ready.[/] Open {url} and log in.")
|
|
142
|
+
else:
|
|
143
|
+
console.print(
|
|
144
|
+
" [yellow]Still provisioning.[/] Check again with "
|
|
145
|
+
"`machina deploy status` in a few minutes."
|
|
146
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""``machina serve`` -- single-port production runtime.
|
|
2
|
+
|
|
3
|
+
Runs the app on ONE public port: uvicorn serves the REST API + WebSocket +
|
|
4
|
+
the built React SPA (via the ``SERVE_STATIC_CLIENT`` block in
|
|
5
|
+
``server/main.py``), plus the Node.js code-exec sidecar on its own internal
|
|
6
|
+
port. Used locally for a production-shaped run AND as the systemd
|
|
7
|
+
``ExecStart`` on a VM provisioned by ``machina deploy``.
|
|
8
|
+
|
|
9
|
+
Unlike ``machina start`` (which runs a separate static-client server + the
|
|
10
|
+
backend + temporal on multiple ports), ``serve`` is single-port and serves
|
|
11
|
+
the client from the backend itself. The Node sidecar is NOT launched by
|
|
12
|
+
``start``/``dev`` today, so ``serve`` adds it (the JS/TS executor nodes need
|
|
13
|
+
it).
|
|
14
|
+
|
|
15
|
+
The long-running uvicorn is invoked via the server venv's interpreter
|
|
16
|
+
directly (not ``uv run``) so the systemd service has no runtime dependency
|
|
17
|
+
on ``uv`` being on PATH.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import typer
|
|
27
|
+
|
|
28
|
+
from cli._common import preflight
|
|
29
|
+
from cli.buildenv import validate_build
|
|
30
|
+
from cli.colors import console
|
|
31
|
+
from cli.platform_ import IS_WINDOWS, server_dir, server_venv
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _venv_python(root: Path | None = None) -> str:
|
|
35
|
+
"""Absolute path to the server venv's Python interpreter."""
|
|
36
|
+
venv = server_venv(root)
|
|
37
|
+
rel = "Scripts/python.exe" if IS_WINDOWS else "bin/python"
|
|
38
|
+
return str(venv / rel)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def serve_command(port: int | None = None) -> None:
|
|
42
|
+
from cli.supervisor import Manager, ServiceSpec
|
|
43
|
+
|
|
44
|
+
cfg, root = preflight()
|
|
45
|
+
os.environ.setdefault("PYTHONUTF8", "1")
|
|
46
|
+
validate_build(root, require_client_dist=True)
|
|
47
|
+
|
|
48
|
+
# Public port: --port flag > $PORT (Cloud Run / systemd convention) >
|
|
49
|
+
# PYTHON_BACKEND_PORT from the env files.
|
|
50
|
+
bind_port = port or int(os.environ.get("PORT") or cfg.backend_port)
|
|
51
|
+
|
|
52
|
+
# Free the ports we will bind (clears stale orphans; idempotent).
|
|
53
|
+
from cli.ports import kill_port
|
|
54
|
+
|
|
55
|
+
for p in {bind_port, cfg.nodejs_port}:
|
|
56
|
+
kill_port(p)
|
|
57
|
+
|
|
58
|
+
console.print()
|
|
59
|
+
console.print(" [bold]MachinaOS[/] serve (single-port)")
|
|
60
|
+
console.print(f" App: http://0.0.0.0:{bind_port} (API + WebSocket + SPA)")
|
|
61
|
+
console.print(f" Sidecar: 127.0.0.1:{cfg.nodejs_port} (JS/TS executor)")
|
|
62
|
+
console.print()
|
|
63
|
+
|
|
64
|
+
sidecar = server_dir(root) / "nodejs" / "dist" / "index.js"
|
|
65
|
+
|
|
66
|
+
specs = [
|
|
67
|
+
ServiceSpec(
|
|
68
|
+
name="server",
|
|
69
|
+
argv=[
|
|
70
|
+
_venv_python(root),
|
|
71
|
+
"-m",
|
|
72
|
+
"uvicorn",
|
|
73
|
+
"main:app",
|
|
74
|
+
"--host",
|
|
75
|
+
"0.0.0.0",
|
|
76
|
+
"--port",
|
|
77
|
+
str(bind_port),
|
|
78
|
+
"--log-level",
|
|
79
|
+
"warning",
|
|
80
|
+
],
|
|
81
|
+
cwd=server_dir(root),
|
|
82
|
+
env={"SERVE_STATIC_CLIENT": "1", "PORT": str(bind_port)},
|
|
83
|
+
ready_port=bind_port,
|
|
84
|
+
),
|
|
85
|
+
ServiceSpec(
|
|
86
|
+
name="nodejs",
|
|
87
|
+
argv=["node", str(sidecar)],
|
|
88
|
+
cwd=server_dir(root) / "nodejs",
|
|
89
|
+
env={"NODEJS_EXECUTOR_PORT": str(cfg.nodejs_port)},
|
|
90
|
+
ready_port=cfg.nodejs_port,
|
|
91
|
+
),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
manager = Manager()
|
|
95
|
+
manager.add_all(specs)
|
|
96
|
+
rc = asyncio.run(manager.run())
|
|
97
|
+
if rc != 0:
|
|
98
|
+
raise typer.Exit(code=rc)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.3"
|
|
3
|
+
required_providers {
|
|
4
|
+
google = {
|
|
5
|
+
source = "hashicorp/google"
|
|
6
|
+
version = "~> 5.0"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
provider "google" {
|
|
12
|
+
project = var.project
|
|
13
|
+
region = var.region
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
locals {
|
|
17
|
+
# The instance is always named "machinaos" (one deployment per project).
|
|
18
|
+
use_local = var.source_mode == "local"
|
|
19
|
+
res_name = "machinaos"
|
|
20
|
+
bucket_name = "${var.project}-machinaos"
|
|
21
|
+
object_name = "machinaos.tgz"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# --- Artifact bucket + object (local source only) --------------------------
|
|
25
|
+
|
|
26
|
+
resource "google_storage_bucket" "artifact" {
|
|
27
|
+
count = local.use_local ? 1 : 0
|
|
28
|
+
name = local.bucket_name
|
|
29
|
+
location = var.region
|
|
30
|
+
force_destroy = true
|
|
31
|
+
uniform_bucket_level_access = true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resource "google_storage_bucket_object" "pkg" {
|
|
35
|
+
count = local.use_local ? 1 : 0
|
|
36
|
+
name = local.object_name
|
|
37
|
+
bucket = google_storage_bucket.artifact[0].name
|
|
38
|
+
source = var.pack_tarball
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# --- VM service account (reads the artifact bucket) ------------------------
|
|
42
|
+
|
|
43
|
+
resource "google_service_account" "vm" {
|
|
44
|
+
account_id = local.res_name
|
|
45
|
+
display_name = "MachinaOs VM"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resource "google_storage_bucket_iam_member" "vm_read" {
|
|
49
|
+
count = local.use_local ? 1 : 0
|
|
50
|
+
bucket = google_storage_bucket.artifact[0].name
|
|
51
|
+
role = "roles/storage.objectViewer"
|
|
52
|
+
member = "serviceAccount:${google_service_account.vm.email}"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# --- Firewall: app port + SSH ----------------------------------------------
|
|
56
|
+
|
|
57
|
+
resource "google_compute_firewall" "app" {
|
|
58
|
+
name = "${local.res_name}-app"
|
|
59
|
+
network = "default"
|
|
60
|
+
|
|
61
|
+
allow {
|
|
62
|
+
# CF-proxied front door (80 http-redirected, 443 TLS via nginx) + SSH +
|
|
63
|
+
# the app port for direct-IP access.
|
|
64
|
+
protocol = "tcp"
|
|
65
|
+
ports = ["80", "443", tostring(var.port), "22"]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
source_ranges = [var.allow_cidr]
|
|
69
|
+
target_tags = [local.res_name]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# --- Instance --------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
resource "google_compute_instance" "vm" {
|
|
75
|
+
name = local.res_name
|
|
76
|
+
machine_type = var.machine_type
|
|
77
|
+
zone = var.zone
|
|
78
|
+
tags = [local.res_name]
|
|
79
|
+
|
|
80
|
+
boot_disk {
|
|
81
|
+
initialize_params {
|
|
82
|
+
# Ubuntu 24.04 ships Python 3.12 natively — required by the package's
|
|
83
|
+
# install (server needs >=3.11,<3.13; the CLI needs >=3.12). 22.04
|
|
84
|
+
# ships 3.10 and the npm postinstall hard-fails on it.
|
|
85
|
+
image = "ubuntu-os-cloud/ubuntu-2404-lts-amd64"
|
|
86
|
+
size = 40
|
|
87
|
+
type = "pd-balanced"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
network_interface {
|
|
92
|
+
network = "default"
|
|
93
|
+
access_config {} # ephemeral external IP
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
service_account {
|
|
97
|
+
email = google_service_account.vm.email
|
|
98
|
+
scopes = ["cloud-platform"]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
metadata = {
|
|
102
|
+
startup-script = templatefile("${path.module}/startup.sh.tftpl", {
|
|
103
|
+
app_env = var.app_env
|
|
104
|
+
source_mode = var.source_mode
|
|
105
|
+
version = var.machinaos_version
|
|
106
|
+
bucket = local.use_local ? google_storage_bucket.artifact[0].name : ""
|
|
107
|
+
object = local.object_name
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# The startup script reads the bucket object; ensure it exists + is readable first.
|
|
112
|
+
depends_on = [
|
|
113
|
+
google_storage_bucket_object.pkg,
|
|
114
|
+
google_storage_bucket_iam_member.vm_read,
|
|
115
|
+
]
|
|
116
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
output "external_ip" {
|
|
2
|
+
value = google_compute_instance.vm.network_interface[0].access_config[0].nat_ip
|
|
3
|
+
description = "The VM's public IP."
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "url" {
|
|
7
|
+
value = "http://${google_compute_instance.vm.network_interface[0].access_config[0].nat_ip}:${var.port}"
|
|
8
|
+
description = "The app URL (login gate)."
|
|
9
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# MachinaOs VM startup script (rendered by Terraform `templatefile`).
|
|
3
|
+
# Installs the toolchain, installs MachinaOs (npm), writes the login-gate env
|
|
4
|
+
# file, and runs it under systemd via `machina serve`.
|
|
5
|
+
#
|
|
6
|
+
# NOTE on syntax: Terraform renders this file with templatefile(), so any
|
|
7
|
+
# dollar-brace sequence is a Terraform interpolation. Bash variables are
|
|
8
|
+
# therefore written UNBRACED ($TOKEN, $MACHINA_BIN) so Terraform leaves them
|
|
9
|
+
# alone. `set -x` is intentionally NOT used so secrets in the env file are
|
|
10
|
+
# never echoed to the serial console.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
13
|
+
|
|
14
|
+
echo "[machinaos] installing toolchain..."
|
|
15
|
+
apt-get update
|
|
16
|
+
apt-get install -y curl ca-certificates git build-essential pkg-config libffi-dev libssl-dev
|
|
17
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
18
|
+
apt-get install -y nodejs
|
|
19
|
+
corepack enable || true
|
|
20
|
+
# uv on a stable system path (machina build uses it to sync the Python venv).
|
|
21
|
+
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh
|
|
22
|
+
|
|
23
|
+
echo "[machinaos] installing MachinaOs package..."
|
|
24
|
+
%{ if source_mode == "local" ~}
|
|
25
|
+
TOKEN=$(curl -s -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" | python3 -c 'import sys, json; print(json.load(sys.stdin)["access_token"])')
|
|
26
|
+
curl -fsS -H "Authorization: Bearer $TOKEN" -o /tmp/machinaos.tgz "https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media"
|
|
27
|
+
npm install -g /tmp/machinaos.tgz
|
|
28
|
+
%{ else ~}
|
|
29
|
+
npm install -g machinaos@${version}
|
|
30
|
+
%{ endif ~}
|
|
31
|
+
|
|
32
|
+
echo "[machinaos] writing login-gate env..."
|
|
33
|
+
mkdir -p /etc/machinaos
|
|
34
|
+
cat > /etc/machinaos/machina.env <<'MACHINA_ENV_EOF'
|
|
35
|
+
%{ for k, v in app_env ~}
|
|
36
|
+
${k}=${v}
|
|
37
|
+
%{ endfor ~}
|
|
38
|
+
MACHINA_ENV_EOF
|
|
39
|
+
chmod 600 /etc/machinaos/machina.env
|
|
40
|
+
|
|
41
|
+
mkdir -p /var/lib/machinaos
|
|
42
|
+
|
|
43
|
+
echo "[machinaos] installing systemd service..."
|
|
44
|
+
MACHINA_BIN=$(command -v machina)
|
|
45
|
+
cat > /etc/systemd/system/machinaos.service <<SERVICE_EOF
|
|
46
|
+
[Unit]
|
|
47
|
+
Description=MachinaOs (single login-gated instance)
|
|
48
|
+
After=network-online.target
|
|
49
|
+
Wants=network-online.target
|
|
50
|
+
|
|
51
|
+
[Service]
|
|
52
|
+
Type=simple
|
|
53
|
+
EnvironmentFile=/etc/machinaos/machina.env
|
|
54
|
+
ExecStart=$MACHINA_BIN serve
|
|
55
|
+
Restart=on-failure
|
|
56
|
+
RestartSec=5
|
|
57
|
+
TimeoutStopSec=20
|
|
58
|
+
|
|
59
|
+
[Install]
|
|
60
|
+
WantedBy=multi-user.target
|
|
61
|
+
SERVICE_EOF
|
|
62
|
+
|
|
63
|
+
systemctl daemon-reload
|
|
64
|
+
systemctl enable --now machinaos
|
|
65
|
+
echo "[machinaos] done."
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Shared deployment variable interface (every provider module declares the
|
|
2
|
+
# same set, so `machina deploy` writes one tfvars shape regardless of provider).
|
|
3
|
+
|
|
4
|
+
variable "project" {
|
|
5
|
+
type = string
|
|
6
|
+
description = "GCP project id."
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
variable "region" {
|
|
10
|
+
type = string
|
|
11
|
+
description = "GCP region."
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
variable "zone" {
|
|
15
|
+
type = string
|
|
16
|
+
description = "GCP zone for the instance."
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
variable "machine_type" {
|
|
20
|
+
type = string
|
|
21
|
+
description = "Compute Engine machine type."
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "port" {
|
|
25
|
+
type = number
|
|
26
|
+
description = "Public port the app binds and the firewall opens."
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
variable "allow_cidr" {
|
|
30
|
+
type = string
|
|
31
|
+
description = "Firewall source range (e.g. 0.0.0.0/0 or <your-ip>/32)."
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
variable "source_mode" {
|
|
35
|
+
type = string
|
|
36
|
+
description = "Install source: 'local' (npm pack tarball via bucket) or 'release' (npm registry)."
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variable "machinaos_version" {
|
|
40
|
+
type = string
|
|
41
|
+
description = "machinaos version to install when source_mode = 'release'."
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
variable "pack_tarball" {
|
|
45
|
+
type = string
|
|
46
|
+
default = ""
|
|
47
|
+
description = "Absolute path to the npm pack tarball (source_mode = 'local')."
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
variable "app_env" {
|
|
51
|
+
type = map(string)
|
|
52
|
+
sensitive = true
|
|
53
|
+
description = "KEY=VALUE map rendered into the VM's /etc/machinaos/machina.env."
|
|
54
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{f as u,n as d,o as p,p as m,q as f,r as g,k as x}from"./index-
|
|
1
|
+
import{f as u,n as d,o as p,p as m,q as f,r as g,k as x}from"./index-CW3r0-Ob.js";import{j as l}from"./vendor-query-SzWcOU0G.js";import{q as b}from"./vendor-icons-CVrPjN2Q.js";function h(r){const t=u.c(7),a=d(),n=p(),s=m(),i=f(),o=g();if(!r)return null;let e;return t[0]!==n||t[1]!==i||t[2]!==r||t[3]!==s||t[4]!==o||t[5]!==a?(e={whatsapp:a,android:n,twitter:s,google:i,telegram:o}[r]??null,t[0]=n,t[1]=i,t[2]=r,t[3]=s,t[4]=o,t[5]=a,t[6]=e):e=t[6],e}const y=r=>{const t=u.c(7),{actions:a,loading:n}=r;let s;if(t[0]!==a||t[1]!==n){let o;t[3]!==n?(o=e=>{const c=n===e.key;return l.jsxs(x,{intent:e.intent,onClick:e.onClick,disabled:e.disabled||c,children:[c?l.jsx(b,{className:"h-4 w-4 animate-spin"}):e.icon,e.label]},e.key)},t[3]=n,t[4]=o):o=t[4],s=a.filter(j).map(o),t[0]=a,t[1]=n,t[2]=s}else s=t[2];let i;return t[5]!==s?(i=l.jsx("div",{className:"flex justify-center gap-2 border-t border-border pt-3",children:s}),t[5]=s,t[6]=i):i=t[6],i};function j(r){return!r.hidden}export{y as A,h as u};
|