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,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
|
+
}
|