@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.
- package/LICENSE +201 -0
- package/README.md +275 -0
- package/assets/kkt-readme-modern.png +0 -0
- package/bin/kkt-install.mjs +25 -0
- package/cmd/kkt/main.go +15 -0
- package/go.mod +3 -0
- package/internal/workflow/cli.go +349 -0
- package/internal/workflow/cli_test.go +1266 -0
- package/internal/workflow/guardrails.go +900 -0
- package/internal/workflow/init.go +164 -0
- package/internal/workflow/init_test.go +113 -0
- package/internal/workflow/operations.go +1855 -0
- package/internal/workflow/validation_commands.go +291 -0
- package/internal/workflow/workspace.go +802 -0
- package/internal/workflow/workspace_test.go +285 -0
- package/package.json +45 -0
- package/scripts/install-cli.sh +210 -0
- package/scripts/install.sh +644 -0
- package/skills/kkt/SKILL.md +74 -0
- package/skills/kkt/references/discovery-tooling.md +53 -0
- package/skills/kkt/references/feature-optimization-model.md +120 -0
- package/skills/kkt/references/kkt-kernel.md +58 -0
- package/skills/kkt/references/layered-modeling-methods.md +101 -0
- package/skills/kkt/references/plan-assimilation.md +46 -0
- package/skills/kkt/references/schemas.md +231 -0
- package/skills/kkt/references/state-contract.md +76 -0
- package/skills/kkt-loop/SKILL.md +99 -0
- package/skills/kkt-loop/references/discovery-tooling.md +53 -0
- package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
- package/skills/kkt-loop/references/kkt-kernel.md +58 -0
- package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-loop/references/plan-assimilation.md +46 -0
- package/skills/kkt-loop/references/schemas.md +231 -0
- package/skills/kkt-loop/references/state-contract.md +76 -0
- package/skills/kkt-model/SKILL.md +76 -0
- package/skills/kkt-model/references/discovery-tooling.md +53 -0
- package/skills/kkt-model/references/feature-optimization-model.md +120 -0
- package/skills/kkt-model/references/kkt-kernel.md +58 -0
- package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-model/references/plan-assimilation.md +46 -0
- package/skills/kkt-model/references/schemas.md +231 -0
- package/skills/kkt-model/references/state-contract.md +76 -0
- package/skills/kkt-run/SKILL.md +62 -0
- package/skills/kkt-run/references/discovery-tooling.md +53 -0
- package/skills/kkt-run/references/feature-optimization-model.md +120 -0
- package/skills/kkt-run/references/kkt-kernel.md +58 -0
- package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-run/references/plan-assimilation.md +46 -0
- package/skills/kkt-run/references/schemas.md +231 -0
- 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 "$@"
|