visionos-monorepo 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/.claude/worktrees/competent-burnell-8d1330/README.md +138 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/package.json +35 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/scripts/copy-web-assets.mjs +12 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/logout.ts +12 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/open.ts +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/start.ts +97 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/status.ts +23 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/userinfo.ts +47 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/index.ts +23 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/auth.ts +84 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/browser.ts +37 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/localState.ts +80 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/runtime.ts +203 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/runtime/index.ts +36 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/src/types/inquirer.d.ts +9 -0
- package/.claude/worktrees/competent-burnell-8d1330/cli/tsconfig.json +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/index.html +15 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/package.json +27 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/postcss.config.cjs +7 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/App.tsx +57 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/components/CliAuthPage.tsx +385 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/components/ManifestoPage.tsx +946 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/components/TrackCard.tsx +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/lib/api.ts +58 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/main.tsx +11 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/styles/index.css +33 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/src/styles/manifesto.css +1398 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/tailwind.config.ts +36 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/tsconfig.json +25 -0
- package/.claude/worktrees/competent-burnell-8d1330/client/vite.config.ts +20 -0
- package/.claude/worktrees/competent-burnell-8d1330/package-lock.json +5278 -0
- package/.claude/worktrees/competent-burnell-8d1330/package.json +24 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/package.json +25 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/app.ts +71 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/config/env.ts +14 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/features/auth/sessionStore.ts +74 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/index.ts +8 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/routes/auth.ts +112 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/src/routes/health.ts +14 -0
- package/.claude/worktrees/competent-burnell-8d1330/server/tsconfig.json +19 -0
- package/.claude/worktrees/competent-burnell-8d1330/shared/package.json +24 -0
- package/.claude/worktrees/competent-burnell-8d1330/shared/src/index.ts +91 -0
- package/.claude/worktrees/competent-burnell-8d1330/shared/tsconfig.json +16 -0
- package/.claude/worktrees/competent-burnell-8d1330/tsconfig.base.json +12 -0
- package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/index.html +392 -0
- package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/script.js +146 -0
- package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/styles.css +1082 -0
- package/.claude/worktrees/vigilant-napier-0de76f/README.md +138 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/package.json +35 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/scripts/copy-web-assets.mjs +12 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/logout.ts +12 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/open.ts +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/start.ts +97 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/status.ts +23 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/userinfo.ts +47 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/index.ts +23 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/auth.ts +84 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/browser.ts +37 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/localState.ts +80 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/runtime.ts +203 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/runtime/index.ts +36 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/src/types/inquirer.d.ts +9 -0
- package/.claude/worktrees/vigilant-napier-0de76f/cli/tsconfig.json +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/index.html +15 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/package.json +27 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/postcss.config.cjs +7 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/App.tsx +57 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/CliAuthPage.tsx +385 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/ManifestoPage.tsx +946 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/TrackCard.tsx +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/lib/api.ts +58 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/main.tsx +11 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/styles/index.css +33 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/src/styles/manifesto.css +1398 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/tailwind.config.ts +36 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/tsconfig.json +25 -0
- package/.claude/worktrees/vigilant-napier-0de76f/client/vite.config.ts +20 -0
- package/.claude/worktrees/vigilant-napier-0de76f/package-lock.json +5278 -0
- package/.claude/worktrees/vigilant-napier-0de76f/package.json +24 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/package.json +25 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/app.ts +71 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/config/env.ts +14 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/features/auth/sessionStore.ts +74 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/index.ts +8 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/routes/auth.ts +112 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/src/routes/health.ts +14 -0
- package/.claude/worktrees/vigilant-napier-0de76f/server/tsconfig.json +19 -0
- package/.claude/worktrees/vigilant-napier-0de76f/shared/package.json +24 -0
- package/.claude/worktrees/vigilant-napier-0de76f/shared/src/index.ts +91 -0
- package/.claude/worktrees/vigilant-napier-0de76f/shared/tsconfig.json +16 -0
- package/.claude/worktrees/vigilant-napier-0de76f/tsconfig.base.json +12 -0
- package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/index.html +392 -0
- package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/script.js +146 -0
- package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/styles.css +1082 -0
- package/.github/workflows/publish.yml +30 -0
- package/README.md +175 -0
- package/cli/README.md +165 -0
- package/cli/package.json +36 -0
- package/cli/scripts/copy-web-assets.mjs +12 -0
- package/cli/src/commands/lessons.ts +68 -0
- package/cli/src/commands/login.ts +46 -0
- package/cli/src/commands/logout.ts +12 -0
- package/cli/src/commands/open.ts +29 -0
- package/cli/src/commands/start.ts +146 -0
- package/cli/src/commands/status.ts +28 -0
- package/cli/src/commands/userinfo.ts +59 -0
- package/cli/src/index.ts +109 -0
- package/cli/src/lib/auth.ts +84 -0
- package/cli/src/lib/browser.ts +37 -0
- package/cli/src/lib/content.ts +57 -0
- package/cli/src/lib/lessonPrinter.ts +38 -0
- package/cli/src/lib/lessonRunner.ts +381 -0
- package/cli/src/lib/localState.ts +114 -0
- package/cli/src/lib/loginFlow.ts +74 -0
- package/cli/src/lib/progress.ts +94 -0
- package/cli/src/lib/runtime.ts +220 -0
- package/cli/src/lib/validator.ts +401 -0
- package/cli/src/runtime/index.ts +108 -0
- package/cli/src/types/inquirer.d.ts +9 -0
- package/cli/tsconfig.json +19 -0
- package/client/index.html +15 -0
- package/client/package.json +27 -0
- package/client/postcss.config.cjs +7 -0
- package/client/src/App.tsx +102 -0
- package/client/src/components/AccountPage.tsx +79 -0
- package/client/src/components/AuthPanel.tsx +312 -0
- package/client/src/components/CliAuthPage.tsx +367 -0
- package/client/src/components/CreatorPortal.tsx +885 -0
- package/client/src/components/ErrorBoundary.tsx +92 -0
- package/client/src/components/ManifestoPage.tsx +1126 -0
- package/client/src/components/TrackCard.tsx +19 -0
- package/client/src/lib/api.ts +215 -0
- package/client/src/main.tsx +14 -0
- package/client/src/styles/index.css +33 -0
- package/client/src/styles/manifesto.css +1828 -0
- package/client/tailwind.config.ts +36 -0
- package/client/tsconfig.json +25 -0
- package/client/vercel.json +8 -0
- package/client/vite.config.ts +33 -0
- package/package.json +27 -0
- package/server/package.json +26 -0
- package/server/src/app.ts +132 -0
- package/server/src/config/env.ts +135 -0
- package/server/src/features/accounts/accountStore.ts +359 -0
- package/server/src/features/accounts/contentStore.ts +264 -0
- package/server/src/features/accounts/password.ts +26 -0
- package/server/src/features/auth/sessionStore.ts +79 -0
- package/server/src/index.ts +8 -0
- package/server/src/routes/auth.ts +328 -0
- package/server/src/routes/content.ts +174 -0
- package/server/src/routes/health.ts +14 -0
- package/server/src/routes/progress.ts +105 -0
- package/server/tsconfig.json +19 -0
- package/shared/package.json +24 -0
- package/shared/src/index.ts +455 -0
- package/shared/tsconfig.json +16 -0
- package/tsconfig.base.json +12 -0
- package/visionos-manifesto/index.html +392 -0
- package/visionos-manifesto/script.js +146 -0
- package/visionos-manifesto/styles.css +1082 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createApp } from "../../../server/src/app.js";
|
|
4
|
+
import { readUserProfile, writeAccountState, writeUserProfile, readLocalProgress, writeLocalProgress } from "../lib/localState.js";
|
|
5
|
+
import { type LearningProgress, type UpsertLearningProgressRequest } from "../../../shared/src/index.js";
|
|
6
|
+
|
|
7
|
+
const host = process.env.HOST ?? "127.0.0.1";
|
|
8
|
+
const port = Number(process.env.PORT ?? 4173);
|
|
9
|
+
const appUrl = process.env.CLIENT_ORIGIN ?? `http://${host}:${port}`;
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
|
|
12
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function resolveStaticSiteDir(dir: string): string {
|
|
15
|
+
const envPath = process.env.VISIONOS_WEB_DIST;
|
|
16
|
+
if (envPath && fs.existsSync(envPath) && fs.existsSync(path.join(envPath, "index.html"))) {
|
|
17
|
+
return envPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const candidates = [
|
|
21
|
+
path.resolve(dir, "../web"),
|
|
22
|
+
path.resolve(dir, "../../dist/web"),
|
|
23
|
+
path.resolve(dir, "../../../client/dist"),
|
|
24
|
+
path.resolve(dir, "../../../dist/web")
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
if (fs.existsSync(candidate) && fs.existsSync(path.join(candidate, "index.html"))) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return path.resolve(dir, "../web");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const staticSiteDir = resolveStaticSiteDir(currentDir);
|
|
37
|
+
|
|
38
|
+
const app = createApp({
|
|
39
|
+
apiBaseUrl: appUrl,
|
|
40
|
+
clientOrigin: appUrl,
|
|
41
|
+
corsOrigin: true,
|
|
42
|
+
getCurrentUser: readUserProfile,
|
|
43
|
+
mode: "embedded",
|
|
44
|
+
getProgress: async (userId) => {
|
|
45
|
+
return readLocalProgress();
|
|
46
|
+
},
|
|
47
|
+
upsertProgress: async (userId, input: UpsertLearningProgressRequest) => {
|
|
48
|
+
const progressList = await readLocalProgress();
|
|
49
|
+
const existingIndex = progressList.findIndex((p) => p.trackId === input.trackId);
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
|
|
52
|
+
let completedLessonIds = input.completedLessonIds ?? [];
|
|
53
|
+
let completedStepIds = input.completedStepIds ?? [];
|
|
54
|
+
let currentLessonId = input.currentLessonId;
|
|
55
|
+
|
|
56
|
+
if (existingIndex >= 0) {
|
|
57
|
+
const existing = progressList[existingIndex];
|
|
58
|
+
completedLessonIds = Array.from(new Set([...existing.completedLessonIds, ...completedLessonIds]));
|
|
59
|
+
completedStepIds = Array.from(new Set([...existing.completedStepIds, ...completedStepIds]));
|
|
60
|
+
currentLessonId = currentLessonId ?? existing.currentLessonId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const updated: LearningProgress = {
|
|
64
|
+
userId,
|
|
65
|
+
trackId: input.trackId,
|
|
66
|
+
completedLessonIds,
|
|
67
|
+
completedStepIds,
|
|
68
|
+
currentLessonId,
|
|
69
|
+
updatedAt: now
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (existingIndex >= 0) {
|
|
73
|
+
progressList[existingIndex] = updated;
|
|
74
|
+
} else {
|
|
75
|
+
progressList.push(updated);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await writeLocalProgress(progressList);
|
|
79
|
+
return updated;
|
|
80
|
+
},
|
|
81
|
+
onAuthenticated: async (session) => {
|
|
82
|
+
if (!session.learner || !session.authenticatedAt) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await writeUserProfile({
|
|
87
|
+
authenticatedAt: session.authenticatedAt,
|
|
88
|
+
email: session.learner.email,
|
|
89
|
+
name: session.learner.name,
|
|
90
|
+
sessionId: session.sessionId,
|
|
91
|
+
trackId: session.trackId,
|
|
92
|
+
userId: session.learner.userId
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (session.accountToken) {
|
|
96
|
+
await writeAccountState({
|
|
97
|
+
authenticatedAt: session.authenticatedAt,
|
|
98
|
+
token: session.accountToken,
|
|
99
|
+
userId: session.learner.userId
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
staticSiteDir
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.listen(port, host, () => {
|
|
107
|
+
console.log(`VisionOS embedded runtime listening on ${appUrl}`);
|
|
108
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022"
|
|
8
|
+
],
|
|
9
|
+
"types": [
|
|
10
|
+
"node"
|
|
11
|
+
],
|
|
12
|
+
"noEmit": true
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*",
|
|
16
|
+
"../server/src/**/*.ts",
|
|
17
|
+
"../shared/src/**/*.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="VisionOS — A CLI-first terminal learning platform. Practice real commands with guided lessons." />
|
|
7
|
+
<title>VisionOS — Terminal Learning Platform</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
</head>
|
|
11
|
+
<body style="background:#E0DFD5; margin:0;">
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visionos/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc --noEmit && vite build",
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"lint": "tsc --noEmit",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"react": "^18.3.1",
|
|
14
|
+
"react-dom": "^18.3.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^18.3.12",
|
|
18
|
+
"@types/react-dom": "^18.3.1",
|
|
19
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
20
|
+
"autoprefixer": "^10.4.20",
|
|
21
|
+
"postcss": "^8.4.49",
|
|
22
|
+
"tailwindcss": "^3.4.14",
|
|
23
|
+
"typescript": "^5.6.3",
|
|
24
|
+
"vite": "^5.4.10"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { type VisionOsRuntimeInfo, type VisionOsUserProfile } from "../../shared/src/index.js";
|
|
3
|
+
import { AccountPage } from "./components/AccountPage.js";
|
|
4
|
+
import { CliAuthPage } from "./components/CliAuthPage.js";
|
|
5
|
+
import { ManifestoPage } from "./components/ManifestoPage.js";
|
|
6
|
+
import { CreatorPortal } from "./components/CreatorPortal.js";
|
|
7
|
+
import { getCurrentUser, getRuntimeInfo, storeAuthToken, getStoredAuthToken, VisionOsApiError } from "./lib/api.js";
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
const cliAuthSessionId = new URLSearchParams(window.location.search).get("cliAuthSession");
|
|
11
|
+
const accountMode = window.location.pathname === "/register" ? "register" : window.location.pathname === "/login" ? "login" : null;
|
|
12
|
+
|
|
13
|
+
// Intercept token from URL search parameters on initial load
|
|
14
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
15
|
+
const urlToken = urlParams.get("token");
|
|
16
|
+
if (urlToken) {
|
|
17
|
+
storeAuthToken(urlToken);
|
|
18
|
+
urlParams.delete("token");
|
|
19
|
+
const newSearch = urlParams.toString();
|
|
20
|
+
const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : "") + window.location.hash;
|
|
21
|
+
window.history.replaceState({}, "", newUrl);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [user, setUser] = useState<VisionOsUserProfile | null>(null);
|
|
25
|
+
const [_runtime, setRuntime] = useState<VisionOsRuntimeInfo | null>(null);
|
|
26
|
+
const [_errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
|
|
31
|
+
async function loadHomeState() {
|
|
32
|
+
try {
|
|
33
|
+
const [userResponse, runtimeResponse] = await Promise.all([
|
|
34
|
+
getCurrentUser().catch(() => ({ user: null })),
|
|
35
|
+
getRuntimeInfo()
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
if (cancelled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const runtime = runtimeResponse.runtime;
|
|
43
|
+
const currentOrigin = window.location.origin.replace(/\/$/, "");
|
|
44
|
+
const mainAppUrl = runtime.mainAppUrl?.replace(/\/$/, "");
|
|
45
|
+
|
|
46
|
+
// If running in dynamic embedded mode and not on the primary web origin, redirect immediately
|
|
47
|
+
if (runtime.mode === "embedded" && mainAppUrl && mainAppUrl !== currentOrigin) {
|
|
48
|
+
const targetUrl = new URL(mainAppUrl);
|
|
49
|
+
targetUrl.pathname = window.location.pathname;
|
|
50
|
+
|
|
51
|
+
const params = new URLSearchParams(window.location.search);
|
|
52
|
+
const storedToken = getStoredAuthToken();
|
|
53
|
+
if (storedToken) {
|
|
54
|
+
params.set("token", storedToken);
|
|
55
|
+
}
|
|
56
|
+
params.set("embeddedApi", runtime.apiBaseUrl);
|
|
57
|
+
targetUrl.search = params.toString();
|
|
58
|
+
|
|
59
|
+
window.location.href = targetUrl.toString();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setUser(userResponse.user);
|
|
64
|
+
setRuntime(runtime);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (cancelled) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (error instanceof VisionOsApiError) {
|
|
71
|
+
setErrorMessage(error.message);
|
|
72
|
+
} else if (error instanceof Error) {
|
|
73
|
+
setErrorMessage(error.message);
|
|
74
|
+
} else {
|
|
75
|
+
setErrorMessage("Unable to load VisionOS runtime details.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
void loadHomeState();
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
cancelled = true;
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
// CLI Auth flow → dedicated auth page
|
|
88
|
+
if (cliAuthSessionId) {
|
|
89
|
+
return <CliAuthPage sessionId={cliAuthSessionId} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (accountMode) {
|
|
93
|
+
return <AccountPage initialMode={accountMode} onAuthenticated={setUser} />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (window.location.pathname === "/creator") {
|
|
97
|
+
return <CreatorPortal user={user} />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Main manifesto landing page
|
|
101
|
+
return <ManifestoPage user={user} />;
|
|
102
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { type AccountAuthMode, type VisionOsUserProfile } from "../../../shared/src/index.js";
|
|
3
|
+
import { AuthPanel } from "./AuthPanel.js";
|
|
4
|
+
|
|
5
|
+
interface AccountPageProps {
|
|
6
|
+
initialMode: AccountAuthMode;
|
|
7
|
+
onAuthenticated: (user: VisionOsUserProfile) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AccountPage({ initialMode, onAuthenticated }: AccountPageProps) {
|
|
11
|
+
const [mode] = useState(initialMode);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="monitor-bezel">
|
|
15
|
+
<div className="monitor-screen" style={{ display: "flex", flexDirection: "column", minHeight: "calc(100vh - 48px)" }}>
|
|
16
|
+
<header className="m-site-header">
|
|
17
|
+
<div className="header-left">
|
|
18
|
+
<span className="header-brand">VisionOS</span>
|
|
19
|
+
<span className="header-meta">Account Access</span>
|
|
20
|
+
</div>
|
|
21
|
+
<nav className="header-nav">
|
|
22
|
+
<a href="/">Home <span className="arrow">↗</span></a>
|
|
23
|
+
<a href="/creator" className="creator-nav-link">Create Lessons <span className="arrow">↗</span></a>
|
|
24
|
+
</nav>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<main style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: "40px 60px" }}>
|
|
28
|
+
<div style={{ display: "grid", gridTemplateColumns: "1.05fr 0.95fr", gap: 48, width: "100%", maxWidth: 1050, alignItems: "center" }}>
|
|
29
|
+
<div>
|
|
30
|
+
<p className="text-label" style={{ fontFamily: "var(--font-mono)" }}>
|
|
31
|
+
account sync
|
|
32
|
+
</p>
|
|
33
|
+
<h1 className="text-section-title" style={{ marginTop: 12 }}>
|
|
34
|
+
Save progress<br />
|
|
35
|
+
<span style={{ fontWeight: 400, color: "var(--ink-muted)" }}>across sessions.</span>
|
|
36
|
+
</h1>
|
|
37
|
+
<p className="m-text-body" style={{ marginTop: 20 }}>
|
|
38
|
+
Create an account or log in to keep your VisionOS profile and learning progress in MongoDB.
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div style={{
|
|
43
|
+
background: "var(--m-white)",
|
|
44
|
+
borderRadius: "var(--radius-lg)",
|
|
45
|
+
padding: 32,
|
|
46
|
+
border: "1px solid rgba(0,0,0,0.06)",
|
|
47
|
+
boxShadow: "0 4px 24px rgba(0,0,0,0.08)",
|
|
48
|
+
position: "relative",
|
|
49
|
+
overflow: "hidden"
|
|
50
|
+
}}>
|
|
51
|
+
<div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 4, background: "var(--accent-teal)" }} />
|
|
52
|
+
<span className="text-label" style={{ fontFamily: "var(--font-mono)", color: "var(--accent-teal)" }}>
|
|
53
|
+
{mode === "register" ? "new account" : "existing account"}
|
|
54
|
+
</span>
|
|
55
|
+
<h2 style={{ marginTop: 8, marginBottom: 24, fontSize: "1.6rem", fontWeight: 800, color: "var(--m-ink)", fontFamily: "var(--font-sans)" }}>
|
|
56
|
+
{mode === "register" ? "Register" : "Login"}
|
|
57
|
+
</h2>
|
|
58
|
+
<AuthPanel
|
|
59
|
+
mode={mode}
|
|
60
|
+
onAuthenticated={(auth) => {
|
|
61
|
+
onAuthenticated({
|
|
62
|
+
...auth.user,
|
|
63
|
+
authenticatedAt: new Date().toISOString(),
|
|
64
|
+
sessionId: "web",
|
|
65
|
+
trackId: "git"
|
|
66
|
+
});
|
|
67
|
+
window.location.href = "/";
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</main>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="bezel-notch">
|
|
75
|
+
<div className="bezel-notch-inner" />
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type FormEvent } from "react";
|
|
2
|
+
import { type AccountAuthMode, type AccountAuthResponse } from "../../../shared/src/index.js";
|
|
3
|
+
import {
|
|
4
|
+
VisionOsApiError,
|
|
5
|
+
completeCliAuthSession,
|
|
6
|
+
completeCliAuthSessionWithGoogle,
|
|
7
|
+
loginAccount,
|
|
8
|
+
loginWithGoogle,
|
|
9
|
+
registerAccount,
|
|
10
|
+
storeAuthToken
|
|
11
|
+
} from "../lib/api.js";
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
google?: {
|
|
16
|
+
accounts: {
|
|
17
|
+
id: {
|
|
18
|
+
initialize(options: {
|
|
19
|
+
client_id: string;
|
|
20
|
+
callback: (response: { credential?: string }) => void;
|
|
21
|
+
}): void;
|
|
22
|
+
renderButton(
|
|
23
|
+
element: HTMLElement,
|
|
24
|
+
options: {
|
|
25
|
+
shape?: string;
|
|
26
|
+
size?: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
theme?: string;
|
|
29
|
+
width?: number;
|
|
30
|
+
logo_alignment?: string;
|
|
31
|
+
}
|
|
32
|
+
): void;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface AuthPanelProps {
|
|
40
|
+
mode?: AccountAuthMode;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
onAuthenticated?: (auth: AccountAuthResponse) => void;
|
|
43
|
+
onCliSessionAuthenticated?: (payload: Awaited<ReturnType<typeof completeCliAuthSession>>) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getErrorMessage(error: unknown) {
|
|
47
|
+
if (error instanceof VisionOsApiError || error instanceof Error) {
|
|
48
|
+
return error.message;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return "Unable to authenticate right now.";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const googleIdentityScriptId = "visionos-google-identity";
|
|
55
|
+
const googleIdentityScriptSrc = "https://accounts.google.com/gsi/client";
|
|
56
|
+
|
|
57
|
+
export function AuthPanel({
|
|
58
|
+
mode = "login",
|
|
59
|
+
sessionId,
|
|
60
|
+
onAuthenticated,
|
|
61
|
+
onCliSessionAuthenticated
|
|
62
|
+
}: AuthPanelProps) {
|
|
63
|
+
const [authMode, setAuthMode] = useState<AccountAuthMode>(mode);
|
|
64
|
+
const [name, setName] = useState("");
|
|
65
|
+
const [email, setEmail] = useState("");
|
|
66
|
+
const [password, setPassword] = useState("");
|
|
67
|
+
const [submitting, setSubmitting] = useState(false);
|
|
68
|
+
const [googleMessage, setGoogleMessage] = useState<string | null>("Loading Google sign-in...");
|
|
69
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
70
|
+
const googleButtonRef = useRef<HTMLDivElement>(null);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
loadGoogleSignIn();
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
const script = document.getElementById(googleIdentityScriptId) as HTMLScriptElement | null;
|
|
77
|
+
|
|
78
|
+
if (script) {
|
|
79
|
+
script.onload = null;
|
|
80
|
+
script.onerror = null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}, [sessionId]);
|
|
84
|
+
|
|
85
|
+
function renderGoogleButton() {
|
|
86
|
+
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID as string | undefined;
|
|
87
|
+
|
|
88
|
+
if (!clientId?.trim()) {
|
|
89
|
+
setGoogleMessage("Google sign-in needs VITE_GOOGLE_CLIENT_ID.");
|
|
90
|
+
setErrorMessage("Google sign-in is not configured. Add VITE_GOOGLE_CLIENT_ID to the client environment.");
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!window.google || !googleButtonRef.current) {
|
|
95
|
+
setGoogleMessage("Google sign-in is still loading.");
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.google.accounts.id.initialize({
|
|
100
|
+
client_id: clientId,
|
|
101
|
+
callback: (response) => {
|
|
102
|
+
if (response.credential) {
|
|
103
|
+
void handleGoogleCredential(response.credential);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
googleButtonRef.current.innerHTML = "";
|
|
108
|
+
window.google.accounts.id.renderButton(googleButtonRef.current, {
|
|
109
|
+
shape: "rectangular",
|
|
110
|
+
size: "large",
|
|
111
|
+
text: "signin_with",
|
|
112
|
+
theme: "outline",
|
|
113
|
+
width: 320,
|
|
114
|
+
logo_alignment: "left"
|
|
115
|
+
});
|
|
116
|
+
setGoogleMessage(null);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadGoogleSignIn() {
|
|
121
|
+
setErrorMessage(null);
|
|
122
|
+
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID as string | undefined;
|
|
123
|
+
|
|
124
|
+
if (!clientId?.trim()) {
|
|
125
|
+
setGoogleMessage("Google sign-in needs VITE_GOOGLE_CLIENT_ID.");
|
|
126
|
+
setErrorMessage("Google sign-in is not configured. Add VITE_GOOGLE_CLIENT_ID to the client environment.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (window.google) {
|
|
131
|
+
renderGoogleButton();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setGoogleMessage("Loading Google sign-in...");
|
|
136
|
+
|
|
137
|
+
const existingScript = document.getElementById(googleIdentityScriptId) as HTMLScriptElement | null;
|
|
138
|
+
const script = existingScript ?? document.createElement("script");
|
|
139
|
+
|
|
140
|
+
if (!existingScript) {
|
|
141
|
+
script.id = googleIdentityScriptId;
|
|
142
|
+
script.src = googleIdentityScriptSrc;
|
|
143
|
+
script.async = true;
|
|
144
|
+
script.defer = true;
|
|
145
|
+
document.head.appendChild(script);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
script.onload = () => {
|
|
149
|
+
renderGoogleButton();
|
|
150
|
+
};
|
|
151
|
+
script.onerror = () => {
|
|
152
|
+
setGoogleMessage("Google sign-in could not load. Try again.");
|
|
153
|
+
setErrorMessage("Google sign-in could not load. Check your network, ad blocker, or Google OAuth configuration.");
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function finishAuth(auth: AccountAuthResponse) {
|
|
158
|
+
storeAuthToken(auth.token);
|
|
159
|
+
onAuthenticated?.(auth);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleGoogleCredential(credential: string) {
|
|
163
|
+
setSubmitting(true);
|
|
164
|
+
setErrorMessage(null);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
if (sessionId) {
|
|
168
|
+
const payload = await completeCliAuthSessionWithGoogle(sessionId, { credential });
|
|
169
|
+
|
|
170
|
+
if (payload.auth) {
|
|
171
|
+
await finishAuth(payload.auth);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onCliSessionAuthenticated?.(payload);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await finishAuth(await loginWithGoogle({ credential }));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
setErrorMessage(getErrorMessage(error));
|
|
181
|
+
} finally {
|
|
182
|
+
setSubmitting(false);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
setSubmitting(true);
|
|
189
|
+
setErrorMessage(null);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
if (sessionId) {
|
|
193
|
+
const payload = await completeCliAuthSession(sessionId, {
|
|
194
|
+
mode: authMode,
|
|
195
|
+
learnerEmail: email,
|
|
196
|
+
learnerName: name,
|
|
197
|
+
password
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (payload.auth) {
|
|
201
|
+
await finishAuth(payload.auth);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
onCliSessionAuthenticated?.(payload);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const auth = authMode === "register"
|
|
209
|
+
? await registerAccount({ email, name, password })
|
|
210
|
+
: await loginAccount({ email, password });
|
|
211
|
+
|
|
212
|
+
await finishAuth(auth);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
setErrorMessage(getErrorMessage(error));
|
|
215
|
+
} finally {
|
|
216
|
+
setSubmitting(false);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const googleMark = (
|
|
221
|
+
<svg className="auth-google-mark" aria-hidden="true" viewBox="0 0 18 18">
|
|
222
|
+
<path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z" />
|
|
223
|
+
<path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.33-1.58-5.04-3.72H.94v2.34A9 9 0 0 0 9 18Z" />
|
|
224
|
+
<path fill="#FBBC05" d="M3.96 10.7A5.41 5.41 0 0 1 3.68 9c0-.59.1-1.16.28-1.7V4.96H.94A9 9 0 0 0 0 9c0 1.45.34 2.82.94 4.04l3.02-2.34Z" />
|
|
225
|
+
<path fill="#EA4335" d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A8.64 8.64 0 0 0 9 0 9 9 0 0 0 .94 4.96L3.96 7.3C4.67 5.16 6.66 3.58 9 3.58Z" />
|
|
226
|
+
</svg>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div className="auth-panel">
|
|
231
|
+
<div className="auth-mode-toggle" role="tablist" aria-label="Authentication mode">
|
|
232
|
+
<button
|
|
233
|
+
className={authMode === "login" ? "active" : ""}
|
|
234
|
+
type="button"
|
|
235
|
+
onClick={() => setAuthMode("login")}
|
|
236
|
+
>
|
|
237
|
+
Login
|
|
238
|
+
</button>
|
|
239
|
+
<button
|
|
240
|
+
className={authMode === "register" ? "active" : ""}
|
|
241
|
+
type="button"
|
|
242
|
+
onClick={() => setAuthMode("register")}
|
|
243
|
+
>
|
|
244
|
+
Register
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div className="auth-google-slot" aria-busy={submitting}>
|
|
249
|
+
<div
|
|
250
|
+
className="auth-google-render-host"
|
|
251
|
+
hidden={Boolean(googleMessage)}
|
|
252
|
+
ref={googleButtonRef}
|
|
253
|
+
/>
|
|
254
|
+
{googleMessage && (
|
|
255
|
+
<button
|
|
256
|
+
className="auth-google-fallback"
|
|
257
|
+
disabled={submitting}
|
|
258
|
+
onClick={loadGoogleSignIn}
|
|
259
|
+
type="button"
|
|
260
|
+
>
|
|
261
|
+
{googleMark}
|
|
262
|
+
<span>Sign in with Google</span>
|
|
263
|
+
</button>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="auth-divider">
|
|
268
|
+
<span />
|
|
269
|
+
<strong>or</strong>
|
|
270
|
+
<span />
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<form className="auth-form" onSubmit={(event) => void handleSubmit(event)}>
|
|
274
|
+
{authMode === "register" && (
|
|
275
|
+
<label>
|
|
276
|
+
<span>Name</span>
|
|
277
|
+
<input
|
|
278
|
+
autoComplete="name"
|
|
279
|
+
onChange={(event) => setName(event.target.value)}
|
|
280
|
+
placeholder="Ada Lovelace"
|
|
281
|
+
value={name}
|
|
282
|
+
/>
|
|
283
|
+
</label>
|
|
284
|
+
)}
|
|
285
|
+
<label>
|
|
286
|
+
<span>Email</span>
|
|
287
|
+
<input
|
|
288
|
+
autoComplete="email"
|
|
289
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
290
|
+
placeholder="ada@example.com"
|
|
291
|
+
type="email"
|
|
292
|
+
value={email}
|
|
293
|
+
/>
|
|
294
|
+
</label>
|
|
295
|
+
<label>
|
|
296
|
+
<span>Password</span>
|
|
297
|
+
<input
|
|
298
|
+
autoComplete={authMode === "register" ? "new-password" : "current-password"}
|
|
299
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
300
|
+
placeholder="Minimum 8 characters"
|
|
301
|
+
type="password"
|
|
302
|
+
value={password}
|
|
303
|
+
/>
|
|
304
|
+
</label>
|
|
305
|
+
{errorMessage && <div className="auth-error">{errorMessage}</div>}
|
|
306
|
+
<button className="auth-submit" disabled={submitting} type="submit">
|
|
307
|
+
{submitting ? "Working..." : authMode === "register" ? "Create account" : "Login"}
|
|
308
|
+
</button>
|
|
309
|
+
</form>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|