@dannylee1020/kkt 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +275 -0
  3. package/assets/kkt-readme-modern.png +0 -0
  4. package/bin/kkt-install.mjs +25 -0
  5. package/cmd/kkt/main.go +15 -0
  6. package/go.mod +3 -0
  7. package/internal/workflow/cli.go +349 -0
  8. package/internal/workflow/cli_test.go +1266 -0
  9. package/internal/workflow/guardrails.go +900 -0
  10. package/internal/workflow/init.go +164 -0
  11. package/internal/workflow/init_test.go +113 -0
  12. package/internal/workflow/operations.go +1855 -0
  13. package/internal/workflow/validation_commands.go +291 -0
  14. package/internal/workflow/workspace.go +802 -0
  15. package/internal/workflow/workspace_test.go +285 -0
  16. package/package.json +45 -0
  17. package/scripts/install-cli.sh +210 -0
  18. package/scripts/install.sh +644 -0
  19. package/skills/kkt/SKILL.md +74 -0
  20. package/skills/kkt/references/discovery-tooling.md +53 -0
  21. package/skills/kkt/references/feature-optimization-model.md +120 -0
  22. package/skills/kkt/references/kkt-kernel.md +58 -0
  23. package/skills/kkt/references/layered-modeling-methods.md +101 -0
  24. package/skills/kkt/references/plan-assimilation.md +46 -0
  25. package/skills/kkt/references/schemas.md +231 -0
  26. package/skills/kkt/references/state-contract.md +76 -0
  27. package/skills/kkt-loop/SKILL.md +99 -0
  28. package/skills/kkt-loop/references/discovery-tooling.md +53 -0
  29. package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
  30. package/skills/kkt-loop/references/kkt-kernel.md +58 -0
  31. package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
  32. package/skills/kkt-loop/references/plan-assimilation.md +46 -0
  33. package/skills/kkt-loop/references/schemas.md +231 -0
  34. package/skills/kkt-loop/references/state-contract.md +76 -0
  35. package/skills/kkt-model/SKILL.md +76 -0
  36. package/skills/kkt-model/references/discovery-tooling.md +53 -0
  37. package/skills/kkt-model/references/feature-optimization-model.md +120 -0
  38. package/skills/kkt-model/references/kkt-kernel.md +58 -0
  39. package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
  40. package/skills/kkt-model/references/plan-assimilation.md +46 -0
  41. package/skills/kkt-model/references/schemas.md +231 -0
  42. package/skills/kkt-model/references/state-contract.md +76 -0
  43. package/skills/kkt-run/SKILL.md +62 -0
  44. package/skills/kkt-run/references/discovery-tooling.md +53 -0
  45. package/skills/kkt-run/references/feature-optimization-model.md +120 -0
  46. package/skills/kkt-run/references/kkt-kernel.md +58 -0
  47. package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
  48. package/skills/kkt-run/references/plan-assimilation.md +46 -0
  49. package/skills/kkt-run/references/schemas.md +231 -0
  50. package/skills/kkt-run/references/state-contract.md +76 -0
