@tuturuuu/utils 0.21.0 → 0.23.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/CHANGELOG.md +21 -0
- package/biome.json +1 -1
- package/package.json +13 -13
- package/src/__tests__/api-proxy-guard.test.ts +29 -0
- package/src/api-proxy-guard.ts +5 -0
- package/src/build-info-route.test.ts +23 -0
- package/src/build-info-route.ts +29 -0
- package/src/launchable-apps.test.ts +11 -0
- package/src/launchable-apps.ts +23 -0
- package/src/next-config.test.ts +15 -0
- package/src/next-config.ts +8 -1
- package/src/onboarding.test.ts +35 -0
- package/src/onboarding.ts +199 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.23.0](https://github.com/tutur3u/platform/compare/utils-v0.22.0...utils-v0.23.0) (2026-08-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **platform:** expose the running build over HTTP for every app ([7b90d42](https://github.com/tutur3u/platform/commit/7b90d425a38048a1bf317b46f0da78225474f0e5))
|
|
9
|
+
|
|
10
|
+
## [0.22.0](https://github.com/tutur3u/platform/compare/utils-v0.21.0...utils-v0.22.0) (2026-08-04)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **chat:** add connected site sync bridge ([#5078](https://github.com/tutur3u/platform/issues/5078)) ([3de1a07](https://github.com/tutur3u/platform/commit/3de1a07dfa2fec96982aa0d116e003d0aa3fe47c))
|
|
16
|
+
* **onboarding:** connect product guidance across apps ([68cf626](https://github.com/tutur3u/platform/commit/68cf626c9650e5044b6c123f9423a6cebf1bba9a))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **ai:** accept rich embedding payloads ([a65b13f](https://github.com/tutur3u/platform/commit/a65b13fa80034959e5f18ee4515b3081002558e2))
|
|
22
|
+
* **tooling:** support webpack verification builds ([5275e10](https://github.com/tutur3u/platform/commit/5275e10c4007ae92e84be6c057a79f223980700c))
|
|
23
|
+
|
|
3
24
|
## [0.21.0](https://github.com/tutur3u/platform/compare/utils-v0.20.0...utils-v0.21.0) (2026-07-28)
|
|
4
25
|
|
|
5
26
|
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/utils",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -20,39 +20,39 @@
|
|
|
20
20
|
"@noble/ciphers": "^2.2.0",
|
|
21
21
|
"@noble/hashes": "^2.2.0",
|
|
22
22
|
"@tanstack/react-query": "^5.101.4",
|
|
23
|
-
"@tiptap/core": "3.
|
|
24
|
-
"@tiptap/react": "3.
|
|
23
|
+
"@tiptap/core": "3.29.2",
|
|
24
|
+
"@tiptap/react": "3.29.2",
|
|
25
25
|
"@tuturuuu/google": "0.1.0",
|
|
26
26
|
"@tuturuuu/icons": "0.1.0",
|
|
27
|
-
"@tuturuuu/internal-api": "0.
|
|
27
|
+
"@tuturuuu/internal-api": "0.29.0",
|
|
28
28
|
"@tuturuuu/supabase": "0.5.0",
|
|
29
29
|
"@upstash/ratelimit": "^2.0.8",
|
|
30
|
-
"@upstash/redis": "^1.38.
|
|
30
|
+
"@upstash/redis": "^1.38.2",
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"date-fns": "^4.4.0",
|
|
33
33
|
"dayjs": "^1.11.21",
|
|
34
34
|
"diff": "^9.0.0",
|
|
35
|
-
"effect": "^3.22.
|
|
35
|
+
"effect": "^3.22.1",
|
|
36
36
|
"moment": "^2.30.1",
|
|
37
|
-
"next": "^16.3.0
|
|
37
|
+
"next": "^16.3.0",
|
|
38
38
|
"prosemirror-model": "^1.25.11",
|
|
39
39
|
"react": "^19.2.8",
|
|
40
40
|
"react-dom": "^19.2.8",
|
|
41
41
|
"tailwind-merge": "^3.6.0",
|
|
42
42
|
"uuid": "^14.0.1",
|
|
43
43
|
"y-prosemirror": "^1.3.7",
|
|
44
|
-
"yjs": "^13.6.
|
|
44
|
+
"yjs": "^13.6.32",
|
|
45
45
|
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@tuturuuu/types": "0.
|
|
48
|
+
"@tuturuuu/types": "0.27.0",
|
|
49
49
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
50
50
|
"@types/diff": "^8.0.0",
|
|
51
|
-
"@types/node": "^26.1.
|
|
52
|
-
"@types/react": "^19.2.
|
|
53
|
-
"@vitejs/plugin-react": "^6.0.
|
|
51
|
+
"@types/node": "^26.1.2",
|
|
52
|
+
"@types/react": "^19.2.18",
|
|
53
|
+
"@vitejs/plugin-react": "^6.0.5",
|
|
54
54
|
"typescript": "7.0.2",
|
|
55
|
-
"vite": "^8.
|
|
55
|
+
"vite": "^8.2.0",
|
|
56
56
|
"vitest": "^4.1.10"
|
|
57
57
|
},
|
|
58
58
|
"exports": {
|
|
@@ -159,6 +159,35 @@ describe('guardApiProxyRequest', () => {
|
|
|
159
159
|
expect(mocks.validateEmoji).not.toHaveBeenCalled();
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
it('can skip form-content validation without skipping payload limits', async () => {
|
|
163
|
+
mocks.validateEmoji.mockResolvedValue(
|
|
164
|
+
new Response(JSON.stringify({ error: 'Bad Request' }), { status: 400 })
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const { guardApiProxyRequest } = await import('../api-proxy-guard.js');
|
|
168
|
+
const response = await guardApiProxyRequest(
|
|
169
|
+
makeRequest('/v1/embeddings', 'POST'),
|
|
170
|
+
{
|
|
171
|
+
prefixBase: 'proxy:test:ai',
|
|
172
|
+
skipContentValidation: true,
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(response).toBeNull();
|
|
177
|
+
expect(mocks.validateEmoji).not.toHaveBeenCalled();
|
|
178
|
+
|
|
179
|
+
const oversized = await guardApiProxyRequest(
|
|
180
|
+
makeRequest('/v1/embeddings', 'POST', {
|
|
181
|
+
'content-length': `${1024 * 1024 + 1}`,
|
|
182
|
+
}),
|
|
183
|
+
{
|
|
184
|
+
prefixBase: 'proxy:test:ai',
|
|
185
|
+
skipContentValidation: true,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
expect(oversized?.status).toBe(413);
|
|
189
|
+
});
|
|
190
|
+
|
|
162
191
|
it('returns an IP-block response when the client is blocked', async () => {
|
|
163
192
|
vi.stubEnv('NODE_ENV', 'production');
|
|
164
193
|
mocks.extractIp.mockReturnValue('1.2.3.4');
|
package/src/api-proxy-guard.ts
CHANGED
|
@@ -88,6 +88,7 @@ export type GuardOptions = {
|
|
|
88
88
|
additionalRoutePolicies?: ProxyRoutePolicy[];
|
|
89
89
|
prefixBase: string;
|
|
90
90
|
routePolicies?: ProxyRoutePolicy[];
|
|
91
|
+
skipContentValidation?: boolean;
|
|
91
92
|
trustedBypassRules?: TrustedProxyBypassRule[];
|
|
92
93
|
};
|
|
93
94
|
|
|
@@ -1385,6 +1386,10 @@ export async function guardApiProxyRequest(
|
|
|
1385
1386
|
? ['snapshot']
|
|
1386
1387
|
: undefined;
|
|
1387
1388
|
|
|
1389
|
+
if (options.skipContentValidation) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1388
1393
|
return (
|
|
1389
1394
|
(await validateRequestEmojiLimit(req, {
|
|
1390
1395
|
allowDescriptionYjsState,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createBuildInfoHandler } from './build-info-route';
|
|
3
|
+
|
|
4
|
+
describe('createBuildInfoHandler', () => {
|
|
5
|
+
it('reports the app name and the build the bundle was made from', async () => {
|
|
6
|
+
const response = createBuildInfoHandler('tasks')();
|
|
7
|
+
const body = await response.json();
|
|
8
|
+
|
|
9
|
+
expect(response.status).toBe(200);
|
|
10
|
+
expect(body.appName).toBe('tasks');
|
|
11
|
+
expect(typeof body.commitHash).toBe('string');
|
|
12
|
+
expect(body.commitHash.length).toBeGreaterThan(0);
|
|
13
|
+
expect(typeof body.version).toBe('string');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('is never cached', () => {
|
|
17
|
+
// A cached build stamp would defeat the point: it could report a commit
|
|
18
|
+
// the edge served earlier rather than the one running now.
|
|
19
|
+
const response = createBuildInfoHandler('web')();
|
|
20
|
+
|
|
21
|
+
expect(response.headers.get('cache-control')).toContain('no-store');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getPlatformReleaseInfo } from './platform-release';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Answer "which commit is actually serving this app right now?" over HTTP.
|
|
5
|
+
*
|
|
6
|
+
* The build metadata has always been generated into the bundle, but nothing
|
|
7
|
+
* exposed it at runtime, so the only way to check what a deployment contained
|
|
8
|
+
* was to infer it. That inference is unreliable: a deployment cancelled by a
|
|
9
|
+
* later push can leave an app serving code from before a merge while every
|
|
10
|
+
* workflow reports success, and `vercel inspect` returns no git metadata for
|
|
11
|
+
* these projects. Without this endpoint, "is my fix live?" is unanswerable.
|
|
12
|
+
*
|
|
13
|
+
* Returns a plain `Response` so route handlers in every app can re-export it
|
|
14
|
+
* without this module depending on a framework.
|
|
15
|
+
*/
|
|
16
|
+
export function createBuildInfoHandler(appName: string) {
|
|
17
|
+
return function GET(): Response {
|
|
18
|
+
const info = getPlatformReleaseInfo(appName);
|
|
19
|
+
|
|
20
|
+
return new Response(JSON.stringify(info, null, 2), {
|
|
21
|
+
headers: {
|
|
22
|
+
'content-type': 'application/json; charset=utf-8',
|
|
23
|
+
// Never let a CDN answer this: a cached build stamp is worse than none.
|
|
24
|
+
'cache-control': 'no-store, max-age=0',
|
|
25
|
+
},
|
|
26
|
+
status: 200,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
getLaunchableApp,
|
|
4
|
+
getLaunchableAppByHostname,
|
|
4
5
|
getLaunchableAppByTitle,
|
|
5
6
|
getLaunchableAppOrigin,
|
|
6
7
|
LAUNCHABLE_APP_CATEGORIES,
|
|
@@ -36,6 +37,16 @@ describe('launchable apps', () => {
|
|
|
36
37
|
expect(getLaunchableAppByTitle('apps gateway')?.slug).toBe('apps');
|
|
37
38
|
});
|
|
38
39
|
|
|
40
|
+
it('resolves production and Portless hostnames', () => {
|
|
41
|
+
expect(getLaunchableAppByHostname('calendar.tuturuuu.com')?.slug).toBe(
|
|
42
|
+
'calendar'
|
|
43
|
+
);
|
|
44
|
+
expect(getLaunchableAppByHostname('finance.tuturuuu.localhost')?.slug).toBe(
|
|
45
|
+
'finance'
|
|
46
|
+
);
|
|
47
|
+
expect(getLaunchableAppByHostname('localhost:7808')?.slug).toBe('finance');
|
|
48
|
+
});
|
|
49
|
+
|
|
39
50
|
it('keeps retired apps out of the catalog and pins category overrides', () => {
|
|
40
51
|
expect(LAUNCHABLE_APPS.map((app) => app.slug)).not.toContain('external');
|
|
41
52
|
expect(LAUNCHABLE_APPS.map((app) => app.slug)).not.toContain('playground');
|
package/src/launchable-apps.ts
CHANGED
|
@@ -416,6 +416,29 @@ export function getLaunchableAppByTitle(value?: string | null) {
|
|
|
416
416
|
);
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
export function getLaunchableAppByHostname(hostname: string) {
|
|
420
|
+
const normalized = hostname.trim().toLowerCase();
|
|
421
|
+
if (!normalized) return null;
|
|
422
|
+
const normalizedHostname = normalized.split(':')[0];
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
LAUNCHABLE_APPS.find((app) => {
|
|
426
|
+
if (normalizedHostname === new URL(app.productionUrl).hostname)
|
|
427
|
+
return true;
|
|
428
|
+
if (
|
|
429
|
+
normalizedHostname === `${app.portlessApp}.tuturuuu.localhost` ||
|
|
430
|
+
normalizedHostname === `${app.slug}.tuturuuu.localhost`
|
|
431
|
+
) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return 'localhostOrigin' in app && app.localhostOrigin
|
|
436
|
+
? normalized === new URL(app.localhostOrigin).host
|
|
437
|
+
: false;
|
|
438
|
+
}) ?? null
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
419
442
|
function getAutoEnvironment(currentOrigin?: string): LaunchableAppEnvironment {
|
|
420
443
|
if (currentOrigin?.includes('tuturuuu.localhost')) return 'portless';
|
|
421
444
|
if (currentOrigin?.includes('localhost')) return 'localhost';
|
package/src/next-config.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createTuturuuuNextConfig,
|
|
5
5
|
isTuturuuuNextCacheComponentsEnabled,
|
|
6
6
|
isTuturuuuNextReactCompilerEnabled,
|
|
7
|
+
isTuturuuuTurbopackRustReactCompilerEnabled,
|
|
7
8
|
resolveTuturuuuInfrastructureAppUrl,
|
|
8
9
|
resolveTuturuuuWebAppUrl,
|
|
9
10
|
TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS,
|
|
@@ -267,6 +268,20 @@ describe('isTuturuuuNextCacheComponentsEnabled', () => {
|
|
|
267
268
|
});
|
|
268
269
|
});
|
|
269
270
|
|
|
271
|
+
describe('isTuturuuuTurbopackRustReactCompilerEnabled', () => {
|
|
272
|
+
it('keeps the native compiler enabled for normal builds', () => {
|
|
273
|
+
expect(isTuturuuuTurbopackRustReactCompilerEnabled({})).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('disables the Turbopack-only compiler for webpack fallback builds', () => {
|
|
277
|
+
expect(
|
|
278
|
+
isTuturuuuTurbopackRustReactCompilerEnabled({
|
|
279
|
+
NEXT_WEBPACK_BUILD: '1',
|
|
280
|
+
})
|
|
281
|
+
).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
270
285
|
describe('isTuturuuuNextReactCompilerEnabled', () => {
|
|
271
286
|
it('keeps React Compiler enabled for next dev', () => {
|
|
272
287
|
expect(
|
package/src/next-config.ts
CHANGED
|
@@ -83,6 +83,12 @@ export function isTuturuuuNextCacheComponentsEnabled(
|
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function isTuturuuuTurbopackRustReactCompilerEnabled(
|
|
87
|
+
env: Environment = process.env
|
|
88
|
+
) {
|
|
89
|
+
return env.NEXT_WEBPACK_BUILD !== '1';
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
|
|
87
93
|
const experimentalConfig = config.experimental ?? {};
|
|
88
94
|
const imageConfig = config.images ?? {};
|
|
@@ -116,7 +122,8 @@ export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
|
|
|
116
122
|
turbopackFileSystemCacheForBuild:
|
|
117
123
|
experimentalConfig.turbopackFileSystemCacheForBuild ?? true,
|
|
118
124
|
turbopackRustReactCompiler:
|
|
119
|
-
experimentalConfig.turbopackRustReactCompiler ??
|
|
125
|
+
experimentalConfig.turbopackRustReactCompiler ??
|
|
126
|
+
isTuturuuuTurbopackRustReactCompilerEnabled(),
|
|
120
127
|
optimizePackageImports: mergeStringArrays(
|
|
121
128
|
TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS,
|
|
122
129
|
experimentalConfig.optimizePackageImports
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { LAUNCHABLE_APPS } from './launchable-apps';
|
|
3
|
+
import {
|
|
4
|
+
ONBOARDING_MISSIONS,
|
|
5
|
+
ONBOARDING_PERSONAS,
|
|
6
|
+
recommendOnboardingApps,
|
|
7
|
+
} from './onboarding';
|
|
8
|
+
|
|
9
|
+
describe('connected onboarding manifest', () => {
|
|
10
|
+
it('has one dedicated mission for every launchable app', () => {
|
|
11
|
+
expect(ONBOARDING_MISSIONS.map(({ appSlug }) => appSlug).sort()).toEqual(
|
|
12
|
+
LAUNCHABLE_APPS.map(({ slug }) => slug).sort()
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('keeps every mission short and action-oriented', () => {
|
|
17
|
+
expect(
|
|
18
|
+
ONBOARDING_MISSIONS.every(({ capabilities }) => capabilities.length === 3)
|
|
19
|
+
).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it.each(ONBOARDING_PERSONAS)(
|
|
23
|
+
'returns launchable recommendations for %s',
|
|
24
|
+
(persona) => {
|
|
25
|
+
const recommendations = recommendOnboardingApps({
|
|
26
|
+
goals: [],
|
|
27
|
+
persona,
|
|
28
|
+
});
|
|
29
|
+
const launchable = new Set(LAUNCHABLE_APPS.map(({ slug }) => slug));
|
|
30
|
+
|
|
31
|
+
expect(recommendations.length).toBeGreaterThan(0);
|
|
32
|
+
expect(recommendations.every((slug) => launchable.has(slug))).toBe(true);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { LAUNCHABLE_APPS } from './launchable-apps';
|
|
2
|
+
|
|
3
|
+
export const ONBOARDING_PERSONAS = [
|
|
4
|
+
'professional',
|
|
5
|
+
'student',
|
|
6
|
+
'founder',
|
|
7
|
+
'executive',
|
|
8
|
+
'team_leader',
|
|
9
|
+
'educator',
|
|
10
|
+
'creator',
|
|
11
|
+
'developer',
|
|
12
|
+
'operations',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export const ONBOARDING_GOALS = [
|
|
16
|
+
'focus',
|
|
17
|
+
'collaborate',
|
|
18
|
+
'operate',
|
|
19
|
+
'learn',
|
|
20
|
+
'build',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type OnboardingPersona = (typeof ONBOARDING_PERSONAS)[number];
|
|
24
|
+
export type OnboardingGoal = (typeof ONBOARDING_GOALS)[number];
|
|
25
|
+
export type OnboardingAppSlug = (typeof LAUNCHABLE_APPS)[number]['slug'];
|
|
26
|
+
export type OnboardingActionSafety =
|
|
27
|
+
| 'preview'
|
|
28
|
+
| 'reversible'
|
|
29
|
+
| 'confirmation_required';
|
|
30
|
+
|
|
31
|
+
export type OnboardingMission = {
|
|
32
|
+
appSlug: OnboardingAppSlug;
|
|
33
|
+
capabilities: readonly [string, string, string];
|
|
34
|
+
goal: OnboardingGoal;
|
|
35
|
+
safety: OnboardingActionSafety;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type OnboardingJourneyPreferences = {
|
|
39
|
+
goals: OnboardingGoal[];
|
|
40
|
+
persona: OnboardingPersona | null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type OnboardingJourneyProgress = OnboardingJourneyPreferences & {
|
|
44
|
+
completedMissionIds: string[];
|
|
45
|
+
dismissedAt: string | null;
|
|
46
|
+
mode: 'standard' | 'employee_test';
|
|
47
|
+
replayApp: OnboardingAppSlug | null;
|
|
48
|
+
revision: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const ONBOARDING_JOURNEY_REVISION = 2;
|
|
52
|
+
|
|
53
|
+
export const PERSONA_RECOMMENDATIONS = {
|
|
54
|
+
professional: ['tasks', 'calendar', 'mail', 'drive', 'assistant'],
|
|
55
|
+
student: ['learn', 'rewise', 'calendar', 'tasks', 'mind'],
|
|
56
|
+
founder: ['tasks', 'finance', 'contacts', 'forms', 'assistant'],
|
|
57
|
+
executive: ['platform', 'finance', 'calendar', 'meet', 'assistant'],
|
|
58
|
+
team_leader: ['tasks', 'chat', 'calendar', 'meet', 'contacts'],
|
|
59
|
+
educator: ['teach', 'learn', 'forms', 'meet', 'drive'],
|
|
60
|
+
creator: ['cms', 'drive', 'shortener', 'forms', 'assistant'],
|
|
61
|
+
developer: ['git', 'ai', 'docs', 'tasks', 'tools'],
|
|
62
|
+
operations: ['inventory', 'finance', 'contacts', 'forms', 'tasks'],
|
|
63
|
+
} as const satisfies Record<OnboardingPersona, readonly string[]>;
|
|
64
|
+
|
|
65
|
+
export const GOAL_RECOMMENDATIONS = {
|
|
66
|
+
focus: ['tasks', 'calendar', 'mail', 'track', 'assistant'],
|
|
67
|
+
collaborate: ['chat', 'meet', 'drive', 'tasks', 'forms'],
|
|
68
|
+
operate: ['finance', 'inventory', 'contacts', 'pay', 'storefront'],
|
|
69
|
+
learn: ['learn', 'teach', 'rewise', 'mind', 'docs'],
|
|
70
|
+
build: ['ai', 'git', 'cms', 'nova', 'tools'],
|
|
71
|
+
} as const satisfies Record<OnboardingGoal, readonly string[]>;
|
|
72
|
+
|
|
73
|
+
const MISSIONS = [
|
|
74
|
+
['platform', 'focus', 'reversible', ['overview', 'apps', 'next_move']],
|
|
75
|
+
['calendar', 'focus', 'reversible', ['schedule', 'timebox', 'review']],
|
|
76
|
+
[
|
|
77
|
+
'chat',
|
|
78
|
+
'collaborate',
|
|
79
|
+
'confirmation_required',
|
|
80
|
+
['spaces', 'message', 'follow_up'],
|
|
81
|
+
],
|
|
82
|
+
['cms', 'build', 'confirmation_required', ['content', 'preview', 'publish']],
|
|
83
|
+
[
|
|
84
|
+
'drive',
|
|
85
|
+
'collaborate',
|
|
86
|
+
'confirmation_required',
|
|
87
|
+
['organize', 'share', 'find'],
|
|
88
|
+
],
|
|
89
|
+
['git', 'build', 'preview', ['repositories', 'browse', 'search']],
|
|
90
|
+
['apps', 'focus', 'reversible', ['discover', 'filter', 'launch']],
|
|
91
|
+
['docs', 'learn', 'preview', ['browse', 'search', 'reference']],
|
|
92
|
+
[
|
|
93
|
+
'finance',
|
|
94
|
+
'operate',
|
|
95
|
+
'confirmation_required',
|
|
96
|
+
['wallets', 'transactions', 'reports'],
|
|
97
|
+
],
|
|
98
|
+
['hive', 'build', 'preview', ['world', 'simulate', 'observe']],
|
|
99
|
+
[
|
|
100
|
+
'inventory',
|
|
101
|
+
'operate',
|
|
102
|
+
'confirmation_required',
|
|
103
|
+
['catalog', 'stock', 'movement'],
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
'storefront',
|
|
107
|
+
'operate',
|
|
108
|
+
'confirmation_required',
|
|
109
|
+
['store', 'preview', 'checkout'],
|
|
110
|
+
],
|
|
111
|
+
['learn', 'learn', 'reversible', ['courses', 'study', 'progress']],
|
|
112
|
+
['mail', 'focus', 'confirmation_required', ['inbox', 'compose', 'follow_up']],
|
|
113
|
+
['meet', 'collaborate', 'confirmation_required', ['room', 'invite', 'meet']],
|
|
114
|
+
['mind', 'learn', 'reversible', ['canvas', 'connect', 'organize']],
|
|
115
|
+
[
|
|
116
|
+
'ai',
|
|
117
|
+
'build',
|
|
118
|
+
'confirmation_required',
|
|
119
|
+
['playground', 'models', 'evaluate'],
|
|
120
|
+
],
|
|
121
|
+
['nova', 'build', 'confirmation_required', ['challenge', 'prompt', 'submit']],
|
|
122
|
+
['tools', 'build', 'reversible', ['choose', 'configure', 'generate']],
|
|
123
|
+
['rewise', 'learn', 'reversible', ['material', 'practice', 'review']],
|
|
124
|
+
['shortener', 'build', 'reversible', ['link', 'customize', 'measure']],
|
|
125
|
+
['tasks', 'focus', 'reversible', ['capture', 'plan', 'complete']],
|
|
126
|
+
['teach', 'learn', 'confirmation_required', ['course', 'lesson', 'share']],
|
|
127
|
+
['pay', 'operate', 'confirmation_required', ['request', 'review', 'pay']],
|
|
128
|
+
[
|
|
129
|
+
'contacts',
|
|
130
|
+
'operate',
|
|
131
|
+
'confirmation_required',
|
|
132
|
+
['people', 'fields', 'follow_up'],
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
'forms',
|
|
136
|
+
'collaborate',
|
|
137
|
+
'confirmation_required',
|
|
138
|
+
['build', 'preview', 'share'],
|
|
139
|
+
],
|
|
140
|
+
['track', 'focus', 'reversible', ['timer', 'session', 'insights']],
|
|
141
|
+
] as const satisfies readonly (readonly [
|
|
142
|
+
OnboardingAppSlug,
|
|
143
|
+
OnboardingGoal,
|
|
144
|
+
OnboardingActionSafety,
|
|
145
|
+
readonly [string, string, string],
|
|
146
|
+
])[];
|
|
147
|
+
|
|
148
|
+
export const ONBOARDING_MISSIONS: readonly OnboardingMission[] = MISSIONS.map(
|
|
149
|
+
([appSlug, goal, safety, capabilities]) => ({
|
|
150
|
+
appSlug,
|
|
151
|
+
capabilities,
|
|
152
|
+
goal,
|
|
153
|
+
safety,
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
export function getOnboardingMission(appSlug: string) {
|
|
158
|
+
return ONBOARDING_MISSIONS.find((mission) => mission.appSlug === appSlug);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function recommendOnboardingApps(
|
|
162
|
+
preferences: OnboardingJourneyPreferences,
|
|
163
|
+
limit = 8
|
|
164
|
+
): OnboardingAppSlug[] {
|
|
165
|
+
const ranked = new Map<string, number>();
|
|
166
|
+
const add = (slug: string, score: number) =>
|
|
167
|
+
ranked.set(slug, (ranked.get(slug) ?? 0) + score);
|
|
168
|
+
|
|
169
|
+
if (preferences.persona) {
|
|
170
|
+
PERSONA_RECOMMENDATIONS[preferences.persona].forEach((slug, index) => {
|
|
171
|
+
add(slug, 20 - index);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
preferences.goals.forEach((goal) => {
|
|
176
|
+
GOAL_RECOMMENDATIONS[goal].forEach((slug, index) => {
|
|
177
|
+
add(slug, 12 - index);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const launchable = new Set(LAUNCHABLE_APPS.map((app) => app.slug));
|
|
182
|
+
return [...ranked.entries()]
|
|
183
|
+
.filter(([slug]) => launchable.has(slug as OnboardingAppSlug))
|
|
184
|
+
.sort((left, right) => right[1] - left[1])
|
|
185
|
+
.slice(0, limit)
|
|
186
|
+
.map(([slug]) => slug as OnboardingAppSlug);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function createDefaultOnboardingProgress(): OnboardingJourneyProgress {
|
|
190
|
+
return {
|
|
191
|
+
completedMissionIds: [],
|
|
192
|
+
dismissedAt: null,
|
|
193
|
+
goals: [],
|
|
194
|
+
mode: 'standard',
|
|
195
|
+
persona: null,
|
|
196
|
+
replayApp: null,
|
|
197
|
+
revision: ONBOARDING_JOURNEY_REVISION,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
describe('platform release metadata', () => {
|
|
9
9
|
it('uses the centralized shared browser app version', () => {
|
|
10
|
-
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.35.0'); // x-release-please-version
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('preserves the source timestamp in builtAt and derives a short hash', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.35.0'; // x-release-please-version
|
|
4
4
|
|
|
5
5
|
export type PlatformBuildMetadataInput = {
|
|
6
6
|
/** ISO timestamp of the source commit used for the build, not deployment time. */
|