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,146 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { APP_NAME, type LearningTrackId } from "../../../shared/src/index.js";
|
|
5
|
+
import { fetchRemoteTracks, fetchRemoteLessons } from "../lib/content.js";
|
|
6
|
+
import { VisionOsApiError } from "../lib/auth.js";
|
|
7
|
+
import { printLessonPreview } from "../lib/lessonPrinter.js";
|
|
8
|
+
import { runBrowserLogin } from "../lib/loginFlow.js";
|
|
9
|
+
import { readAccountState, readUserProfile } from "../lib/localState.js";
|
|
10
|
+
import { getRemoteProgress } from "../lib/progress.js";
|
|
11
|
+
import { runLessonInteractive } from "../lib/lessonRunner.js";
|
|
12
|
+
import { ensureVisionOsRuntime } from "../lib/runtime.js";
|
|
13
|
+
|
|
14
|
+
export function createStartCommand() {
|
|
15
|
+
return new Command("start")
|
|
16
|
+
.description("Start an interactive VisionOS learning session.")
|
|
17
|
+
.action(async () => {
|
|
18
|
+
console.log(chalk.cyan.bold(`\n${APP_NAME}`));
|
|
19
|
+
console.log(
|
|
20
|
+
chalk.gray(
|
|
21
|
+
"Complete browser sign-in first, then choose a track and return to the CLI for the guided lesson flow.\n"
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const [account, user] = await Promise.all([readAccountState(), readUserProfile()]);
|
|
27
|
+
const runtime = await ensureVisionOsRuntime();
|
|
28
|
+
|
|
29
|
+
let apiBaseUrl = runtime.apiBaseUrl;
|
|
30
|
+
let token = account?.token;
|
|
31
|
+
let loggedInUser = user;
|
|
32
|
+
|
|
33
|
+
if (!token || !loggedInUser) {
|
|
34
|
+
console.log(chalk.yellow("You need to log in to your VisionOS account to start."));
|
|
35
|
+
// Use "git" as the default track to initiate the browser login session
|
|
36
|
+
const loginSession = await runBrowserLogin("git");
|
|
37
|
+
|
|
38
|
+
if (!loginSession || !loginSession.accountToken || !loginSession.learner) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
token = loginSession.accountToken;
|
|
42
|
+
loggedInUser = {
|
|
43
|
+
userId: loginSession.learner.userId,
|
|
44
|
+
name: loginSession.learner.name,
|
|
45
|
+
email: loginSession.learner.email,
|
|
46
|
+
sessionId: loginSession.sessionId,
|
|
47
|
+
trackId: loginSession.trackId,
|
|
48
|
+
authenticatedAt: loginSession.authenticatedAt
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.green(`Signed in as ${loggedInUser.name} (${loggedInUser.email}).`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tracks = await fetchRemoteTracks(apiBaseUrl);
|
|
55
|
+
|
|
56
|
+
const { trackId } = await inquirer.prompt<{ trackId: LearningTrackId }>([
|
|
57
|
+
{
|
|
58
|
+
type: "list",
|
|
59
|
+
name: "trackId",
|
|
60
|
+
message: "Choose a learning track to begin:",
|
|
61
|
+
choices: tracks.map((track) => ({
|
|
62
|
+
name: `${track.label} - ${track.description}`,
|
|
63
|
+
value: track.id
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const selectedTrack = tracks.find((track) => track.id === trackId);
|
|
69
|
+
|
|
70
|
+
console.log(chalk.green(`\nSelected track: ${selectedTrack?.label ?? trackId}`));
|
|
71
|
+
|
|
72
|
+
// Fetch lessons for this track dynamically
|
|
73
|
+
const lessons = await fetchRemoteLessons(apiBaseUrl, trackId);
|
|
74
|
+
|
|
75
|
+
// Fetch remote progress for this user
|
|
76
|
+
let lesson = null;
|
|
77
|
+
const progressRes = await getRemoteProgress(apiBaseUrl);
|
|
78
|
+
const trackProgress = progressRes?.progress?.find((p) => p.trackId === trackId);
|
|
79
|
+
|
|
80
|
+
if (trackProgress?.currentLessonId) {
|
|
81
|
+
const lastLesson = lessons.find((l) => l.id === trackProgress.currentLessonId);
|
|
82
|
+
if (lastLesson) {
|
|
83
|
+
const { action } = await inquirer.prompt<{ action: "resume" | "beginning" | "catalog" }>([
|
|
84
|
+
{
|
|
85
|
+
type: "list",
|
|
86
|
+
name: "action",
|
|
87
|
+
message: `We found existing progress for this track. Last lesson: "${lastLesson.title}". What would you like to do?`,
|
|
88
|
+
choices: [
|
|
89
|
+
{ name: `Resume last lesson (${lastLesson.title})`, value: "resume" },
|
|
90
|
+
{ name: "Start from beginning (first lesson)", value: "beginning" },
|
|
91
|
+
{ name: "Choose a new lesson (browse track catalog)", value: "catalog" }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
if (action === "resume") {
|
|
97
|
+
lesson = lastLesson;
|
|
98
|
+
} else if (action === "beginning") {
|
|
99
|
+
lesson = lessons[0] ?? null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!lesson) {
|
|
105
|
+
if (lessons.length === 0) {
|
|
106
|
+
console.log(chalk.red("No lessons are available for this track yet."));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const { lessonId } = await inquirer.prompt<{ lessonId: string }>([
|
|
110
|
+
{
|
|
111
|
+
type: "list",
|
|
112
|
+
name: "lessonId",
|
|
113
|
+
message: "Choose a beginner lesson:",
|
|
114
|
+
choices: lessons.map((l) => ({
|
|
115
|
+
name: `${l.title} - ${l.summary}`,
|
|
116
|
+
value: l.id
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
]);
|
|
120
|
+
lesson = lessons.find((l) => l.id === lessonId) ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!lesson) {
|
|
124
|
+
console.log(chalk.red("No lesson was chosen. Exiting."));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Launch the interactive sandbox lesson runner
|
|
129
|
+
await runLessonInteractive(lesson, apiBaseUrl);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
|
|
132
|
+
if (error instanceof VisionOsApiError) {
|
|
133
|
+
console.error(
|
|
134
|
+
chalk.red("Unable to start VisionOS learning through the runtime.")
|
|
135
|
+
);
|
|
136
|
+
console.error(chalk.gray(error.message));
|
|
137
|
+
} else if (error instanceof Error) {
|
|
138
|
+
console.error(chalk.red(error.message));
|
|
139
|
+
} else {
|
|
140
|
+
console.error(chalk.red("An unexpected error interrupted browser authentication."));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { readAccountState, 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 [account, user, runtimeStatus] = await Promise.all([
|
|
11
|
+
readAccountState(),
|
|
12
|
+
readUserProfile(),
|
|
13
|
+
getVisionOsRuntimeStatus()
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
console.log(chalk.cyan.bold("VisionOS Status"));
|
|
17
|
+
console.log(`${chalk.white("Runtime:")} ${runtimeStatus.isRunning ? "running" : "stopped"}`);
|
|
18
|
+
|
|
19
|
+
if (runtimeStatus.runtime) {
|
|
20
|
+
console.log(`${chalk.white("App URL:")} ${runtimeStatus.runtime.appUrl}`);
|
|
21
|
+
console.log(`${chalk.white("API URL:")} ${runtimeStatus.runtime.apiBaseUrl}`);
|
|
22
|
+
console.log(`${chalk.white("PID:")} ${runtimeStatus.runtime.pid}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`${chalk.white("User:")} ${user ? `${user.name} <${user.email}>` : "not signed in"}`);
|
|
26
|
+
console.log(`${chalk.white("Account Token:")} ${account ? "stored" : "missing"}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { LEARNING_TRACKS } from "../../../shared/src/index.js";
|
|
4
|
+
import { getVisionOsStatePaths, readAccountState, 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 [account, user, runtimeStatus] = await Promise.all([
|
|
13
|
+
readAccountState(),
|
|
14
|
+
readUserProfile(),
|
|
15
|
+
getVisionOsRuntimeStatus()
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
if (json) {
|
|
19
|
+
console.log(
|
|
20
|
+
JSON.stringify(
|
|
21
|
+
{
|
|
22
|
+
account: account
|
|
23
|
+
? {
|
|
24
|
+
authenticatedAt: account.authenticatedAt,
|
|
25
|
+
hasToken: Boolean(account.token),
|
|
26
|
+
userId: account.userId
|
|
27
|
+
}
|
|
28
|
+
: null,
|
|
29
|
+
runtime: runtimeStatus.runtime,
|
|
30
|
+
statePaths: getVisionOsStatePaths(),
|
|
31
|
+
user
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
2
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!user) {
|
|
41
|
+
console.log(chalk.yellow("No VisionOS learner profile is stored locally yet."));
|
|
42
|
+
console.log(chalk.gray("Run `visionos start` to authenticate and create one."));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const track = LEARNING_TRACKS.find((item) => item.id === user.trackId);
|
|
47
|
+
|
|
48
|
+
console.log(chalk.cyan.bold("VisionOS User"));
|
|
49
|
+
console.log(`${chalk.white("Name:")} ${user.name}`);
|
|
50
|
+
console.log(`${chalk.white("Email:")} ${user.email}`);
|
|
51
|
+
console.log(`${chalk.white("Account ID:")} ${user.userId}`);
|
|
52
|
+
console.log(`${chalk.white("Last Track:")} ${track?.label ?? user.trackId ?? "not started"}`);
|
|
53
|
+
console.log(`${chalk.white("Authenticated:")} ${user.authenticatedAt ?? "unknown"}`);
|
|
54
|
+
console.log(
|
|
55
|
+
`${chalk.white("Runtime:")} ${runtimeStatus.runtime?.appUrl ?? "not running"}`
|
|
56
|
+
);
|
|
57
|
+
console.log(`${chalk.white("State Dir:")} ${getVisionOsStatePaths().visionOsHomeDir}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
package/cli/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import dotenv from "dotenv";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { createLessonsCommand } from "./commands/lessons.js";
|
|
10
|
+
import { createLoginCommand } from "./commands/login.js";
|
|
11
|
+
import { createLogoutCommand } from "./commands/logout.js";
|
|
12
|
+
import { createOpenCommand } from "./commands/open.js";
|
|
13
|
+
import { createStatusCommand } from "./commands/status.js";
|
|
14
|
+
import { createStartCommand } from "./commands/start.js";
|
|
15
|
+
import { createUserInfoCommand } from "./commands/userinfo.js";
|
|
16
|
+
|
|
17
|
+
function loadEnvFile(filePath: string) {
|
|
18
|
+
if (fs.existsSync(filePath)) {
|
|
19
|
+
dotenv.config({ path: filePath });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadEnvironment() {
|
|
26
|
+
// 1. Try standard current working directory
|
|
27
|
+
dotenv.config();
|
|
28
|
+
|
|
29
|
+
// 2. Walk up parent directories of process.cwd() to find `.env` or `server/.env`
|
|
30
|
+
try {
|
|
31
|
+
let currentDir = process.cwd();
|
|
32
|
+
for (let i = 0; i < 10; i++) {
|
|
33
|
+
const rootEnv = path.join(currentDir, ".env");
|
|
34
|
+
const serverEnv = path.join(currentDir, "server", ".env");
|
|
35
|
+
|
|
36
|
+
if (loadEnvFile(serverEnv)) {
|
|
37
|
+
break;
|
|
38
|
+
} else if (loadEnvFile(rootEnv)) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parent = path.dirname(currentDir);
|
|
43
|
+
if (parent === currentDir) break;
|
|
44
|
+
currentDir = parent;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore fallback errors
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Walk up directories starting from import.meta.url to find `.env` or `server/.env`
|
|
51
|
+
try {
|
|
52
|
+
const startDir = path.dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
let currentDir = startDir;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < 10; i++) {
|
|
56
|
+
const rootEnv = path.join(currentDir, ".env");
|
|
57
|
+
const serverEnv = path.join(currentDir, "server", ".env");
|
|
58
|
+
|
|
59
|
+
if (loadEnvFile(serverEnv)) {
|
|
60
|
+
break;
|
|
61
|
+
} else if (loadEnvFile(rootEnv)) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parent = path.dirname(currentDir);
|
|
66
|
+
if (parent === currentDir) break;
|
|
67
|
+
currentDir = parent;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore fallback errors
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Try global user configuration directory (~/.visionos/.env)
|
|
74
|
+
try {
|
|
75
|
+
const globalEnvPath = path.join(os.homedir(), ".visionos", ".env");
|
|
76
|
+
loadEnvFile(globalEnvPath);
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore fallback errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Load active .env configurations at CLI startup
|
|
83
|
+
loadEnvironment();
|
|
84
|
+
|
|
85
|
+
import { createRequire } from "node:module";
|
|
86
|
+
const require = createRequire(import.meta.url);
|
|
87
|
+
let version = "0.1.7";
|
|
88
|
+
try {
|
|
89
|
+
version = require("../package.json").version;
|
|
90
|
+
} catch {
|
|
91
|
+
// Fallback
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const program = new Command();
|
|
95
|
+
|
|
96
|
+
program
|
|
97
|
+
.name("visionos")
|
|
98
|
+
.description("Interactive CLI for VisionOS learning tracks.")
|
|
99
|
+
.version(version);
|
|
100
|
+
|
|
101
|
+
program.addCommand(createStartCommand());
|
|
102
|
+
program.addCommand(createLoginCommand());
|
|
103
|
+
program.addCommand(createLessonsCommand());
|
|
104
|
+
program.addCommand(createOpenCommand());
|
|
105
|
+
program.addCommand(createUserInfoCommand());
|
|
106
|
+
program.addCommand(createStatusCommand());
|
|
107
|
+
program.addCommand(createLogoutCommand());
|
|
108
|
+
|
|
109
|
+
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,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LearningTrack,
|
|
3
|
+
type LearningLesson,
|
|
4
|
+
LEARNING_TRACKS,
|
|
5
|
+
BEGINNER_LESSONS
|
|
6
|
+
} from "../../../shared/src/index.js";
|
|
7
|
+
import { readAccountState } from "./localState.js";
|
|
8
|
+
|
|
9
|
+
export async function fetchRemoteTracks(apiBaseUrl: string): Promise<LearningTrack[]> {
|
|
10
|
+
try {
|
|
11
|
+
const account = await readAccountState();
|
|
12
|
+
const headers: Record<string, string> = {};
|
|
13
|
+
if (account?.token) {
|
|
14
|
+
headers["Authorization"] = `Bearer ${account.token}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/content/tracks`, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
headers
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Failed to fetch tracks: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = (await response.json()) as { tracks: LearningTrack[] };
|
|
27
|
+
return data.tracks ?? LEARNING_TRACKS;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Graceful offline fallback
|
|
30
|
+
return LEARNING_TRACKS;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchRemoteLessons(apiBaseUrl: string, trackId: string): Promise<LearningLesson[]> {
|
|
35
|
+
try {
|
|
36
|
+
const account = await readAccountState();
|
|
37
|
+
const headers: Record<string, string> = {};
|
|
38
|
+
if (account?.token) {
|
|
39
|
+
headers["Authorization"] = `Bearer ${account.token}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/content/tracks/${trackId}/lessons`, {
|
|
43
|
+
method: "GET",
|
|
44
|
+
headers
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Failed to fetch lessons: ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = (await response.json()) as { lessons: LearningLesson[] };
|
|
52
|
+
return data.lessons ?? BEGINNER_LESSONS.filter((l) => l.trackId === trackId);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Graceful offline fallback
|
|
55
|
+
return BEGINNER_LESSONS.filter((l) => l.trackId === trackId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import {
|
|
3
|
+
LEARNING_TRACKS,
|
|
4
|
+
getLessonsForTrack,
|
|
5
|
+
type LearningLesson,
|
|
6
|
+
type LearningTrackId
|
|
7
|
+
} from "../../../shared/src/index.js";
|
|
8
|
+
|
|
9
|
+
export function printLessonCatalog(trackId: LearningTrackId) {
|
|
10
|
+
const track = LEARNING_TRACKS.find((item) => item.id === trackId);
|
|
11
|
+
const lessons = getLessonsForTrack(trackId);
|
|
12
|
+
|
|
13
|
+
console.log(chalk.cyan.bold(`\n${track?.label ?? trackId} beginner lessons`));
|
|
14
|
+
|
|
15
|
+
for (const lesson of lessons) {
|
|
16
|
+
console.log(chalk.white(`\n${lesson.title}`));
|
|
17
|
+
console.log(chalk.gray(`${lesson.summary} (${lesson.estimatedMinutes} min)`));
|
|
18
|
+
|
|
19
|
+
for (const [index, step] of lesson.steps.entries()) {
|
|
20
|
+
console.log(chalk.gray(` ${index + 1}. ${step.title}`));
|
|
21
|
+
console.log(chalk.yellow(` ${step.command}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function printLessonPreview(lesson: LearningLesson) {
|
|
27
|
+
console.log(chalk.cyan.bold(`\n${lesson.title}`));
|
|
28
|
+
console.log(chalk.gray(`${lesson.summary} (${lesson.estimatedMinutes} min)\n`));
|
|
29
|
+
|
|
30
|
+
for (const [index, step] of lesson.steps.entries()) {
|
|
31
|
+
console.log(chalk.white(`${index + 1}. ${step.title}`));
|
|
32
|
+
console.log(chalk.gray(` Goal: ${step.objective}`));
|
|
33
|
+
console.log(chalk.yellow(` Command: ${step.command}`));
|
|
34
|
+
console.log(chalk.gray(` Why: ${step.explanation}\n`));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.gray("Command execution and validation will be the next implementation step."));
|
|
38
|
+
}
|