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.
Files changed (31) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +101 -0
  3. package/SECURITY.md +34 -0
  4. package/dist/ketoy.js +3165 -0
  5. package/dist/ketoy.js.map +1 -0
  6. package/package.json +78 -0
  7. package/skills/ketoy/README.md +50 -0
  8. package/skills/ketoy/SKILL.md +148 -0
  9. package/skills/ketoy/examples/capabilities-stubs.kt +60 -0
  10. package/skills/ketoy/examples/hilt-config.kt +192 -0
  11. package/skills/ketoy/examples/no-hilt-config.kt +101 -0
  12. package/skills/ketoy/examples/todo-screen.kt +156 -0
  13. package/skills/ketoy/guides/build-and-analyze.md +87 -0
  14. package/skills/ketoy/guides/diagnose-errors.md +129 -0
  15. package/skills/ketoy/guides/init-project.md +127 -0
  16. package/skills/ketoy/guides/migrate.md +190 -0
  17. package/skills/ketoy/guides/publish-deferred.md +46 -0
  18. package/skills/ketoy/guides/safe-edits.md +141 -0
  19. package/skills/ketoy/reference/architecture-cheatsheet.md +122 -0
  20. package/skills/ketoy/reference/capabilities.md +122 -0
  21. package/skills/ketoy/reference/forbidden-apis.md +149 -0
  22. package/skills/ketoy/reference/supported-composables.md +80 -0
  23. package/skills/ketoy/reference/supported-constructors.md +57 -0
  24. package/skills/ketoy/reference/supported-modifiers.md +76 -0
  25. package/skills/ketoy/templates/app-build.gradle.kts.tmpl +109 -0
  26. package/skills/ketoy/templates/ketoy-capabilities.json.tmpl +21 -0
  27. package/skills/ketoy/templates/manifest-snippet.xml.tmpl +33 -0
  28. package/templates/HelloKetoyScreen.kt.tmpl +51 -0
  29. package/templates/MainActivity.kt.tmpl +53 -0
  30. package/templates/MyApplication.kt.tmpl +88 -0
  31. package/templates/ketoy-capabilities.json.tmpl +5 -0
