feat: add checkout service make targets (#1311)

This commit is contained in:
Carlos Florêncio
2026-06-08 19:30:21 +01:00
committed by GitHub
parent aac2afbf93
commit fc4bf8a40c
6 changed files with 167 additions and 3 deletions
+78
View File
@@ -30,6 +30,15 @@ YELLOW := \033[33m
MAGENTA := \033[35m
VERBOSE ?= 0
NODE ?= $(shell command -v node 2>/dev/null || echo node)
SERVICE_CLI := $(CURDIR)/dist/kandev/cli/bin/cli.js
SERVICE_BUNDLE_DIR := $(CURDIR)/dist/kandev
SERVICE_VERSION := $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
SERVICE_ENV := KANDEV_BUNDLE_DIR="$(SERVICE_BUNDLE_DIR)" KANDEV_VERSION="$(SERVICE_VERSION)"
SERVICE_PORT_FLAG := $(if $(PORT),--port $(PORT),)
SERVICE_HOME_DIR_FLAG := $(if $(HOME_DIR),--home-dir "$(HOME_DIR)",)
SERVICE_NO_BOOT_START_FLAG := $(if $(filter 1 true yes,$(NO_BOOT_START)),--no-boot-start,)
SERVICE_INSTALL_FLAGS := $(SERVICE_PORT_FLAG) $(SERVICE_HOME_DIR_FLAG) $(SERVICE_NO_BOOT_START_FLAG)
# Phase headers
define phase
@@ -65,6 +74,20 @@ help:
@echo " start-verbose Start in production mode with info logs from backend + web"
@echo " start VERBOSE=1 Same as start-verbose"
@echo ""
@echo "Service Commands:"
@echo " service-install Install deps, build current checkout, install user service"
@echo " service-install-system Install deps, build current checkout, install system service"
@echo " service-status Show current user service status"
@echo " service-logs Show current user service logs"
@echo " service-logs-follow Follow current user service logs"
@echo " service-start Start current user service"
@echo " service-stop Stop current user service"
@echo " service-restart Restart current user service"
@echo " service-uninstall Uninstall current user service"
@echo " service-config Show service launcher/config paths"
@echo " service-install PORT=3000 HOME_DIR=/path Optional install overrides"
@echo " service-install NO_BOOT_START=1 Skip Linux user-service boot hint"
@echo ""
@echo "Build Commands:"
@echo " build Build backend and web app"
@echo " build-backend Build backend binary"
@@ -189,6 +212,61 @@ start-verbose:
start-debug:
@$(MAKE) start DEBUG=1
#
# Service
#
.PHONY: service-bundle
service-bundle: install build
$(call phase,Packaging Service Bundle)
@scripts/release/package-web.sh
@scripts/release/package-cli.sh
@test -n "$(SERVICE_BUNDLE_DIR)" || { echo "SERVICE_BUNDLE_DIR is empty; aborting."; exit 1; }
@test "$(SERVICE_BUNDLE_DIR)" != "/" || { echo "SERVICE_BUNDLE_DIR must not be /; aborting."; exit 1; }
@test -f "$(BACKEND_DIR)/bin/agentctl-linux-amd64" || $(MAKE) -C $(BACKEND_DIR) build-agentctl-linux
@$(RMDIR) "$(SERVICE_BUNDLE_DIR)/bin"
@mkdir -p "$(SERVICE_BUNDLE_DIR)/bin"
@cp "$(BACKEND_DIR)/bin/kandev" "$(BACKEND_DIR)/bin/agentctl" "$(BACKEND_DIR)/bin/agentctl-linux-amd64" "$(SERVICE_BUNDLE_DIR)/bin/"
@scripts/release/package-bundle.sh
$(call success,Service bundle packaged at $(SERVICE_BUNDLE_DIR))
.PHONY: service-cli-check
service-cli-check:
@test -f "$(SERVICE_CLI)" || { echo "Missing $(SERVICE_CLI). Run 'make service-install' first."; exit 1; }
.PHONY: service-install
service-install: service-bundle
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service install $(SERVICE_INSTALL_FLAGS)
.PHONY: service-install-system
service-install-system: service-bundle
@sudo env $(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service install --system $(SERVICE_INSTALL_FLAGS)
.PHONY: service-uninstall service-start service-stop service-restart service-status service-logs service-logs-follow service-config
service-uninstall: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service uninstall
service-start: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service start
service-stop: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service stop
service-restart: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service restart
service-status: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service status
service-logs: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service logs
service-logs-follow: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service logs -f
service-config: service-cli-check
@$(SERVICE_ENV) "$(NODE)" "$(SERVICE_CLI)" service config
.PHONY: build-backend
build-backend:
@printf "$(CYAN)Building backend...$(RESET)\n"
@@ -25,6 +25,7 @@ const (
installKindHomebrew = "homebrew"
installKindNPM = "npm"
installKindNPX = "npx"
installKindLocal = "local"
manualServiceInstall = "kandev service install"
manualServiceRestart = "kandev service restart"
)
@@ -114,6 +114,47 @@ func TestService_GetForeignServiceDisablesApply(t *testing.T) {
}
}
func TestService_GetLocalBundleServiceDisablesApply(t *testing.T) {
homeDir := t.TempDir()
metadataPath, _ := writeServiceInstallForTest(t, homeDir, serviceInstallMetadata{
Manager: "systemd",
Mode: "user",
Kind: installKindLocal,
HomeDir: homeDir,
LogDir: filepath.Join(homeDir, "logs"),
ServicePath: filepath.Join(homeDir, "kandev.service"),
NodePath: "/usr/local/bin/node",
CLIEntry: "/Users/alice/src/kandev/dist/kandev/cli/bin/cli.js",
BundleDir: "/Users/alice/src/kandev/dist/kandev",
})
t.Setenv(envRunningAsService, "true")
t.Setenv(envServiceMode, "user")
t.Setenv(envServiceManager, "systemd")
t.Setenv(envInstallKind, installKindLocal)
t.Setenv(envServiceMetadata, metadataPath)
svc := NewService(newTestPool(t), "v1.0.0", nil, logger.Default(), WithHomeDir(homeDir))
resp, err := svc.Get()
if err != nil {
t.Fatalf("Get: %v", err)
}
if !resp.Install.ManagedService {
t.Fatalf("expected service to be recognised as managed")
}
if resp.ApplySupported {
t.Fatalf("ApplySupported=true for local bundle service")
}
if resp.ApplyUnsupportedReason == "" {
t.Fatalf("expected unsupported reason for local bundle service")
}
if !hasString(resp.ManualCommands, "kandev service install") {
t.Fatalf("manual commands = %v, want service install command", resp.ManualCommands)
}
if !hasString(resp.ManualCommands, "kandev service restart") {
t.Fatalf("manual commands = %v, want service restart command", resp.ManualCommands)
}
}
func TestManualCommandsNPXHasNoDuplicateBinaryName(t *testing.T) {
cmds := manualCommands(InstallStateResponse{Kind: installKindNPX, Mode: installModeUser}, "v1.2.3")
if !hasString(cmds, "npx -y kandev@1.2.3 service install") {
+13
View File
@@ -187,4 +187,17 @@ describe("captureLauncher shim resolution", () => {
expect(info.kind).toBe("homebrew");
expect(info.shimPath).toBeUndefined();
});
it("classifies a non-Cellar bundle as a local checkout install", () => {
const cliEntry = "/Users/alice/src/kandev/dist/kandev/cli/bin/cli.js";
process.argv = ["/path/to/node", cliEntry];
vi.stubEnv("KANDEV_BUNDLE_DIR", "/Users/alice/src/kandev/dist/kandev");
vi.spyOn(fs, "existsSync").mockImplementation((p) => p === cliEntry);
vi.spyOn(fs, "realpathSync").mockReturnValue(cliEntry);
const info = captureLauncher();
expect(info.kind).toBe("local");
expect(info.bundleDir).toBe("/Users/alice/src/kandev/dist/kandev");
expect(info.shimPath).toBeUndefined();
});
});
+5 -3
View File
@@ -34,7 +34,7 @@ export function macosSystemPlistPath(): string {
return path.join(MACOS_SYSTEM_DAEMON_DIR, `${LAUNCHD_LABEL}.plist`);
}
export type LauncherKind = "homebrew" | "npm" | "npx" | "unknown";
export type LauncherKind = "homebrew" | "npm" | "npx" | "local" | "unknown";
export type LauncherInfo = {
/** Absolute path to node executable (process.execPath at install time). */
@@ -43,7 +43,7 @@ export type LauncherInfo = {
cliEntry: string;
/** Best-guess of how kandev was installed. Used to seed env vars. */
kind: LauncherKind;
/** KANDEV_BUNDLE_DIR if set (Homebrew sets this). */
/** KANDEV_BUNDLE_DIR if set (Homebrew and local checkout installs set this). */
bundleDir?: string;
/** KANDEV_VERSION if set (Homebrew sets this). */
version?: string;
@@ -96,7 +96,9 @@ export function captureLauncher(): LauncherInfo {
const bundleDir = process.env.KANDEV_BUNDLE_DIR;
const version = process.env.KANDEV_VERSION;
const kind: LauncherKind = bundleDir
? "homebrew"
? homebrewShimPath(cliEntry)
? "homebrew"
: "local"
: looksLikeNpxEntry(cliEntry)
? "npx"
: cliEntry.includes(`${path.sep}node_modules${path.sep}`)
+29
View File
@@ -18,6 +18,35 @@ sudo kandev service install --system
After install, kandev is reachable at `http://localhost:38429` (or `--port <N>` if you passed it).
## Run the Current Checkout as a Service
When developing from a cloned repo, use the root Make targets instead of the globally installed `kandev` binary. They install dependencies, build the currently checked-out branch, assemble a local release-style bundle under `dist/kandev`, and install the service with `KANDEV_BUNDLE_DIR` pointing at that bundle.
```bash
git checkout my-branch
make service-install
```
Useful targets:
```bash
make service-install # user service from the current checkout
make service-install PORT=3000
make service-install HOME_DIR=/path/to/kandev-home
make service-install NO_BOOT_START=1
make service-install-system # system service install; other targets below use the user service
make service-status
make service-logs
make service-logs-follow
make service-start
make service-stop
make service-restart
make service-uninstall
make service-config
```
The service runs the built snapshot in `dist/kandev`, not live source files. After switching branches or changing code, rerun `make service-install` to rebuild and refresh the service unit. Checkout-based services are marked as a local install kind, so the System → Updates page will not offer one-click npm/Homebrew self-update; update by rebuilding from the desired branch.
## User Mode vs `--system` Mode
| | **User mode** (default) | **`--system` mode** |