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,381 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import {
|
|
9
|
+
type LearningLesson,
|
|
10
|
+
type LearningProgress,
|
|
11
|
+
type UpsertLearningProgressRequest
|
|
12
|
+
} from "../../../shared/src/index.js";
|
|
13
|
+
import { readUserProfile } from "./localState.js";
|
|
14
|
+
import { getRemoteProgress, syncProgress } from "./progress.js";
|
|
15
|
+
import { validateStep } from "./validator.js";
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
|
|
19
|
+
async function runSystemCommand(command: string, cwd: string): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
20
|
+
try {
|
|
21
|
+
const { stdout, stderr } = await execAsync(command, { cwd });
|
|
22
|
+
return { stdout, stderr, code: 0 };
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
return {
|
|
25
|
+
stdout: error.stdout ?? "",
|
|
26
|
+
stderr: error.stderr ?? error.message ?? "",
|
|
27
|
+
code: error.code ?? 1
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const sandboxBaseDir = path.join(os.homedir(), ".visionos", "sandbox");
|
|
34
|
+
|
|
35
|
+
// Steps that do not leave a physical filesystem footprint.
|
|
36
|
+
// Learners will be asked to type the command they ran to verify its syntax.
|
|
37
|
+
const syntaxOnlyStepIds = new Set([
|
|
38
|
+
"git-branch-list",
|
|
39
|
+
"git-log-short",
|
|
40
|
+
"linux-pwd",
|
|
41
|
+
"linux-ls",
|
|
42
|
+
"linux-cd",
|
|
43
|
+
"linux-cat",
|
|
44
|
+
"react-dev",
|
|
45
|
+
"react-scripts-list",
|
|
46
|
+
"react-lint",
|
|
47
|
+
"os-ps",
|
|
48
|
+
"os-lsof-port",
|
|
49
|
+
"os-kill",
|
|
50
|
+
"os-ping",
|
|
51
|
+
"os-curl-head",
|
|
52
|
+
"os-dns"
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function padString(label: string, text: string, width: number): string {
|
|
56
|
+
const plainLabel = label;
|
|
57
|
+
const plainText = text;
|
|
58
|
+
const paddingLength = Math.max(0, width - 4 - plainLabel.length - plainText.length);
|
|
59
|
+
return chalk.cyan("│ ") + chalk.white.bold(plainLabel) + chalk.green(plainText) + " ".repeat(paddingLength) + chalk.cyan(" │");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function wrapText(text: string, maxWidth: number): string[] {
|
|
63
|
+
const words = text.split(" ");
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
let currentLine = "";
|
|
66
|
+
|
|
67
|
+
for (const word of words) {
|
|
68
|
+
if ((currentLine + " " + word).trim().length <= maxWidth) {
|
|
69
|
+
currentLine = (currentLine + " " + word).trim();
|
|
70
|
+
} else {
|
|
71
|
+
if (currentLine) lines.push(currentLine);
|
|
72
|
+
currentLine = word;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (currentLine) lines.push(currentLine);
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printStepBox(step: { title: string; objective: string; explanation: string }, index: number, total: number) {
|
|
80
|
+
const width = 76;
|
|
81
|
+
console.log(chalk.cyan(`\n┌─ STEP [${String(index + 1).padStart(2, "0")}/${String(total).padStart(2, "0")}] ` + "─".repeat(width - 15) + "┐"));
|
|
82
|
+
console.log(padString("Title: ", step.title, width));
|
|
83
|
+
console.log(padString("Goal: ", step.objective, width));
|
|
84
|
+
console.log(chalk.cyan(`├` + "─".repeat(width - 2) + `┤`));
|
|
85
|
+
|
|
86
|
+
const maxTextWidth = width - 6;
|
|
87
|
+
const wrappedExplanation = wrapText(step.explanation, maxTextWidth);
|
|
88
|
+
for (const line of wrappedExplanation) {
|
|
89
|
+
console.log(chalk.cyan("│ ") + chalk.gray(line.padEnd(width - 4)) + chalk.cyan(" │"));
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.cyan(`└` + "─".repeat(width - 2) + `┘`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printCompletionBadge(lessonTitle: string) {
|
|
95
|
+
console.log(chalk.yellow.bold(`
|
|
96
|
+
┌────────────────────────────────────────────────────────────┐
|
|
97
|
+
│ LESSON COMPLETED │
|
|
98
|
+
│ │
|
|
99
|
+
│ Congratulations! You have completed: │
|
|
100
|
+
│ "${lessonTitle.slice(0, 48).padEnd(48)}" │
|
|
101
|
+
│ │
|
|
102
|
+
│ Your achievements have been synced with the dashboard. │
|
|
103
|
+
└────────────────────────────────────────────────────────────┘
|
|
104
|
+
`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function runLessonInteractive(lesson: LearningLesson, apiBaseUrl: string) {
|
|
108
|
+
const user = await readUserProfile();
|
|
109
|
+
if (!user) {
|
|
110
|
+
console.log(chalk.yellow("No signed-in profile found. Run `visionos start` to authenticate first."));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(chalk.cyan.bold(`\nEntering Interactive Sandbox Lesson Runner...`));
|
|
115
|
+
|
|
116
|
+
// 1. Prompt user to choose between default isolated sandbox or custom directory
|
|
117
|
+
const { workspaceOption } = await inquirer.prompt<{ workspaceOption: "default" | "custom" }>([
|
|
118
|
+
{
|
|
119
|
+
type: "list",
|
|
120
|
+
name: "workspaceOption",
|
|
121
|
+
message: "Choose your practice workspace directory:",
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: "Default isolated sandbox (cleaned automatically)", value: "default" },
|
|
124
|
+
{ name: "Custom directory (work in your own familiar folder)", value: "custom" }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
let sandboxDir = path.join(sandboxBaseDir, lesson.id);
|
|
130
|
+
|
|
131
|
+
if (workspaceOption === "custom") {
|
|
132
|
+
const { customPath } = await inquirer.prompt<{ customPath: string }>([
|
|
133
|
+
{
|
|
134
|
+
type: "input",
|
|
135
|
+
name: "customPath",
|
|
136
|
+
message: "Enter the absolute path to your custom workspace folder:",
|
|
137
|
+
validate: async (val: string) => {
|
|
138
|
+
const trimmed = val.trim();
|
|
139
|
+
if (!trimmed) {
|
|
140
|
+
return "Path cannot be empty.";
|
|
141
|
+
}
|
|
142
|
+
const p = path.resolve(trimmed.replace(/^~/, os.homedir()));
|
|
143
|
+
try {
|
|
144
|
+
const stat = await fs.stat(p);
|
|
145
|
+
if (!stat.isDirectory()) {
|
|
146
|
+
return "Path exists but is not a directory.";
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
// Try to create it
|
|
151
|
+
try {
|
|
152
|
+
await fs.mkdir(p, { recursive: true });
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return "Invalid path or unable to create directory.";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
]);
|
|
161
|
+
sandboxDir = path.resolve(customPath.trim().replace(/^~/, os.homedir()));
|
|
162
|
+
} else {
|
|
163
|
+
// Default isolated sandbox: clean and recreate
|
|
164
|
+
try {
|
|
165
|
+
await fs.rm(sandboxDir, { recursive: true, force: true });
|
|
166
|
+
await fs.mkdir(sandboxDir, { recursive: true });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(chalk.red(`Unable to initialize default sandbox folder: ${sandboxDir}`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(chalk.gray(`\nWorkspace: ${sandboxDir}`));
|
|
174
|
+
console.log(chalk.gray("Open a separate terminal window and operate inside the workspace directory."));
|
|
175
|
+
if (workspaceOption === "custom") {
|
|
176
|
+
console.log(chalk.yellow("Safety Notice: Your custom directory files will NEVER be cleaned or deleted."));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 2. Fetch existing progress
|
|
180
|
+
let completedStepIds: string[] = [];
|
|
181
|
+
let completedLessonIds: string[] = [];
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const remoteRes = await getRemoteProgress(apiBaseUrl);
|
|
185
|
+
const existing = remoteRes?.progress?.find((p) => p.trackId === lesson.trackId);
|
|
186
|
+
if (existing) {
|
|
187
|
+
completedStepIds = existing.completedStepIds ?? [];
|
|
188
|
+
completedLessonIds = existing.completedLessonIds ?? [];
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// Ignore fetch errors; proceed offline
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3. Step execution & validation loop
|
|
195
|
+
let stepIndex = 0;
|
|
196
|
+
const totalSteps = lesson.steps.length;
|
|
197
|
+
|
|
198
|
+
while (stepIndex < totalSteps) {
|
|
199
|
+
const step = lesson.steps[stepIndex];
|
|
200
|
+
printStepBox(step, stepIndex, totalSteps);
|
|
201
|
+
|
|
202
|
+
console.log(chalk.yellow(`\nTarget Command: ${step.command}`));
|
|
203
|
+
console.log(chalk.cyan(`👉 Workspace Path: ${sandboxDir}`));
|
|
204
|
+
console.log(chalk.gray(` To practice, open a SEPARATE terminal tab/window and run:`));
|
|
205
|
+
console.log(chalk.green(` cd ${sandboxDir} && ${step.command}`));
|
|
206
|
+
console.log("");
|
|
207
|
+
|
|
208
|
+
const { action } = await inquirer.prompt<{ action: "verify" | "explain" | "skip" | "exit" }>([
|
|
209
|
+
{
|
|
210
|
+
type: "list",
|
|
211
|
+
name: "action",
|
|
212
|
+
message: "Select your action:",
|
|
213
|
+
choices: [
|
|
214
|
+
{ name: "Verify my command (Check if I did it correctly)", value: "verify" },
|
|
215
|
+
{ name: "Show expected command info again", value: "explain" },
|
|
216
|
+
{ name: "Skip this step", value: "skip" },
|
|
217
|
+
{ name: "Exit lesson", value: "exit" }
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
if (action === "exit") {
|
|
223
|
+
console.log(chalk.cyan("Exiting lesson. Your current progress has been saved."));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (action === "skip") {
|
|
228
|
+
stepIndex++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (action === "explain") {
|
|
233
|
+
continue; // Displays step info again
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 4. Verification routing
|
|
237
|
+
let typedCommand = "";
|
|
238
|
+
|
|
239
|
+
// If syntax-only, prompt learner to type the command they ran
|
|
240
|
+
if (syntaxOnlyStepIds.has(step.id)) {
|
|
241
|
+
const { userTyped } = await inquirer.prompt<{ userTyped: string }>([
|
|
242
|
+
{
|
|
243
|
+
type: "input",
|
|
244
|
+
name: "userTyped",
|
|
245
|
+
message: "Enter the command you ran to verify its syntax:",
|
|
246
|
+
validate: (val: string) => val.trim().length > 0 || "Command cannot be empty."
|
|
247
|
+
}
|
|
248
|
+
]);
|
|
249
|
+
typedCommand = userTyped;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(chalk.gray(`\nVerifying state inside workspace: ${sandboxDir}...`));
|
|
253
|
+
|
|
254
|
+
let systemStdout = "";
|
|
255
|
+
let systemStderr = "";
|
|
256
|
+
|
|
257
|
+
// 1. Run actual command for syntax-only steps to capture system output
|
|
258
|
+
if (syntaxOnlyStepIds.has(step.id) && typedCommand) {
|
|
259
|
+
const result = await runSystemCommand(typedCommand, sandboxDir);
|
|
260
|
+
systemStdout = result.stdout;
|
|
261
|
+
systemStderr = result.stderr;
|
|
262
|
+
|
|
263
|
+
console.log(chalk.gray(`----------------------------------------`));
|
|
264
|
+
console.log(chalk.yellow(`💻 System Output for: `) + chalk.cyan(typedCommand));
|
|
265
|
+
if (systemStdout.trim()) {
|
|
266
|
+
console.log(chalk.green(systemStdout.trim()));
|
|
267
|
+
}
|
|
268
|
+
if (systemStderr.trim()) {
|
|
269
|
+
console.log(chalk.red(systemStderr.trim()));
|
|
270
|
+
}
|
|
271
|
+
if (!systemStdout.trim() && !systemStderr.trim()) {
|
|
272
|
+
console.log(chalk.gray(`(No output returned)`));
|
|
273
|
+
}
|
|
274
|
+
console.log(chalk.gray(`----------------------------------------`));
|
|
275
|
+
} else {
|
|
276
|
+
// 2. Safe state inspection for physical steps
|
|
277
|
+
const inspectionCommands: Record<string, string> = {
|
|
278
|
+
"git-init": "git status",
|
|
279
|
+
"git-status": "git status",
|
|
280
|
+
"git-first-commit": "git log -n 1 --oneline",
|
|
281
|
+
"git-switch-create": "git branch",
|
|
282
|
+
"linux-mkdir": "ls -F",
|
|
283
|
+
"linux-echo-file": "cat practice/hello.txt",
|
|
284
|
+
"react-create": "ls -F my-app 2>/dev/null || ls -F",
|
|
285
|
+
"react-install": "ls -F my-app/node_modules 2>/dev/null || ls -F my-app",
|
|
286
|
+
"react-build": "ls -F my-app/dist 2>/dev/null || ls -F my-app"
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const inspectCmd = inspectionCommands[step.id];
|
|
290
|
+
if (inspectCmd) {
|
|
291
|
+
const result = await runSystemCommand(inspectCmd, sandboxDir);
|
|
292
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
293
|
+
console.log(chalk.gray(`----------------------------------------`));
|
|
294
|
+
console.log(chalk.yellow(`🔍 Current Workspace State (via ${inspectCmd}):`));
|
|
295
|
+
console.log(chalk.gray(result.stdout.trim()));
|
|
296
|
+
console.log(chalk.gray(`----------------------------------------`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// Run validator
|
|
303
|
+
const validation = await validateStep(step, sandboxDir, typedCommand, systemStdout, systemStderr);
|
|
304
|
+
|
|
305
|
+
if (!validation.valid) {
|
|
306
|
+
console.log(chalk.red.bold(`
|
|
307
|
+
┌─ MISTAKE IDENTIFIED ──────────────────────────────────────┐
|
|
308
|
+
│ │`));
|
|
309
|
+
const wrappedMistake = wrapText(validation.mistake ?? "Validation failed.", 54);
|
|
310
|
+
for (const line of wrappedMistake) {
|
|
311
|
+
console.log(chalk.red(" │ ") + chalk.white(line.padEnd(54)) + chalk.red(" │"));
|
|
312
|
+
}
|
|
313
|
+
console.log(chalk.red(` │ │
|
|
314
|
+
└───────────────────────────────────────────────────────────┘
|
|
315
|
+
`));
|
|
316
|
+
|
|
317
|
+
console.log(chalk.yellow(`💡 How to correct this mistake:`));
|
|
318
|
+
console.log(chalk.gray(` 1. Open a SEPARATE terminal tab or window.`));
|
|
319
|
+
console.log(chalk.gray(` 2. Run this command to navigate to the workspace and execute the target:`));
|
|
320
|
+
console.log(chalk.cyan(` cd ${sandboxDir} && ${step.command}`));
|
|
321
|
+
console.log(chalk.gray(` 3. Return here and press [Enter] to try verifying again.\n`));
|
|
322
|
+
|
|
323
|
+
await inquirer.prompt([
|
|
324
|
+
{
|
|
325
|
+
type: "input",
|
|
326
|
+
name: "continue",
|
|
327
|
+
message: "Press [Enter] to try this command again and correct the mistake..."
|
|
328
|
+
}
|
|
329
|
+
]);
|
|
330
|
+
continue; // Repeat current step
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(chalk.green("✓ Verification successful! You completed this step correctly."));
|
|
334
|
+
|
|
335
|
+
// Save step progress
|
|
336
|
+
if (!completedStepIds.includes(step.id)) {
|
|
337
|
+
completedStepIds.push(step.id);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check if all steps are completed
|
|
341
|
+
const isLessonFinished = lesson.steps.every((s) => completedStepIds.includes(s.id));
|
|
342
|
+
if (isLessonFinished && !completedLessonIds.includes(lesson.id)) {
|
|
343
|
+
completedLessonIds.push(lesson.id);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Sync progress
|
|
347
|
+
const syncPayload: UpsertLearningProgressRequest = {
|
|
348
|
+
trackId: lesson.trackId,
|
|
349
|
+
completedLessonIds,
|
|
350
|
+
completedStepIds,
|
|
351
|
+
currentLessonId: lesson.id
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
await syncProgress(apiBaseUrl, syncPayload);
|
|
355
|
+
|
|
356
|
+
if (isLessonFinished) {
|
|
357
|
+
printCompletionBadge(lesson.title);
|
|
358
|
+
await inquirer.prompt([
|
|
359
|
+
{
|
|
360
|
+
type: "input",
|
|
361
|
+
name: "continue",
|
|
362
|
+
message: "Press [Enter] to complete this lesson and return to menu..."
|
|
363
|
+
}
|
|
364
|
+
]);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await inquirer.prompt([
|
|
369
|
+
{
|
|
370
|
+
type: "input",
|
|
371
|
+
name: "continue",
|
|
372
|
+
message: "Press [Enter] to proceed to the next step..."
|
|
373
|
+
}
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
stepIndex++;
|
|
377
|
+
} catch (error: any) {
|
|
378
|
+
console.log(chalk.red(`✗ Validation system error: ${error.message ?? error}`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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, type LearningProgress } 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
|
+
export interface VisionOsAccountState {
|
|
14
|
+
authenticatedAt: string;
|
|
15
|
+
token: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const visionOsHomeDir = path.join(os.homedir(), ".visionos");
|
|
20
|
+
const accountStatePath = path.join(visionOsHomeDir, "account.json");
|
|
21
|
+
const runtimeStatePath = path.join(visionOsHomeDir, "runtime.json");
|
|
22
|
+
const userStatePath = path.join(visionOsHomeDir, "user.json");
|
|
23
|
+
const progressStatePath = path.join(visionOsHomeDir, "progress.json");
|
|
24
|
+
|
|
25
|
+
async function ensureVisionOsHomeDir() {
|
|
26
|
+
await fs.mkdir(visionOsHomeDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
30
|
+
try {
|
|
31
|
+
const contents = await fs.readFile(filePath, "utf8");
|
|
32
|
+
|
|
33
|
+
return JSON.parse(contents) as T;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function writeJsonFile(filePath: string, payload: unknown) {
|
|
44
|
+
await ensureVisionOsHomeDir();
|
|
45
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function removeFileIfPresent(filePath: string) {
|
|
49
|
+
try {
|
|
50
|
+
await fs.unlink(filePath);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getVisionOsStatePaths() {
|
|
59
|
+
return {
|
|
60
|
+
accountStatePath,
|
|
61
|
+
runtimeStatePath,
|
|
62
|
+
userStatePath,
|
|
63
|
+
visionOsHomeDir
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readUserProfile() {
|
|
68
|
+
return readJsonFile<VisionOsUserProfile>(userStatePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeUserProfile(profile: VisionOsUserProfile) {
|
|
72
|
+
return writeJsonFile(userStatePath, profile);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearUserProfile() {
|
|
76
|
+
return removeFileIfPresent(userStatePath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readAccountState() {
|
|
80
|
+
return readJsonFile<VisionOsAccountState>(accountStatePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function writeAccountState(state: VisionOsAccountState) {
|
|
84
|
+
return writeJsonFile(accountStatePath, state);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function clearAccountState() {
|
|
88
|
+
return removeFileIfPresent(accountStatePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readRuntimeState() {
|
|
92
|
+
return readJsonFile<VisionOsRuntimeState>(runtimeStatePath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function writeRuntimeState(state: VisionOsRuntimeState) {
|
|
96
|
+
return writeJsonFile(runtimeStatePath, state);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function clearRuntimeState() {
|
|
100
|
+
return removeFileIfPresent(runtimeStatePath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function readLocalProgress(): Promise<LearningProgress[]> {
|
|
104
|
+
const data = await readJsonFile<LearningProgress[]>(progressStatePath);
|
|
105
|
+
return data ?? [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function writeLocalProgress(progress: LearningProgress[]) {
|
|
109
|
+
return writeJsonFile(progressStatePath, progress);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function clearLocalProgress() {
|
|
113
|
+
return removeFileIfPresent(progressStatePath);
|
|
114
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import {
|
|
3
|
+
APP_NAME,
|
|
4
|
+
LEARNING_TRACKS,
|
|
5
|
+
type CliAuthSession,
|
|
6
|
+
type LearningTrackId
|
|
7
|
+
} from "../../../shared/src/index.js";
|
|
8
|
+
import { createCliAuthSession, waitForCliAuthCompletion } from "./auth.js";
|
|
9
|
+
import { openBrowser } from "./browser.js";
|
|
10
|
+
import { writeAccountState, writeUserProfile } from "./localState.js";
|
|
11
|
+
import { ensureVisionOsRuntime } from "./runtime.js";
|
|
12
|
+
|
|
13
|
+
export async function runBrowserLogin(trackId: LearningTrackId): Promise<CliAuthSession | null> {
|
|
14
|
+
const selectedTrack = LEARNING_TRACKS.find((track) => track.id === trackId);
|
|
15
|
+
const runtime = await ensureVisionOsRuntime();
|
|
16
|
+
const authSession = await createCliAuthSession(runtime.apiBaseUrl, trackId);
|
|
17
|
+
|
|
18
|
+
console.log(chalk.white(`VisionOS runtime ready at ${runtime.appUrl}`));
|
|
19
|
+
console.log(chalk.white("Starting browser account login..."));
|
|
20
|
+
|
|
21
|
+
const browserOpened = await openBrowser(authSession.browserUrl);
|
|
22
|
+
|
|
23
|
+
if (browserOpened) {
|
|
24
|
+
console.log(chalk.gray("A browser window was opened for account sign-in."));
|
|
25
|
+
} else {
|
|
26
|
+
console.log(chalk.yellow("Open this URL to continue account sign-in:"));
|
|
27
|
+
console.log(chalk.white(authSession.browserUrl));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(chalk.gray("Waiting for browser confirmation...\n"));
|
|
31
|
+
|
|
32
|
+
const completedSession = await waitForCliAuthCompletion(
|
|
33
|
+
runtime.apiBaseUrl,
|
|
34
|
+
authSession.session.sessionId,
|
|
35
|
+
authSession.pollIntervalMs
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (completedSession.status !== "authenticated" || !completedSession.learner) {
|
|
39
|
+
console.log(chalk.red("The authentication session expired. Run `visionos login` again."));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const authenticatedAt = completedSession.authenticatedAt ?? new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
await writeUserProfile({
|
|
46
|
+
authenticatedAt,
|
|
47
|
+
email: completedSession.learner.email,
|
|
48
|
+
name: completedSession.learner.name,
|
|
49
|
+
sessionId: completedSession.sessionId,
|
|
50
|
+
trackId: completedSession.trackId,
|
|
51
|
+
userId: completedSession.learner.userId
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (completedSession.accountToken) {
|
|
55
|
+
await writeAccountState({
|
|
56
|
+
authenticatedAt,
|
|
57
|
+
token: completedSession.accountToken,
|
|
58
|
+
userId: completedSession.learner.userId
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(
|
|
63
|
+
chalk.green(
|
|
64
|
+
`Authenticated as ${completedSession.learner.name} (${completedSession.learner.email}).`
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.white(
|
|
69
|
+
`${APP_NAME} account ready for the ${selectedTrack?.label ?? completedSession.trackId} track.`
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return completedSession;
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LearningProgress,
|
|
3
|
+
type LearningTrackId,
|
|
4
|
+
type UpsertLearningProgressRequest
|
|
5
|
+
} from "../../../shared/src/index.js";
|
|
6
|
+
import { VisionOsApiError } from "./auth.js";
|
|
7
|
+
import { readAccountState } from "./localState.js";
|
|
8
|
+
|
|
9
|
+
async function parseResponse<T>(response: Response): Promise<T> {
|
|
10
|
+
const payload = (await response.json().catch(() => null)) as { message?: string } | null;
|
|
11
|
+
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new VisionOsApiError(
|
|
14
|
+
payload?.message ?? `VisionOS API request failed with status ${response.status}.`,
|
|
15
|
+
response.status
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return payload as T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function saveCurrentLessonProgress(
|
|
23
|
+
apiBaseUrl: string,
|
|
24
|
+
input: {
|
|
25
|
+
currentLessonId: string;
|
|
26
|
+
trackId: LearningTrackId;
|
|
27
|
+
}
|
|
28
|
+
) {
|
|
29
|
+
const account = await readAccountState();
|
|
30
|
+
|
|
31
|
+
if (!account?.token) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const body: UpsertLearningProgressRequest = {
|
|
36
|
+
completedLessonIds: [],
|
|
37
|
+
completedStepIds: [],
|
|
38
|
+
currentLessonId: input.currentLessonId,
|
|
39
|
+
trackId: input.trackId
|
|
40
|
+
};
|
|
41
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/progress`, {
|
|
42
|
+
method: "PUT",
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${account.token}`,
|
|
45
|
+
"Content-Type": "application/json"
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(body)
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return parseResponse<{ progress: LearningProgress }>(response);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getRemoteProgress(
|
|
54
|
+
apiBaseUrl: string
|
|
55
|
+
): Promise<{ progress: LearningProgress[] } | null> {
|
|
56
|
+
const account = await readAccountState();
|
|
57
|
+
|
|
58
|
+
if (!account?.token) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/progress`, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${account.token}`
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return parseResponse<{ progress: LearningProgress[] }>(response);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function syncProgress(
|
|
73
|
+
apiBaseUrl: string,
|
|
74
|
+
input: UpsertLearningProgressRequest
|
|
75
|
+
): Promise<{ progress: LearningProgress } | null> {
|
|
76
|
+
const account = await readAccountState();
|
|
77
|
+
|
|
78
|
+
if (!account?.token) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/progress`, {
|
|
83
|
+
method: "PUT",
|
|
84
|
+
headers: {
|
|
85
|
+
Authorization: `Bearer ${account.token}`,
|
|
86
|
+
"Content-Type": "application/json"
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(input)
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return parseResponse<{ progress: LearningProgress }>(response);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|