ketoy-dev 0.1.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 +17 -0
- package/README.md +101 -0
- package/SECURITY.md +34 -0
- package/dist/ketoy.js +3165 -0
- package/dist/ketoy.js.map +1 -0
- package/package.json +78 -0
- package/skills/ketoy/README.md +50 -0
- package/skills/ketoy/SKILL.md +148 -0
- package/skills/ketoy/examples/capabilities-stubs.kt +60 -0
- package/skills/ketoy/examples/hilt-config.kt +192 -0
- package/skills/ketoy/examples/no-hilt-config.kt +101 -0
- package/skills/ketoy/examples/todo-screen.kt +156 -0
- package/skills/ketoy/guides/build-and-analyze.md +87 -0
- package/skills/ketoy/guides/diagnose-errors.md +129 -0
- package/skills/ketoy/guides/init-project.md +127 -0
- package/skills/ketoy/guides/migrate.md +190 -0
- package/skills/ketoy/guides/publish-deferred.md +46 -0
- package/skills/ketoy/guides/safe-edits.md +141 -0
- package/skills/ketoy/reference/architecture-cheatsheet.md +122 -0
- package/skills/ketoy/reference/capabilities.md +122 -0
- package/skills/ketoy/reference/forbidden-apis.md +149 -0
- package/skills/ketoy/reference/supported-composables.md +80 -0
- package/skills/ketoy/reference/supported-constructors.md +57 -0
- package/skills/ketoy/reference/supported-modifiers.md +76 -0
- package/skills/ketoy/templates/app-build.gradle.kts.tmpl +109 -0
- package/skills/ketoy/templates/ketoy-capabilities.json.tmpl +21 -0
- package/skills/ketoy/templates/manifest-snippet.xml.tmpl +33 -0
- package/templates/HelloKetoyScreen.kt.tmpl +51 -0
- package/templates/MainActivity.kt.tmpl +53 -0
- package/templates/MyApplication.kt.tmpl +88 -0
- package/templates/ketoy-capabilities.json.tmpl +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ketoy-dev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-powered CLI for Ketoy — scaffold, write, migrate, diagnose, and build .ktx bundles for Android. BYO API key (Anthropic, OpenAI, Google, Mistral, Groq, xAI, OpenRouter, or local Ollama).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ketoy": "./dist/ketoy.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skills",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"SECURITY.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"start": "node dist/ketoy.js",
|
|
25
|
+
"prepack": "node scripts/prepack-skills.mjs",
|
|
26
|
+
"postpack": "node scripts/postpack-skills.mjs",
|
|
27
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@ai-sdk/anthropic": "^1.0.0",
|
|
31
|
+
"@ai-sdk/google": "^1.0.0",
|
|
32
|
+
"@ai-sdk/groq": "^1.0.0",
|
|
33
|
+
"@ai-sdk/mistral": "^1.0.0",
|
|
34
|
+
"@ai-sdk/openai": "^1.0.0",
|
|
35
|
+
"@ai-sdk/xai": "^1.0.0",
|
|
36
|
+
"@inquirer/prompts": "^8.4.3",
|
|
37
|
+
"@openrouter/ai-sdk-provider": "^0.0.6",
|
|
38
|
+
"ai": "^4.0.0",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"execa": "^9.4.0",
|
|
41
|
+
"fast-xml-parser": "^4.5.0",
|
|
42
|
+
"ollama-ai-provider": "^1.0.0",
|
|
43
|
+
"picocolors": "^1.1.0",
|
|
44
|
+
"tinyglobby": "^0.2.10",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.7.0",
|
|
49
|
+
"tsup": "^8.3.0",
|
|
50
|
+
"typescript": "^5.6.0"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"ketoy",
|
|
54
|
+
"ketoyvm",
|
|
55
|
+
"kbc",
|
|
56
|
+
"android",
|
|
57
|
+
"compose",
|
|
58
|
+
"jetpack-compose",
|
|
59
|
+
"server-driven",
|
|
60
|
+
"cli",
|
|
61
|
+
"ai-agent",
|
|
62
|
+
"vercel-ai-sdk"
|
|
63
|
+
],
|
|
64
|
+
"author": "Aditya Shinde",
|
|
65
|
+
"license": "Apache-2.0",
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://github.com/developerchunk/ketoy-extended.git",
|
|
69
|
+
"directory": "ketoy-cli"
|
|
70
|
+
},
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/developerchunk/ketoy-extended/issues"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://ketoy.dev",
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Ketoy Skill
|
|
2
|
+
|
|
3
|
+
Operational skill for an AI agent helping developers adopt Ketoy.
|
|
4
|
+
|
|
5
|
+
**Entry point**: [SKILL.md](SKILL.md). Read this first.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
skills/ketoy/
|
|
11
|
+
├── SKILL.md # entry — frontmatter + operating rules
|
|
12
|
+
├── reference/ # ground truth for "is X supported"
|
|
13
|
+
│ ├── supported-composables.md
|
|
14
|
+
│ ├── supported-constructors.md
|
|
15
|
+
│ ├── supported-modifiers.md
|
|
16
|
+
│ ├── capabilities.md
|
|
17
|
+
│ ├── forbidden-apis.md
|
|
18
|
+
│ └── architecture-cheatsheet.md
|
|
19
|
+
├── guides/ # workflow playbooks
|
|
20
|
+
│ ├── init-project.md
|
|
21
|
+
│ ├── safe-edits.md # ← surgical-edits policy (READ FIRST)
|
|
22
|
+
│ ├── migrate.md
|
|
23
|
+
│ ├── diagnose-errors.md
|
|
24
|
+
│ ├── build-and-analyze.md
|
|
25
|
+
│ └── publish-deferred.md
|
|
26
|
+
├── examples/ # paste-ready Kotlin code
|
|
27
|
+
│ ├── todo-screen.kt
|
|
28
|
+
│ ├── capabilities-stubs.kt
|
|
29
|
+
│ ├── hilt-config.kt
|
|
30
|
+
│ └── no-hilt-config.kt
|
|
31
|
+
└── templates/ # static templates for `ketoy init`
|
|
32
|
+
├── app-build.gradle.kts.tmpl
|
|
33
|
+
├── ketoy-capabilities.json.tmpl
|
|
34
|
+
└── manifest-snippet.xml.tmpl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## When the skill applies
|
|
38
|
+
|
|
39
|
+
The agent should load this skill whenever the user's task involves:
|
|
40
|
+
|
|
41
|
+
- Adding Ketoy to an Android project
|
|
42
|
+
- Writing `@KetoyComposable` / `@KetoyEntryPoint` / `@KetoyViewModel` / `@KetoyCapabilityStub`
|
|
43
|
+
- Migrating an existing Compose screen to KBC
|
|
44
|
+
- Diagnosing `KetoyBC:` errors, `.ktx` runtime errors
|
|
45
|
+
- Building, analyzing, or shipping `.ktx` bundles
|
|
46
|
+
- Working with `dev.ketoy.vm:*` Maven coordinates
|
|
47
|
+
|
|
48
|
+
## Version this skill targets
|
|
49
|
+
|
|
50
|
+
Ketoy **0.3.4-alpha** on Maven Central. Coordinates use group `dev.ketoy.vm`. Bundle wire format v2 with `minAppVersion` trailing field. Closure conversion via `KBCValue.ClosureRef` shipped. Phase 11E inline-source app bundle mode is the standard adoption path (one `.ktx` per app module, `bundleId = "main"`).
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ketoy
|
|
3
|
+
description: Operate the Ketoy CLI agent. Use for any task involving Ketoy/KetoyBC — scaffolding a Ketoy-enabled Android project, writing or migrating `@KetoyComposable` screens, defining `@KetoyCapabilityStub` declarations, wiring Hilt/non-Hilt configuration, diagnosing compiler-plugin errors, building `.ktx` bundles, inspecting bundle contents. Triggers on: "ketoy", "KBC", ".ktx", "Ketoy", "server-driven Compose", `@KetoyComposable`, `@KetoyCapabilityStub`, `dev.ketoy.vm:*` dependencies.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ketoy Skill — Operating Manual
|
|
7
|
+
|
|
8
|
+
You are a Ketoy CLI agent. Your job is to help Android developers adopt Ketoy: a register-based VM that executes Compose UI from server-shipped `.ktx` bytecode bundles inside their app. The app is the OS; the server ships programs.
|
|
9
|
+
|
|
10
|
+
You run inside the `@ketoy/cli` Node.js CLI built on Vercel AI SDK — the user picked their own AI provider (Anthropic / OpenAI / Google / Mistral / Groq / xAI / OpenRouter / Ollama). The tool surface (`readFile`, `writeFile`, `editFile`, `grep`, `glob`, `bash`, `analyzeKtx`) is identical across providers. See [/KETOY_CLI.md](../../KETOY_CLI.md) at the repo root for the full CLI design.
|
|
11
|
+
|
|
12
|
+
## Truth sources, in priority order
|
|
13
|
+
|
|
14
|
+
1. **The user's working directory** — read their actual `build.gradle.kts`, `AndroidManifest.xml`, KBC source. Never assume.
|
|
15
|
+
2. **`reference/`** in this skill — the curated catalog of what Ketoy actually supports today. Treat as ground truth for "is X supported".
|
|
16
|
+
3. **`KNOWN_ISSUES.md`** and **`CLAUDE.md`** at the repo root of the Ketoy project, if present. Read them when diagnosing weird behavior.
|
|
17
|
+
4. **`docs/`** at the Ketoy repo or `ketoy.dev/docs` — secondary. Some pages still describe per-screen bundles; the actual ship state is **one `.ktx` per app module** (`bundleId = "main"`).
|
|
18
|
+
|
|
19
|
+
## Hard rules (never violate)
|
|
20
|
+
|
|
21
|
+
- **Never rewrite existing user files in full.** All edits to `build.gradle.kts`, `AndroidManifest.xml`, `MainActivity.kt`, `Application` subclasses, `settings.gradle.kts`, or any pre-existing user file MUST be surgical — single-line additions or single-block appends at well-identified anchors. Templates in `templates/` describe the *final shape*, not the *bytes to write*. See [guides/safe-edits.md](guides/safe-edits.md) for the per-file insertion strategy. Show a diff and confirm before touching anything the user is likely to have hand-tuned.
|
|
22
|
+
- KBC source CANNOT call:
|
|
23
|
+
- Reflection (`kotlin.reflect.*`, `Class.get*`, `java.lang.reflect.*`)
|
|
24
|
+
- File I/O (`java.io.*`, `java.nio.*`, `kotlin.io.*`)
|
|
25
|
+
- Direct Android APIs (`android.*`, `androidx.*` outside the registered Compose surface)
|
|
26
|
+
- `GlobalScope`, `runBlocking`, `CoroutineScope(...)` factory
|
|
27
|
+
- Every Android/system side-effect goes through a `@KetoyCapabilityStub` declared in the KBC module and registered in the host's `CapabilityRegistry`.
|
|
28
|
+
- Every visible Compose composable must have a generated adapter — i.e. live in `adapter-scan-roots.txt` (or app-specific scan-roots). If it's not there, it's not callable.
|
|
29
|
+
- KBC closures over outer-scope locals work for `val`/captured reads only (shipped via `KBCValue.ClosureRef`). Don't try clever capture patterns; keep lambdas small.
|
|
30
|
+
|
|
31
|
+
## Workflow you offer
|
|
32
|
+
|
|
33
|
+
### A. Bootstrap (`ketoy init`) — deterministic, no AI tokens
|
|
34
|
+
|
|
35
|
+
When the user wants to add Ketoy to a project, **prefer running deterministic scaffolding over generating files**. The CLI's `init` command makes **surgical, additive edits** to existing files (never full rewrites — see [guides/safe-edits.md](guides/safe-edits.md)) and creates new files where none exist:
|
|
36
|
+
|
|
37
|
+
1. Plugin + dependencies to `build.gradle.kts` (root + app)
|
|
38
|
+
2. `Application` class (if Hilt: `@HiltAndroidApp`; else: plain `Application`)
|
|
39
|
+
3. `MainActivity` wired with `KetoyScreen` host (Hilt vs non-Hilt variants)
|
|
40
|
+
4. `AndroidManifest.xml` updated `android:name` for Application
|
|
41
|
+
5. `app/ketoy-capabilities.json` skeleton
|
|
42
|
+
6. `app/src/main/java/<pkg>/ketoyscreens/Capabilities.kt` with standard `NAV_*` / `VM_*` stubs
|
|
43
|
+
7. The `ketoy { exportFromAppModule = true; bundleId = "main"; bundleVariant = "release" }` block
|
|
44
|
+
8. `.gitignore` entry: `**/keys/*-private.key`
|
|
45
|
+
|
|
46
|
+
Detect Hilt by grepping for `dagger.hilt.android` in any existing `build.gradle.kts` or `@HiltAndroidApp`. **Non-Hilt setup is the default first pass**; if the user later adds Hilt, run `ketoy init --hilt` to upgrade.
|
|
47
|
+
|
|
48
|
+
See `templates/` for the canonical content.
|
|
49
|
+
|
|
50
|
+
### B. Write a new `@KetoyComposable` screen
|
|
51
|
+
|
|
52
|
+
Use [examples/todo-screen.kt](examples/todo-screen.kt) as the canonical pattern. Rules:
|
|
53
|
+
|
|
54
|
+
- File goes under `app/src/main/.../ketoyscreens/`
|
|
55
|
+
- Annotate with `@KetoyEntryPoint @KetoyComposable @Composable` if it's a screen-root.
|
|
56
|
+
- Only call composables listed in [reference/supported-composables.md](reference/supported-composables.md).
|
|
57
|
+
- Use `Modifier` chains only with the named-arg forms documented in [reference/supported-modifiers.md](reference/supported-modifiers.md).
|
|
58
|
+
- For state, dispatch capabilities — `vmGetState(key)`, `vmSetState(key, value)`, `observeTodos()`, etc. — declared as `@KetoyCapabilityStub` in `ketoyscreens/Capabilities.kt`.
|
|
59
|
+
|
|
60
|
+
### C. Migrate an existing Compose screen to KBC
|
|
61
|
+
|
|
62
|
+
Per-file migration only. Never offer "migrate the whole project." Workflow:
|
|
63
|
+
|
|
64
|
+
1. **Audit** the source: list every composable, every Android/Java API call, every state holder, every `LaunchedEffect` / `Flow.collect`.
|
|
65
|
+
2. **Map each call site** to one of:
|
|
66
|
+
- A supported composable → leave it (verify in `reference/supported-composables.md`).
|
|
67
|
+
- A side effect → invent a capability ID at `0x4000+`, add a stub, add the host-side registration.
|
|
68
|
+
- A forbidden API → propose an alternative (Capability call, or "this can't be migrated; keep it native").
|
|
69
|
+
3. **Rewrite the file** with `@KetoyComposable`-annotated public function(s) and any captured `val`s declared at the top of the function body (closure-converted to `KBCValue.ClosureRef`).
|
|
70
|
+
4. **Show the user a diff** before writing — migrations frequently change semantics (e.g. `viewModel()` → `vmGetState`).
|
|
71
|
+
5. **Run `./gradlew :app:ketoyBundle --rerun-tasks`** — surface the actual error if compilation fails.
|
|
72
|
+
|
|
73
|
+
See [guides/migrate.md](guides/migrate.md) for the playbook.
|
|
74
|
+
|
|
75
|
+
### D. Diagnose build / runtime errors
|
|
76
|
+
|
|
77
|
+
Read `guides/diagnose-errors.md`. Common patterns:
|
|
78
|
+
|
|
79
|
+
- `KetoyBC: Direct access to android.*` → host API call, route through capability.
|
|
80
|
+
- `KetoyBC: Direct access to FontWeight.Companion.<get-X>` → token not in `ComposeTokenRegistry` — file a Ketoy issue OR replace with a supported one.
|
|
81
|
+
- `KetoyBC: Reached via: X → Y → Z` → forbidden call in a transitive helper; fix at the leaf (Z), not the root (X).
|
|
82
|
+
- `KetoyBundleMalformedException` at runtime → signature mismatch or stale `.ktx`. Re-run `./gradlew :app:ketoyBundle --rerun-tasks` and verify the public key matches.
|
|
83
|
+
- `KBCEngineNotAvailableException` → `KetoyConfig.enableJIT` set but `dexCacheDir` missing, OR using suspend without `parentScope`.
|
|
84
|
+
|
|
85
|
+
### E. Build & analyze
|
|
86
|
+
|
|
87
|
+
- **Build**: `./gradlew :app:ketoyBundle --rerun-tasks` from project root, or `:app:assembleDebug` (which transitively triggers the bundle).
|
|
88
|
+
- **Analyze**: use the CLI's `analyze <path-to-.ktx>` — wraps a JVM subprocess that calls `KtxReader.read` and prints function count, manifest entries, modifier table size, signature status, and string pool contents.
|
|
89
|
+
|
|
90
|
+
### F. Publish (deferred)
|
|
91
|
+
|
|
92
|
+
Don't write publishing code. If asked: explain that publishing requires written package-name confirmation (safety gate) and the Ketoy backend, which is not yet GA. Track the user's interest and stop there.
|
|
93
|
+
|
|
94
|
+
## Tone & format
|
|
95
|
+
|
|
96
|
+
- Be specific. "Add `dev.ketoy.vm:ketoy-runtime:0.3.4-alpha`" beats "add the runtime dependency."
|
|
97
|
+
- Quote real symbol names from the user's tree. Don't paraphrase.
|
|
98
|
+
- For multi-step migrations, show the plan, get acknowledgment, then execute step-by-step with intermediate verification (gradle compile after each meaningful change).
|
|
99
|
+
- When suggesting an unsupported pattern, link to the alternative in `reference/`. Never say "use X" without saying "because Y is not supported in KBC."
|
|
100
|
+
|
|
101
|
+
## Coordinates reference (current release: 0.3.4-alpha)
|
|
102
|
+
|
|
103
|
+
```kotlin
|
|
104
|
+
plugins {
|
|
105
|
+
alias(libs.plugins.android.application)
|
|
106
|
+
alias(libs.plugins.kotlin.android)
|
|
107
|
+
alias(libs.plugins.kotlin.compose)
|
|
108
|
+
id("dev.ketoy.compiler") version "0.3.4-alpha"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dependencies {
|
|
112
|
+
implementation(platform("dev.ketoy.vm:ketoy-bom:0.3.4-alpha"))
|
|
113
|
+
implementation("dev.ketoy.vm:ketoy-runtime")
|
|
114
|
+
implementation("dev.ketoy.vm:ketoy-annotations")
|
|
115
|
+
implementation("dev.ketoy.vm:ketoy-capabilities-core")
|
|
116
|
+
implementation("dev.ketoy.vm:ketoy-capabilities-navigation")
|
|
117
|
+
implementation("dev.ketoy.vm:ketoy-adapters-material3")
|
|
118
|
+
// Add `dev.ketoy.vm:ketoy-hilt` only if the host opts into Hilt.
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ketoy {
|
|
122
|
+
exportFromAppModule.set(true)
|
|
123
|
+
bundleId.set("main")
|
|
124
|
+
bundleVariant.set("release")
|
|
125
|
+
capabilityRegistryFile.set(file("ketoy-capabilities.json"))
|
|
126
|
+
minAppVersion.set(0)
|
|
127
|
+
debugMode.set(true)
|
|
128
|
+
val signingKey = file("keys/release-private.key")
|
|
129
|
+
if (signingKey.exists()) {
|
|
130
|
+
signingKeyFile.set(signingKey)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The `dev.ketoy.compiler` Gradle plugin auto-attaches the KetoyBC compiler plugin to `compile<bundleVariant>Kotlin`. `exportFromAppModule.set(true)` is ADR-0003 inline-source mode — the bundle is emitted at `app/src/main/assets/ketoy/<bundleId>.ktx`.
|
|
136
|
+
|
|
137
|
+
The plugin must apply to the **module that owns `@KetoyComposable` declarations**, normally `:app`. Plugin version is declared inline on the `id(...) version "..."` line since the plugin is resolved through the Gradle Plugin Portal / Maven Central.
|
|
138
|
+
|
|
139
|
+
## When to read sub-files
|
|
140
|
+
|
|
141
|
+
- Asked "is X supported?" → `reference/supported-composables.md`, `reference/supported-constructors.md`, `reference/capabilities.md`.
|
|
142
|
+
- Asked "what can't I do?" → `reference/forbidden-apis.md`.
|
|
143
|
+
- Writing a screen → `examples/todo-screen.kt`.
|
|
144
|
+
- Defining capabilities → `examples/capabilities-stubs.kt`.
|
|
145
|
+
- Wiring Hilt → `examples/hilt-config.kt`.
|
|
146
|
+
- Scaffolding from scratch → `templates/`.
|
|
147
|
+
- Diagnosing a build error → `guides/diagnose-errors.md`.
|
|
148
|
+
- Migrating a screen → `guides/migrate.md`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Canonical capability stubs for a KBC screen.
|
|
2
|
+
//
|
|
3
|
+
// Place this file at: app/src/main/kotlin/com/example/app/ketoyscreens/Capabilities.kt
|
|
4
|
+
//
|
|
5
|
+
// Every function below is a `@KetoyCapabilityStub` — the body throws because
|
|
6
|
+
// at runtime the compiler plugin replaces calls to these with INVOKE_CAPABILITY
|
|
7
|
+
// opcodes. The body is never executed.
|
|
8
|
+
//
|
|
9
|
+
// IDs in 0x07xx, 0x09xx, 0x0Axx are STANDARD LIBRARY — match exactly.
|
|
10
|
+
// IDs at 0x4000+ are APP-SPECIFIC — pick freely, but pick once and never reuse.
|
|
11
|
+
|
|
12
|
+
@file:Suppress("UnusedParameter")
|
|
13
|
+
|
|
14
|
+
package com.example.app.ketoyscreens
|
|
15
|
+
|
|
16
|
+
import dev.ketoy.annotations.KetoyCapabilityStub
|
|
17
|
+
import kotlinx.coroutines.flow.Flow
|
|
18
|
+
|
|
19
|
+
private const val STUB_MSG = "Ketoy capability stub — replaced by INVOKE_CAPABILITY at compile time"
|
|
20
|
+
|
|
21
|
+
// ── Navigation (standard library) ────────────────────────────────
|
|
22
|
+
|
|
23
|
+
@KetoyCapabilityStub(id = 0x0700, name = "NAV_PUSH")
|
|
24
|
+
public fun navPush(route: String): Unit = error(STUB_MSG)
|
|
25
|
+
|
|
26
|
+
@KetoyCapabilityStub(id = 0x0701, name = "NAV_POP")
|
|
27
|
+
public fun navPop(): Unit = error(STUB_MSG)
|
|
28
|
+
|
|
29
|
+
@KetoyCapabilityStub(id = 0x0702, name = "NAV_REPLACE")
|
|
30
|
+
public fun navReplace(route: String): Unit = error(STUB_MSG)
|
|
31
|
+
|
|
32
|
+
// ── ViewModel state bridge ───────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
@KetoyCapabilityStub(id = 0x0A00, name = "VM_GET_STATE")
|
|
35
|
+
public fun vmGetState(key: String): Any? = error(STUB_MSG)
|
|
36
|
+
|
|
37
|
+
@KetoyCapabilityStub(id = 0x0A01, name = "VM_SET_STATE")
|
|
38
|
+
public fun vmSetState(key: String, value: Any?): Unit = error(STUB_MSG)
|
|
39
|
+
|
|
40
|
+
@KetoyCapabilityStub(id = 0x0A02, name = "VM_OBSERVE_STATE")
|
|
41
|
+
public fun vmObserveState(key: String): Flow<Any?> = error(STUB_MSG)
|
|
42
|
+
|
|
43
|
+
// ── Platform (standard library) ──────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@KetoyCapabilityStub(id = 0x0901, name = "TOAST")
|
|
46
|
+
public fun toast(message: String, duration: Int = 0): Unit = error(STUB_MSG)
|
|
47
|
+
|
|
48
|
+
@KetoyCapabilityStub(id = 0x0900, name = "ANALYTICS_TRACK")
|
|
49
|
+
public fun analyticsTrack(event: String, props: Map<String, Any?> = emptyMap()): Unit = error(STUB_MSG)
|
|
50
|
+
|
|
51
|
+
// ── App-specific (0x4000+, define yours) ─────────────────────────
|
|
52
|
+
|
|
53
|
+
@KetoyCapabilityStub(id = 0x4000, name = "OBSERVE_TODOS")
|
|
54
|
+
public fun observeTodos(): Flow<List<Any?>> = error(STUB_MSG)
|
|
55
|
+
|
|
56
|
+
@KetoyCapabilityStub(id = 0x4001, name = "ADD_TODO")
|
|
57
|
+
public suspend fun addTodo(title: String): Long = error(STUB_MSG)
|
|
58
|
+
|
|
59
|
+
@KetoyCapabilityStub(id = 0x4002, name = "SET_TODO_COMPLETED")
|
|
60
|
+
public suspend fun setTodoCompleted(id: Long, completed: Boolean): Unit = error(STUB_MSG)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Hilt wiring for a Ketoy host APK.
|
|
2
|
+
//
|
|
3
|
+
// Three files in one — separate them in your project:
|
|
4
|
+
// 1. AppCapabilityProvider.kt (the KetoyCapabilityProvider impl)
|
|
5
|
+
// 2. AppHiltModule.kt (@Module with @Binds + @Provides)
|
|
6
|
+
// 3. MainActivity.kt (consumer wiring)
|
|
7
|
+
|
|
8
|
+
// ════════════════════════════════════════════════════════════════════
|
|
9
|
+
// File 1 — AppCapabilityProvider.kt
|
|
10
|
+
// ════════════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
package com.example.app.di
|
|
13
|
+
|
|
14
|
+
import android.content.Context
|
|
15
|
+
import androidx.datastore.core.DataStore
|
|
16
|
+
import androidx.datastore.preferences.core.Preferences
|
|
17
|
+
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
18
|
+
import dev.ketoy.capabilities.core.KetoyHttpClient
|
|
19
|
+
import dev.ketoy.capabilities.core.KBCRoomBridge
|
|
20
|
+
import dev.ketoy.capabilities.core.registerCoreCapabilities
|
|
21
|
+
import dev.ketoy.capabilities.core.registerStorageCapabilities
|
|
22
|
+
import dev.ketoy.hilt.KetoyCapabilityProvider
|
|
23
|
+
import dev.ketoy.runtime.capability.CapabilityRegistry
|
|
24
|
+
import javax.inject.Inject
|
|
25
|
+
import javax.inject.Singleton
|
|
26
|
+
|
|
27
|
+
@Singleton
|
|
28
|
+
public class AppCapabilityProvider @Inject constructor(
|
|
29
|
+
@ApplicationContext private val context: Context,
|
|
30
|
+
private val dataStore: DataStore<Preferences>,
|
|
31
|
+
private val httpClient: KetoyHttpClient,
|
|
32
|
+
// Inject your domain repositories here (Room DAOs, HTTP clients, etc.)
|
|
33
|
+
// private val todoRepository: TodoRepository,
|
|
34
|
+
) : KetoyCapabilityProvider {
|
|
35
|
+
|
|
36
|
+
override fun buildRegistry(): CapabilityRegistry {
|
|
37
|
+
val registry = CapabilityRegistry()
|
|
38
|
+
|
|
39
|
+
// Standard library: HTTP, KV storage, dispatchers, platform, navigation
|
|
40
|
+
registry.registerCoreCapabilities(context, httpClient = httpClient)
|
|
41
|
+
registry.registerStorageCapabilities(dataStore)
|
|
42
|
+
|
|
43
|
+
// App-specific (0x4000+) — registered via KBCRoomBridge for type-safe Room
|
|
44
|
+
KBCRoomBridge(registry).apply {
|
|
45
|
+
// observeList(0x4000) { args -> todoRepository.observeAll() }
|
|
46
|
+
// suspendCapability(0x4001) { args ->
|
|
47
|
+
// val title = args[0] as String
|
|
48
|
+
// todoRepository.add(title)
|
|
49
|
+
// }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return registry
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ════════════════════════════════════════════════════════════════════
|
|
57
|
+
// File 2 — AppHiltModule.kt
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
package com.example.app.di
|
|
61
|
+
|
|
62
|
+
import android.content.Context
|
|
63
|
+
import androidx.compose.material.icons.Icons
|
|
64
|
+
import androidx.compose.material.icons.filled.Settings
|
|
65
|
+
import androidx.compose.ui.text.font.Font
|
|
66
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
67
|
+
import androidx.datastore.core.DataStore
|
|
68
|
+
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
|
69
|
+
import androidx.datastore.preferences.core.Preferences
|
|
70
|
+
import androidx.datastore.preferences.preferencesDataStoreFile
|
|
71
|
+
import dagger.Binds
|
|
72
|
+
import dagger.Module
|
|
73
|
+
import dagger.Provides
|
|
74
|
+
import dagger.hilt.InstallIn
|
|
75
|
+
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
76
|
+
import dagger.hilt.components.SingletonComponent
|
|
77
|
+
import dev.ketoy.adapters.material3.MaterialDrawableResolver
|
|
78
|
+
import dev.ketoy.adapters.material3.MaterialFontFamilyResolver
|
|
79
|
+
import dev.ketoy.adapters.material3.MaterialIconsResolver
|
|
80
|
+
import dev.ketoy.adapters.material3.materialDrawableResolver
|
|
81
|
+
import dev.ketoy.adapters.material3.materialFontFamilyResolver
|
|
82
|
+
import dev.ketoy.adapters.material3.materialIconsResolver
|
|
83
|
+
import dev.ketoy.hilt.KetoyCapabilityProvider
|
|
84
|
+
import dev.ketoy.hilt.KetoyConfigCustomizer
|
|
85
|
+
import dev.ketoy.runtime.security.KetoyKeystore
|
|
86
|
+
import com.example.app.R
|
|
87
|
+
import javax.inject.Singleton
|
|
88
|
+
|
|
89
|
+
@Module
|
|
90
|
+
@InstallIn(SingletonComponent::class)
|
|
91
|
+
public abstract class AppHiltModule {
|
|
92
|
+
|
|
93
|
+
@Binds @Singleton
|
|
94
|
+
public abstract fun bindCapabilityProvider(impl: AppCapabilityProvider): KetoyCapabilityProvider
|
|
95
|
+
|
|
96
|
+
public companion object {
|
|
97
|
+
private const val PREFERENCES_NAME = "app_prefs"
|
|
98
|
+
private const val PUBLIC_KEY_ASSET = "ketoy/keys/sample-public.key"
|
|
99
|
+
|
|
100
|
+
@Provides @Singleton
|
|
101
|
+
public fun provideDataStore(
|
|
102
|
+
@ApplicationContext context: Context,
|
|
103
|
+
): DataStore<Preferences> = PreferenceDataStoreFactory.create {
|
|
104
|
+
context.preferencesDataStoreFile(PREFERENCES_NAME)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Provides @Singleton
|
|
108
|
+
public fun provideKetoyConfigCustomizer(
|
|
109
|
+
@ApplicationContext context: Context,
|
|
110
|
+
iconsResolver: MaterialIconsResolver,
|
|
111
|
+
fontResolver: MaterialFontFamilyResolver,
|
|
112
|
+
drawableResolver: MaterialDrawableResolver,
|
|
113
|
+
): KetoyConfigCustomizer = KetoyConfigCustomizer { default ->
|
|
114
|
+
val publicKey = KetoyKeystore.loadFromAsset(context, PUBLIC_KEY_ASSET)
|
|
115
|
+
default.copy(
|
|
116
|
+
enableSignatureVerification = true,
|
|
117
|
+
publicKey = publicKey,
|
|
118
|
+
imageVectorResolver = iconsResolver,
|
|
119
|
+
fontFamilyResolver = fontResolver,
|
|
120
|
+
drawableResolver = drawableResolver,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Provides @Singleton
|
|
125
|
+
public fun provideMaterialIconsResolver(): MaterialIconsResolver = materialIconsResolver {
|
|
126
|
+
registerFilled("Settings", Icons.Filled.Settings)
|
|
127
|
+
// Add additional icons here as KBC source consumes them.
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Provides @Singleton
|
|
131
|
+
public fun provideMaterialFontFamilyResolver(): MaterialFontFamilyResolver =
|
|
132
|
+
materialFontFamilyResolver {
|
|
133
|
+
// register("courgette_regular", FontFamily(Font(R.font.courgette_regular)))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Provides @Singleton
|
|
137
|
+
public fun provideMaterialDrawableResolver(): MaterialDrawableResolver =
|
|
138
|
+
materialDrawableResolver {
|
|
139
|
+
// register("img", R.drawable.img)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ════════════════════════════════════════════════════════════════════
|
|
145
|
+
// File 3 — MainActivity.kt
|
|
146
|
+
// ════════════════════════════════════════════════════════════════════
|
|
147
|
+
|
|
148
|
+
package com.example.app
|
|
149
|
+
|
|
150
|
+
import android.os.Bundle
|
|
151
|
+
import androidx.activity.ComponentActivity
|
|
152
|
+
import androidx.activity.compose.setContent
|
|
153
|
+
import androidx.activity.enableEdgeToEdge
|
|
154
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
155
|
+
import androidx.compose.ui.Modifier
|
|
156
|
+
import dagger.hilt.android.AndroidEntryPoint
|
|
157
|
+
import dev.ketoy.hilt.KetoyHiltProvider
|
|
158
|
+
import dev.ketoy.hilt.KetoyViewModelFactoryBuilder
|
|
159
|
+
import dev.ketoy.runtime.KetoyRuntime
|
|
160
|
+
import dev.ketoy.runtime.bundle.KetoyBundleLoader
|
|
161
|
+
import dev.ketoy.runtime.bundle.KetoyBundleSource
|
|
162
|
+
import dev.ketoy.runtime.compose.KetoyScreen
|
|
163
|
+
import javax.inject.Inject
|
|
164
|
+
|
|
165
|
+
@AndroidEntryPoint
|
|
166
|
+
public class MainActivity : ComponentActivity() {
|
|
167
|
+
@Inject public lateinit var factoryBuilder: KetoyViewModelFactoryBuilder
|
|
168
|
+
@Inject public lateinit var runtime: KetoyRuntime
|
|
169
|
+
@Inject public lateinit var bundleLoader: KetoyBundleLoader
|
|
170
|
+
|
|
171
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
172
|
+
super.onCreate(savedInstanceState)
|
|
173
|
+
enableEdgeToEdge()
|
|
174
|
+
setContent {
|
|
175
|
+
KetoyHiltProvider(
|
|
176
|
+
factoryBuilder = factoryBuilder,
|
|
177
|
+
bundleLoader = bundleLoader,
|
|
178
|
+
runtime = runtime,
|
|
179
|
+
) {
|
|
180
|
+
KetoyScreen(
|
|
181
|
+
entryPoint = "TodoListScreen",
|
|
182
|
+
bundleSource = KetoyBundleSource.Asset("ketoy/main.ktx"),
|
|
183
|
+
modifier = Modifier.fillMaxSize(),
|
|
184
|
+
nativeFallback = {
|
|
185
|
+
// Required: rendered when bundle is missing / incompatible / corrupt.
|
|
186
|
+
// This should be the same screen the user would see WITHOUT Ketoy.
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Non-Hilt Ketoy setup — the default first-pass for `ketoy init`.
|
|
2
|
+
//
|
|
3
|
+
// Two files:
|
|
4
|
+
// 1. App.kt (Application with manual singleton wiring)
|
|
5
|
+
// 2. MainActivity.kt (consumer)
|
|
6
|
+
|
|
7
|
+
// ════════════════════════════════════════════════════════════════════
|
|
8
|
+
// File 1 — App.kt
|
|
9
|
+
// ════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
package com.example.app
|
|
12
|
+
|
|
13
|
+
import android.app.Application
|
|
14
|
+
import androidx.compose.material.icons.Icons
|
|
15
|
+
import androidx.compose.material.icons.filled.Settings
|
|
16
|
+
import dev.ketoy.adapters.material3.materialDrawableResolver
|
|
17
|
+
import dev.ketoy.adapters.material3.materialFontFamilyResolver
|
|
18
|
+
import dev.ketoy.adapters.material3.materialIconsResolver
|
|
19
|
+
import dev.ketoy.capabilities.core.KetoyHttpClient
|
|
20
|
+
import dev.ketoy.capabilities.core.registerCoreCapabilities
|
|
21
|
+
import dev.ketoy.runtime.KetoyConfig
|
|
22
|
+
import dev.ketoy.runtime.KetoyRuntime
|
|
23
|
+
import dev.ketoy.runtime.bundle.KetoyBundleLoader
|
|
24
|
+
import dev.ketoy.runtime.capability.CapabilityRegistry
|
|
25
|
+
import dev.ketoy.runtime.security.KetoyKeystore
|
|
26
|
+
|
|
27
|
+
public class App : Application() {
|
|
28
|
+
|
|
29
|
+
public lateinit var ketoyRuntime: KetoyRuntime
|
|
30
|
+
private set
|
|
31
|
+
|
|
32
|
+
public lateinit var ketoyBundleLoader: KetoyBundleLoader
|
|
33
|
+
private set
|
|
34
|
+
|
|
35
|
+
override fun onCreate() {
|
|
36
|
+
super.onCreate()
|
|
37
|
+
|
|
38
|
+
val iconsResolver = materialIconsResolver {
|
|
39
|
+
registerFilled("Settings", Icons.Filled.Settings)
|
|
40
|
+
}
|
|
41
|
+
val fontResolver = materialFontFamilyResolver { /* register fonts */ }
|
|
42
|
+
val drawableResolver = materialDrawableResolver { /* register drawables */ }
|
|
43
|
+
|
|
44
|
+
val registry = CapabilityRegistry().apply {
|
|
45
|
+
registerCoreCapabilities(this@App, httpClient = KetoyHttpClient.build())
|
|
46
|
+
// app-specific capabilities here:
|
|
47
|
+
// registerSuspend(0x4001) { args -> ... }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
val config = KetoyConfig(
|
|
51
|
+
enableSignatureVerification = true,
|
|
52
|
+
publicKey = KetoyKeystore.loadFromAsset(this, "ketoy/keys/sample-public.key"),
|
|
53
|
+
imageVectorResolver = iconsResolver,
|
|
54
|
+
fontFamilyResolver = fontResolver,
|
|
55
|
+
drawableResolver = drawableResolver,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
ketoyRuntime = KetoyRuntime(config, registry)
|
|
59
|
+
ketoyBundleLoader = KetoyBundleLoader(ketoyRuntime, this)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ════════════════════════════════════════════════════════════════════
|
|
64
|
+
// File 2 — MainActivity.kt
|
|
65
|
+
// ════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
package com.example.app
|
|
68
|
+
|
|
69
|
+
import android.os.Bundle
|
|
70
|
+
import androidx.activity.ComponentActivity
|
|
71
|
+
import androidx.activity.compose.setContent
|
|
72
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
73
|
+
import androidx.compose.ui.Modifier
|
|
74
|
+
import dev.ketoy.runtime.bundle.KetoyBundleSource
|
|
75
|
+
import dev.ketoy.runtime.compose.KetoyScreen
|
|
76
|
+
|
|
77
|
+
public class MainActivity : ComponentActivity() {
|
|
78
|
+
|
|
79
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
80
|
+
super.onCreate(savedInstanceState)
|
|
81
|
+
val app = application as App
|
|
82
|
+
|
|
83
|
+
setContent {
|
|
84
|
+
KetoyScreen(
|
|
85
|
+
entryPoint = "TodoListScreen",
|
|
86
|
+
bundleSource = KetoyBundleSource.Asset("ketoy/main.ktx"),
|
|
87
|
+
modifier = Modifier.fillMaxSize(),
|
|
88
|
+
runtime = app.ketoyRuntime,
|
|
89
|
+
bundleLoader = app.ketoyBundleLoader,
|
|
90
|
+
nativeFallback = {
|
|
91
|
+
// Required: rendered when bundle missing / incompatible / corrupt.
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Remember to register the Application in AndroidManifest.xml:
|
|
99
|
+
// <application
|
|
100
|
+
// android:name=".App"
|
|
101
|
+
// ... >
|