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.
Files changed (160) hide show
  1. package/.claude/worktrees/competent-burnell-8d1330/README.md +138 -0
  2. package/.claude/worktrees/competent-burnell-8d1330/cli/package.json +35 -0
  3. package/.claude/worktrees/competent-burnell-8d1330/cli/scripts/copy-web-assets.mjs +12 -0
  4. package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/logout.ts +12 -0
  5. package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/open.ts +19 -0
  6. package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/start.ts +97 -0
  7. package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/status.ts +23 -0
  8. package/.claude/worktrees/competent-burnell-8d1330/cli/src/commands/userinfo.ts +47 -0
  9. package/.claude/worktrees/competent-burnell-8d1330/cli/src/index.ts +23 -0
  10. package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/auth.ts +84 -0
  11. package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/browser.ts +37 -0
  12. package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/localState.ts +80 -0
  13. package/.claude/worktrees/competent-burnell-8d1330/cli/src/lib/runtime.ts +203 -0
  14. package/.claude/worktrees/competent-burnell-8d1330/cli/src/runtime/index.ts +36 -0
  15. package/.claude/worktrees/competent-burnell-8d1330/cli/src/types/inquirer.d.ts +9 -0
  16. package/.claude/worktrees/competent-burnell-8d1330/cli/tsconfig.json +19 -0
  17. package/.claude/worktrees/competent-burnell-8d1330/client/index.html +15 -0
  18. package/.claude/worktrees/competent-burnell-8d1330/client/package.json +27 -0
  19. package/.claude/worktrees/competent-burnell-8d1330/client/postcss.config.cjs +7 -0
  20. package/.claude/worktrees/competent-burnell-8d1330/client/src/App.tsx +57 -0
  21. package/.claude/worktrees/competent-burnell-8d1330/client/src/components/CliAuthPage.tsx +385 -0
  22. package/.claude/worktrees/competent-burnell-8d1330/client/src/components/ManifestoPage.tsx +946 -0
  23. package/.claude/worktrees/competent-burnell-8d1330/client/src/components/TrackCard.tsx +19 -0
  24. package/.claude/worktrees/competent-burnell-8d1330/client/src/lib/api.ts +58 -0
  25. package/.claude/worktrees/competent-burnell-8d1330/client/src/main.tsx +11 -0
  26. package/.claude/worktrees/competent-burnell-8d1330/client/src/styles/index.css +33 -0
  27. package/.claude/worktrees/competent-burnell-8d1330/client/src/styles/manifesto.css +1398 -0
  28. package/.claude/worktrees/competent-burnell-8d1330/client/tailwind.config.ts +36 -0
  29. package/.claude/worktrees/competent-burnell-8d1330/client/tsconfig.json +25 -0
  30. package/.claude/worktrees/competent-burnell-8d1330/client/vite.config.ts +20 -0
  31. package/.claude/worktrees/competent-burnell-8d1330/package-lock.json +5278 -0
  32. package/.claude/worktrees/competent-burnell-8d1330/package.json +24 -0
  33. package/.claude/worktrees/competent-burnell-8d1330/server/package.json +25 -0
  34. package/.claude/worktrees/competent-burnell-8d1330/server/src/app.ts +71 -0
  35. package/.claude/worktrees/competent-burnell-8d1330/server/src/config/env.ts +14 -0
  36. package/.claude/worktrees/competent-burnell-8d1330/server/src/features/auth/sessionStore.ts +74 -0
  37. package/.claude/worktrees/competent-burnell-8d1330/server/src/index.ts +8 -0
  38. package/.claude/worktrees/competent-burnell-8d1330/server/src/routes/auth.ts +112 -0
  39. package/.claude/worktrees/competent-burnell-8d1330/server/src/routes/health.ts +14 -0
  40. package/.claude/worktrees/competent-burnell-8d1330/server/tsconfig.json +19 -0
  41. package/.claude/worktrees/competent-burnell-8d1330/shared/package.json +24 -0
  42. package/.claude/worktrees/competent-burnell-8d1330/shared/src/index.ts +91 -0
  43. package/.claude/worktrees/competent-burnell-8d1330/shared/tsconfig.json +16 -0
  44. package/.claude/worktrees/competent-burnell-8d1330/tsconfig.base.json +12 -0
  45. package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/index.html +392 -0
  46. package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/script.js +146 -0
  47. package/.claude/worktrees/competent-burnell-8d1330/visionos-manifesto/styles.css +1082 -0
  48. package/.claude/worktrees/vigilant-napier-0de76f/README.md +138 -0
  49. package/.claude/worktrees/vigilant-napier-0de76f/cli/package.json +35 -0
  50. package/.claude/worktrees/vigilant-napier-0de76f/cli/scripts/copy-web-assets.mjs +12 -0
  51. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/logout.ts +12 -0
  52. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/open.ts +19 -0
  53. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/start.ts +97 -0
  54. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/status.ts +23 -0
  55. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/commands/userinfo.ts +47 -0
  56. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/index.ts +23 -0
  57. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/auth.ts +84 -0
  58. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/browser.ts +37 -0
  59. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/localState.ts +80 -0
  60. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/lib/runtime.ts +203 -0
  61. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/runtime/index.ts +36 -0
  62. package/.claude/worktrees/vigilant-napier-0de76f/cli/src/types/inquirer.d.ts +9 -0
  63. package/.claude/worktrees/vigilant-napier-0de76f/cli/tsconfig.json +19 -0
  64. package/.claude/worktrees/vigilant-napier-0de76f/client/index.html +15 -0
  65. package/.claude/worktrees/vigilant-napier-0de76f/client/package.json +27 -0
  66. package/.claude/worktrees/vigilant-napier-0de76f/client/postcss.config.cjs +7 -0
  67. package/.claude/worktrees/vigilant-napier-0de76f/client/src/App.tsx +57 -0
  68. package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/CliAuthPage.tsx +385 -0
  69. package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/ManifestoPage.tsx +946 -0
  70. package/.claude/worktrees/vigilant-napier-0de76f/client/src/components/TrackCard.tsx +19 -0
  71. package/.claude/worktrees/vigilant-napier-0de76f/client/src/lib/api.ts +58 -0
  72. package/.claude/worktrees/vigilant-napier-0de76f/client/src/main.tsx +11 -0
  73. package/.claude/worktrees/vigilant-napier-0de76f/client/src/styles/index.css +33 -0
  74. package/.claude/worktrees/vigilant-napier-0de76f/client/src/styles/manifesto.css +1398 -0
  75. package/.claude/worktrees/vigilant-napier-0de76f/client/tailwind.config.ts +36 -0
  76. package/.claude/worktrees/vigilant-napier-0de76f/client/tsconfig.json +25 -0
  77. package/.claude/worktrees/vigilant-napier-0de76f/client/vite.config.ts +20 -0
  78. package/.claude/worktrees/vigilant-napier-0de76f/package-lock.json +5278 -0
  79. package/.claude/worktrees/vigilant-napier-0de76f/package.json +24 -0
  80. package/.claude/worktrees/vigilant-napier-0de76f/server/package.json +25 -0
  81. package/.claude/worktrees/vigilant-napier-0de76f/server/src/app.ts +71 -0
  82. package/.claude/worktrees/vigilant-napier-0de76f/server/src/config/env.ts +14 -0
  83. package/.claude/worktrees/vigilant-napier-0de76f/server/src/features/auth/sessionStore.ts +74 -0
  84. package/.claude/worktrees/vigilant-napier-0de76f/server/src/index.ts +8 -0
  85. package/.claude/worktrees/vigilant-napier-0de76f/server/src/routes/auth.ts +112 -0
  86. package/.claude/worktrees/vigilant-napier-0de76f/server/src/routes/health.ts +14 -0
  87. package/.claude/worktrees/vigilant-napier-0de76f/server/tsconfig.json +19 -0
  88. package/.claude/worktrees/vigilant-napier-0de76f/shared/package.json +24 -0
  89. package/.claude/worktrees/vigilant-napier-0de76f/shared/src/index.ts +91 -0
  90. package/.claude/worktrees/vigilant-napier-0de76f/shared/tsconfig.json +16 -0
  91. package/.claude/worktrees/vigilant-napier-0de76f/tsconfig.base.json +12 -0
  92. package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/index.html +392 -0
  93. package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/script.js +146 -0
  94. package/.claude/worktrees/vigilant-napier-0de76f/visionos-manifesto/styles.css +1082 -0
  95. package/.github/workflows/publish.yml +30 -0
  96. package/README.md +175 -0
  97. package/cli/README.md +165 -0
  98. package/cli/package.json +36 -0
  99. package/cli/scripts/copy-web-assets.mjs +12 -0
  100. package/cli/src/commands/lessons.ts +68 -0
  101. package/cli/src/commands/login.ts +46 -0
  102. package/cli/src/commands/logout.ts +12 -0
  103. package/cli/src/commands/open.ts +29 -0
  104. package/cli/src/commands/start.ts +146 -0
  105. package/cli/src/commands/status.ts +28 -0
  106. package/cli/src/commands/userinfo.ts +59 -0
  107. package/cli/src/index.ts +109 -0
  108. package/cli/src/lib/auth.ts +84 -0
  109. package/cli/src/lib/browser.ts +37 -0
  110. package/cli/src/lib/content.ts +57 -0
  111. package/cli/src/lib/lessonPrinter.ts +38 -0
  112. package/cli/src/lib/lessonRunner.ts +381 -0
  113. package/cli/src/lib/localState.ts +114 -0
  114. package/cli/src/lib/loginFlow.ts +74 -0
  115. package/cli/src/lib/progress.ts +94 -0
  116. package/cli/src/lib/runtime.ts +220 -0
  117. package/cli/src/lib/validator.ts +401 -0
  118. package/cli/src/runtime/index.ts +108 -0
  119. package/cli/src/types/inquirer.d.ts +9 -0
  120. package/cli/tsconfig.json +19 -0
  121. package/client/index.html +15 -0
  122. package/client/package.json +27 -0
  123. package/client/postcss.config.cjs +7 -0
  124. package/client/src/App.tsx +102 -0
  125. package/client/src/components/AccountPage.tsx +79 -0
  126. package/client/src/components/AuthPanel.tsx +312 -0
  127. package/client/src/components/CliAuthPage.tsx +367 -0
  128. package/client/src/components/CreatorPortal.tsx +885 -0
  129. package/client/src/components/ErrorBoundary.tsx +92 -0
  130. package/client/src/components/ManifestoPage.tsx +1126 -0
  131. package/client/src/components/TrackCard.tsx +19 -0
  132. package/client/src/lib/api.ts +215 -0
  133. package/client/src/main.tsx +14 -0
  134. package/client/src/styles/index.css +33 -0
  135. package/client/src/styles/manifesto.css +1828 -0
  136. package/client/tailwind.config.ts +36 -0
  137. package/client/tsconfig.json +25 -0
  138. package/client/vercel.json +8 -0
  139. package/client/vite.config.ts +33 -0
  140. package/package.json +27 -0
  141. package/server/package.json +26 -0
  142. package/server/src/app.ts +132 -0
  143. package/server/src/config/env.ts +135 -0
  144. package/server/src/features/accounts/accountStore.ts +359 -0
  145. package/server/src/features/accounts/contentStore.ts +264 -0
  146. package/server/src/features/accounts/password.ts +26 -0
  147. package/server/src/features/auth/sessionStore.ts +79 -0
  148. package/server/src/index.ts +8 -0
  149. package/server/src/routes/auth.ts +328 -0
  150. package/server/src/routes/content.ts +174 -0
  151. package/server/src/routes/health.ts +14 -0
  152. package/server/src/routes/progress.ts +105 -0
  153. package/server/tsconfig.json +19 -0
  154. package/shared/package.json +24 -0
  155. package/shared/src/index.ts +455 -0
  156. package/shared/tsconfig.json +16 -0
  157. package/tsconfig.base.json +12 -0
  158. package/visionos-manifesto/index.html +392 -0
  159. package/visionos-manifesto/script.js +146 -0
  160. package/visionos-manifesto/styles.css +1082 -0
