@kabmo-labs/kabmo 0.6.1 → 0.7.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/dist/client/index.html +1 -1
- package/dist/client/index.js +187 -187
- package/dist/client/static/js/{index-aq0e78gv.js → index-0wvbca0b.js} +1 -1
- package/package.json +1 -1
- package/src/client/hooks/ui/updates/shell-download.test.ts +1 -1
- package/src/server/lib/updates/shell-release.test.ts +89 -18
- package/src/server/lib/updates/shell-release.ts +90 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kabmo-labs/kabmo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Web console and diagnostic workbench for AI coding agents — Elysia + Preact on Bun.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
|
|
17
17
|
/** The published release every case compares against. */
|
|
18
18
|
const RELEASE = {
|
|
19
|
-
url: 'https://github.com/kabmo-labs/kabmo/releases/download/v1.2.3/
|
|
19
|
+
url: 'https://github.com/kabmo-labs/kabmo/releases/download/v1.2.3/Kabmo-1.2.3-android.apk',
|
|
20
20
|
latest: '1.2.3',
|
|
21
21
|
releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v1.2.3',
|
|
22
22
|
};
|
|
@@ -17,26 +17,57 @@ import { fetchShellRelease, resetShellReleaseCache, versionFromAssetName } from
|
|
|
17
17
|
*/
|
|
18
18
|
describe('versionFromAssetName', () => {
|
|
19
19
|
test('reads the version the release workflows write', () => {
|
|
20
|
-
expect(versionFromAssetName('android', '
|
|
21
|
-
expect(versionFromAssetName('ios', '
|
|
20
|
+
expect(versionFromAssetName('android', 'Kabmo-0.5.0-android.apk')).toBe('0.5.0');
|
|
21
|
+
expect(versionFromAssetName('ios', 'Kabmo-0.1.0-ios.ipa')).toBe('0.1.0');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The old spelling is still READ, and that is not sentiment.
|
|
26
|
+
*
|
|
27
|
+
* These names are the only record of a published shell, and every release
|
|
28
|
+
* already on GitHub carries `kabmo-android-v<v>.apk`. A parser that only knew
|
|
29
|
+
* the new scheme would answer "no update" for an installed shell the moment
|
|
30
|
+
* this shipped — silently, because a name that does not match reads exactly
|
|
31
|
+
* like a release with no artifact.
|
|
32
|
+
*/
|
|
33
|
+
test('still reads the legacy spelling every published release carries', () => {
|
|
34
|
+
expect(versionFromAssetName('android', 'kabmo-android-v0.5.0.apk')).toBe('0.5.0');
|
|
35
|
+
expect(versionFromAssetName('ios', 'kabmo-ios-v0.1.0.ipa')).toBe('0.1.0');
|
|
27
36
|
});
|
|
28
37
|
|
|
29
38
|
test('ignores anything that is not that shell’s artifact', () => {
|
|
30
|
-
expect(versionFromAssetName('android', '
|
|
39
|
+
expect(versionFromAssetName('android', 'Kabmo-1.2.3-android.apk.zip')).toBeNull();
|
|
31
40
|
expect(versionFromAssetName('android', 'source.tar.gz')).toBeNull();
|
|
32
|
-
expect(versionFromAssetName('android', '
|
|
41
|
+
expect(versionFromAssetName('android', 'Kabmo-latest-android.apk')).toBeNull();
|
|
42
|
+
expect(versionFromAssetName('android', 'Kabmo--android.apk')).toBeNull();
|
|
33
43
|
expect(versionFromAssetName('android', '')).toBeNull();
|
|
34
44
|
});
|
|
35
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The desktop assets share the `Kabmo-` prefix, so the EXTENSION is what keeps
|
|
48
|
+
* them out. A probe that matched on the prefix alone would offer a Mac `.dmg`
|
|
49
|
+
* to an Android phone.
|
|
50
|
+
*/
|
|
51
|
+
test('does not read a desktop asset as a mobile one', () => {
|
|
52
|
+
for (const name of [
|
|
53
|
+
'Kabmo-0.1.2-mac-arm64.zip',
|
|
54
|
+
'Kabmo-0.1.2-mac-arm64.dmg',
|
|
55
|
+
'Kabmo-0.1.0-win-x64-setup.exe',
|
|
56
|
+
'Kabmo-0.1.0-linux-x86_64.tar.gz',
|
|
57
|
+
'Kabmo-0.1.0-linux-amd64.deb',
|
|
58
|
+
'Kabmo-0.1.0-linux-x86_64.AppImage',
|
|
59
|
+
]) {
|
|
60
|
+
expect(versionFromAssetName('android', name), name).toBeNull();
|
|
61
|
+
expect(versionFromAssetName('ios', name), name).toBeNull();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
36
65
|
test('does not accept one shell’s asset for the other', () => {
|
|
37
66
|
// The two probes read the SAME release list, so a parser that ignored the
|
|
38
67
|
// kind would let an iOS probe answer with the APK (and offer an `.apk` to an
|
|
39
68
|
// iPhone).
|
|
69
|
+
expect(versionFromAssetName('ios', 'Kabmo-0.5.0-android.apk')).toBeNull();
|
|
70
|
+
expect(versionFromAssetName('android', 'Kabmo-0.1.0-ios.ipa')).toBeNull();
|
|
40
71
|
expect(versionFromAssetName('ios', 'kabmo-android-v1.2.3.apk')).toBeNull();
|
|
41
72
|
expect(versionFromAssetName('android', 'kabmo-ios-v1.2.3.ipa')).toBeNull();
|
|
42
73
|
});
|
|
@@ -67,15 +98,15 @@ describe('fetchShellRelease', () => {
|
|
|
67
98
|
assets: [
|
|
68
99
|
{ name: 'source.tar.gz', browser_download_url: 'https://x/src.tgz' },
|
|
69
100
|
{
|
|
70
|
-
name: '
|
|
71
|
-
browser_download_url: 'https://x/
|
|
101
|
+
name: 'Kabmo-1.2.3-android.apk',
|
|
102
|
+
browser_download_url: 'https://x/Kabmo-1.2.3-android.apk',
|
|
72
103
|
},
|
|
73
104
|
],
|
|
74
105
|
},
|
|
75
106
|
]);
|
|
76
107
|
|
|
77
108
|
expect(await fetchShellRelease('android', impl)).toEqual({
|
|
78
|
-
url: 'https://x/
|
|
109
|
+
url: 'https://x/Kabmo-1.2.3-android.apk',
|
|
79
110
|
latest: '1.2.3',
|
|
80
111
|
releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v9.9.9',
|
|
81
112
|
});
|
|
@@ -86,8 +117,8 @@ describe('fetchShellRelease', () => {
|
|
|
86
117
|
{
|
|
87
118
|
html_url: 'https://x/app',
|
|
88
119
|
assets: [
|
|
89
|
-
{ name: '
|
|
90
|
-
{ name: '
|
|
120
|
+
{ name: 'Kabmo-0.9.0-android.apk', browser_download_url: 'https://x/a.apk' },
|
|
121
|
+
{ name: 'Kabmo-0.4.0-ios.ipa', browser_download_url: 'https://x/i.ipa' },
|
|
91
122
|
],
|
|
92
123
|
},
|
|
93
124
|
]);
|
|
@@ -99,14 +130,54 @@ describe('fetchShellRelease', () => {
|
|
|
99
130
|
});
|
|
100
131
|
});
|
|
101
132
|
|
|
133
|
+
/**
|
|
134
|
+
* A release that carries BOTH spellings must answer with the current one.
|
|
135
|
+
*
|
|
136
|
+
* That state is real: a rename lands, and the previous release's asset stays
|
|
137
|
+
* beside the new one until `--clobber` replaces it (or a hand-upload adds
|
|
138
|
+
* one). Measured before the fix: a release holding both
|
|
139
|
+
* `kabmo-android-v0.4.0.apk` and `Kabmo-0.6.0-android.apk` answered `0.4.0` —
|
|
140
|
+
* the console offered an update to the version the user already had, and hid
|
|
141
|
+
* the newer one.
|
|
142
|
+
*/
|
|
143
|
+
test('prefers the current scheme when a release carries both', async () => {
|
|
144
|
+
const { impl } = stubFetch([
|
|
145
|
+
{
|
|
146
|
+
html_url: 'https://x/r',
|
|
147
|
+
assets: [
|
|
148
|
+
{ name: 'kabmo-android-v0.4.0.apk', browser_download_url: 'https://x/old.apk' },
|
|
149
|
+
{ name: 'Kabmo-0.6.0-android.apk', browser_download_url: 'https://x/new.apk' },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
expect(await fetchShellRelease('android', impl)).toEqual({
|
|
155
|
+
url: 'https://x/new.apk',
|
|
156
|
+
latest: '0.6.0',
|
|
157
|
+
releaseUrl: 'https://x/r',
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/** A release carrying ONLY the old spelling must keep answering. */
|
|
162
|
+
test('still resolves a release that only has the legacy spelling', async () => {
|
|
163
|
+
const { impl } = stubFetch([
|
|
164
|
+
{
|
|
165
|
+
html_url: 'https://x/r2',
|
|
166
|
+
assets: [{ name: 'kabmo-android-v0.4.0.apk', browser_download_url: 'https://x/old.apk' }],
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect((await fetchShellRelease('android', impl))?.latest).toBe('0.4.0');
|
|
171
|
+
});
|
|
172
|
+
|
|
102
173
|
test('the two shells cache separately', async () => {
|
|
103
174
|
// One shared slot would make the second probe answer with the first one's
|
|
104
175
|
// artifact — the release list is the same read, the asset is not.
|
|
105
176
|
const release = {
|
|
106
177
|
html_url: 'https://x/app',
|
|
107
178
|
assets: [
|
|
108
|
-
{ name: '
|
|
109
|
-
{ name: '
|
|
179
|
+
{ name: 'Kabmo-1.0.0-android.apk', browser_download_url: 'https://x/a.apk' },
|
|
180
|
+
{ name: 'Kabmo-2.0.0-ios.ipa', browser_download_url: 'https://x/i.ipa' },
|
|
110
181
|
],
|
|
111
182
|
};
|
|
112
183
|
expect((await fetchShellRelease('android', stubFetch([release]).impl))?.latest).toBe('1.0.0');
|
|
@@ -129,7 +200,7 @@ describe('fetchShellRelease', () => {
|
|
|
129
200
|
{ html_url: 'https://x/kabmo-ui', assets: [] },
|
|
130
201
|
{
|
|
131
202
|
html_url: 'https://x/app',
|
|
132
|
-
assets: [{ name: '
|
|
203
|
+
assets: [{ name: 'Kabmo-0.3.0-android.apk', browser_download_url: 'https://x/a.apk' }],
|
|
133
204
|
},
|
|
134
205
|
]);
|
|
135
206
|
|
|
@@ -145,16 +216,16 @@ describe('fetchShellRelease', () => {
|
|
|
145
216
|
{
|
|
146
217
|
draft: true,
|
|
147
218
|
html_url: 'https://x/draft',
|
|
148
|
-
assets: [{ name: '
|
|
219
|
+
assets: [{ name: 'Kabmo-9.9.9-android.apk', browser_download_url: 'https://x/draft.apk' }],
|
|
149
220
|
},
|
|
150
221
|
{
|
|
151
222
|
prerelease: true,
|
|
152
223
|
html_url: 'https://x/pre',
|
|
153
|
-
assets: [{ name: '
|
|
224
|
+
assets: [{ name: 'Kabmo-8.0.0-android.apk', browser_download_url: 'https://x/pre.apk' }],
|
|
154
225
|
},
|
|
155
226
|
{
|
|
156
227
|
html_url: 'https://x/app',
|
|
157
|
-
assets: [{ name: '
|
|
228
|
+
assets: [{ name: 'Kabmo-0.3.0-android.apk', browser_download_url: 'https://x/a.apk' }],
|
|
158
229
|
},
|
|
159
230
|
]);
|
|
160
231
|
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
*
|
|
18
18
|
* - `url` is the asset to download. It is resolved from the release's OWN asset
|
|
19
19
|
* list rather than built as `releases/latest/download/<name>`, because that
|
|
20
|
-
* path needs the asset name to match exactly and the name carries the
|
|
21
|
-
* (`
|
|
22
|
-
* the workflow's naming rule, and the first release that renames it
|
|
20
|
+
* path needs the asset name to match exactly and the name carries the shell's
|
|
21
|
+
* version (`Kabmo-0.5.0-android.apk`) — a name built here would be a second
|
|
22
|
+
* copy of the workflow's naming rule, and the first release that renames it
|
|
23
|
+
* 404s.
|
|
23
24
|
* - `latest` is the version that asset ships, read from the asset name. The
|
|
24
25
|
* release TAG cannot be used: the artifact is attached to the app's release,
|
|
25
26
|
* whose tag names the SERVER version, and the two move independently.
|
|
@@ -42,25 +43,67 @@ export interface ShellRelease {
|
|
|
42
43
|
releaseUrl: string | null;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/**
|
|
47
|
+
* How each shell's asset is named by its publish workflow, plus the prefix it
|
|
48
|
+
* carried BEFORE the rename.
|
|
49
|
+
*
|
|
50
|
+
* ONE scheme for every shell in the project — `<App>-<version>-<platform>.<ext>`:
|
|
51
|
+
* `Kabmo-0.5.0-android.apk`, `Kabmo-0.1.0-ios.ipa`, and the desktop trio's
|
|
52
|
+
* `Kabmo-0.1.2-mac-arm64.dmg` / `-win-x64-setup.exe` / `-linux-x86_64.tar.gz`.
|
|
53
|
+
* The mobile pair carries no architecture because neither artifact is per-arch:
|
|
54
|
+
* the APK is one universal build (the shell is pure Kotlin with no native
|
|
55
|
+
* libraries, so there is nothing to split), and the IPA contains only the device
|
|
56
|
+
* slice (`arm64-apple-ios`). A `-arm64` in those names would claim a distinction
|
|
57
|
+
* that does not exist and would make the parser look for a second artifact.
|
|
58
|
+
*
|
|
59
|
+
* **`legacyPrefix` is not dead code.** These names are PARSED to answer "is a
|
|
60
|
+
* newer shell published", and every release already on GitHub carries the old
|
|
61
|
+
* spelling. Dropping it would make the console answer "no update" for an
|
|
62
|
+
* installed shell the moment this ships — silently, because a name that does not
|
|
63
|
+
* match reads exactly like a release with no artifact. The probe tries the
|
|
64
|
+
* current prefix first, then the old one; new releases only ever write the
|
|
65
|
+
* current one.
|
|
66
|
+
*/
|
|
67
|
+
export const SHELL_ASSETS: Record<
|
|
68
|
+
ShellKind,
|
|
69
|
+
{ prefix: string; extension: string; legacyPrefix: string; legacyExtension: string }
|
|
70
|
+
> = {
|
|
71
|
+
android: {
|
|
72
|
+
prefix: 'Kabmo-',
|
|
73
|
+
extension: '-android.apk',
|
|
74
|
+
legacyPrefix: 'kabmo-android-v',
|
|
75
|
+
legacyExtension: '.apk',
|
|
76
|
+
},
|
|
77
|
+
ios: { prefix: 'Kabmo-', extension: '-ios.ipa', legacyPrefix: 'kabmo-ios-v', legacyExtension: '.ipa' },
|
|
49
78
|
};
|
|
50
79
|
|
|
51
80
|
/**
|
|
52
|
-
* `
|
|
81
|
+
* `Kabmo-0.5.0-android.apk` → `0.5.0` (and the `.ipa` twin), plus the legacy
|
|
82
|
+
* `kabmo-android-v0.5.0.apk` spelling every published release still carries.
|
|
53
83
|
*
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
56
|
-
*
|
|
84
|
+
* The version is the MIDDLE of the name in the current scheme and the TAIL of the
|
|
85
|
+
* legacy one, so the two are parsed by different rules rather than by one that
|
|
86
|
+
* tries to cover both.
|
|
57
87
|
*/
|
|
58
88
|
export function versionFromAssetName(kind: ShellKind, name: string): string | null {
|
|
59
|
-
const { prefix, extension } = SHELL_ASSETS[kind];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
89
|
+
const { prefix, extension, legacyPrefix, legacyExtension } = SHELL_ASSETS[kind];
|
|
90
|
+
|
|
91
|
+
// Current: `<prefix><version><extension>`, where the prefix is the app's name
|
|
92
|
+
// and the extension names the platform.
|
|
93
|
+
if (name.startsWith(prefix) && name.endsWith(extension)) {
|
|
94
|
+
const middle = name.slice(prefix.length, -extension.length);
|
|
95
|
+
if (/^\d+\.\d+\.\d+$/.test(middle)) return middle;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Legacy: `kabmo-android-v<version>.apk`. Kept for the releases already
|
|
99
|
+
// published; no new release writes it.
|
|
100
|
+
if (name.startsWith(legacyPrefix) && name.endsWith(legacyExtension)) {
|
|
101
|
+
const middle = name.slice(legacyPrefix.length, -legacyExtension.length);
|
|
102
|
+
const version = middle.startsWith('v') ? middle.slice(1) : middle;
|
|
103
|
+
return /^\d+\.\d+\.\d+$/.test(version) ? version : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
64
107
|
}
|
|
65
108
|
|
|
66
109
|
/**
|
|
@@ -144,17 +187,41 @@ export async function fetchShellRelease(
|
|
|
144
187
|
/** How many releases to look back through for the artifact. */
|
|
145
188
|
const RELEASE_SCAN = 20;
|
|
146
189
|
|
|
147
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* The asset of a release that names this shell's artifact, or null.
|
|
192
|
+
*
|
|
193
|
+
* The CURRENT scheme wins over the legacy one, and that is not cosmetic. A
|
|
194
|
+
* release that carries both — which happens the moment a rename lands and
|
|
195
|
+
* `--clobber` has not yet replaced the old asset, or a hand-upload adds one
|
|
196
|
+
* beside the other — would otherwise resolve to whichever the asset list
|
|
197
|
+
* happened to order first. Measured: a release holding both
|
|
198
|
+
* `kabmo-android-v0.4.0.apk` and `Kabmo-0.6.0-android.apk` answered `0.4.0`,
|
|
199
|
+
* i.e. the console offered an update to a version it already had and hid the
|
|
200
|
+
* newer one.
|
|
201
|
+
*
|
|
202
|
+
* Both schemes are still ACCEPTED (a release with only the old spelling must
|
|
203
|
+
* keep working), so the preference is a second pass rather than a filter.
|
|
204
|
+
*/
|
|
148
205
|
function shellAsset(kind: ShellKind, assets: unknown): Record<string, unknown> | null {
|
|
149
206
|
if (!Array.isArray(assets)) return null;
|
|
150
207
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
208
|
+
const named = assets.filter(
|
|
209
|
+
(entry): entry is Record<string, unknown> =>
|
|
210
|
+
entry !== null &&
|
|
211
|
+
typeof entry === 'object' &&
|
|
212
|
+
typeof (entry as Record<string, unknown>).name === 'string' &&
|
|
213
|
+
versionFromAssetName(kind, String((entry as Record<string, unknown>).name)) !== null,
|
|
214
|
+
);
|
|
156
215
|
|
|
157
|
-
|
|
216
|
+
const { prefix, extension } = SHELL_ASSETS[kind];
|
|
217
|
+
return (
|
|
218
|
+
named.find((asset) => {
|
|
219
|
+
const name = String(asset.name);
|
|
220
|
+
return name.startsWith(prefix) && name.endsWith(extension);
|
|
221
|
+
}) ??
|
|
222
|
+
named[0] ??
|
|
223
|
+
null
|
|
224
|
+
);
|
|
158
225
|
}
|
|
159
226
|
|
|
160
227
|
const CACHE_MS = 5 * 60_000;
|