diff --git a/.tekton/hwlab-nc01-production-pac.yaml b/.tekton/hwlab-nc01-production-pac.yaml index 790dc35f..d5cc737d 100644 --- a/.tekton/hwlab-nc01-production-pac.yaml +++ b/.tekton/hwlab-nc01-production-pac.yaml @@ -9,7 +9,7 @@ metadata: pipelinesascode.tekton.dev/on-cel-expression: "event == 'push' && target_branch == 'release' && node == 'NC01'" pipelinesascode.tekton.dev/max-keep-runs: "8" unidesk.ai/owning-config-ref: "config/hwlab-node-lanes.yaml#lanes.production.targets.NC01" - unidesk.ai/effective-config-sha256: sha256:8c54c1d70b44aecc890f729e5d849c1a3ca68d7069c8ee20774dcd5a1b7ff365 + unidesk.ai/effective-config-sha256: sha256:c01c32df9c12067100035d9416f7b462adde33781e6a568e400b5a33928aab33 unidesk.ai/source-artifact-renderer: hwlab-runtime-lane unidesk.ai/source-artifact-mode: remote-pipeline-annotation pipelinesascode.tekton.dev/pipeline: ci/pipelines/hwlab-nc01-production-ci-image-publish.yaml @@ -77,7 +77,7 @@ spec: - name: registry-prefix value: 127.0.0.1:5000/hwlab - name: services - value: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" + value: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-tasktree-api,hwlab-tasktree-worker,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" - name: base-image value: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim - name: build-cache-mode diff --git a/.tekton/hwlab-nc01-v03-pac.yaml b/.tekton/hwlab-nc01-v03-pac.yaml index 43b4e788..b52309bf 100644 --- a/.tekton/hwlab-nc01-v03-pac.yaml +++ b/.tekton/hwlab-nc01-v03-pac.yaml @@ -77,7 +77,7 @@ spec: - name: registry-prefix value: 127.0.0.1:5000/hwlab - name: services - value: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" + value: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-tasktree-api,hwlab-tasktree-worker,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" - name: base-image value: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim - name: build-cache-mode diff --git a/ci/pipelines/hwlab-nc01-production-ci-image-publish.yaml b/ci/pipelines/hwlab-nc01-production-ci-image-publish.yaml index 5d708e62..da5f9608 100644 --- a/ci/pipelines/hwlab-nc01-production-ci-image-publish.yaml +++ b/ci/pipelines/hwlab-nc01-production-ci-image-publish.yaml @@ -13,7 +13,7 @@ metadata: hwlab.pikastech.local/download-profile: jd01-node-default hwlab.pikastech.local/network-profile: jd01-node-ci-egress unidesk.ai/owning-config-ref: "config/hwlab-node-lanes.yaml#lanes.production.targets.NC01" - unidesk.ai/effective-config-sha256: sha256:8c54c1d70b44aecc890f729e5d849c1a3ca68d7069c8ee20774dcd5a1b7ff365 + unidesk.ai/effective-config-sha256: sha256:c01c32df9c12067100035d9416f7b462adde33781e6a568e400b5a33928aab33 unidesk.ai/source-artifact-renderer: hwlab-runtime-lane unidesk.ai/source-artifact-mode: remote-pipeline-annotation spec: @@ -53,7 +53,7 @@ spec: default: 127.0.0.1:5000/hwlab - name: services type: string - default: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" + default: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-tasktree-api,hwlab-tasktree-worker,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" - name: base-image type: string default: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim @@ -359,6 +359,14 @@ spec: description: hwlab-project-management rollout affected according to ci-plan - name: build-hwlab-project-management description: hwlab-project-management image build required according to ci-plan + - name: affected-hwlab-tasktree-api + description: hwlab-tasktree-api rollout affected according to ci-plan + - name: build-hwlab-tasktree-api + description: hwlab-tasktree-api image build required according to ci-plan + - name: affected-hwlab-tasktree-worker + description: hwlab-tasktree-worker rollout affected according to ci-plan + - name: build-hwlab-tasktree-worker + description: hwlab-tasktree-worker image build required according to ci-plan - name: affected-hwlab-cloud-web description: hwlab-cloud-web rollout affected according to ci-plan - name: build-hwlab-cloud-web @@ -1264,6 +1272,422 @@ spec: value: $(params.base-image) - name: build-cache-mode value: $(params.build-cache-mode) + - name: build-hwlab-tasktree-api + runAfter: + - plan-artifacts + workspaces: + - name: source + workspace: source + when: + - input: $(tasks.plan-artifacts.results.build-hwlab-tasktree-api) + operator: in + values: + - "true" + taskSpec: + params: + - name: revision + - name: lane + - name: catalog-path + - name: image-tag-mode + - name: registry-prefix + - name: service-id + - name: base-image + - name: build-cache-mode + results: + - name: status + description: hwlab-tasktree-api status + - name: service-id + description: hwlab-tasktree-api service-id + - name: image + description: hwlab-tasktree-api image + - name: image-tag + description: hwlab-tasktree-api image-tag + - name: digest + description: hwlab-tasktree-api digest + - name: repository-digest + description: hwlab-tasktree-api repository-digest + - name: source-commit-id + description: hwlab-tasktree-api source-commit-id + - name: component-input-hash + description: hwlab-tasktree-api component-input-hash + - name: environment-input-hash + description: hwlab-tasktree-api environment-input-hash + - name: code-input-hash + description: hwlab-tasktree-api code-input-hash + - name: runtime-mode + description: hwlab-tasktree-api runtime-mode + - name: boot-repo + description: hwlab-tasktree-api boot-repo + - name: boot-commit + description: hwlab-tasktree-api boot-commit + - name: boot-sh + description: hwlab-tasktree-api boot-sh + - name: build-created-at + description: hwlab-tasktree-api build-created-at + - name: build-backend + description: hwlab-tasktree-api build-backend + - name: reused-from + description: hwlab-tasktree-api reused-from + - name: go-base-image + description: hwlab-tasktree-api go-base-image + - name: go-base-image-status + description: hwlab-tasktree-api go-base-image-status + - name: go-base-digest + description: hwlab-tasktree-api go-base-digest + - name: go-base-build-duration-ms + description: hwlab-tasktree-api go-base-build-duration-ms + - name: go-base-buildkit-cache-ref + description: hwlab-tasktree-api go-base-buildkit-cache-ref + workspaces: + - name: source + volumes: + - name: service-work + emptyDir: + {} + - name: buildkit-bin + emptyDir: + {} + - name: buildkit-run + emptyDir: + {} + sidecars: + - name: buildkitd + image: 127.0.0.1:5000/hwlab/buildkit:rootless + args: + - "--addr" + - unix:///workspace/buildkit-run/buildkitd.sock + - "--oci-worker-no-process-sandbox" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + volumeMounts: + - name: buildkit-run + mountPath: /workspace/buildkit-run + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: true + appArmorProfile: + type: Unconfined + seccompProfile: + type: Unconfined + steps: + - name: prepare-buildkit-client + image: 127.0.0.1:5000/hwlab/buildkit:rootless + securityContext: + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + - name: publish + image: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + volumeMounts: + - name: service-work + mountPath: /workspace/service-work + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + - name: buildkit-run + mountPath: /workspace/buildkit-run + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-cicd-timing\",schemaVersion:\"v1\",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:\"scripts/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node npm git python3 ssh curl; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -s /workspace/source/affected-services.json ] && ! node -e 'const fs=require(\"node:fs\"); const p=JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\",\"utf8\")); const service=process.argv[1]; const item=(p.entries||[]).find((entry)=>entry.serviceId===service); process.exit(item && item.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"production\",\"sourceBranch\":\"release\",\"gitopsBranch\":\"release-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-production\",\"runtimeRenderDir\":\"runtime-production\",\"runtimeNamespace\":\"hwlab-production\",\"environment\":\"production\",\"catalogPath\":\"deploy/artifact-catalog.nc01-production.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab.hwpod.com\",\"publicApiUrl\":\"https://lab.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\",\"migrationSecretName\":\"hwlab-cloud-api-production-db\",\"migrationSecretKey\":\"migration-database-url\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-release.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-production-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-release\",\"secretNamespace\":\"agentrun-release\",\"providerSecretNames\":{\"codex\":\"agentrun-release-provider-codex\",\"gpt-pika\":\"agentrun-release-provider-gpt-pika\",\"grok\":\"agentrun-release-provider-grok\",\"dsflash-go\":\"agentrun-release-provider-dsflash-go\"},\"toolSecretNames\":{\"githubPr\":\"agentrun-release-tool-github-pr\",\"unideskSsh\":\"agentrun-release-tool-unidesk-ssh\"},\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-production-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-production-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-production-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-production-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-production-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-production-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"deployYamlGitMirror\":{\"id\":\"nc01-production\",\"node\":\"NC01\",\"lane\":\"production\",\"namespace\":\"devops-infra\",\"serviceReadName\":\"git-mirror-http\",\"serviceWriteName\":\"git-mirror-write\",\"cachePvcName\":\"hwlab-git-mirror-cache\",\"cachePvcStorage\":\"20Gi\",\"cacheHostPath\":null,\"servicePort\":8080,\"secretName\":\"git-mirror-github-ssh\",\"syncConfigMapName\":\"git-mirror-sync-script\",\"syncJobPrefix\":\"git-mirror-hwlab-nc01-v03-sync-manual\",\"flushJobPrefix\":\"git-mirror-hwlab-nc01-v03-flush-manual\",\"toolsImage\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"toolsImagePullPolicy\":\"Always\",\"sourceRepository\":\"pikasTech/HWLAB\",\"sourceBranch\":\"release\",\"gitopsBranch\":\"release-gitops\",\"egressProxy\":{\"mode\":\"node-global\",\"required\":true,\"clientName\":\"nc01-host-proxy\",\"namespace\":\"platform-infra\",\"serviceName\":\"nc01-host-proxy\",\"port\":10808,\"proxyUrl\":\"http://10.42.0.1:10808\",\"noProxy\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"kubernetes\",\"kubernetes.default\",\"kubernetes.default.svc\",\"argocd-repo-server\",\"argocd-repo-server.argocd\",\"argocd-redis\",\"argocd-redis.argocd\",\"git-mirror-http\",\"git-mirror-http.devops-infra\",\"git-mirror-write\",\"git-mirror-write.devops-infra\",\"10.0.0.0/8\",\"10.42.0.0/16\",\"10.43.0.0/16\",\"172.16.0.0/12\",\"192.168.0.0/16\",\"152.53.229.148\",\"152.53.229.148\",\"hyueapi.com\",\".hyueapi.com\"]},\"githubTransport\":{\"mode\":\"https\",\"username\":\"x-access-token\",\"tokenSecretName\":\"git-mirror-github-token\",\"tokenSecretKey\":\"GITHUB_TOKEN\",\"tokenSourceRef\":\"/root/.unidesk/.env/gh_token.txt\",\"tokenSourceKey\":\"GH_TOKEN\"}},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n ...(envRecipe.downloadStack || {}),\n httpProxy: overlay.dockerProxyHttp,\n httpsProxy: overlay.dockerProxyHttps,\n noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n ...lane,\n node: overlay.nodeId,\n sourceBranch: overlay.sourceBranch,\n gitopsBranch: overlay.gitopsBranch,\n namespace: overlay.runtimeNamespace,\n endpoint: overlay.publicApiUrl,\n publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n artifactCatalog: overlay.catalogPath,\n runtimePath: overlay.runtimeRenderDir,\n imageTagMode: 'full',\n sourceRepo: overlay.gitUrl,\n observability: overlay.observability,\n envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nif (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\nnode scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --registry-prefix \"$(params.registry-prefix)\" --report \"/workspace/source/service-results/$(params.service-id).json\" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services \"$(params.service-id)\" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr \"$HWLAB_BUILDKIT_ADDR\" --build-cache-mode \"$(params.build-cache-mode)\"\n" + imagePullPolicy: Always + params: + - name: revision + value: $(params.revision) + - name: lane + value: $(params.lane) + - name: catalog-path + value: $(params.catalog-path) + - name: image-tag-mode + value: $(params.image-tag-mode) + - name: registry-prefix + value: $(params.registry-prefix) + - name: service-id + value: hwlab-tasktree-api + - name: base-image + value: $(params.base-image) + - name: build-cache-mode + value: $(params.build-cache-mode) + - name: build-hwlab-tasktree-worker + runAfter: + - plan-artifacts + workspaces: + - name: source + workspace: source + when: + - input: $(tasks.plan-artifacts.results.build-hwlab-tasktree-worker) + operator: in + values: + - "true" + taskSpec: + params: + - name: revision + - name: lane + - name: catalog-path + - name: image-tag-mode + - name: registry-prefix + - name: service-id + - name: base-image + - name: build-cache-mode + results: + - name: status + description: hwlab-tasktree-worker status + - name: service-id + description: hwlab-tasktree-worker service-id + - name: image + description: hwlab-tasktree-worker image + - name: image-tag + description: hwlab-tasktree-worker image-tag + - name: digest + description: hwlab-tasktree-worker digest + - name: repository-digest + description: hwlab-tasktree-worker repository-digest + - name: source-commit-id + description: hwlab-tasktree-worker source-commit-id + - name: component-input-hash + description: hwlab-tasktree-worker component-input-hash + - name: environment-input-hash + description: hwlab-tasktree-worker environment-input-hash + - name: code-input-hash + description: hwlab-tasktree-worker code-input-hash + - name: runtime-mode + description: hwlab-tasktree-worker runtime-mode + - name: boot-repo + description: hwlab-tasktree-worker boot-repo + - name: boot-commit + description: hwlab-tasktree-worker boot-commit + - name: boot-sh + description: hwlab-tasktree-worker boot-sh + - name: build-created-at + description: hwlab-tasktree-worker build-created-at + - name: build-backend + description: hwlab-tasktree-worker build-backend + - name: reused-from + description: hwlab-tasktree-worker reused-from + - name: go-base-image + description: hwlab-tasktree-worker go-base-image + - name: go-base-image-status + description: hwlab-tasktree-worker go-base-image-status + - name: go-base-digest + description: hwlab-tasktree-worker go-base-digest + - name: go-base-build-duration-ms + description: hwlab-tasktree-worker go-base-build-duration-ms + - name: go-base-buildkit-cache-ref + description: hwlab-tasktree-worker go-base-buildkit-cache-ref + workspaces: + - name: source + volumes: + - name: service-work + emptyDir: + {} + - name: buildkit-bin + emptyDir: + {} + - name: buildkit-run + emptyDir: + {} + sidecars: + - name: buildkitd + image: 127.0.0.1:5000/hwlab/buildkit:rootless + args: + - "--addr" + - unix:///workspace/buildkit-run/buildkitd.sock + - "--oci-worker-no-process-sandbox" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + volumeMounts: + - name: buildkit-run + mountPath: /workspace/buildkit-run + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: true + appArmorProfile: + type: Unconfined + seccompProfile: + type: Unconfined + steps: + - name: prepare-buildkit-client + image: 127.0.0.1:5000/hwlab/buildkit:rootless + securityContext: + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + - name: publish + image: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + volumeMounts: + - name: service-work + mountPath: /workspace/service-work + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + - name: buildkit-run + mountPath: /workspace/buildkit-run + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-cicd-timing\",schemaVersion:\"v1\",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:\"scripts/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node npm git python3 ssh curl; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -s /workspace/source/affected-services.json ] && ! node -e 'const fs=require(\"node:fs\"); const p=JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\",\"utf8\")); const service=process.argv[1]; const item=(p.entries||[]).find((entry)=>entry.serviceId===service); process.exit(item && item.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"production\",\"sourceBranch\":\"release\",\"gitopsBranch\":\"release-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-production\",\"runtimeRenderDir\":\"runtime-production\",\"runtimeNamespace\":\"hwlab-production\",\"environment\":\"production\",\"catalogPath\":\"deploy/artifact-catalog.nc01-production.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab.hwpod.com\",\"publicApiUrl\":\"https://lab.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\",\"migrationSecretName\":\"hwlab-cloud-api-production-db\",\"migrationSecretKey\":\"migration-database-url\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-release.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-production-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-release\",\"secretNamespace\":\"agentrun-release\",\"providerSecretNames\":{\"codex\":\"agentrun-release-provider-codex\",\"gpt-pika\":\"agentrun-release-provider-gpt-pika\",\"grok\":\"agentrun-release-provider-grok\",\"dsflash-go\":\"agentrun-release-provider-dsflash-go\"},\"toolSecretNames\":{\"githubPr\":\"agentrun-release-tool-github-pr\",\"unideskSsh\":\"agentrun-release-tool-unidesk-ssh\"},\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-production-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-production-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-production-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-production-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-production-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-production-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"deployYamlGitMirror\":{\"id\":\"nc01-production\",\"node\":\"NC01\",\"lane\":\"production\",\"namespace\":\"devops-infra\",\"serviceReadName\":\"git-mirror-http\",\"serviceWriteName\":\"git-mirror-write\",\"cachePvcName\":\"hwlab-git-mirror-cache\",\"cachePvcStorage\":\"20Gi\",\"cacheHostPath\":null,\"servicePort\":8080,\"secretName\":\"git-mirror-github-ssh\",\"syncConfigMapName\":\"git-mirror-sync-script\",\"syncJobPrefix\":\"git-mirror-hwlab-nc01-v03-sync-manual\",\"flushJobPrefix\":\"git-mirror-hwlab-nc01-v03-flush-manual\",\"toolsImage\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"toolsImagePullPolicy\":\"Always\",\"sourceRepository\":\"pikasTech/HWLAB\",\"sourceBranch\":\"release\",\"gitopsBranch\":\"release-gitops\",\"egressProxy\":{\"mode\":\"node-global\",\"required\":true,\"clientName\":\"nc01-host-proxy\",\"namespace\":\"platform-infra\",\"serviceName\":\"nc01-host-proxy\",\"port\":10808,\"proxyUrl\":\"http://10.42.0.1:10808\",\"noProxy\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"kubernetes\",\"kubernetes.default\",\"kubernetes.default.svc\",\"argocd-repo-server\",\"argocd-repo-server.argocd\",\"argocd-redis\",\"argocd-redis.argocd\",\"git-mirror-http\",\"git-mirror-http.devops-infra\",\"git-mirror-write\",\"git-mirror-write.devops-infra\",\"10.0.0.0/8\",\"10.42.0.0/16\",\"10.43.0.0/16\",\"172.16.0.0/12\",\"192.168.0.0/16\",\"152.53.229.148\",\"152.53.229.148\",\"hyueapi.com\",\".hyueapi.com\"]},\"githubTransport\":{\"mode\":\"https\",\"username\":\"x-access-token\",\"tokenSecretName\":\"git-mirror-github-token\",\"tokenSecretKey\":\"GITHUB_TOKEN\",\"tokenSourceRef\":\"/root/.unidesk/.env/gh_token.txt\",\"tokenSourceKey\":\"GH_TOKEN\"}},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n ...(envRecipe.downloadStack || {}),\n httpProxy: overlay.dockerProxyHttp,\n httpsProxy: overlay.dockerProxyHttps,\n noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n ...lane,\n node: overlay.nodeId,\n sourceBranch: overlay.sourceBranch,\n gitopsBranch: overlay.gitopsBranch,\n namespace: overlay.runtimeNamespace,\n endpoint: overlay.publicApiUrl,\n publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n artifactCatalog: overlay.catalogPath,\n runtimePath: overlay.runtimeRenderDir,\n imageTagMode: 'full',\n sourceRepo: overlay.gitUrl,\n observability: overlay.observability,\n envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nif (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\nnode scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --registry-prefix \"$(params.registry-prefix)\" --report \"/workspace/source/service-results/$(params.service-id).json\" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services \"$(params.service-id)\" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr \"$HWLAB_BUILDKIT_ADDR\" --build-cache-mode \"$(params.build-cache-mode)\"\n" + imagePullPolicy: Always + params: + - name: revision + value: $(params.revision) + - name: lane + value: $(params.lane) + - name: catalog-path + value: $(params.catalog-path) + - name: image-tag-mode + value: $(params.image-tag-mode) + - name: registry-prefix + value: $(params.registry-prefix) + - name: service-id + value: hwlab-tasktree-worker + - name: base-image + value: $(params.base-image) + - name: build-cache-mode + value: $(params.build-cache-mode) - name: build-hwlab-cloud-web runAfter: - plan-artifacts @@ -2102,6 +2526,8 @@ spec: - build-hwlab-workbench-runtime - build-hwlab-user-billing - build-hwlab-project-management + - build-hwlab-tasktree-api + - build-hwlab-tasktree-worker - build-hwlab-cloud-web - build-hwlab-gateway - build-hwlab-edge-proxy diff --git a/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml b/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml index c0e7b6c6..b3440268 100644 --- a/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml +++ b/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml @@ -53,7 +53,7 @@ spec: default: 127.0.0.1:5000/hwlab - name: services type: string - default: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" + default: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-tasktree-api,hwlab-tasktree-worker,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills" - name: base-image type: string default: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim @@ -359,6 +359,14 @@ spec: description: hwlab-project-management rollout affected according to ci-plan - name: build-hwlab-project-management description: hwlab-project-management image build required according to ci-plan + - name: affected-hwlab-tasktree-api + description: hwlab-tasktree-api rollout affected according to ci-plan + - name: build-hwlab-tasktree-api + description: hwlab-tasktree-api image build required according to ci-plan + - name: affected-hwlab-tasktree-worker + description: hwlab-tasktree-worker rollout affected according to ci-plan + - name: build-hwlab-tasktree-worker + description: hwlab-tasktree-worker image build required according to ci-plan - name: affected-hwlab-cloud-web description: hwlab-cloud-web rollout affected according to ci-plan - name: build-hwlab-cloud-web @@ -1264,6 +1272,422 @@ spec: value: $(params.base-image) - name: build-cache-mode value: $(params.build-cache-mode) + - name: build-hwlab-tasktree-api + runAfter: + - plan-artifacts + workspaces: + - name: source + workspace: source + when: + - input: $(tasks.plan-artifacts.results.build-hwlab-tasktree-api) + operator: in + values: + - "true" + taskSpec: + params: + - name: revision + - name: lane + - name: catalog-path + - name: image-tag-mode + - name: registry-prefix + - name: service-id + - name: base-image + - name: build-cache-mode + results: + - name: status + description: hwlab-tasktree-api status + - name: service-id + description: hwlab-tasktree-api service-id + - name: image + description: hwlab-tasktree-api image + - name: image-tag + description: hwlab-tasktree-api image-tag + - name: digest + description: hwlab-tasktree-api digest + - name: repository-digest + description: hwlab-tasktree-api repository-digest + - name: source-commit-id + description: hwlab-tasktree-api source-commit-id + - name: component-input-hash + description: hwlab-tasktree-api component-input-hash + - name: environment-input-hash + description: hwlab-tasktree-api environment-input-hash + - name: code-input-hash + description: hwlab-tasktree-api code-input-hash + - name: runtime-mode + description: hwlab-tasktree-api runtime-mode + - name: boot-repo + description: hwlab-tasktree-api boot-repo + - name: boot-commit + description: hwlab-tasktree-api boot-commit + - name: boot-sh + description: hwlab-tasktree-api boot-sh + - name: build-created-at + description: hwlab-tasktree-api build-created-at + - name: build-backend + description: hwlab-tasktree-api build-backend + - name: reused-from + description: hwlab-tasktree-api reused-from + - name: go-base-image + description: hwlab-tasktree-api go-base-image + - name: go-base-image-status + description: hwlab-tasktree-api go-base-image-status + - name: go-base-digest + description: hwlab-tasktree-api go-base-digest + - name: go-base-build-duration-ms + description: hwlab-tasktree-api go-base-build-duration-ms + - name: go-base-buildkit-cache-ref + description: hwlab-tasktree-api go-base-buildkit-cache-ref + workspaces: + - name: source + volumes: + - name: service-work + emptyDir: + {} + - name: buildkit-bin + emptyDir: + {} + - name: buildkit-run + emptyDir: + {} + sidecars: + - name: buildkitd + image: 127.0.0.1:5000/hwlab/buildkit:rootless + args: + - "--addr" + - unix:///workspace/buildkit-run/buildkitd.sock + - "--oci-worker-no-process-sandbox" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + volumeMounts: + - name: buildkit-run + mountPath: /workspace/buildkit-run + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: true + appArmorProfile: + type: Unconfined + seccompProfile: + type: Unconfined + steps: + - name: prepare-buildkit-client + image: 127.0.0.1:5000/hwlab/buildkit:rootless + securityContext: + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + - name: publish + image: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + volumeMounts: + - name: service-work + mountPath: /workspace/service-work + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + - name: buildkit-run + mountPath: /workspace/buildkit-run + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-cicd-timing\",schemaVersion:\"v1\",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:\"scripts/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node npm git python3 ssh curl; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -s /workspace/source/affected-services.json ] && ! node -e 'const fs=require(\"node:fs\"); const p=JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\",\"utf8\")); const service=process.argv[1]; const item=(p.entries||[]).find((entry)=>entry.serviceId===service); process.exit(item && item.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"v03\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-v03\",\"runtimeRenderDir\":\"runtime-v03\",\"runtimeNamespace\":\"hwlab-v03\",\"environment\":\"development\",\"catalogPath\":\"deploy/artifact-catalog.nc01-v03.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab-dev.hwpod.com\",\"publicApiUrl\":\"https://lab-dev.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-v03-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-v02\",\"secretNamespace\":\"agentrun-v02\",\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-v03-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-v03-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-v03-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-v03-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-v03-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-v03-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"deployYamlGitMirror\":{\"id\":\"nc01-v03\",\"node\":\"NC01\",\"lane\":\"v03\",\"namespace\":\"devops-infra\",\"serviceReadName\":\"git-mirror-http\",\"serviceWriteName\":\"git-mirror-write\",\"cachePvcName\":\"hwlab-git-mirror-cache\",\"cachePvcStorage\":\"20Gi\",\"cacheHostPath\":null,\"servicePort\":8080,\"secretName\":\"git-mirror-github-ssh\",\"syncConfigMapName\":\"git-mirror-sync-script\",\"syncJobPrefix\":\"git-mirror-hwlab-nc01-v03-sync-manual\",\"flushJobPrefix\":\"git-mirror-hwlab-nc01-v03-flush-manual\",\"toolsImage\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"toolsImagePullPolicy\":\"Always\",\"sourceRepository\":\"pikasTech/HWLAB\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"egressProxy\":{\"mode\":\"node-global\",\"required\":true,\"clientName\":\"nc01-host-proxy\",\"namespace\":\"platform-infra\",\"serviceName\":\"nc01-host-proxy\",\"port\":10808,\"proxyUrl\":\"http://10.42.0.1:10808\",\"noProxy\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"kubernetes\",\"kubernetes.default\",\"kubernetes.default.svc\",\"argocd-repo-server\",\"argocd-repo-server.argocd\",\"argocd-redis\",\"argocd-redis.argocd\",\"git-mirror-http\",\"git-mirror-http.devops-infra\",\"git-mirror-write\",\"git-mirror-write.devops-infra\",\"10.0.0.0/8\",\"10.42.0.0/16\",\"10.43.0.0/16\",\"172.16.0.0/12\",\"192.168.0.0/16\",\"152.53.229.148\",\"152.53.229.148\",\"hyueapi.com\",\".hyueapi.com\"]},\"githubTransport\":{\"mode\":\"https\",\"username\":\"x-access-token\",\"tokenSecretName\":\"git-mirror-github-token\",\"tokenSecretKey\":\"GITHUB_TOKEN\",\"tokenSourceRef\":\"/root/.unidesk/.env/gh_token.txt\",\"tokenSourceKey\":\"GH_TOKEN\"}},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n ...(envRecipe.downloadStack || {}),\n httpProxy: overlay.dockerProxyHttp,\n httpsProxy: overlay.dockerProxyHttps,\n noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n ...lane,\n node: overlay.nodeId,\n sourceBranch: overlay.sourceBranch,\n gitopsBranch: overlay.gitopsBranch,\n namespace: overlay.runtimeNamespace,\n endpoint: overlay.publicApiUrl,\n publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n artifactCatalog: overlay.catalogPath,\n runtimePath: overlay.runtimePath,\n imageTagMode: 'full',\n sourceRepo: overlay.gitUrl,\n observability: overlay.observability,\n envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nif (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\nnode scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --registry-prefix \"$(params.registry-prefix)\" --report \"/workspace/source/service-results/$(params.service-id).json\" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services \"$(params.service-id)\" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr \"$HWLAB_BUILDKIT_ADDR\" --build-cache-mode \"$(params.build-cache-mode)\"\n" + imagePullPolicy: Always + params: + - name: revision + value: $(params.revision) + - name: lane + value: $(params.lane) + - name: catalog-path + value: $(params.catalog-path) + - name: image-tag-mode + value: $(params.image-tag-mode) + - name: registry-prefix + value: $(params.registry-prefix) + - name: service-id + value: hwlab-tasktree-api + - name: base-image + value: $(params.base-image) + - name: build-cache-mode + value: $(params.build-cache-mode) + - name: build-hwlab-tasktree-worker + runAfter: + - plan-artifacts + workspaces: + - name: source + workspace: source + when: + - input: $(tasks.plan-artifacts.results.build-hwlab-tasktree-worker) + operator: in + values: + - "true" + taskSpec: + params: + - name: revision + - name: lane + - name: catalog-path + - name: image-tag-mode + - name: registry-prefix + - name: service-id + - name: base-image + - name: build-cache-mode + results: + - name: status + description: hwlab-tasktree-worker status + - name: service-id + description: hwlab-tasktree-worker service-id + - name: image + description: hwlab-tasktree-worker image + - name: image-tag + description: hwlab-tasktree-worker image-tag + - name: digest + description: hwlab-tasktree-worker digest + - name: repository-digest + description: hwlab-tasktree-worker repository-digest + - name: source-commit-id + description: hwlab-tasktree-worker source-commit-id + - name: component-input-hash + description: hwlab-tasktree-worker component-input-hash + - name: environment-input-hash + description: hwlab-tasktree-worker environment-input-hash + - name: code-input-hash + description: hwlab-tasktree-worker code-input-hash + - name: runtime-mode + description: hwlab-tasktree-worker runtime-mode + - name: boot-repo + description: hwlab-tasktree-worker boot-repo + - name: boot-commit + description: hwlab-tasktree-worker boot-commit + - name: boot-sh + description: hwlab-tasktree-worker boot-sh + - name: build-created-at + description: hwlab-tasktree-worker build-created-at + - name: build-backend + description: hwlab-tasktree-worker build-backend + - name: reused-from + description: hwlab-tasktree-worker reused-from + - name: go-base-image + description: hwlab-tasktree-worker go-base-image + - name: go-base-image-status + description: hwlab-tasktree-worker go-base-image-status + - name: go-base-digest + description: hwlab-tasktree-worker go-base-digest + - name: go-base-build-duration-ms + description: hwlab-tasktree-worker go-base-build-duration-ms + - name: go-base-buildkit-cache-ref + description: hwlab-tasktree-worker go-base-buildkit-cache-ref + workspaces: + - name: source + volumes: + - name: service-work + emptyDir: + {} + - name: buildkit-bin + emptyDir: + {} + - name: buildkit-run + emptyDir: + {} + sidecars: + - name: buildkitd + image: 127.0.0.1:5000/hwlab/buildkit:rootless + args: + - "--addr" + - unix:///workspace/buildkit-run/buildkitd.sock + - "--oci-worker-no-process-sandbox" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + volumeMounts: + - name: buildkit-run + mountPath: /workspace/buildkit-run + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: true + appArmorProfile: + type: Unconfined + seccompProfile: + type: Unconfined + steps: + - name: prepare-buildkit-client + image: 127.0.0.1:5000/hwlab/buildkit:rootless + securityContext: + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n" + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + - name: publish + image: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: ALL_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: all_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com" + - name: HWLAB_NODE_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_ALL_PROXY_URL + value: http://127.0.0.1:10808 + - name: HWLAB_NODE_NO_PROXY + value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com" + - name: HOME + value: /tekton/home + - name: XDG_CONFIG_HOME + value: /tekton/home/.config + volumeMounts: + - name: service-work + mountPath: /workspace/service-work + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + - name: buildkit-run + mountPath: /workspace/buildkit-run + script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n mkdir -p \"$ci_node_deps_target\"\n for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-cicd-timing\",schemaVersion:\"v1\",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:\"scripts/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node npm git python3 ssh curl; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -s /workspace/source/affected-services.json ] && ! node -e 'const fs=require(\"node:fs\"); const p=JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\",\"utf8\")); const service=process.argv[1]; const item=(p.entries||[]).find((entry)=>entry.serviceId===service); process.exit(item && item.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"v03\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-v03\",\"runtimeRenderDir\":\"runtime-v03\",\"runtimeNamespace\":\"hwlab-v03\",\"environment\":\"development\",\"catalogPath\":\"deploy/artifact-catalog.nc01-v03.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab-dev.hwpod.com\",\"publicApiUrl\":\"https://lab-dev.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-v03-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-v02\",\"secretNamespace\":\"agentrun-v02\",\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-v03-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-v03-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-v03-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-v03-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-v03-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-v03-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"deployYamlGitMirror\":{\"id\":\"nc01-v03\",\"node\":\"NC01\",\"lane\":\"v03\",\"namespace\":\"devops-infra\",\"serviceReadName\":\"git-mirror-http\",\"serviceWriteName\":\"git-mirror-write\",\"cachePvcName\":\"hwlab-git-mirror-cache\",\"cachePvcStorage\":\"20Gi\",\"cacheHostPath\":null,\"servicePort\":8080,\"secretName\":\"git-mirror-github-ssh\",\"syncConfigMapName\":\"git-mirror-sync-script\",\"syncJobPrefix\":\"git-mirror-hwlab-nc01-v03-sync-manual\",\"flushJobPrefix\":\"git-mirror-hwlab-nc01-v03-flush-manual\",\"toolsImage\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"toolsImagePullPolicy\":\"Always\",\"sourceRepository\":\"pikasTech/HWLAB\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"egressProxy\":{\"mode\":\"node-global\",\"required\":true,\"clientName\":\"nc01-host-proxy\",\"namespace\":\"platform-infra\",\"serviceName\":\"nc01-host-proxy\",\"port\":10808,\"proxyUrl\":\"http://10.42.0.1:10808\",\"noProxy\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"kubernetes\",\"kubernetes.default\",\"kubernetes.default.svc\",\"argocd-repo-server\",\"argocd-repo-server.argocd\",\"argocd-redis\",\"argocd-redis.argocd\",\"git-mirror-http\",\"git-mirror-http.devops-infra\",\"git-mirror-write\",\"git-mirror-write.devops-infra\",\"10.0.0.0/8\",\"10.42.0.0/16\",\"10.43.0.0/16\",\"172.16.0.0/12\",\"192.168.0.0/16\",\"152.53.229.148\",\"152.53.229.148\",\"hyueapi.com\",\".hyueapi.com\"]},\"githubTransport\":{\"mode\":\"https\",\"username\":\"x-access-token\",\"tokenSecretName\":\"git-mirror-github-token\",\"tokenSecretKey\":\"GITHUB_TOKEN\",\"tokenSourceRef\":\"/root/.unidesk/.env/gh_token.txt\",\"tokenSourceKey\":\"GH_TOKEN\"}},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n ...(envRecipe.downloadStack || {}),\n httpProxy: overlay.dockerProxyHttp,\n httpsProxy: overlay.dockerProxyHttps,\n noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n ...lane,\n node: overlay.nodeId,\n sourceBranch: overlay.sourceBranch,\n gitopsBranch: overlay.gitopsBranch,\n namespace: overlay.runtimeNamespace,\n endpoint: overlay.publicApiUrl,\n publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n artifactCatalog: overlay.catalogPath,\n runtimePath: overlay.runtimePath,\n imageTagMode: 'full',\n sourceRepo: overlay.gitUrl,\n observability: overlay.observability,\n envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nif (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\nnode scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --registry-prefix \"$(params.registry-prefix)\" --report \"/workspace/source/service-results/$(params.service-id).json\" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services \"$(params.service-id)\" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr \"$HWLAB_BUILDKIT_ADDR\" --build-cache-mode \"$(params.build-cache-mode)\"\n" + imagePullPolicy: Always + params: + - name: revision + value: $(params.revision) + - name: lane + value: $(params.lane) + - name: catalog-path + value: $(params.catalog-path) + - name: image-tag-mode + value: $(params.image-tag-mode) + - name: registry-prefix + value: $(params.registry-prefix) + - name: service-id + value: hwlab-tasktree-worker + - name: base-image + value: $(params.base-image) + - name: build-cache-mode + value: $(params.build-cache-mode) - name: build-hwlab-cloud-web runAfter: - plan-artifacts @@ -2102,6 +2526,8 @@ spec: - build-hwlab-workbench-runtime - build-hwlab-user-billing - build-hwlab-project-management + - build-hwlab-tasktree-api + - build-hwlab-tasktree-worker - build-hwlab-cloud-web - build-hwlab-gateway - build-hwlab-edge-proxy diff --git a/cmd/hwlab-tasktree-api/main.ts b/cmd/hwlab-tasktree-api/main.ts new file mode 100644 index 00000000..1b6468ab --- /dev/null +++ b/cmd/hwlab-tasktree-api/main.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env bun +import { createTaskTreeDispatcher } from "../../internal/tasktree/dispatcher.ts"; +import { createTaskTreeHttpApp } from "../../internal/tasktree/http.ts"; +import { taskTreeRuntime } from "../../internal/tasktree/runtime.ts"; + +const runtime = taskTreeRuntime(); +const dispatch = createTaskTreeDispatcher(runtime); +const app = createTaskTreeHttpApp({ dispatch, close: () => runtime.store.close() }); +const host = process.env.TASKTREE_API_HOST || "0.0.0.0"; +const port = Number.parseInt(process.env.TASKTREE_API_PORT || process.env.PORT || "6673", 10); +const server = Bun.serve({ hostname: host, port, fetch: (request) => app.fetch(request) }); + +process.stdout.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-api", status: "listening", host, port: server.port })}\n`); +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.once(signal, async () => { + server.stop(true); + await app.close(); + process.exit(0); + }); +} diff --git a/cmd/hwlab-tasktree-worker/main.ts b/cmd/hwlab-tasktree-worker/main.ts new file mode 100644 index 00000000..9aa81aed --- /dev/null +++ b/cmd/hwlab-tasktree-worker/main.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env bun +import { NativeConnection, Worker } from "@temporalio/worker"; +import path from "node:path"; + +import { createTaskTreeActivities } from "../../internal/tasktree/activities.ts"; +import { taskTreeRuntime } from "../../internal/tasktree/runtime.ts"; + +const runtime = taskTreeRuntime(); +if (!runtime.temporalAddress) throw new Error("TASKTREE_TEMPORAL_ADDRESS is required"); +await runtime.store.ensureSchema(); +const connection = await NativeConnection.connect({ address: runtime.temporalAddress }); +const worker = await Worker.create({ + connection, + namespace: runtime.temporalNamespace, + taskQueue: runtime.taskQueue, + workflowsPath: path.resolve(import.meta.dir, "../../internal/tasktree/workflows.ts"), + activities: createTaskTreeActivities(runtime.store) +}); +const healthPort = Number.parseInt(process.env.TASKTREE_WORKER_HEALTH_PORT || "6674", 10); +const health = Bun.serve({ + hostname: process.env.TASKTREE_WORKER_HEALTH_HOST || "0.0.0.0", + port: healthPort, + fetch(request) { + const pathname = new URL(request.url).pathname; + if (pathname !== "/health/live" && pathname !== "/health/ready") return Response.json({ ok: false, error: "not_found" }, { status: 404 }); + return Response.json({ ok: true, serviceId: "hwlab-tasktree-worker", namespace: runtime.temporalNamespace, taskQueue: runtime.taskQueue }); + } +}); + +process.stdout.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-worker", status: "started", namespace: runtime.temporalNamespace, taskQueue: runtime.taskQueue })}\n`); +for (const signal of ["SIGINT", "SIGTERM"] as const) process.once(signal, () => worker.shutdown()); +try { + await worker.run(); +} finally { + health.stop(true); + await connection.close(); + await runtime.store.close(); + process.stdout.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-worker", status: "stopped" })}\n`); +} diff --git a/config/hwlab-access-control/nav-profiles.yaml b/config/hwlab-access-control/nav-profiles.yaml index 15ec6180..9c072bef 100644 --- a/config/hwlab-access-control/nav-profiles.yaml +++ b/config/hwlab-access-control/nav-profiles.yaml @@ -14,3 +14,4 @@ spec: allowedIds: - workbench.code - project.mdtodo + - project.tasktree diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml index 2f0a45b2..4c46201b 100644 --- a/deploy/deploy.yaml +++ b/deploy/deploy.yaml @@ -509,6 +509,8 @@ lanes: - hwlab-workbench-runtime - hwlab-user-billing - hwlab-project-management + - hwlab-tasktree-api + - hwlab-tasktree-worker - hwlab-cloud-web - hwlab-gateway - hwlab-edge-proxy @@ -518,6 +520,8 @@ lanes: hwlab-workbench-runtime: deploy/runtime/boot/hwlab-workbench-runtime.sh hwlab-user-billing: deploy/runtime/boot/hwlab-user-billing.sh hwlab-project-management: deploy/runtime/boot/hwlab-project-management.sh + hwlab-tasktree-api: deploy/runtime/boot/hwlab-tasktree-api.sh + hwlab-tasktree-worker: deploy/runtime/boot/hwlab-tasktree-worker.sh hwlab-cloud-web: deploy/runtime/boot/hwlab-cloud-web.sh hwlab-gateway: deploy/runtime/boot/hwlab-gateway.sh hwlab-edge-proxy: deploy/runtime/boot/hwlab-edge-proxy.sh @@ -633,6 +637,37 @@ lanes: env: TZ: Asia/Shanghai observable: true + hwlab-tasktree-api: + runtimeKind: bun-command + entrypoint: cmd/hwlab-tasktree-api/main.ts + artifactKind: bun-command + healthPath: /health/ready + healthPort: 6673 + componentPaths: + - cmd/hwlab-tasktree-api/ + - internal/tasktree/ + - tools/src/tasktree-cli.ts + - package.json + - package-lock.json + - deploy/runtime/boot/hwlab-tasktree-api.sh + env: + TZ: Asia/Shanghai + observable: true + hwlab-tasktree-worker: + runtimeKind: bun-command + entrypoint: cmd/hwlab-tasktree-worker/main.ts + artifactKind: bun-command + healthPath: /health/ready + healthPort: 6674 + componentPaths: + - cmd/hwlab-tasktree-worker/ + - internal/tasktree/ + - package.json + - package-lock.json + - deploy/runtime/boot/hwlab-tasktree-worker.sh + env: + TZ: Asia/Shanghai + observable: true hwlab-cloud-web: runtimeKind: cloud-web entrypoint: web/hwlab-cloud-web/index.html @@ -860,6 +895,7 @@ lanes: HWLAB_USER_BILLING_URL: http://hwlab-user-billing.hwlab-v03.svc.cluster.local:6670 HWLAB_WORKBENCH_RUNTIME_URL: http://hwlab-workbench-runtime.hwlab-v03.svc.cluster.local:6671 HWLAB_PROJECT_MANAGEMENT_URL: http://hwlab-project-management.hwlab-v03.svc.cluster.local:6672 + HWLAB_TASKTREE_URL: http://hwlab-tasktree-api.hwlab-v03.svc.cluster.local:6673 HWLAB_HWPOD_SPEC_REGISTRY_DIRS: /etc/hwlab/hwpod-specs HWLAB_HWPOD_NODE_WS_TOKEN: secretRef:hwlab-v03-hwpod-node-auth/token HWLAB_USER_BILLING_LOGIN_RETRY_ATTEMPTS: "2" @@ -967,6 +1003,24 @@ lanes: configMapName: hwlab-v03-project-management-bootstrap mountPath: /etc/hwlab/project-management readOnly: true + - serviceId: hwlab-tasktree-api + replicas: 1 + env: + TASKTREE_DATABASE_URL: secretRef:hwlab-tasktree-db/database-url + TASKTREE_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233 + TASKTREE_TEMPORAL_NAMESPACE: unidesk + TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-v03-tasktree + TASKTREE_API_PORT: "6673" + OTEL_SERVICE_NAME: hwlab-tasktree-api + - serviceId: hwlab-tasktree-worker + replicas: 1 + env: + TASKTREE_DATABASE_URL: secretRef:hwlab-tasktree-db/database-url + TASKTREE_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233 + TASKTREE_TEMPORAL_NAMESPACE: unidesk + TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-v03-tasktree + TASKTREE_WORKER_HEALTH_PORT: "6674" + OTEL_SERVICE_NAME: hwlab-tasktree-worker production: name: Production node: NC01 @@ -1031,6 +1085,7 @@ lanes: HWLAB_USER_BILLING_URL: http://hwlab-user-billing.hwlab-production.svc.cluster.local:6670 HWLAB_WORKBENCH_RUNTIME_URL: http://hwlab-workbench-runtime.hwlab-production.svc.cluster.local:6671 HWLAB_PROJECT_MANAGEMENT_URL: http://hwlab-project-management.hwlab-production.svc.cluster.local:6672 + HWLAB_TASKTREE_URL: http://hwlab-tasktree-api.hwlab-production.svc.cluster.local:6673 HWLAB_USER_BILLING_LOGIN_RETRY_ATTEMPTS: "2" HWLAB_USER_BILLING_LOGIN_RETRY_DELAY_MS: "250" HWLAB_USER_BILLING_CODE_AGENT_ENABLED: "1" @@ -1083,9 +1138,33 @@ lanes: HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY: secretRef:hwlab-production-master-server-admin-api-key/api-key OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces OTEL_SERVICE_NAME: hwlab-project-management + - serviceId: hwlab-tasktree-api + replicas: 1 + env: + TASKTREE_DATABASE_URL: secretRef:hwlab-tasktree-db/database-url + TASKTREE_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233 + TASKTREE_TEMPORAL_NAMESPACE: unidesk + TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-production-tasktree + TASKTREE_API_PORT: "6673" + OTEL_SERVICE_NAME: hwlab-tasktree-api + - serviceId: hwlab-tasktree-worker + replicas: 1 + env: + TASKTREE_DATABASE_URL: secretRef:hwlab-tasktree-db/database-url + TASKTREE_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233 + TASKTREE_TEMPORAL_NAMESPACE: unidesk + TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-production-tasktree + TASKTREE_WORKER_HEALTH_PORT: "6674" + OTEL_SERVICE_NAME: hwlab-tasktree-worker envRecipe: *v03EnvRecipe bootConfig: *v03BootConfig services: + - serviceId: hwlab-tasktree-api + namespace: hwlab-dev + healthPath: /health/live + - serviceId: hwlab-tasktree-worker + namespace: hwlab-dev + healthPath: /health/live - serviceId: hwlab-cloud-api namespace: hwlab-dev healthPath: /health/live diff --git a/deploy/k8s/base/services.yaml b/deploy/k8s/base/services.yaml index dac826e9..4055ed78 100644 --- a/deploy/k8s/base/services.yaml +++ b/deploy/k8s/base/services.yaml @@ -2,6 +2,34 @@ "apiVersion": "v1", "kind": "List", "items": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "hwlab-tasktree-api", + "namespace": "hwlab-dev", + "labels": { "app.kubernetes.io/name": "hwlab-tasktree-api", "hwlab.pikastech.local/service-id": "hwlab-tasktree-api" } + }, + "spec": { + "type": "ClusterIP", + "selector": { "app.kubernetes.io/name": "hwlab-tasktree-api" }, + "ports": [{ "name": "http", "port": 6673, "targetPort": "http" }] + } + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "hwlab-tasktree-worker", + "namespace": "hwlab-dev", + "labels": { "app.kubernetes.io/name": "hwlab-tasktree-worker", "hwlab.pikastech.local/service-id": "hwlab-tasktree-worker" } + }, + "spec": { + "type": "ClusterIP", + "selector": { "app.kubernetes.io/name": "hwlab-tasktree-worker" }, + "ports": [{ "name": "http", "port": 6674, "targetPort": "http" }] + } + }, { "apiVersion": "v1", "kind": "Service", diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index ec474793..7cb5ad7a 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -2,6 +2,54 @@ "apiVersion": "v1", "kind": "List", "items": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "hwlab-tasktree-api", + "namespace": "hwlab-dev", + "labels": { "app.kubernetes.io/name": "hwlab-tasktree-api", "hwlab.pikastech.local/service-id": "hwlab-tasktree-api" } + }, + "spec": { + "replicas": 0, + "selector": { "matchLabels": { "app.kubernetes.io/name": "hwlab-tasktree-api" } }, + "template": { + "metadata": { "labels": { "app.kubernetes.io/name": "hwlab-tasktree-api", "hwlab.pikastech.local/service-id": "hwlab-tasktree-api" } }, + "spec": { "containers": [{ + "name": "hwlab-tasktree-api", + "image": "127.0.0.1:5000/hwlab/hwlab-tasktree-api:dev", + "ports": [{ "name": "http", "containerPort": 6673 }], + "env": [{ "name": "TASKTREE_API_PORT", "value": "6673" }], + "readinessProbe": { "httpGet": { "path": "/health/ready", "port": "http" } }, + "livenessProbe": { "httpGet": { "path": "/health/live", "port": "http" } } + }] } + } + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "hwlab-tasktree-worker", + "namespace": "hwlab-dev", + "labels": { "app.kubernetes.io/name": "hwlab-tasktree-worker", "hwlab.pikastech.local/service-id": "hwlab-tasktree-worker" } + }, + "spec": { + "replicas": 0, + "selector": { "matchLabels": { "app.kubernetes.io/name": "hwlab-tasktree-worker" } }, + "template": { + "metadata": { "labels": { "app.kubernetes.io/name": "hwlab-tasktree-worker", "hwlab.pikastech.local/service-id": "hwlab-tasktree-worker" } }, + "spec": { "containers": [{ + "name": "hwlab-tasktree-worker", + "image": "127.0.0.1:5000/hwlab/hwlab-tasktree-worker:dev", + "ports": [{ "name": "http", "containerPort": 6674 }], + "env": [{ "name": "TASKTREE_WORKER_HEALTH_PORT", "value": "6674" }], + "readinessProbe": { "httpGet": { "path": "/health/ready", "port": "http" } }, + "livenessProbe": { "httpGet": { "path": "/health/live", "port": "http" } } + }] } + } + } + }, { "apiVersion": "v1", "kind": "PersistentVolumeClaim", diff --git a/deploy/runtime/boot/hwlab-tasktree-api.sh b/deploy/runtime/boot/hwlab-tasktree-api.sh new file mode 100755 index 00000000..91470d2d --- /dev/null +++ b/deploy/runtime/boot/hwlab-tasktree-api.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu + +export HWLAB_SERVICE_ID=hwlab-tasktree-api +export HWLAB_SERVICE_ENTRYPOINT=cmd/hwlab-tasktree-api/main.ts +export TASKTREE_API_PORT="${TASKTREE_API_PORT:-6673}" +export PORT="$TASKTREE_API_PORT" + +exec "${HWLAB_BUN_COMMAND:-bun}" run cmd/hwlab-tasktree-api/main.ts diff --git a/deploy/runtime/boot/hwlab-tasktree-worker.sh b/deploy/runtime/boot/hwlab-tasktree-worker.sh new file mode 100755 index 00000000..48493e6c --- /dev/null +++ b/deploy/runtime/boot/hwlab-tasktree-worker.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu + +export HWLAB_SERVICE_ID=hwlab-tasktree-worker +export HWLAB_SERVICE_ENTRYPOINT=cmd/hwlab-tasktree-worker/main.ts +export TASKTREE_WORKER_HEALTH_PORT="${TASKTREE_WORKER_HEALTH_PORT:-6674}" +export PORT="$TASKTREE_WORKER_HEALTH_PORT" + +exec "${HWLAB_BUN_COMMAND:-bun}" run cmd/hwlab-tasktree-worker/main.ts diff --git a/gitops/reuse.ymal b/gitops/reuse.ymal index 02c0fd1e..ecd107d1 100644 --- a/gitops/reuse.ymal +++ b/gitops/reuse.ymal @@ -23,6 +23,8 @@ spec: services: - hwlab-cloud-api - hwlab-project-management + - hwlab-tasktree-api + - hwlab-tasktree-worker - hwlab-cloud-web - hwlab-gateway - hwlab-edge-proxy @@ -138,6 +140,53 @@ spec: - package-lock.json - deploy/runtime/launcher/hwlab-env-reuse-launcher.ts - internal/dev-entrypoint/artifact-runtime.mjs + hwlab-tasktree-api: + runtimeReuse: + enabled: true + codeIdentity: + paths: + - cmd/hwlab-tasktree-api + - internal/tasktree + - tools/src/tasktree-cli.ts + - deploy/runtime/boot/hwlab-tasktree-api.sh + envIdentity: + paths: + - package.json + - package-lock.json + - deploy/runtime/launcher/hwlab-env-reuse-launcher.ts + - internal/dev-entrypoint/artifact-runtime.mjs + envReuse: + enabled: true + mode: env-reuse-git-mirror-checkout + nodeDepsPath: /opt/hwlab-env/node_modules + envIdentityFiles: + - package.json + - package-lock.json + - deploy/runtime/launcher/hwlab-env-reuse-launcher.ts + - internal/dev-entrypoint/artifact-runtime.mjs + hwlab-tasktree-worker: + runtimeReuse: + enabled: true + codeIdentity: + paths: + - cmd/hwlab-tasktree-worker + - internal/tasktree + - deploy/runtime/boot/hwlab-tasktree-worker.sh + envIdentity: + paths: + - package.json + - package-lock.json + - deploy/runtime/launcher/hwlab-env-reuse-launcher.ts + - internal/dev-entrypoint/artifact-runtime.mjs + envReuse: + enabled: true + mode: env-reuse-git-mirror-checkout + nodeDepsPath: /opt/hwlab-env/node_modules + envIdentityFiles: + - package.json + - package-lock.json + - deploy/runtime/launcher/hwlab-env-reuse-launcher.ts + - internal/dev-entrypoint/artifact-runtime.mjs hwlab-cloud-web: runtimeReuse: enabled: true diff --git a/internal/cloud/server.ts b/internal/cloud/server.ts index c4d0f4af..8114ed70 100644 --- a/internal/cloud/server.ts +++ b/internal/cloud/server.ts @@ -95,6 +95,7 @@ import { import { HWPOD_TOPOLOGY_CONTRACT_VERSION } from "./hwpod-topology-read-model.ts"; import { handleCaseRunHttp } from "./server-caserun-http.ts"; import { handleProjectManagementProxyHttp } from "./project-management-proxy.ts"; +import { handleTaskTreeProxyHttp } from "./tasktree-proxy.ts"; import { handleDashboardSummaryHttp } from "./server-dashboard-http.ts"; import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts"; @@ -718,6 +719,10 @@ async function handleRestAdapter(request, response, url, options) { await handleProjectManagementProxyHttp(request, response, url, options); return; } + if (url.pathname === "/v1/tasktree" || url.pathname.startsWith("/v1/tasktree/")) { + await handleTaskTreeProxyHttp(request, response, url, options); + return; + } if (url.pathname === "/v1/users/me/profile" && request.method === "PATCH") { await handleUserBillingProfileHttp(request, response, options); @@ -1014,6 +1019,7 @@ function navIdForRestPath(pathname, method = "GET") { if (pathname.startsWith("/v1/admin/billing/")) return "admin.billing"; if (pathname === "/v1/skills" || pathname.startsWith("/v1/skills/")) return "system.skills"; if (pathname === "/v1/project-management" || pathname.startsWith("/v1/project-management/")) return "project.mdtodo"; + if (pathname === "/v1/tasktree" || pathname.startsWith("/v1/tasktree/")) return "project.tasktree"; if (pathname === "/v1/api-keys" || pathname === "/v1/api-keys/default" || pathname.startsWith("/v1/api-keys/")) return "user.apiKeys"; if (pathname === "/v1/users/me/profile" || pathname === "/v1/users/me/password") return "system.settings"; if (pathname === "/v1/workbench/debug/fake-sse" || pathname.startsWith("/v1/workbench/debug/fake-sse/") || pathname === "/v1/workbench/debug/kafka-sse" || pathname.startsWith("/v1/workbench/debug/kafka-sse/")) return "workbench.debug"; diff --git a/internal/cloud/tasktree-proxy.test.ts b/internal/cloud/tasktree-proxy.test.ts new file mode 100644 index 00000000..f897a748 --- /dev/null +++ b/internal/cloud/tasktree-proxy.test.ts @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { handleTaskTreeProxyHttp } from "./tasktree-proxy.ts"; + +test("TaskTree proxy requires auth and forwards the unchanged API path", async () => { + const originalFetch = globalThis.fetch; + let upstreamUrl = ""; + globalThis.fetch = (async (input) => { + upstreamUrl = String(input); + return Response.json({ ok: true, operation: "group.list", data: { groups: [] } }); + }) as typeof fetch; + const response = captureResponse(); + try { + await handleTaskTreeProxyHttp( + { method: "GET", headers: {} }, + response, + new URL("http://cloud.test/v1/tasktree/groups"), + { + env: { HWLAB_TASKTREE_URL: "http://hwlab-tasktree-api.hwlab-v03.svc.cluster.local:6673" }, + accessController: { async authenticate() { return { ok: true, actor: { id: "usr_test" } }; } } + } + ); + assert.equal(upstreamUrl, "http://hwlab-tasktree-api.hwlab-v03.svc.cluster.local:6673/v1/tasktree/groups"); + assert.equal(response.status, 200); + assert.deepEqual(JSON.parse(response.body), { ok: true, operation: "group.list", data: { groups: [] } }); + } finally { + globalThis.fetch = originalFetch; + } +}); + +function captureResponse() { + return { + status: 0, + body: "", + writeHead(status: number) { this.status = status; }, + end(body: string) { this.body = body; } + }; +} diff --git a/internal/cloud/tasktree-proxy.ts b/internal/cloud/tasktree-proxy.ts new file mode 100644 index 00000000..8853bec4 --- /dev/null +++ b/internal/cloud/tasktree-proxy.ts @@ -0,0 +1,22 @@ +import { readBody, sendJson } from "./server-http-utils.ts"; + +export async function handleTaskTreeProxyHttp(request, response, url, options = {}) { + const env = options.env ?? process.env; + const baseUrl = String(env.HWLAB_TASKTREE_URL ?? "").trim(); + if (!baseUrl) return sendJson(response, 503, { ok: false, error: { code: "tasktree_service_unconfigured", message: "HWLAB_TASKTREE_URL is required" }, valuesRedacted: true }); + const auth = await options.accessController.authenticate(request, { required: true }); + if (!auth.ok) return sendJson(response, auth.status, auth); + try { + const upstreamUrl = new URL(`${url.pathname}${url.search}`, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`); + const headers = new Headers({ accept: "application/json", "x-hwlab-bridge-service": "hwlab-cloud-api" }); + const contentType = request.headers["content-type"]; + if (contentType) headers.set("content-type", Array.isArray(contentType) ? contentType[0] : contentType); + if (auth.actor?.id) headers.set("x-hwlab-actor-id", auth.actor.id); + const body = request.method === "GET" || request.method === "HEAD" ? undefined : await readBody(request, options.bodyLimitBytes); + const upstream = await fetch(upstreamUrl, { method: request.method, headers, body, signal: AbortSignal.timeout(Number(env.HWLAB_TASKTREE_PROXY_TIMEOUT_MS || 30000)) }); + response.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") || "application/json; charset=utf-8", "cache-control": "no-store", "x-hwlab-proxy-service": "hwlab-tasktree-api" }); + response.end(await upstream.text() || "\n"); + } catch (error) { + sendJson(response, 502, { ok: false, error: { code: "tasktree_proxy_failed", message: error?.message ?? String(error) }, valuesRedacted: true }); + } +} diff --git a/internal/tasktree/activities.ts b/internal/tasktree/activities.ts new file mode 100644 index 00000000..5999bbb8 --- /dev/null +++ b/internal/tasktree/activities.ts @@ -0,0 +1,13 @@ +import { TaskTreeStore } from "./store.ts"; + +export function createTaskTreeActivities(store: TaskTreeStore) { + return { + async recordTaskExecution(input: { taskId: string }) { + const task = await store.getTask(input.taskId); + if (!task) throw Object.assign(new Error("task was not found"), { code: "task_not_found" }); + const { report } = await store.writeReport({ taskId: task.id, title: "Temporal execution report", body: `Task ${task.title} completed by Temporal worker.`, status: "succeeded" }); + await store.completeTask(task.id); + return { taskId: task.id, reportId: report.id }; + } + }; +} diff --git a/internal/tasktree/contracts.ts b/internal/tasktree/contracts.ts new file mode 100644 index 00000000..e7d16887 --- /dev/null +++ b/internal/tasktree/contracts.ts @@ -0,0 +1,92 @@ +export type TaskStatus = "pending" | "in_progress" | "completed" | "blocked"; +export type TaskKind = "task" | "subtask" | "subsubtask"; + +export type TaskGroup = { + id: string; + name: string; + description: string; + createdAt: string; + updatedAt: string; +}; + +export type TaskItem = { + id: string; + groupId: string; + parentId: string | null; + kind: TaskKind; + title: string; + description: string; + status: TaskStatus; + startAt: string | null; + dueAt: string | null; + sortOrder: number; + createdAt: string; + updatedAt: string; +}; + +export type Milestone = { + id: string; + groupId: string; + taskId: string | null; + title: string; + occursAt: string; + createdAt: string; +}; + +export type ExecutionReport = { + id: string; + taskId: string; + title: string; + body: string; + status: string; + createdAt: string; +}; + +export type Timeline = { + group: TaskGroup; + tasks: TaskItem[]; + milestones: Milestone[]; + reports: ExecutionReport[]; +}; + +export type TaskGroupOverview = { + group: TaskGroup; + taskCount: number; + subtaskCount: number; + subsubtaskCount: number; + reportCount: number; + startAt: string | null; + dueAt: string | null; +}; + +export type TaskTreeCommand = + | { operation: "health" } + | { operation: "group.list" } + | { operation: "group.overview" } + | { operation: "group.get"; groupId: string } + | { operation: "group.create"; name: string; description?: string } + | { operation: "group.delete"; groupId: string } + | { operation: "group.stats"; groupId: string } + | { operation: "group.import-markdown"; sourcePath: string; sourceModifiedAt: string; markdown: string; reports: Array<{ href: string; sourcePath: string; body: string; modifiedAt: string }>; groupName?: string; dryRun?: boolean } + | { operation: "task.list"; groupId: string } + | { operation: "task.get"; taskId: string } + | { operation: "task.create"; groupId: string; title: string; description?: string; startAt?: string; dueAt?: string; parentId?: string } + | { operation: "task.create-batch"; groupId: string; titles: string[]; parentId?: string; description?: string; startAt?: string; dueAt?: string } + | { operation: "task.update"; taskId: string; title?: string; description?: string; status?: TaskStatus; startAt?: string | null; dueAt?: string | null } + | { operation: "task.delete"; taskId: string } + | { operation: "task.start"; taskId: string } + | { operation: "task.complete"; taskId: string } + | { operation: "milestone.create"; groupId: string; title: string; occursAt: string; taskId?: string } + | { operation: "report.list"; taskId: string } + | { operation: "report.get"; reportId: string } + | { operation: "report.write"; taskId: string; title: string; body: string; status?: string } + | { operation: "report.create"; taskId: string; title: string; body: string; status?: string } + | { operation: "timeline.get"; groupId: string } + | { operation: "workflow.start"; taskId: string }; + +export type TaskTreeResult = { + ok: boolean; + operation: TaskTreeCommand["operation"]; + data?: unknown; + error?: { code: string; message: string; detail?: unknown }; +}; diff --git a/internal/tasktree/dispatcher.ts b/internal/tasktree/dispatcher.ts new file mode 100644 index 00000000..25bc7e17 --- /dev/null +++ b/internal/tasktree/dispatcher.ts @@ -0,0 +1,150 @@ +import { Client, Connection } from "@temporalio/client"; + +import type { TaskItem, TaskTreeCommand, TaskTreeResult } from "./contracts.ts"; +import { parseMdtodoImport } from "./mdtodo-import.ts"; +import { TaskTreeStore } from "./store.ts"; + +export type TaskTreeDispatcherOptions = { + store: TaskTreeStore; + temporalAddress?: string; + temporalNamespace?: string; + taskQueue?: string; +}; + +export function createTaskTreeDispatcher(options: TaskTreeDispatcherOptions) { + const { store } = options; + return async function dispatch(command: TaskTreeCommand): Promise { + try { + const operation = command.operation; + let data: unknown; + if (operation === "health") data = await store.health(); + else if (operation === "group.list") data = { groups: await store.listGroups() }; + else if (operation === "group.overview") data = { groups: await store.groupOverview() }; + else if (operation === "group.get") data = required(await store.getGroup(command.groupId), "group_not_found", "taskgroup was not found"); + else if (operation === "group.create") data = await store.createGroup(requiredText(command.name, "name"), command.description); + else if (operation === "group.delete") data = { + deleted: requiredMutation(await store.deleteGroup(command.groupId), "group_not_found", "taskgroup was not found"), + mutation: true + }; + else if (operation === "group.stats") { + required(await store.getGroup(command.groupId), "group_not_found", "taskgroup was not found"); + data = await store.groupStats(command.groupId); + } + else if (operation === "group.import-markdown") { + const plan = parseMdtodoImport(command); + data = command.dryRun ? { dryRun: true, ...plan, tasks: undefined } : await store.importMdtodo(plan); + } + else if (operation === "task.list") { + required(await store.getGroup(command.groupId), "group_not_found", "taskgroup was not found"); + data = { tasks: taskTree(await store.listTasks(command.groupId)) }; + } + else if (operation === "task.get") { + const task = required(await store.getTask(command.taskId), "task_not_found", "task was not found"); + const tasks = await store.listTasks(task.groupId); + data = { ...taskNode(task, tasks), reports: await store.listReports(task.id) }; + } + else if (operation === "task.create") { + const startAt = optionalDate(command.startAt, "startAt"); + const dueAt = optionalDate(command.dueAt, "dueAt"); + validTimeRange(startAt, dueAt); + data = await store.createTask({ + ...command, + title: validTitle(command.title), + startAt, + dueAt + }); + } + else if (operation === "task.create-batch") { + const titles = command.titles.map(validTitle); + if (titles.length < 1) throw codedError("invalid_input", "at least one title is required"); + const startAt = optionalDate(command.startAt, "startAt"); + const dueAt = optionalDate(command.dueAt, "dueAt"); + validTimeRange(startAt, dueAt); + data = { tasks: await store.createTasks({ + ...command, + titles, + startAt, + dueAt + }) }; + } + else if (operation === "task.update") { + if (command.status === "completed") throw codedError("completion_command_required", "use task complete so the execution report requirement is enforced"); + data = required(await store.updateTask(command.taskId, { + ...command, + title: command.title === undefined ? undefined : validTitle(command.title), + status: command.status === undefined ? undefined : validStatus(command.status), + startAt: nullableDate(command.startAt, "startAt"), + dueAt: nullableDate(command.dueAt, "dueAt") + }), "task_not_found", "task was not found"); + } + else if (operation === "task.delete") data = { + deleted: requiredMutation(await store.deleteTask(command.taskId), "task_not_found", "task was not found"), + mutation: true + }; + else if (operation === "task.start") data = required(await store.updateTask(command.taskId, { status: "in_progress" }), "task_not_found", "task was not found"); + else if (operation === "task.complete") data = required(await store.completeTask(command.taskId), "task_not_found", "task was not found"); + else if (operation === "milestone.create") data = await store.createMilestone({ ...command, title: requiredText(command.title, "title"), occursAt: validDate(command.occursAt, "occursAt") }); + else if (operation === "report.list") { + required(await store.getTask(command.taskId), "task_not_found", "task was not found"); + data = { reports: await store.listReports(command.taskId) }; + } + else if (operation === "report.get") data = required(await store.getReport(command.reportId), "report_not_found", "execution report was not found"); + else if (operation === "report.write") data = await store.writeReport({ ...command, title: requiredText(command.title, "title"), body: requiredText(command.body, "body") }); + else if (operation === "report.create") data = await store.createReport({ ...command, title: requiredText(command.title, "title"), body: requiredText(command.body, "body") }); + else if (operation === "timeline.get") data = required(await store.timeline(command.groupId), "group_not_found", "taskgroup was not found"); + else if (operation === "workflow.start") data = await startTaskWorkflow(options, command.taskId); + else throw codedError("unsupported_operation", `unsupported operation: ${String(operation)}`); + return { ok: true, operation, data }; + } catch (error: any) { + return { ok: false, operation: command.operation, error: { code: error?.code ?? "tasktree_error", message: error?.message ?? String(error) } }; + } + }; +} + +async function startTaskWorkflow(options: TaskTreeDispatcherOptions, taskId: string) { + if (!options.temporalAddress) throw codedError("temporal_address_required", "Temporal address is required for workflow.start"); + const connection = await Connection.connect({ address: options.temporalAddress }); + try { + const client = new Client({ connection, namespace: options.temporalNamespace ?? "unidesk" }); + const workflowId = `tasktree-${taskId}-${Date.now()}`; + const handle = await client.workflow.start("taskExecutionWorkflow", { taskQueue: options.taskQueue ?? "hwlab-v03-tasktree", workflowId, args: [{ taskId }] }); + return { workflowId, runId: handle.firstExecutionRunId, taskId, result: await handle.result() }; + } finally { + await connection.close(); + } +} + +function required(value: T | null, code: string, message: string): T { if (value === null) throw codedError(code, message); return value; } +function requiredMutation(value: boolean, code: string, message: string): true { if (!value) throw codedError(code, message); return true; } +function requiredText(value: unknown, field: string): string { const text = String(value ?? "").trim(); if (!text) throw codedError("invalid_input", `${field} is required`); return text; } +function validTitle(value: unknown): string { + const title = requiredText(value, "title"); + if (/[\r\n\u0000]/u.test(title)) throw codedError("invalid_title", "title must be non-empty single-line text"); + return title; +} +function validStatus(value: unknown): "pending" | "in_progress" | "completed" | "blocked" { + if (value === "pending" || value === "in_progress" || value === "completed" || value === "blocked") return value; + throw codedError("invalid_status", "status must be pending, in_progress, completed, or blocked"); +} +function optionalDate(value: string | undefined, field: string): string | undefined { return value === undefined ? undefined : validDate(value, field); } +function nullableDate(value: string | null | undefined, field: string): string | null | undefined { return value === null || value === undefined ? value : validDate(value, field); } +function validDate(value: unknown, field: string): string { const date = new Date(String(value ?? "")); if (Number.isNaN(date.valueOf())) throw codedError("invalid_input", `${field} must be an ISO date`); return date.toISOString(); } +function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); } +function validTimeRange(startAt?: string, dueAt?: string) { + if (startAt !== undefined && dueAt !== undefined && new Date(startAt).valueOf() > new Date(dueAt).valueOf()) { + throw codedError("invalid_time_range", "startAt must be before or equal to dueAt"); + } +} + +type TaskNode = TaskItem & { children: TaskNode[] }; + +function taskTree(tasks: TaskItem[]): TaskNode[] { + return tasks.filter((task) => task.parentId === null).map((task) => taskNode(task, tasks)); +} + +function taskNode(task: TaskItem, tasks: TaskItem[]): TaskNode { + return { + ...task, + children: tasks.filter((candidate) => candidate.parentId === task.id).map((child) => taskNode(child, tasks)) + }; +} diff --git a/internal/tasktree/http.ts b/internal/tasktree/http.ts new file mode 100644 index 00000000..225bfc63 --- /dev/null +++ b/internal/tasktree/http.ts @@ -0,0 +1,36 @@ +import type { TaskTreeCommand } from "./contracts.ts"; + +export function createTaskTreeHttpApp(options: { dispatch: (command: TaskTreeCommand) => Promise; close?: () => Promise }) { + return { + async fetch(request: Request): Promise { + const url = new URL(request.url); + if (url.pathname === "/health/live") return json(200, { ok: true, serviceId: "hwlab-tasktree-api", status: "live" }); + if (url.pathname === "/health/ready") return resultResponse(await options.dispatch({ operation: "health" })); + if (url.pathname === "/v1/tasktree/groups" && request.method === "GET") return resultResponse(await options.dispatch({ operation: "group.list" })); + if (url.pathname === "/v1/tasktree/overview" && request.method === "GET") return resultResponse(await options.dispatch({ operation: "group.overview" })); + if (url.pathname === "/v1/tasktree/groups" && request.method === "POST") { + const body = await bodyObject(request); + return resultResponse(await options.dispatch({ operation: "group.create", name: text(body.name), description: text(body.description) }), 201); + } + const timeline = /^\/v1\/tasktree\/groups\/([^/]+)\/timeline$/u.exec(url.pathname); + if (timeline && request.method === "GET") return resultResponse(await options.dispatch({ operation: "timeline.get", groupId: decodeURIComponent(timeline[1]) })); + if (url.pathname === "/v1/tasktree/commands" && request.method === "POST") { + return resultResponse(await options.dispatch(await bodyObject(request) as TaskTreeCommand)); + } + return json(404, { ok: false, error: { code: "not_found", message: "TaskTree route was not found" } }); + }, + close: options.close ?? (async () => {}) + }; +} + +function resultResponse(result: any, successStatus = 200) { + const status = result?.ok === true ? successStatus : result?.error?.code?.includes("not_found") ? 404 : 400; + return json(status, result); +} +async function bodyObject(request: Request): Promise> { + const body = await request.json().catch(() => null); + if (!body || typeof body !== "object" || Array.isArray(body)) throw Object.assign(new Error("JSON object body is required"), { code: "invalid_json" }); + return body as Record; +} +function text(value: unknown) { return String(value ?? ""); } +function json(status: number, body: unknown) { return Response.json(body, { status, headers: { "cache-control": "no-store" } }); } diff --git a/internal/tasktree/mdtodo-import.test.ts b/internal/tasktree/mdtodo-import.test.ts new file mode 100644 index 00000000..c82f563c --- /dev/null +++ b/internal/tasktree/mdtodo-import.test.ts @@ -0,0 +1,72 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { parseMdtodoImport } from "./mdtodo-import.ts"; + +test("MDTODO import preserves three Rxx levels and attaches reports", () => { + const plan = parseMdtodoImport({ + sourcePath: "/tmp/demo/MDTODO.md", + sourceModifiedAt: "2026-07-15T12:00:00.000Z", + markdown: `# Demo\n\n## R1 Root [in_progress]\n\nSee [任务报告](./details/R1_Task_Report.md).\n\n### R1.1 Child [completed]\n\n#### R1.1.1 Deep child [blocked]\n`, + reports: [{ href: "./details/R1_Task_Report.md", sourcePath: "/tmp/demo/details/R1_Task_Report.md", body: "# R1 report\n\nPassed.", modifiedAt: "2026-07-17T09:30:00.000Z" }] + }); + + assert.equal(plan.groupName, "Demo"); + assert.deepEqual(plan.summary, { taskCount: 1, subtaskCount: 1, subsubtaskCount: 1, reportCount: 1, missingReportCount: 0, flattenedTaskCount: 0, warningCount: 0 }); + assert.equal(plan.tasks[0]?.status, "in_progress"); + assert.equal(plan.tasks[1]?.status, "completed"); + assert.equal(plan.tasks[2]?.status, "blocked"); + assert.equal(plan.tasks[2]?.parentSourceId, "R1.1"); + assert.equal(plan.tasks[0]?.reports[0]?.body, "# R1 report\n\nPassed."); + assert.equal(plan.tasks[0]?.startAt, "2026-07-14T12:00:00.000Z"); + assert.equal(plan.tasks[0]?.dueAt, "2026-07-17T09:30:00.000Z"); + assert.equal(plan.tasks[1]?.startAt, "2026-07-14T12:00:00.000Z"); + assert.equal(plan.tasks[1]?.dueAt, "2026-07-15T12:00:00.000Z"); +}); + +test("MDTODO import flattens only levels deeper than subsubtask", () => { + const plan = parseMdtodoImport({ + sourcePath: "/tmp/demo/MDTODO.md", + sourceModifiedAt: "2026-07-15T12:00:00.000Z", + markdown: "# Demo\n\n## R2 Root\n\n### R2.9 Child\n\n#### R2.9.1 Grandchild\n\n##### R2.9.1.1 Deeper\n", + reports: [] + }); + assert.equal(plan.tasks[2]?.parentSourceId, "R2.9"); + assert.equal(plan.tasks[3]?.parentSourceId, "R2.9"); + assert.equal(plan.summary.subsubtaskCount, 2); + assert.equal(plan.summary.flattenedTaskCount, 1); +}); + +test("MDTODO import reports missing linked report files", () => { + const plan = parseMdtodoImport({ + sourcePath: "/tmp/demo/MDTODO.md", + sourceModifiedAt: "2026-07-15T12:00:00.000Z", + markdown: "# Demo\n\n## R1 Root\n\n[Task report](./R1_Task_Report.md)\n", + reports: [] + }); + assert.equal(plan.summary.missingReportCount, 1); + assert.equal(plan.warnings[0]?.code, "report_file_missing"); +}); + +test("MDTODO import expands a parent range to contain report-dated children", () => { + const plan = parseMdtodoImport({ + sourcePath: "/tmp/demo/MDTODO.md", + sourceModifiedAt: "2026-07-15T12:00:00.000Z", + markdown: `# Demo\n\n## R1 Root\n\n### R1.1 Child\n\nSee [Task report](./R1.1_Task_Report.md).\n`, + reports: [{ href: "./R1.1_Task_Report.md", sourcePath: "/tmp/demo/R1.1_Task_Report.md", body: "# Child report", modifiedAt: "2026-07-17T09:30:00.000Z" }] + }); + assert.equal(plan.tasks[1]?.startAt, "2026-07-16T09:30:00.000Z"); + assert.equal(plan.tasks[1]?.dueAt, "2026-07-17T09:30:00.000Z"); + assert.equal(plan.tasks[0]?.startAt, "2026-07-14T12:00:00.000Z"); + assert.equal(plan.tasks[0]?.dueAt, "2026-07-17T09:30:00.000Z"); +}); + +test("MDTODO import never truncates title content at URL colons", () => { + const plan = parseMdtodoImport({ + sourcePath: "/tmp/demo/MDTODO.md", + sourceModifiedAt: "2026-07-15T12:00:00.000Z", + markdown: "# Demo\n\n## R1\n\n完成 [UniDesk #2361](https://github.com/pikasTech/unidesk/issues/2361) 并保留后续文本。\n", + reports: [] + }); + assert.equal(plan.tasks[0]?.title, "R1 完成 [UniDesk #2361](https://github.com/pikasTech/unidesk/issues/2361) 并保留后续文本。"); +}); diff --git a/internal/tasktree/mdtodo-import.ts b/internal/tasktree/mdtodo-import.ts new file mode 100644 index 00000000..4e54528b --- /dev/null +++ b/internal/tasktree/mdtodo-import.ts @@ -0,0 +1,245 @@ +import { readFile, stat } from "node:fs/promises"; +import path from "node:path"; + +import type { TaskStatus } from "./contracts.ts"; + +export type MdtodoReportSource = { href: string; sourcePath: string; body: string; modifiedAt: string }; +export type MdtodoImportPayload = { + sourcePath: string; + sourceModifiedAt: string; + markdown: string; + reports: MdtodoReportSource[]; + groupName?: string; + dryRun?: boolean; +}; + +export type MdtodoImportTask = { + sourceId: string; + parentSourceId: string | null; + title: string; + description: string; + status: TaskStatus; + startAt: string; + dueAt: string; + reports: Array<{ title: string; body: string; status: string; createdAt: string }>; +}; + +export type MdtodoImportPlan = { + sourcePath: string; + groupName: string; + groupDescription: string; + parserMode: "rxx" | "legacy-checkbox"; + tasks: MdtodoImportTask[]; + summary: { + taskCount: number; + subtaskCount: number; + subsubtaskCount: number; + reportCount: number; + missingReportCount: number; + flattenedTaskCount: number; + warningCount: number; + }; + warnings: Array<{ code: string; message: string; sourceId?: string; href?: string }>; +}; + +const rxxHeadingPattern = /^(#{2,})\s+(R\d+(?:\.\d+)*)(?:\s+(.*?))?\s*$/iu; +const checkboxPattern = /^(\s*)(?:[-*+]|\d+[.)])\s+\[([ xX-])\]\s+(.*)$/u; +const markdownLinkPattern = /\[([^\]]*)\]\(([^)]+)\)/gu; + +export async function loadMdtodoImportPayload(filePath: string, options: { groupName?: string; dryRun?: boolean } = {}): Promise { + const sourcePath = path.resolve(filePath); + const [markdown, sourceInfo] = await Promise.all([readFile(sourcePath, "utf8"), stat(sourcePath)]); + const reports: MdtodoReportSource[] = []; + const seen = new Set(); + for (const link of reportLinks(markdown)) { + const reportPath = path.resolve(path.dirname(sourcePath), link.href); + if (seen.has(reportPath)) continue; + seen.add(reportPath); + try { + const [body, reportInfo] = await Promise.all([readFile(reportPath, "utf8"), stat(reportPath)]); + reports.push({ href: link.href, sourcePath: reportPath, body, modifiedAt: reportInfo.mtime.toISOString() }); + } catch { + // Missing report files are reported by the pure parser so dry-run and import agree. + } + } + return { sourcePath, sourceModifiedAt: sourceInfo.mtime.toISOString(), markdown, reports, groupName: options.groupName, dryRun: options.dryRun }; +} + +export function parseMdtodoImport(payload: MdtodoImportPayload): MdtodoImportPlan { + const lines = String(payload.markdown ?? "").split(/\r?\n/u); + const rxxBlocks = collectRxxBlocks(lines); + const parsed = rxxBlocks.length ? parseRxx(lines, rxxBlocks, payload) : parseCheckboxes(lines, payload); + if (!parsed.tasks.length) throw codedError("mdtodo_tasks_not_found", "MDTODO file does not contain Rxx headings or checkbox tasks"); + const reportCount = parsed.tasks.reduce((count, task) => count + task.reports.length, 0); + const missingReportCount = parsed.warnings.filter((warning) => warning.code === "report_file_missing").length; + const flattenedTaskCount = parsed.warnings.filter((warning) => warning.code === "nested_task_flattened").length; + const tasksBySourceId = new Map(parsed.tasks.map((task) => [task.sourceId, task])); + return { + sourcePath: payload.sourcePath, + groupName: cleanTitle(payload.groupName || documentTitle(lines, payload.sourcePath)), + groupDescription: `Imported from MDTODO: ${payload.sourcePath}`, + parserMode: rxxBlocks.length ? "rxx" : "legacy-checkbox", + tasks: parsed.tasks, + summary: { + taskCount: parsed.tasks.filter((task) => !task.parentSourceId).length, + subtaskCount: parsed.tasks.filter((task) => taskHierarchyDepth(task, tasksBySourceId) === 1).length, + subsubtaskCount: parsed.tasks.filter((task) => taskHierarchyDepth(task, tasksBySourceId) === 2).length, + reportCount, + missingReportCount, + flattenedTaskCount, + warningCount: parsed.warnings.length + }, + warnings: parsed.warnings.slice(0, 100) + }; +} + +function parseRxx(lines: string[], blocks: RxxBlock[], payload: MdtodoImportPayload) { + const tasks: MdtodoImportTask[] = []; + const warnings: MdtodoImportPlan["warnings"] = []; + const sourceIds = new Set(blocks.map((block) => block.sourceId)); + for (const block of blocks) { + const depth = rxxDepth(block.sourceId); + const supportedSourceId = depth > 2 ? block.sourceId.split(".").slice(0, 3).join(".") : block.sourceId; + const parentSourceId = nearestRxxParent(supportedSourceId, sourceIds); + if (depth > 2) warnings.push({ code: "nested_task_flattened", sourceId: block.sourceId, message: `${block.sourceId} was flattened to the supported third level under ${parentSourceId ?? "the taskgroup"}` }); + if (depth > 0 && !parentSourceId) warnings.push({ code: "parent_task_missing", sourceId: block.sourceId, message: `${block.sourceId} has no importable parent task and was imported as a task` }); + const description = lines.slice(block.lineIndex + 1, block.endLine).join("\n").trim(); + const reports = reportsForTask(description, payload, block.sourceId, warnings); + tasks.push({ + sourceId: block.sourceId, + parentSourceId, + title: cleanTitle(`${block.sourceId} ${block.title || conciseBodyTitle(description)}`), + description, + status: block.status, + ...importedTaskRange(payload.sourceModifiedAt, reports), + reports + }); + } + expandParentRanges(tasks); + return { tasks, warnings }; +} + +function parseCheckboxes(lines: string[], payload: MdtodoImportPayload) { + const tasks: MdtodoImportTask[] = []; + const warnings: MdtodoImportPlan["warnings"] = []; + const parentsByDepth: string[] = []; + for (let index = 0; index < lines.length; index += 1) { + const match = checkboxPattern.exec(lines[index]); + if (!match) continue; + const depth = Math.max(0, Math.floor(match[1].replace(/\t/gu, " ").length / 2)); + const sourceId = `L${index + 1}`; + const supportedDepth = Math.min(depth, 2); + const parentSourceId = supportedDepth > 0 ? parentsByDepth[supportedDepth - 1] ?? null : null; + if (depth > 2) warnings.push({ code: "nested_task_flattened", sourceId, message: `${sourceId} was flattened to the supported third checkbox level` }); + tasks.push({ + sourceId, + parentSourceId, + title: cleanTitle(match[3]), + description: "", + status: match[2].toLowerCase() === "x" ? "completed" : match[2] === "-" ? "blocked" : "pending", + ...importedTaskRange(payload.sourceModifiedAt, []), + reports: [] + }); + parentsByDepth[supportedDepth] = sourceId; + parentsByDepth.length = supportedDepth + 1; + } + expandParentRanges(tasks); + return { tasks, warnings }; +} + +function reportsForTask(body: string, payload: MdtodoImportPayload, sourceId: string, warnings: MdtodoImportPlan["warnings"]) { + const reports: MdtodoImportTask["reports"] = []; + for (const link of reportLinks(body)) { + const resolved = path.resolve(path.dirname(payload.sourcePath), link.href); + const report = payload.reports.find((candidate) => path.resolve(candidate.sourcePath) === resolved); + if (!report) { + warnings.push({ code: "report_file_missing", sourceId, href: link.href, message: `Report file could not be read: ${link.href}` }); + continue; + } + reports.push({ title: documentTitle(report.body.split(/\r?\n/u), report.sourcePath), body: report.body, status: "imported", createdAt: requiredIsoDate(report.modifiedAt, "report.modifiedAt") }); + } + return reports; +} + +function importedTaskRange(sourceModifiedAt: string, reports: MdtodoImportTask["reports"]) { + const sourceTime = new Date(requiredIsoDate(sourceModifiedAt, "sourceModifiedAt")).getTime(); + const reportTimes = reports.map((report) => new Date(report.createdAt).getTime()); + const dueTime = reportTimes.length ? Math.max(...reportTimes) : sourceTime; + return { startAt: new Date(dueTime - 86400000).toISOString(), dueAt: new Date(dueTime).toISOString() }; +} + +function expandParentRanges(tasks: MdtodoImportTask[]) { + const tasksBySourceId = new Map(tasks.map((task) => [task.sourceId, task])); + const deepestFirst = [...tasks].sort((left, right) => taskHierarchyDepth(right, tasksBySourceId) - taskHierarchyDepth(left, tasksBySourceId)); + for (const child of deepestFirst) { + if (!child.parentSourceId) continue; + const parent = tasksBySourceId.get(child.parentSourceId); + if (!parent) continue; + const startAt = Math.min(new Date(parent.startAt).getTime(), new Date(child.startAt).getTime()); + const dueAt = Math.max(new Date(parent.dueAt).getTime(), new Date(child.dueAt).getTime()); + parent.startAt = new Date(startAt).toISOString(); + parent.dueAt = new Date(dueAt).toISOString(); + } +} + +function nearestRxxParent(sourceId: string, sourceIds: Set) { + const parts = sourceId.split("."); + while (parts.length > 1) { + parts.pop(); + const candidate = parts.join("."); + if (sourceIds.has(candidate)) return candidate; + } + return null; +} + +function taskHierarchyDepth(task: MdtodoImportTask, tasksBySourceId: Map) { + let depth = 0; + let parentSourceId = task.parentSourceId; + while (parentSourceId && depth < 3) { + depth += 1; + parentSourceId = tasksBySourceId.get(parentSourceId)?.parentSourceId ?? null; + } + return depth; +} + +function reportLinks(markdown: string) { + const links: Array<{ label: string; href: string }> = []; + for (const match of String(markdown ?? "").matchAll(markdownLinkPattern)) { + const label = String(match[1] ?? "").trim(); + const href = String(match[2] ?? "").trim().replace(/^<|>$/gu, "").split(/\s+/u)[0] ?? ""; + if (!href || !/\.md(?:#.*)?$/iu.test(href) || /^(?:[a-z][a-z0-9+.-]*:|\/|[A-Za-z]:\/)/iu.test(href)) continue; + if (!/(?:任务报告|task[_ -]?report|report)/iu.test(`${label} ${href}`)) continue; + links.push({ label, href: href.split("#")[0] }); + } + return links; +} + +type RxxBlock = { sourceId: string; title: string; status: TaskStatus; lineIndex: number; endLine: number }; +function collectRxxBlocks(lines: string[]): RxxBlock[] { + const blocks: Omit[] = []; + for (let index = 0; index < lines.length; index += 1) { + const match = rxxHeadingPattern.exec(lines[index]); + if (!match) continue; + const rest = String(match[3] ?? "").trim(); + blocks.push({ sourceId: match[2].replace(/^r/iu, "R"), title: stripStatus(rest), status: statusFrom(rest), lineIndex: index }); + } + return blocks.map((block, index) => ({ ...block, endLine: blocks[index + 1]?.lineIndex ?? lines.length })); +} + +function statusFrom(value: string): TaskStatus { + const markers = [...value.matchAll(/\[([^\]]+)\]/gu)].map((match) => String(match[1]).trim().toLowerCase().replace(/[\s-]+/gu, "_")); + if (markers.some((marker) => marker === "blocked")) return "blocked"; + if (markers.some((marker) => ["completed", "finished", "done"].includes(marker))) return "completed"; + if (markers.some((marker) => ["in_progress", "processing"].includes(marker))) return "in_progress"; + return "pending"; +} +function stripStatus(value: string) { return value.replace(/\s*\[(?:open|todo|blocked|in[_ -]?progress|processing|completed|finished|done)\]\s*/giu, " ").trim(); } +function rxxDepth(sourceId: string) { return Math.max(0, sourceId.split(".").length - 1); } +function documentTitle(lines: string[], sourcePath: string) { return cleanTitle(lines.find((line) => /^#{1,6}\s+\S/u.test(line))?.replace(/^#{1,6}\s+/u, "") || path.basename(sourcePath, path.extname(sourcePath))); } +function firstBodyLine(body: string) { return body.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || "Untitled task"; } +function conciseBodyTitle(body: string) { + return firstBodyLine(body); +} +function cleanTitle(value: string) { return String(value ?? "").replace(/\s+\s*$/u, "").trim().slice(0, 500) || "Untitled task"; } +function requiredIsoDate(value: unknown, field: string) { const date = new Date(String(value ?? "")); if (Number.isNaN(date.valueOf())) throw codedError("invalid_mdtodo_file_time", `${field} must be an ISO date`); return date.toISOString(); } +function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); } diff --git a/internal/tasktree/runtime.ts b/internal/tasktree/runtime.ts new file mode 100644 index 00000000..744081f9 --- /dev/null +++ b/internal/tasktree/runtime.ts @@ -0,0 +1,13 @@ +import { TaskTreeStore } from "./store.ts"; + +export function taskTreeRuntime(env: Record = process.env) { + const databaseUrl = env.TASKTREE_DATABASE_URL || env.DATABASE_URL || ""; + if (!databaseUrl) throw Object.assign(new Error("TASKTREE_DATABASE_URL or DATABASE_URL is required"), { code: "missing_database_url" }); + const store = new TaskTreeStore(databaseUrl); + return { + store, + temporalAddress: env.TASKTREE_TEMPORAL_ADDRESS || env.TEMPORAL_ADDRESS || "", + temporalNamespace: env.TASKTREE_TEMPORAL_NAMESPACE || "unidesk", + taskQueue: env.TASKTREE_TEMPORAL_TASK_QUEUE || "hwlab-v03-tasktree" + }; +} diff --git a/internal/tasktree/store.ts b/internal/tasktree/store.ts new file mode 100644 index 00000000..7c6222c1 --- /dev/null +++ b/internal/tasktree/store.ts @@ -0,0 +1,384 @@ +import { randomUUID } from "node:crypto"; +import pg from "pg"; + +import type { ExecutionReport, Milestone, TaskGroup, TaskGroupOverview, TaskItem, TaskStatus, Timeline } from "./contracts.ts"; +import type { MdtodoImportPlan } from "./mdtodo-import.ts"; + +const { Pool } = pg; + +const schema = [ + `CREATE TABLE IF NOT EXISTS tasktree_schema_migrations (migration_id TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())`, + `CREATE TABLE IF NOT EXISTS tasktree_groups ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now())`, + `CREATE TABLE IF NOT EXISTS tasktree_tasks ( + id TEXT PRIMARY KEY, group_id TEXT NOT NULL REFERENCES tasktree_groups(id) ON DELETE CASCADE, + parent_id TEXT REFERENCES tasktree_tasks(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('task','subtask','subsubtask')), + title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending','in_progress','completed','blocked')), + start_at TIMESTAMPTZ, due_at TIMESTAMPTZ, sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CHECK (due_at IS NULL OR start_at IS NULL OR due_at >= start_at))`, + `CREATE TABLE IF NOT EXISTS tasktree_milestones ( + id TEXT PRIMARY KEY, group_id TEXT NOT NULL REFERENCES tasktree_groups(id) ON DELETE CASCADE, + task_id TEXT REFERENCES tasktree_tasks(id) ON DELETE CASCADE, title TEXT NOT NULL, + occurs_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`, + `CREATE TABLE IF NOT EXISTS tasktree_execution_reports ( + id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasktree_tasks(id) ON DELETE CASCADE, + title TEXT NOT NULL, body TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'succeeded', + created_at TIMESTAMPTZ NOT NULL DEFAULT now())`, + `CREATE INDEX IF NOT EXISTS idx_tasktree_tasks_group_order ON tasktree_tasks(group_id, sort_order, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_tasktree_tasks_parent ON tasktree_tasks(parent_id)`, + `CREATE INDEX IF NOT EXISTS idx_tasktree_reports_task ON tasktree_execution_reports(task_id, created_at DESC)` + ,`ALTER TABLE tasktree_tasks DROP CONSTRAINT IF EXISTS tasktree_tasks_kind_check` + ,`ALTER TABLE tasktree_tasks ADD CONSTRAINT tasktree_tasks_kind_check CHECK (kind IN ('task','subtask','subsubtask'))` +]; + +export class TaskTreeStore { + private readonly pool: pg.Pool; + private ready = false; + + constructor(databaseUrl: string) { + if (!databaseUrl) throw new Error("TASKTREE_DATABASE_URL or DATABASE_URL is required"); + this.pool = new Pool({ connectionString: databaseUrl, max: 4, connectionTimeoutMillis: 5000, ssl: { rejectUnauthorized: false } }); + } + + async ensureSchema() { + if (this.ready) return; + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + for (const statement of schema) await client.query(statement); + await client.query("INSERT INTO tasktree_schema_migrations (migration_id) VALUES ($1),($2) ON CONFLICT DO NOTHING", ["tasktree-20260716-v1", "tasktree-20260717-v2-subsubtask"]); + await client.query("COMMIT"); + this.ready = true; + } catch (error) { + await client.query("ROLLBACK").catch(() => {}); + throw error; + } finally { + client.release(); + } + } + + async health() { + await this.ensureSchema(); + const result = await this.pool.query("SELECT current_database() AS database, now() AS now"); + return { storage: "native-postgresql", schemaReady: true, database: result.rows[0]?.database, checkedAt: result.rows[0]?.now }; + } + + async listGroups(): Promise { + await this.ensureSchema(); + const result = await this.pool.query("SELECT * FROM tasktree_groups ORDER BY updated_at DESC, name"); + return result.rows.map(groupRow); + } + + async groupOverview(): Promise { + await this.ensureSchema(); + const result = await this.pool.query(` + SELECT g.*, + COUNT(DISTINCT t.id) FILTER (WHERE t.kind='task')::int AS task_count, + COUNT(DISTINCT t.id) FILTER (WHERE t.kind='subtask')::int AS subtask_count, + COUNT(DISTINCT t.id) FILTER (WHERE t.kind='subsubtask')::int AS subsubtask_count, + COUNT(DISTINCT r.id)::int AS report_count, + MIN(t.start_at) AS start_at, + MAX(t.due_at) AS due_at + FROM tasktree_groups g + LEFT JOIN tasktree_tasks t ON t.group_id=g.id + LEFT JOIN tasktree_execution_reports r ON r.task_id=t.id + GROUP BY g.id + ORDER BY g.updated_at DESC, g.name + `); + return result.rows.map(overviewRow); + } + + async getGroup(id: string): Promise { + await this.ensureSchema(); + const result = await this.pool.query("SELECT * FROM tasktree_groups WHERE id=$1", [id]); + return result.rows[0] ? groupRow(result.rows[0]) : null; + } + + async createGroup(name: string, description = ""): Promise { + await this.ensureSchema(); + const result = await this.pool.query("INSERT INTO tasktree_groups (id,name,description) VALUES ($1,$2,$3) RETURNING *", [`tg_${randomUUID()}`, name, description]); + return groupRow(result.rows[0]); + } + + async deleteGroup(id: string): Promise { + await this.ensureSchema(); + return (await this.pool.query("DELETE FROM tasktree_groups WHERE id=$1", [id])).rowCount === 1; + } + + async groupStats(groupId: string) { + await this.ensureSchema(); + const result = await this.pool.query(` + SELECT COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE status='completed')::int AS completed, + COUNT(*) FILTER (WHERE status='in_progress')::int AS in_progress, + COUNT(*) FILTER (WHERE status='pending')::int AS pending, + COUNT(*) FILTER (WHERE status='blocked')::int AS blocked, + COUNT(*) FILTER (WHERE kind='task')::int AS tasks, + COUNT(*) FILTER (WHERE kind='subtask')::int AS subtasks, + COUNT(*) FILTER (WHERE kind='subsubtask')::int AS subsubtasks, + (SELECT COUNT(*)::int FROM tasktree_execution_reports r + JOIN tasktree_tasks rt ON rt.id=r.task_id WHERE rt.group_id=$1) AS reports + FROM tasktree_tasks WHERE group_id=$1 + `, [groupId]); + const row = result.rows[0] ?? {}; + return { + total: Number(row.total), + completed: Number(row.completed), + inProgress: Number(row.in_progress), + pending: Number(row.pending), + blocked: Number(row.blocked), + tasks: Number(row.tasks), + subtasks: Number(row.subtasks), + subsubtasks: Number(row.subsubtasks), + reports: Number(row.reports) + }; + } + + async listTasks(groupId: string): Promise { + await this.ensureSchema(); + const result = await this.pool.query("SELECT * FROM tasktree_tasks WHERE group_id=$1 ORDER BY sort_order,created_at", [groupId]); + return result.rows.map(taskRow); + } + + async getTask(id: string): Promise { + await this.ensureSchema(); + const result = await this.pool.query("SELECT * FROM tasktree_tasks WHERE id=$1", [id]); + return result.rows[0] ? taskRow(result.rows[0]) : null; + } + + async createTask(input: { groupId: string; title: string; description?: string; startAt?: string; dueAt?: string; parentId?: string }): Promise { + return (await this.createTasks({ ...input, titles: [input.title] }))[0]; + } + + async createTasks(input: { groupId: string; titles: string[]; description?: string; startAt?: string; dueAt?: string; parentId?: string }): Promise { + await this.ensureSchema(); + ensureTimeRange(input.startAt ?? null, input.dueAt ?? null); + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const group = await client.query("SELECT id FROM tasktree_groups WHERE id=$1 FOR UPDATE", [input.groupId]); + if (!group.rows[0]) throw domainError("group_not_found", "taskgroup was not found"); + let kind: TaskItem["kind"] = "task"; + if (input.parentId) { + const parent = await client.query("SELECT group_id,kind FROM tasktree_tasks WHERE id=$1", [input.parentId]); + if (!parent.rows[0]) throw domainError("parent_not_found", "parent task was not found"); + if (parent.rows[0].group_id !== input.groupId) throw domainError("parent_group_mismatch", "parent task belongs to another taskgroup"); + if (parent.rows[0].kind === "subsubtask") throw domainError("maximum_depth_exceeded", "TaskTree supports task, subtask, and subsubtask only"); + kind = parent.rows[0].kind === "task" ? "subtask" : "subsubtask"; + } + const orderResult = await client.query("SELECT COALESCE(MAX(sort_order),-1)::int AS value FROM tasktree_tasks WHERE group_id=$1", [input.groupId]); + const firstOrder = Number(orderResult.rows[0]?.value ?? -1) + 1; + const tasks: TaskItem[] = []; + for (let index = 0; index < input.titles.length; index += 1) { + const result = await client.query( + `INSERT INTO tasktree_tasks (id,group_id,parent_id,kind,title,description,start_at,due_at,sort_order) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`, + [`tt_${randomUUID()}`, input.groupId, input.parentId ?? null, kind, input.titles[index], input.description ?? "", input.startAt ?? null, input.dueAt ?? null, firstOrder + index] + ); + tasks.push(taskRow(result.rows[0])); + } + await client.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [input.groupId]); + await client.query("COMMIT"); + return tasks; + } catch (error) { + await client.query("ROLLBACK").catch(() => {}); + throw error; + } finally { + client.release(); + } + } + + async updateTask(id: string, patch: { title?: string; description?: string; status?: TaskStatus; startAt?: string | null; dueAt?: string | null }): Promise { + await this.ensureSchema(); + const current = await this.pool.query("SELECT * FROM tasktree_tasks WHERE id=$1", [id]); + if (!current.rows[0]) return null; + const row = taskRow(current.rows[0]); + const startAt = patch.startAt === undefined ? row.startAt : patch.startAt; + const dueAt = patch.dueAt === undefined ? row.dueAt : patch.dueAt; + ensureTimeRange(startAt, dueAt); + const result = await this.pool.query( + `UPDATE tasktree_tasks SET title=$2,description=$3,status=$4,start_at=$5,due_at=$6,updated_at=now() WHERE id=$1 RETURNING *`, + [id, patch.title ?? row.title, patch.description ?? row.description, patch.status ?? row.status, + startAt, dueAt] + ); + await this.pool.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [row.groupId]); + return taskRow(result.rows[0]); + } + + async deleteTask(id: string): Promise { + await this.ensureSchema(); + const current = await this.getTask(id); + if (!current) return false; + const deleted = (await this.pool.query("DELETE FROM tasktree_tasks WHERE id=$1", [id])).rowCount === 1; + if (deleted) await this.pool.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [current.groupId]); + return deleted; + } + + async completeTask(id: string): Promise { + await this.ensureSchema(); + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const current = await client.query("SELECT * FROM tasktree_tasks WHERE id=$1 FOR UPDATE", [id]); + if (!current.rows[0]) { + await client.query("ROLLBACK"); + return null; + } + const reports = await client.query("SELECT COUNT(*)::int AS count FROM tasktree_execution_reports WHERE task_id=$1", [id]); + if (Number(reports.rows[0]?.count ?? 0) < 1) throw domainError("execution_report_required", "an execution report is required before completing a task"); + const result = await client.query("UPDATE tasktree_tasks SET status='completed',updated_at=now() WHERE id=$1 RETURNING *", [id]); + await client.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [result.rows[0].group_id]); + await client.query("COMMIT"); + return taskRow(result.rows[0]); + } catch (error) { + await client.query("ROLLBACK").catch(() => {}); + throw error; + } finally { + client.release(); + } + } + + async createMilestone(input: { groupId: string; title: string; occursAt: string; taskId?: string }): Promise { + await this.ensureSchema(); + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const group = await client.query("SELECT id FROM tasktree_groups WHERE id=$1 FOR UPDATE", [input.groupId]); + if (!group.rows[0]) throw domainError("group_not_found", "taskgroup was not found"); + if (input.taskId) { + const task = await client.query("SELECT group_id FROM tasktree_tasks WHERE id=$1", [input.taskId]); + if (!task.rows[0]) throw domainError("task_not_found", "task was not found"); + if (task.rows[0].group_id !== input.groupId) throw domainError("task_group_mismatch", "task belongs to another taskgroup"); + } + const result = await client.query( + "INSERT INTO tasktree_milestones (id,group_id,task_id,title,occurs_at) VALUES ($1,$2,$3,$4,$5) RETURNING *", + [`tm_${randomUUID()}`, input.groupId, input.taskId ?? null, input.title, input.occursAt] + ); + await client.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [input.groupId]); + await client.query("COMMIT"); + return milestoneRow(result.rows[0]); + } catch (error) { + await client.query("ROLLBACK").catch(() => {}); + throw error; + } finally { + client.release(); + } + } + + async createReport(input: { taskId: string; title: string; body: string; status?: string }): Promise { + await this.ensureSchema(); + const result = await this.pool.query( + "INSERT INTO tasktree_execution_reports (id,task_id,title,body,status) VALUES ($1,$2,$3,$4,$5) RETURNING *", + [`tr_${randomUUID()}`, input.taskId, input.title, input.body, input.status ?? "succeeded"] + ); + return reportRow(result.rows[0]); + } + + async listReports(taskId: string): Promise { + await this.ensureSchema(); + const result = await this.pool.query("SELECT * FROM tasktree_execution_reports WHERE task_id=$1 ORDER BY created_at DESC,id", [taskId]); + return result.rows.map(reportRow); + } + + async getReport(reportId: string): Promise { + await this.ensureSchema(); + const result = await this.pool.query("SELECT * FROM tasktree_execution_reports WHERE id=$1", [reportId]); + return result.rows[0] ? reportRow(result.rows[0]) : null; + } + + async writeReport(input: { taskId: string; title: string; body: string; status?: string }): Promise<{ report: ExecutionReport; mutation: boolean }> { + await this.ensureSchema(); + const task = await this.getTask(input.taskId); + if (!task) throw domainError("task_not_found", "task was not found"); + const current = await this.pool.query( + "SELECT * FROM tasktree_execution_reports WHERE task_id=$1 AND title=$2 ORDER BY created_at DESC,id LIMIT 1", + [input.taskId, input.title] + ); + const status = input.status ?? "succeeded"; + if (current.rows[0] && current.rows[0].body === input.body && current.rows[0].status === status) { + return { report: reportRow(current.rows[0]), mutation: false }; + } + const result = current.rows[0] + ? await this.pool.query( + "UPDATE tasktree_execution_reports SET body=$2,status=$3,created_at=now() WHERE id=$1 RETURNING *", + [current.rows[0].id, input.body, status] + ) + : await this.pool.query( + "INSERT INTO tasktree_execution_reports (id,task_id,title,body,status) VALUES ($1,$2,$3,$4,$5) RETURNING *", + [`tr_${randomUUID()}`, input.taskId, input.title, input.body, status] + ); + await this.pool.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [task.groupId]); + return { report: reportRow(result.rows[0]), mutation: true }; + } + + async importMdtodo(plan: MdtodoImportPlan) { + await this.ensureSchema(); + const client = await this.pool.connect(); + const groupId = `tg_${randomUUID()}`; + const taskIds = new Map(); + try { + await client.query("BEGIN"); + const groupResult = await client.query( + "INSERT INTO tasktree_groups (id,name,description) VALUES ($1,$2,$3) RETURNING *", + [groupId, plan.groupName, plan.groupDescription] + ); + for (let index = 0; index < plan.tasks.length; index += 1) { + const task = plan.tasks[index]; + const taskId = `tt_${randomUUID()}`; + taskIds.set(task.sourceId, taskId); + const parentId = task.parentSourceId ? taskIds.get(task.parentSourceId) ?? null : null; + const parentTask = parentId ? plan.tasks.find((candidate) => candidate.sourceId === task.parentSourceId) : null; + const kind = !parentId ? "task" : parentTask?.parentSourceId ? "subsubtask" : "subtask"; + await client.query( + `INSERT INTO tasktree_tasks (id,group_id,parent_id,kind,title,description,status,start_at,due_at,sort_order) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, + [taskId, groupId, parentId, kind, task.title, task.description, task.status, task.startAt, task.dueAt, index] + ); + for (const report of task.reports) { + await client.query( + "INSERT INTO tasktree_execution_reports (id,task_id,title,body,status,created_at) VALUES ($1,$2,$3,$4,$5,$6)", + [`tr_${randomUUID()}`, taskId, report.title, report.body, report.status, report.createdAt] + ); + } + } + await client.query("COMMIT"); + return { dryRun: false, sourcePath: plan.sourcePath, group: groupRow(groupResult.rows[0]), parserMode: plan.parserMode, summary: plan.summary, warnings: plan.warnings }; + } catch (error) { + await client.query("ROLLBACK").catch(() => {}); + throw error; + } finally { + client.release(); + } + } + + async timeline(groupId: string): Promise { + await this.ensureSchema(); + const group = await this.getGroup(groupId); + if (!group) return null; + const [tasks, milestones, reports] = await Promise.all([ + this.pool.query("SELECT * FROM tasktree_tasks WHERE group_id=$1 ORDER BY sort_order,created_at", [groupId]), + this.pool.query("SELECT * FROM tasktree_milestones WHERE group_id=$1 ORDER BY occurs_at", [groupId]), + this.pool.query("SELECT r.* FROM tasktree_execution_reports r JOIN tasktree_tasks t ON t.id=r.task_id WHERE t.group_id=$1 ORDER BY r.created_at DESC", [groupId]) + ]); + return { group, tasks: tasks.rows.map(taskRow), milestones: milestones.rows.map(milestoneRow), reports: reports.rows.map(reportRow) }; + } + + async close() { await this.pool.end(); } +} + +function groupRow(row: any): TaskGroup { return { id: row.id, name: row.name, description: row.description, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) }; } +function overviewRow(row: any): TaskGroupOverview { return { group: groupRow(row), taskCount: Number(row.task_count), subtaskCount: Number(row.subtask_count), subsubtaskCount: Number(row.subsubtask_count), reportCount: Number(row.report_count), startAt: nullableIso(row.start_at), dueAt: nullableIso(row.due_at) }; } +function taskRow(row: any): TaskItem { return { id: row.id, groupId: row.group_id, parentId: row.parent_id, kind: row.kind, title: row.title, description: row.description, status: row.status, startAt: nullableIso(row.start_at), dueAt: nullableIso(row.due_at), sortOrder: row.sort_order, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) }; } +function milestoneRow(row: any): Milestone { return { id: row.id, groupId: row.group_id, taskId: row.task_id, title: row.title, occursAt: iso(row.occurs_at), createdAt: iso(row.created_at) }; } +function reportRow(row: any): ExecutionReport { return { id: row.id, taskId: row.task_id, title: row.title, body: row.body, status: row.status, createdAt: iso(row.created_at) }; } +function iso(value: unknown): string { return new Date(value as any).toISOString(); } +function nullableIso(value: unknown): string | null { return value == null ? null : iso(value); } +function domainError(code: string, message: string) { return Object.assign(new Error(message), { code }); } +function ensureTimeRange(startAt: string | null, dueAt: string | null) { + if (startAt !== null && dueAt !== null && new Date(startAt).valueOf() > new Date(dueAt).valueOf()) { + throw domainError("invalid_time_range", "startAt must be before or equal to dueAt"); + } +} diff --git a/internal/tasktree/tasktree.test.ts b/internal/tasktree/tasktree.test.ts new file mode 100644 index 00000000..7009412b --- /dev/null +++ b/internal/tasktree/tasktree.test.ts @@ -0,0 +1,168 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { createTaskTreeDispatcher } from "./dispatcher.ts"; +import { createTaskTreeHttpApp } from "./http.ts"; +import { TaskTreeStore } from "./store.ts"; + +test("HTTP command endpoint returns the same dispatcher DTO", async () => { + const group = { + id: "tg_contract", + name: "Contract", + description: "", + createdAt: "2026-07-16T00:00:00.000Z", + updatedAt: "2026-07-16T00:00:00.000Z" + }; + const store = { + async listGroups() { return [group]; } + }; + const dispatch = createTaskTreeDispatcher({ store: store as TaskTreeStore }); + const expected = await dispatch({ operation: "group.list" }); + const app = createTaskTreeHttpApp({ dispatch }); + const response = await app.fetch(new Request("http://tasktree/v1/tasktree/commands", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ operation: "group.list" }) + })); + + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), expected); +}); + +test("dispatcher keeps batch validation atomic and completion gated", async () => { + let batchCalls = 0; + let createCalls = 0; + let updateCalls = 0; + const store = { + async createTask() { createCalls += 1; return null; }, + async createTasks() { batchCalls += 1; return []; }, + async updateTask() { updateCalls += 1; return null; }, + async deleteTask() { return false; } + }; + const dispatch = createTaskTreeDispatcher({ store: store as TaskTreeStore }); + + const batch = await dispatch({ operation: "task.create-batch", groupId: "tg_1", titles: ["valid", "invalid\nline"] }); + assert.equal(batch.ok, false); + assert.equal(batch.error?.code, "invalid_title"); + assert.equal(batchCalls, 0); + + const invalidRange = await dispatch({ + operation: "task.create", + groupId: "tg_1", + title: "Invalid range", + startAt: "2026-07-19T00:00:00.000Z", + dueAt: "2026-07-18T00:00:00.000Z" + }); + assert.equal(invalidRange.ok, false); + assert.equal(invalidRange.error?.code, "invalid_time_range"); + assert.equal(createCalls, 0); + + const bypass = await dispatch({ operation: "task.update", taskId: "tt_1", status: "completed" }); + assert.equal(bypass.ok, false); + assert.equal(bypass.error?.code, "completion_command_required"); + assert.equal(updateCalls, 0); + + const missing = await dispatch({ operation: "task.delete", taskId: "tt_missing" }); + assert.equal(missing.ok, false); + assert.equal(missing.error?.code, "task_not_found"); + + const workflow = await dispatch({ operation: "workflow.start", taskId: "tt_1" }); + assert.equal(workflow.ok, false); + assert.equal(workflow.error?.code, "temporal_address_required"); +}); + +test("native PostgreSQL store exposes overview and three task levels", async () => { + const databaseUrl = process.env.TASKTREE_DATABASE_URL; + assert.ok(databaseUrl, "TASKTREE_DATABASE_URL is required for the TaskTree store integration test"); + const store = new TaskTreeStore(databaseUrl); + const group = await store.createGroup(`TaskTree test ${Date.now()}`); + const otherGroup = await store.createGroup(`TaskTree cross-group test ${Date.now()}`); + try { + const task = await store.createTask({ + groupId: group.id, + title: "Root task", + startAt: "2026-07-16T00:00:00.000Z", + dueAt: "2026-07-18T00:00:00.000Z" + }); + const subtask = await store.createTask({ groupId: group.id, parentId: task.id, title: "Subtask" }); + const subsubtask = await store.createTask({ groupId: group.id, parentId: subtask.id, title: "Subsubtask" }); + const batch = await store.createTasks({ groupId: group.id, parentId: task.id, titles: ["Batch one", "Batch two"] }); + assert.deepEqual(batch.map((item) => item.title), ["Batch one", "Batch two"]); + await store.createMilestone({ groupId: group.id, taskId: task.id, title: "Review", occursAt: "2026-07-17T00:00:00.000Z" }); + await assert.rejects( + store.createMilestone({ groupId: otherGroup.id, taskId: task.id, title: "Wrong group", occursAt: "2026-07-17T00:00:00.000Z" }), + (error: any) => error?.code === "task_group_mismatch" + ); + await assert.rejects( + store.createMilestone({ groupId: "tg_missing", title: "Missing group", occursAt: "2026-07-17T00:00:00.000Z" }), + (error: any) => error?.code === "group_not_found" + ); + await assert.rejects( + store.createMilestone({ groupId: group.id, taskId: "tt_missing", title: "Missing task", occursAt: "2026-07-17T00:00:00.000Z" }), + (error: any) => error?.code === "task_not_found" + ); + await assert.rejects( + store.createTask({ + groupId: group.id, + title: "Invalid range", + startAt: "2026-07-19T00:00:00.000Z", + dueAt: "2026-07-18T00:00:00.000Z" + }), + (error: any) => error?.code === "invalid_time_range" + ); + await assert.rejects( + store.updateTask(task.id, { dueAt: "2026-07-15T00:00:00.000Z" }), + (error: any) => error?.code === "invalid_time_range" + ); + assert.equal((await store.getTask(task.id))?.dueAt, "2026-07-18T00:00:00.000Z"); + await assert.rejects( + store.completeTask(task.id), + (error: any) => error?.code === "execution_report_required" + ); + const firstReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" }); + const repeatedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" }); + const changedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed with evidence" }); + assert.equal(firstReport.mutation, true); + assert.equal(repeatedReport.mutation, false); + assert.equal(changedReport.mutation, true); + assert.equal(firstReport.report.id, repeatedReport.report.id); + assert.equal(firstReport.report.id, changedReport.report.id); + assert.equal((await store.completeTask(task.id))?.status, "completed"); + + await assert.rejects( + store.createTask({ groupId: group.id, parentId: subsubtask.id, title: "Unsupported fourth level" }), + (error: any) => error?.code === "maximum_depth_exceeded" + ); + const countBeforeRejectedBatch = (await store.listTasks(group.id)).length; + await assert.rejects( + store.createTasks({ groupId: group.id, parentId: subsubtask.id, titles: ["Unsupported one", "Unsupported two"] }), + (error: any) => error?.code === "maximum_depth_exceeded" + ); + assert.equal((await store.listTasks(group.id)).length, countBeforeRejectedBatch); + const timeline = await store.timeline(group.id); + assert.equal(timeline?.tasks.length, 5); + assert.equal(timeline?.tasks[0]?.kind, "task"); + assert.equal(timeline?.tasks[1]?.kind, "subtask"); + assert.equal(timeline?.tasks[2]?.kind, "subsubtask"); + assert.equal(timeline?.milestones.length, 1); + assert.equal(timeline?.reports.length, 1); + assert.equal((await store.getTask(subtask.id))?.title, "Subtask"); + assert.equal((await store.listReports(task.id)).length, 1); + assert.equal((await store.getReport(firstReport.report.id))?.body, "passed with evidence"); + const stats = await store.groupStats(group.id); + assert.equal(stats.total, 5); + assert.equal(stats.completed, 1); + assert.equal(stats.reports, 1); + const overview = (await store.groupOverview()).find((item) => item.group.id === group.id); + assert.equal(overview?.taskCount, 1); + assert.equal(overview?.subtaskCount, 3); + assert.equal(overview?.subsubtaskCount, 1); + assert.equal(overview?.reportCount, 1); + assert.equal(overview?.startAt, "2026-07-16T00:00:00.000Z"); + assert.equal(overview?.dueAt, "2026-07-18T00:00:00.000Z"); + } finally { + await store.deleteGroup(otherGroup.id); + await store.deleteGroup(group.id); + await store.close(); + } +}); diff --git a/internal/tasktree/workflows.ts b/internal/tasktree/workflows.ts new file mode 100644 index 00000000..0c9c4444 --- /dev/null +++ b/internal/tasktree/workflows.ts @@ -0,0 +1,11 @@ +import { proxyActivities } from "@temporalio/workflow"; + +const { recordTaskExecution } = proxyActivities<{ recordTaskExecution(input: { taskId: string }): Promise<{ taskId: string; reportId: string }> }>({ + startToCloseTimeout: "30 seconds", + retry: { maximumAttempts: 3 } +}); + +export async function taskExecutionWorkflow(input: { taskId: string }) { + const result = await recordTaskExecution(input); + return { status: "completed", ...result }; +} diff --git a/package-lock.json b/package-lock.json index 74b3c094..079638ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,490 @@ "version": "0.0.0-l0", "dependencies": { "@openai/codex": "^0.128.0", + "@swc/wasm": "^1.15.43", + "@temporalio/activity": "^1.20.3", + "@temporalio/client": "^1.20.3", + "@temporalio/worker": "^1.20.3", + "@temporalio/workflow": "^1.20.3", "fzstd": "0.1.1", "kafkajs": "^2.2.4", + "loader-utils": "^3.3.1", "pg": "^8.21.0", "playwright": "1.59.1", "yaml": "^2.8.3" }, "devDependencies": { "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.64.0.tgz", + "integrity": "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.64.0.tgz", + "integrity": "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.64.0.tgz", + "integrity": "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.64.0.tgz", + "integrity": "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.64.0.tgz", + "integrity": "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.64.0.tgz", + "integrity": "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.64.0.tgz", + "integrity": "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.64.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz", + "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/@openai/codex": { @@ -141,6 +617,897 @@ "node": ">=16" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==" + }, + "node_modules/@swc/core": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@swc/wasm": { + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.15.43.tgz", + "integrity": "sha512-jYqeckrzZGAU+9OSfmL15MWfhkKWRCC8QMssL/MZu/MaIP76mU3VHjzqxlwKIz9DOSR/9jCqi1+QlWE0ILNehA==" + }, + "node_modules/@temporalio/activity": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/activity/-/activity-1.20.3.tgz", + "integrity": "sha512-7jtIkCkUe6wwVrnlCd/7DmnVcu1/r4KFvVHvPVszd/EXAz1ziYfsjhenLfxWIvyxSIU9v3tC96uHODN3ABdv4A==", + "dependencies": { + "@temporalio/client": "1.20.3", + "@temporalio/common": "1.20.3" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/client": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.20.3.tgz", + "integrity": "sha512-RmEX0Z7h3mWq8PMqeDSbbDZPK8IweajGnRsghiJI0fnaWx61YeW8bhAAKfD/iSop+0PM0oe0KaW5XPAKEY4XEw==", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.20.3", + "@temporalio/proto": "1.20.3", + "abort-controller": "^3.0.0", + "long": "^5.2.3", + "nexus-rpc": "^0.0.2", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/common": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.20.3.tgz", + "integrity": "sha512-VM9GyUAa7tl413nM24mcBhUfUeqcGazmDkx/GfDA0c+KuEVusclMg9eqa63EDv/rguC/YhWaXu6q5Kl8+99GSw==", + "dependencies": { + "@temporalio/proto": "1.20.3", + "long": "^5.2.3", + "ms": "3.0.0-canary.1", + "nexus-rpc": "^0.0.2", + "proto3-json-serializer": "^2.0.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/core-bridge": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.20.3.tgz", + "integrity": "sha512-i7hu6lg6bm2esyYYDwHh0XmGkOWSL5iicLjC7DNRU4b4As/dKcRxYMxNY6dXtKzbM94xhmX6Fh4j5/fxen/4kw==", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.20.3" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/nexus": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/nexus/-/nexus-1.20.3.tgz", + "integrity": "sha512-OSl73enJ9M8OMk2QaGHr4cQ1DDLEkjMhU1Z3tBt2zbWkKDs2IHKkl1wU/rMeXg7AJf5uqHcp209SBaDN/rqDiA==", + "dependencies": { + "@temporalio/client": "1.20.3", + "@temporalio/common": "1.20.3", + "@temporalio/proto": "1.20.3", + "long": "^5.2.3", + "nexus-rpc": "^0.0.2" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/proto": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.20.3.tgz", + "integrity": "sha512-RdbHC3zH+N0QqG1Q38h2LilFIQ3lbGLVmLYCyNcV+PPfzrtrZ8pwBACpSGwEuF7WWFziLAhJ2oMVXTnmXY5ocQ==", + "dependencies": { + "long": "^5.2.3", + "protobufjs": "^7.6.4" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/worker": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/worker/-/worker-1.20.3.tgz", + "integrity": "sha512-29W39KxGoz8QfiELT5al1jr+nnZEnpWCjr5gYT2a6ZlXr2WKxbJw4rRBy+OyGwHs5kGT0pPYayYvwcDTvWRpQQ==", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@swc/core": "^1.3.102", + "@temporalio/activity": "1.20.3", + "@temporalio/client": "1.20.3", + "@temporalio/common": "1.20.3", + "@temporalio/core-bridge": "1.20.3", + "@temporalio/nexus": "1.20.3", + "@temporalio/proto": "1.20.3", + "@temporalio/workflow": "1.20.3", + "heap-js": "^2.6.0", + "memfs": "^4.6.0", + "nexus-rpc": "^0.0.2", + "protobufjs": "^7.6.4", + "rxjs": "^7.8.1", + "source-map": "^0.7.4", + "source-map-loader": "^5.0.0", + "supports-color": "^8.1.1", + "swc-loader": "^0.2.3", + "unionfs": "^4.5.1", + "webpack": "^5.108.4" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/workflow": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/@temporalio/workflow/-/workflow-1.20.3.tgz", + "integrity": "sha512-6LRfnWp3HcvqvKFwbOhs5iDQkHNIM5Rxtxhsi2E5ViPDvJMBqlQ0sQ4oRo5SEgr5Bri650i7GfEQQJMVl7xoKg==", + "dependencies": { + "@temporalio/common": "1.20.3", + "@temporalio/proto": "1.20.3", + "nexus-rpc": "^0.0.2" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -160,6 +1527,95 @@ "resolved": "https://registry.npmjs.org/fzstd/-/fzstd-0.1.1.tgz", "integrity": "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==" }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/kafkajs": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", @@ -169,6 +1625,165 @@ "node": ">=14.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/memfs": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.64.0.tgz", + "integrity": "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-to-fsa": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/nexus-rpc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.2.tgz", + "integrity": "sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg==", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "engines": { + "node": ">=18" + } + }, "node_modules/pg": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", @@ -258,6 +1873,11 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, "node_modules/playwright": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", @@ -327,6 +1947,138 @@ "node": ">=0.10.0" } }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -336,6 +2088,120 @@ "node": ">= 10.x" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -349,6 +2215,139 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + }, + "node_modules/unionfs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/unionfs/-/unionfs-4.6.0.tgz", + "integrity": "sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==", + "dependencies": { + "fs-monkey": "^1.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.108.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.4.tgz", + "integrity": "sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -358,6 +2357,14 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", @@ -371,6 +2378,31 @@ "funding": { "url": "https://github.com/sponsors/eemeli" } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } } } } diff --git a/package.json b/package.json index 5ef79cec..cfa99dea 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,14 @@ }, "dependencies": { "@openai/codex": "^0.128.0", + "@swc/wasm": "^1.15.43", + "@temporalio/activity": "^1.20.3", + "@temporalio/client": "^1.20.3", + "@temporalio/worker": "^1.20.3", + "@temporalio/workflow": "^1.20.3", "fzstd": "0.1.1", "kafkajs": "^2.2.4", + "loader-utils": "^3.3.1", "pg": "^8.21.0", "playwright": "1.59.1", "yaml": "^2.8.3" diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index 964e280d..75c87607 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -711,9 +711,9 @@ test("artifact reuse paths preserve catalog provenance instead of current planne assert.match(artifactPublish, /envReuseRecipe: servicePlan\.envReuseRecipe \?\? null/u); assert.match(artifactPublish, /componentInputHash: serviceResultValue\(env, service\.serviceId, "COMPONENT_INPUT_HASH"\) \?\? \(reusedServiceImage \? null : service\.componentInputHash \?\? null\)/u); - const gitopsRender = await readFile("scripts/gitops-render.mjs", "utf8"); - assert.match(gitopsRender, /const componentInputHash = envReuse \? \(planned\.componentInputHash \|\| service\.componentInputHash \|\| ""\) : \(service\.componentInputHash \|\| ""\);/u); - assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u); + const perServicePublish = await readFile("scripts/src/gitops-render/templates/per-service-publish.sh", "utf8"); + assert.match(perServicePublish, /const componentInputHash = envReuse \? \(planned\.componentInputHash \|\| service\.componentInputHash \|\| ""\) : \(service\.componentInputHash \|\| ""\);/u); + assert.doesNotMatch(perServicePublish, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u); }); test("artifact publish ignores stale external reports for envreuse services that do not need current build artifacts", async () => { @@ -1483,6 +1483,38 @@ test("v03 planner reads env reuse declarations and catalog from v03 lane config" } }); +test("v03 TaskTree API and worker share one TypeScript environment artifact", async () => { + const reuse = await readStructuredFile(process.cwd(), "gitops/reuse.ymal"); + const group = reuse.spec?.envArtifactGroups?.["hwlab-ts-runtime-env"]; + const services = reuse.spec?.services ?? {}; + const tasktreeServiceIds = ["hwlab-tasktree-api", "hwlab-tasktree-worker"]; + const expectedEnvIdentity = [ + "package.json", + "package-lock.json", + "deploy/runtime/launcher/hwlab-env-reuse-launcher.ts", + "internal/dev-entrypoint/artifact-runtime.mjs" + ]; + + assert.ok(group, "shared TypeScript environment group"); + for (const serviceId of tasktreeServiceIds) { + assert.equal(group.services?.includes(serviceId), true, `${serviceId} shared environment membership`); + assert.deepEqual(services[serviceId]?.runtimeReuse?.envIdentity?.paths, expectedEnvIdentity, `${serviceId} runtime env identity`); + assert.deepEqual(services[serviceId]?.envReuse?.envIdentityFiles, expectedEnvIdentity, `${serviceId} env reuse identity`); + assert.equal(services[serviceId]?.envReuse?.mode, "env-reuse-git-mirror-checkout", `${serviceId} runtime mode`); + } + assert.deepEqual(services["hwlab-tasktree-api"].runtimeReuse.codeIdentity.paths, [ + "cmd/hwlab-tasktree-api", + "internal/tasktree", + "tools/src/tasktree-cli.ts", + "deploy/runtime/boot/hwlab-tasktree-api.sh" + ]); + assert.deepEqual(services["hwlab-tasktree-worker"].runtimeReuse.codeIdentity.paths, [ + "cmd/hwlab-tasktree-worker", + "internal/tasktree", + "deploy/runtime/boot/hwlab-tasktree-worker.sh" + ]); +}); + test("v03 planner builds one shared Go env artifact for compatible Go services", async () => { const repo = await createFixtureRepo({ catalog: false }); await addGoEnvReuseFixture(repo); diff --git a/tools/hwlab-cli/bin/hwlab-cli.ts b/tools/hwlab-cli/bin/hwlab-cli.ts index dfc91ad9..e653fa75 100644 --- a/tools/hwlab-cli/bin/hwlab-cli.ts +++ b/tools/hwlab-cli/bin/hwlab-cli.ts @@ -2,7 +2,19 @@ import { main } from "../../src/hwlab-cli-lib.ts"; const argv = process.argv.slice(2); -if (argv[0] === "kafka") { +if (argv[0] === "tasktree") { + const { runTaskTreeCli } = await import("../../src/tasktree-cli.ts"); + try { + const tasktreeArgs = argv.slice(1); + const stdin = tasktreeArgs.includes("--stdin") ? await Bun.stdin.text() : undefined; + const result = await runTaskTreeCli(tasktreeArgs, process.env, stdin); + console.log(JSON.stringify(result, null, 2)); + process.exitCode = result?.ok === false ? 1 : 0; + } catch (error) { + console.log(JSON.stringify({ ok: false, operation: "tasktree", error: { code: (error as any)?.code ?? "tasktree_cli_error", message: (error as any)?.message ?? String(error) } }, null, 2)); + process.exitCode = 1; + } +} else if (argv[0] === "kafka") { const { mainKafkaCli } = await import("../../src/hwlab-cli/kafka-regenerate.ts"); await mainKafkaCli(argv.slice(1)); } else await main(argv); diff --git a/tools/src/tasktree-cli.test.ts b/tools/src/tasktree-cli.test.ts new file mode 100644 index 00000000..e564a158 --- /dev/null +++ b/tools/src/tasktree-cli.test.ts @@ -0,0 +1,152 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { runTaskTreeCli } from "./tasktree-cli.ts"; + +test("--overapi preserves the command DTO and only changes transport", async () => { + const originalFetch = globalThis.fetch; + let received: unknown; + globalThis.fetch = (async (input, init) => { + assert.equal(String(input), "http://tasktree.test/v1/tasktree/commands"); + received = JSON.parse(String(init?.body)); + return Response.json({ ok: true, operation: "timeline.get", data: { group: { id: "tg_1" }, tasks: [], milestones: [], reports: [] } }); + }) as typeof fetch; + try { + const result = await runTaskTreeCli(["timeline", "--group", "tg_1", "--overapi"], { + HWLAB_TASKTREE_API_URL: "http://tasktree.test" + }); + assert.deepEqual(received, { operation: "timeline.get", groupId: "tg_1" }); + assert.equal(result.transport, "api"); + assert.equal(result.httpStatus, 200); + assert.equal(result.operation, "timeline.get"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("TaskTree command surface covers hierarchy, lifecycle, batch, stats, and reports", async () => { + const originalFetch = globalThis.fetch; + const received: unknown[] = []; + globalThis.fetch = (async (_input, init) => { + const command = JSON.parse(String(init?.body)); + received.push(command); + return Response.json({ ok: true, operation: command.operation, data: {} }); + }) as typeof fetch; + const env = { HWLAB_TASKTREE_API_URL: "http://tasktree.test" }; + try { + const cases: Array<[string[], unknown, string?]> = [ + [["group", "stats", "--group", "tg_1", "--overapi"], { operation: "group.stats", groupId: "tg_1" }], + [["task", "list", "--group", "tg_1", "--overapi"], { operation: "task.list", groupId: "tg_1" }], + [["task", "get", "--task", "tt_1", "--overapi"], { operation: "task.get", taskId: "tt_1" }], + [["task", "create", "--group", "tg_1", "--parent", "tt_1", "--stdin", "--overapi"], { + operation: "task.create", groupId: "tg_1", parentId: "tt_1", title: "Child" + }, "Child\n"], + [["task", "create-batch", "--group", "tg_1", "--parent", "tt_1", "One", "Two", "--overapi"], { + operation: "task.create-batch", groupId: "tg_1", parentId: "tt_1", titles: ["One", "Two"] + }], + [["task", "update", "--task", "tt_1", "--stdin", "--overapi"], { + operation: "task.update", taskId: "tt_1", title: "Renamed" + }, "Renamed\n"], + [["task", "start", "--task", "tt_1", "--overapi"], { operation: "task.start", taskId: "tt_1" }], + [["task", "done", "--task", "tt_1", "--overapi"], { operation: "task.complete", taskId: "tt_1" }], + [["task", "remove", "--task", "tt_1", "--overapi"], { operation: "task.delete", taskId: "tt_1" }], + [["report", "list", "--task", "tt_1", "--overapi"], { operation: "report.list", taskId: "tt_1" }], + [["report", "get", "--report", "tr_1", "--overapi"], { operation: "report.get", reportId: "tr_1" }], + [["report", "write", "--task", "tt_1", "--title", "Execution", "--stdin", "--overapi"], { + operation: "report.write", taskId: "tt_1", title: "Execution", body: "# Result\n\nPassed.\n" + }, "# Result\n\nPassed.\n"] + ]; + for (const [argv, expected, stdin] of cases) { + await runTaskTreeCli(argv, env, stdin); + assert.deepEqual(received.at(-1), expected); + } + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("TaskTree help and public operations do not expose the legacy domain term", async () => { + const result = await runTaskTreeCli(["--help"]); + assert.doesNotMatch(JSON.stringify(result), /mdtodo/iu); + assert.ok(result.usage.includes("hwlab-cli tasktree group list|overview")); + assert.ok(result.usage.includes("hwlab-cli tasktree group get|delete|stats --group ID")); + assert.match(result.usage.join("\n"), /task update .*--start ISO\|null.*--due ISO\|null/u); + assert.doesNotMatch(result.usage.join("\n"), /report create/u); +}); + +test("TaskTree rejects ambiguous title input and empty update patches before transport", async () => { + await assert.rejects( + runTaskTreeCli(["task", "create", "--group", "tg_1", "--title", "One", "--stdin", "--overapi"], { HWLAB_TASKTREE_API_URL: "http://tasktree.test" }, "Two"), + (error: any) => error?.code === "title_source_conflict" + ); + await assert.rejects( + runTaskTreeCli(["task", "update", "--task", "tt_1", "--overapi"], { HWLAB_TASKTREE_API_URL: "http://tasktree.test" }), + (error: any) => error?.code === "update_patch_required" + ); +}); + +test("TaskTree rejects unknown, duplicate, hidden, and extra CLI arguments", async () => { + const cases: Array<[string[], string]> = [ + [["group", "list", "unexpected"], "invalid_arguments"], + [["group", "list", "--bogus", "value"], "invalid_option"], + [["group", "create", "--name", "One", "--name", "Two"], "duplicate_option"], + [["report", "create", "--task", "tt_1", "--title", "Hidden", "--body", "body"], "unsupported_command"], + [["group", "list", "--api-url", "http://tasktree.test"], "invalid_option"], + [["group", "list", "--stdin"], "invalid_option"] + ]; + for (const [argv, code] of cases) { + await assert.rejects(runTaskTreeCli(argv), (error: any) => error?.code === code); + } +}); + +test("TaskTree accepts an explicit empty description value", async () => { + const originalFetch = globalThis.fetch; + let received: unknown; + globalThis.fetch = (async (_input, init) => { + received = JSON.parse(String(init?.body)); + return Response.json({ ok: true, operation: "group.create", data: {} }); + }) as typeof fetch; + try { + await runTaskTreeCli( + ["group", "create", "--name", "Group", "--description", "", "--overapi"], + { HWLAB_TASKTREE_API_URL: "http://tasktree.test" } + ); + assert.deepEqual(received, { operation: "group.create", name: "Group", description: "" }); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("TaskTree returns stable transport and runtime configuration errors", async () => { + await assert.rejects( + runTaskTreeCli(["group", "list", "--overapi"]), + (error: any) => error?.code === "missing_api_url" + ); + await assert.rejects( + runTaskTreeCli(["health"], {}), + (error: any) => error?.code === "missing_database_url" + ); + + const originalFetch = globalThis.fetch; + try { + globalThis.fetch = (async () => { throw new Error("connection refused"); }) as typeof fetch; + await assert.rejects( + runTaskTreeCli(["group", "list", "--overapi"], { HWLAB_TASKTREE_API_URL: "http://tasktree.test" }), + (error: any) => error?.code === "api_unreachable" + ); + + for (const response of [ + new Response("not json", { status: 502 }), + Response.json({ ok: true, operation: "task.get", data: {} }), + Response.json({ ok: true, operation: "group.list", data: {} }, { status: 500 }) + ]) { + globalThis.fetch = (async () => response) as typeof fetch; + await assert.rejects( + runTaskTreeCli(["group", "list", "--overapi"], { HWLAB_TASKTREE_API_URL: "http://tasktree.test" }), + (error: any) => error?.code === "invalid_api_response" + ); + } + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tools/src/tasktree-cli.ts b/tools/src/tasktree-cli.ts new file mode 100644 index 00000000..39a76c75 --- /dev/null +++ b/tools/src/tasktree-cli.ts @@ -0,0 +1,239 @@ +import { readFile } from "node:fs/promises"; + +import type { TaskStatus, TaskTreeCommand } from "../../internal/tasktree/contracts.ts"; +import { createTaskTreeDispatcher } from "../../internal/tasktree/dispatcher.ts"; +import { loadMdtodoImportPayload } from "../../internal/tasktree/mdtodo-import.ts"; +import { taskTreeRuntime } from "../../internal/tasktree/runtime.ts"; + +export async function runTaskTreeCli(argv: string[], env: Record = process.env, stdin?: string) { + const parsed = parse(argv); + if (parsed.help || parsed.positionals.length === 0) return help(); + validateArguments(parsed); + const command = await commandFrom(parsed, stdin); + if (parsed.overapi) return overApi(command, env, parsed); + const runtime = taskTreeRuntime(env); + try { + return await createTaskTreeDispatcher(runtime)(command); + } finally { + await runtime.store.close(); + } +} + +function help() { + return { + ok: true, + operation: "help", + mode: "local-dispatcher-default", + usage: [ + "hwlab-cli tasktree health [--overapi]", + "hwlab-cli tasktree group list|overview", + "hwlab-cli tasktree group get|delete|stats --group ID", + "hwlab-cli tasktree group create --name TEXT [--description TEXT]", + "hwlab-cli tasktree group import-markdown --file PATH [--name TEXT] [--dry-run]", + "hwlab-cli tasktree task list --group ID", + "hwlab-cli tasktree task get --task ID", + "hwlab-cli tasktree task create --group ID [--parent ID] (--title TEXT|--stdin) [--description TEXT] [--start ISO] [--due ISO]", + "hwlab-cli tasktree task create-batch --group ID [--parent ID] [--description TEXT] [--start ISO] [--due ISO] TITLE...", + "hwlab-cli tasktree task update --task ID [--title TEXT|--stdin] [--description TEXT] [--status STATUS] [--start ISO|null] [--due ISO|null]", + "hwlab-cli tasktree task start|complete|done|delete|remove --task ID", + "hwlab-cli tasktree milestone create --group ID [--task ID] --title TEXT --at ISO", + "hwlab-cli tasktree report list|get --task ID|--report ID", + "hwlab-cli tasktree report write --task ID --title TEXT (--body TEXT|--body-file PATH|--stdin) [--status STATUS]", + "hwlab-cli tasktree timeline --group ID [--overapi]", + "hwlab-cli tasktree workflow start --task ID" + ], + transportContract: "--overapi only changes transport; commands and options are identical", + transportOptions: "--overapi [--api-url URL]", + hierarchy: "taskgroup -> task -> subtask -> subsubtask", + completionContract: "a task requires at least one execution report before completion" + }; +} + +async function commandFrom(parsed: Parsed, stdin?: string): Promise { + const [group, action = ""] = parsed.positionals; + if (group === "health") return { operation: "health" }; + if (group === "timeline") return { operation: "timeline.get", groupId: required(parsed, "group") }; + if (group === "group" && action === "list") return { operation: "group.list" }; + if (group === "group" && action === "overview") return { operation: "group.overview" }; + if (group === "group" && action === "get") return { operation: "group.get", groupId: required(parsed, "group") }; + if (group === "group" && action === "create") return { operation: "group.create", name: required(parsed, "name"), description: parsed.values.description }; + if (group === "group" && action === "delete") return { operation: "group.delete", groupId: required(parsed, "group") }; + if (group === "group" && action === "stats") return { operation: "group.stats", groupId: required(parsed, "group") }; + if (group === "group" && action === "import-markdown") return { + operation: "group.import-markdown", + ...await loadMdtodoImportPayload(required(parsed, "file"), { groupName: parsed.values.name, dryRun: parsed.flags.has("dry-run") }) + }; + if (group === "task" && action === "list") return { operation: "task.list", groupId: required(parsed, "group") }; + if (group === "task" && action === "get") return { operation: "task.get", taskId: required(parsed, "task") }; + if (group === "task" && action === "create") return { + operation: "task.create", + groupId: required(parsed, "group"), + parentId: parsed.values.parent, + title: titleInput(parsed, stdin), + description: parsed.values.description, + startAt: parsed.values.start, + dueAt: parsed.values.due + }; + if (group === "task" && action === "create-batch") return { + operation: "task.create-batch", + groupId: required(parsed, "group"), + parentId: parsed.values.parent, + titles: parsed.positionals.slice(2), + description: parsed.values.description, + startAt: parsed.values.start, + dueAt: parsed.values.due + }; + if (group === "task" && action === "update") { + const command: TaskTreeCommand = { + operation: "task.update", + taskId: required(parsed, "task"), + title: parsed.flags.has("stdin") ? titleInput(parsed, stdin) : parsed.values.title, + description: parsed.values.description, + status: parsed.values.status as TaskStatus | undefined, + startAt: nullableDateOption(parsed, "start"), + dueAt: nullableDateOption(parsed, "due") + }; + if (command.title === undefined && command.description === undefined && command.status === undefined && command.startAt === undefined && command.dueAt === undefined) { + throw codedError("update_patch_required", "task update requires at least one field"); + } + return command; + } + if (group === "task" && (action === "delete" || action === "remove")) return { operation: "task.delete", taskId: required(parsed, "task") }; + if (group === "task" && action === "start") return { operation: "task.start", taskId: required(parsed, "task") }; + if (group === "task" && (action === "complete" || action === "done")) return { operation: "task.complete", taskId: required(parsed, "task") }; + if (group === "milestone" && action === "create") return { operation: "milestone.create", groupId: required(parsed, "group"), taskId: parsed.values.task, title: required(parsed, "title"), occursAt: required(parsed, "at") }; + if (group === "report" && action === "list") return { operation: "report.list", taskId: required(parsed, "task") }; + if (group === "report" && action === "get") return { operation: "report.get", reportId: required(parsed, "report") }; + if (group === "report" && action === "write") return { operation: "report.write", taskId: required(parsed, "task"), title: required(parsed, "title"), body: await bodyInput(parsed, stdin), status: parsed.values.status }; + if (group === "workflow" && action === "start") return { operation: "workflow.start", taskId: required(parsed, "task") }; + throw codedError("unsupported_command", `unsupported TaskTree command: ${group} ${action}`.trim()); +} + +async function overApi(command: TaskTreeCommand, env: Record, parsed: Parsed) { + const baseUrl = parsed.values["api-url"] || env.HWLAB_TASKTREE_API_URL; + if (!baseUrl) throw codedError("missing_api_url", "HWLAB_TASKTREE_API_URL or --api-url is required with --overapi"); + let response: Response; + try { + response = await fetch(`${baseUrl.replace(/\/$/u, "")}/v1/tasktree/commands`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(command) + }); + } catch { + throw codedError("api_unreachable", "TaskTree API request failed"); + } + const body = await response.json().catch(() => null); + if ( + !body + || typeof body !== "object" + || typeof (body as any).ok !== "boolean" + || (body as any).operation !== command.operation + || response.ok !== (body as any).ok + ) { + throw codedError("invalid_api_response", `TaskTree API returned an invalid response (${response.status})`); + } + return { ...(body as object), transport: "api", httpStatus: response.status }; +} + +type Parsed = { positionals: string[]; values: Record; flags: Set; overapi: boolean; help: boolean }; +function parse(argv: string[]): Parsed { + const parsed: Parsed = { positionals: [], values: {}, flags: new Set(), overapi: false, help: false }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === "--overapi") { + if (parsed.overapi) throw codedError("duplicate_option", "--overapi may only be provided once"); + parsed.overapi = true; + } + else if (token === "--dry-run" || token === "--stdin") { + const flag = token.slice(2); + if (parsed.flags.has(flag)) throw codedError("duplicate_option", `${token} may only be provided once`); + parsed.flags.add(flag); + } + else if (token === "--help" || token === "-h") parsed.help = true; + else if (token.startsWith("--")) { + const key = token.slice(2); + const value = argv[index + 1]; + if (parsed.values[key] !== undefined) throw codedError("duplicate_option", `${token} may only be provided once`); + if (value === undefined || value.startsWith("--")) throw codedError("missing_option_value", `${token} requires a value`); + parsed.values[key] = value; + index += 1; + } else parsed.positionals.push(token); + } + return parsed; +} +function required(parsed: Parsed, key: string) { const value = parsed.values[key]; if (!value) throw codedError("missing_option", `--${key} is required`); return value; } +function titleInput(parsed: Parsed, stdin?: string) { + if (parsed.flags.has("stdin") && parsed.values.title !== undefined) throw codedError("title_source_conflict", "--title and --stdin are mutually exclusive"); + const value = parsed.flags.has("stdin") ? stripOneLineEnding(stdin ?? "") : parsed.values.title; + if (value === undefined || value.length === 0) throw codedError("title_required", "--title or --stdin is required"); + return value; +} +async function bodyInput(parsed: Parsed, stdin?: string) { + const sources = Number(parsed.values.body !== undefined) + Number(parsed.values["body-file"] !== undefined) + Number(parsed.flags.has("stdin")); + if (sources !== 1) throw codedError("body_source_invalid", "exactly one of --body, --body-file, or --stdin is required"); + if (parsed.flags.has("stdin")) return stdin ?? ""; + return parsed.values.body ?? await readFile(required(parsed, "body-file"), "utf8"); +} +function nullableDateOption(parsed: Parsed, key: string): string | null | undefined { + const value = parsed.values[key]; + return value === "null" ? null : value; +} +function stripOneLineEnding(value: string) { return value.endsWith("\r\n") ? value.slice(0, -2) : value.endsWith("\n") ? value.slice(0, -1) : value; } +function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); } + +type CommandSpec = { + positionals: number | { min: number; max?: number }; + options?: string[]; + flags?: string[]; +}; + +const COMMAND_SPECS: Record = { + health: { positionals: 1 }, + timeline: { positionals: 1, options: ["group"] }, + "group list": { positionals: 2 }, + "group overview": { positionals: 2 }, + "group get": { positionals: 2, options: ["group"] }, + "group create": { positionals: 2, options: ["name", "description"] }, + "group delete": { positionals: 2, options: ["group"] }, + "group stats": { positionals: 2, options: ["group"] }, + "group import-markdown": { positionals: 2, options: ["file", "name"], flags: ["dry-run"] }, + "task list": { positionals: 2, options: ["group"] }, + "task get": { positionals: 2, options: ["task"] }, + "task create": { positionals: 2, options: ["group", "parent", "title", "description", "start", "due"], flags: ["stdin"] }, + "task create-batch": { positionals: { min: 3 }, options: ["group", "parent", "description", "start", "due"] }, + "task update": { positionals: 2, options: ["task", "title", "description", "status", "start", "due"], flags: ["stdin"] }, + "task delete": { positionals: 2, options: ["task"] }, + "task remove": { positionals: 2, options: ["task"] }, + "task start": { positionals: 2, options: ["task"] }, + "task complete": { positionals: 2, options: ["task"] }, + "task done": { positionals: 2, options: ["task"] }, + "milestone create": { positionals: 2, options: ["group", "task", "title", "at"] }, + "report list": { positionals: 2, options: ["task"] }, + "report get": { positionals: 2, options: ["report"] }, + "report write": { positionals: 2, options: ["task", "title", "body", "body-file", "status"], flags: ["stdin"] }, + "workflow start": { positionals: 2, options: ["task"] } +}; + +function validateArguments(parsed: Parsed) { + const key = parsed.positionals.slice(0, 2).join(" "); + const spec = COMMAND_SPECS[key] ?? COMMAND_SPECS[parsed.positionals[0]]; + if (!spec) throw codedError("unsupported_command", `unsupported TaskTree command: ${key}`); + const positionals = typeof spec.positionals === "number" + ? { min: spec.positionals, max: spec.positionals } + : spec.positionals; + if (parsed.positionals.length < positionals.min || (positionals.max !== undefined && parsed.positionals.length > positionals.max)) { + throw codedError("invalid_arguments", `unexpected positional arguments for ${key}`); + } + const allowedOptions = new Set(spec.options ?? []); + if (parsed.overapi) allowedOptions.add("api-url"); + for (const option of Object.keys(parsed.values)) { + if (!allowedOptions.has(option)) throw codedError("invalid_option", `--${option} is not valid for ${key}`); + } + if (parsed.values["api-url"] !== undefined && !parsed.overapi) { + throw codedError("invalid_option", "--api-url requires --overapi"); + } + const allowedFlags = new Set(spec.flags ?? []); + for (const flag of parsed.flags) { + if (!allowedFlags.has(flag)) throw codedError("invalid_option", `--${flag} is not valid for ${key}`); + } +} diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json index b7a86872..719d8da4 100644 --- a/web/hwlab-cloud-web/package.json +++ b/web/hwlab-cloud-web/package.json @@ -6,6 +6,10 @@ "scripts": { "deps": "node ../../scripts/worktree-deps.mjs --web", "dev": "bun run deps --quiet && vite", + "tasktree:native:start": "bun run scripts/tasktree-native-service.ts start", + "tasktree:native:stop": "bun run scripts/tasktree-native-service.ts stop", + "tasktree:native:status": "bun run scripts/tasktree-native-service.ts status", + "tasktree:native:logs": "bun run scripts/tasktree-native-service.ts logs", "check": "bun run deps --quiet && bun run scripts/tsc-check.ts && bun test scripts", "check:tsc": "bun run deps --quiet && bun run scripts/tsc-check.ts", "check:tsc-strict": "bun run deps --quiet && bun run scripts/tsc-check.ts --strict", diff --git a/web/hwlab-cloud-web/scripts/tasktree-native-service.ts b/web/hwlab-cloud-web/scripts/tasktree-native-service.ts new file mode 100644 index 00000000..242c8996 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/tasktree-native-service.ts @@ -0,0 +1,95 @@ +import { mkdirSync, openSync, readFileSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; + +const action = process.argv[2]; +const root = process.cwd(); +const stateDir = resolve(root, ".state/tasktree-native"); +const pidPath = resolve(stateDir, "supervisor.pid"); +const logPath = resolve(stateDir, "supervisor.log"); +const servicesLogPath = resolve(stateDir, "services.log"); + +function readPid(): number | null { + try { + const pid = Number(readFileSync(pidPath, "utf8").trim()); + return Number.isInteger(pid) && pid > 0 ? pid : null; + } catch { + return null; + } +} + +function isRunning(pid: number | null): boolean { + if (!pid) return false; + try { + process.kill(pid, 0); + try { + const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); + const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0]; + if (state === "Z") return false; + } catch { + // Non-Linux hosts do not expose /proc; the signal probe remains authoritative there. + } + return true; + } catch { + return false; + } +} + +async function waitForExit(pid: number): Promise { + const deadline = Date.now() + 15000; + while (Date.now() < deadline) { + if (!isRunning(pid)) return true; + await Bun.sleep(25); + } + return !isRunning(pid); +} + +mkdirSync(stateDir, { recursive: true }); +const pid = readPid(); + +if (action === "start") { + if (isRunning(pid)) { + console.log(JSON.stringify({ ok: true, action, status: "already-running", pid, logPath })); + process.exit(0); + } + rmSync(pidPath, { force: true }); + const logFd = openSync(logPath, "a"); + const child = Bun.spawn([process.execPath, "scripts/tasktree-native-supervisor.ts"], { + cwd: root, + env: process.env, + stdin: "ignore", + stdout: logFd, + stderr: logFd, + detached: true + }); + child.unref(); + await Bun.write(pidPath, `${child.pid}\n`); + console.log(JSON.stringify({ ok: true, action, status: "started", pid: child.pid, logPath })); +} else if (action === "stop") { + if (!isRunning(pid)) { + rmSync(pidPath, { force: true }); + console.log(JSON.stringify({ ok: true, action, status: "stopped" })); + } else { + process.kill(pid!, "SIGTERM"); + if (!await waitForExit(pid!)) { + console.error(JSON.stringify({ ok: false, action, status: "stop-timeout", pid })); + process.exit(1); + } + rmSync(pidPath, { force: true }); + console.log(JSON.stringify({ ok: true, action, status: "stopped", pid })); + } +} else if (action === "status") { + const running = isRunning(pid); + console.log(JSON.stringify({ ok: running, action, status: running ? "running" : "stopped", pid: running ? pid : null, logPath, servicesLogPath })); + process.exit(running ? 0 : 1); +} else if (action === "logs") { + for (const path of [logPath, servicesLogPath]) { + try { + console.log(readFileSync(path, "utf8").trimEnd().split("\n").slice(-80).join("\n")); + } catch { + // A log is optional until its process has started. + } + } +} else { + console.error("usage: bun run scripts/tasktree-native-service.ts "); + process.exit(2); +} diff --git a/web/hwlab-cloud-web/scripts/tasktree-native-supervisor.ts b/web/hwlab-cloud-web/scripts/tasktree-native-supervisor.ts new file mode 100644 index 00000000..22393237 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/tasktree-native-supervisor.ts @@ -0,0 +1,58 @@ +import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const webRoot = process.cwd(); +const repoRoot = resolve(webRoot, "../.."); +const stateDir = resolve(webRoot, ".state/tasktree-native"); +const databaseEnvFile = process.env.TASKTREE_NATIVE_DATABASE_ENV_FILE; +const temporalAddress = process.env.TASKTREE_TEMPORAL_ADDRESS; + +if (!databaseEnvFile) throw new Error("TASKTREE_NATIVE_DATABASE_ENV_FILE is required"); +if (!temporalAddress) throw new Error("TASKTREE_TEMPORAL_ADDRESS is required"); + +function readEnvValue(path: string, key: string): string { + const line = readFileSync(path, "utf8") + .split(/\r?\n/u) + .find((candidate) => candidate.startsWith(`${key}=`)); + if (!line) throw new Error(`${key} is missing from the configured env source`); + const raw = line.slice(key.length + 1).trim(); + if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) return raw.slice(1, -1); + return raw; +} + +mkdirSync(stateDir, { recursive: true }); +const logFd = openSync(resolve(stateDir, "services.log"), "a"); +const databaseUrl = readEnvValue(databaseEnvFile, "DATABASE_URL"); +const commonEnv = { + ...process.env, + TASKTREE_DATABASE_URL: databaseUrl, + TASKTREE_TEMPORAL_ADDRESS: temporalAddress, + TASKTREE_TEMPORAL_NAMESPACE: process.env.TASKTREE_TEMPORAL_NAMESPACE ?? "unidesk", + TASKTREE_TEMPORAL_TASK_QUEUE: process.env.TASKTREE_TEMPORAL_TASK_QUEUE ?? "hwlab-v03-tasktree" +}; + +const children = [ + Bun.spawn([process.execPath, "cmd/hwlab-tasktree-worker/main.ts"], { cwd: repoRoot, env: commonEnv, stdin: "ignore", stdout: logFd, stderr: logFd }), + Bun.spawn([process.execPath, "cmd/hwlab-tasktree-api/main.ts"], { cwd: repoRoot, env: { ...commonEnv, TASKTREE_API_HOST: "127.0.0.1", TASKTREE_API_PORT: "6673" }, stdin: "ignore", stdout: logFd, stderr: logFd }), + Bun.spawn([process.execPath, "x", "vite", "--config", "scripts/tasktree-native-vite.config.ts"], { cwd: webRoot, env: { ...process.env, HWLAB_TASKTREE_NATIVE_API_URL: "http://127.0.0.1:6673" }, stdin: "ignore", stdout: logFd, stderr: logFd }) +]; + +process.stdout.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-native", status: "started", pids: children.map((child) => child.pid), valuesPrinted: false })}\n`); + +let stopping = false; +async function stop(): Promise { + if (stopping) return; + stopping = true; + for (const child of children) child.kill("SIGTERM"); + await Promise.allSettled(children.map((child) => child.exited)); + closeSync(logFd); + process.exit(0); +} + +process.once("SIGINT", stop); +process.once("SIGTERM", stop); +const exited = await Promise.race(children.map(async (child, index) => ({ index, code: await child.exited }))); +if (!stopping) { + process.stderr.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-native", status: "child-exited", ...exited })}\n`); + await stop(); +} diff --git a/web/hwlab-cloud-web/scripts/tasktree-native-vite.config.ts b/web/hwlab-cloud-web/scripts/tasktree-native-vite.config.ts new file mode 100644 index 00000000..ab0227d7 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/tasktree-native-vite.config.ts @@ -0,0 +1,91 @@ +import { fileURLToPath, URL } from "node:url"; + +import vue from "@vitejs/plugin-vue"; +import { defineConfig, type Plugin } from "vite"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const tasktreeApi = process.env.HWLAB_TASKTREE_NATIVE_API_URL ?? "http://127.0.0.1:6673"; +const webHost = process.env.TASKTREE_NATIVE_WEB_HOST ?? "127.0.0.1"; +const webPort = Number.parseInt(process.env.TASKTREE_NATIVE_WEB_PORT ?? "4173", 10); + +const authPayload = { + authenticated: true, + mode: "server", + authMethod: "native-web", + identityAuthority: "hwlab-local", + sessionKind: "browser", + actor: { id: "usr_tasktree_native", username: "tasktree-native", role: "admin", status: "active" }, + capabilities: { web: "available", admin: "available" }, + access: { nav: { profileId: "tasktree-native", allowedIds: ["project.tasktree"], valuesRedacted: true } }, + expiresAt: null, + valuesRedacted: true +}; + +function nativeAuth(): Plugin { + return { + name: "hwlab-tasktree-native-auth", + configureServer(server) { + server.middlewares.use((request, response, next) => { + if (request.url === "/auth/logout") { + response.writeHead(204, { "cache-control": "no-store" }); + response.end(); + return; + } + if (request.url !== "/auth/session" && request.url !== "/auth/bootstrap" && request.url !== "/auth/login") return next(); + response.writeHead(200, { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + ...(request.url === "/auth/login" ? { "set-cookie": "hwlab_session=tasktree-native; Path=/; HttpOnly; SameSite=Lax" } : {}) + }); + response.end(JSON.stringify(authPayload)); + }); + } + }; +} + +function nativeRuntimeConfig(): Plugin { + return { + name: "hwlab-tasktree-native-runtime-config", + transformIndexHtml() { + return [{ + tag: "script", + injectTo: "head-prepend", + children: `window.HWLAB_CLOUD_WEB_CONFIG = { + displayTime: { timeZone: "Asia/Shanghai", locale: "zh-CN", label: "北京时间" }, + workbench: { + realtimeFeatures: { liveKafkaSse: false, kafkaRefreshReplay: false, projectionRealtime: true }, + debugCapabilities: { isolatedKafka: false, rawHwlabEventWindow: { enabled: false, maxEntries: 1, maxRetainedBytes: 1 } }, + traceTimeline: { autoExpandRunning: false, autoCollapseTerminal: false } + } + };` + }]; + } + }; +} + +export default defineConfig({ + root, + plugins: [nativeRuntimeConfig(), nativeAuth(), vue()], + resolve: { + alias: { + "@": fileURLToPath(new URL("../src", import.meta.url)) + } + }, + server: { + host: webHost, + port: webPort, + strictPort: true, + hmr: { clientPort: webPort }, + watch: { + usePolling: true, + interval: 300, + ignored: ["**/node_modules/**", "**/.git/**", "**/.state/**"] + }, + proxy: { + "/v1/tasktree": { + target: tasktreeApi, + changeOrigin: false + } + } + } +}); diff --git a/web/hwlab-cloud-web/src/api/index.ts b/web/hwlab-cloud-web/src/api/index.ts index 5f3bcb50..4cb09787 100644 --- a/web/hwlab-cloud-web/src/api/index.ts +++ b/web/hwlab-cloud-web/src/api/index.ts @@ -17,6 +17,7 @@ export { usageAPI } from "./usage"; export { billingAPI } from "./billing"; export { systemAPI, type SkillUploadFileInput } from "./system"; export { projectManagementAPI, type MdtodoExecutionContext, type MdtodoFileRecord, type MdtodoLaunchContextResponse, type MdtodoReportPreviewRecord, type MdtodoTaskDetailAuthority, type MdtodoTaskDetailRecord, type MdtodoTaskLinkRecord, type MdtodoTaskMutationInput, type MdtodoTaskMutationResponse, type MdtodoTaskPage, type MdtodoTaskRecord, type MdtodoTaskStatus, type ProjectNavigationResponse, type ProjectRecord, type ProjectSource, type ProjectSourceInput, type ProjectWorkbenchLinkRecord } from "./projectManagement"; +export { tasktreeAPI, type TaskTreeGroup, type TaskTreeGroupOverview, type TaskTreeMilestone, type TaskTreeReport, type TaskTreeTask, type TaskTreeTimeline } from "./tasktree"; import { fetchJson } from "./client"; import { accessAPI } from "./access"; @@ -32,6 +33,7 @@ import { billingAPI } from "./billing"; import { workbenchAPI } from "./workbench"; import { systemAPI } from "./system"; import { projectManagementAPI } from "./projectManagement"; +import { tasktreeAPI } from "./tasktree"; import type { ApiResult, LiveBuildsPayload, LiveProbePayload, SkillsResponse } from "@/types"; export const api = { @@ -47,6 +49,7 @@ export const api = { usage: usageAPI, billing: billingAPI, projectManagement: projectManagementAPI, + tasktree: tasktreeAPI, system: systemAPI, healthLive: (): Promise> => fetchJson("/health/live", { timeoutMs: 12000, timeoutName: "health/live" }), health: (): Promise> => fetchJson("/health", { timeoutMs: 12000, timeoutName: "health" }), diff --git a/web/hwlab-cloud-web/src/api/tasktree.ts b/web/hwlab-cloud-web/src/api/tasktree.ts new file mode 100644 index 00000000..fc3d3daa --- /dev/null +++ b/web/hwlab-cloud-web/src/api/tasktree.ts @@ -0,0 +1,16 @@ +import { fetchJson } from "@/api/client"; +import type { ApiResult } from "@/types"; + +export type TaskTreeGroup = { id: string; name: string; description: string; createdAt: string; updatedAt: string }; +export type TaskTreeTask = { id: string; groupId: string; parentId: string | null; kind: "task" | "subtask" | "subsubtask"; title: string; description: string; status: string; startAt: string | null; dueAt: string | null; sortOrder: number; createdAt: string; updatedAt: string }; +export type TaskTreeMilestone = { id: string; groupId: string; taskId: string | null; title: string; occursAt: string; createdAt: string }; +export type TaskTreeReport = { id: string; taskId: string; title: string; body: string; status: string; createdAt: string }; +export type TaskTreeTimeline = { group: TaskTreeGroup; tasks: TaskTreeTask[]; milestones: TaskTreeMilestone[]; reports: TaskTreeReport[] }; +export type TaskTreeGroupOverview = { group: TaskTreeGroup; taskCount: number; subtaskCount: number; subsubtaskCount: number; reportCount: number; startAt: string | null; dueAt: string | null }; +type Result = { ok?: boolean; data?: T; error?: { code?: string; message?: string } }; + +export const tasktreeAPI = { + groups: (): Promise>> => fetchJson("/v1/tasktree/groups", { timeoutMs: 12000, timeoutName: "TaskTree groups" }), + overview: (): Promise>> => fetchJson("/v1/tasktree/overview", { timeoutMs: 12000, timeoutName: "TaskTree overview" }), + timeline: (groupId: string): Promise>> => fetchJson(`/v1/tasktree/groups/${encodeURIComponent(groupId)}/timeline`, { timeoutMs: 12000, timeoutName: "TaskTree timeline" }) +}; diff --git a/web/hwlab-cloud-web/src/components/common/BaseDialog.vue b/web/hwlab-cloud-web/src/components/common/BaseDialog.vue index 2987f8c9..4ef6cab5 100644 --- a/web/hwlab-cloud-web/src/components/common/BaseDialog.vue +++ b/web/hwlab-cloud-web/src/components/common/BaseDialog.vue @@ -13,18 +13,20 @@ const props = withDefaults(defineProps<{ closeOnBackdrop?: boolean; busy?: boolean; initialFocus?: string; + surfaceClass?: string; }>(), { description: "", wide: false, fullscreen: false, closeOnBackdrop: true, busy: false, - initialFocus: "" + initialFocus: "", + surfaceClass: "" }); defineEmits<{ close: [] }>(); diff --git a/web/hwlab-cloud-web/src/components/common/OverlaySurface.vue b/web/hwlab-cloud-web/src/components/common/OverlaySurface.vue index 8d33b66a..c3341617 100644 --- a/web/hwlab-cloud-web/src/components/common/OverlaySurface.vue +++ b/web/hwlab-cloud-web/src/components/common/OverlaySurface.vue @@ -71,7 +71,7 @@ function close(): void { >
-

{{ title }}

+

{{ title }}

{{ description }}