@@ -0,0 +1,138 @@
1
+ # VisionOS
2
+
3
+ VisionOS is a CLI-first learning platform for command-line practice across tracks such as Git, Linux, React CLI, and OS commands.
4
+
5
+ The current build includes:
6
+
7
+ - An installable `visionos-cli` package with a `visionos` binary
8
+ - An embedded local runtime that serves the web UI and API automatically
9
+ - Browser-based CLI authentication handoff
10
+ - Local user/session persistence in `~/.visionos/`
11
+ - Commands for `start`, `open`, `userinfo`, `status`, and `logout`
12
+ - A monorepo dev setup for separate server/client development
13
+
14
+ ## Monorepo Layout
15
+
16
+ ```text
17
+ visionos/
18
+ ├── brain/
19
+ ├── cli/
20
+ ├── client/
21
+ ├── server/
22
+ ├── shared/
23
+ └── README.md
24
+ ```
25
+
26
+ ## Install The CLI
27
+
28
+ The npm package name is `visionos-cli`, but the command you run is `visionos`.
29
+
30
+ ### From npm
31
+
32
+ Once the package is published, install it globally with:
33
+
34
+ ```bash
35
+ npm install -g visionos-cli
36
+ ```
37
+
38
+ Then run:
39
+
40
+ ```bash
41
+ visionos start
42
+ ```
43
+
44
+ ### From this repository
45
+
46
+ This repo is not published automatically. To build the installable tarball locally:
47
+
48
+ ```bash
49
+ npm install
50
+ npm run pack:cli
51
+ npm install -g ./visionos-cli-0.1.1.tgz
52
+ ```
53
+
54
+ After that, the CLI works the same way:
55
+
56
+ ```bash
57
+ visionos start
58
+ ```
59
+
60
+ You do not need `npm run dev:cli -- start` for installed usage.
61
+
62
+ ## CLI Commands
63
+
64
+ - `visionos start`
65
+ Starts the local VisionOS runtime if needed, opens the browser auth flow, and waits for the session to complete.
66
+
67
+ - `visionos open`
68
+ Opens the local VisionOS web interface without restarting the monorepo dev servers.
69
+
70
+ - `visionos userinfo`
71
+ Shows the locally stored learner profile from `~/.visionos/user.json`.
72
+
73
+ - `visionos userinfo --json`
74
+ Prints the stored learner profile, runtime info, and state file paths as JSON.
75
+
76
+ - `visionos status`
77
+ Shows whether the embedded runtime is running and whether a learner profile is stored.
78
+
79
+ - `visionos logout`
80
+ Removes the locally stored learner profile.
81
+
82
+ ## Installed Runtime Behavior
83
+
84
+ - `visionos start` boots a local embedded runtime on `127.0.0.1` using an available port.
85
+ - The runtime serves both the React frontend and the API from the same origin.
86
+ - The browser auth page writes the authenticated learner profile to `~/.visionos/user.json`.
87
+ - Runtime metadata is stored in `~/.visionos/runtime.json`.
88
+
89
+ ## Development Workflow
90
+
91
+ If you want to work on the monorepo directly instead of using the installed package:
92
+
93
+ ```bash
94
+ npm install
95
+ npm run dev
96
+ ```
97
+
98
+ That starts:
99
+
100
+ - React on `http://localhost:5173`
101
+ - Express on `http://127.0.0.1:4000`
102
+
103
+ You can still run the CLI in dev mode with:
104
+
105
+ ```bash
106
+ npm run dev:cli -- start
107
+ ```
108
+
109
+ The backend API stays available at `http://127.0.0.1:4000`.
110
+
111
+ ## Workspace Scripts
112
+
113
+ - `npm run build` builds every workspace in dependency order, including the packaged CLI runtime assets
114
+ - `npm run pack:cli` builds the repo and creates `visionos-cli-0.1.1.tgz`
115
+ - `npm run dev` starts the Express API and React frontend together
116
+ - `npm run dev:cli` starts the CLI in development mode
117
+ - `npm run dev:server` starts the Express API with file watching
118
+ - `npm run dev:client` starts the React frontend
119
+
120
+ ## Current Web Surface
121
+
122
+ - `/` serves the VisionOS homepage and runtime dashboard
123
+ - `/?cliAuthSession=...` serves the browser auth confirmation page
124
+ - `/health` returns API health JSON
125
+ - `/auth/me` returns the stored learner profile for the current machine
126
+ - `/runtime/info` returns the current app/API origin information
127
+
128
+ ## Current Limitations
129
+
130
+ - The lesson execution engine is not implemented yet
131
+ - Progress sync/resume is not implemented yet
132
+ - The package is ready to publish, but publishing to npm has not been done from this repository
133
+
134
+ ## Next Milestones
135
+
136
+ - Command execution and validation engine
137
+ - Local progress persistence and backend sync
138
+ - Resume support across CLI and web experiences
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "visionos-cli",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "visionos": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18.18.0"
13
+ },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts src/runtime/index.ts --format esm --clean --out-dir dist && node ./scripts/copy-web-assets.mjs",
16
+ "dev": "tsx src/index.ts",
17
+ "lint": "tsc --noEmit -p tsconfig.json"
18
+ },
19
+ "dependencies": {
20
+ "chalk": "^5.3.0",
21
+ "commander": "^12.1.0",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^16.4.5",
24
+ "express": "^4.21.1",
25
+ "inquirer": "^9.3.7"
26
+ },
27
+ "devDependencies": {
28
+ "@types/cors": "^2.8.17",
29
+ "@types/express": "^4.17.21",
30
+ "@types/node": "^22.10.1",
31
+ "tsup": "^8.3.0",
32
+ "tsx": "^4.19.1",
33
+ "typescript": "^5.6.3"
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
6
+ const cliDir = path.resolve(currentDir, "..");
7
+ const sourceDir = path.resolve(cliDir, "../client/dist");
8
+ const targetDir = path.resolve(cliDir, "./dist/web");
9
+
10
+ await fs.rm(targetDir, { force: true, recursive: true });
11
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
12
+ await fs.cp(sourceDir, targetDir, { recursive: true });
@@ -0,0 +1,12 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { clearUserProfile } from "../lib/localState.js";
4
+
5
+ export function createLogoutCommand() {
6
+ return new Command("logout")
7
+ .description("Remove the locally stored VisionOS learner profile.")
8
+ .action(async () => {
9
+ await clearUserProfile();
10
+ console.log(chalk.yellow("Cleared the local VisionOS learner profile."));
11
+ });
12
+ }
@@ -0,0 +1,19 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { openBrowser } from "../lib/browser.js";
4
+ import { ensureVisionOsRuntime } from "../lib/runtime.js";
5
+
6
+ export function createOpenCommand() {
7
+ return new Command("open")
8
+ .description("Open the VisionOS web interface.")
9
+ .action(async () => {
10
+ const runtime = await ensureVisionOsRuntime();
11
+ const browserOpened = await openBrowser(runtime.appUrl);
12
+
13
+ console.log(chalk.cyan(`VisionOS web interface: ${runtime.appUrl}`));
14
+
15
+ if (!browserOpened) {
16
+ console.log(chalk.yellow("Open the URL above manually if your browser did not launch."));
17
+ }
18
+ });
19
+ }
@@ -0,0 +1,97 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import inquirer from "inquirer";
4
+ import { APP_NAME, LEARNING_TRACKS, type LearningTrackId } from "../../../shared/src/index.js";
5
+ import { VisionOsApiError, createCliAuthSession, waitForCliAuthCompletion } from "../lib/auth.js";
6
+ import { openBrowser } from "../lib/browser.js";
7
+ import { ensureVisionOsRuntime } from "../lib/runtime.js";
8
+
9
+ export function createStartCommand() {
10
+ return new Command("start")
11
+ .description("Start an interactive VisionOS learning session.")
12
+ .action(async () => {
13
+ console.log(chalk.cyan.bold(`\n${APP_NAME}`));
14
+ console.log(
15
+ chalk.gray(
16
+ "Choose a track, complete browser sign-in, and return to the CLI for the guided lesson flow.\n"
17
+ )
18
+ );
19
+
20
+ const { trackId } = await inquirer.prompt<{ trackId: LearningTrackId }>([
21
+ {
22
+ type: "list",
23
+ name: "trackId",
24
+ message: "Choose a learning track to begin:",
25
+ choices: LEARNING_TRACKS.map((track) => ({
26
+ name: `${track.label} - ${track.description}`,
27
+ value: track.id
28
+ }))
29
+ }
30
+ ]);
31
+
32
+ const selectedTrack = LEARNING_TRACKS.find((track) => track.id === trackId);
33
+
34
+ console.log(chalk.green(`\nSelected track: ${selectedTrack?.label ?? trackId}`));
35
+
36
+ try {
37
+ const runtime = await ensureVisionOsRuntime();
38
+ const authSession = await createCliAuthSession(runtime.apiBaseUrl, trackId);
39
+
40
+ console.log(chalk.white(`VisionOS runtime ready at ${runtime.appUrl}`));
41
+ console.log(chalk.white("Starting browser authentication..."));
42
+
43
+ const browserOpened = await openBrowser(authSession.browserUrl);
44
+
45
+ if (browserOpened) {
46
+ console.log(chalk.gray("A browser window was opened for sign-in."));
47
+ } else {
48
+ console.log(chalk.yellow("Open this URL to continue authentication:"));
49
+ console.log(chalk.white(authSession.browserUrl));
50
+ }
51
+
52
+ console.log(chalk.gray("Waiting for browser confirmation...\n"));
53
+
54
+ const completedSession = await waitForCliAuthCompletion(
55
+ runtime.apiBaseUrl,
56
+ authSession.session.sessionId,
57
+ authSession.pollIntervalMs
58
+ );
59
+
60
+ if (completedSession.status !== "authenticated" || !completedSession.learner) {
61
+ console.log(
62
+ chalk.red(
63
+ "The authentication session expired before it was completed. Run `visionos start` again."
64
+ )
65
+ );
66
+ return;
67
+ }
68
+
69
+ console.log(
70
+ chalk.green(
71
+ `Authenticated as ${completedSession.learner.name} (${completedSession.learner.email}).`
72
+ )
73
+ );
74
+ console.log(
75
+ chalk.white(
76
+ `Browser handoff completed for the ${selectedTrack?.label ?? completedSession.trackId} track.`
77
+ )
78
+ );
79
+ console.log(
80
+ chalk.yellow("Next milestone: command execution, validation, and persistent progress.\n")
81
+ );
82
+ } catch (error) {
83
+ if (error instanceof VisionOsApiError) {
84
+ console.error(
85
+ chalk.red("Unable to start browser authentication through the VisionOS runtime.")
86
+ );
87
+ console.error(chalk.gray(error.message));
88
+ } else if (error instanceof Error) {
89
+ console.error(chalk.red(error.message));
90
+ } else {
91
+ console.error(chalk.red("An unexpected error interrupted browser authentication."));
92
+ }
93
+
94
+ process.exitCode = 1;
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,23 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { readUserProfile } from "../lib/localState.js";
4
+ import { getVisionOsRuntimeStatus } from "../lib/runtime.js";
5
+
6
+ export function createStatusCommand() {
7
+ return new Command("status")
8
+ .description("Show the VisionOS runtime and local auth status.")
9
+ .action(async () => {
10
+ const [user, runtimeStatus] = await Promise.all([readUserProfile(), getVisionOsRuntimeStatus()]);
11
+
12
+ console.log(chalk.cyan.bold("VisionOS Status"));
13
+ console.log(`${chalk.white("Runtime:")} ${runtimeStatus.isRunning ? "running" : "stopped"}`);
14
+
15
+ if (runtimeStatus.runtime) {
16
+ console.log(`${chalk.white("App URL:")} ${runtimeStatus.runtime.appUrl}`);
17
+ console.log(`${chalk.white("API URL:")} ${runtimeStatus.runtime.apiBaseUrl}`);
18
+ console.log(`${chalk.white("PID:")} ${runtimeStatus.runtime.pid}`);
19
+ }
20
+
21
+ console.log(`${chalk.white("User:")} ${user ? `${user.name} <${user.email}>` : "not signed in"}`);
22
+ });
23
+ }
@@ -0,0 +1,47 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { LEARNING_TRACKS } from "../../../shared/src/index.js";
4
+ import { getVisionOsStatePaths, readUserProfile } from "../lib/localState.js";
5
+ import { getVisionOsRuntimeStatus } from "../lib/runtime.js";
6
+
7
+ export function createUserInfoCommand() {
8
+ return new Command("userinfo")
9
+ .description("Show the locally stored VisionOS learner profile.")
10
+ .option("--json", "Print the stored profile as JSON.")
11
+ .action(async ({ json }: { json?: boolean }) => {
12
+ const [user, runtimeStatus] = await Promise.all([readUserProfile(), getVisionOsRuntimeStatus()]);
13
+
14
+ if (json) {
15
+ console.log(
16
+ JSON.stringify(
17
+ {
18
+ runtime: runtimeStatus.runtime,
19
+ statePaths: getVisionOsStatePaths(),
20
+ user
21
+ },
22
+ null,
23
+ 2
24
+ )
25
+ );
26
+ return;
27
+ }
28
+
29
+ if (!user) {
30
+ console.log(chalk.yellow("No VisionOS learner profile is stored locally yet."));
31
+ console.log(chalk.gray("Run `visionos start` to authenticate and create one."));
32
+ return;
33
+ }
34
+
35
+ const track = LEARNING_TRACKS.find((item) => item.id === user.trackId);
36
+
37
+ console.log(chalk.cyan.bold("VisionOS User"));
38
+ console.log(`${chalk.white("Name:")} ${user.name}`);
39
+ console.log(`${chalk.white("Email:")} ${user.email}`);
40
+ console.log(`${chalk.white("Last Track:")} ${track?.label ?? user.trackId}`);
41
+ console.log(`${chalk.white("Authenticated:")} ${user.authenticatedAt}`);
42
+ console.log(
43
+ `${chalk.white("Runtime:")} ${runtimeStatus.runtime?.appUrl ?? "not running"}`
44
+ );
45
+ console.log(`${chalk.white("State Dir:")} ${getVisionOsStatePaths().visionOsHomeDir}`);
46
+ });
47
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { createLogoutCommand } from "./commands/logout.js";
5
+ import { createOpenCommand } from "./commands/open.js";
6
+ import { createStatusCommand } from "./commands/status.js";
7
+ import { createStartCommand } from "./commands/start.js";
8
+ import { createUserInfoCommand } from "./commands/userinfo.js";
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name("visionos")
14
+ .description("Interactive CLI for VisionOS learning tracks.")
15
+ .version("0.1.0");
16
+
17
+ program.addCommand(createStartCommand());
18
+ program.addCommand(createOpenCommand());
19
+ program.addCommand(createUserInfoCommand());
20
+ program.addCommand(createStatusCommand());
21
+ program.addCommand(createLogoutCommand());
22
+
23
+ void program.parseAsync(process.argv);
@@ -0,0 +1,84 @@
1
+ import {
2
+ type CliAuthSession,
3
+ type CliAuthSessionEnvelope,
4
+ type CreateCliAuthSessionResponse,
5
+ type LearningTrackId,
6
+ type VisionOsRuntimeInfoResponse,
7
+ type VisionOsUserInfoResponse
8
+ } from "../../../shared/src/index.js";
9
+
10
+ export class VisionOsApiError extends Error {
11
+ constructor(
12
+ message: string,
13
+ readonly status: number
14
+ ) {
15
+ super(message);
16
+ this.name = "VisionOsApiError";
17
+ }
18
+ }
19
+
20
+ async function parseResponse<T>(response: Response): Promise<T> {
21
+ const payload = (await response.json().catch(() => null)) as { message?: string } | null;
22
+
23
+ if (!response.ok) {
24
+ throw new VisionOsApiError(
25
+ payload?.message ?? `VisionOS API request failed with status ${response.status}.`,
26
+ response.status
27
+ );
28
+ }
29
+
30
+ return payload as T;
31
+ }
32
+
33
+ function sleep(ms: number) {
34
+ return new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
36
+
37
+ async function request<T>(apiBaseUrl: string, path: string, init?: RequestInit): Promise<T> {
38
+ const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}${path}`, init);
39
+
40
+ return parseResponse<T>(response);
41
+ }
42
+
43
+ export async function createCliAuthSession(
44
+ apiBaseUrl: string,
45
+ trackId: LearningTrackId
46
+ ): Promise<CreateCliAuthSessionResponse> {
47
+ return request<CreateCliAuthSessionResponse>(apiBaseUrl, "/auth/cli/sessions", {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json"
51
+ },
52
+ body: JSON.stringify({ trackId })
53
+ });
54
+ }
55
+
56
+ export async function fetchCliAuthSession(apiBaseUrl: string, sessionId: string): Promise<CliAuthSession> {
57
+ const payload = await request<CliAuthSessionEnvelope>(apiBaseUrl, `/auth/cli/sessions/${sessionId}`);
58
+
59
+ return payload.session;
60
+ }
61
+
62
+ export async function waitForCliAuthCompletion(
63
+ apiBaseUrl: string,
64
+ sessionId: string,
65
+ pollIntervalMs: number
66
+ ): Promise<CliAuthSession> {
67
+ while (true) {
68
+ const session = await fetchCliAuthSession(apiBaseUrl, sessionId);
69
+
70
+ if (session.status !== "pending") {
71
+ return session;
72
+ }
73
+
74
+ await sleep(pollIntervalMs);
75
+ }
76
+ }
77
+
78
+ export function fetchCurrentUser(apiBaseUrl: string) {
79
+ return request<VisionOsUserInfoResponse>(apiBaseUrl, "/auth/me");
80
+ }
81
+
82
+ export function fetchRuntimeInfo(apiBaseUrl: string) {
83
+ return request<VisionOsRuntimeInfoResponse>(apiBaseUrl, "/runtime/info");
84
+ }
@@ -0,0 +1,37 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ function getBrowserCommand(url: string): { command: string; args: string[] } {
4
+ if (process.platform === "darwin") {
5
+ return { command: "open", args: [url] };
6
+ }
7
+
8
+ if (process.platform === "win32") {
9
+ return { command: "cmd", args: ["/c", "start", "", url] };
10
+ }
11
+
12
+ return { command: "xdg-open", args: [url] };
13
+ }
14
+
15
+ export function openBrowser(url: string): Promise<boolean> {
16
+ const { command, args } = getBrowserCommand(url);
17
+
18
+ return new Promise((resolve) => {
19
+ try {
20
+ const child = spawn(command, args, {
21
+ detached: true,
22
+ stdio: "ignore"
23
+ });
24
+
25
+ child.once("error", () => {
26
+ resolve(false);
27
+ });
28
+
29
+ child.once("spawn", () => {
30
+ child.unref();
31
+ resolve(true);
32
+ });
33
+ } catch {
34
+ resolve(false);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,80 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { type VisionOsRuntimeInfo, type VisionOsUserProfile } from "../../../shared/src/index.js";
5
+
6
+ export interface VisionOsRuntimeState extends VisionOsRuntimeInfo {
7
+ host: string;
8
+ pid: number;
9
+ port: number;
10
+ startedAt: string;
11
+ }
12
+
13
+ const visionOsHomeDir = path.join(os.homedir(), ".visionos");
14
+ const runtimeStatePath = path.join(visionOsHomeDir, "runtime.json");
15
+ const userStatePath = path.join(visionOsHomeDir, "user.json");
16
+
17
+ async function ensureVisionOsHomeDir() {
18
+ await fs.mkdir(visionOsHomeDir, { recursive: true });
19
+ }
20
+
21
+ async function readJsonFile<T>(filePath: string): Promise<T | null> {
22
+ try {
23
+ const contents = await fs.readFile(filePath, "utf8");
24
+
25
+ return JSON.parse(contents) as T;
26
+ } catch (error) {
27
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
28
+ return null;
29
+ }
30
+
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ async function writeJsonFile(filePath: string, payload: unknown) {
36
+ await ensureVisionOsHomeDir();
37
+ await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
38
+ }
39
+
40
+ async function removeFileIfPresent(filePath: string) {
41
+ try {
42
+ await fs.unlink(filePath);
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+
50
+ export function getVisionOsStatePaths() {
51
+ return {
52
+ runtimeStatePath,
53
+ userStatePath,
54
+ visionOsHomeDir
55
+ };
56
+ }
57
+
58
+ export function readUserProfile() {
59
+ return readJsonFile<VisionOsUserProfile>(userStatePath);
60
+ }
61
+
62
+ export function writeUserProfile(profile: VisionOsUserProfile) {
63
+ return writeJsonFile(userStatePath, profile);
64
+ }
65
+
66
+ export function clearUserProfile() {
67
+ return removeFileIfPresent(userStatePath);
68
+ }
69
+
70
+ export function readRuntimeState() {
71
+ return readJsonFile<VisionOsRuntimeState>(runtimeStatePath);
72
+ }
73
+
74
+ export function writeRuntimeState(state: VisionOsRuntimeState) {
75
+ return writeJsonFile(runtimeStatePath, state);
76
+ }
77
+
78
+ export function clearRuntimeState() {
79
+ return removeFileIfPresent(runtimeStatePath);
80
+ }