Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
747 lines
21 KiB
Bash
Executable File
747 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
DESKTOP_APP_DIR="${ROOT_DIR}/apps/desktop"
|
|
TUTTID_DIR="${ROOT_DIR}/services/tuttid"
|
|
TUTTID_BIN_DIR="${DESKTOP_APP_DIR}/build/tuttid"
|
|
NODE_VERSION_FILE="${ROOT_DIR}/.node-version"
|
|
GO_MOD_FILE="${TUTTID_DIR}/go.mod"
|
|
PACKAGE_JSON_FILE="${ROOT_DIR}/package.json"
|
|
|
|
export TUTTI_ENV="${DEV_GUI_TUTTI_ENV:-development}"
|
|
|
|
GO_BIN=""
|
|
DEV_GUI_CHILD_PID=""
|
|
DEV_GUI_PID_PATH=""
|
|
DEV_GUI_INITIAL_TUTTID_PID=""
|
|
DEV_GUI_DESKTOP_STARTED=0
|
|
DEV_GUI_SHUTDOWN_STARTED=0
|
|
DEV_GUI_SHUTDOWN_TIMEOUT_SECONDS="${DEV_GUI_SHUTDOWN_TIMEOUT_SECONDS:-10}"
|
|
|
|
log() {
|
|
printf '[dev-gui] %s\n' "$*" >&2
|
|
}
|
|
|
|
fail() {
|
|
printf '[dev-gui] %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
prepend_command_dir() {
|
|
local command_path="$1"
|
|
local command_dir
|
|
|
|
command_dir="$(dirname "${command_path}")"
|
|
export PATH="${command_dir}:${PATH}"
|
|
hash -r
|
|
}
|
|
|
|
resolve_corepack_command() {
|
|
local node_exec_path
|
|
local node_bin_dir
|
|
local node_version
|
|
local node_platform
|
|
local node_arch
|
|
local local_node_corepack
|
|
local corepack_candidate
|
|
|
|
if command_exists corepack; then
|
|
command -v corepack
|
|
return
|
|
fi
|
|
|
|
if command_exists node; then
|
|
node_exec_path="$(node -p 'process.execPath' 2>/dev/null || true)"
|
|
if [[ -n "${node_exec_path}" ]]; then
|
|
node_bin_dir="$(dirname "${node_exec_path}")"
|
|
if [[ -x "${node_bin_dir}/corepack" ]]; then
|
|
printf '%s\n' "${node_bin_dir}/corepack"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
|
node_platform="$(node -p 'process.platform' 2>/dev/null || true)"
|
|
node_arch="$(node -p 'process.arch' 2>/dev/null || true)"
|
|
if [[ -n "${node_version}" && -n "${node_platform}" && -n "${node_arch}" ]]; then
|
|
local_node_corepack="${HOME}/.local/node-v${node_version}-${node_platform}-${node_arch}/bin/corepack"
|
|
if [[ -x "${local_node_corepack}" ]]; then
|
|
printf '%s\n' "${local_node_corepack}"
|
|
return
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
while IFS= read -r corepack_candidate; do
|
|
if [[ -x "${corepack_candidate}" ]]; then
|
|
printf '%s\n' "${corepack_candidate}"
|
|
return
|
|
fi
|
|
done < <(find "${HOME}/.local" -maxdepth 3 -path '*/bin/corepack' 2>/dev/null | sort -r)
|
|
|
|
return 1
|
|
}
|
|
|
|
run_go() {
|
|
env -u GOROOT "${GO_BIN}" "$@"
|
|
}
|
|
|
|
is_process_running() {
|
|
local pid="$1"
|
|
local stat
|
|
|
|
[[ -n "${pid}" ]] || return 1
|
|
kill -0 "${pid}" >/dev/null 2>&1 || return 1
|
|
|
|
stat="$(ps -p "${pid}" -o stat= 2>/dev/null | tr -d '[:space:]' || true)"
|
|
[[ "${stat}" != Z* ]]
|
|
}
|
|
|
|
wait_for_process_exit() {
|
|
local pid="$1"
|
|
local timeout_seconds="$2"
|
|
local deadline=$((SECONDS + timeout_seconds))
|
|
|
|
while (( SECONDS < deadline )); do
|
|
if ! is_process_running "${pid}"; then
|
|
return 0
|
|
fi
|
|
sleep 0.2
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
resolve_dev_gui_process_group_id() {
|
|
ps -p "$$" -o pgid= 2>/dev/null | tr -d '[:space:]'
|
|
}
|
|
|
|
restore_terminal_foreground() {
|
|
local pgid
|
|
|
|
[[ -t 0 && -e /dev/tty ]] || return
|
|
|
|
pgid="$(resolve_dev_gui_process_group_id)"
|
|
[[ "${pgid}" =~ ^[0-9]+$ ]] || return
|
|
|
|
(
|
|
trap '' TTOU
|
|
perl -MPOSIX=tcsetpgrp -e 'tcsetpgrp(STDIN, int($ARGV[0]))' "${pgid}" \
|
|
< /dev/tty >/dev/null 2>&1 || true
|
|
)
|
|
}
|
|
|
|
wait_for_desktop_dev_exit() {
|
|
local pid="$1"
|
|
local status
|
|
|
|
while is_process_running "${pid}"; do
|
|
restore_terminal_foreground
|
|
sleep 0.5
|
|
done
|
|
|
|
restore_terminal_foreground
|
|
|
|
set +e
|
|
wait "${pid}" >/dev/null 2>&1
|
|
status="$?"
|
|
set -e
|
|
return "${status}"
|
|
}
|
|
|
|
collect_child_processes() {
|
|
local parent_pid="$1"
|
|
local child_pid
|
|
|
|
while IFS= read -r child_pid; do
|
|
[[ "${child_pid}" =~ ^[0-9]+$ ]] || continue
|
|
collect_child_processes "${child_pid}"
|
|
printf '%s\n' "${child_pid}"
|
|
done < <(pgrep -P "${parent_pid}" 2>/dev/null || true)
|
|
}
|
|
|
|
stop_process_tree() {
|
|
local pid="$1"
|
|
local signal="$2"
|
|
local pids=()
|
|
|
|
if [[ -z "${pid}" ]] || ! is_process_running "${pid}"; then
|
|
return
|
|
fi
|
|
|
|
while IFS= read -r child_pid; do
|
|
[[ -n "${child_pid}" ]] || continue
|
|
pids+=("${child_pid}")
|
|
done < <(
|
|
{
|
|
collect_child_processes "${pid}"
|
|
printf '%s\n' "${pid}"
|
|
} | awk '!seen[$0]++'
|
|
)
|
|
|
|
if (( ${#pids[@]} == 0 )); then
|
|
return
|
|
fi
|
|
|
|
kill "-${signal}" "${pids[@]}" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
read_process_command() {
|
|
local pid="$1"
|
|
|
|
ps -p "${pid}" -o comm= -o args= 2>/dev/null | tr -d '\n'
|
|
}
|
|
|
|
is_likely_tuttid_process() {
|
|
local command="$1"
|
|
local part
|
|
|
|
[[ -n "${command//[[:space:]]/}" ]] || return 1
|
|
|
|
for part in ${command}; do
|
|
case "${part##*/}" in
|
|
tuttid|tuttid.exe)
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
stop_tuttid_from_pid_file() {
|
|
local pid_path="${DEV_GUI_PID_PATH}"
|
|
local raw_pid
|
|
local pid
|
|
local command
|
|
|
|
[[ -n "${pid_path}" ]] || return
|
|
[[ -f "${pid_path}" ]] || return
|
|
|
|
raw_pid="$(tr -d '[:space:]' < "${pid_path}" 2>/dev/null || true)"
|
|
[[ "${raw_pid}" =~ ^[0-9]+$ ]] || return
|
|
|
|
pid="${raw_pid}"
|
|
if [[ -n "${DEV_GUI_INITIAL_TUTTID_PID}" ]] && [[ "${pid}" == "${DEV_GUI_INITIAL_TUTTID_PID}" ]]; then
|
|
log "leaving pre-existing tuttid pid ${pid} alone"
|
|
return
|
|
fi
|
|
|
|
if ! is_process_running "${pid}"; then
|
|
rm -f "${pid_path}"
|
|
return
|
|
fi
|
|
|
|
command="$(read_process_command "${pid}" || true)"
|
|
if ! is_likely_tuttid_process "${command}"; then
|
|
log "leaving pid ${pid} alone because it is not a tuttid process"
|
|
return
|
|
fi
|
|
|
|
log "stopping managed tuttid pid ${pid}"
|
|
stop_process_tree "${pid}" TERM
|
|
if ! wait_for_process_exit "${pid}" "${DEV_GUI_SHUTDOWN_TIMEOUT_SECONDS}"; then
|
|
log "force stopping managed tuttid pid ${pid}"
|
|
stop_process_tree "${pid}" KILL
|
|
wait_for_process_exit "${pid}" 2 >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
cleanup_dev_gui() {
|
|
if [[ "${DEV_GUI_SHUTDOWN_STARTED}" == "1" ]]; then
|
|
return
|
|
fi
|
|
DEV_GUI_SHUTDOWN_STARTED=1
|
|
|
|
if [[ "${DEV_GUI_DESKTOP_STARTED}" == "1" ]] && [[ -n "${DEV_GUI_CHILD_PID}" ]]; then
|
|
log "stopping desktop dev process tree ${DEV_GUI_CHILD_PID}"
|
|
stop_process_tree "${DEV_GUI_CHILD_PID}" TERM
|
|
if ! wait_for_process_exit "${DEV_GUI_CHILD_PID}" "${DEV_GUI_SHUTDOWN_TIMEOUT_SECONDS}"; then
|
|
log "force stopping desktop dev process tree ${DEV_GUI_CHILD_PID}"
|
|
stop_process_tree "${DEV_GUI_CHILD_PID}" KILL
|
|
fi
|
|
wait "${DEV_GUI_CHILD_PID}" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
if [[ "${DEV_GUI_DESKTOP_STARTED}" == "1" ]]; then
|
|
stop_tuttid_from_pid_file
|
|
fi
|
|
|
|
}
|
|
|
|
read_tuttid_pid_file() {
|
|
local pid_path="$1"
|
|
|
|
[[ -n "${pid_path}" ]] || return 0
|
|
[[ -f "${pid_path}" ]] || return 0
|
|
tr -d '[:space:]' < "${pid_path}" 2>/dev/null || true
|
|
}
|
|
|
|
resolve_tuttid_pid_path() {
|
|
node - "${ROOT_DIR}/config/tutti.defaults.json" <<'NODE'
|
|
const { readFileSync } = require("node:fs");
|
|
const { homedir } = require("node:os");
|
|
const { join } = require("node:path");
|
|
|
|
const defaults = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
const explicitPIDPath = process.env.TUTTID_PID_PATH?.trim();
|
|
if (explicitPIDPath) {
|
|
console.log(explicitPIDPath);
|
|
process.exit(0);
|
|
}
|
|
|
|
const envValue = process.env.TUTTI_ENV?.trim().toLowerCase();
|
|
const isDevelopment =
|
|
envValue === "" ||
|
|
envValue === undefined ||
|
|
envValue === "dev" ||
|
|
envValue === "development" ||
|
|
envValue === "local";
|
|
const stateDir =
|
|
process.env.TUTTI_STATE_DIR?.trim() ||
|
|
join(
|
|
homedir(),
|
|
isDevelopment
|
|
? defaults.state.developmentDirName
|
|
: defaults.state.productionDirName
|
|
);
|
|
const runDir =
|
|
process.env.TUTTID_RUN_DIR?.trim() ||
|
|
join(stateDir, defaults.state.runDirName);
|
|
|
|
console.log(join(runDir, defaults.state.pidFileName));
|
|
NODE
|
|
}
|
|
|
|
prepare_dev_gui_runtime() {
|
|
DEV_GUI_PID_PATH="$(resolve_tuttid_pid_path)"
|
|
DEV_GUI_INITIAL_TUTTID_PID="$(read_tuttid_pid_file "${DEV_GUI_PID_PATH}")"
|
|
prepare_managed_posix_shell
|
|
prepare_bundled_rtk
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
node "${ROOT_DIR}/tools/scripts/prepare-dev-login-protocol.mjs"
|
|
fi
|
|
}
|
|
|
|
prepare_bundled_rtk() {
|
|
if [[ -n "${TUTTI_BUNDLED_RTK_PATH:-}" ]]; then
|
|
[[ -f "${TUTTI_BUNDLED_RTK_PATH}" ]] || fail \
|
|
"TUTTI_BUNDLED_RTK_PATH does not point to a file: ${TUTTI_BUNDLED_RTK_PATH}"
|
|
return
|
|
fi
|
|
local platform
|
|
platform="$(node -e 'const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"; const arch = process.arch === "arm64" ? "arm64" : "amd64"; process.stdout.write(`${os}-${arch}`)')"
|
|
log "preparing bundled RTK for ${platform}"
|
|
node "${DESKTOP_APP_DIR}/scripts/vendor-rtk.mjs" --platform="${platform}"
|
|
if [[ "${platform}" == windows-* ]]; then
|
|
export TUTTI_BUNDLED_RTK_PATH="${DESKTOP_APP_DIR}/build/rtk/rtk.exe"
|
|
else
|
|
export TUTTI_BUNDLED_RTK_PATH="${DESKTOP_APP_DIR}/build/rtk/rtk"
|
|
fi
|
|
[[ -f "${TUTTI_BUNDLED_RTK_PATH}" ]] || fail \
|
|
"bundled RTK executable is missing after preparation: ${TUTTI_BUNDLED_RTK_PATH}"
|
|
}
|
|
|
|
prepare_managed_posix_shell() {
|
|
# Workspace app packages keep a stable bootstrap.sh contract. On Windows the
|
|
# daemon executes that contract through the vendored MSYS2 shell; packaged
|
|
# launches discover it from runtime.json, while dev launches need to inject
|
|
# the same absolute path explicitly.
|
|
if [[ "$(node -p 'process.platform')" != "win32" ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ -n "${TUTTI_MANAGED_POSIX_SHELL:-}" ]]; then
|
|
[[ -f "${TUTTI_MANAGED_POSIX_SHELL}" ]] || fail \
|
|
"TUTTI_MANAGED_POSIX_SHELL does not point to a file: ${TUTTI_MANAGED_POSIX_SHELL}"
|
|
log "using managed POSIX shell from TUTTI_MANAGED_POSIX_SHELL=${TUTTI_MANAGED_POSIX_SHELL}"
|
|
return
|
|
fi
|
|
|
|
local runtime_root="${DESKTOP_APP_DIR}/build/managed-posix-shell"
|
|
local metadata_path="${runtime_root}/runtime.json"
|
|
local shell_path=""
|
|
|
|
if [[ -f "${metadata_path}" ]]; then
|
|
shell_path="$(node - "${metadata_path}" <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const metadataPath = process.argv[2];
|
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
if (metadata.schemaVersion !== "tutti.managed-posix-shell.v1" || typeof metadata.executable !== "string") {
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write(path.resolve(path.dirname(metadataPath), metadata.executable));
|
|
NODE
|
|
)" || shell_path=""
|
|
fi
|
|
|
|
if [[ -z "${shell_path}" ]] || [[ ! -f "${shell_path}" ]]; then
|
|
log "preparing managed POSIX shell for Windows dev GUI"
|
|
node "${DESKTOP_APP_DIR}/scripts/vendor-managed-posix-shell.mjs" \
|
|
--platform=windows-amd64
|
|
shell_path="$(node - "${metadata_path}" <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const metadataPath = process.argv[2];
|
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
if (metadata.schemaVersion !== "tutti.managed-posix-shell.v1" || typeof metadata.executable !== "string") {
|
|
throw new Error("managed POSIX shell runtime metadata is invalid");
|
|
}
|
|
process.stdout.write(path.resolve(path.dirname(metadataPath), metadata.executable));
|
|
NODE
|
|
)"
|
|
fi
|
|
|
|
[[ -f "${shell_path}" ]] || fail \
|
|
"managed POSIX shell executable is missing after preparation: ${shell_path}"
|
|
export TUTTI_MANAGED_POSIX_SHELL="${shell_path}"
|
|
log "using managed POSIX shell for Windows dev GUI: ${TUTTI_MANAGED_POSIX_SHELL}"
|
|
}
|
|
|
|
resolve_required_node_major() {
|
|
tr -d '[:space:]' < "${NODE_VERSION_FILE}"
|
|
}
|
|
|
|
resolve_required_go_minor() {
|
|
sed -nE 's/^go ([0-9]+\.[0-9]+).*/\1/p' "${GO_MOD_FILE}" | head -n 1
|
|
}
|
|
|
|
resolve_required_pnpm_version() {
|
|
sed -nE 's/.*"packageManager":[[:space:]]*"pnpm@([^"]+)".*/\1/p' \
|
|
"${PACKAGE_JSON_FILE}" | head -n 1
|
|
}
|
|
|
|
ensure_node_runtime() {
|
|
local current_node_major=""
|
|
local required_node_major
|
|
|
|
required_node_major="$(resolve_required_node_major)"
|
|
|
|
if command_exists node; then
|
|
current_node_major="$(node -p 'process.versions.node.split(".")[0]')"
|
|
fi
|
|
|
|
if [[ -n "${current_node_major}" ]] && (( current_node_major >= required_node_major )); then
|
|
log "node $(node --version)"
|
|
return
|
|
fi
|
|
|
|
if [[ -s "${HOME}/.nvm/nvm.sh" ]]; then
|
|
log "installing Node.js ${required_node_major}.x with nvm"
|
|
# shellcheck disable=SC1090
|
|
source "${HOME}/.nvm/nvm.sh"
|
|
nvm install "${required_node_major}"
|
|
nvm use "${required_node_major}" >/dev/null
|
|
hash -r
|
|
log "node $(node --version)"
|
|
return
|
|
fi
|
|
|
|
fail "Node.js ${required_node_major}+ is required. Install nvm or Node.js ${required_node_major}+ and rerun make dev-gui."
|
|
}
|
|
|
|
ensure_pnpm() {
|
|
local required_pnpm_version
|
|
local required_pnpm_semver
|
|
local current_pnpm_version=""
|
|
local corepack_command=""
|
|
local resolved_pnpm_version
|
|
|
|
required_pnpm_version="$(resolve_required_pnpm_version)"
|
|
required_pnpm_semver="${required_pnpm_version%%+*}"
|
|
|
|
if corepack_command="$(resolve_corepack_command)"; then
|
|
prepend_command_dir "${corepack_command}"
|
|
fi
|
|
|
|
if command_exists pnpm; then
|
|
current_pnpm_version="$(pnpm --version 2>/dev/null || true)"
|
|
fi
|
|
|
|
if [[ "${current_pnpm_version}" == "${required_pnpm_semver}" ]]; then
|
|
log "pnpm ${current_pnpm_version}"
|
|
return
|
|
fi
|
|
|
|
if [[ -z "${corepack_command}" ]] && ! corepack_command="$(resolve_corepack_command)"; then
|
|
fail "corepack is required to install pnpm ${required_pnpm_version} automatically."
|
|
fi
|
|
|
|
log "installing pnpm ${required_pnpm_version} with corepack"
|
|
"${corepack_command}" enable
|
|
"${corepack_command}" prepare "pnpm@${required_pnpm_version}" --activate
|
|
prepend_command_dir "${corepack_command}"
|
|
|
|
resolved_pnpm_version="$(pnpm --version 2>/dev/null || true)"
|
|
if [[ "${resolved_pnpm_version}" != "${required_pnpm_semver}" ]]; then
|
|
fail "pnpm ${required_pnpm_version} installation did not succeed; found ${resolved_pnpm_version:-unknown version}."
|
|
fi
|
|
|
|
log "pnpm ${resolved_pnpm_version}"
|
|
}
|
|
|
|
ensure_go_runtime() {
|
|
local required_go_minor
|
|
local current_go_minor=""
|
|
local brew_formula
|
|
local brew_prefix
|
|
|
|
required_go_minor="$(resolve_required_go_minor)"
|
|
|
|
if command_exists go; then
|
|
current_go_minor="$(
|
|
env -u GOROOT go version 2>/dev/null | sed -nE 's/^go version go([0-9]+\.[0-9]+).*/\1/p'
|
|
)"
|
|
if [[ "${current_go_minor}" == "${required_go_minor}" ]]; then
|
|
GO_BIN="$(command -v go)"
|
|
log "$(run_go version)"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
if [[ "$(uname -s)" != "Darwin" ]] || ! command_exists brew; then
|
|
fail "Go ${required_go_minor}.x is required. Install it and rerun make dev-gui."
|
|
fi
|
|
|
|
brew_formula="go@${required_go_minor}"
|
|
if [[ -z "$(brew list --versions "${brew_formula}" 2>/dev/null)" ]]; then
|
|
log "installing ${brew_formula} with Homebrew"
|
|
brew install "${brew_formula}"
|
|
else
|
|
log "using installed ${brew_formula}"
|
|
fi
|
|
|
|
brew_prefix="$(brew --prefix "${brew_formula}")"
|
|
GO_BIN="${brew_prefix}/bin/go"
|
|
export PATH="${brew_prefix}/bin:${PATH}"
|
|
hash -r
|
|
|
|
current_go_minor="$(
|
|
env -u GOROOT "${GO_BIN}" version 2>/dev/null | sed -nE 's/^go version go([0-9]+\.[0-9]+).*/\1/p'
|
|
)"
|
|
if [[ "${current_go_minor}" != "${required_go_minor}" ]]; then
|
|
fail "expected Go ${required_go_minor}.x after installing ${brew_formula}, found $("${GO_BIN}" version)"
|
|
fi
|
|
|
|
log "$(run_go version)"
|
|
}
|
|
|
|
check_runtime_prerequisites() {
|
|
log "checking runtime prerequisites"
|
|
ensure_node_runtime
|
|
ensure_pnpm
|
|
ensure_go_runtime
|
|
}
|
|
|
|
configure_desktop_dev_version() {
|
|
if [[ -n "${TUTTI_APP_VERSION:-}" ]]; then
|
|
log "desktop version ${TUTTI_APP_VERSION} (environment override)"
|
|
return
|
|
fi
|
|
|
|
TUTTI_APP_VERSION="$(
|
|
node "${DESKTOP_APP_DIR}/scripts/resolve-build-version.mjs"
|
|
)"
|
|
export TUTTI_APP_VERSION
|
|
log "desktop version ${TUTTI_APP_VERSION}"
|
|
}
|
|
|
|
configure_agent_extension_sources() {
|
|
local package_dir="${DEV_GUI_KIMI_CODE_PACKAGE_DIR:-}"
|
|
local helper_executable="${DEV_GUI_KIMI_CODE_ACCOUNT_USAGE_EXECUTABLE:-}"
|
|
local helper_candidate=""
|
|
local account_usage_profile=""
|
|
|
|
if [[ -z "${package_dir}" ]]; then
|
|
# A stale shell or launchd override must not silently shadow the signed
|
|
# remote release used by the shipped product.
|
|
unset TUTTI_AGENT_EXTENSION_KIMI_CODE_PACKAGE_DIR
|
|
unset TUTTI_AGENT_EXTENSION_KIMI_CODE_ACCOUNT_USAGE_EXECUTABLE
|
|
return
|
|
fi
|
|
|
|
[[ -d "${package_dir}" ]] || fail \
|
|
"local kimi-code agent extension directory does not exist: ${package_dir}"
|
|
[[ -f "${package_dir}/tutti.agent.json" ]] || fail \
|
|
"local kimi-code agent extension is not an unpacked package (missing tutti.agent.json): ${package_dir}"
|
|
|
|
package_dir="$(cd "${package_dir}" && pwd -P)"
|
|
export TUTTI_AGENT_EXTENSION_KIMI_CODE_PACKAGE_DIR="${package_dir}"
|
|
log "using local kimi-code agent extension at ${package_dir}"
|
|
|
|
if [[ -z "${helper_executable}" ]]; then
|
|
helper_candidate="${package_dir}/../../../packages/account-usage-probe/dist/cli.cjs"
|
|
if [[ -f "${helper_candidate}" && -x "${helper_candidate}" ]]; then
|
|
helper_executable="${helper_candidate}"
|
|
fi
|
|
fi
|
|
if [[ -z "${helper_executable}" ]]; then
|
|
account_usage_profile="$(
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
process.stdout.write(manifest.profiles?.accountUsage ?? "");
|
|
' "${package_dir}/tutti.agent.json"
|
|
)" || fail "could not inspect local kimi-code account usage profile"
|
|
if [[ -n "${account_usage_profile}" ]]; then
|
|
fail "local kimi-code account usage helper is missing; build it or set DEV_GUI_KIMI_CODE_ACCOUNT_USAGE_EXECUTABLE"
|
|
fi
|
|
unset TUTTI_AGENT_EXTENSION_KIMI_CODE_ACCOUNT_USAGE_EXECUTABLE
|
|
return
|
|
fi
|
|
[[ -f "${helper_executable}" && -x "${helper_executable}" ]] || fail \
|
|
"local kimi-code account usage helper is not executable: ${helper_executable}"
|
|
helper_executable="$(cd "$(dirname "${helper_executable}")" && pwd -P)/$(basename "${helper_executable}")"
|
|
export TUTTI_AGENT_EXTENSION_KIMI_CODE_ACCOUNT_USAGE_EXECUTABLE="${helper_executable}"
|
|
log "using local kimi-code account usage helper"
|
|
}
|
|
|
|
resolve_tuttid_binary_name() {
|
|
case "$(uname -s)" in
|
|
CYGWIN*|MINGW*|MSYS*)
|
|
printf 'tuttid.exe\n'
|
|
;;
|
|
*)
|
|
printf 'tuttid\n'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
resolve_node_process_path() {
|
|
local candidate_path="$1"
|
|
|
|
if [[ "$(node -p 'process.platform')" != "win32" ]]; then
|
|
printf '%s\n' "${candidate_path}"
|
|
return
|
|
fi
|
|
|
|
if ! command_exists cygpath; then
|
|
fail "cygpath is required to launch Windows processes from the managed POSIX shell."
|
|
fi
|
|
|
|
cygpath -w -- "${candidate_path}"
|
|
}
|
|
|
|
ensure_workspace_dependencies() {
|
|
local installed_lockfile="${ROOT_DIR}/node_modules/.pnpm/lock.yaml"
|
|
local workspace_lockfile="${ROOT_DIR}/pnpm-lock.yaml"
|
|
|
|
if [[ -d "${ROOT_DIR}/node_modules" ]] && [[ -f "${installed_lockfile}" ]]; then
|
|
if cmp -s "${workspace_lockfile}" "${installed_lockfile}"; then
|
|
log "workspace dependencies already installed"
|
|
return
|
|
fi
|
|
|
|
log "workspace dependency lockfile changed; refreshing with pnpm"
|
|
else
|
|
log "installing workspace dependencies with pnpm"
|
|
fi
|
|
|
|
(
|
|
cd "${ROOT_DIR}"
|
|
pnpm install
|
|
)
|
|
}
|
|
|
|
prepare_tuttid_binary() {
|
|
local binary_name="$1"
|
|
local binary_path="${TUTTID_BIN_DIR}/${binary_name}"
|
|
|
|
mkdir -p "${TUTTID_BIN_DIR}"
|
|
|
|
log "generating builtin workspace apps"
|
|
(
|
|
cd "${ROOT_DIR}"
|
|
pnpm generate:builtin-apps
|
|
)
|
|
|
|
log "downloading daemon Go modules"
|
|
(
|
|
cd "${TUTTID_DIR}"
|
|
run_go mod download
|
|
)
|
|
|
|
log "building daemon dev binary at ${binary_path}"
|
|
(
|
|
cd "${TUTTID_DIR}"
|
|
run_go build -buildvcs=false -o "${binary_path}" .
|
|
)
|
|
|
|
TUTTID_BINARY_PATH="${binary_path}"
|
|
}
|
|
|
|
install_dev_cli() {
|
|
log "installing tutti-dev CLI"
|
|
(
|
|
cd "${ROOT_DIR}"
|
|
GO_BIN="${GO_BIN}" node ./tools/scripts/install-dev-cli.mjs
|
|
)
|
|
}
|
|
|
|
start_desktop_dev() {
|
|
local tuttid_bin_path="$1"
|
|
local node_tuttid_bin_path
|
|
local status
|
|
# why-did-you-render instruments every React component and hook. On a real
|
|
# workspace that work can block the renderer for seconds while AgentGUI
|
|
# restores its session directory, which also keeps the Workbench hydration
|
|
# barrier (and therefore the Dock) non-interactive. Keep it opt-in so the
|
|
# normal development path has production-like scheduling characteristics.
|
|
local why_did_you_render="${VITE_TUTTI_WHY_DID_YOU_RENDER:-0}"
|
|
|
|
node_tuttid_bin_path="$(resolve_node_process_path "${tuttid_bin_path}")"
|
|
log "starting desktop dev with prebuilt tuttid"
|
|
if [[ "${why_did_you_render}" == "1" ]]; then
|
|
log "why-did-you-render diagnostics enabled"
|
|
fi
|
|
prepare_dev_gui_runtime
|
|
DEV_GUI_DESKTOP_STARTED=1
|
|
(
|
|
cd "${ROOT_DIR}"
|
|
TUTTID_BIN="${node_tuttid_bin_path}" \
|
|
TUTTID_LOG_OUTPUT="${TUTTID_LOG_OUTPUT:-tee}" \
|
|
VITE_TUTTI_WHY_DID_YOU_RENDER="${why_did_you_render}" \
|
|
pnpm dev:desktop < /dev/null
|
|
) &
|
|
DEV_GUI_CHILD_PID="$!"
|
|
|
|
set +e
|
|
wait_for_desktop_dev_exit "${DEV_GUI_CHILD_PID}"
|
|
status="$?"
|
|
set -e
|
|
DEV_GUI_CHILD_PID=""
|
|
return "${status}"
|
|
}
|
|
|
|
main() {
|
|
local binary_name
|
|
|
|
check_runtime_prerequisites
|
|
configure_agent_extension_sources
|
|
configure_desktop_dev_version
|
|
ensure_workspace_dependencies
|
|
|
|
binary_name="$(resolve_tuttid_binary_name)"
|
|
prepare_tuttid_binary "${binary_name}"
|
|
install_dev_cli
|
|
|
|
if [[ "${DEV_GUI_SKIP_START:-0}" == "1" ]]; then
|
|
log "skipping desktop launch because DEV_GUI_SKIP_START=1"
|
|
return
|
|
fi
|
|
|
|
start_desktop_dev "${TUTTID_BINARY_PATH}"
|
|
}
|
|
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
trap 'cleanup_dev_gui' EXIT
|
|
|
|
main "$@"
|