@@ -0,0 +1,285 @@
1
+ package workflow
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ func TestStartWorkflowCreatesPlanState(t *testing.T) {
11
+ root := t.TempDir()
12
+ workspace, err := StartWorkflow(root, "implement KKT workflow CLI", "plan")
13
+ if err != nil {
14
+ t.Fatal(err)
15
+ }
16
+ if workspace.Path != filepath.Join(root, ".kkt") {
17
+ t.Fatalf("Path = %q, want %q", workspace.Path, filepath.Join(root, ".kkt"))
18
+ }
19
+ required := []string{"kkt.yaml"}
20
+ for _, name := range required {
21
+ if _, err := os.Stat(filepath.Join(workspace.Path, name)); err != nil {
22
+ t.Fatalf("missing %s: %v", name, err)
23
+ }
24
+ }
25
+ current, err := os.ReadFile(filepath.Join(root, ".kkt", "current"))
26
+ if err != nil {
27
+ t.Fatal(err)
28
+ }
29
+ if len(current) == 0 {
30
+ t.Fatal("current pointer is empty")
31
+ }
32
+ state, err := os.ReadFile(filepath.Join(workspace.Path, "kkt.yaml"))
33
+ if err != nil {
34
+ t.Fatal(err)
35
+ }
36
+ if text := string(state); !strings.Contains(text, "workspace_type: plan") || !strings.Contains(text, "profile: plan") {
37
+ t.Fatalf("plan state did not use plan profile:\n%s", text)
38
+ }
39
+ for _, want := range []string{
40
+ "planning_contract:",
41
+ "objective_function:",
42
+ "files_to_modify:",
43
+ "constraint_functions:",
44
+ "decision_variables:",
45
+ "validation_proof:",
46
+ } {
47
+ if !strings.Contains(string(state), want) {
48
+ t.Fatalf("plan state missing %q:\n%s", want, state)
49
+ }
50
+ }
51
+ }
52
+
53
+ func TestStartWorkflowCreatesModelWorkspace(t *testing.T) {
54
+ root := t.TempDir()
55
+ workspace, err := StartWorkflow(root, "choose API shape", "model")
56
+ if err != nil {
57
+ t.Fatal(err)
58
+ }
59
+ if got, want := filepath.Dir(workspace.Path), filepath.Join(root, ".kkt", "model"); got != want {
60
+ t.Fatalf("workspace parent = %q, want %q", got, want)
61
+ }
62
+ required := []string{"kkt.yaml", "intent.md", "discovery.md", "model.md", "guardrails.json"}
63
+ for _, name := range required {
64
+ if _, err := os.Stat(filepath.Join(workspace.Path, name)); err != nil {
65
+ t.Fatalf("missing %s: %v", name, err)
66
+ }
67
+ }
68
+ if _, err := os.Stat(filepath.Join(workspace.Path, "plan.md")); !os.IsNotExist(err) {
69
+ t.Fatalf("plan.md should not exist for model workspace: %v", err)
70
+ }
71
+ state, err := os.ReadFile(filepath.Join(workspace.Path, "kkt.yaml"))
72
+ if err != nil {
73
+ t.Fatal(err)
74
+ }
75
+ if text := string(state); !strings.Contains(text, "active_layer: intent") || !strings.Contains(text, "method: pending") {
76
+ t.Fatalf("model workspace should start with pending intent:\n%s", text)
77
+ }
78
+ }
79
+
80
+ func TestStartWorkflowCreatesLoopWorkspace(t *testing.T) {
81
+ root := t.TempDir()
82
+ workspace, err := StartWorkflow(root, "run multi-step migration", "loop")
83
+ if err != nil {
84
+ t.Fatal(err)
85
+ }
86
+ if got, want := filepath.Dir(workspace.Path), filepath.Join(root, ".kkt", "loop"); got != want {
87
+ t.Fatalf("workspace parent = %q, want %q", got, want)
88
+ }
89
+ required := []string{"kkt.yaml", "intent.md", "discovery.md", "model.md", "guardrails.json", "plan.md", "progress.md", "evidence.md", "notes.md", "events.jsonl"}
90
+ for _, name := range required {
91
+ if _, err := os.Stat(filepath.Join(workspace.Path, name)); err != nil {
92
+ t.Fatalf("missing %s: %v", name, err)
93
+ }
94
+ }
95
+ state, err := os.ReadFile(filepath.Join(workspace.Path, "kkt.yaml"))
96
+ if err != nil {
97
+ t.Fatal(err)
98
+ }
99
+ if text := string(state); !strings.Contains(text, "loop_state:") || !strings.Contains(text, "acceptance_criteria:") {
100
+ t.Fatalf("loop state block missing from kkt.yaml:\n%s", text)
101
+ }
102
+ if text := string(state); !strings.Contains(text, "active_layer: intent") || !strings.Contains(text, "method_invocations: []") {
103
+ t.Fatalf("loop workspace should start with pending method selection:\n%s", text)
104
+ }
105
+ }
106
+
107
+ func TestStartWorkflowCreatesRunWorkspace(t *testing.T) {
108
+ root := t.TempDir()
109
+ workspace, err := StartWorkflow(root, "execute selected model", "run")
110
+ if err != nil {
111
+ t.Fatal(err)
112
+ }
113
+ if got, want := filepath.Dir(workspace.Path), filepath.Join(root, ".kkt", "run"); got != want {
114
+ t.Fatalf("workspace parent = %q, want %q", got, want)
115
+ }
116
+ required := []string{"kkt.yaml", "intent.md", "discovery.md", "model.md", "guardrails.json", "plan.md", "progress.md", "evidence.md", "notes.md"}
117
+ for _, name := range required {
118
+ if _, err := os.Stat(filepath.Join(workspace.Path, name)); err != nil {
119
+ t.Fatalf("missing %s: %v", name, err)
120
+ }
121
+ }
122
+ if _, err := os.Stat(filepath.Join(workspace.Path, "events.jsonl")); !os.IsNotExist(err) {
123
+ t.Fatalf("events.jsonl should not exist for run workspace: %v", err)
124
+ }
125
+ state, err := os.ReadFile(filepath.Join(workspace.Path, "kkt.yaml"))
126
+ if err != nil {
127
+ t.Fatal(err)
128
+ }
129
+ if text := string(state); !strings.Contains(text, "workspace_type: run") || !strings.Contains(text, "guardrails: guardrails.json") {
130
+ t.Fatalf("run state missing expected contract:\n%s", text)
131
+ }
132
+ }
133
+
134
+ func TestResolveWorkspaceUsesCurrentPointer(t *testing.T) {
135
+ root := t.TempDir()
136
+ created, err := StartWorkflow(root, "choose API shape", "model")
137
+ if err != nil {
138
+ t.Fatal(err)
139
+ }
140
+ resolved, err := ResolveWorkspace(root, "")
141
+ if err != nil {
142
+ t.Fatal(err)
143
+ }
144
+ if resolved != created.Path {
145
+ t.Fatalf("resolved = %q, want %q", resolved, created.Path)
146
+ }
147
+ }
148
+
149
+ func TestStartWorkflowUsesNearestGitRoot(t *testing.T) {
150
+ root := t.TempDir()
151
+ if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil {
152
+ t.Fatal(err)
153
+ }
154
+ nested := filepath.Join(root, "packages", "app")
155
+ if err := os.MkdirAll(nested, 0o755); err != nil {
156
+ t.Fatal(err)
157
+ }
158
+
159
+ workspace, err := StartWorkflow(nested, "anchor workspace at project root", "plan")
160
+ if err != nil {
161
+ t.Fatal(err)
162
+ }
163
+ if got, want := workspace.Path, filepath.Join(root, ".kkt"); got != want {
164
+ t.Fatalf("workspace = %q, want %q", got, want)
165
+ }
166
+ if _, err := os.Stat(filepath.Join(nested, ".kkt")); !os.IsNotExist(err) {
167
+ t.Fatalf("nested .kkt should not exist: %v", err)
168
+ }
169
+ }
170
+
171
+ func TestResolveWorkspaceUsesProjectRootFromNestedDirectory(t *testing.T) {
172
+ root := t.TempDir()
173
+ if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil {
174
+ t.Fatal(err)
175
+ }
176
+ firstNested := filepath.Join(root, "packages", "app")
177
+ secondNested := filepath.Join(root, "cmd", "tool")
178
+ if err := os.MkdirAll(firstNested, 0o755); err != nil {
179
+ t.Fatal(err)
180
+ }
181
+ if err := os.MkdirAll(secondNested, 0o755); err != nil {
182
+ t.Fatal(err)
183
+ }
184
+
185
+ created, err := StartWorkflow(firstNested, "choose API shape", "model")
186
+ if err != nil {
187
+ t.Fatal(err)
188
+ }
189
+ resolved, err := ResolveWorkspace(secondNested, "")
190
+ if err != nil {
191
+ t.Fatal(err)
192
+ }
193
+ if resolved != created.Path {
194
+ t.Fatalf("resolved = %q, want %q", resolved, created.Path)
195
+ }
196
+ }
197
+
198
+ func TestProjectRootAcceptsGitFile(t *testing.T) {
199
+ root := t.TempDir()
200
+ if err := os.WriteFile(filepath.Join(root, ".git"), []byte("gitdir: ../.git/worktrees/app\n"), 0o644); err != nil {
201
+ t.Fatal(err)
202
+ }
203
+ nested := filepath.Join(root, "src")
204
+ if err := os.MkdirAll(nested, 0o755); err != nil {
205
+ t.Fatal(err)
206
+ }
207
+
208
+ workspace, err := StartWorkflow(nested, "support worktree roots", "plan")
209
+ if err != nil {
210
+ t.Fatal(err)
211
+ }
212
+ if got, want := workspace.Path, filepath.Join(root, ".kkt"); got != want {
213
+ t.Fatalf("workspace = %q, want %q", got, want)
214
+ }
215
+ }
216
+
217
+ func TestProjectRootFallsBackToStartOutsideGit(t *testing.T) {
218
+ root := t.TempDir()
219
+ nested := filepath.Join(root, "src")
220
+ if err := os.MkdirAll(nested, 0o755); err != nil {
221
+ t.Fatal(err)
222
+ }
223
+
224
+ resolved, err := projectRoot(nested)
225
+ if err != nil {
226
+ t.Fatal(err)
227
+ }
228
+ if resolved != nested {
229
+ t.Fatalf("projectRoot = %q, want %q", resolved, nested)
230
+ }
231
+ }
232
+
233
+ func TestResolveWorkspaceUsesExplicitPathOutsideProjectRoot(t *testing.T) {
234
+ root := t.TempDir()
235
+ if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil {
236
+ t.Fatal(err)
237
+ }
238
+ nested := filepath.Join(root, "src")
239
+ if err := os.MkdirAll(nested, 0o755); err != nil {
240
+ t.Fatal(err)
241
+ }
242
+ external := t.TempDir()
243
+ created, err := StartWorkflow(external, "external explicit workspace", "model")
244
+ if err != nil {
245
+ t.Fatal(err)
246
+ }
247
+
248
+ resolved, err := ResolveWorkspace(nested, created.Path)
249
+ if err != nil {
250
+ t.Fatal(err)
251
+ }
252
+ if resolved != created.Path {
253
+ t.Fatalf("resolved = %q, want %q", resolved, created.Path)
254
+ }
255
+ }
256
+
257
+ func TestValidateWorkspaceFailsPendingEvidence(t *testing.T) {
258
+ root := t.TempDir()
259
+ workspace, err := StartWorkflow(root, "implement KKT workflow CLI", "loop")
260
+ if err != nil {
261
+ t.Fatal(err)
262
+ }
263
+ result, err := ValidateWorkspace(workspace.Path)
264
+ if err != nil {
265
+ t.Fatal(err)
266
+ }
267
+ if result.OK {
268
+ t.Fatal("ValidateWorkspace should fail while evidence is pending")
269
+ }
270
+ }
271
+
272
+ func TestValidateModelWorkspaceDoesNotRequireExecutionFiles(t *testing.T) {
273
+ root := t.TempDir()
274
+ workspace, err := StartWorkflow(root, "choose API shape", "model")
275
+ if err != nil {
276
+ t.Fatal(err)
277
+ }
278
+ result, err := ValidateWorkspace(workspace.Path)
279
+ if err != nil {
280
+ t.Fatal(err)
281
+ }
282
+ if !result.OK {
283
+ t.Fatalf("ValidateWorkspace should accept model workspace, got issues: %v", result.Issues)
284
+ }
285
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@dannylee1020/kkt",
3
+ "version": "0.5.0",
4
+ "description": "KKT constrained-optimization workflow skills and CLI installer for coding agents.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dannylee1020/kkt.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/dannylee1020/kkt/issues"
13
+ },
14
+ "homepage": "https://github.com/dannylee1020/kkt#readme",
15
+ "keywords": [
16
+ "pi-package",
17
+ "agent-skills",
18
+ "coding-agent",
19
+ "claude-code",
20
+ "codex",
21
+ "opencode",
22
+ "pi",
23
+ "kkt"
24
+ ],
25
+ "bin": {
26
+ "kkt-install": "bin/kkt-install.mjs"
27
+ },
28
+ "pi": {
29
+ "skills": [
30
+ "./skills"
31
+ ],
32
+ "image": "https://raw.githubusercontent.com/dannylee1020/kkt/main/assets/kkt-readme-modern.png"
33
+ },
34
+ "files": [
35
+ "assets/",
36
+ "bin/",
37
+ "cmd/",
38
+ "internal/",
39
+ "scripts/",
40
+ "skills/",
41
+ "go.mod",
42
+ "README.md",
43
+ "LICENSE"
44
+ ]
45
+ }
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ kkt CLI installer
7
+
8
+ Usage:
9
+ scripts/install-cli.sh [options]
10
+ curl -fsSL https://raw.githubusercontent.com/dannylee1020/kkt/main/scripts/install-cli.sh | bash
11
+ curl -fsSL https://raw.githubusercontent.com/dannylee1020/kkt/main/scripts/install-cli.sh | bash -s -- [options]
12
+ curl -fsSL <install-cli.sh-url> | KKT_INSTALL_URL=<archive-url> bash -s -- [options]
13
+
14
+ Installs the Go kkt CLI. KKT skills can use this binary for deterministic
15
+ .kkt state scaffolding, status, next-step hints, and validation.
16
+
17
+ Options:
18
+ --bin-dir <path> Install the kkt CLI here. Defaults to ~/.local/bin.
19
+ --help Show this help.
20
+
21
+ Environment:
22
+ KKT_INSTALL_URL Source archive URL. Defaults to main branch archive.
23
+ KKT_BINARY_URL Explicit prebuilt kkt binary URL.
24
+ KKT_BUILD_FROM_SOURCE Build the CLI from source without downloading a release binary.
25
+ KKT_VERSION GitHub Release tag to install. Defaults to latest.
26
+ EOF
27
+ }
28
+
29
+ log() {
30
+ printf '%s\n' "$*" >&2
31
+ }
32
+
33
+ fail() {
34
+ printf 'Error: %s\n' "$*" >&2
35
+ exit 1
36
+ }
37
+
38
+ need_command() {
39
+ command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
40
+ }
41
+
42
+ script_dir() {
43
+ cd -- "$(dirname -- "$0")" && pwd -P
44
+ }
45
+
46
+ try_download_file() {
47
+ local url="$1"
48
+ local dest="$2"
49
+
50
+ if command -v curl >/dev/null 2>&1; then
51
+ curl -fsSL "$url" -o "$dest"
52
+ elif command -v wget >/dev/null 2>&1; then
53
+ wget -qO "$dest" "$url"
54
+ else
55
+ fail "curl or wget is required to download KKT."
56
+ fi
57
+ }
58
+
59
+ download_file() {
60
+ local url="$1"
61
+ local dest="$2"
62
+
63
+ try_download_file "$url" "$dest" || fail "Failed to download $url."
64
+ }
65
+
66
+ github_release_base_url() {
67
+ printf '%s\n' "https://github.com/dannylee1020/kkt/releases"
68
+ }
69
+
70
+ detect_platform() {
71
+ local os arch
72
+ os="$(uname -s)"
73
+ arch="$(uname -m)"
74
+
75
+ case "$os" in
76
+ Darwin) os="darwin" ;;
77
+ Linux) os="linux" ;;
78
+ *) return 1 ;;
79
+ esac
80
+
81
+ case "$arch" in
82
+ x86_64|amd64) arch="amd64" ;;
83
+ arm64|aarch64) arch="arm64" ;;
84
+ *) return 1 ;;
85
+ esac
86
+
87
+ printf '%s-%s\n' "$os" "$arch"
88
+ }
89
+
90
+ default_binary_url() {
91
+ local platform asset base
92
+ platform="$(detect_platform)" || return 1
93
+ asset="kkt-$platform"
94
+ base="$(github_release_base_url)"
95
+
96
+ if [ -n "${KKT_VERSION:-}" ]; then
97
+ printf '%s/download/%s/%s\n' "$base" "$KKT_VERSION" "$asset"
98
+ else
99
+ printf '%s/latest/download/%s\n' "$base" "$asset"
100
+ fi
101
+ }
102
+
103
+ default_archive_url() {
104
+ if [ -n "${KKT_VERSION:-}" ]; then
105
+ printf '%s\n' "https://github.com/dannylee1020/kkt/archive/refs/tags/${KKT_VERSION}.tar.gz"
106
+ return
107
+ fi
108
+ printf '%s\n' "https://github.com/dannylee1020/kkt/archive/refs/heads/main.tar.gz"
109
+ }
110
+
111
+ extract_archive() {
112
+ local archive="$1"
113
+ local dest="$2"
114
+
115
+ need_command tar
116
+ mkdir -p "$dest"
117
+ tar -xzf "$archive" -C "$dest" --strip-components 1
118
+ }
119
+
120
+ resolve_root() {
121
+ if [ -f "$0" ] && [ "$0" != "bash" ] && [ "$0" != "sh" ]; then
122
+ local dir
123
+ dir="$(script_dir)"
124
+ for root in "$dir" "$dir/.."; do
125
+ if [ -f "$root/cmd/kkt/main.go" ] && [ -d "$root/internal/workflow" ]; then
126
+ cd -- "$root"
127
+ pwd -P
128
+ return
129
+ fi
130
+ done
131
+ fi
132
+
133
+ local install_url tmp_dir archive
134
+ install_url="${KKT_INSTALL_URL:-$(default_archive_url)}"
135
+ tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/kkt-cli-install.XXXXXX")"
136
+ archive="$tmp_dir/kkt.tar.gz"
137
+ download_file "$install_url" "$archive"
138
+ log "source: downloaded $install_url"
139
+ extract_archive "$archive" "$tmp_dir/src"
140
+ printf '%s\n' "$tmp_dir/src"
141
+ }
142
+
143
+ expand_home() {
144
+ case "$1" in
145
+ \~) printf '%s\n' "$HOME" ;;
146
+ \~/*) printf '%s/%s\n' "$HOME" "${1#~/}" ;;
147
+ *) printf '%s\n' "$1" ;;
148
+ esac
149
+ }
150
+
151
+ parse_args() {
152
+ bin_dir="${KKT_BIN_DIR:-$HOME/.local/bin}"
153
+
154
+ while [ "$#" -gt 0 ]; do
155
+ case "$1" in
156
+ --bin-dir)
157
+ [ "$#" -ge 2 ] || fail "--bin-dir requires a value."
158
+ bin_dir="$(expand_home "$2")"
159
+ shift 2
160
+ ;;
161
+ --help)
162
+ usage
163
+ exit 0
164
+ ;;
165
+ *)
166
+ fail "Unknown argument: $1"
167
+ ;;
168
+ esac
169
+ done
170
+ }
171
+
172
+ install_cli() {
173
+ local kkt_command binary_url build_from_source root
174
+ kkt_command="$(expand_home "$bin_dir")/kkt"
175
+ build_from_source="${KKT_BUILD_FROM_SOURCE:-}"
176
+ binary_url="${KKT_BINARY_URL:-}"
177
+ if [ -z "$binary_url" ] && [ "$build_from_source" != "1" ] && [ "$build_from_source" != "true" ]; then
178
+ binary_url="$(default_binary_url || true)"
179
+ fi
180
+
181
+ mkdir -p "$(dirname "$kkt_command")"
182
+
183
+ if [ -n "$binary_url" ]; then
184
+ log "cli: downloading $binary_url"
185
+ if try_download_file "$binary_url" "$kkt_command"; then
186
+ chmod +x "$kkt_command"
187
+ log "cli: downloaded $kkt_command"
188
+ return
189
+ fi
190
+ rm -f "$kkt_command"
191
+ log "cli: prebuilt unavailable $binary_url"
192
+ fi
193
+
194
+ if command -v go >/dev/null 2>&1; then
195
+ root="$(resolve_root)"
196
+ log "cli: building $kkt_command"
197
+ (cd "$root" && go build -ldflags="-X github.com/dannylee1020/kkt/internal/workflow.Version=${KKT_VERSION:-dev}" -o "$kkt_command" ./cmd/kkt)
198
+ else
199
+ fail "Could not install kkt CLI: prebuilt binary unavailable and Go is not installed."
200
+ fi
201
+
202
+ log "cli: built $kkt_command"
203
+ }
204
+
205
+ main() {
206
+ parse_args "$@"
207
+ install_cli
208
+ }
209
+
210
+ main "$@"