@@ -0,0 +1,156 @@
1
+ // CANONICAL KBC SCREEN — copy this shape verbatim for new screens.
2
+ //
3
+ // This is an excerpt from the Ketoy repo's `app/.../ketoyscreens/TodoListScreen.kt`,
4
+ // trimmed to highlight the patterns that matter. Every line is exercised by
5
+ // the real Phase 11E in-tree app-module export pipeline.
6
+ //
7
+ // Key things to notice:
8
+ // 1. Annotations: @KetoyEntryPoint + @KetoyComposable + @Composable on the root.
9
+ // 2. Outer-scope `val title` captured into the Column content lambda — proves
10
+ // closure conversion works (KBCValue.ClosureRef under the hood).
11
+ // 3. Modifier chains use named args (padding(horizontal=, vertical=), etc.).
12
+ // 4. Color via literal `Color(0xff4F378B)` or named `Color.White`.
13
+ // 5. Shapes always `RoundedCornerShape(N.dp)` or `RoundedCornerShape(percentInt)`.
14
+ // 6. Images: `painterResource(R.drawable.X)` — host must register X.
15
+ // 7. Custom font: `FontFamily(Font(R.font.X))` — host must register X.
16
+ // 8. No vars, no state-holding objects, no LaunchedEffect-with-mutable-closure.
17
+
18
+ package com.example.app.ketoyscreens
19
+
20
+ import androidx.compose.foundation.Image
21
+ import androidx.compose.foundation.background
22
+ import androidx.compose.foundation.layout.Arrangement
23
+ import androidx.compose.foundation.layout.Box
24
+ import androidx.compose.foundation.layout.Column
25
+ import androidx.compose.foundation.layout.Row
26
+ import androidx.compose.foundation.layout.Spacer
27
+ import androidx.compose.foundation.layout.fillMaxSize
28
+ import androidx.compose.foundation.layout.fillMaxWidth
29
+ import androidx.compose.foundation.layout.height
30
+ import androidx.compose.foundation.layout.padding
31
+ import androidx.compose.foundation.shape.RoundedCornerShape
32
+ import androidx.compose.material3.Button
33
+ import androidx.compose.material3.ButtonDefaults
34
+ import androidx.compose.material3.Card
35
+ import androidx.compose.material3.CardDefaults
36
+ import androidx.compose.material3.ExperimentalMaterial3Api
37
+ import androidx.compose.material3.Scaffold
38
+ import androidx.compose.material3.Text
39
+ import androidx.compose.material3.TopAppBar
40
+ import androidx.compose.material3.TopAppBarDefaults
41
+ import androidx.compose.runtime.Composable
42
+ import androidx.compose.ui.Alignment
43
+ import androidx.compose.ui.Modifier
44
+ import androidx.compose.ui.draw.clip
45
+ import androidx.compose.ui.graphics.Color
46
+ import androidx.compose.ui.res.painterResource
47
+ import androidx.compose.ui.text.font.Font
48
+ import androidx.compose.ui.text.font.FontFamily
49
+ import androidx.compose.ui.text.font.FontWeight
50
+ import androidx.compose.ui.unit.dp
51
+ import androidx.compose.ui.unit.sp
52
+ import com.example.app.R
53
+ import dev.ketoy.annotations.KetoyComposable
54
+ import dev.ketoy.annotations.KetoyEntryPoint
55
+
56
+ @OptIn(ExperimentalMaterial3Api::class)
57
+ @KetoyEntryPoint
58
+ @KetoyComposable
59
+ @Composable
60
+ public fun TodoListScreen(modifier: Modifier = Modifier) {
61
+ // Captured into content lambdas via closure conversion.
62
+ val title = "Ketoy Todo"
63
+ val subtitle = "Shipped as .ktx bundle"
64
+
65
+ Scaffold(
66
+ modifier = Modifier.fillMaxSize(),
67
+ topBar = {
68
+ TopAppBar(
69
+ title = { Text(title, color = Color.White) },
70
+ colors = TopAppBarDefaults.topAppBarColors(
71
+ containerColor = Color(0xFF4F378B),
72
+ ),
73
+ )
74
+ },
75
+ floatingActionButton = {
76
+ Button(
77
+ onClick = { navPush("todo_add") }, // capability call
78
+ modifier = Modifier.padding(16.dp),
79
+ shape = RoundedCornerShape(100),
80
+ colors = ButtonDefaults.buttonColors(
81
+ containerColor = Color(0xff4F378B),
82
+ contentColor = Color.White,
83
+ ),
84
+ ) {
85
+ Text("+", fontSize = 24.sp)
86
+ }
87
+ },
88
+ ) { paddingValues ->
89
+ Column(
90
+ modifier = Modifier
91
+ .padding(paddingValues = paddingValues)
92
+ .fillMaxSize()
93
+ .padding(horizontal = 20.dp, vertical = 20.dp),
94
+ ) {
95
+ Card(
96
+ modifier = Modifier.fillMaxWidth(),
97
+ shape = RoundedCornerShape(40.dp),
98
+ colors = CardDefaults.cardColors(containerColor = Color(0xFFB69DF8)),
99
+ ) {
100
+ Row(
101
+ modifier = Modifier.fillMaxSize(),
102
+ verticalAlignment = Alignment.CenterVertically,
103
+ horizontalArrangement = Arrangement.Center,
104
+ ) {
105
+ Text(
106
+ text = "Hello, $title!",
107
+ fontSize = 28.sp,
108
+ fontWeight = FontWeight.Bold,
109
+ color = Color.White,
110
+ )
111
+ }
112
+ }
113
+
114
+ Spacer(modifier = Modifier.height(20.dp))
115
+
116
+ // Decorative image — host registers "img" → R.drawable.img
117
+ Image(
118
+ painter = painterResource(R.drawable.img),
119
+ contentDescription = subtitle,
120
+ modifier = Modifier
121
+ .fillMaxWidth()
122
+ .height(120.dp)
123
+ .clip(shape = RoundedCornerShape(20.dp)),
124
+ )
125
+
126
+ Spacer(modifier = Modifier.height(20.dp))
127
+
128
+ // Custom font — host registers "courgette_regular" → R.font.courgette_regular
129
+ Text(
130
+ text = subtitle,
131
+ fontSize = 16.sp,
132
+ fontWeight = FontWeight.SemiBold,
133
+ fontFamily = FontFamily(Font(R.font.courgette_regular)),
134
+ color = Color.Black,
135
+ )
136
+
137
+ Spacer(modifier = Modifier.height(20.dp))
138
+
139
+ Box(
140
+ modifier = Modifier
141
+ .fillMaxWidth()
142
+ .height(100.dp)
143
+ .clip(shape = RoundedCornerShape(30))
144
+ .background(color = Color(0xffFFECF1)),
145
+ contentAlignment = Alignment.Center,
146
+ ) {
147
+ Text(
148
+ text = "Bottom panel",
149
+ fontSize = 14.sp,
150
+ color = Color(0xFFD0706B),
151
+ fontWeight = FontWeight.Bold,
152
+ )
153
+ }
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,87 @@
1
+ # Build and Analyze Ketoy Bundles
2
+
3
+ ## Build
4
+
5
+ The bundle is produced as part of the host APK build:
6
+
7
+ ```bash
8
+ ./gradlew :app:ketoyBundle --rerun-tasks # compile KBC only (faster iteration)
9
+ ./gradlew :app:assembleDebug # full APK (transitively triggers ketoyBundle)
10
+ ./gradlew :app:assembleRelease # signed release APK
11
+ ```
12
+
13
+ Bundle output path: `app/src/main/assets/ketoy/main.ktx`
14
+
15
+ Successful build logs end with:
16
+ ```
17
+ KetoyBC: Compilation complete — N functions emitted, M composables, K view models, J entry points.
18
+ Bundle ID: main. Wrote NNNN bytes to ... (signed)
19
+ ```
20
+
21
+ The `(signed)` suffix is present only when `signingKeyFile` was supplied. Unsigned bundles work in development but should never ship to a release APK (the host runtime will reject them when `enableSignatureVerification = true`).
22
+
23
+ ## Analyze a `.ktx` bundle
24
+
25
+ The CLI `analyze <path>` subcommand wraps a JVM subprocess that calls `KtxReader.read` and prints:
26
+
27
+ - Format version
28
+ - Bundle ID
29
+ - Function count, composable count, view-model count, entry-point count
30
+ - String pool size + contents (with `--strings`)
31
+ - Adapter manifest (with `--manifest`)
32
+ - Constructor manifest
33
+ - Capability manifest
34
+ - Modifier table entry count
35
+ - Signature present (yes/no)
36
+ - Minimum runtime version
37
+
38
+ Implementation sketch — under the hood it runs:
39
+ ```bash
40
+ java -cp <ketoy-bundle-format-jar>:<ketoy-bytecode-jar> \
41
+ -Dktx.path=path/to/main.ktx \
42
+ dev.ketoy.bundle.KtxInspectorMain
43
+ ```
44
+
45
+ If you don't have `KtxInspectorMain`, write a tiny one — it's ~30 lines using `KtxReader.read(bytes, verifySignature = false)` + pretty-printing.
46
+
47
+ ## Quick byte-level proofs
48
+
49
+ For diagnosing wire-format issues:
50
+
51
+ ```bash
52
+ # magic bytes — must be 4B 54 4F 59 ("KTOY")
53
+ xxd -l 4 app/src/main/assets/ketoy/main.ktx
54
+
55
+ # format version (offset 4, big-endian u16) — should be 0x0002
56
+ xxd -s 4 -l 2 app/src/main/assets/ketoy/main.ktx
57
+
58
+ # bundle has signature trailer? last 64 bytes should be non-zero
59
+ xxd -s -64 -l 64 app/src/main/assets/ketoy/main.ktx
60
+
61
+ # scan string pool for unwanted references — should NOT contain raw Compose getter FQs
62
+ strings -n 4 app/src/main/assets/ketoy/main.ktx | grep '<get-'
63
+ ```
64
+
65
+ The last command is a regression canary: if it prints anything, the compiler-plugin's encoder short-circuits aren't catching some token / icon / font / drawable reference and the bundle is carrying dead getter strings.
66
+
67
+ ## Bundle delivery options
68
+
69
+ A built `.ktx` can be consumed via four `KetoyBundleSource` variants:
70
+
71
+ | Source | Use case | Caching |
72
+ |---|---|---|
73
+ | `KetoyBundleSource.Asset("ketoy/main.ktx")` | Default — ship inside APK | None (APK is the cache) |
74
+ | `KetoyBundleSource.Preloaded(bundle)` | Warm-up, tests, custom caches | Caller-managed |
75
+ | `KetoyBundleSource.Raw(bytes)` | Manual byte handoff | Caller-managed |
76
+ | `KetoyBundleSource.Remote(url, headers)` | CDN delivery, hot updates | ETag-based, `context.cacheDir/ketoy_bundles/` |
77
+
78
+ For production server-driven delivery, build the bundle in CI, sign it with the production key, upload to your CDN, and use `Remote(url)`. The host verifies the signature on every load.
79
+
80
+ ## Bundle size optimization
81
+
82
+ - Brotli compression is on by default for CODE / MODIFIER_TABLE / DEBUG_INFO sections.
83
+ - `debugMode = false` strips DEBUG_INFO (saves ~10% on typical bundles).
84
+ - Trim unused capability stubs from `Capabilities.kt` — every declared stub adds to the manifest.
85
+ - String pool dedup is automatic; same string used N times costs 1 entry.
86
+
87
+ A sample-app bundle today is ~3.5 KB signed. A real production screen with 20+ widgets, 5 fonts, 3 drawables, and 15 capabilities is typically 8–25 KB.
@@ -0,0 +1,129 @@
1
+ # Diagnose Ketoy Errors
2
+
3
+ The Ketoy compiler plugin emits typed diagnostics with `KetoyBC:` prefix, a rationale, a `Fix:` block, a `Docs:` link, and (for transitive errors) a `Reached via:` call-chain breadcrumb. **Always show the user the full error text** — don't paraphrase.
4
+
5
+ ## Compile-time errors
6
+
7
+ ### `KetoyBC: Direct access to <android.X> ...`
8
+
9
+ A KBC screen called an Android API. Fix: add a `@KetoyCapabilityStub` for the operation and route through it.
10
+
11
+ Find the call site (use the breadcrumb's leaf). Examples:
12
+
13
+ | Banned call | Replacement |
14
+ |---|---|
15
+ | `Toast.makeText(...).show()` | `toast(msg)` capability |
16
+ | `Vibrator.vibrate(...)` | `vibrate(ms)` capability |
17
+ | `Log.d(tag, msg)` | `log(tag, msg)` capability |
18
+ | `context.startActivity(intent)` | `openUrl(uri)` or custom capability |
19
+
20
+ ### `KetoyBC: Reflection is not allowed in KBC source`
21
+
22
+ Replace with sealed classes + exhaustive `when`. KBC has no runtime type metadata.
23
+
24
+ ### `KetoyBC: File I/O is not allowed in KBC source`
25
+
26
+ Use `kvGet/kvSet` (DataStore-backed) or `dbQuery/dbObserve` (Room-backed via `KBCRoomBridge`).
27
+
28
+ ### `KetoyBC: GlobalScope is forbidden — use viewModelScope`
29
+
30
+ Allowed alternatives:
31
+ - Inside `@KetoyComposable`: `LaunchedEffect(key) { ... }`
32
+ - Structured concurrency: `coroutineScope { ... }`
33
+ - Background work via `withContext(Dispatchers.IO) { ... }` (uses `DISPATCHER_IO` capability)
34
+
35
+ ### `KetoyBC: Unregistered composable <FQ>`
36
+
37
+ The composable isn't in the adapter scan-roots. Two paths:
38
+ 1. **Same-module helper** — add `@KetoyComposable` so it becomes a KBC function (the closure walk auto-includes it via reachability).
39
+ 2. **External composable not in the catalog** — either inline the body or write a custom `KBCComposableAdapter` host-side.
40
+
41
+ ### `KetoyBC: Unregistered capability <name> id=0xXXXX`
42
+
43
+ The stub references a capability ID that doesn't appear in `ketoy-capabilities.json`. Add it:
44
+
45
+ ```json
46
+ { "id": 16385, "name": "YOUR_CAP",
47
+ "fqName": "com.example.app.ketoyscreens.yourCap",
48
+ "kind": "SUSPEND", "required": true }
49
+ ```
50
+
51
+ (Decimal IDs in JSON: `0x4001` = `16385`.)
52
+
53
+ ### `KetoyBC: Direct construction of <FQ>`
54
+
55
+ A constructor not in `reference/supported-constructors.md`. Options:
56
+ - Use a supported alternative.
57
+ - Move construction host-side; pass via state.
58
+ - Custom `KBCConstructorAdapter` (advanced).
59
+
60
+ ### `KetoyBC: Direct access to <Token>.<get-X>` (e.g. `FontWeight.<get-Bold>`)
61
+
62
+ The token isn't in `ComposeTokenRegistry`. The standard Compose tokens (FontWeight.Bold/Medium/SemiBold/Normal/Light/Thin/ExtraBold/ExtraLight/Black) ARE supported — if you see this, you're likely using a non-standard FontWeight subclass or a 0.x version mismatch. Verify your `compose-material3` BOM matches what `ketoy-adapters-material3` was generated against.
63
+
64
+ ## Transitive errors with breadcrumbs
65
+
66
+ ```
67
+ KetoyBC: Direct access to android.util.Log.d
68
+ Reached via: TodoListScreen → renderHeader → trackImpression
69
+ ...
70
+ ```
71
+
72
+ **Fix at the leaf** (`trackImpression`), not the root. The breadcrumb is the path you need.
73
+
74
+ If the leaf is genuinely necessary and shouldn't be migrated, mark the helper non-KBC by removing its `@KetoyComposable` annotation AND ensuring no KBC root calls it. Then it compiles to plain DEX in the same `:app` module — totally fine.
75
+
76
+ ## Bundle build errors
77
+
78
+ ### `Could not find dev.ketoy.compiler:dev.ketoy.compiler.gradle.plugin`
79
+
80
+ The plugin isn't published / not on classpath. Verify:
81
+ - `settings.gradle.kts` includes `mavenCentral()` in `pluginManagement.repositories`
82
+ - Gradle plugin version matches the runtime version (both `0.2.1-alpha`)
83
+
84
+ ### `Task 'ketoyBundle' not found in project ':app'`
85
+
86
+ The `dev.ketoy.compiler` plugin isn't applied. Add `id("dev.ketoy.compiler")` to `app/build.gradle.kts`.
87
+
88
+ ### `Unsigned bundle warning` in build output
89
+
90
+ Your `signingKeyFile` is missing. Generate one (see `guides/init-project.md` step 8) or accept unsigned bundles (development only — host must set `enableSignatureVerification = false`).
91
+
92
+ ## Runtime errors
93
+
94
+ ### `KetoyBundleMalformedException: Invalid magic bytes`
95
+
96
+ The `.ktx` file is corrupt or not a KBC bundle. Rebuild via `./gradlew :app:ketoyBundle --rerun-tasks`.
97
+
98
+ ### `KetoyBundleSignatureException: Signature verification failed`
99
+
100
+ The public key in the APK doesn't match the private key used at compile time. Either:
101
+ - Regenerate both halves (see init step 8) and rebuild.
102
+ - Set `enableSignatureVerification = false` in `KetoyConfig` (dev only).
103
+
104
+ ### `KetoyBundleVersionException: Bundle requires runtime version N`
105
+
106
+ The bundle was compiled against a newer Ketoy runtime than the host APK ships. Bump the `dev.ketoy.vm:*` dependency versions in the host.
107
+
108
+ ### `KetoyMissingCapabilityException: capability id=0xXXXX (NAME) not registered`
109
+
110
+ The host's `CapabilityRegistry` doesn't have a registration for that ID. Add it inside `KetoyCapabilityProvider.buildRegistry()` or wherever you build the registry.
111
+
112
+ ### `KBCEngineNotAvailableException: Coroutine engine`
113
+
114
+ Trying to call a `suspend` capability without wiring `parentScope` into the VM. Use `KetoyVirtualViewModel` (which provides `viewModelScope`) — `KetoyScreen` does this automatically.
115
+
116
+ ### `KetoyMissingAdapterException: adapter id=0xXXXX`
117
+
118
+ The bundle was compiled with a composable adapter that the host doesn't ship. If you added `dev.ketoy.vm:ketoy-adapters-material3`, you have the full Material3 set. Check that you haven't introduced a custom adapter compile-side without registering it runtime-side.
119
+
120
+ ### `ClosureRef captures > N registers`
121
+
122
+ Closure conversion captured too many outer-scope locals. Refactor to fewer captures — typically by computing values inline or via state.
123
+
124
+ ## When you don't recognize the error
125
+
126
+ 1. Search this skill's `reference/` files for matching FQ names.
127
+ 2. Search the Ketoy repo's `KNOWN_ISSUES.md` — there are 12+ known limitations documented with workarounds.
128
+ 3. Read `CLAUDE.md` at the repo root — every phase has a "Key Technical Decisions" block that often explains weird behavior.
129
+ 4. Search the `ketoy-compiler-plugin/src/main/.../KetoyCompilationError.kt` source for the diagnostic message text — the variant class points at the validator path that produced it.
@@ -0,0 +1,127 @@
1
+ # Init a new Ketoy-enabled project
2
+
3
+ The `ketoy init` command is **deterministic and AI-free**. It makes **surgical, additive edits** to existing user files and creates new files only where none exist. **Never rewrite an existing `build.gradle.kts`, `AndroidManifest.xml`, `MainActivity.kt`, or `Application` class in full** — that would destroy the user's existing dependencies, signing config, theme, etc. See [safe-edits.md](safe-edits.md) for the per-file insertion strategy. Templates in `templates/` describe the *final shape*; they are NOT the bytes to write into existing files.
4
+
5
+ ## Inputs to collect
6
+
7
+ 1. **Existing project?** — detect via `settings.gradle.kts` + `app/build.gradle.kts`. If neither exists, suggest using `gradle init` or Android Studio first; Ketoy is an add-on, not a project generator.
8
+ 2. **Hilt or not?** — grep `app/build.gradle.kts` and any `*.kt` for `dagger.hilt`. Default to NO if not detected.
9
+ 3. **Application ID & namespace** — read from existing `app/build.gradle.kts`.
10
+ 4. **Package** for `ketoyscreens/` — same as namespace by default.
11
+ 5. **minSdk** — must be 26+. If existing is below, abort and tell user.
12
+
13
+ ## Steps the `init` command performs
14
+
15
+ ### Step 1 — Surgically edit `app/build.gradle.kts` (never overwrite)
16
+
17
+ Three additive edits — see [safe-edits.md § app/build.gradle.kts](safe-edits.md) for exact insertion anchors:
18
+
19
+ - Insert `id("dev.ketoy.compiler")` at the end of the existing `plugins { }` block (skip if already present).
20
+ - Append a `// Ketoy` dependency group at the end of the existing `dependencies { }` block — never touch the user's existing entries.
21
+ - Append the `ketoy { ... }` block AFTER the last `}` of the file. Don't insert mid-file.
22
+ - If `minSdk < 26` → STOP and ask. Don't silently bump.
23
+ - Never touch `android { }`, `defaultConfig { }`, `buildTypes { }`, `compileOptions`, signing configs, or custom blocks.
24
+
25
+ ### Step 2 — Create `app/ketoy-capabilities.json`
26
+
27
+ Use `templates/ketoy-capabilities.json.tmpl`. Substitute `{{PACKAGE}}` with the namespace.
28
+
29
+ ### Step 3 — Create `app/src/main/.../ketoyscreens/`
30
+
31
+ - `Capabilities.kt` — copy from `examples/capabilities-stubs.kt`, package-rebase to `{{PACKAGE}}.ketoyscreens`.
32
+ - `HelloKetoyScreen.kt` — a minimal `@KetoyEntryPoint @KetoyComposable` screen so the bundle has something to compile:
33
+
34
+ ```kotlin
35
+ package {{PACKAGE}}.ketoyscreens
36
+
37
+ import androidx.compose.foundation.layout.Column
38
+ import androidx.compose.foundation.layout.fillMaxSize
39
+ import androidx.compose.foundation.layout.padding
40
+ import androidx.compose.material3.Text
41
+ import androidx.compose.runtime.Composable
42
+ import androidx.compose.ui.Modifier
43
+ import androidx.compose.ui.unit.dp
44
+ import androidx.compose.ui.unit.sp
45
+ import dev.ketoy.annotations.KetoyComposable
46
+ import dev.ketoy.annotations.KetoyEntryPoint
47
+
48
+ @KetoyEntryPoint
49
+ @KetoyComposable
50
+ @Composable
51
+ public fun HelloKetoyScreen() {
52
+ Column(
53
+ modifier = Modifier.fillMaxSize().padding(20.dp),
54
+ ) {
55
+ Text(text = "Hello, Ketoy!", fontSize = 24.sp)
56
+ Text(text = "This screen shipped as a .ktx bundle.")
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### Step 4 — Application class (create OR surgically edit)
62
+
63
+ - **No existing `Application` subclass**: create `App.kt` (non-Hilt) or `MyApplication.kt` (Hilt) from the examples.
64
+ - **Existing `Application` subclass**: do NOT replace. For Hilt, add `@HiltAndroidApp` if missing. For non-Hilt, insert the Ketoy field declarations + the init lines inside `onCreate()` AFTER `super.onCreate()`. Don't disturb any other init code.
65
+
66
+ ### Step 5 — MainActivity (almost always surgical)
67
+
68
+ If MainActivity is the Android Studio template boilerplate (single `setContent { Greeting(...) }`), it's safe to replace after diff confirmation. If it contains real user code, do NOT replace — instead add imports + `@Inject lateinit var` declarations + `@AndroidEntryPoint` (Hilt) as surgical edits, then hand the user the `KetoyScreen` snippet to paste inside their existing `setContent { }`. Never silently clobber.
69
+
70
+ ### Step 6 — AndroidManifest.xml (single attribute edit)
71
+
72
+ Add ONLY the `android:name=".App"` (or `.MyApplication`) attribute to the existing `<application>` tag. Don't replace the `<application>` block — the user has `android:icon`, `android:theme`, `android:label`, `android:allowBackup`, possibly `tools:replace`, network security config, etc. If `android:name` already points at a different Application class → stop and ask.
73
+
74
+ ### Step 7 — Hilt module (Hilt projects only)
75
+
76
+ If no existing `@Module @InstallIn(SingletonComponent::class)` exists, create `AppHiltModule.kt` + `AppCapabilityProvider.kt` from `examples/hilt-config.kt`.
77
+
78
+ If the user already has a singleton-scoped Hilt module, append the new `@Binds KetoyCapabilityProvider` + `@Provides` resolvers + config customizer as additive entries inside their existing module rather than introducing a parallel one. Show a diff first.
79
+
80
+ ### Step 8 — Signing keys (optional)
81
+
82
+ Skip by default. If user opts in, run:
83
+ ```bash
84
+ mkdir -p app/keys
85
+ openssl genpkey -algorithm Ed25519 -outform DER -out /tmp/key.der
86
+ tail -c 32 /tmp/key.der > app/keys/sample-private.key
87
+ mkdir -p app/src/main/assets/ketoy/keys
88
+ openssl pkey -in /tmp/key.der -inform DER -pubout -outform DER -out /tmp/pub.der
89
+ tail -c 32 /tmp/pub.der > app/src/main/assets/ketoy/keys/sample-public.key
90
+ echo "**/keys/*-private.key" >> .gitignore
91
+ ```
92
+
93
+ If `openssl` is unavailable, fall back to generating via `KetoyKeystore.generateKeyPair()` at first run (the runtime can do this; document it but don't auto-execute).
94
+
95
+ ### Step 9 — Verify the build
96
+
97
+ Run `./gradlew :app:assembleDebug`. On failure, fall through to `guides/diagnose-errors.md`.
98
+
99
+ ## What `init` does NOT do
100
+
101
+ - Generate KBC screens automatically (use `migrate` for that).
102
+ - Register app-specific capabilities (user must add to `AppCapabilityProvider.buildRegistry()`).
103
+ - Set up CDN delivery (that's `KetoyBundleSource.Remote(url)` + your CDN config).
104
+ - Touch existing screens / ViewModels. Migration is explicit and per-file.
105
+
106
+ ## Project structure after init
107
+
108
+ ```
109
+ app/
110
+ ├── build.gradle.kts (edited: + ketoy block)
111
+ ├── ketoy-capabilities.json (new)
112
+ ├── keys/sample-private.key (new, gitignored)
113
+ └── src/main/
114
+ ├── AndroidManifest.xml (edited)
115
+ ├── assets/ketoy/keys/sample-public.key (new)
116
+ ├── java/com/example/app/
117
+ │ ├── App.kt (new)
118
+ │ ├── MainActivity.kt (new or edited)
119
+ │ ├── ketoyscreens/
120
+ │ │ ├── Capabilities.kt (new)
121
+ │ │ └── HelloKetoyScreen.kt (new)
122
+ │ └── di/ (only if Hilt)
123
+ │ ├── AppHiltModule.kt
124
+ │ └── AppCapabilityProvider.kt
125
+ ```
126
+
127
+ After a successful `./gradlew :app:assembleDebug`, the bundle ends up at `app/src/main/assets/ketoy/main.ktx`.