vanspod 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +74 -2
  2. package/bin/register.cjs +11 -0
  3. package/bin/vanspod.cjs +2 -0
  4. package/dist/adapterSettings.js +37 -0
  5. package/dist/adapters/chat-output.js +69 -0
  6. package/dist/adapters/chat-stream.js +44 -0
  7. package/dist/adapters/chat.js +63 -0
  8. package/dist/adapters/claude-reasoning.js +118 -0
  9. package/dist/adapters/claude-request.js +190 -0
  10. package/dist/adapters/claude-response.js +79 -0
  11. package/dist/adapters/index.js +19 -0
  12. package/dist/adapters/messages-stream.js +126 -0
  13. package/dist/adapters/messages.js +21 -0
  14. package/dist/adapters/responses-stream.js +70 -0
  15. package/dist/adapters/responses.js +28 -0
  16. package/dist/adapters/stream.js +99 -0
  17. package/dist/adapters/transport.js +26 -0
  18. package/dist/adapters/types.js +35 -0
  19. package/dist/appearance.js +10 -0
  20. package/dist/classroomApp.js +1814 -0
  21. package/dist/classroomUpdate.js +155 -0
  22. package/dist/claudeWebSearch.js +248 -0
  23. package/dist/clientDetection.js +35 -0
  24. package/dist/clientIcons.js +33 -0
  25. package/dist/clientRouteRuntime.js +149 -0
  26. package/dist/clientRoutes.js +118 -0
  27. package/dist/clientSettingsBackup.js +110 -0
  28. package/dist/clientTraffic.js +26 -0
  29. package/dist/clipboard.js +32 -0
  30. package/dist/codexWebSearch.js +80 -0
  31. package/dist/codexWire/apply-patch-envelope.js +139 -0
  32. package/dist/codexWire/chat-options.js +51 -0
  33. package/dist/codexWire/chat-tools.js +80 -0
  34. package/dist/codexWire/freeform-wrapper-scan.js +326 -0
  35. package/dist/codexWire/progressive-freeform-input.js +133 -0
  36. package/dist/codexWire/request.js +73 -0
  37. package/dist/codexWire/responses-tools.js +88 -0
  38. package/dist/codexWire/tool-arguments.js +39 -0
  39. package/dist/codexWire/tool-guidance.js +77 -0
  40. package/dist/commandRunner.js +100 -0
  41. package/dist/connectionMascot.js +235 -0
  42. package/dist/courseCatalog.js +131 -0
  43. package/dist/desktop.js +25 -0
  44. package/dist/entry.js +17 -0
  45. package/dist/environmentIcons.js +11 -0
  46. package/dist/exitGuard.js +17 -0
  47. package/dist/folderPicker.js +35 -0
  48. package/dist/installProgress.js +171 -0
  49. package/dist/installations.js +72 -0
  50. package/dist/instance.js +160 -0
  51. package/dist/launcher.js +155 -0
  52. package/dist/legacyMigration.js +87 -0
  53. package/dist/lifecyclePage.js +41 -0
  54. package/dist/modelProxy.js +105 -0
  55. package/dist/modelStatus.js +42 -0
  56. package/dist/page.js +1016 -0
  57. package/dist/pagePrototype.js +528 -0
  58. package/dist/platformStorage.js +91 -0
  59. package/dist/removal.js +41 -0
  60. package/dist/responsesChat.js +835 -0
  61. package/dist/routeFiles.js +646 -0
  62. package/dist/routerSessionModels.js +32 -0
  63. package/dist/runtimeOptions.js +14 -0
  64. package/dist/secureStorage.js +63 -0
  65. package/dist/settingsActions.js +100 -0
  66. package/dist/start.js +822 -0
  67. package/dist/storageErrors.js +17 -0
  68. package/dist/storageLocation.js +58 -0
  69. package/dist/version.js +4 -0
  70. package/dist/vsCodeRouteSettings.js +208 -0
  71. package/dist/webSearch.js +56 -0
  72. package/dist/windowsSecureStorage.js +163 -0
  73. package/media/brand-icon.ico +0 -0
  74. package/media/brand-logo.png +0 -0
  75. package/media/fadou-superhero-blink.png +0 -0
  76. package/media/fadou-superhero.png +0 -0
  77. package/package.json +27 -4
  78. package/scripts/tray.ps1 +40 -0
  79. package/scripts/uninstall.ps1 +133 -0
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.windowsFolderPickerCommand = void 0;
4
+ exports.pickProjectFolder = pickProjectFolder;
5
+ const node_child_process_1 = require("node:child_process");
6
+ exports.windowsFolderPickerCommand = [
7
+ "$ErrorActionPreference = 'Stop'",
8
+ "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding",
9
+ "Add-Type -AssemblyName System.Windows.Forms",
10
+ "$owner = New-Object System.Windows.Forms.Form",
11
+ "$owner.TopMost = $true",
12
+ "$owner.ShowInTaskbar = $false",
13
+ "$owner.StartPosition = 'CenterScreen'",
14
+ "$owner.Opacity = 0",
15
+ "$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
16
+ "$dialog.Description = '選擇專案資料夾'",
17
+ "$dialog.ShowNewFolderButton = $true",
18
+ // TopMost takes effect only after the owner has a visible window handle.
19
+ "$owner.Show()",
20
+ "$owner.Activate()",
21
+ "try { if ($dialog.ShowDialog($owner) -eq 'OK') { $dialog.SelectedPath } } finally { $dialog.Dispose(); $owner.Dispose() }",
22
+ ].join("; ");
23
+ function pickProjectFolder() {
24
+ if (process.platform !== "win32")
25
+ return Promise.resolve(undefined);
26
+ return new Promise((resolve, reject) => {
27
+ (0, node_child_process_1.execFile)("powershell.exe", ["-NoProfile", "-STA", "-Command", exports.windowsFolderPickerCommand], { timeout: 120_000, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
28
+ if (error) {
29
+ reject(error);
30
+ return;
31
+ }
32
+ resolve(stdout.replaceAll("\r", "").trim() || undefined);
33
+ });
34
+ });
35
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installProgressScript = void 0;
4
+ exports.installProgressScript = `
5
+ var installBusy = false;
6
+ var installOpening = false;
7
+ function closeInstallDialog(target) {
8
+ target.closest(".confirm-dialog").remove();
9
+ installBusy = false;
10
+ }
11
+ document.addEventListener("click", function (event) {
12
+ var button = event.target?.closest?.("[data-install-dismiss]");
13
+ if (button && !button.disabled) closeInstallDialog(button);
14
+ });
15
+ document.addEventListener("submit", async function (event) {
16
+ var form = event.target;
17
+ if (form?.matches?.("form[data-abort-install]")) {
18
+ if (!window.confirm("停止後安裝可能不完整,確定停止?")) { event.preventDefault(); return; }
19
+ if (!installBusy || !form.closest(".confirm-panel")) return;
20
+ event.preventDefault();
21
+ var abortButton = form.querySelector("button");
22
+ abortButton.disabled = true;
23
+ form.closest(".confirm-panel").querySelector("[data-install-status]").textContent = "正在停止安裝…";
24
+ try {
25
+ var stopped = await fetch(form.action, { method: "POST", headers: { "X-Classroom-Progress": "1" } });
26
+ if (!stopped.ok) throw new Error("abort failed");
27
+ } catch (error) {
28
+ abortButton.disabled = false;
29
+ }
30
+ return;
31
+ }
32
+ if (form?.matches?.('form[action="/environment"]')) {
33
+ var card = form.closest("section");
34
+ if (!card) return;
35
+ card.querySelectorAll('button[form="settings-environment"]').forEach(function (start) { start.hidden = true; });
36
+ card.querySelectorAll("form[data-abort-install]").forEach(function (abort) { abort.hidden = false; });
37
+ return;
38
+ }
39
+ if (form?.matches?.('form[action="/prepare"]')) {
40
+ event.preventDefault();
41
+ if (installOpening || installBusy || document.querySelector(".confirm-dialog")) return;
42
+ installOpening = true;
43
+ var button = form.querySelector("button");
44
+ var card = form.closest(".install-card");
45
+ var previousError = card?.querySelector(".install-open-error");
46
+ if (previousError) previousError.remove();
47
+ button.disabled = true;
48
+ try {
49
+ var prepared = await fetch(form.action, {
50
+ method: "POST",
51
+ headers: { "X-Classroom-Progress": "1" },
52
+ body: new URLSearchParams(new FormData(form)),
53
+ });
54
+ if (!prepared.ok) throw new Error("prepare failed");
55
+ var page = new DOMParser().parseFromString(await prepared.text(), "text/html");
56
+ var dialog = page.querySelector(".confirm-dialog");
57
+ if (!dialog) throw new Error("dialog unavailable");
58
+ document.body.appendChild(dialog);
59
+ } catch (error) {
60
+ var message = document.createElement("p");
61
+ message.className = "error install-open-error";
62
+ message.setAttribute("role", "alert");
63
+ message.textContent = "無法開啟安裝視窗,請確認專案資料夾已設定後再試。";
64
+ (card || form).appendChild(message);
65
+ } finally {
66
+ button.disabled = false;
67
+ installOpening = false;
68
+ }
69
+ return;
70
+ }
71
+ if (!form || !form.matches || !form.matches('form[action="/confirm"], form[action="/cancel"]')) return;
72
+ event.preventDefault();
73
+ if (installBusy) return;
74
+ installBusy = true;
75
+ var panel = form.closest(".confirm-panel");
76
+ var title = panel.querySelector("h2");
77
+ var status = panel.querySelector("[data-install-status]");
78
+ var output = panel.querySelector("[data-install-output]");
79
+ var dismiss = panel.querySelector("[data-install-dismiss]");
80
+ var choices = panel.querySelector(".choices");
81
+ var buttons = panel.querySelectorAll("button");
82
+ buttons.forEach(function (button) { button.disabled = true; });
83
+ if (form.matches('form[action="/cancel"]')) {
84
+ try {
85
+ var canceled = await fetch(form.action, {
86
+ method: "POST",
87
+ headers: { "X-Classroom-Progress": "1" },
88
+ body: new URLSearchParams(new FormData(form)),
89
+ });
90
+ if (!canceled.ok) throw new Error("cancel failed");
91
+ closeInstallDialog(form);
92
+ } catch (error) {
93
+ status.textContent = "取消失敗,請再試一次。";
94
+ buttons.forEach(function (button) { button.disabled = false; });
95
+ installBusy = false;
96
+ }
97
+ return;
98
+ }
99
+ var abortForm = panel.querySelector("form[data-abort-install]");
100
+ if (abortForm) {
101
+ panel.querySelector('form[action="/confirm"]')?.setAttribute("data-reserved", "");
102
+ var cancelForm = panel.querySelector('form[action="/cancel"]');
103
+ if (cancelForm) cancelForm.hidden = true;
104
+ abortForm.hidden = false;
105
+ abortForm.querySelector("button").disabled = false;
106
+ }
107
+ panel.setAttribute("aria-busy", "true");
108
+ title.textContent = "安裝中";
109
+ status.textContent = "正在執行,請稍候…";
110
+ output.hidden = false;
111
+ var finished = false;
112
+ var timer;
113
+ async function update() {
114
+ try {
115
+ var response = await fetch("/command-status", { cache: "no-store" });
116
+ if (!response.ok) throw new Error("status unavailable");
117
+ var progress = await response.json();
118
+ if (finished) return;
119
+ var atBottom = output.scrollTop + output.clientHeight >= output.scrollHeight - 24;
120
+ output.textContent = progress.output || "等待安裝程式輸出…";
121
+ if (atBottom) output.scrollTop = output.scrollHeight;
122
+ status.textContent = "正在執行,請稍候…";
123
+ } catch (error) {
124
+ if (!finished) status.textContent = "暫時無法更新進度,正在重新連線…";
125
+ }
126
+ if (!finished) timer = setTimeout(update, 500);
127
+ }
128
+ try {
129
+ var request = fetch(form.action, {
130
+ method: "POST",
131
+ headers: { "X-Classroom-Progress": "1" },
132
+ body: new URLSearchParams(new FormData(form)),
133
+ });
134
+ update();
135
+ var result = await request;
136
+ if (!result.ok) throw new Error("confirmation failed");
137
+ var outcome = await result.json();
138
+ if (outcome.succeeded && Array.isArray(outcome.installedActionIds)) {
139
+ document.querySelectorAll('form[action="/prepare"] input[name="action_id"]').forEach(function (input) {
140
+ if (!outcome.installedActionIds.includes(input.value)) return;
141
+ var card = input.closest(".install-card");
142
+ if (!card || card.classList.contains("installed")) return;
143
+ card.classList.add("installed");
144
+ var button = card.querySelector('form[action="/prepare"] button');
145
+ button.classList.add("quiet");
146
+ button.textContent = "重新安裝";
147
+ });
148
+ }
149
+ finished = true;
150
+ clearTimeout(timer);
151
+ if (abortForm) abortForm.hidden = true;
152
+ panel.setAttribute("aria-busy", "false");
153
+ title.textContent = outcome.aborted ? "已中止" : outcome.succeeded ? "安裝成功" : "安裝失敗";
154
+ status.textContent = "請查看執行結果,按下確認後關閉。";
155
+ output.textContent = outcome.output || (outcome.succeeded ? "安裝完成。" : "指令執行失敗。");
156
+ choices.hidden = true;
157
+ dismiss.disabled = false;
158
+ dismiss.hidden = false;
159
+ } catch (error) {
160
+ finished = true;
161
+ clearTimeout(timer);
162
+ if (abortForm) abortForm.hidden = true;
163
+ panel.setAttribute("aria-busy", "false");
164
+ title.textContent = "無法確認安裝結果";
165
+ status.textContent = "請重新整理頁面查看結果,避免重複安裝。";
166
+ choices.hidden = true;
167
+ dismiss.disabled = false;
168
+ dismiss.hidden = false;
169
+ }
170
+ });
171
+ `;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installations = installations;
7
+ exports.registerInstallation = registerInstallation;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_sea_1 = require("node:sea");
10
+ const promises_1 = __importDefault(require("node:fs/promises"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const instance_1 = require("./instance");
13
+ const manifestFile = () => node_path_1.default.join((0, instance_1.managementDirectory)(), "installations.json");
14
+ async function installations() {
15
+ try {
16
+ const value = JSON.parse(await promises_1.default.readFile(manifestFile(), "utf8"));
17
+ if (!Array.isArray(value) || value.some(item => !item || (item.kind !== "exe" && item.kind !== "npm")))
18
+ throw new Error("VansPod 安裝紀錄無效,請先修復再移除。");
19
+ return value;
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT")
23
+ return [];
24
+ throw error;
25
+ }
26
+ }
27
+ async function registerInstallation() {
28
+ if (await (0, instance_1.removalInProgress)())
29
+ throw new Error("VansPod 完整移除中,請稍後重試。");
30
+ let installation;
31
+ if ((0, node_sea_1.isSea)())
32
+ installation = { kind: "exe", file: process.execPath, sha256: (0, node_crypto_1.createHash)("sha256").update(await promises_1.default.readFile(process.execPath)).digest("hex") };
33
+ else {
34
+ const root = node_path_1.default.resolve(__dirname, "..");
35
+ if (node_path_1.default.basename(root).toLowerCase() === "vanspod" && node_path_1.default.basename(node_path_1.default.dirname(root)) === "node_modules") {
36
+ const npmCli = process.env.npm_execpath && node_path_1.default.basename(process.env.npm_execpath) === "npm-cli.js"
37
+ ? process.env.npm_execpath : node_path_1.default.join(node_path_1.default.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
38
+ if (await promises_1.default.stat(npmCli).catch(() => undefined))
39
+ installation = { kind: "npm", root, prefix: node_path_1.default.dirname(node_path_1.default.dirname(root)), node: process.execPath, npmCli };
40
+ }
41
+ }
42
+ if (!installation)
43
+ return;
44
+ await promises_1.default.mkdir((0, instance_1.managementDirectory)(), { recursive: true });
45
+ const lock = node_path_1.default.join((0, instance_1.managementDirectory)(), "installations.lock");
46
+ let acquired = false;
47
+ for (let attempt = 0; attempt < 100; attempt++) {
48
+ try {
49
+ await promises_1.default.mkdir(lock);
50
+ acquired = true;
51
+ break;
52
+ }
53
+ catch (error) {
54
+ if (error.code !== "EEXIST")
55
+ throw error;
56
+ }
57
+ await new Promise(resolve => setTimeout(resolve, 50));
58
+ }
59
+ if (!acquired)
60
+ throw new Error("安裝紀錄正在更新,請稍後重試。");
61
+ try {
62
+ const previous = await installations();
63
+ const location = (item) => (item.kind === "exe" ? item.file : item.root).toLowerCase();
64
+ const updated = [...previous.filter(item => location(item) !== location(installation)), installation];
65
+ const temporary = `${manifestFile()}.tmp`;
66
+ await promises_1.default.writeFile(temporary, JSON.stringify(updated), { mode: 0o600 });
67
+ await promises_1.default.rename(temporary, manifestFile());
68
+ }
69
+ finally {
70
+ await promises_1.default.rmdir(lock);
71
+ }
72
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.managementDirectory = exports.dataDirectory = void 0;
7
+ exports.readInstance = readInstance;
8
+ exports.isAlive = isAlive;
9
+ exports.removalInProgress = removalInProgress;
10
+ exports.saveInstance = saveInstance;
11
+ exports.releaseInstance = releaseInstance;
12
+ exports.reserveInstance = reserveInstance;
13
+ exports.control = control;
14
+ const node_crypto_1 = require("node:crypto");
15
+ const promises_1 = __importDefault(require("node:fs/promises"));
16
+ const node_os_1 = __importDefault(require("node:os"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const version_1 = require("./version");
19
+ const dataDirectory = () => node_path_1.default.join(node_os_1.default.homedir(), ".vanspod");
20
+ exports.dataDirectory = dataDirectory;
21
+ const managementDirectory = () => node_path_1.default.join(process.env.LOCALAPPDATA || node_os_1.default.homedir(), "VansPod");
22
+ exports.managementDirectory = managementDirectory;
23
+ const lockDirectory = () => node_path_1.default.join((0, exports.managementDirectory)(), "runtime");
24
+ const stateFile = () => node_path_1.default.join(lockDirectory(), "instance.json");
25
+ async function readInstance() {
26
+ try {
27
+ const value = JSON.parse(await promises_1.default.readFile(stateFile(), "utf8"));
28
+ if (!Number.isSafeInteger(value.pid) || value.pid < 1 || !/^[a-f0-9]{64}$/.test(value.token) || !/^[a-f0-9]{32}$/.test(value.id) || typeof value.version !== "string" || (value.origin !== undefined && !/^http:\/\/127\.0\.0\.1:\d+$/.test(value.origin)))
29
+ throw new Error("VansPod 執行狀態格式無效。");
30
+ return value;
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT")
34
+ return undefined;
35
+ throw error;
36
+ }
37
+ }
38
+ function isAlive(pid) {
39
+ try {
40
+ process.kill(pid, 0);
41
+ return true;
42
+ }
43
+ catch (error) {
44
+ return error.code === "EPERM";
45
+ }
46
+ }
47
+ async function removalInProgress() {
48
+ try {
49
+ const value = JSON.parse(await promises_1.default.readFile(node_path_1.default.join((0, exports.managementDirectory)(), "uninstall-ready"), "utf8"));
50
+ return !Number.isSafeInteger(value.pid) || isAlive(value.pid);
51
+ }
52
+ catch (error) {
53
+ return error.code !== "ENOENT";
54
+ }
55
+ }
56
+ async function saveInstance(instance) {
57
+ const temporary = `${stateFile()}.${instance.id}.${process.pid}.tmp`;
58
+ await promises_1.default.writeFile(temporary, JSON.stringify(instance), { mode: 0o600 });
59
+ for (let attempt = 0;; attempt++) {
60
+ try {
61
+ await promises_1.default.rename(temporary, stateFile());
62
+ return;
63
+ }
64
+ catch (error) {
65
+ if (attempt >= 40 || !["EPERM", "EBUSY"].includes(error.code ?? ""))
66
+ throw error;
67
+ // Windows may briefly hold the old record while a launcher reads it.
68
+ await new Promise(resolve => setTimeout(resolve, 50));
69
+ }
70
+ }
71
+ }
72
+ async function releaseInstance(id) {
73
+ if ((await readInstance())?.id !== id)
74
+ return;
75
+ const retired = node_path_1.default.join(lockDirectory(), `retired-${id}.json`);
76
+ try {
77
+ await promises_1.default.rename(stateFile(), retired);
78
+ }
79
+ catch (error) {
80
+ if (error.code === "ENOENT")
81
+ return;
82
+ throw error;
83
+ }
84
+ const actual = JSON.parse(await promises_1.default.readFile(retired, "utf8"));
85
+ if (actual.id !== id) {
86
+ await promises_1.default.rename(retired, stateFile());
87
+ return;
88
+ }
89
+ await promises_1.default.rm(retired);
90
+ await promises_1.default.rmdir(lockDirectory()).catch(error => { if (!['ENOENT', 'ENOTEMPTY'].includes(error.code ?? ""))
91
+ throw error; });
92
+ }
93
+ async function reserveInstance() {
94
+ await promises_1.default.mkdir((0, exports.managementDirectory)(), { recursive: true, mode: 0o700 });
95
+ for (let attempt = 0; attempt < 200; attempt++) {
96
+ if (await removalInProgress())
97
+ throw new Error("VansPod 完整移除中,請等待移除結果。");
98
+ try {
99
+ await promises_1.default.mkdir(lockDirectory(), { mode: 0o700 });
100
+ if (await removalInProgress()) {
101
+ await promises_1.default.rmdir(lockDirectory());
102
+ throw new Error("VansPod 完整移除中,請等待移除結果。");
103
+ }
104
+ const instance = { pid: process.pid, id: (0, node_crypto_1.randomBytes)(16).toString("hex"), token: (0, node_crypto_1.randomBytes)(32).toString("hex"), version: version_1.VERSION };
105
+ await saveInstance(instance);
106
+ return { instance, owner: true };
107
+ }
108
+ catch (error) {
109
+ if (error.code !== "EEXIST")
110
+ throw error;
111
+ }
112
+ const existing = await readInstance();
113
+ if (!existing) {
114
+ const files = await promises_1.default.readdir(lockDirectory()).catch(() => []);
115
+ let removedRetired = false;
116
+ for (const file of files.filter(name => /^retired-[a-f0-9]{32}\.json$/.test(name))) {
117
+ const retired = node_path_1.default.join(lockDirectory(), file);
118
+ const value = await promises_1.default.readFile(retired, "utf8").then(raw => JSON.parse(raw)).catch(() => undefined);
119
+ if (value && file === `retired-${value.id}.json` && !isAlive(value.pid)) {
120
+ await promises_1.default.rm(retired, { force: true });
121
+ removedRetired = true;
122
+ }
123
+ }
124
+ for (const file of files) {
125
+ const temporary = /^instance\.json\.[a-f0-9]{32}\.(\d+)\.tmp$/.exec(file);
126
+ if (temporary && !isAlive(Number(temporary[1]))) {
127
+ await promises_1.default.rm(node_path_1.default.join(lockDirectory(), file), { force: true });
128
+ removedRetired = true;
129
+ }
130
+ }
131
+ const state = await promises_1.default.stat(lockDirectory()).catch(() => undefined);
132
+ if (state && (removedRetired || Date.now() - state.mtimeMs > 30000))
133
+ await promises_1.default.rmdir(lockDirectory()).catch(() => undefined);
134
+ }
135
+ if (existing && !isAlive(existing.pid)) {
136
+ await releaseInstance(existing.id).catch(() => undefined);
137
+ }
138
+ else if (existing?.origin) {
139
+ return { instance: existing, owner: false };
140
+ }
141
+ await new Promise(resolve => setTimeout(resolve, 100));
142
+ }
143
+ throw new Error("VansPod 正在啟動或未回應,請稍後重試。");
144
+ }
145
+ async function control(instance, action, body) {
146
+ if (!instance.origin)
147
+ throw new Error("VansPod 尚未完成啟動。");
148
+ const response = await fetch(`${instance.origin}/_vanspod/${action}`, {
149
+ method: body === undefined ? "GET" : "POST",
150
+ headers: { Authorization: `Bearer ${instance.token}`, "Content-Type": "application/json" },
151
+ body: body === undefined ? undefined : JSON.stringify(body),
152
+ signal: AbortSignal.timeout(20000),
153
+ });
154
+ if (!response.ok)
155
+ throw new Error("VansPod 管理操作授權失敗,請確認本機執行紀錄。");
156
+ const result = await response.json();
157
+ if (result.id !== instance.id)
158
+ throw new Error("VansPod 實例身分驗證失敗。");
159
+ return result;
160
+ }
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runCli = runCli;
7
+ const version_1 = require("./version");
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_sea_1 = require("node:sea");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const instance_1 = require("./instance");
12
+ const promises_1 = __importDefault(require("node:fs/promises"));
13
+ const desktop_1 = require("./desktop");
14
+ const installations_1 = require("./installations");
15
+ const promises_2 = require("node:readline/promises");
16
+ async function runCli(args = process.argv.slice(2)) {
17
+ if (args[0] === "--version") {
18
+ process.stdout.write(`${version_1.VERSION}\n`);
19
+ return;
20
+ }
21
+ if (args[0] === "--help") {
22
+ process.stdout.write("VansPod\nvanspod [start | status | stop | uninstall]\nvanspod --version\nvanspod --help\n");
23
+ return;
24
+ }
25
+ const command = args[0] ?? "start";
26
+ if (!["start", "status", "stop", "uninstall"].includes(command) || args.slice(1).some(arg => arg !== "--json"))
27
+ throw new Error("指令無效,請執行 vanspod --help。");
28
+ if (process.platform !== "win32")
29
+ throw new Error("此版本完整支援 Windows,其他平台尚未支援。");
30
+ if (process.env.VANSPOD_WORKER_TOKEN) {
31
+ let instance = await (0, instance_1.readInstance)();
32
+ if (!instance || instance.token !== process.env.VANSPOD_WORKER_TOKEN)
33
+ throw new Error("VansPod 啟動授權無效。");
34
+ for (let attempt = 0; instance.pid !== process.pid && attempt < 200; attempt++) {
35
+ await new Promise(resolve => setTimeout(resolve, 50));
36
+ const current = await (0, instance_1.readInstance)();
37
+ if (!current || current.id !== instance.id)
38
+ throw new Error("VansPod 啟動身分已改變。");
39
+ instance = current;
40
+ }
41
+ if (instance.pid !== process.pid)
42
+ throw new Error("VansPod 啟動交接逾時。");
43
+ await require("./start").main(instance);
44
+ return;
45
+ }
46
+ if (command === "status") {
47
+ const instance = await (0, instance_1.readInstance)();
48
+ const status = instance && (0, instance_1.isAlive)(instance.pid) ? await (0, instance_1.control)(instance, "status") : { id: "", running: false, pid: undefined, origin: undefined, version: undefined };
49
+ process.stdout.write(args.includes("--json") ? `${JSON.stringify(status)}\n` : status.running ? `VansPod ${status.version} 執行中(PID ${status.pid})\n${status.origin}/\n` : "VansPod 未執行。\n");
50
+ return;
51
+ }
52
+ if (command === "stop") {
53
+ const instance = await (0, instance_1.readInstance)();
54
+ if (!instance || !(0, instance_1.isAlive)(instance.pid)) {
55
+ process.stdout.write("VansPod 未執行。\n");
56
+ return;
57
+ }
58
+ let result = await (0, instance_1.control)(instance, "stop", {});
59
+ if (result.confirmation && await confirm(result.message ?? "確認中止並結束 VansPod?"))
60
+ result = await (0, instance_1.control)(instance, "stop", { confirmed: true });
61
+ if (!result.ok)
62
+ throw new Error(result.message ?? "無法結束 VansPod。");
63
+ for (let attempt = 0; attempt < 200 && (0, instance_1.isAlive)(instance.pid); attempt++)
64
+ await new Promise(resolve => setTimeout(resolve, 50));
65
+ if ((0, instance_1.isAlive)(instance.pid))
66
+ throw new Error("VansPod 尚未完成結束。");
67
+ process.stdout.write("VansPod 已結束。\n");
68
+ return;
69
+ }
70
+ if (command === "uninstall") {
71
+ if (await (0, instance_1.removalInProgress)())
72
+ throw new Error("VansPod 完整移除中,請等待移除結果。");
73
+ await (0, installations_1.registerInstallation)();
74
+ let instance = await (0, instance_1.readInstance)();
75
+ if (!instance || !(0, instance_1.isAlive)(instance.pid)) {
76
+ await runCli(["start"]);
77
+ instance = await (0, instance_1.readInstance)();
78
+ }
79
+ if (!instance)
80
+ throw new Error("無法啟動完整移除工具。");
81
+ const preview = await (0, instance_1.control)(instance, "uninstall-preview");
82
+ if (!preview.ok)
83
+ throw new Error(preview.message);
84
+ process.stdout.write(`${preview.message}\n`);
85
+ if (!await confirm("確定完整移除 VansPod?"))
86
+ throw new Error("已取消移除。請在互動終端機或 VansPod 頁面確認移除。");
87
+ const body = { confirmed: true, waitForPid: (0, node_sea_1.isSea)() ? process.pid : undefined };
88
+ let result = await (0, instance_1.control)(instance, "uninstall", body);
89
+ if (result.confirmation && await confirm(result.message ?? "確認中止並移除 VansPod?"))
90
+ result = await (0, instance_1.control)(instance, "uninstall", { ...body, interruptConfirmed: true });
91
+ if (!result.ok)
92
+ throw new Error(result.message);
93
+ process.stdout.write(`${result.message}\n`);
94
+ return;
95
+ }
96
+ await (0, installations_1.registerInstallation)();
97
+ const { instance, owner } = await (0, instance_1.reserveInstance)();
98
+ if (owner) {
99
+ await promises_1.default.rm(node_path_1.default.join((0, instance_1.managementDirectory)(), "startup-error.json"), { force: true });
100
+ if (process.env.CLASSROOM_APP_FOREGROUND === "1") {
101
+ await require("./start").main(instance);
102
+ return;
103
+ }
104
+ const child = (0, node_child_process_1.spawn)(process.execPath, (0, node_sea_1.isSea)() ? [] : [node_path_1.default.join(__dirname, "entry.js")], {
105
+ detached: true, stdio: "ignore", windowsHide: true,
106
+ env: { ...process.env, VANSPOD_WORKER_TOKEN: instance.token },
107
+ });
108
+ await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); }).catch(async (error) => { await (0, instance_1.releaseInstance)(instance.id); throw error; });
109
+ instance.pid = child.pid;
110
+ await (0, instance_1.saveInstance)(instance);
111
+ child.unref();
112
+ }
113
+ let ready;
114
+ for (let attempt = 0; attempt < 200; attempt++) {
115
+ const current = await (0, instance_1.readInstance)();
116
+ if (!current || !(0, instance_1.isAlive)(current.pid)) {
117
+ const failure = await promises_1.default.readFile(node_path_1.default.join((0, instance_1.managementDirectory)(), "startup-error.json"), "utf8").then(raw => JSON.parse(raw).message).catch(() => "請檢查本機環境與連接埠後重試。");
118
+ throw new Error(`VansPod 啟動失敗:${failure}`);
119
+ }
120
+ if (current.origin) {
121
+ const status = await (0, instance_1.control)(current, "status").catch(async (error) => {
122
+ const failure = await promises_1.default.readFile(node_path_1.default.join((0, instance_1.managementDirectory)(), "startup-error.json"), "utf8").then(raw => JSON.parse(raw).message).catch(() => undefined);
123
+ if (failure)
124
+ throw new Error(failure);
125
+ throw error;
126
+ });
127
+ if (status.startupError)
128
+ throw new Error(status.startupError);
129
+ if (status.ready !== false) {
130
+ ready = status;
131
+ break;
132
+ }
133
+ }
134
+ await new Promise(resolve => setTimeout(resolve, 100));
135
+ }
136
+ if (!ready)
137
+ throw new Error("VansPod 啟動逾時,請稍後用 vanspod status 檢查。");
138
+ const live = (await (0, instance_1.readInstance)());
139
+ await (0, instance_1.control)(live, "open", { version: version_1.VERSION });
140
+ (0, desktop_1.openBrowser)(`${ready.origin}/`);
141
+ process.stdout.write(`VansPod ${ready.version}: ${ready.origin}/\n`);
142
+ if (ready.version !== version_1.VERSION)
143
+ process.stdout.write(`本次開啟版本 ${version_1.VERSION},目前執行版本 ${ready.version};換版請先結束 VansPod。\n`);
144
+ }
145
+ async function confirm(message) {
146
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
147
+ return false;
148
+ const input = (0, promises_2.createInterface)({ input: process.stdin, output: process.stdout });
149
+ try {
150
+ return /^(y|yes|是)$/i.test((await input.question(`${message} [y/N] `)).trim());
151
+ }
152
+ finally {
153
+ input.close();
154
+ }
155
+ }